如何实现『抖音时刻』的双摄拍照

4,470 阅读8分钟

2022 年 12 月初,抖音推出「抖音时刻」这一新功能,主要以前后置双摄作为主打功能点,打造真实无压力的社交概念,通过前后置摄像头同时拍摄,打造新式拍照方式。

210618-1673342256.png

在 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 之间建立连接。如下图所示:

未命名绘图 (3).png

当使用 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,目前尚处于摇篮阶段,有什么写的不对或不好的,欢迎大家提意见。