iOS13.0 began to support multi-camera preview AVCaptureMultiCamSession, and then iOS15.0 added support for camera picture-in-picture preview. Before using it, we use isMultiCamSupported() to judge whether it supports simultaneous preview of multiple cameras. Let’s see the effect first:
1. Camera Architecture
1. Camera pipeline
Camera consists of AVCaptureDeviceInput, AVCaptureSession, and AVCaptureOutput. As shown below:
2. Single Camera Architecture
A single Camera architecture means that there is only one AVCaptureDeviceInput, which outputs VideoData and DepthData synchronously, and supports preview and output files. As shown below:
3. Multi-Camera architecture
Compared with the single camera architecture, the multi-camera architecture includes multiple input sources AVCaptureDeviceInput, and multiple cameras preview simultaneously, as shown in the following figure:
2. Camera class diagram structure
The Camera class diagram includes AVCaptureDeviceInput, AVCaptureMultiCamSession, AVCaptureVideoDataOutput, AVCaptureVideoPreviewLayer, and AVAssetWriter. As shown below:
3. Camera input and output
Camera input includes: front camera, rear camera, microphone, output includes: preview data, pictures, files, Metadata, managed by AVCaptureMultiCamSession. As shown below:
4. MultiCamera stream synchronization
Multiple cameras preview at the same time, and they share resolution and frame rate. Stream synchronization is also required, including the following:
- exposure
- focus
- white balance
5. Camera picture-in-picture preview
1. Initialization
In the initialization phase, mainly set the preview layer and configure the capture session. The sample code is as follows:
override func viewDidLoad() { super. viewDidLoad() \t\t // Set the front and rear camera preview layers backCameraVideoPreviewView.videoPreviewLayer.setSessionWithNoConnection(session) frontCameraVideoPreviewView.videoPreviewLayer.setSessionWithNoConnection(session) \t\t // configure capture session sessionQueue. async { self. configureSession() } }
2. Configure session
The sample code for configuring the capture session is as follows:
private func configureSession() { guard setupResult == .success else { return } \t\t guard AVCaptureMultiCamSession.isMultiCamSupported else { print("MultiCam not supported on this device") setupResult = .multiCamNotSupported return } session.beginConfiguration() defer { session.commitConfiguration() if setupResult == .success { checkSystemCost() } } guard configureBackCamera() else { setupResult = .configurationFailed return } \t\t guard configureFrontCamera() else { setupResult = .configurationFailed return } }
3. Configure rear camera
The configuration process includes: finding the rear camera, adding it to the session, connecting the input device to the output data, connecting the input device to the preview layer, etc. The sample code is as follows:
private func configureBackCamera() -> Bool { session.beginConfiguration() defer { session.commitConfiguration() } \t\t // Find the rear camera guard let backCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else { print("Could not find the back camera") return false } \t\t // Add post camera to session do { backCameraDeviceInput = try AVCaptureDeviceInput(device: backCamera) \t\t\t guard let backCameraDeviceInput = backCameraDeviceInput, session.canAddInput(backCameraDeviceInput) else { return false } session.addInputWithNoConnections(backCameraDeviceInput) } catch { print("Could not create back camera device input: \(error)") return false } \t\t // Find the rear camera input video port guard let backCameraDeviceInput = backCameraDeviceInput, let backCameraVideoPort = backCameraDeviceInput.ports(for: .video, sourceDeviceType: backCamera.deviceType, sourceDevicePosition: backCamera. position). first else { return false } \t\t // Add post camera to output video data guard session.canAddOutput(backCameraVideoDataOutput) else { print("Could not add the back camera video data output") return false } session.addOutputWithNoConnections(backCameraVideoDataOutput) backCameraVideoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)] backCameraVideoDataOutput.setSampleBufferDelegate(self, queue: dataOutputQueue) \t\t // Connect rear camera input to data output let backCameraVideoDataOutputConnection = AVCaptureConnection(inputPorts: [backCameraVideoPort], output: backCameraVideoDataOutput) guard session.canAddConnection(backCameraVideoDataOutputConnection) else { print("Could not add a connection to the back camera video data output") return false } session.addConnection(backCameraVideoDataOutputConnection) backCameraVideoDataOutputConnection.videoOrientation = .portrait // Connect rear camera input to preview layer guard let backCameraVideoPreviewLayer = backCameraVideoPreviewLayer else { return false } let backCameraVideoPreviewLayerConnection = AVCaptureConnection(inputPort: backCameraVideoPort, videoPreviewLayer: backCameraVideoPreviewLayer) guard session.canAddConnection(backCameraVideoPreviewLayerConnection) else { print("Could not add a connection to the back camera video preview layer") return false } session.addConnection(backCameraVideoPreviewLayerConnection) \t\t return true }
4. Configure the front camera
The configuration process of the front camera is similar to that of the rear camera, except that back is replaced by front. In addition, mirroring is enabled on the front camera. The sample code is as follows:
private func configureFrontCamera() -> Bool { session.beginConfiguration() defer { session.commitConfiguration() } \t\t // Find the front camera guard let frontCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else { print("Could not find the front camera") return false } \t\t // Add front camera to session do { frontCameraDeviceInput = try AVCaptureDeviceInput(device: frontCamera) \t\t\t guard let frontCameraDeviceInput = frontCameraDeviceInput, session.canAddInput(frontCameraDeviceInput) else { return false } session.addInputWithNoConnections(frontCameraDeviceInput) } catch { print("Could not create front camera device input: \(error)") return false } \t\t // Find the front camera input video port guard let frontCameraDeviceInput = frontCameraDeviceInput, let frontCameraVideoPort = frontCameraDeviceInput.ports(for: .video, sourceDeviceType: frontCamera.deviceType, sourceDevicePosition: frontCamera. position). first else { return false } \t\t // Add front camera to output video data guard session.canAddOutput(frontCameraVideoDataOutput) else { print("Could not add the front camera video data output") return false } session.addOutputWithNoConnections(frontCameraVideoDataOutput) \t\t frontCameraVideoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)] frontCameraVideoDataOutput.setSampleBufferDelegate(self, queue: dataOutputQueue) \t\t // Connect front camera input to data output let frontCameraVideoDataOutputConnection = AVCaptureConnection(inputPorts: [frontCameraVideoPort], output: frontCameraVideoDataOutput) guard session.canAddConnection(frontCameraVideoDataOutputConnection) else { print("Could not add a connection to the front camera video data output") return false } session.addConnection(frontCameraVideoDataOutputConnection) frontCameraVideoDataOutputConnection.videoOrientation = .portrait // Connect the front camera input to the preview layer guard let frontCameraVideoPreviewLayer = frontCameraVideoPreviewLayer else { return false } let frontCameraVideoPreviewLayerConnection = AVCaptureConnection(inputPort: frontCameraVideoPort, videoPreviewLayer: frontCameraVideoPreviewLayer) guard session.canAddConnection(frontCameraVideoPreviewLayerConnection) else { print("Could not add a connection to the front camera video preview layer") return false } session.addConnection(frontCameraVideoPreviewLayerConnection) // Turn on mirroring with the front camera frontCameraVideoPreviewLayerConnection.isVideoMirrored = true frontCameraVideoPreviewLayerConnection. automaticallyAdjustsVideoMirroring = false \t\t return true }
5. Configure two-way microphone
In addition to providing a picture-in-picture camera, it also provides front and rear two-way microphones. The sample code is as follows:
private func configureMicrophone() -> Bool { session.beginConfiguration() defer { session.commitConfiguration() } \t\t // find the microphone guard let microphone = AVCaptureDevice.default(for: .audio) else { print("Could not find the microphone") return false } \t\t // add microphone to session do { microphoneDeviceInput = try AVCaptureDeviceInput(device: microphone) \t\t\t guard let microphoneDeviceInput = microphoneDeviceInput, session.canAddInput(microphoneDeviceInput) else { return false } session.addInputWithNoConnections(microphoneDeviceInput) } catch { print("Could not create microphone input: \(error)") return false } \t\t // Find the rear audio port of the input device guard let microphoneDeviceInput = microphoneDeviceInput, let backMicrophonePort = microphoneDeviceInput.ports(for: .audio, sourceDeviceType: microphone.deviceType, sourceDevicePosition: .back).first else { return false } \t\t // Find the front audio port of the input device guard let frontMicrophonePort = microphoneDeviceInput.ports(for: .audio, sourceDeviceType: microphone.deviceType, sourceDevicePosition:.front).first else { return false } \t\t // Add rear microphone to output data guard session.canAddOutput(backMicrophoneAudioDataOutput) else { print("Could not add the back microphone audio data output") return false } session.addOutputWithNoConnections(backMicrophoneAudioDataOutput) backMicrophoneAudioDataOutput.setSampleBufferDelegate(self, queue: dataOutputQueue) \t\t // add front microphone to output data guard session.canAddOutput(frontMicrophoneAudioDataOutput) else { print("Could not add the front microphone audio data output") return false } session.addOutputWithNoConnections(frontMicrophoneAudioDataOutput) frontMicrophoneAudioDataOutput.setSampleBufferDelegate(self, queue: dataOutputQueue) \t\t // connect rear microphone to output data let backMicrophoneAudioDataOutputConnection = AVCaptureConnection(inputPorts: [backMicrophonePort], output: backMicrophoneAudioDataOutput) guard session.canAddConnection(backMicrophoneAudioDataOutputConnection) else { print("Could not add a connection to the back microphone audio data output") return false } session.addConnection(backMicrophoneAudioDataOutputConnection) \t\t // connect front microphone to output data let frontMicrophoneAudioDataOutputConnection = AVCaptureConnection(inputPorts: [frontMicrophonePort], output: frontMicrophoneAudioDataOutput) guard session.canAddConnection(frontMicrophoneAudioDataOutputConnection) else { print("Could not add a connection to the front microphone audio data output") return false } session.addConnection(frontMicrophoneAudioDataOutputConnection) \t\t return true }
6. Reduce power consumption
iOS provides API to get hardware power consumption:
var hardwareCost: Float { get } // value [0.0, 1.0]
At the same time, an API is provided to obtain the system pressure power consumption:
var systemPressureCost: Float { get } // value [0.0, 1.0]
Possible solutions for reducing power consumption are as follows:
- Set the maximum frame rate
- Reduce Camera resolution
- Choose a low-resolution pixel format