You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift中AVAssetWriter输出视频时长与实际录制不同步问题

问题原因与解决方案

核心原因分析

  1. 时间基准错位:你当前以第一个到达的媒体样本(音频或视频)作为Session起始时间,但音频和视频的时间戳来自不同捕获源,存在微小偏移;且视频设置了mediaTimeScale=600,音频未显式设置(默认用采样率44100),不同时间尺度的转换会引入精度误差。
  2. AAC音频帧的补全特性:AAC编码的音频帧固定包含1024个采样点(44100Hz下单帧时长≈0.0232秒),录制结束时AVAssetWriter会自动补全最后一帧不完整的音频数据,导致音频轨道时长略长于视频轨道,而MP4总时长取最长轨道的时长。
  3. 未同步停止逻辑:如果停止录制时未显式对齐音视频的结束时间,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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 18:55:31