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
相关产品推荐
相关产品推荐

