AVAssetExportSession应用CIFilter导出视频非确定性失败报-11841错误
AVAssetExportSession导出带CIFilter的视频报-11841错误修复
问题现象
使用AVAssetExportSession导出应用了CIFilter的视频时,存在非确定性失败问题,暂未找到稳定复现路径:
- iPhone原生相机录制的视频可正常完成导出
- 导出从WhatsApp下载的视频、或此前经过一次导出的视频时,会触发如下错误:
Error Domain=AVFoundationErrorDomain Code=-11841 "Operation Stopped"
UserInfo={NSLocalizedFailureReason=The video could not be composed.,
NSLocalizedDescription=Operation Stopped,
NSUnderlyingError=0x2839eaf10 {Error Domain=NSOSStatusErrorDomain
Code=-17390 "(null)"}}
测试验证:注释exporter.videoComposition = mutableComposition代码行,或将导出预设修改为AVAssetExportPresetPassthrough时错误消失,但此时导出的视频不会应用CIFilter效果,可确认问题与AVMutableVideoComposition的使用逻辑相关。
原问题实现代码如下:
var mutableComposition = AVMutableVideoComposition() let exposureFilter = CIFilter.exposureAdjust() func updateComposition() { mutableComposition = AVMutableVideoComposition(asset: player.currentItem!.asset, applyingCIFiltersWithHandler: { request in self.exposureFilter.inputImage = request.sourceImage.clampedToExtent() self.exposureFilter.ev = 5 let output = self.exposureFilter.outputImage!.cropped(to: request.sourceImage.extent) request.finish(with: output, context: nil) }) player.currentItem?.videoComposition = mutableComposition } func exportVideo() { let sourceAsset = AVURLAsset(url: videoURL) let composition = AVMutableComposition() let sourceVideoTrack = sourceAsset.tracks(withMediaType: .video)[0] let compositionVideoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) try? compositionVideoTrack!.insertTimeRange(CMTimeRangeMake(start: .zero, duration: sourceAsset.duration), of: sourceVideoTrack, at: .zero) if sourceAsset.tracks(withMediaType: .audio).isEmpty == false { // Video has sound let sourceAudioTrack = sourceAsset.tracks(withMediaType: .audio)[0] let compositionAudioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) try? compositionAudioTrack!.insertTimeRange(CMTimeRangeMake(start: .zero, duration: sourceAsset.duration), of: sourceAudioTrack, at: .zero) } guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } let videoID = UUID().uuidString let videofileName = "\(videoID).mov" let outputURL = documentsDirectory.appendingPathComponent(videofileName) if FileManager.default.fileExists(atPath: outputURL.path) { do { try FileManager.default.removeItem(at: outputURL) } catch {} } let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality)! exporter.outputFileType = .mov exporter.outputURL = outputURL exporter.videoComposition = mutableComposition exporter.exportAsynchronously(completionHandler: { switch exporter.status { case .failed: print("Export failed \(exporter.error!)") case .completed: UISaveVideoAtPathToSavedPhotosAlbum(outputURL.path, self, #selector(video(_:didFinishSavingWithError:contextInfo:)), nil) default: break } }) }
问题根源
代码存在三个核心问题:
- 播放用的
mutableComposition是基于播放器当前加载的原始asset生成的,导出时传入的是新建的AVMutableComposition实例,两者的轨道ID、时间映射、几何属性不匹配,原生录制的视频参数刚好和系统默认推断值对齐所以能导出成功,第三方来源视频参数不符合默认推断逻辑就会直接报错。 - 全局复用了同一个
CIFilter实例,AVVideoComposition的帧处理回调是跨多线程调用的,多个线程同时修改同一个filter的属性会触发竞态问题,导致随机失败。 - 没有显式设置
AVMutableVideoComposition的renderSize、frameDuration等必填参数,完全依赖系统自动推断参数,兼容性差。
修复方案
- 不要复用播放阶段生成的
AVMutableVideoComposition实例给导出流程使用,导出时基于当前导出用的composition资产重新生成对应的videoComposition。 - 不要全局复用CIFilter实例,在帧处理回调内部每次新建filter实例,避免多线程竞态。
- 显式设置videoComposition的
renderSize、frameDuration属性,对齐源视频参数。
修复后的导出核心代码参考:
func exportVideo() { let sourceAsset = AVURLAsset(url: videoURL) let composition = AVMutableComposition() guard let sourceVideoTrack = sourceAsset.tracks(withMediaType: .video).first else { return } let compositionVideoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) try? compositionVideoTrack?.insertTimeRange(CMTimeRangeMake(start: .zero, duration: sourceAsset.duration), of: sourceVideoTrack, at: .zero) // 显式设置合成轨的transform,对齐源视频方向 compositionVideoTrack?.preferredTransform = sourceVideoTrack.preferredTransform if let sourceAudioTrack = sourceAsset.tracks(withMediaType: .audio).first { let compositionAudioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) try? compositionAudioTrack?.insertTimeRange(CMTimeRangeMake(start: .zero, duration: sourceAsset.duration), of: sourceAudioTrack, at: .zero) } // 基于当前导出用的composition生成新的videoComposition,不要复用播放用的实例 let videoComposition = AVMutableVideoComposition(asset: composition, applyingCIFiltersWithHandler: { request in // 回调内部新建filter实例,不要全局复用 let exposureFilter = CIFilter.exposureAdjust() exposureFilter.inputImage = request.sourceImage.clampedToExtent() exposureFilter.ev = 5 guard let output = exposureFilter.outputImage?.cropped(to: request.sourceImage.extent) else { request.finish(with: NSError(domain: "FilterError", code: -1, userInfo: nil)) return } request.finish(with: output, context: nil) }) // 显式设置渲染尺寸和帧率,对齐源视频参数 videoComposition.renderSize = sourceVideoTrack.naturalSize.applying(sourceVideoTrack.preferredTransform) videoComposition.frameDuration = CMTimeMake(value: 1, timescale: 30) // 按源视频实际帧率修改即可 // 后续导出逻辑保持不变 guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } let outputURL = documentsDirectory.appendingPathComponent("\(UUID().uuidString).mov") if FileManager.default.fileExists(atPath: outputURL.path) { try? FileManager.default.removeItem(at: outputURL) } guard let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality) else { return } exporter.outputFileType = .mov exporter.outputURL = outputURL exporter.videoComposition = videoComposition // 传入新生成的、匹配当前composition的实例 exporter.exportAsynchronously(completionHandler: { switch exporter.status { case .failed: print("Export failed \(exporter.error!)") case .completed: UISaveVideoAtPathToSavedPhotosAlbum(outputURL.path, self, #selector(video(_:didFinishSavingWithError:contextInfo:)), nil) default: break } }) }
内容的提问来源于stack exchange,提问作者El Tecla
相关产品推荐
相关产品推荐

