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

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
        }
    })
}

问题根源

代码存在三个核心问题:

  1. 播放用的mutableComposition是基于播放器当前加载的原始asset生成的,导出时传入的是新建的AVMutableComposition实例,两者的轨道ID、时间映射、几何属性不匹配,原生录制的视频参数刚好和系统默认推断值对齐所以能导出成功,第三方来源视频参数不符合默认推断逻辑就会直接报错。
  2. 全局复用了同一个CIFilter实例,AVVideoComposition的帧处理回调是跨多线程调用的,多个线程同时修改同一个filter的属性会触发竞态问题,导致随机失败。
  3. 没有显式设置AVMutableVideoComposition的renderSize、frameDuration等必填参数,完全依赖系统自动推断参数,兼容性差。

修复方案

  1. 不要复用播放阶段生成的AVMutableVideoComposition实例给导出流程使用,导出时基于当前导出用的composition资产重新生成对应的videoComposition。
  2. 不要全局复用CIFilter实例,在帧处理回调内部每次新建filter实例,避免多线程竞态。
  3. 显式设置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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 12:09:21