(二十一)深入了解AVFoundation-编辑:导出视频与格式转换的全流程

博客专栏链接:AVFoundation架构与实践

博客专栏源码链接:AVFoundation-建议转存大量实战源码

一. 引言

在前面的系列博客中,我们主要聚焦于音视频的播放与合成技术,完成了视频的剪辑、水印叠加等功能,但并未深入讲解如何将编辑好的内容导出成新的视频文件。

本篇博客将详细介绍 AVFoundation 中视频导出的实现,重点讲解:

  • 参数化导出配置:如何灵活设置导出质量、格式等参数,满足不同需求;
  • 导出进度监听与取消:利用定时器轮询实现进度反馈,并支持用户主动取消导出;
  • 错误处理:针对导出失败的常见情况给出解决思路;
  • 保存到相册:导出完成后如何将视频保存至系统相册,及权限相关处理。

通过这篇文章,你将掌握一个可复用的导出组件设计思路,实战中可以直接使用并根据需求灵活调整。

二. 导出功能详解

在前面的博客中,我们使用了比较简陋的导出方式,参数固定且缺少对导出进度、错误和取消的处理。实际开发中,这些功能至关重要,能极大提升用户体验和程序稳定性。

本节将围绕我们封装的 PHCompositionExporter 类,详细介绍如何实现:

  1. 参数化导出配置
  2. 导出进度监听
  3. 取消导出
  4. 错误处理
  5. 保存到相册

2.1 参数化导出配置

AVFoundation 中,AVAssetExportSession 允许我们通过不同的预设(preset)控制导出质量、分辨率等,同时也支持多种输出格式。

在 PHCompositionExporter 里,beginExport 方法支持传入三个参数:

Swift 复制代码
func beginExport(presetName: String = AVAssetExportPresetHighestQuality,
                 outputFileType: AVFileType = .mp4,
                 saveToPhotoLibrary: Bool = true)

presetName:导出预设名称,常用的有

  • AVAssetExportPresetLowQuality
  • AVAssetExportPresetMediumQuality
  • AVAssetExportPresetHighestQuality(默认)
  • 还有专门针对HEVC编码的预设,如 AVAssetExportPresetHEVCHighestQuality

outputFileType:输出文件格式,常见有 .mp4, .mov, .m4v 等,需与预设兼容,否则导出会失败。

saveToPhotoLibrary:导出完成后是否自动保存到相册,默认为 true。

通过参数化配置,开发者可以根据具体场景灵活调整导出效果和格式,满足从快速预览到高质量成片的不同需求。

2.2 导出进度监听

AVAssetExportSession 自身不支持通过 KVO 监听导出进度,因此我们通过一个定时器每 0.1 秒轮询 exportSession.progress。

在类中我们通过 progressTimer 实现:

Swift 复制代码
self.progressTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
    guard let self = self, let session = self.exportSession else { return }
    self.onProgress?(session.progress)
}

然后在外部通过设置 onProgress 闭包,就能实时拿到进度数据,比如更新进度条。

2.3 取消导出

用户可能在导出过程中主动取消操作,我们需要提供接口取消导出,并正确处理回调。

PHCompositionExporter 提供了 cancelExport() 方法,内部调用 exportSession.cancelExport(),并且停止进度定时器:

Swift 复制代码
func cancelExport() {
    exportSession?.cancelExport()
    progressTimer?.invalidate()
    progressTimer = nil
}

导出取消后,exportSession.status 会变成 .cancelled,我们在回调中判断并返回对应错误。

2.4 错误处理

导出失败的情况较多,常见包括:

  • 磁盘空间不足
  • 导出格式和预设不兼容
  • 权限问题(写入沙盒或相册失败)
  • 导出过程异常中断

在导出完成的回调中,我们会判断 exportSession.status,如果是 .failed,则将错误信息通过 onCompletion 回调返回。

Swift 复制代码
case .failed:
    let error = exportSession.error ?? NSError(domain: "Export", code: -6, userInfo: [NSLocalizedDescriptionKey: "Export failed with unknown error."])
    self.onCompletion?(.failure(error))

这样调用方就可以据此做出对应的 UI 提示或重试逻辑。

2.5 保存到相册

导出视频后,通常需要保存到系统相册方便用户查看。

PHCompositionExporter 提供了 saveToPhotoLibrary 方法,利用 Photos 框架实现保存:

Swift 复制代码
PHPhotoLibrary.shared().performChanges({
    PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: outputURL)
}, completionHandler: { success, error in
    // 回调保存结果
})

注意,iOS 14+ 需要用户授权访问相册,若用户拒绝则保存失败。

