news 2026/5/26 11:29:36

【技术选型】AVFoundation 双路径:从 AVCaptureMovieFileOutput 到 AVAssetWriter 的小视频录制实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【技术选型】AVFoundation 双路径:从 AVCaptureMovieFileOutput 到 AVAssetWriter 的小视频录制实战

1. 为什么需要双路径视频录制方案

最近在做一个社交类App的项目时,遇到了一个很有意思的技术选型问题:如何实现小视频录制功能?一开始我天真地以为直接用系统提供的UIImagePickerController就完事了,结果产品经理甩过来一长串需求清单:实时滤镜、精准裁剪、自定义分辨率...得,看来得搬出AVFoundation这个大家伙了。

AVFoundation提供了两种截然不同的视频录制路径:基于AVCaptureMovieFileOutput的"快速封装"方案,和基于AVAssetWriter的"深度定制"方案。这就像做菜一样,前者像是用现成的调料包,简单快捷但味道固定;后者则是自己调配调料,过程复杂但可以做出米其林级别的定制口味。

我在实际项目中两种方案都用过,AVCaptureMovieFileOutput方案从零开始到出Demo只用了半天,而AVAssetWriter方案光是调试视频参数就折腾了两天。但后者带来的灵活性让产品经理眼前一亮——我们可以在录制过程中实时添加水印、调整画质,甚至实现动态码率控制。

2. AVCaptureMovieFileOutput快速方案实战

2.1 基础搭建五部曲

先来看看这个"快餐式"方案的具体做法。记得第一次用这个方案时,我按照官方文档一步步操作,结果在真机上跑起来发现画面是倒置的——这就是没仔细看设备方向的典型坑。

// 1. 创建会话 let session = AVCaptureSession() session.sessionPreset = .hd1280x720 // 这个参数直接影响画质和性能 // 2. 配置设备输入 guard let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back), let audioDevice = AVCaptureDevice.default(for: .audio) else { return } let videoInput = try AVCaptureDeviceInput(device: videoDevice) let audioInput = try AVCaptureDeviceInput(device: audioDevice) // 3. 添加文件输出 let movieOutput = AVCaptureMovieFileOutput() if session.canAddOutput(movieOutput) { session.addOutput(movieOutput) } // 4. 设置预览层 let previewLayer = AVCaptureVideoPreviewLayer(session: session) previewLayer.videoGravity = .resizeAspectFill view.layer.addSublayer(previewLayer) // 5. 启动会话 DispatchQueue.global(qos: .userInitiated).async { session.startRunning() }

这里有个性能优化的小技巧:sessionPreset不要盲目选择最高分辨率,4K视频在旧设备上会导致明显卡顿。我通常先检测设备型号,中低端设备用720p,高端设备才考虑1080p。

2.2 录制控制的那些坑

开始录制看似简单,但有几个魔鬼细节需要注意:

