如何正确合并AVAssetWriter生成的MP4分段文件以避免音频剪辑/卡顿问题
我明白你现在遇到的麻烦——用AVAssetWriter生成的MP4分段文件,合并后时不时出现音频剪辑、卡顿的问题,还发现不同分段的timescale时而是600(视频常用基准)时而是44100(音频采样率),这确实挺让人挠头的。咱们来一步步拆解问题,找到靠谱的解决办法。
先说说问题根源
你遇到的卡顿/剪辑问题,核心出在两个地方:
- 分段时间基准(Timescale)不统一:视频轨道常用600作为timescale,音频轨道则用采样率(比如44100),直接合并时如果没统一时间基准,轨道时间轴就会对齐出错,导致音频卡顿。
- 合并时的轨道同步处理不到位:你现在只是基于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

