2022 年 12 月初,抖音推出「抖音时刻」这一新功能,主要以前后置双摄作为主打功能点,打造真实无压力的社交概念,通过前后置摄像头同时拍摄,打造新式拍照方式。
在 WWDC19 的时候,Apple 就将双摄拍照的能力提供给开发者使用,感兴趣的读者可以观看下 Advances in Camera Capture & Portrait Segmentation。下面,笔者将带大家如何利用 AVFoundation 来实现抖音上的双摄拍照功能。
关于过程中相机的开发细节,不理解的读者可以先看看 AV Foundation 相机之拍照 一文,再阅读此文
基本要求
在 iPhone 上实现双摄拍照的能力,Apple 要求 iPhone 在硬件上需要搭载 A12(包含 A12)以上的芯片,在软件上需要升级到 iOS 13(包含 iOS 13)以上的系统。此外,开发者也可以通过 AVCaptureMultiCamSession 的 multiCamSupported 属性判断设备是否支持双摄拍照能力,从而简化繁琐的判断流程。如下代码所示:
+ (BOOL)isMultiCamSupported {
if (@available(iOS 13.0, *)) {
return AVCaptureMultiCamSession.isMultiCamSupported;
} else {
return NO;
}
}
构建 AVCaptureMultiCamSession
在 AV Foundation 相机之拍照一文中,笔者就用到 AVCaptureSession,用于协调 input 向 output 传输采集数据。而在双摄能力上,AVCaptureSession 满足不了,因此 Apple 提供了 AVCaptureMultiCamSession,下文简称 session,用于协调多 input 向多 output 传输采集的数据。
AVCaptureMultiCamSession 的初始化方式如下代码所示:
AVCaptureMultiCamSession *session = [[AVCaptureMultiCamSession alloc] init];
构建双摄 Input
双摄功能需要同时驱动前置和后置摄像头同时采集数据。因此,开发者需要同时获取到前置和后置摄像头,并构建对应 input。
构建前后置摄像头
开发者需要获取前置和后置摄像头,下文简称为 frontDevcie 和 backDevice,如下代码所示:
- (BOOL)setupCameraDevice {
SYLog(TAG, "setupCameraDevice");
_frontDevice = [self fetchCameraDeviceWithPosition:AVCaptureDevicePositionFront];
_backDevice = [self fetchCameraDeviceWithPosition:AVCaptureDevicePositionBack];
if (_frontDevice && _backDevice) {
return YES;
} else {
return NO;
}
}
fetchCameraDeviceWithPosition: 方法是根据传入的 AVCaptureDevicePosition 来获取不同的设备,代码如下所示:
- (AVCaptureDevice *)fetchCameraDeviceWithPosition:(AVCaptureDevicePosition)position {
AVCaptureDevice *device;
if (position == AVCaptureDevicePositionBack) {
NSArray *deviceType;
if (@available(iOS 13.0, *)) {
deviceType = @[AVCaptureDeviceTypeBuiltInTripleCamera, AVCaptureDeviceTypeBuiltInDualWideCamera, AVCaptureDeviceTypeBuiltInWideAngleCamera];
AVCaptureDeviceDiscoverySession *deviceSession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceType mediaType:AVMediaTypeVideo position:position];
device = deviceSession.devices.firstObject;
} 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;
}
构建 AVCaptureDeviceInput
当开发者创建好 frontDevice 和 backDevice,下一步就是创建对应的 AVCaptureDeviceInput 实例,下文简称 frontVideoInput 和 backVideoInput。流程如下代码所示:
- (void)setupVideoDeviceInput {
SYLog(TAG, "setupVideoDeviceInput");
NSError *error = nil;
// 1. 创建 AVCaptureDeviceInput 实例 frontVideoInput
AVCaptureDeviceInput *frontVideoInput = [[AVCaptureDeviceInput alloc] initWithDevice:_frontDevice error:&error];
if (error) {
SYLog(TAG, "setupVideoDeviceInput initWithDevice frontVideoInput failure,error = %@", error.description);
return;
}
// 2. 创建 AVCaptureDeviceInput 实例 backVideoInput
AVCaptureDeviceInput *backVideoInput = [[AVCaptureDeviceInput alloc] initWithDevice:_backDevice error:&error];
if (error) {
SYLog(TAG, "setupVideoDeviceInput initWithDevice backVideoInput failure,error = %@", error.description);
return;
}
// 3. 调用 session 的 canAddInput: 方法判断能否添加 frontVideoInput
if ([self.session canAddInput:frontVideoInput]) {
// 4.调用 session 的 addInputWithNoConnections: 方法添加 frontVideoInput
[self.session addInputWithNoConnections:frontVideoInput];
_frontVideoInput = frontVideoInput;
} else {
SYLog(TAG, "setupVideoDeviceInput addFrontInput failure");
}
// 5. 调用 session 的 canAddInput: 方法判断能否添加 backVideoInput
if ([self.session canAddInput:backVideoInput]) {
// 6.调用 session 的 addInputWithNoConnections: 方法添加 backVideoInput
[self.session addInputWithNoConnections:backVideoInput];
_backVideoInput = backVideoInput;
} else {
SYLog(TAG, "setupVideoDeviceInput addBackInput failure");
}
}
在 AV Foundation 相机之拍照一文中,使用的是 session 的 addInput: 方法添加 input,和这里使用的是 addInputWithNoConnections: 方法添加 input 是有不一样的效果的。
AVCaptureDeviceInput 像一种数据输入源,可以是摄像头、麦克风等设备的数据源,而要将 input 的数据传输给 output,需要在二者之间建立连接。Apple 提供了 AVCaptureConnection 用于在 input 和 output 之间建立连接。如下图所示:
当使用 session 的 addInput: 方法添加 input 后,系统会自己帮助开发者在 input 和 output 间建立连接。而使用 session 的 addInputWithNoConnections: 方法添加 input,则需要开发者再单独与关联的 output 建立连接。这是因为,在双摄这里,涉及到了多个 input 和多个 output 建立连接,需要将 frontVideoInput 和 backVideoInput 采集的数据分别传输到指定的 output 上。
当使用 session 的 addInputWithNoConnections: 方法添加 input 后,后续添加 output 时(除 AVCapturePhotoOutput 外),也需要采取 addOutputWithNoConnections: 类似的方式,手动建立连接。
构建双摄 Output
接下来就是构建接受前后置摄像头采集数据的 Output,这里有两类,分别是 AVCaptureVideoDataOutput 和 AVCaptureVideoPreviewLayer。
构建 AVCaptureVideoDataOutput
AVCaptureVideoDataOutput 可以获取 input 到采集的数据——CMSampleBufferRef,CMSampleBufferRef 可以用来表示视频数据或音频数据,开发者可以对 CMSampleBufferRef 进行修饰后再显示。
双摄功能下,需要构建两个 AVCaptureVideoDataOutput 实例,分别简称为 frontVideoOutput 和 backVideoOutput,流程如下代码所示:
- (void)setupVideoOutput {
// 1.通过 frontVideoInput 和 frontDevice,拿到端口 frontDeviceVideoPort
AVCaptureInputPort *frontDeviceVideoPort = [[_frontVideoInput portsWithMediaType:AVMediaTypeVideo sourceDeviceType:_frontDevice.deviceType sourceDevicePosition:_frontDevice.position] firstObject];
// 2.通过 backVideoInput 和 backDevice,拿到端口 backDeviceVideoPort
AVCaptureInputPort *backDeviceVideoPort = [[_backVideoInput portsWithMediaType:AVMediaTypeVideo sourceDeviceType:_backDevice.deviceType sourceDevicePosition:_backDevice.position] firstObject];
if (_frontVideoOutput == nil) {
// 3.构建前置的 AVCaptureVideoDataOutput 实例 frontVideoOutput
_frontVideoOutput = [[AVCaptureVideoDataOutput alloc] init];
// 4.当 frontVideoOutput 的视频帧出现延迟时是否要丢弃,NO 为不丢弃
[_frontVideoOutput setAlwaysDiscardsLateVideoFrames:NO];
// 5.设置 frontVideoOutput 的视频格式为 32BRGA
[_frontVideoOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
// 6.设置 frontVideoOutput 的视频帧输出队列为 cameraProcessQueue
[_frontVideoOutput setSampleBufferDelegate:self queue:self.cameraProcessQueue];
// 7.调用 session 的 addOutputWithNoConnections: 方法添加 frontVideoOutput
if ([self.session canAddOutput:_frontVideoOutput]) {
[self.session addOutputWithNoConnections:_frontVideoOutput];
} else {
SYLog(TAG, "setupVideoOutput addFrontOutput failure");
}
// 8.基于 frontVideoOutput 和 frontDeviceVideoPort,构建 AVCaptureConnection 的实例 frontInputConnection
AVCaptureConnection *frontInputConnection = [[AVCaptureConnection alloc] initWithInputPorts:@[frontDeviceVideoPort] output:_frontVideoOutput];
// 9.调用 session 的 canAddConnection: 方法判断是否允许加入 frontInputConnection
if ([self.session canAddConnection:frontInputConnection]) {
// 10. 调用 session 的 addConnection: 方法添加 frontInputConnection
[self.session addConnection:frontInputConnection];
// 11. 设置 frontInputConnection 的方向
[frontInputConnection setVideoOrientation:AVCaptureVideoOrientationPortrait];
// 12. 设置 frontInputConnection 的镜像方向
[frontInputConnection setAutomaticallyAdjustsVideoMirroring:NO];
[frontInputConnection setVideoMirrored:YES];
} else {
SYLog(TAG, "setupVideoOutput addFrontConnection failure");
}
}
if (_backVideoOutput == nil) {
// 13.构建前置的 AVCaptureVideoDataOutput 实例 backVideoOutput
_backVideoOutput = [[AVCaptureVideoDataOutput alloc] init];
[_backVideoOutput setAlwaysDiscardsLateVideoFrames:NO];
[_backVideoOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
[_backVideoOutput setSampleBufferDelegate:self queue:self.cameraProcessQueue];
// 14. 调用 session 的 addOutputWithNoConnections: 方法添加 backVideoOutput
if ([self.session canAddOutput:_backVideoOutput]) {
[self.session addOutputWithNoConnections:_backVideoOutput];
} else {
SYLog(TAG, "setupVideoOutput addFrontOutput failure");
}
// 15.基于 backVideoOutput 和 backDeviceVideoPort,构建 AVCaptureConnection 的实例 backInputConnection
AVCaptureConnection *backInputConnection = [[AVCaptureConnection alloc] initWithInputPorts:@[backDeviceVideoPort] output:_backVideoOutput];
// 16.调用 session 的 addConnection: 方法添加 backInputConnection
if ([self.session canAddConnection:backInputConnection]) {
[self.session addConnection:backInputConnection];
[backInputConnection setVideoOrientation:AVCaptureVideoOrientationPortrait];
} else {
SYLog(TAG, "setupVideoOutput addBackConnection failure");
}
}
}
一个 AVCaptureDeviceInput 有一个或多个 AVCaptureInputPort,AVCaptureInputPort 相当于输入数据源上的输入端口,AVCaptureInputPort 封装了数据的采样率、媒体类型和格式等属性。选择不同的 AVCaptureInputPort,获取的数据也会不一样。
构建 AVCaptureConnection 实例的时候,会传入 input 对应 device 的 inputPort 和 output,从而在 input 和 output 之间建立连接,传输数据。
构建 AVCaptureVideoPreviewLayer
AVCaptureVideoPreviewLayer 是用于在 UIView 呈现视频数据的 Output,双摄功能下,需要创建两个 AVCaptureVideoPreviewLayer 实例,分别为 frontPreviewLayer 和 backPreviewLayer。流程如下代码所示:
// 1.使用 setSessionWithNoConnection: 给 frontPreviewLayer 和 backPreviewLayer 设置 session
[frontPreviewLayer setSessionWithNoConnection:self.session];
[backPreviewLayer setSessionWithNoConnection:self.session];
// 2.构建 frontPreviewLayer 的 AVCaptureConnection 的 frontVideoPreviewLayerConnection
__block AVCaptureConnection *frontVideoPreviewLayerConnection;
__weak typeof(self) weakSelf = self;
dispatch_sync(dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
frontVideoPreviewLayerConnection = [[AVCaptureConnection alloc] initWithInputPort:frontDeviceVideoPort videoPreviewLayer:frontPreviewLayer];
});
// 3. 调用 session 的 addConnection: 方法添加 frontVideoPreviewLayerConnection
if ([self.session canAddConnection:frontVideoPreviewLayerConnection]) {
[self.session addConnection:frontVideoPreviewLayerConnection];
} else {
SYLog(TAG, "setupVideoOutput addFrontPreviewConnection failure");
}
// 4.构建 backPreviewLayer 的 AVCaptureConnection 的 backVideoPreviewLayerConnection
__block AVCaptureConnection *backVideoPreviewLayerConnection;
__weak typeof(self) weakSelf = self;
dispatch_sync(dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
backVideoPreviewLayerConnection = [[AVCaptureConnection alloc] initWithInputPort:backDeviceVideoPort videoPreviewLayer:backPreviewLayer];
});
// 5. 调用 session 的 addConnection: 方法添加 backVideoPreviewLayerConnection
if ([self.session canAddConnection:backVideoPreviewLayerConnection]) {
[self.session addConnection:backVideoPreviewLayerConnection];
} else {
SYLog(TAG, "setupVideoOutput addbackPreviewConnection failure");
}
拍照
双摄的拍照,也需要构建两个 AVCapturePhotoOutput 实例,分别简称为 frontPhotoOutput 和 backPhotoOutput。具体流程如下代码所示:
- (void)setupPhotoOutput {
if (_frontPhotoOutput == nil) {
_frontPhotoOutput = [AVCapturePhotoOutput new];
[_frontPhotoOutput setHighResolutionCaptureEnabled:YES];
if ([self.session canAddOutput:_frontPhotoOutput]) {
[self.session addOutput:_frontPhotoOutput];
} else {
SYLog(TAG, "setupPhotoOutput addFrontOutput failure");
}
}
if (_backPhotoOutput == nil) {
_backPhotoOutput = [AVCapturePhotoOutput new];
[_backPhotoOutput setHighResolutionCaptureEnabled:YES];
if ([self.session canAddOutput:_backPhotoOutput]) {
[self.session addOutput:_backPhotoOutput];
} else {
SYLog(TAG, "setupPhotoOutput addBackOutput failure");
}
}
}
当双摄进行拍照的时候,可以采取先让一个 photoOutput 拍摄完后,再让另一个 photoOutput 拍摄,下面的代码是先用 backPhotoOutput 拍摄,再用 frontPhotoOutput 拍摄:
// backPhotoOutput 拍照封装
- (void)takeBackCameraPhoto {
NSDictionary *dict = @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)};
AVCapturePhotoSettings *setting = [AVCapturePhotoSettings photoSettingsWithFormat:dict];
// 设置高清晰
[setting setHighResolutionPhotoEnabled:YES];
if ([self->_backDevice hasFlash]) {
[setting setFlashMode:self.flashMode];
}
[self->_backPhotoOutput capturePhotoWithSettings:setting delegate:self];
}
// frontPhotoOutput 拍照封装
- (void)takeFrontCameraPhoto {
NSDictionary *dict = @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)};
AVCapturePhotoSettings *setting = [AVCapturePhotoSettings photoSettingsWithFormat:dict];
// 设置高清晰
[setting setHighResolutionPhotoEnabled:YES];
if ([self->_frontDevice hasFlash]) {
[setting setFlashMode:self.flashMode];
}
AVCaptureConnection *frontPhotoOutputConnection = [self->_frontPhotoOutput connectionWithMediaType:AVMediaTypeVideo];
if (frontPhotoOutputConnection) {
frontPhotoOutputConnection.videoMirrored = YES;
}
[self->_frontPhotoOutput capturePhotoWithSettings:setting delegate:self];
}
// 1. 先用 backPhotoOutput 拍照
[self takeBackCameraPhoto];
// 2.在 AVCapturePhotoCaptureDelegate 的 captureOutput:didFinishProcessingPhoto:error: 方法中获取 backPhotoOutput 的照片
- (void)captureOutput:(AVCapturePhotoOutput *)output didFinishProcessingPhoto:(AVCapturePhoto *)photo error:(NSError *)error {
// 处理第一张图片 ...
// 3. 再用 frontPhotoOutput 拍照
[self takeFrontCameraPhoto];
}
总结
双摄功能并不是所有 iPhone 设备都能使用,只有搭载 A12 芯片和 iOS 13 系统的设备才能使用 Apple 提供的双摄能力。双摄依赖的 session 是 AVCaptureMultiCamSession,用于协调多个 input 和多个 output 传输数据。本文先是构建了两个 AVCaptureDevcie 实例 frontDevice 和 backDevice,基于 frontDevice 和 backDevice 构建 AVCaptureDeviceInput 实例 frontVideoInput 和 backVideoInput 用于采集数据。接着构建两个 AVCaptureVideoDataOutput 实例 frontVideoOutput 和 backVideoOutput,和两个 AVCaptureVideoPreviewLayer 实例 frontPreviewLayer 和 backPreviewLayer。最后,将这些 inputs 和 outputs,通过自定义 AVCaptureConnection 的方式建立连接,实现数据传输。
双摄的拍照,同样需要构建两个 AVCapturePhotoOutput,然后分别进行拍照。
本文涉及的相关代码可以参考 SwiftyCamera,SwiftyCamera 旨在打造一个易用强大的相机 SDK,目前尚处于摇篮阶段,有什么写的不对或不好的,欢迎大家提意见。