func startRecording() { let outputURL = URL(fileURLWithPath: NSTemporaryDirectory()) .appendingPathComponent("tempMovie.mp4") // 这个参数影响视频方向 if let connection = movieOutput.connection(with: .video) { connection.videoOrientation = .portrait } movieOutput.startRecording(to: outputURL, recordingDelegate: self) } func stopRecording() { movieOutput.stopRecording() }

录制过程中最容易被忽视的是存储空间检查。有次用户反馈录制突然中断,排查后发现是手机存储空间不足——现在我会在开始录制前检查可用空间,小于100MB就提前提示用户。

2.3 代理回调的精准把控

实现AVCaptureFileOutputRecordingDelegate时,要注意这三个关键回调:

extension ViewController: AVCaptureFileOutputRecordingDelegate { // 实际开始写入文件时触发 func fileOutput(_ output: AVCaptureFileOutput, didStartRecordingTo fileURL: URL, from connections: [AVCaptureConnection]) { print("真正开始写入时间戳:\(CACurrentMediaTime())") } // 录制过程中可能出现的错误 func fileOutput(_ output: AVCaptureFileOutput, wasInterruptedAt timestamp: CMTime, reason: AVCaptureSession.InterruptionReason) { showAlert("录制被中断:\(reason.rawValue)") } // 录制完成回调 func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) { if let error = error { print("录制失败:\(error.localizedDescription)") return } processVideo(at: outputFileURL) } }

特别提醒:didFinishRecordingTo回调完成不代表文件已经写入完毕!我遇到过文件尚未完全写入就被后续处理代码读取的情况,现在都会用FileCoordinator确保文件完整性。

3. AVAssetWriter深度定制方案

3.1 数据流处理架构

当产品提出"要在录制时实时添加滤镜"的需求时,我知道必须上AVAssetWriter了。这个方案的核心在于处理CMSampleBuffer数据流,下面是它的基本架构:

// 配置视频输出 let videoOutput = AVCaptureVideoDataOutput() videoOutput.setSampleBufferDelegate(self, queue: processingQueue) videoOutput.alwaysDiscardsLateVideoFrames = true // 丢帧保性能 // 配置音频输出 let audioOutput = AVCaptureAudioDataOutput() audioOutput.setSampleBufferDelegate(self, queue: processingQueue) // 创建AssetWriter var assetWriter: AVAssetWriter? var videoWriterInput: AVAssetWriterInput? var audioWriterInput: AVAssetWriterInput? func setupWriter(outputURL: URL) throws { assetWriter = try AVAssetWriter(url: outputURL, fileType: .mp4) // 视频输入配置 let videoSettings: [String: Any] = [ AVVideoCodecKey: AVVideoCodecType.h264, AVVideoWidthKey: 720, AVVideoHeightKey: 1280, AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill ] videoWriterInput = AVAssetWriterInput( mediaType: .video, outputSettings: videoSettings ) videoWriterInput?.expectsMediaDataInRealTime = true // 音频输入配置 let audioSettings: [String: Any] = [ AVFormatIDKey: kAudioFormatMPEG4AAC, AVSampleRateKey: 44100, AVNumberOfChannelsKey: 2, AVEncoderBitRateKey: 128000 ] audioWriterInput = AVAssetWriterInput( mediaType: .audio, outputSettings: audioSettings ) audioWriterInput?.expectsMediaDataInRealTime = true guard let writer = assetWriter, writer.canAdd(videoWriterInput!), writer.canAdd(audioWriterInput!) else { throw NSError() } writer.add(videoWriterInput!) writer.add(audioWriterInput!) }

这里有个性能关键点:expectsMediaDataInRealTime必须设为true,否则在录制实时视频时会出现写入延迟。

3.2 实时处理的艺术

处理数据流时最考验技术功底,既要保证实时性又要处理各种异常情况:

extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate { func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { guard isRecording else { return } // 确保Writer就绪 if assetWriter?.status == .unknown { let startTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) assetWriter?.startWriting() assetWriter?.startSession(atSourceTime: startTime) } // 处理视频帧 if output is AVCaptureVideoDataOutput { processVideoBuffer(sampleBuffer) } // 处理音频帧 else if output is AVCaptureAudioDataOutput { processAudioBuffer(sampleBuffer) } } private func processVideoBuffer(_ buffer: CMSampleBuffer) { guard let writerInput = videoWriterInput, writerInput.isReadyForMoreMediaData else { return } // 这里可以插入滤镜处理 let filteredBuffer = applyFilters(to: buffer) if !writerInput.append(filteredBuffer) { print("视频帧写入失败:\(assetWriter?.error?.localizedDescription ?? "")") } } }

实测发现,在iPhone X上处理1080p视频时,单个滤镜会使帧率从30fps降到22fps左右。我的优化方案是:在低端设备上先降低分辨率再应用滤镜。

3.3 优雅的录制收尾

停止录制时的处理直接影响最终视频的完整性:

func stopRecording() { processingQueue.async { [weak self] in guard let self = self else { return } self.videoWriterInput?.markAsFinished() self.audioWriterInput?.markAsFinished() self.assetWriter?.finishWriting { [weak self] in guard let self = self, self.assetWriter?.status == .completed else { print("视频合成失败") return } DispatchQueue.main.async { self.didFinishRecording(url: self.assetWriter?.outputURL) } } } }

这里有个血泪教训:一定要在同一个队列(processingQueue)中完成markAsFinished操作,否则可能导致最后一帧丢失。我曾经因为这个问题被测试提了十几个视频残缺的bug。

4. 双方案深度对比与选型指南

4.1 性能参数实测对比

为了给团队提供客观的选型依据,我在iPhone 12上做了组对比测试:

指标AVCaptureMovieFileOutputAVAssetWriter
CPU占用率(720p)12%-18%22%-35%
内存占用80MB120MB
启动延迟200ms500ms
视频处理延迟无实时处理能力3-5帧延迟
1080p录制续航4小时2.5小时

有趣的是,当开启实时滤镜后,AVAssetWriter方案的CPU占用会飙升到45%以上,这时候必须考虑降低分辨率或者优化滤镜算法。

4.2 六大选型决策因素

根据项目经验,我总结出这些决策要点:

  1. 开发周期:如果赶时间上线,MovieFileOutput方案能节省至少3天开发量
  2. 定制需求:需要实时处理?选AVAssetWriter没商量
  3. 设备兼容性:针对旧设备优化时,MovieFileOutput的稳定性更好
  4. 后期处理:如果要对视频二次编辑,AVAssetWriter的元数据更完整
  5. 团队技能:AVAssetWriter需要更深的视频编码知识
  6. 功耗敏感:直播类应用要谨慎使用AVAssetWriter

最近一个电商项目里,我们最终采用了混合方案:普通商品展示用MovieFileOutput,需要AR试妆的特殊场景用AVAssetWriter实时添加妆容效果。

4.3 常见场景适配建议

  • 社交APP短视频:先用MovieFileOutput快速上线,迭代时逐步替换为AVAssetWriter
  • 教育类录屏:AVAssetWriter可以实时添加标注和画中画
  • 安防监控:MovieFileOutput更稳定,且支持分段存储
  • AR应用:必须用AVAssetWriter处理实时3D渲染结果

记得有个运动类APP的需求是在录制时实时标注心率数据,用AVAssetWriter配合Core Graphics在每帧上绘制数据曲线,最终效果让客户直呼"黑科技"。

5. 进阶技巧与避坑指南

5.1 方向处理的正确姿势

视频方向问题坑过无数开发者,这是我的终极解决方案:

func adjustVideoOrientation( _ buffer: CMSampleBuffer, connection: AVCaptureConnection ) -> CGAffineTransform { var transform = CGAffineTransform.identity // 设备方向 let deviceOrientation = UIDevice.current.orientation // 界面方向 let interfaceOrientation = UIApplication.shared.statusBarOrientation // 计算正确的变换矩阵 if connection.isVideoOrientationSupported { switch interfaceOrientation { case .portraitUpsideDown: connection.videoOrientation = .portraitUpsideDown transform = CGAffineTransform(rotationAngle: .pi) case .landscapeLeft: connection.videoOrientation = .landscapeLeft transform = CGAffineTransform(rotationAngle: .pi/2) case .landscapeRight: connection.videoOrientation = .landscapeRight transform = CGAffineTransform(rotationAngle: -.pi/2) default: connection.videoOrientation = .portrait } } // 前置摄像头需要镜像翻转 if cameraPosition == .front { transform = transform.scaledBy(x: -1, y: 1) } return transform }

把这个变换矩阵设置到AVAssetWriterInput的transform属性上,能解决99%的视频方向问题。

5.2 内存优化实战

处理高分辨率视频时,内存管理尤为关键:

  1. 使用CMSampleBuffer池:避免频繁创建释放缓冲区
  2. 及时释放CVImageBuffer
buffer.withImageBuffer { imageBuffer in // 处理图像... CVPixelBufferUnlockBaseAddress(imageBuffer, .readOnly) }
  1. 设置合理的像素格式:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange比ARGB节省40%内存
  2. 监控内存警告
NotificationCenter.default.addObserver( forName: UIApplication.didReceiveMemoryWarningNotification, object: nil, queue: .main ) { [weak self] _ in self?.handleMemoryWarning() }

在最近的项目中,通过这些优化将4K视频处理时的内存峰值从1.2GB降到了600MB左右。

5.3 录制质量调优

视频质量的黄金三角法则:分辨率、帧率、码率需要平衡:

// 高质量配置 let highQualitySettings: [String: Any] = [ AVVideoCodecKey: AVVideoCodecType.hevc, AVVideoWidthKey: 1920, AVVideoHeightKey: 1080, AVVideoCompressionPropertiesKey: [ AVVideoAverageBitRateKey: 8_000_000, AVVideoExpectedSourceFrameRateKey: 30, AVVideoMaxKeyFrameIntervalKey: 30 ] ] // 均衡配置 let balancedSettings: [String: Any] = [ AVVideoCodecKey: AVVideoCodecType.h264, AVVideoWidthKey: 1280, AVVideoHeightKey: 720, AVVideoCompressionPropertiesKey: [ AVVideoAverageBitRateKey: 3_000_000, AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel ] ]

实测发现,HEVC编码比H.264节省约40%存储空间,但对设备要求更高。我的经验法则是:面向年轻用户群用HEVC,大众应用还是保守选择H.264。

6. 扩展功能实现思路

6.1 实时滤镜的三种实现方式

  1. Core Image方案
func applyFilter(to buffer: CMSampleBuffer) -> CMSampleBuffer? { guard let imageBuffer = CMSampleBufferGetImageBuffer(buffer) else { return nil } let ciImage = CIImage(cvImageBuffer: imageBuffer) let filter = CIFilter(name: "CISepiaTone")! filter.setValue(ciImage, forKey: kCIInputImageKey) filter.setValue(0.8, forKey: kCIInputIntensityKey) let context = CIContext(options: nil) context.render(filter.outputImage!, to: imageBuffer) return buffer }

优点:简单易用,内置滤镜丰富 缺点:性能消耗大,不适合复杂滤镜链

  1. Metal方案: 使用MTKView配合自定义着色器,性能最好但开发成本高
  2. 第三方库: 像GPUImage这样的成熟框架平衡了性能和易用性

在最近的美颜相机项目中,我们最终选择Metal方案,通过预编译着色器和管线优化,将滤镜处理时间控制在5ms以内。

6.2 动态码率控制技巧

根据场景动态调整码率可以显著改善用户体验:

func adjustBitRate( for motionLevel: CGFloat, currentBitRate: Int ) -> Int { let baseBitRate = 3_000_000 let maxBitRate = 6_000_000 // 运动剧烈时提高码率 let adjusted = baseBitRate + Int(motionLevel * 1_000_000) return min(max(adjusted, baseBitRate), maxBitRate) } // 在视频处理回调中 func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { let motion = calculateMotionLevel(from: sampleBuffer) let newBitRate = adjustBitRate(for: motion, currentBitRate: currentBitRate) if newBitRate != currentBitRate { videoWriterInput?.setBitRate(newBitRate) currentBitRate = newBitRate } }

这个技巧在运动类APP中特别有用,静态场景节省存储空间,快速运动时保证画质。

6.3 音频处理的隐藏关卡

很多人只关注视频处理,其实音频也有不少坑:

// 解决音频不同步问题 let audioSettings: [String: Any] = [ AVFormatIDKey: kAudioFormatMPEG4AAC, AVSampleRateKey: 44100, AVNumberOfChannelsKey: 2, AVEncoderBitRateKey: 128000, AVEncoderBitRateStrategyKey: AVAudioBitRateStrategy_Constant, AVEncoderBitDepthHintKey: 16 ] // 处理音频采样率转换 func adjustAudioBuffer(_ buffer: CMSampleBuffer) -> CMSampleBuffer? { var timingInfo = CMSampleTimingInfo() CMSampleBufferGetSampleTimingInfo(buffer, at: 0, timingInfoOut: &timingInfo) timingInfo.presentationTimeStamp = adjustTimeStamp( timingInfo.presentationTimeStamp ) var newBuffer: CMSampleBuffer? CMSampleBufferCreateCopyWithNewTiming( allocator: kCFAllocatorDefault, sampleBuffer: buffer, sampleTimingEntryCount: 1, sampleTimingArray: &timingInfo, sampleBufferOut: &newBuffer ) return newBuffer }

曾经有个项目因为没处理好音频时间戳,导致视频越长音画不同步越明显,最后不得不重新处理所有用户上传的视频。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/26 11:29:18

MongoDB find() 实战优化:从查不到到87毫秒的完整链路

1. 这不是语法手册,而是一份能让你当天就写出可靠查询的实战笔记 MongoDB find() 是每个刚接触文档数据库的人最先敲下的命令,但绝大多数人卡在“写出来能跑”和“写出来能用”之间——查不到数据时反复改条件却不知从何排查,聚合管道里字段名…

作者头像 李华
网站建设 2026/5/26 11:29:10

探索picacomic-downloader:基于Tauri架构的现代化漫画下载器深度解析

探索picacomic-downloader:基于Tauri架构的现代化漫画下载器深度解析 【免费下载链接】picacomic-downloader 哔咔漫画 picacomic pica漫画 bika漫画 PicACG 多线程下载器,带图形界面 带收藏夹,已打包exe 下载速度飞快 项目地址: https://g…

作者头像 李华
网站建设 2026/5/26 11:29:06

5分钟自动化部署:Brigadier解决Mac Boot Camp驱动管理难题

5分钟自动化部署:Brigadier解决Mac Boot Camp驱动管理难题 【免费下载链接】brigadier Fetch and install Boot Camp ESDs with ease. 项目地址: https://gitcode.com/gh_mirrors/bri/brigadier 在IT运维和跨平台部署的实际工作中,Mac电脑的Boot …

作者头像 李华
网站建设 2026/5/26 11:29:04

PUBG罗技压枪脚本终极指南:从零配置到实战精通

PUBG罗技压枪脚本终极指南:从零配置到实战精通 【免费下载链接】PUBG-Logitech PUBG罗技鼠标宏自动识别压枪 项目地址: https://gitcode.com/gh_mirrors/pu/PUBG-Logitech PUBG-Logitech是一款基于罗技鼠标宏的绝地求生自动压枪解决方案,通过先进…

作者头像 李华
网站建设 2026/5/26 11:28:59

Unity刮刮乐实现:RenderTexture像素擦除与UI性能优化

1. 这个“刮刮乐”不是玩具,是 Unity UI 渲染机制的微型沙盒 你有没有试过在 Unity 里用 RawImage 做遮罩,结果发现刮开区域边缘发虚、多次刮擦后性能断崖式下跌、甚至在 Android 设备上直接黑屏?我去年帮一个校园活动做互动展板时就栽在这上…

作者头像 李华
网站建设 2026/5/26 11:28:58

VL01N还是CNS0?SAP项目发货场景选择指南:结合里程碑开票讲透区别

VL01N与CNS0:SAP项目发货场景的深度决策框架项目发货场景的核心决策困境在SAP项目实施过程中,发货环节的选择往往成为业务流畅性的关键转折点。VL01N和CNS0这两个事务代码看似都能完成发货操作,但背后的业务流程、财务影响和系统逻辑却存在本…

作者头像 李华