如何在AVFoundation合并视频时为单个视频设置独立变换?
问题分析与解决方案
核心问题点
- 音频轨道错误插入视频轨道:你把音频片段插入到了视频轨道中,这会导致轨道数据混乱,甚至导出失败。必须为音频单独创建composition轨道。
- 全局变换覆盖单个片段旋转:直接修改
videoCompositionTrack.preferredTransform会对整个轨道生效,无法为不同旋转状态的视频片段单独设置正确方向。正确做法是通过AVVideoComposition和AVVideoCompositionLayerInstruction为每个片段单独应用变换。 - 未固定输出竖屏尺寸:没有指定导出视频的目标尺寸(9:16),导致导出时自动适配轨道自然尺寸,出现比例拉伸或方向错误。
- 异步轨道获取的时序问题:
getTracks是异步方法,循环中lastTime的更新会因异步回调顺序混乱,导致片段插入位置错误。
修正后的代码
private static func mergeVideos( videoPaths: [URL], outputURL: URL, handler: @escaping (_ path: URL)-> Void ) { let videoComposition = AVMutableComposition() var lastTime: CMTime = .zero // 分别创建视频和音频轨道 guard let videoCompTrack = videoComposition.addMutableTrack(withMediaType: .video, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)), let audioCompTrack = videoComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) else { return } // 用DispatchGroup处理异步轨道获取,确保所有片段插入完成后再执行导出 let group = DispatchGroup() var videoLayerInstructions: [AVVideoCompositionLayerInstruction] = [] for path in videoPaths { group.enter() let asset = AVAsset(url: path) // 同步获取轨道(若原getTracks为异步,可在其回调内处理后续逻辑) let videoTracks = asset.tracks(withMediaType: .video) let audioTracks = asset.tracks(withMediaType: .audio) guard let sourceVideoTrack = videoTracks.first else { group.leave() continue } do { // 插入视频片段到合成轨道 let timeRange = CMTimeRange(start: .zero, duration: asset.duration) try videoCompTrack.insertTimeRange(timeRange, of: sourceVideoTrack, at: lastTime) // 创建当前视频片段的层指令,应用正确的旋转变换 let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoCompTrack) let transform = sourceVideoTrack.preferredTransform let naturalSize = sourceVideoTrack.naturalSize // 固定9:16竖屏输出尺寸,可根据需求调整 let renderSize = CGSize(width: 1080, height: 1920) var adjustedTransform = transform // 适配不同旋转方向的视频,确保内容正确填充竖屏 switch (transform.a, transform.b) { case (0, 1), (0, -1): // 90/270度旋转的竖屏视频 adjustedTransform = transform.concatenating(CGAffineTransform(translationX: naturalSize.height, y: 0)) if transform.b == -1 { adjustedTransform = adjustedTransform.concatenating(CGAffineTransform(translationX: 0, y: naturalSize.width)) } default: // 0/180度旋转的视频 break } layerInstruction.setTransform(adjustedTransform, at: lastTime) videoLayerInstructions.append(layerInstruction) // 插入音频片段(如果有) if let sourceAudioTrack = audioTracks.first { try audioCompTrack.insertTimeRange(timeRange, of: sourceAudioTrack, at: lastTime) } // 更新时间轴 lastTime = CMTimeAdd(lastTime, asset.duration) } catch { print("插入轨道失败: \(error.localizedDescription)") } group.leave() } // 等待所有片段处理完成后,创建视频合成配置 group.notify(queue: .main) { // 创建合成指令,设置层指令和时间范围 let mainInstruction = AVMutableVideoCompositionInstruction() mainInstruction.timeRange = CMTimeRange(start: .zero, duration: lastTime) mainInstruction.layerInstructions = videoLayerInstructions let videoComp = AVMutableVideoComposition() videoComp.instructions = [mainInstruction] videoComp.renderSize = CGSize(width: 1080, height: 1920) // 固定9:16竖屏输出 videoComp.frameDuration = CMTimeMake(value: 1, timescale: 30) // 帧率可根据需求调整 // 初始化导出会话 guard let exporter = AVAssetExportSession(asset: videoComposition, presetName: AVAssetExportPresetHighestQuality) else { return } exporter.outputURL = outputURL exporter.outputFileType = .mp4 exporter.shouldOptimizeForNetworkUse = true exporter.videoComposition = videoComp // 绑定视频合成配置 exporter.exportAsynchronously(completionHandler: { switch exporter.status { case .failed: print("导出失败: \(exporter.error?.localizedDescription ?? "未知错误")") case .completed: print("导出完成") handler(outputURL) default: break } }) } }
关键修正说明
- 分离音视频轨道:单独创建音频合成轨道,将音频片段插入到正确轨道中,避免数据混乱。
- 层指令处理单个片段旋转:通过
AVVideoCompositionLayerInstruction为每个视频片段单独设置变换,确保不同旋转状态的视频都能正确显示,不会覆盖全局轨道设置。 - 固定输出尺寸:指定
videoComp.renderSize为9:16竖屏尺寸,确保导出视频始终保持目标比例,避免拉伸变形。 - 异步任务同步处理:用
DispatchGroup等待所有视频片段的插入和指令创建完成后再执行导出,解决异步回调导致的时序错误。
内容的提问来源于stack exchange,提问作者faraaz
相关产品推荐
相关产品推荐

