Multi-Camera Concurrent Mode (ArkTS)
Starting from API version 18, devices support multi-camera concurrent mode, enabling applications to use both front and rear cameras simultaneously for capturing photos and recording videos.
NOTE
Operating both cameras at the same time imposes significant restrictions on available features. The current implementation supports seven core functions, as listed below. When using multi-camera concurrent mode, avoid accessing or enabling any capabilities beyond them.
- Flash
- Exposure
- Zoom
- Exposure compensation
- Focus
- Stabilization
- Color space
How to Develop
Read Camera for the API reference.
For details about how to obtain the context, see Obtaining the Context of UIAbility.
-
Import dependencies related to the camera, media library, and image modules.
import { photoAccessHelper } from '@kit.MediaLibraryKit'; import { camera } from '@kit.CameraKit'; import { media } from '@kit.MediaKit'; import { fileIo } from '@kit.CoreFileKit'; import { common } from '@kit.AbilityKit'; import { BusinessError } from '@kit.BasicServicesKit'; -
Cal getCameraDevice to obtain the front and rear cameras.
function getSupportedCamerasFn(cameraManager: camera.CameraManager) { let cameras = cameraManager.getSupportedCameras(); // Exit if fewer than two cameras are available (multi-camera not supported). if (cameras.length < 2) { return; } // Obtain the logical rear and front cameras. let curCameraDeviceBack = cameraManager.getCameraDevice(camera.CameraPosition.CAMERA_POSITION_BACK, camera.CameraType.CAMERA_TYPE_DEFAULT); let curCameraDeviceFront = cameraManager.getCameraDevice(camera.CameraPosition.CAMERA_POSITION_FRONT, camera.CameraType.CAMERA_TYPE_DEFAULT); } -
Obtain the corresponding concurrent capability set.
Call getCameraConcurrentInfos to obtain an array of CameraConcurrentInfo objects, each of which includes the modes and output capabilities supported by the camera under the corresponding concurrency mode. If an empty array is returned, the current device does not support concurrency mode.
function getSupportedOutputCapabilityFn(cameraManager: camera.CameraManager, curCameraDeviceFront: camera.CameraDevice, curCameraDeviceBack: camera.CameraDevice) { // Check whether the camera supports photo mode and obtain the original capability set. let sceneModes = cameraManager.getSupportedSceneModes(curCameraDeviceFront); if (sceneModes === undefined) { return; } let isSupported = sceneModes.findIndex((sceneMode: camera.SceneMode) => { return sceneMode === camera.SceneMode.NORMAL_PHOTO; }); if (!isSupported) { return; } let cameraOutputCapability = cameraManager.getSupportedOutputCapability(curCameraDeviceFront, camera.SceneMode.NORMAL_PHOTO); let deviceArray: Array<camera.CameraDevice> = [curCameraDeviceFront, curCameraDeviceBack]; // Obtain the concurrency capability set. let concurrentInfo: Array<camera.CameraConcurrentInfo> = cameraManager.getCameraConcurrentInfos(deviceArray); if (concurrentInfo.length === 0) { return; } // Replace the original capability set with the concurrency capability set. for (let i = 0; i < concurrentInfo.length; i++) { if (concurrentInfo[i].device.cameraPosition == camera.CameraPosition.CAMERA_POSITION_FRONT) { cameraOutputCapability = concurrentInfo[i].outputCapabilities[0]; break; } } } -
Determine the preview output stream.
function getPreviewOutputFn(cameraManager: camera.CameraManager, cameraOutputCapability: camera.CameraOutputCapability, surfaceId: string) { // Create a preview output stream using the preview profile with the format 1003 and size 1920*1080 as an example. let previewProfileObj: camera.Profile = { format: 1003, size: { width: 1920, height: 1080 } }; // Check whether the preview profile is supported. let previewProfiles = cameraOutputCapability.previewProfiles; if (previewProfiles.length < 1) { return; } let index = previewProfiles.findIndex((previewProfile: camera.Profile) => { return previewProfile.size.width === previewProfileObj.size.width && previewProfile.size.height === previewProfileObj.size.height && previewProfile.format === previewProfileObj.format; }); if (index === -1) { return; } // Create a previewOutput object. let previewOutput = cameraManager.createPreviewOutput(previewProfileObj, surfaceId); if (previewOutput === undefined) { return; } } -
Determine the photo output stream.
function getPhotoOutputFn(cameraManager: camera.CameraManager, cameraOutputCapability: camera.CameraOutputCapability) { // Create a photo output stream using the photo profile with the format 2000 and size 1920*1080 as an example. let photoProfileObj: camera.Profile = { format: 2000, size: { width: 1920, height: 1080 } }; // Check whether the photo profile is supported. let photoProfiles = cameraOutputCapability.photoProfiles; if (photoProfiles.length < 1) { return; } let index = photoProfiles.findIndex((photoProfile: camera.Profile) => { return photoProfile.size.width === photoProfileObj.size.width && photoProfile.size.height === photoProfileObj.size.height && photoProfile.format === photoProfileObj.format; }); if (index === -1) { return; } let photoOutput = cameraManager.createPhotoOutput(photoProfileObj); if (photoOutput === undefined) { return; } } -
Determine the video output stream.
async function createAVRecorder(): Promise<media.AVRecorder | undefined> { let avRecorder: media.AVRecorder | undefined = undefined; try { avRecorder = await media.createAVRecorder(); } catch (error) { console.error('createAVRecorder error') } return avRecorder; } function initFd(context: common.Context): number { let filesDir = context.filesDir; let filePath = filesDir + `/${Date.now()}.mp4`; AppStorage.setOrCreate<string>('filePath', filePath); let file: fileIo.File = fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE); return file.fd; } async function prepareAVRecorder(videoProfileObj: camera.VideoProfile, curCameraDevice: camera.CameraDevice, avRecorder: media.AVRecorder, context: common.Context): Promise<void> { let fd = initFd(context); let videoConfig: media.AVRecorderConfig = { audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV, profile: { audioBitrate: 48000, audioChannels: 2, audioCodec: media.CodecMimeType.AUDIO_AAC, audioSampleRate: 48000, fileFormat: media.ContainerFormatType.CFT_MPEG_4, videoBitrate: 512000, videoCodec: media.CodecMimeType.VIDEO_AVC, videoFrameWidth: videoProfileObj.size.width, videoFrameHeight: videoProfileObj.size.height, videoFrameRate: videoProfileObj.frameRateRange.min }, url: `fd://${fd.toString()}`, rotation: curCameraDevice?.cameraOrientation }; await avRecorder?.prepare(videoConfig).catch((err: BusinessError): void => { console.error(`prepareAVRecorder prepare err`); }); } async function getVideoOutputFn(cameraManager: camera.CameraManager, cameraOutputCapability: camera.CameraOutputCapability, concurrentInfo: Array<camera.CameraConcurrentInfo>, curCameraDeviceFront: camera.CameraDevice, context: common.Context) { // Create a video output stream using the video profile with the format 1003 and size 1920*1080 as an example. let videoProfileObj: camera.VideoProfile = { format: 1003, size: { width: 1920, height: 1080 }, frameRateRange: { min: 30, max: 60 } }; // Replace the capability set. for (let i = 0; i < concurrentInfo.length; i++) { if (concurrentInfo[i].device.cameraPosition == camera.CameraPosition.CAMERA_POSITION_FRONT) { cameraOutputCapability = concurrentInfo[i].outputCapabilities[1]; break; } } let videoProfiles = cameraOutputCapability.videoProfiles; if (videoProfiles.length < 1) { return; } let index = videoProfiles.findIndex((videoProfile: camera.VideoProfile) => { return videoProfile.size.width === videoProfileObj.size.width && videoProfile.size.height === videoProfileObj.size.height && videoProfile.format === videoProfileObj.format && videoProfile.frameRateRange.min <= 60 && videoProfile.frameRateRange.max <= 60; }); if (index === -1) { return; } videoProfileObj = videoProfiles[index]; let avRecorder = await createAVRecorder(); if (avRecorder === undefined) { return; } await prepareAVRecorder(videoProfileObj, curCameraDeviceFront, avRecorder, context); let videoSurfaceId = await avRecorder.getInputSurface(); let videoOutput = cameraManager.createVideoOutput(videoProfileObj, videoSurfaceId); if (videoOutput === undefined) { return; } } -
Open the camera.
Call open to open the specified camera in multi-camera concurrent mode. Before using this API, check whether the camera supports concurrent capabilities and call getCameraConcurrentInfos to obtain the concurrent capability set in the multi-camera concurrent mode. Do not use open without querying the concurrency capability set, as this will result in camera opening failure.
async function initCamera(cameraManager: camera.CameraManager, cameraDevice: camera.CameraDevice) { let cameraInput: camera.CameraInput | undefined = undefined; try { cameraInput = cameraManager.createCameraInput(cameraDevice); console.info('createCameraInputFn success'); } catch (error) { console.error(`createCameraInputFn failed`); } if (cameraInput === undefined) { return; } let isOpenSuccess = false; try { // The current version supports opening the camera in camera.CameraConcurrentType.CAMERA_LIMITED_CAPABILITY mode. await cameraInput.open(camera.CameraConcurrentType.CAMERA_LIMITED_CAPABILITY); isOpenSuccess = true; } catch (error) { console.error(`createCameraInput failed`); } if (!isOpenSuccess) { return; } } -
Implement the session flow.
// Listen for capture session error changes. function onSessionErrorChange(session: camera.PhotoSession | camera.VideoSession): void { try { session.on('error', (captureSessionError: BusinessError): void => { }); } catch (error) { console.error('onCaptureSessionErrorChange error'); } } let handlePhotoAssetCb: (photoAsset: photoAccessHelper.PhotoAsset) => void = () => { }; // Listen for photo capture events. function photoOutputCallBack(photoOutput: camera.PhotoOutput): void { try { // Listen for capture start events. photoOutput.on('captureStartWithInfo', (err: BusinessError, captureStartInfo: camera.CaptureStartInfo): void => { }); // Listen for frame shutter events. photoOutput.on('frameShutter', (err: BusinessError, frameShutterInfo: camera.FrameShutterInfo): void => { }); // Listen for capture end events. photoOutput.on('captureEnd', (err: BusinessError, captureEndInfo: camera.CaptureEndInfo): void => { }); // Listen for photo capture errors. photoOutput.on('error', (data: BusinessError): void => { }); photoOutput.on('photoAssetAvailable', (err: BusinessError, photoAsset: photoAccessHelper.PhotoAsset) => { if (photoAsset === undefined) { return; } handlePhotoAssetCb(photoAsset); }); } catch (err) { } } // Session flow. async function sessionFlowFn(cameraManager: camera.CameraManager, cameraInput: camera.CameraInput, previewOutput: camera.PreviewOutput, photoOutput: camera.PhotoOutput | undefined, videoOutput: camera.VideoOutput | undefined, curSceneMode: camera.SceneMode): Promise<void> { let session: camera.PhotoSession | camera.VideoSession | undefined = undefined; try { // Create a CaptureSession instance. if (curSceneMode === camera.SceneMode.NORMAL_PHOTO) { session = cameraManager.createSession(curSceneMode) as camera.PhotoSession; } else if (curSceneMode === camera.SceneMode.NORMAL_VIDEO) { session = cameraManager.createSession(curSceneMode) as camera.VideoSession; } if (session === undefined) { return; } onSessionErrorChange(session); // Start configuration for the session. session.beginConfig(); // Add the camera input stream to the session. session.addInput(cameraInput); // Add the preview output stream to the session. session.addOutput(previewOutput); if (curSceneMode === camera.SceneMode.NORMAL_PHOTO) { if (photoOutput === undefined) { return; } // Photo capture event listeners. photoOutputCallBack(photoOutput); // Add the photo output stream to the session. session.addOutput(photoOutput); } else if (curSceneMode === camera.SceneMode.NORMAL_VIDEO) { if (videoOutput === undefined) { return; } // Add the video output stream to the session. session.addOutput(videoOutput); } // Commit the configuration. await session.commitConfig(); } catch (error) { console.error(`sessionFlowFn fail`); } } -
Take a photo using the front or rear camera configured in step 8.
async function takePicture(photoOutput: camera.PhotoOutput): Promise<void> { if (photoOutput === undefined) { return; } if (photoOutput === null) { return; } if (photoOutput) { await photoOutput.capture(); } else { console.info('photoOutput is undefined or null'); } } -
Record a video.
let isRecording = false; // Start recording. async function startVideo(videoOutput: camera.VideoOutput, avRecorder: media.AVRecorder): Promise<void> { try { await videoOutput?.start(); await avRecorder?.start(); } catch (error) { console.error(`startVideo err`); } } // Stop recording. async function stopVideo(videoOutput: camera.VideoOutput, avRecorder: media.AVRecorder): Promise<void> { if (isRecording) { return; } try { if (avRecorder) { await avRecorder.stop(); } if (videoOutput) { await videoOutput.stop(); } isRecording = false; } catch (error) { console.error(`stopVideo err`); } } -
The following provides examples of configurable capabilities for front and rear cameras in multi-camera concurrent mode. (Currently, only the seven core functions listed at the beginning of this document are supported.)
// Flash. function hasFlashFn(flashMode: camera.FlashMode, session: camera.PhotoSession | camera.VideoSession | undefined = undefined): void { // Check whether the camera device has flash. let hasFlash = session?.hasFlash(); // Check whether a flash mode is supported. let isFlashModeSupported = session?.isFlashModeSupported(flashMode); // Set the flash mode. if (isFlashModeSupported) { session?.setFlashMode(flashMode); } } // Exposure. function hasExposureFn(ExposureMode: camera.ExposureMode, session: camera.PhotoSession | camera.VideoSession | undefined = undefined): void { // Check whether an exposure mode is supported. let hasFlash = session?.isExposureModeSupported(ExposureMode); // Set the exposure mode. if (hasFlash) { session?.setExposureMode(ExposureMode); } } // Obtain the zoom ratio range. function getZoomRatioRange(session: camera.PhotoSession | camera.VideoSession | undefined = undefined): Array<number> { let zoomRatioRange: Array<number> = []; if (session !== undefined) { zoomRatioRange = session.getZoomRatioRange(); } return zoomRatioRange; } // Zoom. function setZoomRatioFn(zoomRatio: number, session: camera.PhotoSession | camera.VideoSession | undefined = undefined): void { // Obtain the supported zoom ratio range. let zoomRatioRange = getZoomRatioRange(); try { session?.setZoomRatio(zoomRatio); } catch (error) { console.error(`setZoomRatioFn fail`); } } // Exposure compensation. function setExposureBiasFn(exposureBias: number, session: camera.PhotoSession | camera.VideoSession | undefined = undefined): void { // Obtain the exposure compensation values of the camera device. let biasRangeArray: Array<number> | undefined = []; biasRangeArray = session?.getExposureBiasRange(); // Set an exposure compensation value for the device. session?.setExposureBias(exposureBias); } // Focus mode. function setFocusMode(focusMode: camera.FocusMode, session: camera.PhotoSession | camera.VideoSession | undefined = undefined): void { // Check whether a focus mode is supported. let isSupported = session?.isFocusModeSupported(focusMode); // Set the focus mode. if (!isSupported) { return; } session?.setFocusMode(focusMode); }