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

如何在AVFoundation合并视频时为单个视频设置独立变换?

问题分析与解决方案

核心问题点

  1. 音频轨道错误插入视频轨道:你把音频片段插入到了视频轨道中,这会导致轨道数据混乱,甚至导出失败。必须为音频单独创建composition轨道。
  2. 全局变换覆盖单个片段旋转:直接修改videoCompositionTrack.preferredTransform会对整个轨道生效,无法为不同旋转状态的视频片段单独设置正确方向。正确做法是通过AVVideoComposition和AVVideoCompositionLayerInstruction为每个片段单独应用变换。
  3. 未固定输出竖屏尺寸:没有指定导出视频的目标尺寸(9:16),导致导出时自动适配轨道自然尺寸,出现比例拉伸或方向错误。
  4. 异步轨道获取的时序问题: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
            }
        })
    }
}

关键修正说明

  1. 分离音视频轨道:单独创建音频合成轨道,将音频片段插入到正确轨道中,避免数据混乱。
  2. 层指令处理单个片段旋转:通过AVVideoCompositionLayerInstruction为每个视频片段单独设置变换,确保不同旋转状态的视频都能正确显示,不会覆盖全局轨道设置。
  3. 固定输出尺寸:指定videoComp.renderSize为9:16竖屏尺寸,确保导出视频始终保持目标比例,避免拉伸变形。
  4. 异步任务同步处理:用DispatchGroup等待所有视频片段的插入和指令创建完成后再执行导出,解决异步回调导致的时序错误。

内容的提问来源于stack exchange,提问作者faraaz

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 22:34:55