From da647024600e6151344a98ab918c07cd93291a4e Mon Sep 17 00:00:00 2001 From: sunchao Date: Fri, 4 Jul 2025 15:57:40 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=8F=8C=E8=B7=AF?= =?UTF-8?q?=E9=A2=84=E8=A7=88=E5=9B=BE=E5=83=8F=E5=A4=84=E7=90=86format?= =?UTF-8?q?=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: sunchao --- .../camera/camera-dual-channel-preview.md | 40 ++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/zh-cn/application-dev/media/camera/camera-dual-channel-preview.md b/zh-cn/application-dev/media/camera/camera-dual-channel-preview.md index 64afa772722..9024a5b0de6 100644 --- a/zh-cn/application-dev/media/camera/camera-dual-channel-preview.md +++ b/zh-cn/application-dev/media/camera/camera-dual-channel-preview.md @@ -55,6 +55,10 @@ 3. 注册监听处理预览流每帧图像数据:通过ImageReceiver组件中imageArrival事件监听获取底层返回的图像数据,详细的API说明请参考[Image API参考](../../reference/apis-image-kit/js-apis-image.md)。 +> **说明:** +> +> ImageReceiver组件中解析图像数据设置的Size、Format等属性必须和相机预览输出流previewProfile中配置的Size、Format保持一致,ImageRecevier图片像素格式请参考[PixelMapFormat](../../reference/apis-image-kit/js-apis-image.md#pixelmapformat7),相机预览输出流previewProfile输出格式请参考[CameraFormat](../../reference/apis-camera-kit/arkts-apis-camera-e.md#cameraformat)。 + ```ts function onImageArrival(receiver: image.ImageReceiver): void { // 注册imageArrival监听。 @@ -79,8 +83,9 @@ // stride与width一致。 if (stride == width) { let pixelMap = await image.createPixelMap(imgComponent.byteBuffer, { + // pixelMap创建的size、srcPixelFormat需要与相机预览输出流previewProfile中的size、format保持一致。 size: { height: height, width: width }, - srcPixelFormat: 8, + srcPixelFormat: image.PixelMapFormat.RGBA_8888, }) } else { // stride与width不一致。 @@ -91,8 +96,9 @@ dstArr.set(srcBuf, j * width) } let pixelMap = await image.createPixelMap(dstArr.buffer, { + // pixelMap创建的size、srcPixelFormat需要与相机预览输出流previewProfile中的size、format保持一致。 size: { height: height, width: width }, - srcPixelFormat: 8, + srcPixelFormat: image.PixelMapFormat.NV21, }) } } else { @@ -125,7 +131,8 @@ dstArr.set(srcBuf, j * width); } let pixelMap = await image.createPixelMap(dstArr.buffer, { - size: { height: height, width: width }, srcPixelFormat: 8 + // pixelMap创建使用的size、srcPixelFormat需要与相机预览输出流previewProfile中的size、format保持一致。 + size: { height: height, width: width }, srcPixelFormat: image.PixelMapFormat.NV21 }); ``` @@ -134,7 +141,8 @@ ```ts // 创建pixelMap,width宽传行距stride的值。 let pixelMap = await image.createPixelMap(imgComponent.byteBuffer, { - size:{height: height, width: stride}, srcPixelFormat: 8}); + // pixelMap创建使用的size、srcPixelFormat需要与相机预览输出流previewProfile中的size、format保持一致。 + size:{height: height, width: stride}, srcPixelFormat: image.PixelMapFormat.NV21}); // 裁剪多余的像素。 pixelMap.cropSync({size:{width:width, height:height}, x:0, y:0}); ``` @@ -305,12 +313,14 @@ struct Index { // stride与width一致。 if (stride == width) { let pixelMap = await image.createPixelMap(imgComponent.byteBuffer, { + // pixelMap创建使用的size、srcPixelFormat需要与相机预览输出流previewProfile中的size、format保持一致。 size: { height: height, width: width }, - srcPixelFormat: 8, + srcPixelFormat: image.PixelMapFormat.NV21, }) } else { // stride与width不一致。 - const dstBufferSize = width * height * 1.5 // 以NV21为例(YUV_420_SP格式的图片)YUV_420_SP内存计算公式:长x宽+(长x宽)/2。 + // 以NV21为例(YUV_420_SP格式的图片)YUV_420_SP内存计算公式:长x宽+(长x宽)/2。 + const dstBufferSize = width * height * 1.5 const dstArr = new Uint8Array(dstBufferSize) for (let j = 0; j < height * 1.5; j++) { const srcBuf = new Uint8Array(imgComponent.byteBuffer, j * stride, width) @@ -318,7 +328,8 @@ struct Index { } let pixelMap = await image.createPixelMap(dstArr.buffer, { size: { height: height, width: width }, - srcPixelFormat: 8, + // pixelMap创建使用的size、srcPixelFormat需要与相机预览输出流previewProfile中的size、format保持一致。 + srcPixelFormat: image.PixelMapFormat.NV21, }) } } else { @@ -384,9 +395,20 @@ struct Index { this.cameraManager.getSupportedOutputCapability(this.cameras[0], camera.SceneMode.NORMAL_VIDEO); if (!capability) { console.error('initCamera getSupportedOutputCapability'); + } + let surfaceRatio : number = this.imageWidth / this.imageHeight; + let minRatioDiff : number = 0.01; + for (let index = 0; index < capability.previewProfiles.length; index++) { + const tempProfile = capability.previewProfiles[index]; + let tempRatio = tempProfile.size.width >= tempProfile.size.height ? + tempProfile.size.width / tempProfile.size.height : tempProfile.size.height / tempProfile.size.wight; + let currentRatio = Math.abs(tempRatio - surfacerRatio); + // 根据业务需求选择一个支持的预览流profile,此处以CAMERA_FORMAT_YUV_420_SP(NV21)为例。 + // 此处选择最接近16:9宽高比的分辨率Size。 + if (currentRatio <= 0.01 && tempProfile.format = camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP) { + previeProfile = tempProfile; + } } - // 根据业务需求选择一个支持的预览流profile。 - let previewProfile: camera.Profile = capability.previewProfiles[0]; this.imageWidth = previewProfile.size.width; // 更新xComponent组件的宽。 this.imageHeight = previewProfile.size.height; // 更新xComponent组件的高。 console.info(`initCamera imageWidth:${this.imageWidth} imageHeight:${this.imageHeight}`); -- Gitee From b73e43fdf8ad421d797c7b4a005624f82258e1ba Mon Sep 17 00:00:00 2001 From: sunchao106 Date: Wed, 23 Jul 2025 20:44:39 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E6=91=84=E5=83=8F=E5=A4=B4=E5=AE=9E=E8=B7=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: sunchao106 --- .../application-dev/media/camera/Readme-CN.md | 1 + .../media/camera/camera-auto-switch.md | 401 ++++++++++++++++++ 2 files changed, 402 insertions(+) create mode 100644 zh-cn/application-dev/media/camera/camera-auto-switch.md diff --git a/zh-cn/application-dev/media/camera/Readme-CN.md b/zh-cn/application-dev/media/camera/Readme-CN.md index 42b55b878f8..47880662508 100644 --- a/zh-cn/application-dev/media/camera/Readme-CN.md +++ b/zh-cn/application-dev/media/camera/Readme-CN.md @@ -22,6 +22,7 @@ - [相机基础动效(ArkTS)](camera-animation.md) - [在Worker线程中使用相机(ArkTS)](camera-worker.md) - [相机启动恢复实践(ArkTS)](camera-background-recovery.md) + - [自动切换摄像头实践](camera-auto-switch.md) - [高性能拍照(仅对系统应用开放)(ArkTS)](camera-deferred-photo.md) - [高性能拍照实践(仅对系统应用开放)(ArkTS)](camera-deferred-photo-case.md) diff --git a/zh-cn/application-dev/media/camera/camera-auto-switch.md b/zh-cn/application-dev/media/camera/camera-auto-switch.md new file mode 100644 index 00000000000..cdb6a46c542 --- /dev/null +++ b/zh-cn/application-dev/media/camera/camera-auto-switch.md @@ -0,0 +1,401 @@ +# 自动切换摄像头实践(ArkTS) + +本文档只针折叠屏设备下自动切换前置摄像头的场景。在不同折叠状态下自动切换到当前折叠状态下支持的摄像头。 +例如:折叠设备A拥有三颗摄像头:B(后置)、C(前置)、D(前置)。在展开状态下,通过[CameraManager.getSupportedCameras](../../reference/apis-camera-kit/arkts-apis-camera-CameraManager.md#getsupportedcameras)接口可获取到B(后置)和C(前置)两颗摄像头,而在折叠状态下,可获取到B(后置)和D(前置)摄像头。 +在打开当前折叠状态下支持的前置摄像头,并且调用[enableAutoDeviceSwitch](../../reference/apis-camera-kit/arkts-apis-camera-AutoDeviceSwitch.md#enableautodeviceswitch13)开启自动切换镜头下,可以实现下次折叠屏状态变化的时候会自动切换到对应折叠状态下的前置摄像头。 + +详细的API说明请参考[Camera API参考](../../reference/apis-camera-kit/arkts-apis-camera.md)。 + +Context获取方式请参考:[获取UIAbility的上下文信息](../../application-models/uiability-usage.md#获取uiability的上下文信息)。 + +在开发相机应用时,需要先参考开发准备[申请相关权限](camera-preparation.md)。 + +## 导入相关依赖 +```ts +import { camera } from '@kit.CameraKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { abilityAccessCtrl } from '@kit.AbilityKit'; +``` +## 创建XComponent +使用[XComponent](../../reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md)展示摄像头的预览画面。 +```ts +@Entry +@Component +struct Index { + private mXComponentController: XComponentController = new XComponentController(); + private mXComponentOptions: XComponentOptions = { + type: XComponentType.SURFACE, + controller: this.mXComponentController + } + + async loadXComponent() { + //初始化XComponent。 + } + + build() { + Stack() { + XComponent(this.mXComponentOptions) + .onLoad(async () => { + await this.loadXComponent(); + }) + .width(this.getUIContext().px2vp(1080)) + .height(this.getUIContext().px2vp(1920)) + Text('切换相机') + .size({ width: 80, height: 48 }) + .position({ x: 1, y: 1 }) + .backgroundColor(Color.White) + .textAlign(TextAlign.Center) + .borderRadius(24) + .onClick(async () => { + this.mCameraPosition = this.mCameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK ? + camera.CameraPosition.CAMERA_POSITION_FRONT : camera.CameraPosition.CAMERA_POSITION_BACK; + await this.loadXComponent(); + }) + } + .size({ width: '100%', height: '100%' }) + .backgroundColor(Color.Black) + } +} +``` +## 开启自动切换摄像头 +调用[enableAutoDeviceSwitch](../../reference/apis-camera-kit/arkts-apis-camera-AutoDeviceSwitch.md#enableautodeviceswitch13)接口前需要通过 +[isAutoDeviceSwitchSupported](../../reference/apis-camera-kit/arkts-apis-camera-AutoDeviceSwitchQuery.md#isautodeviceswitchsupported13)接口查询当前设备是否支持自动切换摄像头能力。 +```ts +function enableAutoDeviceSwitch(session: camera.PhotoSession) { + if (session.isAutoDeviceSwitchSupported()) { + session.enableAutoDeviceSwitch(true); + } +} +``` +## 监听或解监听自动切换摄像头状态 +可以通过[enableAutoDeviceSwitch](../../reference/apis-camera-kit/arkts-apis-camera-PhotoSession.md#onautodeviceswitchstatuschange13)监听自动切换摄像头的结果。 +在自动切换摄像头期间不允许做任何session相关的接口调用。 +```ts +function callback(err: BusinessError, autoDeviceSwitchStatus: camera.AutoDeviceSwitchStatus): void { + if (err !== undefined && err.code !== 0) { + console.error(`Callback Error, errorCode: ${err.code}`); + return; + } + console.info(`isDeviceSwitched: ${autoDeviceSwitchStatus.isDeviceSwitched}, isDeviceCapabilityChanged: ${autoDeviceSwitchStatus.isDeviceCapabilityChanged}`); +} + +function registerAutoDeviceSwitchStatus(photoSession: camera.PhotoSession): void { + photoSession.on('autoDeviceSwitchStatusChange', callback); +} +function unregisterAutoDeviceSwitchStatus(photoSession: camera.PhotoSession): void { + photoSession.off('autoDeviceSwitchStatusChange', callback); +} +``` + +## 完整示例代码 +```ts +import { camera } from '@kit.CameraKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { abilityAccessCtrl } from '@kit.AbilityKit'; + +const TAG = 'AutoSwitchCameraDemo '; + +@Entry +@Component +struct Index { + @State isShow: boolean = false; + @State reloadXComponentFlag: boolean = false; + private mXComponentController: XComponentController = new XComponentController(); + private mXComponentOptions: XComponentOptions = { + type: XComponentType.SURFACE, + controller: this.mXComponentController + } + private mSurfaceId: string = ''; + private mCameraPosition: camera.CameraPosition = camera.CameraPosition.CAMERA_POSITION_BACK; + private mCameraManager: camera.CameraManager | undefined = undefined; + // surface宽高根据需要自行选择。 + private surfaceRect: SurfaceRect = { + surfaceWidth: 1080, + surfaceHeight: 1920 + }; + private curCameraDevice: camera.CameraDevice | undefined = undefined; + private mCameraInput: camera.CameraInput | undefined = undefined; + private mPreviewOutput: camera.PreviewOutput | undefined = undefined; + private mPhotoSession: camera.PhotoSession | undefined = undefined; + // One of the recommended preview resolutions. + private previewProfileObj: camera.Profile = { + format: 1003, + size: { + width: 1920, + height: 1080 + } + }; + private mContext: Context | undefined = undefined; + autoDeviceSwitchCallback: (err: BusinessError, autoDeviceSwitchStatus: camera.AutoDeviceSwitchStatus) => void = + (err: BusinessError, autoDeviceSwitchStatus: camera.AutoDeviceSwitchStatus) => { + if (err !== undefined && err.code !== 0) { + console.error(`${TAG} Callback Error, errorCode: ${err.code}`); + return; + } + console.info(`${TAG} isDeviceSwitched: ${autoDeviceSwitchStatus.isDeviceSwitched}, isDeviceCapabilityChanged: ${autoDeviceSwitchStatus.isDeviceCapabilityChanged}`); + } + + requestPermissionsFn(): void { + let atManager = abilityAccessCtrl.createAtManager(); + atManager.requestPermissionsFromUser(this.mContext, [ + 'ohos.permission.CAMERA' + ]).then((): void => { + this.isShow = true; + }).catch((error: BusinessError): void => { + console.error(TAG + 'ohos.permission.CAMERA no permission.'); + }); + } + + initContext(): void { + let uiContext = this.getUIContext(); + this.mContext = uiContext.getHostContext(); + } + + initCameraManager(): void { + this.mCameraManager = camera.getCameraManager(this.mContext); + } + + aboutToAppear(): void { + console.log(TAG + 'aboutToAppear is called'); + this.initContext(); + this.requestPermissionsFn(); + this.initCameraManager(); + } + + async aboutToDisappear(): Promise { + await this.releaseCamera(); + } + + async onPageShow(): Promise { + await this.initCamera(this.mSurfaceId, this.mCameraPosition); + } + + async releaseCamera(): Promise { + // 停止当前会话。 + try { + await this.mPhotoSession?.stop(); + } catch (error) { + let err = error as BusinessError; + console.error(TAG + 'Failed to stop session, errorCode = ' + err.code); + } + + // 释放相机输入流。 + try { + await this.mCameraInput?.close(); + } catch (error) { + let err = error as BusinessError; + console.error(TAG + 'Failed to close device, errorCode = ' + err.code); + } + + // 释放预览输出流。 + try { + await this.mPreviewOutput?.release(); + } catch (error) { + let err = error as BusinessError; + console.error(TAG + 'Failed to release previewOutput, errorCode = ' + err.code); + } + + this.mPreviewOutput = undefined; + + // 释放会话。 + try { + await this.mPhotoSession?.release(); + } catch (error) { + let err = error as BusinessError; + console.error(TAG + 'Failed to release photoSession, errorCode = ' + err.code); + } + + // 会话置空。 + this.mPhotoSession = undefined; + } + + async loadXComponent(): Promise { + this.mSurfaceId = this.mXComponentController.getXComponentSurfaceId(); + console.info(TAG + `mCameraPosition: ${this.mCameraPosition}`) + await this.initCamera(this.mSurfaceId, this.mCameraPosition); + } + + getPreviewProfile(cameraOutputCapability: camera.CameraOutputCapability): camera.Profile | undefined { + let previewProfiles = cameraOutputCapability.previewProfiles; + if (previewProfiles.length < 1) { + return undefined; + } + let index = previewProfiles.findIndex((previewProfile: camera.Profile) => { + return previewProfile.size.width === this.previewProfileObj.size.width && + previewProfile.size.height === this.previewProfileObj.size.height && + previewProfile.format === this.previewProfileObj.format; + }) + if (index === -1) { + return undefined; + } + return previewProfiles[index]; + } + + async initCamera(surfaceId: string, cameraPosition: camera.CameraPosition, + connectionType: camera.ConnectionType = camera.ConnectionType.CAMERA_CONNECTION_BUILT_IN): Promise { + await this.releaseCamera(); + // 创建CameraManager对象。 + if (!this.mCameraManager) { + console.error(TAG + 'camera.getCameraManager error'); + return; + } + + // 获取相机列表。 + let cameraArray: Array = this.mCameraManager.getSupportedCameras(); + if (cameraArray.length <= 0) { + console.error(TAG + 'cameraManager.getSupportedCameras error'); + return; + } + + for (let index = 0; index < cameraArray.length; index++) { + console.info(TAG + 'cameraId : ' + cameraArray[index].cameraId); // 获取相机ID。 + console.info(TAG + 'cameraPosition : ' + cameraArray[index].cameraPosition); // 获取相机位置。 + console.info(TAG + 'cameraType : ' + cameraArray[index].cameraType); // 获取相机类型。 + console.info(TAG + 'connectionType : ' + cameraArray[index].connectionType); // 获取相机连接类型。 + } + + let deviceIndex = cameraArray.findIndex((cameraDevice: camera.CameraDevice) => { + return cameraDevice.cameraPosition === cameraPosition && cameraDevice.connectionType === connectionType; + }) + // 没有找到对应位置的摄像头,可选择其他摄像头,具体场景具体对待。 + if (deviceIndex === -1) { + deviceIndex = 0; + console.error(TAG + 'not found camera'); + } + this.curCameraDevice = cameraArray[deviceIndex]; + + // 创建相机输入流。 + try { + this.mCameraInput = this.mCameraManager.createCameraInput(this.curCameraDevice); + } catch (error) { + let err = error as BusinessError; + console.error(TAG + 'Failed to createCameraInput errorCode = ' + err.code); + } + if (this.mCameraInput === undefined) { + return; + } + + // 打开相机。 + try { + await this.mCameraInput.open(); + } catch (error) { + let err = error as BusinessError; + console.error(TAG + 'Failed to open device, errorCode = ' + err.code); + } + + // 获取支持的模式类型。 + let sceneModes: Array = this.mCameraManager.getSupportedSceneModes(this.curCameraDevice); + let isSupportPhotoMode: boolean = sceneModes.indexOf(camera.SceneMode.NORMAL_PHOTO) >= 0; + if (!isSupportPhotoMode) { + console.error(TAG + 'photo mode not support'); + return; + } + + // 获取相机设备支持的输出流能力。 + let cameraOutputCapability: camera.CameraOutputCapability = + this.mCameraManager.getSupportedOutputCapability(this.curCameraDevice, camera.SceneMode.NORMAL_PHOTO); + if (!cameraOutputCapability) { + console.error(TAG + 'cameraManager.getSupportedOutputCapability error'); + return; + } + console.info(TAG + 'outputCapability: ' + JSON.stringify(cameraOutputCapability)); + let previewProfile = this.getPreviewProfile(cameraOutputCapability); + if (previewProfile === undefined) { + console.error(TAG + 'The resolution of the current preview stream is not supported.'); + return; + } + this.previewProfileObj = previewProfile; + + // 创建预览输出流,其中参数 surfaceId 参考上文 XComponent 组件,预览流为XComponent组件提供的surface。 + try { + this.mPreviewOutput = this.mCameraManager.createPreviewOutput(this.previewProfileObj, surfaceId); + } catch (error) { + let err = error as BusinessError; + console.error(TAG + `Failed to create the PreviewOutput instance. error code: ${err.code}`); + } + if (this.mPreviewOutput === undefined) { + return; + } + + //创建会话。 + try { + this.mPhotoSession = this.mCameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession; + } catch (error) { + let err = error as BusinessError; + console.error(TAG + 'Failed to create the session instance. errorCode = ' + err.code); + } + if (this.mPhotoSession === undefined) { + return; + } + if (this.mPhotoSession.isAutoDeviceSwitchSupported()) { + this.mPhotoSession.enableAutoDeviceSwitch(true); + this.mPhotoSession.on('autoDeviceSwitchStatusChange', this.autoDeviceSwitchCallback); + } + // 开始配置会话。 + try { + this.mPhotoSession.beginConfig(); + } catch (error) { + let err = error as BusinessError; + console.error(TAG + 'Failed to beginConfig. errorCode = ' + err.code); + } + + // 向会话中添加相机输入流。 + try { + this.mPhotoSession.addInput(this.mCameraInput); + } catch (error) { + let err = error as BusinessError; + console.error(TAG + 'Failed to addInput. errorCode = ' + err.code); + } + + // 向会话中添加预览输出流。 + try { + this.mPhotoSession.addOutput(this.mPreviewOutput); + } catch (error) { + let err = error as BusinessError; + console.error(TAG + 'Failed to addOutput(previewOutput). errorCode = ' + err.code); + } + + // 提交会话配置。 + try { + await this.mPhotoSession.commitConfig(); + } catch (error) { + let err = error as BusinessError; + console.error(TAG + 'Failed to commit session configuration, errorCode = ' + err.code); + } + + // 启动会话。 + try { + await this.mPhotoSession.start() + } catch (error) { + let err = error as BusinessError; + console.error(TAG + 'Failed to start session. errorCode = ' + err.code); + } + } + + build() { + if (this.isShow) { + Stack() { + XComponent(this.mXComponentOptions) + .onLoad(async () => { + await this.loadXComponent(); + }) + .width(this.getUIContext().px2vp(1080)) + .height(this.getUIContext().px2vp(1920)) + Text('切换相机') + .size({ width: 80, height: 48 }) + .position({ x: 1, y: 1 }) + .backgroundColor(Color.White) + .textAlign(TextAlign.Center) + .borderRadius(24) + .onClick(async () => { + this.mCameraPosition = this.mCameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK ? + camera.CameraPosition.CAMERA_POSITION_FRONT : camera.CameraPosition.CAMERA_POSITION_BACK; + await this.loadXComponent(); + }) + } + .size({ width: '100%', height: '100%' }) + .backgroundColor(Color.Black) + } + } +} +``` \ No newline at end of file -- Gitee