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

iOS多视频高效合并:AVFoundation替代方案及性能优化

iOS 高效合并多段视频的优化方案

我在iOS应用里用AVFoundation实现多段视频合并,但处理速度很慢,试过包括AVAssetExportPreset1280x720在内的多种预设都没改善。想知道有没有GPU加速的框架或技术能提升性能,以下是我当前的实现代码:

func mergeVideos(
    _ items: [VideoItem],
    presetName: String,
    overlay: Overlay?,
    completionHandler: @escaping (Result<URL, Error>) -> Void
) {
    let fileName = "video-\(Date().description)"
    let composition = AVMutableComposition()
    let allItemsAreMuted = items.allSatisfy(\.isMuted)
    guard
        let compVideoTrack = composition.addMutableTrack(
            withMediaType: .video,
            preferredTrackID: CMPersistentTrackID(kCMPersistentTrackID_Invalid)
        )
    else {
        return completionHandler(.failure(VideoEditorError.unknown))
    }
    var compAudioTrack: AVMutableCompositionTrack?
    if !allItemsAreMuted {
        compAudioTrack = composition.addMutableTrack(
            withMediaType: .audio,
            preferredTrackID: CMPersistentTrackID(kCMPersistentTrackID_Invalid)
        )
        if compAudioTrack == nil {
            return completionHandler(.failure(VideoEditorError.unknown))
        }
    }

    var currentTime = CMTime.zero
    var loadedVideoTracks = [AVAsset: AVAssetTrack]()
    var loadedAudioTracks = [AVAsset: AVAssetTrack]()
    items.forEach { item in
        do {
            var audioTrack: AVAssetTrack?
            let range = item.range ?? CMTimeRangeMake(start: CMTime.zero, duration: item.asset.duration)
            var scaledDuration: CMTime?
            if let rate = item.rate, rate != 1 {
                scaledDuration = CMTime(seconds: range.duration.seconds / Double(rate), preferredTimescale: 1000)
            }
            if let videoTrack = loadedVideoTracks[item.asset] ?? item.asset.tracks(withMediaType: .video).first {
                loadedVideoTracks[item.asset] = videoTrack
                try compVideoTrack.insertTimeRange(range, of: videoTrack, at: currentTime)
            }
            if !item.isMuted {
                audioTrack = loadedAudioTracks[item.asset] ?? item.asset.tracks(withMediaType: .audio).first
                if let audioTrack = audioTrack {
                    loadedAudioTracks[item.asset] = audioTrack
                    try compAudioTrack?.insertTimeRange(range, of: audioTrack, at: currentTime)
                }
            }
            if let scaledDuration = scaledDuration {
                let currentRange = CMTimeRangeMake(start: currentTime, duration: range.duration)
                compVideoTrack.scaleTimeRange(currentRange, toDuration: scaledDuration)
                compAudioTrack?.scaleTimeRange(currentRange, toDuration: scaledDuration)
            }
            currentTime = CMTimeAdd(currentTime, scaledDuration ?? range.duration)
        } catch {
            completionHandler(.failure(error))
            return
        }
    }

    let exportURL = URL(fileURLWithPath: NSTemporaryDirectory())
        .appendingPathComponent(fileName)
        .appendingPathExtension("mov")
    guard let export = AVAssetExportSession(
        asset: composition,
        presetName: presetName
    ) else {
        completionHandler(.failure(VideoEditorError.unknown))
        return
    }

    if let overlay = overlay {
        let videoInfo = orientation(from: loadedVideoTracks.first!.value.preferredTransform)
        let videoSize: CGSize
        if videoInfo.isPortrait {
            videoSize = CGSize(
                width: loadedVideoTracks.first!.value.naturalSize.height,
                height: loadedVideoTracks.first!.value.naturalSize.width
            )
        } else {
            videoSize = loadedVideoTracks.first!.value.naturalSize
        }

        let overlayLayer = CALayer()
        overlayLayer.contents = overlay.image.cgImage
        overlayLayer.frame = CGRect(x: 0, y: 0, width: videoSize.width, height: videoSize.height)
        overlayLayer.opacity = 1
        overlayLayer.masksToBounds = true
        overlayLayer.transform = CATransform3DMakeTranslation(overlay.rotationDegrees, 0, 0)

        let radians = CGFloat(overlay.rotationDegrees) * .pi / 180.0
        overlayLayer.transform = CATransform3DMakeRotation(radians, 1, 0, 0)

        let videoLayer = CALayer()
        videoLayer.frame = CGRect(x: 0, y: 0, width: videoSize.width, height: videoSize.height)

        let animationLayer = CALayer()
        animationLayer.frame = CGRect(x: 0, y: 0, width: videoSize.width, height: videoSize.height)
        animationLayer.addSublayer(videoLayer)
        animationLayer.addSublayer(overlayLayer)

        // Add the animation layer to the video composition
        let videoComposition = AVMutableVideoComposition(propertiesOf: composition)
        videoComposition.animationTool = AVVideoCompositionCoreAnimationTool(
            postProcessingAsVideoLayers: [videoLayer],
            in: animationLayer
        )
        export.videoComposition = videoComposition
    }

    export.outputURL = exportURL
    export.outputFileType = .mov
    export.shouldOptimizeForNetworkUse = true
    export.exportAsynchronously {
        DispatchQueue.main.async {
            switch export.status {
            case .completed:
                completionHandler(.success(exportURL))
            default:
                completionHandler(.failure(export.error ?? VideoEditorError.unknown))
            }
        }
    }
}

