做iOS音视频开发,绕不开AVFoundation这套苹果原生的多媒体框架。这些年我用它做过拍照、短视频拍摄、实时预览,也接过直播推流,踩坑不少,但积累下来的经验也最值钱。这篇文章我就完整聊聊怎么用AVFoundation搭一套照片/视频捕捉能力,再延伸到小视频录制和直播推流场景,全是我实际验证过、可以直接拿去用的东西。
先说明一下这篇博文适合谁看:如果你是想自己做相机App、扫码工具、自定义滤镜拍照的开发者,或者在校学生刚接触iOS多媒体开发,又或者是做直播、短视频类产品需要自研采集层的技术负责人,这篇文章都能给你提供一个完整的落地思路。读完你能搞清楚AVFoundation的核心架构怎么组织、照片和视频捕捉的代码怎么写、直播推流怎么和AVFoundation衔接,以及那些文档里不会写但实际开发中一定会遇到的坑。
1. 内容整体设计与思路拆解
1.1 为什么不用系统相机,非要自己写AVFoundation
很多朋友做App需要拍照或录像功能时,第一反应是直接用UIImagePickerController。它确实能用一行代码调起系统相机,但问题也很明显:你拿不到实时帧做美颜、滤镜、贴纸;界面风格不能定制;也没法在录像的同时做声音特效或者插入第三方推流。说白了,系统相机是黑盒,你只能拿到最终结果,过程完全不可控。
而AVFoundation从设计上就把“采集”“处理”“输出”拆开了。你可以把采集到的每一帧视频数据拿过来,先交给滤镜处理,再写入本地文件或推到直播服务器。这个灵活性,是所有短视频App、直播App都必须依赖的基础能力。我自己第一次做“对准物体实时识别并拍照”的功能时,靠的就是AVFoundation里的AVCaptureVideoDataOutput,把实时帧交给CoreImage去分析,拿到结果后再决定要不要拍照。这条链路用系统UIViewController是完全走不通的。
补一句,如果你只是做企业内部工具,要求截止时间紧,那UIImagePickerController也能用,这个方案没有错。但如果你想做的是内容型产品,后续要加美颜、特效、直播,那就别犹豫,直接上AVFoundation,早写早省心。
1.2 AVFoundation核心架构:会话、设备、输入、输出
AVFoundation拍照和录像的架构,可以把核心部件拆成四层:
- AVCaptureSession:采集会话,是整个捕捉流程的中枢。它负责把输入设备(摄像头、麦克风)和输出端(照片、视频文件、视频帧数据)组合在一起,并管理运行状态。
- AVCaptureDevice:物理设备,比如前置摄像头、后置摄像头、麦克风。它描述了设备的能力,比如支持哪些分辨率、哪些帧率、有没有闪光灯。
- AVCaptureInput:设备的输入抽象。把摄像头和麦克风对象包装成
AVCaptureDeviceInput,然后添加到AVCaptureSession。 - AVCaptureOutput:输出端。常见的有
AVCapturePhotoOutput(拍照片)、AVCaptureMovieFileOutput(录制视频到文件)、AVCaptureVideoDataOutput(拿到一帧一帧的视频数据)、AVCaptureAudioDataOutput(拿到音频数据)。
理解了这个架构,后面所有代码都是围绕这几个对象在转。我个人习惯把这一层比作“水管”:AVCaptureDevice是水龙头,AVCaptureInput是接水龙头到主管道的那段管子,AVCaptureSession是主管道,AVCaptureOutput则是水龙头下不同的接水容器。你换一种输出,就相当于换了接水的容器,但水源和水管是那套。
1.3 我为什么推荐用AVCaptureSessionPresetHigh加AVCapturePhotoOutput组合
苹果在iOS里扔了一堆sessionPreset预设,什么AVCaptureSessionPreset3840x2160、AVCaptureSessionPreset1920x1080、AVCaptureSessionPresetHigh。初学者容易纠结:到底该选哪个?
我的经验是先搞明白“目标用途”。如果你的App是垂直类的工具型拍照应用,比如AR测距、文档扫描,那完全没必要开高分辨率,AVCaptureSessionPreset1280x720就很够了,处理快、内存压力小。但如果你做的是短视频拍摄,至少需要1080p,我一般直接设AVCaptureSessionPresetHigh,因为这个预设会根据当前设备自动选择最优分辨率,一般就是1920x1080,而且兼容性最好。
至于AVCapturePhotoOutput,它是UIImagePickerController底层也在用的照片输出端,支持HEIF、JPEG格式、RAW照片,也能拿到实时预览的深度数据。用它拍照片,流程比老一代的AVCaptureStillImageOutput简单,而且不阻塞采集管线。我在项目里几乎只认准它。
2. 核心细节解析与实操要点
2.1 权限处理:摄像头和麦克风的隐私合规细节
做相机功能,第一步是处理隐私权限。iOS对摄像头和麦克风卡得很严,你必须在Info.plist里声明NSCameraUsageDescription和NSMicrophoneUsageDescription,否则App一调用相关API就直接崩溃。这里有个很容易被忽略的点:你会发现只录制视频不录制声音,也还是要申请麦克风权限。因为AVCaptureDeviceInput是同时支持音频输入的,一旦你尝试把麦克风加进session里捕捉音频,没有权限说明文本就直接闪退。
我通常在页面加载时先调AVCaptureDevice.authorizationStatus(for: .video)查状态,如果还没决定就主动弹一次权限询问框。代码写起来不长,但这块务必稳一点。另外我见过不少开发者在“仅拍照”模式下也申请麦克风权限,导致用户觉得产品很“贪心”,我的建议是:拍照页面只申请相机权限,等真正要录视频了再去请求麦克风权限。
权限的合规性不止是“能不能用”,还包括你用相机数据做什么。这里提醒一句:不要偷偷在后台或用户不知情的情况下持续采集视频帧。苹果审核对这类行为特别敏感,一旦被发现,可能不只是下架这么简单,而是直接封开发者账号。我们自己项目里就见过同行的App因为后台采集被下架的例子,成本太高了。
2.2 视频格式和帧率选择:要清楚细节背后的代价
1080p30和720p60哪个好?这个问题没有标准答案,取决于你的业务需要。做直播推流,我建议用720p加30fps,因为观众手机屏幕就那么大,720p完全够看,而且直播是一种实时交互场景,低延迟比清晰度更重要。做后期精剪的短视频,建议用1080p60,因为60帧的素材在慢动作、变速剪辑里能保留更多细节,画面更流畅。
设置帧率时需要用到AVCaptureDevice.Format,很多新手直接改activeVideoMinFrameDuration却无效,就是因为没先设置好对应的activeFormat。正确操作是:
- 在设备支持的所有格式里,找到
CMVideoFormatDescriptionGetDimensions(format.formatDescription)分辨率符合需求的格式。 - 再看这个格式是否支持
30p或60p的videoSupportedFrameRateRanges。 - 先给设备设置
activeFormat,再设置activeVideoMinFrameDuration = CMTime(value: 1, timescale: 30)。
这不是黑魔法,而是AVFoundation的约束顺序。设置帧率时还有一个细节,某些设备在开启HDR或特定格式时,支持的帧率范围会缩小。所以当你在旧机型上发现60fps设不了,先别怀疑代码,去检查一下格式的支持列表。
2.3 照片输出与视频输出的差异:别混用错了对象
照片输出AVCapturePhotoOutput和视频输出AVCaptureMovieFileOutput,看起来都是输出,但内部机制完全不同。
AVCapturePhotoOutput的capturePhoto(with:delegate:),通过代理回调给你AVCapturePhoto对象,你可以从里面取出fileDataRepresentation(),然后转成UIImage。它支持连拍、RAW、闪光灯控制、预览图等。我实际用下来的感受是:它是“事件驱动”的,按下快门的一刻才去拍一张,不持续占用处理资源。
AVCaptureMovieFileOutput则不一样,它是“录制驱动”的,startRecording(to:recordingDelegate:)之后持续接收画面和声音,直到调stopRecording(),最终把数据写入指定文件URL。视频写入的过程中,你很难中途对单一帧做处理。所以如果要做实时滤镜录制,AVCaptureMovieFileOutput基本不够用,你得用AVCaptureVideoDataOutput拿到原始帧,处理后用AVAssetWriter写入。这点很多刚接触的人会踩坑,觉得录视频“有现成组件”就够了,直到需求变成“录视频同时加贴纸”,才发现方案整个要推翻。
2.4 实时预览层的输出方式与UI渲染
预览层AVCaptureVideoPreviewLayer是把采集到的画面渲染到屏幕上的核心。它不是UIView,而是CALayer的子类,可以嵌入任意UIView的layer中。我通常这样用:
let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) previewLayer.frame = view.bounds previewLayer.videoGravity = .resizeAspectFill view.layer.insertSublayer(previewLayer, at: 0)videoGravity这里我基本都是选.resizeAspectFill,因为它可以让画面填满整个预览框,同时保证比例的完整,视野刚好留出裁剪余地。注意这里很容易出现“画面方向不对”的问题,尤其是竖屏应用。在设置connection.videoOrientation = .portrait之前,画面默认可能是横着的。我自己习惯在viewDidLayoutSubviews里根据当前UI方向去更新previewLayer.connection.videoOrientation,否则旋转过后画面朝向会很奇怪。
3. 实操过程与核心环节实现
3.1 搭建一个最小可用的采集会话
直接上一套我实际用过的、经过压测的最小采集代码,配置顺序是关键:
import AVFoundation final class CameraEngine { private let session = AVCaptureSession() private let photoOutput = AVCapturePhotoOutput() private let movieOutput = AVCaptureMovieFileOutput() private(set) var previewLayer: AVCaptureVideoPreviewLayer? private var videoDevice: AVCaptureDevice? { AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) } private var audioDevice: AVCaptureDevice? { AVCaptureDevice.default(for: .audio) } private let sessionQueue = DispatchQueue(label: "com.camera.session") func setup() { sessionQueue.async { [weak self] in guard let self = self else { return } self.session.beginConfiguration() self.session.sessionPreset = .high // 添加视频输入 if let videoDevice = self.videoDevice, let input = try? AVCaptureDeviceInput(device: videoDevice), self.session.canAddInput(input) { self.session.addInput(input) } // 添加音频输入 if let audioDevice = self.audioDevice, let input = try? AVCaptureDeviceInput(device: audioDevice), self.session.canAddInput(input) { self.session.addInput(input) } // 添加照片输出 if self.session.canAddOutput(self.photoOutput) { self.session.addOutput(self.photoOutput) } // 添加视频文件输出 if self.session.canAddOutput(self.movieOutput) { self.session.addOutput(self.movieOutput) } self.session.commitConfiguration() self.session.startRunning() } } }这段代码有几个关键点值得说道。session.beginConfiguration()和commitConfiguration()必须成对出现,它们让这一系列的增删输入输出成为一次原子操作,中间不发生中断。startRunning()一定不要在主线程调用,它的耗时在切换摄像头或重新配置时可能达到几百毫秒,直接卡UI。我习惯把所有对session的操作都放到sessionQueue串行队列里,保证线程安全。
3.2 照片拍摄的完整实现:从取景到回调
有了session之后,拍照片的逻辑就简单了。先设置输出参数的AVCapturePhotoSettings,然后调capturePhoto:
extension CameraEngine { func takePhoto(completion: @escaping (UIImage?) -> Void) { guard let connection = photoOutput.connection(with: .video) else { completion(nil) return } connection.videoOrientation = .portrait let settings = AVCapturePhotoSettings() settings.flashMode = .auto photoOutput.capturePhoto(with: settings, delegate: PhotoCaptureProcessor(completion: completion)) } } final class PhotoCaptureProcessor: NSObject, AVCapturePhotoCaptureDelegate { private let completion: (UIImage?) -> Void init(completion: @escaping (UIImage?) -> Void) { self.completion = completion } func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { guard error == nil, let data = photo.fileDataRepresentation(), let image = UIImage(data: data) else { completion(nil) return } completion(image) } }这里有一个非常值得注意的细节:AVCapturePhotoSettings如果需要基于某个设置对象创建副本,比如先拍一张用于测光的预览图,再在这个基础上拍真正的成片,可以用init(from:)复制一个settings对象。我在做HDR拍摄和闪光灯分步拍摄时会用这种方案。另外,很多相机App在拍完照片后会做一次裁切或方向矫正,这步建议放到后台线程处理,不要把大图的UIImage在主线程里反复draw,否则很容易出现瞬时卡顿。
3.3 视频录制与落盘:注意空间和时长控制
用AVCaptureMovieFileOutput录制视频,核心代码不长:
extension CameraEngine { func startRecording() { guard let connection = movieOutput.connection(with: .video) else { return } connection.videoOrientation = .portrait connection.isVideoStabilizationEnabled = true let outputURL = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString) .appendingPathExtension("mov") movieOutput.startRecording(to: outputURL, recordingDelegate: self) } func stopRecording() { movieOutput.stopRecording() } }录制完成的后代理方法里,你可以拿到outputFileURL。我一般会立刻把它存进相册或者交给下一个处理模块。有几个坑要提醒:
- 视频方向的坑。如果录音中旋转了手机,连接方向没及时更新,成片有可能会出现“横着躺”的情况。我建议在录制期间锁定界面为竖屏,或者使用
AVCaptureConnection的videoRotationAngle动态调整。 - 视频稳定化。
isVideoStabilizationEnabled = true确实能提升手持画面的稳定感,但它会放大画面裁切,改变视场角。如果产品对画幅要求极高,我建议关闭稳定化,靠设备的光学防抖。 - 时长和文件大小。
AVCaptureMovieFileOutput在存储空间不足时,会自动停止录制,并通过代理回调error提示。所以录制前最好检查一下剩余空间,实践下来剩余空间低于500MB就别让用户继续录了,不然容易录到一半中断。
3.4 小视频/直播场景的帧数据输出与推流衔接
走到这一步,AVFoundation的另一个重要输出就要出场了:AVCaptureVideoDataOutput。小视频里的实时滤镜、贴纸,直播里的美颜、推流,都靠它拿原始帧。
let videoOutput = AVCaptureVideoDataOutput() videoOutput.videoSettings = [ kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA ] videoOutput.setSampleBufferDelegate(self, queue: videoProcessQueue)实现captureOutput(_:didOutput:from:)代理后,每个视频帧都会以CMSampleBuffer的形式回调过来。从这个缓冲里你可以拿到CVPixelBuffer,交给CoreImage做磨皮、美白、滤镜,也可以直接送到编码器进行H264编码。
在直播推流场景里,我最常用的路径是:AVFoundation采集视频帧和音频帧 → 把帧同步交给推流SDK(比如LFLiveKit、七牛/腾讯云直播SDK) → SDK内部完成H264/AAC编码 → RTMP协议推送到直播服务器。你自己的App要做的事情,其实就是把AVFoundation的输入桥接到SDK的帧输入接口。市面上的直播SDK基本都封装了推流协议、断线重连、缓冲区控制,你不需要自己碰RTMP。
这里要特别留意一个问题:音频和视频的同步。直播的清晰度反而不是第一优先级,第一优先级是音画同步和延迟。AVFoundation的视频帧回调时间戳和音频帧回调时间戳是不同源的,推流SDK内部有一套对齐逻辑,所以你需要尽量保持两个回调的原始时间戳不被改动。不要自己乱改CMSampleBuffer的PTS,改错了就是音画不同步,而且这种问题很难排查。
如果自己用VideoToolbox硬编码实现推流,我劝你三思。这条路对工程能力要求非常高,编码参数、关键帧间隔、码率控制、网络抖动缓冲,任何一个环节有问题都会导致直播卡顿花屏。我自己第一次做的时候,光是关键帧间隔(GOP)和码率自适应就调了好长时间。如果你的核心业务不是“自研推流引擎”,而只是“把直播功能上线”,那用成熟的推流SDK是性价比最高的选择。
3.5 小视频录制的另一种形态:本地精致录制
直播走的是“实时编码推流”,而小视频录完一般是要落盘保存、剪辑的。如果你想要在录制的同时加滤镜和美颜,AVFoundation原生AVCaptureMovieFileOutput做不到,它只能存原始画面。这时候就需要AVCaptureVideoDataOutput硬编码落盘方案:
流程图可以这样理解:采集帧 → 逐帧做滤镜处理 → 用AVAssetWriter写入MP4文件。
AVAssetWriter配合AVAssetWriterInputPixelBufferAdaptor,可以把每一帧处理后的CVPixelBuffer写入到文件中。H264编码由系统底层完成,不用自己面对复杂的视频编码算法。我在做一个小视频App时,就是这样实现“录制时实时磨皮美白”的效果的。性能上注意两点:
AVAssetWriterInput的expectsMediaDataInRealTime要设为true,告诉系统这些数据是实时产生的。- 一定要控制帧率的稳定性,如果某一帧处理太慢,不要积压,直接丢掉更合理。暂停录制的瞬间,最好调用一次
markAsFinished()再finishWriting()。
这种方式比AVCaptureMovieFileOutput复杂一些,但它换来了最大的后期灵活性:你可以对每一帧随心所欲地做处理,也可以随时切换滤镜强度而不影响视频连续性。现在很多主流小视频App的拍摄逻辑其实就是这套思路。
4. 常见问题与排查技巧实录
4.1 黑屏或预览层不显示
遇到预览黑屏,先别急着重装App。99%的原因是下面这几个:没有相机权限、startRunning()没有真正跑起来、AVCaptureVideoPreviewLayer的session绑定错了、或者摄像头创建失败。
我的排查顺序是这样的:
- 检查
Info.plist权限描述文本是否存在。 - 在
setup()里加日志,确认session.isRunning为true。 - 确认
previewLayer.session已设置为同一个captureSession,并且previewLayer被添加到视图层级中。 - 模拟器上有时会黑屏,这不代表代码错了。模拟器对AVFoundation支持有限,务必用真机测试。
注意:不要在
viewDidLoad里立刻做session.startRunning(),此时视图还没完全布局好,预览层的位置可能还是零,建议放到viewDidAppear或者等view已经在窗口层级中后再启动。
4.2 画面卡顿和帧率不稳
如果你发现预览画面一卡一卡的,首先要区分是采集端卡还是渲染端卡。可以先关掉所有加在视频帧上的处理逻辑,只保留原始预览,看会不会卡。如果不会,问题就在你的处理链路耗时。比如CoreImage的实时美颜很吃性能,尤其在没有GPU加速的情况下。
一个很常见的优化手段:降级处理分辨率。直播场景不需要处理4K,采集1280x720已足够,这能大幅减少每帧的处理时间。另外,iOS上AVCaptureVideoDataOutput默认是“按需丢弃帧”的,即处理速度跟不上时自动丢帧,如果你反而觉得“更卡了”,检查一下代码里是不是用alwaysDiscardsLateVideoFrames = false把丢帧关掉了,对实时预览来说应该保持为true。
4.3 光线差时画面噪点多、对焦慢
AVFoundation和系统相机一样,依赖曝光和对焦。单反级别的自动对焦算法苹果不一定开放给你单独调用,但你可以自己做几件事:
- 在光线弱时提升ISO增益,接受一点噪点,保证画面可看。
- 通过
focusPointOfInterest = CGPoint(x: 0.5, y: 0.5)让用户点击画面锁定对焦位置。 - 使用
AVCaptureDevice.setExposureTargetBias(_:completionHandler:)做曝光补偿,防止人脸过曝。
做直播类的产品时,我建议额外提供一个“专业模式”开关,让用户手动控制ISO、曝光补偿和白平衡。这看起来是小事,但能明显提升主播的体验,因为直播灯光条件往往不理想。
4.4 前后摄像头切换与设备代理
切换摄像头时有一个常见崩溃:在AVCaptureSession正在运行时直接移除输入设备。正确逻辑是:
session.beginConfiguration() session.removeInput(videoInput) let newDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) videoInput = try? AVCaptureDeviceInput(device: newDevice) session.addInput(videoInput!) session.commitConfiguration()切换后,之前添加到session的输出端不受影响,但connection对象会变,所以之后在拍照、录视频前都要重新获取connection。另外,切换过程中画面会有短暂中断,这是一个预期行为。如果觉得中断太久,可以先用AVCaptureVideoPreviewLayer的session重新绑定,让预览层快速恢复。
我还遇到过一个问题:在做直播时切换前置和后置摄像头,SDK的编码器状态可能没有同步重置,导致切完摄像头后画面花屏。后来发现是对接SDK时,没有通知SDK“输入源发生了切换”,需要在切换后给SDK一个reset信号,或者重新创建编码器。这个问题非常隐蔽,如果你也遇到“切摄像头后就花屏”的情况,优先往这个方向排查。
5. 几个我压箱底的经验技巧
最后再分享几个不容易在文档里看到,但实战非常有用的经验。
第一个是AVCaptureSession的beginConfiguration和commitConfiguration一定要配对出现,这不仅是原则,也是很多奇怪bug的源头。如果你在改session配置时同时做了多步操作,比如同时删输入加输出,不包在这些方法里很容易出现中间状态,轻则动效卡顿,重则session直接崩溃。
第二个是内存监控。视频采集是高内存场景,尤其开了4K后,内存会像坐火箭一样飙升。我在自己的App里会监听APP_ACTIVITY_MEMORY_WARNING通知,收到警告后先停止录制,再逐步释放临时大尺寸缓冲图。对小视频App来说,宁可录制失败,也别让整个App退到后台。
第三个是效率工具的积累。我会把拍摄相关的工具方法沉淀成一个CapturePhotoUtility类,比如生成唯一文件名、清空临时目录、检查磁盘剩余空间、把视频压缩到目标分辨率。这种基础工具在每个音视频项目里都会用到,提前沉淀能节省很多时间。
第四个是推流链路要定期做“真机弱网测试”。直播对弱网的要求非常高。我见过很多团队用了高端配置,结果一上移动网络就卡顿。建议在真机上测试,用Network Link Conditioner模拟不同的网络丢包率、带宽,全链路观察延迟和花屏情况。别只看直播App内的延迟,要关注观众端和推流端的整体体验。
写到这里,我回顾了一下自己从第一次写AVFoundation到现在,最深的感触就是:这套框架门槛不低,但啃下来之后是一劳永逸。拍照、录视频、实时帧处理、推流,都建立在同一个架构上。一旦想通session - input - output的模型,后面很多东西都是触类旁通,上手短视频和直播也只是时间问题。希望这篇内容能帮你少踩几个坑,用更短的时间做出稳定、流畅的相机和直播功能。