4、视频采集与预览:使用AVCaptureSession进行视频采集、配置摄像头与分辨率、实时预览与画面调整
视频采集,说白了就是让摄像头把光信号变成数字信号。iOS 上做这件事,绕不开 AVCaptureSession。我最早接触它的时候,觉得这玩意儿配置起来挺啰嗦的,但用熟了就会发现,它的设计其实很优雅——把采集、预览、输出拆得清清楚楚。
这一章,我们就来聊聊怎么用 AVCaptureSession 搞定视频采集。我会把我在项目中踩过的坑、积累的经验都抖出来。
4.1 AVCaptureSession 的核心架构
先看一张逻辑图,帮你快速建立认知:
AVCaptureSession
├── AVCaptureDeviceInput(输入:摄像头/麦克风)
├── AVCaptureVideoDataOutput(输出:原始视频帧)
├── AVCaptureAudioDataOutput(输出:原始音频数据)
└── AVCaptureVideoPreviewLayer(预览:实时画面)
嗯,结构就这么简单。一个 session 管理输入和输出,中间由系统帮你做数据流转。你只需要告诉它:
- 从哪个设备采集(前置/后置摄像头)
- 输出什么格式(视频帧、音频数据)
- 要不要实时预览(预览层)
我个人习惯把 session 的配置封装成一个方法,这样在多个页面复用时特别方便。你想想看,如果每个页面都写一遍配置代码,那维护起来得多痛苦。
4.2 配置摄像头与分辨率
配置摄像头,第一步是找到可用的设备。iOS 设备上的摄像头种类不少:
| 设备类型 | 说明 | 常见场景 |
|---|---|---|
.builtInWideAngleCamera |
广角摄像头 | 日常拍摄、直播 |
.builtInTelephotoCamera |
长焦摄像头 | 远距离拍摄 |
.builtInUltraWideCamera |
超广角摄像头 | 大场景拍摄 |
.builtInDualCamera |
双摄(广角+长焦) | 人像模式 |
获取设备的代码很简单:
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera,
for: .video,
position: .back) else {
print("后置摄像头不可用")
return
}
这里有个坑——position 参数。你如果指定了 .back,但设备只有前置摄像头(比如某些老款 iPad),那就会返回 nil。我在项目中遇到过这种情况,后来加了个降级逻辑:如果指定位置不可用,就取第一个可用的摄像头。
NSCameraUsageDescription 权限描述,否则会 crash。
4.3 分辨率的选择
分辨率这东西,不是越高越好。你想想看,4K 分辨率虽然清晰,但每帧数据量巨大,处理起来 CPU 和 GPU 都吃不消。我建议根据场景来选:
| 分辨率 | 宽高比 | 推荐场景 |
|---|---|---|
AVCaptureSessionPreset640x480 |
4:3 | 老设备、低功耗场景 |
AVCaptureSessionPreset1280x720 |
16:9 | 直播推流(性价比最高) |
AVCaptureSessionPreset1920x1080 |
16:9 | 高清录制、短视频 |
AVCaptureSessionPreset3840x2160 |
16:9 | 专业级录制(慎用) |
设置分辨率时,记得先检查设备是否支持:
if device.supportsSessionPreset(.hd1920x1080) {
session.sessionPreset = .hd1920x1080
} else {
session.sessionPreset = .hd1280x720
}
我曾经在一个项目里直接写死了 1080p,结果在 iPhone 6 上跑不起来——那台设备最高只支持 720p。嗯,从那以后我就养成了检查支持列表的习惯。
4.4 实时预览的实现
预览层用的是 AVCaptureVideoPreviewLayer,它本质上是一个 CALayer 子类。你把它加到 view 的 layer 上,就能看到摄像头画面了。
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.frame = view.bounds
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
这里有个细节:videoGravity 属性。它决定了画面如何适配预览层的大小:
.resizeAspect:保持宽高比,可能会有黑边.resizeAspectFill:保持宽高比,填满整个区域,可能会裁剪.resize:拉伸画面,不保持比例(一般不推荐)
我个人习惯用 .resizeAspectFill,因为直播场景下,画面铺满屏幕看起来更舒服。但要注意,如果你要做画面上的标注(比如人脸框),坐标需要做转换——预览层的坐标系和实际画面坐标系是不一样的。
previewLayer.connection?.videoOrientation = .portrait 可以修正。
4.5 画面调整:对焦、曝光与白平衡
画面调整是直播场景里的刚需。用户可能从暗处走到亮处,或者想拍清楚某个物体。这些操作都对应着摄像头硬件的调节。
4.5.1 对焦
对焦有两种模式:自动对焦和手动对焦。自动对焦是默认开启的,但如果你想实现「点击屏幕对焦」的效果,就需要手动设置:
func focus(at point: CGPoint) {
guard let device = AVCaptureDevice.default(for: .video) else { return }
do {
try device.lockForConfiguration()
if device.isFocusPointOfInterestSupported {
device.focusPointOfInterest = point
device.focusMode = .autoFocus
}
device.unlockForConfiguration()
} catch {
print("对焦失败: \(error)")
}
}
注意,point 是归一化坐标,范围是 (0,0) 到 (1,1)。你需要把用户点击的屏幕坐标转换一下。
4.5.2 曝光
曝光调整和对焦类似,也是通过 lockForConfiguration 来设置。我建议在直播场景里使用连续自动曝光(.continuousAutoExposure),这样用户从暗处走到亮处时,画面会自动调整亮度。
if device.isExposureModeSupported(.continuousAutoExposure) {
device.exposureMode = .continuousAutoExposure
}
但如果你要做专业相机 App,可能需要手动曝光补偿。我记得有一次做户外直播,阳光太强,自动曝光把画面压得太暗。后来我加了个滑块,让用户可以手动调节曝光补偿值(-2 到 +2),效果好了很多。
4.5.3 白平衡
白平衡决定了画面的色温。自动白平衡在大多数场景下够用,但如果你在混合光源下(比如日光灯+自然光),画面可能会偏色。这时候可以锁定白平衡:
if device.isWhiteBalanceModeSupported(.locked) {
device.whiteBalanceMode = .locked
}
嗯,这里要提醒一下:锁定白平衡后,如果环境光变化,画面颜色不会自动调整。所以一般直播场景还是用 .continuousAutoWhiteBalance 更稳妥。
4.6 完整的采集流程
把上面这些串起来,一个完整的视频采集流程大概是这样的:
// 1. 创建 session
let session = AVCaptureSession()
// 2. 配置输入
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera,
for: .video,
position: .back) else { return }
guard let input = try? AVCaptureDeviceInput(device: device) else { return }
if session.canAddInput(input) {
session.addInput(input)
}
// 3. 配置输出
let videoOutput = AVCaptureVideoDataOutput()
videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "videoQueue"))
if session.canAddOutput(videoOutput) {
session.addOutput(videoOutput)
}
// 4. 配置预览
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.frame = view.bounds
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
// 5. 启动采集
session.startRunning()
这段代码看起来简单,但实际项目中要注意的点不少。比如 startRunning 是一个同步方法,会阻塞当前线程。我建议把它放到后台线程执行,避免卡 UI。
4.7 避坑指南
最后,分享几个我踩过的坑:
- 权限问题: 没有权限时,
AVCaptureDeviceInput初始化会抛异常。记得在调用前检查AVCaptureDevice.authorizationStatus(for: .video)。 - Session 中断: 电话进来、App 切后台时,session 会自动中断。监听
AVCaptureSessionWasInterruptedNotification和AVCaptureSessionInterruptionEndedNotification来做恢复处理。 - 内存泄漏: 如果你在
AVCaptureVideoDataOutput的代理方法里做了耗时操作,记得用autoreleasepool包裹,否则内存会暴涨。
我曾经在一个直播项目里,因为没处理 session 中断,导致用户接完电话后画面卡死。嗯,从那以后,我每次都会加上中断监听和恢复逻辑。
好了,这一章的内容就到这里。视频采集是音视频开发的基石,把 AVCaptureSession 玩熟了,后面的编码、推流才能走得顺。下一章,我们来聊聊音频采集——你会发现,很多思路是相通的。