优化方案

1. 选择硬件加速友好的导出预设

  • 优先使用与源视频编码格式匹配的预设,比如源视频是H.264则选AVAssetExportPresetHighestQuality,HEVC则选AVAssetExportPresetHEVCHighestQuality,避免跨格式转码的性能开销。
  • 非必要不使用非匹配分辨率的预设,强制分辨率转换会触发全帧重新编码,大幅降低速度。

2. 替换AVAssetExportSession为AVAssetWriter

AVAssetExportSession是封装层,复杂场景下性能不如直接使用AVAssetWriter:

  • 若所有源视频编码参数一致,可直接遍历轨道,将CMSampleBuffer写入AVAssetWriter,跳过AVComposition的中间合成步骤,实现无转码拼接。
  • 需变速或叠加时,用AVAssetReader读取样本,通过CoreImage处理后写入,全程利用GPU加速。

3. 优化AVComposition的变速逻辑

原代码中scaleTimeRange会触发CPU端帧重采样,改用硬件加速的变速方式:

// 替换scaleTimeRange为AVVideoCompositionLayerInstruction的变速设置
let videoInstruction = AVMutableVideoCompositionInstruction()
videoInstruction.timeRange = CMTimeRange(start: currentTime, duration: scaledDuration)
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: compVideoTrack)
layerInstruction.setTransform(compVideoTrack.preferredTransform, at: currentTime)
layerInstruction.setRate(item.rate!, timeRange: CMTimeRange(start: currentTime, duration: range.duration))
videoInstruction.layerInstructions = [layerInstruction]
videoComposition.instructions.append(videoInstruction)

4. 替换CoreAnimation叠加为GPU加速的图像合成

用CoreImage实现overlay叠加,避免CPU端CALayer合成的开销:

let filter = CIFilter(name: "CISourceOverCompositing")!
let overlayImage = CIImage(image: overlay.image)!
filter.setValue(overlayImage, forKey: "inputImage")

let videoComposition = AVMutableVideoComposition(asset: composition) { request in
    let sourceImage = request.sourceImage.applyingOrientation(videoInfo.orientation.rawValue)
    filter.setValue(sourceImage, forKey: "inputBackgroundImage")
    request.finish(with: filter.outputImage!, context: nil)
}
videoComposition.renderSize = videoSize
export.videoComposition = videoComposition

5. 其他性能细节

  • 关闭shouldOptimizeForNetworkUse,该选项会增加文件碎片化优化,本地导出无需开启。
  • 提前预加载所有AVAsset的关键属性,避免合成时阻塞:
items.forEach { item in
    item.asset.loadValuesAsynchronously(forKeys: ["tracks"]) {
        // 处理加载完成逻辑,确保轨道信息就绪后再执行合成
    }
}
  • 使用串行后台队列处理合成和导出,避免线程竞争。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 01:32:04