Swift中AVAssetWriter输出视频时长与实际录制不同步问题
问题原因与解决方案
核心原因分析
- 时间基准错位:你当前以第一个到达的媒体样本(音频或视频)作为Session起始时间,但音频和视频的时间戳来自不同捕获源,存在微小偏移;且视频设置了
mediaTimeScale=600,音频未显式设置(默认用采样率44100),不同时间尺度的转换会引入精度误差。 - AAC音频帧的补全特性:AAC编码的音频帧固定包含1024个采样点(44100Hz下单帧时长≈0.0232秒),录制结束时AVAssetWriter会自动补全最后一帧不完整的音频数据,导致音频轨道时长略长于视频轨道,而MP4总时长取最长轨道的时长。
- 未同步停止逻辑:如果停止录制时未显式对齐音视频的结束时间,AVAssetWriter会以最后写入的样本时间戳作为结束点,若音频最后一个样本晚于视频,就会拉长总时长。
具体解决方案
1. 强制以视频时间戳作为Session基准
把视频的第一个样本时间戳设为Session起始点,同时调整音频时间戳对齐该基准:
// 新增变量记录视频最后一帧时间戳 private var lastVideoPresentationTime: CMTime? public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { guard CMSampleBufferDataIsReady(sampleBuffer) else { return } if sessionAtSourceTime == nil { // 仅以视频的第一个样本作为Session起始 guard output is AVCaptureVideoDataOutput else { return } sessionAtSourceTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) videoWriter.startSession(atSourceTime: sessionAtSourceTime!) } if output is AVCaptureVideoDataOutput && videoWriterInput.isReadyForMoreMediaData { let presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) lastVideoPresentationTime = presentationTime videoWriterInput.append(sampleBuffer) } else if output is AVCaptureAudioDataOutput && audioWriterInput.isReadyForMoreMediaData { // 调整音频时间戳,对齐视频起始时间 var adjustedBuffer = sampleBuffer let audioTime = CMSampleBufferGetPresentationTimeStamp(adjustedBuffer) let offset = CMTimeSubtract(audioTime, sessionAtSourceTime!) let adjustedTime = CMTimeAdd(sessionAtSourceTime!, offset) CMSampleBufferSetPresentationTimeStamp(&adjustedBuffer, adjustedTime) audioWriterInput.append(adjustedBuffer) } }
2. 统一音视频的时间尺度
给音频输入设置和视频一致的mediaTimeScale,避免时间转换误差:
func setupWriter() { // ... 原有代码 ... // 配置音频输入时添加 audioWriterInput = AVAssetWriterInput(mediaType: AVMediaType.audio, outputSettings: [ AVFormatIDKey: kAudioFormatMPEG4AAC, AVNumberOfChannelsKey: 1, AVSampleRateKey: 44100, AVEncoderBitRateKey: 64000, ]) audioWriterInput.mediaTimeScale = CMTimeScale(bitPattern: 600) // 与视频统一 audioWriterInput.expectsMediaDataInRealTime = true // ... 原有代码 ... }
3. 同步停止录制,显式设置结束时间
停止时以视频最后一帧时间戳作为Session结束点,强制总时长匹配视频预期:
func stopRecording() { videoWriterInput.markAsFinished() audioWriterInput.markAsFinished() // 用视频最后一帧时间戳结束Session if let endTime = lastVideoPresentationTime { videoWriter.endSession(atSourceTime: endTime) } videoWriter.finishWriting { // 处理写入完成逻辑 } }
4. 后期裁剪(可选)
如果上述方法仍有偏差,可录制完成后用AVAssetExportSession将音频轨道裁剪至视频时长:
func trimAudioToVideo(videoURL: URL, outputURL: URL) { let asset = AVAsset(url: videoURL) guard let videoTrack = asset.tracks(withMediaType: .video).first else { return } let videoDuration = videoTrack.timeRange.duration let exporter = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetPassthrough)! exporter.outputURL = outputURL exporter.outputFileType = .mp4 exporter.timeRange = CMTimeRange(start: .zero, duration: videoDuration) exporter.exportAsynchronously { // 处理导出结果 } }
内容的提问来源于stack exchange,提问作者Doyel Joboy
相关产品推荐
相关产品推荐

