创作不易,喜欢的话请点赞收藏转载,您的支持是我更新的最大动力!!!
转眼已经到了 WWDC24,iPhone 的相机功能愈发强大,成为人们记录生活、分享时刻的重要工具。作为 iOS 开发者,掌握相机开发是一门必修课。本文我将结合自己多年来开发相机的经验,来分享相机开发的知识。
权限申请
在使用相机 API 前,需要先申请相机权限。第一步,需要在 info.plist 增加 NSCameraUsageDescription Key,并且提供文案解释为什么需要使用摄像头权限。如下所示:
<dict>
...
<key> NSCameraUsageDescription </key>
<string> This app requires access to your camera to take photos and videos. </string>
...
</dict>
如果少了这一步,在调用相机相关 API 的时候可能会触发崩溃,且在 AppStore 机审阶段被拒审。 接下来,在使用相机相关 API 时,我们都需要先请求下相机权限状态,用户允许调用摄像头的情况下,才能正常使用相机功能,否则,则无法使用。申请权限如下代码所示:
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { (ret) in
DispatchQueue.main.async { [weak self] in
guard let `self` = self else { return }
if ret {
self.setupCamera()
}
}
}
case .authorized:
self.setupCamera()
default: break
}
首次访问相机权限状态时,获取到的权限状态是 .notDetermied,即未定义态,需要调用 AVCaptureDevice 的静态方法 requestAccess(for:completionHandler:) 触发系统弹窗。如下图所示:
授权结果会异步回调给 completionHandler。因此在 completionHandler 里操作 UI 相关时,需要切换到主线程。
如果用户授权成功后,我们就可以开始初始化相机相关的 API。
AVCaptureSession 配置
iOS 提供了 AVCaptureSession,用于协调从摄像头、麦克风等输入设备捕获数据到屏幕或图片上。下面,我们了解下 AVCaptureSession 的能力。
定义分辨率
AVCaptureSession 提供了 sessionPreset 属性来定义摄像头采集数据的分辨率,一般在拍照模式下,使用的是 AVCaptureSessionPresetPhoto,而录屏模式下使用 AVCaptureSessionPresetHigh。具体使用要根据用户的使用场景以及设备的情况去决定,可以考虑开发文档。
使用代码如下所示:
[_session setSessionPreset:AVCaptureSessionPresetPhoto];
协调输入和输出
AVCaptureSession 以流的方式管理输入和输出设备,提供了 addInput 方法用于添加输入设备流,addOuptut 添加输出设备流。如下图所示:
一般在添加之前,需要通过 canAddInput 或者 canAddOutput 方法判断是否允许添加设备流。如下代码所示:
// 添加输入流
if ([_session canAddInput:videoInput]) {
[_session addInput:videoInput];
_videoInput = videoInput;
}
// 添加输出流
if ([_session canAddOutput:_videoOutput]) {
[_session addOutput:_videoOutput];
}
当对应的设备流不再使用,对于输入流,需要通过 removeInput 方法移除。对于输出流,需要通过 removeOutput 方法移除。
后文会更加详细的讲解输入输出流的类别和使用。
生命周期控制
AVCaptureSession 负责启动和停止输入流和输出流工作。一般而言,当输入输出流配置完成后,就可以启动流工作。当相机页不可见时,就需要停止输入输出流工作,为用户节省资源。使用代码如下所示:
// 启动
if (![strongSelf->_session isRunning]) {
[strongSelf->_session startRunning];
}
// 停止
if ([strongSelf->_session isRunning]) {
[strongSelf->_session stopRunning];
}
输入
iOS 提供了 AVCaptureDeviceInput 用于向 AVCaptureSession 提供捕获设备的媒体流。
AVCaptureDeviceInput 的创建方法如下代码所示:
+ (instancetype)deviceInputWithDevice:(AVCaptureDevice *)device error:(NSError * _Nullable *)outError;
- (instancetype)initWithDevice:(AVCaptureDevice *)device error:(NSError * _Nullable *)outError;
AVCaptureDeviceInput 的创建需要有一个可用的 AVCaptureDevice 实例。采集画面数据,自然需要用到摄像头设备。因此,在初始化 input 之前,需要先初始化一个可用的 device。
iPhone 发展到已经到了第 15 代了,摄像头也一直在更新迭代,进化的摄像头的数量和功能也越来越丰富,有超广角、景深等摄像头。iOS 通过镜头组的方式管理设备,摄像头设备之间的调度和使用,由系统处理,对开发者极为友好。
AVCaptureDevice 实例的初始化如下代码所示:
- (AVCaptureDevice *)fetchCameraDeviceWithPosition:(AVCaptureDevicePosition)position
{
AVCaptureDevice *device;
if (position == AVCaptureDevicePositionBack) {
NSArray *deviceType;
if (@available(iOS 13.0, *)) {
deviceType = @[AVCaptureDeviceTypeBuiltInTripleCamera, AVCaptureDeviceTypeBuiltInDualWideCamera, AVCaptureDeviceTypeBuiltInWideAngleCamera];
} else {
deviceType = @[AVCaptureDeviceTypeBuiltInDualCamera, AVCaptureDeviceTypeBuiltInWideAngleCamera];
}
AVCaptureDeviceDiscoverySession *deviceSession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceType mediaType:AVMediaTypeVideo position:position];
device = deviceSession.devices.firstObject;
} else {
AVCaptureDevice *frontDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:position];
device = frontDevice;
}
return device;
}
- 初始化 device 前,需要判断是初始化前置摄像头还是后置摄像头;
- 后置摄像头的功能和数量较为丰富,在 iOS 13 以上(包含 iOS 13),Apple 提供了三摄摄像头,除此之外还有双摄摄像头;
- AVCaptureDeviceDiscoverySession 是系统提供的 API,用于帮助开发者根据当前设备的系统和硬件,查找出可使用且适合的摄像头设备,这有助于开发者减少写 if...else... 这样的啰嗦逻辑,方便后期的维护;
- 前置摄像头是一个普通的广角摄像头,因此一般通过 AVCaptureDevice 的 defaultDeviceWithDeviceType:mediaType:position: 方法找到可用的设备即可。
由于 session 的操作是采用事务的方式,对阻塞当前的调用线程。如果在主线程调用相机相关的 API,可能会导致主线程阻塞。因此,会初始化一个串行队列 sessionQueue,用于处理相机操作。如下代码所示:
// 初始化 _sessionQueue
_sessionQueue = dispatch_queue_create("com.machenshuang.camera.AVCameraSessionQueue", DISPATCH_QUEUE_SERIAL);
// 处理相机
__weak typeof(self)weakSelf = self;
dispatch_async(_sessionQueue, ^{
__strong typeof(weakSelf)strongSelf = weakSelf;
[_session beginConfiguration];
// 添加 input
// 添加 output
[_session commitConfiguration];
});
- session 是以事务的方式调度的,beginConfiguration 和 commitConfiguration 是成对出现的;
当 device 创建完之后,需要通过 input 以事务的方式才可以添加到 session 中去。 如下代码所示:
__weak typeof(self)weakSelf = self;
dispatch_async(_sessionQueue, ^{
__strong typeof(weakSelf)strongSelf = weakSelf;
[_session beginConfiguration];
// 添加 input
[self configureVideoDeviceInput];
// 添加 output
[_session commitConfiguration];
});
- (void)configureVideoDeviceInput
{
if (_videoInput != nil &&_inputCamera != nil && _videoInput.device == _inputCamera) {
return;
}
NSError *error = nil;
// 添加 input
AVCaptureDeviceInput *videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:_inputCamera error:&error];
if (_videoInput) {
[_session removeInput:_videoInput];
}
if ([_session canAddInput:videoInput]) {
[_session addInput:videoInput];
_videoInput = videoInput;
} else {
[_session addInput:_videoInput];
}
}
- 通过 device 初始化新的 input;
- 若 session 存在旧的 input,则建议先通过 session 的 removeInput 移除掉旧 input;
- 在添加 input 前,需要先通过 session 的 canAddInput 方法询问 session 是否可以添加;
- 若 session 可以添加新的 input,则调用 session 的 addInput 添加新 input;
- 若 session 不可以添加新的 input,则重新添加回旧 input,保证原有功能正常运行。
输出
一般而言,摄像头采集的画面数据,session 需要将画面内容输出到屏幕或保存成图片。因此会涉及到两个输出。
输出到 View
将数据输出到屏幕,有两种方式,一种是通过 AVCaptureVideoDataOutput 获取到 CVPixelBuffer,在通过 OpenGL 将其绘制到屏幕上。这种方式相对比较复杂,对开发者要求较高,需要掌握 OpenGL 相关的开发技能。这种方式以后有机会讲 OpenGL 相关内容时再作分享。
另一种方式则是借助系统的能力,这种方式相对简单。了解 CALayer 的读者都知道,iOS 提供了各式各样的 CALayer 用于满足不同的场景需要。其中, AVCaptureVideoPreviewLayer 用于呈现摄像头采集的画面内容。
首先,需要自定义一个 UIView,重写类方法 layerClass,将原本的 CALayer 类型改为 AVCaptureVideoPreviewLayer。如下代码所示:
+ (Class)layerClass
{
return [AVCaptureVideoPreviewLayer class];
}
AVCaptureVideoPreviewLayer 也有一个 AVCaptureSession 属性,只要将关联了 input 的 session,赋值给 AVCaptureVideoPreviewLayer 实例,系统则会自动将摄像头采集的数据推送到 AVCaptureVideoPreviewLayer 所在的 UIView,实现相机预览功能。如下代码所示:
// 赋值 session
- (AVCaptureVideoPreviewLayer *)previewLayer
{
return (AVCaptureVideoPreviewLayer *)self.layer;
}
- (void)setSession:(AVCaptureSession *)session
{
self.previewLayer.session = session;
}
一般情况下,摄像头采集的画面宽高和 View 不一定完全相等,会导致画面变形等奇怪问题。所幸,因此,AVCaptureVideoPreviewLayer 提供了 videoGravity 用于设置画面的填充样式,主要提供了三种选择,分别是 AVLayerVideoGravityResizeAspectFill、AVLayerVideoGravityResize 和 AVLayerVideoGravityResizeAspect。
一般都会将其设置为 AVLayerVideoGravityResizeAspectFill。这样做的好处是不会留黑边,也不会让画面变形,根据 View 的实际比例,去呈现最佳的画面内容。如下代码所示:
self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
输出图片
iOS 提供了 AVCapturePhotoOutput 类型输出,是用于静态图片、动态图片等其他拍照流程的捕获输出。这里主要讲下静态图片的输出。
AVCapturePhotoOutput 的初始化和 input 类似,如下代码所示:
- (void)configurePhotoOutput
{
if (_photoOutput == nil) {
_photoOutput = [AVCapturePhotoOutput new];
[_photoOutput setHighResolutionCaptureEnabled:YES];
if ([_session canAddOutput:_photoOutput]) {
[_session addOutput:_photoOutput];
}
}
}
- highResolutionCaptureEnabled 为 YES,表示以高分辨率获取静态图片;
- 在添加 photoOutput 前,同样需要先调用 session 的 canAddOutput 判断是否允许添加 photoOutput;
- 如果允许添加 photoOutput,则通过 session 的 addOutput 添加 photoOutput。
拍照
拍照是通过调用 photoOutput 的 capturePhotoWithSettings:delegate: 方法触发的,该方法需要传入两个参数。其中一个是 AVCapturePhotoSettings 类型参数,用于定义出图的格式、清晰度、防抖等属性;另一个是 AVCapturePhotoCaptureDelegate 类型参数,用于回调拍照结果数据。
拍照代码如下所示:
- (void)takePhoto
{
__weak typeof(self)weakSelf = self;
dispatch_async(_captureQueue, ^{
__strong typeof(weakSelf)strongSelf = weakSelf;
NSDictionary *dict = @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)};
AVCapturePhotoSettings *setting = [AVCapturePhotoSettings photoSettingsWithFormat:dict];
// 设置高清晰
[setting setHighResolutionPhotoEnabled:YES];
// 防抖
[setting setAutoStillImageStabilizationEnabled:YES];
AVCaptureConnection *photoOutputConnection = [self->_photoOutput connectionWithMediaType:AVMediaTypeVideo];
if (photoOutputConnection) {
photoOutputConnection.videoOrientation = self.orientation;
photoOutputConnection.videoMirrored = self.cameraPosition == AVCaptureDevicePositionFront;
}
if ([strongSelf->_inputCamera hasFlash]) {
[setting setFlashMode:strongSelf->_flashMode];
}
if (@available(iOS 13.0, *)) {
[setting setPhotoQualityPrioritization:AVCapturePhotoQualityPrioritizationBalanced];
}
[strongSelf->_photoOutput capturePhotoWithSettings:setting delegate:self];
});
}
- 创建 AVCapturePhotoSettings 类型参数 setting,用于定义图片属性;
- kCVPixelBufferPixelFormatTypeKey 定义图片的 RGBA 通道排列方式,kCVPixelFormatType_32BGRA 表示是 32位的 BGRA 格式;
- highResolutionPhotoEnabled 默认情况下为 NO,图片的宽高会同 AVCaptureDevice 的 activeFormat.formatDescription 大小一致,即预览大小,若设置为 YES,则大小会以 AVCaptureDevice 的 activeFormat.highResolutionStillImageDimensions 大小一致,如 session 的 sessionPreset 为 AVCaptureSessionPresetPhoto 模式,图片的大小是 4032 x 3024;
- videoOrientation 用于设置当前设备方向,方便后期出图,可以根据图片成像方向作调整;
- 使用前置摄像头时,在默认情况下,拍照后的图片是镜像的,因此需要在设置 setting 的时候,将 videoMirrored 设置为 YES,输出的图片才是预览时看到的样子;
- flashMode 用于设置闪光灯模式;
- 在 iOS 13,photoQualityPrioritization 用于设置图片的产出等级,总共有三类,分别是以出图速度优先的 AVCapturePhotoQualityPrioritizationSpeed,以质量优先的 AVCapturePhotoQualityPrioritizationQuality,以及平衡两者的 AVCapturePhotoQualityPrioritizationBalanced
- 调用 photoOutput 的 capturePhotoWithSettings:delegate: 方法进行拍照。
当调用了 photoOutput 的 capturePhotoWithSettings:delegate: 方法后,图片会通过 AVCapturePhotoCaptureDelegate 的 captureOutput:didFinishProcessingPhoto:error: 方法回调。如下代码所示:
- (void)captureOutput:(AVCapturePhotoOutput *)output didFinishProcessingPhoto:(AVCapturePhoto *)photo error:(NSError *)error
{
if (_delegateCache.cameraDidFinishProcessingPhoto) {
NSData *imageData = [photo fileDataRepresentation];
if (imageData == nil) {
[_delegate cameraDidFinishProcessingPhoto:nil withMetaData:nil withManager:self withError:error];
return;
}
UIImage *image = [[UIImage alloc] initWithData:imageData];
if (image == nil) {
[_delegate cameraDidFinishProcessingPhoto:nil withMetaData:nil withManager:self withError:error];
return;
}
[_delegate cameraDidFinishProcessingPhoto:image withMetaData:photo.metadata withManager:self withError:error];
}
}
- 图片数据存在在回调回来的 AVCapturePhoto 实例中,通过fileDataRepresentation 方法获取图片 Data;
- 将 Data 转换为 UIImage 实例,便可以看到图片内容了。
总结
本文主要是分享了 AV Foundation 中相机的配置、初始化、运行,以及拍照相关知识。先是从权限申请开始。当申请成功后,接下来才能调用 AVCaptureSession 去添加 input 和 output,input 一般是采集摄像头数据的 AVCaptureDeviceInput,output 则是 View 或者图片。当添加 input 和 output 完成后,即可让 session startRunning,向 View 推送画面和拍照。
本文涉及的相关代码可以参考 SwiftyCamera,SwiftyCamera 旨在打造一个易用强大的相机 SDK,目前尚处于摇篮阶段,有什么写的不对或不好的,欢迎大家提意见。