三. Demo:导出功能完整示例

接下来我们以实际代码演示如何结合前面封装好的导出组件,实现完整的导出流程,包括显示导出进度视图、响应取消和完成操作。

Swift 复制代码
/// 执行导出
private func export() {
    let timeLine = timeLineViewController.timeLine
    let compositionBuilder = PHCompositionBuilderFactory.buildComositionBuilder(timeLine: timeLine)
    guard let composition = compositionBuilder.buildComposition() else {
        print("Failed to build composition for export")
        return
    }
    // 初始化导出助手
    exportHelper = PHCompositionExporter(composition: composition)
    // 显示导出进度视图
    showExportView()
    // 绑定导出进度和完成回调
    exportHelper?.onProgress = { [weak self] progress in
        // 更新导出视图进度
        guard let self = self else { return }
        self.exportView?.updateState(progress: progress, result: nil)
    }
    exportHelper?.onCompletion = { [weak self] result in
        // 导出完成,更新视图状态
        guard let self = self else { return }
        switch result {
        case .success(let url):
            print("导出成功,文件路径:\(url)")
            DispatchQueue.main.async {
                // 在主线程更新 UI
                self.exportView?.updateState(progress: 1.0, result: true)
            }
            // 可根据需求处理导出成功后的逻辑,比如保存、分享
        case .failure(let error):
            print("导出失败:\(error.localizedDescription)")
            DispatchQueue.main.async {
                // 在主线程更新 UI
                self.exportView?.updateState(progress: 0.0, result: false)
            }
        }
    }
    // 开始导出
    exportHelper?.beginExport()
    print("开始导出视频")
}

/// 显示导出视图
/// Show the export progress view, keep a weak reference, and handle user actions.
private func showExportView() {
    // 若已有导出视图,先移除
    if let existView = exportView {
        existView.removeFromSuperview()
    }
    // 初始化导出视图
    let exportView = PHExportView()
    // 设置视图样式及布局
    exportView.layer.masksToBounds = true
    exportView.layer.cornerRadius = 20.0
    self.view.addSubview(exportView)
    exportView.snp.makeConstraints { make in
        make.width.height.equalTo(250.0)
        make.center.equalToSuperview()
    }
    // 弱引用保存
    self.exportView = exportView
    // 绑定取消按钮回调
    exportView.onCancel = { [weak self] in
        // 取消导出
        self?.exportHelper?.cancelExport()
        // 移除导出视图并清理资源
        self?.exportView?.removeFromSuperview()
        self?.exportView = nil
        self?.exportHelper = nil
    }
    // 绑定完成按钮回调
    exportView.onComplete = { [weak self] in
        // 移除导出视图并清理资源
        self?.exportView?.removeFromSuperview()
        self?.exportView = nil
        self?.exportHelper = nil
    }
}

四. 结语

本篇博客我们详细介绍了 AVFoundation 中视频导出的全流程实现,从参数化配置、进度监听、取消导出,到错误处理与保存相册,都做了深入讲解和实战示范。

通过封装 PHCompositionExporter 和自定义的导出视图,我们实现了一个易用、灵活且用户体验友好的导出模块,方便在实际项目中直接集成。

接下来,你可以在此基础上继续扩展更多功能,比如导出格式支持更多编码参数,或者结合后台任务实现更稳定的长时间导出。

希望这篇文章对你的开发有所帮助,欢迎留言交流!

相关推荐
瑶光守护者1 小时前
【卫星通信】超低码率语音编码ULBC:EnCodec神经音频编解码器架构深度解析
深度学习·音视频·卫星通信·语音编解码·ulbc
枫叶梨花6 小时前
从 M4S 到 MP4:用 FFmpeg 轻松合并音视频文件
ffmpeg·音视频
奋斗的小羊羊1 天前
HTML5关键知识点之多种视频编码工具的使用方法
前端·音视频·html5
跨界混迹车辆网的Android工程师2 天前
HEVC(H.265)与HVC1的关系及区别
音视频·h.265
melonbo2 天前
视频清晰度:静态码率比动态码率更优秀吗?
音视频
8K超高清2 天前
广播级讯道摄像机CCU后挂上的PGM、ENG、PROD音频旋钮是做什么用的?
大数据·人工智能·科技·数码相机·音视频·智能硬件
Jason_zhao_MR2 天前
RK3576赋能无人机巡检:多路视频+AI识别引领智能化变革
人工智能·音视频·嵌入式·无人机
写点啥呢2 天前
Android为ijkplayer设置音频发音类型usage
android·音视频·usage·mediaplayer·jikplayer