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

如何正确合并AVAssetWriter生成的MP4分段文件以避免音频剪辑/卡顿问题

如何正确合并AVAssetWriter生成的MP4分段文件以避免音频剪辑/卡顿问题

我明白你现在遇到的麻烦——用AVAssetWriter生成的MP4分段文件,合并后时不时出现音频剪辑、卡顿的问题,还发现不同分段的timescale时而是600(视频常用基准)时而是44100(音频采样率),这确实挺让人挠头的。咱们来一步步拆解问题,找到靠谱的解决办法。

先说说问题根源

你遇到的卡顿/剪辑问题,核心出在两个地方:

  1. 分段时间基准(Timescale)不统一:视频轨道常用600作为timescale,音频轨道则用采样率(比如44100),直接合并时如果没统一时间基准,轨道时间轴就会对齐出错,导致音频卡顿。
  2. 合并时的轨道同步处理不到位:你现在只是基于AVAsset的duration来拼接,但AVAsset的duration是取轨道中最长的那个,并没有处理视频和音频轨道各自的时间同步细节。

接下来给你具体的解决步骤,都是可以直接落地的:

一、统一分段的时间基准,精准处理轨道同步

合并时不要依赖每个分段自身的timescale,而是把所有时间转换到一个全局统一的基准(比如1000,毫秒级精度完全足够),同时针对视频、音频轨道分别处理时间范围插入,确保轨道同步。

修改你的_run()函数:

private func _run() async throws -> AVComposition {
    let segments = try getSegmentURLs(from: segmentsDirectory, order: .forward)
    guard segments.count > 0 else { throw SegmentComposerError.noSegments }
    
    let composition = AVMutableComposition()
    // 全局统一timescale,避免不同轨道基准冲突
    let targetTimescale: CMTimeScale = 1000
    var cursorTime: CMTime = .zero
    
    let options: [String: Any] = [
        AVURLAssetPreferPreciseDurationAndTimingKey: true,
        AVURLAssetReferenceRestrictionsKey: AVURLAssetReferenceRestrictions.referenceRestrictionForbidNone.rawValue
    ]
    
    for segmentURL in segments {
        let asset = AVURLAsset(url: segmentURL, options: options)
        // 加载所有视频、音频轨道,确保不遗漏
        let videoTracks = try await asset.loadTracks(withMediaType: .video)
        let audioTracks = try await asset.loadTracks(withMediaType: .audio)
        let allTracks = videoTracks + audioTracks
        guard !allTracks.isEmpty else { continue }
        
        // 转换分段时长到全局基准
        let assetDuration = try await asset.load(.duration)
        let normalizedDuration = CMTimeConvertScale(assetDuration, targetTimescale, .roundHalfAwayFromZero)
        
        // 为每个轨道单独处理插入逻辑,确保同步
        for track in allTracks {
            let trackType = track.mediaType
            // 复用已有的合成轨道,或者新建
            let compositionTrack = composition.tracks(withMediaType: trackType).first ?? 
                                  composition.addMutableTrack(withMediaType: trackType, preferredTrackID: kCMPersistentTrackID_Invalid)!
            
            // 转换轨道时间范围到全局基准
            let trackTimeRange = try await track.load(.timeRange)
            let normalizedStart = CMTimeConvertScale(trackTimeRange.start, targetTimescale, .roundHalfAwayFromZero)
            let normalizedTrackDuration = CMTimeConvertScale(trackTimeRange.duration, targetTimescale, .roundHalfAwayFromZero)
            let normalizedTrackRange = CMTimeRange(start: normalizedStart, duration: normalizedTrackDuration)
            
            // 插入到合成轨道的对应位置
            try compositionTrack.insertTimeRange(normalizedTrackRange, of: track, at: cursorTime)
        }
        
        cursorTime = CMTimeAdd(cursorTime, normalizedDuration)
        print(">>> 分段 \(segmentURL.lastPathComponent) - 标准化时长: \(normalizedDuration)")
    }
    
    return composition
}

二、优化录制时的分段生成逻辑

你当前的分段生成代码里,用queue.sync处理回调可能会阻塞AVAssetWriter的录制线程,导致分段数据的时间戳不连续,这也是卡顿的潜在诱因。另外,建议在创建AVAssetWriter时,统一视频、音频输入的timescale,从源头减少基准不一致的问题。

