
做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然后调capturePhotoextension 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的模型后面很多东西都是触类旁通上手短视频和直播也只是时间问题。希望这篇内容能帮你少踩几个坑用更短的时间做出稳定、流畅的相机和直播功能。