1. 替换同步回调为异步

nonisolated func assetWriter(
    _ writer: AVAssetWriter,
    didOutputSegmentData segmentData: Data,
    segmentType: AVAssetSegmentType
) {
    // 用async替代sync,避免阻塞录制线程
    queue.async {
        assumeIsolated { $0.onNewSegment(segmentData: segmentData, segmentType: segmentType) }
    }
}

2. 统一录制时的时间基准

在创建AVAssetWriter的输入配置时,把视频和音频的timescale设为同一个值(比如600):

func createWriter(delegate: AVAssetWriterDelegate) -> AVAssetWriter {
    let tempURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("temp.mp4")
    let writer = try! AVAssetWriter(outputURL: tempURL, fileType: .mp4)
    
    // 视频输入配置
    let videoSettings: [String: Any] = [
        AVVideoCodecKey: AVVideoCodecType.h264,
        AVVideoWidthKey: 1920,
        AVVideoHeightKey: 1080,
        AVVideoCompressionPropertiesKey: [
            AVVideoAverageBitRateKey: 5000000,
            AVVideoMaxKeyFrameIntervalKey: 3600 // 和6秒分段对应(600*6=3600)
        ]
    ]
    let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings)
    videoInput.expectsMediaDataInRealTime = true
    // 强制视频input用600作为timescale
    if var formatDesc = videoInput.sourceFormatHint?.mutableCopy() as? CMFormatDescription {
        CMFormatDescriptionSetMediaTimeScale(&formatDesc, 600)
        videoInput.sourceFormatHint = formatDesc
    }
    writer.add(videoInput)
    
    // 音频输入配置
    let audioSettings: [String: Any] = [
        AVFormatIDKey: kAudioFormatMPEG4AAC,
        AVSampleRateKey: 44100,
        AVNumberOfChannelsKey: 2,
        AVEncoderBitRateKey: 128000
    ]
    let audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioSettings)
    audioInput.expectsMediaDataInRealTime = true
    // 音频input也统一用600作为timescale
    if var formatDesc = audioInput.sourceFormatHint?.mutableCopy() as? CMFormatDescription {
        CMFormatDescriptionSetMediaTimeScale(&formatDesc, 600)
        audioInput.sourceFormatHint = formatDesc
    }
    writer.add(audioInput)
    
    writer.delegate = delegate
    writer.shouldOptimizeForNetworkUse = true
    writer.startWriting()
    writer.startSession(atSourceTime: .zero)
    
    return writer
}

三、导出合成文件时的注意事项

导出AVComposition时,尽量避免不必要的重新编码,确保轨道同步:

func exportComposition(_ composition: AVComposition, to outputURL: URL) async throws {
    let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality)!
    exporter.outputURL = outputURL
    exporter.outputFileType = .mp4
    exporter.shouldOptimizeForNetworkUse = false
    // 复用原合成的视频、音频配置,避免重新编码
    exporter.videoComposition = AVMutableVideoComposition(propertiesOf: composition)
    exporter.audioMix = AVMutableAudioMix(propertiesOf: composition)
    
    let semaphore = DispatchSemaphore(value: 0)
    var exportError: Error?
    
    exporter.exportAsynchronously {
        switch exporter.status {
        case .completed:
            semaphore.signal()
        case .failed, .cancelled:
            exportError = exporter.error ?? NSError(domain: "ExportError", code: -1, userInfo: [NSLocalizedDescriptionKey: "导出失败或取消"])
            semaphore.signal()
        default: break
        }
    }
    
    semaphore.wait()
    if let error = exportError {
        throw error
    }
}

小技巧:验证分段时间连续性

你可以在生成分段时,记录每个分段的实际持续时间(从AVAssetWriter的会话中获取),并存成一个简单的元数据文件(比如JSON)。合并时直接用这些预存的时间数据,比依赖AVAsset加载的duration更准确,能进一步降低同步误差。

总结一下:核心就是统一时间基准+单独处理轨道同步,再配合录制时的线程优化,就能彻底解决音频卡顿问题。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.07 13:08:11