【鸿蒙开发】第五十一章 Camera Kit(相机服务)

目录

[1 Camera Kit简介](#1 Camera Kit简介)

[1.1 开发模型](#1.1 开发模型)

[2 开发准备](#2 开发准备)

[2.1 申请权限](#2.1 申请权限)

[2.2 开发指导](#2.2 开发指导)

[3. 相机开发](#3. 相机开发)

[3.1 相机管理(ArkTS)](#3.1 相机管理(ArkTS))

[3.1.1 开发步骤](#3.1.1 开发步骤)

[3.1.2 状态监听](#3.1.2 状态监听)

[3.2 设备输入(ArkTS)](#3.2 设备输入(ArkTS))

[3.2.1 开发步骤](#3.2.1 开发步骤)

[3.3 会话管理(ArkTS)](#3.3 会话管理(ArkTS))

[3.3.1 开发步骤](#3.3.1 开发步骤)

[3.4 预览(ArkTS)](#3.4 预览(ArkTS))

[3.4.1 开发步骤](#3.4.1 开发步骤)

[3.4.2 状态监听](#3.4.2 状态监听)

[3.5 拍照(ArkTS)](#3.5 拍照(ArkTS))

[3.5.1 开发步骤](#3.5.1 开发步骤)

[3.5.2 状态监听](#3.5.2 状态监听)

[3.6 录像(ArkTS)](#3.6 录像(ArkTS))

[3.6.1 开发步骤](#3.6.1 开发步骤)

[3.6.2 状态监听](#3.6.2 状态监听)

[3.7 元数据(ArkTS)](#3.7 元数据(ArkTS))

[3.7.1 开发步骤](#3.7.1 开发步骤)

[3.7.2 状态监听](#3.7.2 状态监听)

[3.8 对焦(ArkTS)](#3.8 对焦(ArkTS))

[3.8.1 开发步骤](#3.8.1 开发步骤)

[3.8.2 状态监听](#3.8.2 状态监听)

[3.9 手电筒使用(ArkTS)](#3.9 手电筒使用(ArkTS))

[3.9.1 开发步骤](#3.9.1 开发步骤)

[3.9.2 状态监听](#3.9.2 状态监听)

[3.10 适配不同折叠状态的摄像头变更(ArkTS)](#3.10 适配不同折叠状态的摄像头变更(ArkTS))

[3.10.1 创建XComponent](#3.10.1 创建XComponent)

[3.10.2 获取设备折叠状态](#3.10.2 获取设备折叠状态)

[3.11 分段式拍照(ArkTS)](#3.11 分段式拍照(ArkTS))

[3.11.1 开发步骤](#3.11.1 开发步骤)

[3.11.2 状态监听](#3.11.2 状态监听)

[3.12 动态照片(ArkTS)](#3.12 动态照片(ArkTS))

[3.12.1 开发步骤](#3.12.1 开发步骤)

[3.12.2 状态监听](#3.12.2 状态监听)

[3.13 相机基础动效(ArkTS)](#3.13 相机基础动效(ArkTS))

[3.13.1 闪黑动效](#3.13.1 闪黑动效)

[3.13.2 模糊动效](#3.13.2 模糊动效)

[3.14 在Worker线程中使用相机(ArkTS)](#3.14 在Worker线程中使用相机(ArkTS))

[3.14.1 开发步骤](#3.14.1 开发步骤)

[3.14.2 trace对比](#3.14.2 trace对比)

[3.15 适配相机旋转角度(ArkTS)](#3.15 适配相机旋转角度(ArkTS))

[3.15.1 创建会话](#3.15.1 创建会话)

[3.15.2 预览](#3.15.2 预览)

[3.15.3 录像](#3.15.3 录像)

[3.15.4 计算设备旋转角度](#3.15.4 计算设备旋转角度)

[3.16 安全相机(ArkTS)](#3.16 安全相机(ArkTS))

[3.16.1 开发步骤](#3.16.1 开发步骤)

[3.17 动态调整预览帧率(ArkTS)](#3.17 动态调整预览帧率(ArkTS))

[3.17.1 约束与限制](#3.17.1 约束与限制)

[3.17.2 开发流程](#3.17.2 开发流程)

[3.17.3 创建Session会话并指定模式](#3.17.3 创建Session会话并指定模式)

[3.17.4 调整帧率](#3.17.4 调整帧率)

[3.18 使用相机预配置(ArkTS)](#3.18 使用相机预配置(ArkTS))

[3.18.1 规格说明](#3.18.1 规格说明)

[3.18.2 开发步骤](#3.18.2 开发步骤)


1 Camera Kit简介

通过调用Camera Kit(相机服务)提供的接口可以开发相机应用,应用通过访问和操作相机硬件,实现基础操作,如预览、拍照和录像;还可以通过接口组合完成更多操作,如控制闪光灯和曝光时间、对焦或调焦等。

1.1 开发模型

相机调用摄像头采集、加工图像视频数据,精确控制对应的硬件,灵活输出图像、视频内容,满足多镜头硬件适配(如广角、长焦、TOF)、多业务场景适配(如不同分辨率、不同格式、不同效果)的要求。

相机的工作流程如图所示,可概括为相机输入设备管理、会话管理和相机输出管理三部分。

  • 相机设备调用摄像头采集数据,作为相机输入流。

  • 会话管理可配置输入流,即选择哪些镜头进行拍摄。另外还可以配置闪光灯、曝光时间、对焦和调焦等参数,实现不同效果的拍摄,从而适配不同的业务场景。应用可以通过切换会话满足不同场景的拍摄需求。

  • 配置相机的输出流,即将内容以预览流、拍照流或视频流输出。

图1 相机工作流程

图2 相机开发模型

相机应用通过控制相机,实现图像显示(预览)、照片保存(拍照)、视频录制(录像)等基础操作。在实现基本操作过程中,相机服务会控制相机设备采集和输出数据,采集的图像数据在相机底层的设备硬件接口(HDI,Hardware Device Interfaces),直接通过BufferQueue传递到具体的功能模块进行处理。BufferQueue在应用开发中无需关注,用于将底层处理的数据及时送到上层进行图像显示。

以视频录制为例进行说明,相机应用在录制视频过程中,媒体录制服务先创建一个视频Surface用于传递数据,并提供给相机服务,相机服务可控制相机设备采集视频数据,生成视频流。采集的数据通过底层相机HDI处理后,通过Surface将视频流传递给媒体录制服务,媒体录制服务对视频数据进行处理后,保存为视频文件,完成视频录制。

2 开发准备

相机应用开发的主要流程包含开发准备、设备输入、会话管理、预览、拍照和录像等。

2.1 申请权限

在开发相机应用时,需要先申请相机相关权限,确保应用拥有访问相机硬件及其他功能的权限,需要的权限如下表。

  • 使用相机拍摄前,需要申请ohos.permission.CAMERA相机权限。
  • 当需要使用麦克风同时录制音频时,需要申请ohos.permission.MICROPHONE麦克风权限。
  • 当需要拍摄的图片/视频显示地理位置信息时,需要申请ohos.permission.MEDIA_LOCATION,来访问用户媒体文件中的地理位置信息。

以上权限均需要通过弹窗向用户申请授权,具体申请方式及校验方式,请参考向用户申请授权

仅应用需要克隆、备份或同步用户公共目录的图片、视频类文件时,可申请ohos.permission.READ_IMAGEVIDEO、ohos.permission.WRITE_IMAGEVIDEO权限来读写音频文件,申请方式请参考申请受控权限,通过AGC审核后才能使用。为避免应用的上架申请被驳回,开发者应优先使用Picker/控件等替代方案,仅少量符合特殊场景的应用被允许申请受限权限。

2.2 开发指导

当前相机提供了ArkTS和C++两种开发语言的开发指导,如下表所示。

开发流程 ArkTS开发指导 C++开发指导
设备输入 设备输入(ArkTS) 设备输入(C/C++)
会话管理 会话管理(ArkTS) 会话管理(C/C++)
预览 预览(ArkTS) 预览(C/C++)
预览流二次处理 - 预览流二次处理(C/C++)
拍照 拍照(ArkTS) 拍照(C/C++)
分段式拍照 分段式拍照(ArkTS) -
动态照片 动态照片(ArkTS) -
录像 录像(ArkTS) 录像(C/C++)
元数据 元数据(ArkTS) 元数据(C/C++)

3. 相机开发

3.1 相机管理(ArkTS)

3.1.1 开发步骤

  1. 导入camera接口,接口中提供了相机相关的属性和方法,导入方法如下。

    import { camera } from '@kit.CameraKit';
    import { BusinessError } from '@kit.BasicServicesKit';
    import { common } from '@kit.AbilityKit';

  2. 通过getCameraManager方法,获取cameraManager对象。

Context获取方式请参考:获取UIAbility的上下文信息

复制代码
function getCameraManager(context: common.BaseContext): camera.CameraManager {
  let cameraManager: camera.CameraManager = camera.getCameraManager(context);
  return cameraManager;
}

说明

如果获取对象失败,说明相机可能被占用或无法使用。如果被占用,须等到相机被释放后才能重新获取。

  1. 通过CameraManager类中的getSupportedCameras方法,获取当前设备支持的相机列表,列表中存储了设备支持的所有相机ID。若列表不为空,则说明列表中的每个ID都支持独立创建相机对象;否则,说明当前设备无可用相机,不可继续后续操作。

    function getCameraDevices(cameraManager: camera.CameraManager): Array<camera.CameraDevice> {
    let cameraArray: Array<camera.CameraDevice> = cameraManager.getSupportedCameras();
    if (cameraArray != undefined && cameraArray.length > 0) {
    for (let index = 0; index < cameraArray.length; index++) {
    console.info('cameraId : ' + cameraArray[index].cameraId); // 获取相机ID。
    console.info('cameraPosition : ' + cameraArray[index].cameraPosition); // 获取相机位置。
    console.info('cameraType : ' + cameraArray[index].cameraType); // 获取相机类型。
    console.info('connectionType : ' + cameraArray[index].connectionType); // 获取相机连接类型。
    }
    return cameraArray;
    } else {
    console.error("cameraManager.getSupportedCameras error");
    return [];
    }
    }

3.1.2 状态监听

在相机应用开发过程中,可以随时监听相机状态,包括新相机的出现、相机的移除、相机的可用状态。在回调函数中,通过相机ID、相机状态这两个参数进行监听,如当有新相机出现时,可以将新相机加入到应用的备用相机中。

通过注册cameraStatus事件,通过回调返回监听结果,callback返回CameraStatusInfo参数,参数的具体内容可参考相机管理器回调接口实例CameraStatusInfo

复制代码
function onCameraStatusChange(cameraManager: camera.CameraManager): void {
  cameraManager.on('cameraStatus', (err: BusinessError, cameraStatusInfo: camera.CameraStatusInfo) => {
    if (err !== undefined && err.code !== 0) {
      console.error(`Callback Error, errorCode: ${err.code}`);
      return;
    }
    // 如果当通过USB连接相机设备时,回调函数会返回新的相机出现状态。
    if (cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_APPEAR) {
      console.info(`New Camera device appear.`);
    }
    // 如果当断开相机设备USB连接时,回调函数会返回相机被移除状态。
    if (cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_DISAPPEAR) {
      console.info(`Camera device has been removed.`);
    }
    // 相机被关闭时,回调函数会返回相机可用状态。
    if (cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_AVAILABLE) {
      console.info(`Current Camera is available.`);
    }
    // 相机被打开/占用时,回调函数会返回相机不可用状态。
    if (cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_UNAVAILABLE) {
      console.info(`Current Camera has been occupied.`);
    }
    console.info(`camera: ${cameraStatusInfo.camera.cameraId}`);
    console.info(`status: ${cameraStatusInfo.status}`);
  });
}

3.2 设备输入(ArkTS)

3.2.1 开发步骤

  1. 导入camera接口,接口中提供了相机相关的属性和方法,导入方法如下。

    import { camera } from '@kit.CameraKit';
    import { BusinessError } from '@kit.BasicServicesKit';

  2. 通过cameraManager类中的createCameraInput方法创建相机输入流。

    async function createInput(cameraDevice: camera.CameraDevice, cameraManager: camera.CameraManager): Promise<camera.CameraInput | undefined> {
    // 创建相机输入流。
    let cameraInput: camera.CameraInput | undefined = undefined;
    try {
    cameraInput = cameraManager.createCameraInput(cameraDevice);
    } catch (error) {
    let err = error as BusinessError;
    console.error('Failed to createCameraInput errorCode = ' + err.code);
    }
    if (cameraInput === undefined) {
    return undefined;
    }
    // 监听cameraInput错误信息。
    cameraInput.on('error', cameraDevice, (error: BusinessError) => {
    console.error(Camera input error code: ${error.code});
    });
    // 打开相机。
    await cameraInput.open();
    return cameraInput;
    }

  3. 通过getSupportedSceneModes方法,获取当前相机设备支持的模式列表,列表中存储了相机设备支持的所有模式SceneMode

    function getSupportedSceneMode(cameraDevice: camera.CameraDevice, cameraManager: camera.CameraManager): Array<camera.SceneMode> {
    // 获取相机设备支持的模式列表。
    let sceneModeArray: Array<camera.SceneMode> = cameraManager.getSupportedSceneModes(cameraDevice);
    if (sceneModeArray != undefined && sceneModeArray.length > 0) {
    for (let index = 0; index < sceneModeArray.length; index++) {
    console.info('Camera SceneMode : ' + sceneModeArray[index]);
    }
    return sceneModeArray;
    } else {
    console.error("cameraManager.getSupportedSceneModes error");
    return [];
    }
    }

  4. 通过getSupportedOutputCapability方法,获取当前相机设备支持的所有输出流,如预览流、拍照流、录像流等。输出流在CameraOutputCapability中的各个profile字段中,根据相机设备指定模式SceneMode的不同,需要添加不同类型的输出流。

    async function getSupportedOutputCapability(cameraDevice: camera.CameraDevice, cameraManager: camera.CameraManager, sceneMode: camera.SceneMode): Promise<camera.CameraOutputCapability | undefined> {
    // 获取相机设备支持的输出流能力。
    let cameraOutputCapability: camera.CameraOutputCapability = cameraManager.getSupportedOutputCapability(cameraDevice, sceneMode);
    if (!cameraOutputCapability) {
    console.error("cameraManager.getSupportedOutputCapability error");
    return undefined;
    }
    console.info("outputCapability: " + JSON.stringify(cameraOutputCapability));
    // 以NORMAL_PHOTO模式为例,需要添加预览流、拍照流。
    // previewProfiles属性为获取当前设备支持的预览输出流。
    let previewProfilesArray: Array<camera.Profile> = cameraOutputCapability.previewProfiles;
    if (!previewProfilesArray) {
    console.error("createOutput previewProfilesArray == null || undefined");
    }
    //photoProfiles属性为获取当前设备支持的拍照输出流。
    let photoProfilesArray: Array<camera.Profile> = cameraOutputCapability.photoProfiles;
    if (!photoProfilesArray) {
    console.error("createOutput photoProfilesArray == null || undefined");
    }
    return cameraOutputCapability;
    }

3.3 会话管理(ArkTS)

相机使用预览、拍照、录像、元数据功能前,均需要创建相机会话。

在会话中,可以完成以下功能:

  • 配置相机的输入流和输出流。相机在拍摄前,必须完成输入输出流的配置。

    配置输入流即添加设备输入,对用户而言,相当于选择设备的某一摄像头拍摄;配置输出流,即选择数据将以什么形式输出。当应用需要实现拍照时,输出流应配置为预览流和拍照流,预览流的数据将显示在XComponent组件上,拍照流的数据将通过ImageReceiver接口的能力保存到相册中。

  • 添加闪光灯、调整焦距等配置。具体支持的配置及接口说明请参考Camera API参考

  • 会话切换控制。应用可以通过移除和添加输出流的方式,切换相机模式。如当前会话的输出流为拍照流,应用可以将拍照流移除,然后添加视频流作为输出流,即完成了拍照到录像的切换。

完成会话配置后,应用提交和开启会话,可以开始调用相机相关功能。

3.3.1 开发步骤

  1. 导入相关接口,导入方法如下。

    import { camera } from '@kit.CameraKit';
    import { BusinessError } from '@kit.BasicServicesKit';

  2. 调用cameraManager类中的createSession方法创建一个会话。

    function getSession(cameraManager: camera.CameraManager): camera.Session | undefined {
    let session: camera.Session | undefined = undefined;
    try {
    session = cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession;
    } catch (error) {
    let err = error as BusinessError;
    console.error(Failed to create the session instance. error: ${JSON.stringify(err)});
    }
    return session;
    }

  3. 调用PhotoSession类中的beginConfig方法配置会话。

    function beginConfig(photoSession: camera.PhotoSession): void {
    try {
    photoSession.beginConfig();
    } catch (error) {
    let err = error as BusinessError;
    console.error(Failed to beginConfig. error: ${JSON.stringify(err)});
    }
    }

  4. 使能。向会话中添加相机的输入流和输出流,调用addInput添加相机的输入流;调用addOutput添加相机的输出流。以下示例代码以添加预览流previewOutput和拍照流photoOutput为例,即当前模式支持拍照和预览。

调用PhotoSession类中的commitConfigstart方法提交相关配置,并启动会话。

复制代码
async function startSession(photoSession: camera.PhotoSession, cameraInput: camera.CameraInput, previewOutput: camera.PreviewOutput, photoOutput: camera.PhotoOutput): Promise<void> {
  try {
    photoSession.addInput(cameraInput);
  } catch (error) {
    let err = error as BusinessError;
    console.error(`Failed to addInput. error: ${JSON.stringify(err)}`);
  }
  try {
    photoSession.addOutput(previewOutput);
  } catch (error) {
    let err = error as BusinessError;
    console.error(`Failed to add previewOutput. error: ${JSON.stringify(err)}`);
  }
  try {
    photoSession.addOutput(photoOutput);
  } catch (error) {
    let err = error as BusinessError;
    console.error(`Failed to add photoOutput. error: ${JSON.stringify(err)}`);
  }
  try {
    await photoSession.commitConfig();
  } catch (error) {
    let err = error as BusinessError;
    console.error(`Failed to commitConfig. error: ${JSON.stringify(err)}`);
  }

  try {
    await photoSession.start();
  } catch (error) {
    let err = error as BusinessError;
    console.error(`Failed to start. error: ${JSON.stringify(err)}`);
  }
}
  1. 会话控制。调用PhotoSession类中的stop方法可以停止当前会话。调用removeOutputaddOutput方法可以完成会话切换控制。以下示例代码以移除拍照流photoOutput,添加视频流videoOutput为例,完成了拍照到录像的切换。

    async function switchOutput(photoSession: camera.PhotoSession, videoOutput: camera.VideoOutput, photoOutput: camera.PhotoOutput): Promise<void> {
    try {
    await photoSession.stop();
    } catch (error) {
    let err = error as BusinessError;
    console.error(Failed to stop. error: ${JSON.stringify(err)});
    }

    try {
    photoSession.beginConfig();
    } catch (error) {
    let err = error as BusinessError;
    console.error(Failed to beginConfig. error: ${JSON.stringify(err)});
    }
    // 从会话中移除拍照输出流。
    try {
    photoSession.removeOutput(photoOutput);
    } catch (error) {
    let err = error as BusinessError;
    console.error(Failed to remove photoOutput. error: ${JSON.stringify(err)});
    }
    // 向会话中添加视频输出流。
    try {
    photoSession.addOutput(videoOutput);
    } catch (error) {
    let err = error as BusinessError;
    console.error(Failed to add videoOutput. error: ${JSON.stringify(err)});
    }
    }

3.4 预览(ArkTS)

预览是启动相机后看见的画面,通常在拍照和录像前执行。

3.4.1 开发步骤

  1. 导入camera接口,接口中提供了相机相关的属性和方法,导入方法如下。

    import { camera } from '@kit.CameraKit';
    import { BusinessError } from '@kit.BasicServicesKit';

  2. 创建Surface。

XComponent组件为预览流提供的Surface(获取surfaceId请参考getXcomponentSurfaceId方法),而XComponent的能力由UI提供,相关介绍可参考XComponent组件参考

说明

预览流与录像输出流的分辨率的宽高比要保持一致,如果设置XComponent组件中的Surface显示区域宽高比为1920:1080 = 16:9,则需要预览流中的分辨率的宽高比也为16:9,如分辨率选择640:360,或960:540,或1920:1080,以此类推。

  1. 通过CameraOutputCapability类中的previewProfiles属性获取当前设备支持的预览能力,返回previewProfilesArray数组 。通过createPreviewOutput方法创建预览输出流,其中,createPreviewOutput方法中的两个参数分别是previewProfilesArray数组中的第一项和步骤二中获取的surfaceId。

    function getPreviewOutput(cameraManager: camera.CameraManager, cameraOutputCapability: camera.CameraOutputCapability, surfaceId: string): camera.PreviewOutput | undefined {
    let previewProfilesArray: Array<camera.Profile> = cameraOutputCapability.previewProfiles;
    let previewOutput: camera.PreviewOutput | undefined = undefined;
    try {
    previewOutput = cameraManager.createPreviewOutput(previewProfilesArray[0], surfaceId);
    } catch (error) {
    let err = error as BusinessError;
    console.error("Failed to create the PreviewOutput instance. error code: " + err.code);
    }
    return previewOutput;
    }

  2. 使能。通过Session.start方法输出预览流,接口调用失败会返回相应错误码,错误码类型参见Camera错误码

    async function startPreviewOutput(cameraManager: camera.CameraManager, previewOutput: camera.PreviewOutput): Promise<void> {
    let cameraArray: Array<camera.CameraDevice> = [];
    cameraArray = cameraManager.getSupportedCameras();
    if (cameraArray.length == 0) {
    console.error('no camera.');
    return;
    }
    // 获取支持的模式类型。
    let sceneModes: Array<camera.SceneMode> = cameraManager.getSupportedSceneModes(cameraArray[0]);
    let isSupportPhotoMode: boolean = sceneModes.indexOf(camera.SceneMode.NORMAL_PHOTO) >= 0;
    if (!isSupportPhotoMode) {
    console.error('photo mode not support');
    return;
    }
    let cameraInput: camera.CameraInput | undefined = undefined;
    cameraInput = cameraManager.createCameraInput(cameraArray[0]);
    if (cameraInput === undefined) {
    console.error('cameraInput is undefined');
    return;
    }
    // 打开相机。
    await cameraInput.open();
    let session: camera.PhotoSession = cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession;
    session.beginConfig();
    session.addInput(cameraInput);
    session.addOutput(previewOutput);
    await session.commitConfig();
    await session.start();
    }

3.4.2 状态监听

在相机应用开发过程中,可以随时监听预览输出流状态,包括预览流启动、预览流结束、预览流输出错误。

  • 通过注册固定的frameStart回调函数获取监听预览启动结果,previewOutput创建成功时即可监听,预览第一次曝光时触发,有该事件返回结果则认为预览流已启动。

    function onPreviewOutputFrameStart(previewOutput: camera.PreviewOutput): void {
    previewOutput.on('frameStart', (err: BusinessError) => {
    if (err !== undefined && err.code !== 0) {
    return;
    }
    console.info('Preview frame started');
    });
    }

  • 通过注册固定的frameEnd回调函数获取监听预览结束结果,previewOutput创建成功时即可监听,预览完成最后一帧时触发,有该事件返回结果则认为预览流已结束。

    function onPreviewOutputFrameEnd(previewOutput: camera.PreviewOutput): void {
    previewOutput.on('frameEnd', (err: BusinessError) => {
    if (err !== undefined && err.code !== 0) {
    return;
    }
    console.info('Preview frame ended');
    });
    }

  • 通过注册固定的error回调函数获取监听预览输出错误结果,回调返回预览输出接口使用错误时对应的错误码,错误码类型参见Camera错误码

    function onPreviewOutputError(previewOutput: camera.PreviewOutput): void {
    previewOutput.on('error', (previewOutputError: BusinessError) => {
    console.error(Preview output error code: ${previewOutputError.code});
    });
    }

3.5 拍照(ArkTS)

拍照是相机的最重要功能之一,拍照模块基于相机复杂的逻辑,为了保证用户拍出的照片质量,在中间步骤可以设置分辨率、闪光灯、焦距、照片质量及旋转角度等信息。

3.5.1 开发步骤

  1. 导入image接口。创建拍照输出流的SurfaceId以及拍照输出的数据,都需要用到系统提供的image接口能力,导入image接口的方法如下。

    import { image } from '@kit.ImageKit';
    import { camera } from '@kit.CameraKit';
    import { fileIo as fs } from '@kit.CoreFileKit';
    import { photoAccessHelper } from '@kit.MediaLibraryKit';
    import { BusinessError } from '@kit.BasicServicesKit';

  2. 创建拍照输出流。

通过CameraOutputCapability类中的photoProfiles属性,可获取当前设备支持的拍照输出流,通过createPhotoOutput方法传入支持的某一个输出流及步骤一获取的SurfaceId创建拍照输出流。

复制代码
function getPhotoOutput(cameraManager: camera.CameraManager, cameraOutputCapability: camera.CameraOutputCapability): camera.PhotoOutput | undefined {
  let photoProfilesArray: Array<camera.Profile> = cameraOutputCapability.photoProfiles;
  if (!photoProfilesArray) {
    console.error("createOutput photoProfilesArray == null || undefined");
  }
  let photoOutput: camera.PhotoOutput | undefined = undefined;
  try {
    photoOutput = cameraManager.createPhotoOutput(photoProfilesArray[0]);
  } catch (error) {
    let err = error as BusinessError;
    console.error(`Failed to createPhotoOutput. error: ${JSON.stringify(err)}`);
  }
  return photoOutput;
}
  1. 设置拍照photoAvailable的回调,并将拍照的buffer保存为图片。

Context获取方式请参考:获取UIAbility的上下文信息

如需要在图库中看到所保存的图片、视频资源,需要将其保存到媒体库,保存方式请参考:保存媒体库资源

需要在photoOutput.on('photoAvailable')接口获取到buffer时,将buffer在安全控件中保存到媒体库。

复制代码
let context = getContext(this);

function setPhotoOutputCb(photoOutput: camera.PhotoOutput) {
//设置回调之后,调用photoOutput的capture方法,就会将拍照的buffer回传到回调中。
  photoOutput.on('photoAvailable', (errCode: BusinessError, photo: camera.Photo): void => {
     console.info('getPhoto start');
     console.info(`err: ${JSON.stringify(errCode)}`);
     if (errCode || photo === undefined) {
       console.error('getPhoto failed');
       return;
     }
     let imageObj: image.Image = photo.main;
     imageObj.getComponent(image.ComponentType.JPEG, (errCode: BusinessError, component: image.Component): void => {
       console.info('getComponent start');
       if (errCode || component === undefined) {
         console.error('getComponent failed');
         return;
       }
       let buffer: ArrayBuffer;
       if (component.byteBuffer) {
         buffer = component.byteBuffer;
       } else {
         console.error('byteBuffer is null');
         return;
       }
       // 如需要在图库中看到所保存的图片、视频资源,请使用用户无感的安全控件创建媒体资源。

       // buffer处理结束后需要释放该资源,如果未正确释放资源会导致后续拍照获取不到buffer。
       imageObj.release(); 
     });
   });
}
  1. 参数配置。

配置相机的参数可以调整拍照的一些功能,包括闪光灯、变焦、焦距等。

复制代码
function configuringSession(photoSession: camera.PhotoSession): void {
  // 判断设备是否支持闪光灯。
  let flashStatus: boolean = false;
  try {
    flashStatus = photoSession.hasFlash();
  } catch (error) {
    let err = error as BusinessError;
    console.error(`Failed to hasFlash. error: ${JSON.stringify(err)}`);
  }
  console.info(`Returned with the flash light support status: ${flashStatus}`);
  if (flashStatus) {
    // 判断是否支持自动闪光灯模式。
    let flashModeStatus: boolean = false;
    try {
      let status: boolean = photoSession.isFlashModeSupported(camera.FlashMode.FLASH_MODE_AUTO);
      flashModeStatus = status;
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Failed to check whether the flash mode is supported. error: ${JSON.stringify(err)}`);
    }
    if (flashModeStatus) {
      // 设置自动闪光灯模式。
      try {
        photoSession.setFlashMode(camera.FlashMode.FLASH_MODE_AUTO);
      } catch (error) {
        let err = error as BusinessError;
        console.error(`Failed to set the flash mode. error: ${JSON.stringify(err)}`);
      }
    }
  }
  // 判断是否支持连续自动变焦模式。
  let focusModeStatus: boolean = false;
  try {
    let status: boolean = photoSession.isFocusModeSupported(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO);
    focusModeStatus = status;
  } catch (error) {
    let err = error as BusinessError;
    console.error(`Failed to check whether the focus mode is supported. error: ${JSON.stringify(err)}`);
  }
  if (focusModeStatus) {
    // 设置连续自动变焦模式。
    try {
      photoSession.setFocusMode(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO);
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Failed to set the focus mode. error: ${JSON.stringify(err)}`);
    }
  }
  // 获取相机支持的可变焦距比范围。
  let zoomRatioRange: Array<number> = [];
  try {
    zoomRatioRange = photoSession.getZoomRatioRange();
  } catch (error) {
    let err = error as BusinessError;
    console.error(`Failed to get the zoom ratio range. error: ${JSON.stringify(err)}`);
  }
  if (zoomRatioRange.length <= 0 ) {
    return;
  }
  // 设置可变焦距比。
  try {
    photoSession.setZoomRatio(zoomRatioRange[0]);
  } catch (error) {
    let err = error as BusinessError;
    console.error(`Failed to set the zoom ratio value. error: ${JSON.stringify(err)}`);
  }
}
  1. 触发拍照。

通过photoOutput类的capture方法,执行拍照任务。该方法有两个参数,第一个参数为拍照设置参数的setting,setting中可以设置照片的质量和旋转角度,第二参数为回调函数。

获取拍照旋转角度的方法为,通过通过PhotoOutput类中的getPhotoRotation方法获取rotation实际的值。

复制代码
function capture(captureLocation: camera.Location, photoOutput: camera.PhotoOutput): void {
  let settings: camera.PhotoCaptureSetting = {
    quality: camera.QualityLevel.QUALITY_LEVEL_HIGH,  // 设置图片质量高。
    rotation: camera.ImageRotation.ROTATION_0,  // 设置图片旋转角度的camera.ImageRotation.ROTATION_0是通过说明中获取拍照角度的getPhotoRotation方法获取的值进行设置。
    location: captureLocation,  // 设置图片地理位置。
    mirror: false  // 设置镜像使能开关(默认关)。
  };
  photoOutput.capture(settings, (err: BusinessError) => {
    if (err) {
      console.error(`Failed to capture the photo. error: ${JSON.stringify(err)}`);
      return;
    }
    console.info('Callback invoked to indicate the photo capture request success.');
  });
}

3.5.2 状态监听

在相机应用开发过程中,可以随时监听拍照输出流状态,包括拍照流开始、拍照帧的开始与结束、拍照输出流的错误。

  • 通过注册固定的captureStart回调函数获取监听拍照开始结果,photoOutput创建成功时即可监听,相机设备已经准备开始这次拍照时触发,该事件返回此次拍照的captureId。

    function onPhotoOutputCaptureStart(photoOutput: camera.PhotoOutput): void {
    photoOutput.on('captureStartWithInfo', (err: BusinessError, captureStartInfo: camera.CaptureStartInfo) => {
    if (err !== undefined && err.code !== 0) {
    return;
    }
    console.info(photo capture started, captureId : ${captureStartInfo.captureId});
    });
    }

  • 通过注册固定的captureEnd回调函数获取监听拍照结束结果,photoOutput创建成功时即可监听,该事件返回结果为拍照完全结束后的相关信息CaptureEndInfo

    function onPhotoOutputCaptureEnd(photoOutput: camera.PhotoOutput): void {
    photoOutput.on('captureEnd', (err: BusinessError, captureEndInfo: camera.CaptureEndInfo) => {
    if (err !== undefined && err.code !== 0) {
    return;
    }
    console.info(photo capture end, captureId : ${captureEndInfo.captureId});
    console.info(frameCount : ${captureEndInfo.frameCount});
    });
    }

  • 通过注册固定的captureReady回调函数获取监听可拍下一张结果,photoOutput创建成功时即可监听,当下一张可拍时触发,该事件返回结果为下一张可拍的相关信息。

    function onPhotoOutputCaptureReady(photoOutput: camera.PhotoOutput): void {
    photoOutput.on('captureReady', (err: BusinessError) => {
    if (err !== undefined && err.code !== 0) {
    return;
    }
    console.info(photo capture ready);
    });
    }

  • 通过注册固定的error回调函数获取监听拍照输出流的错误结果。回调返回拍照输出接口使用错误时的对应错误码,错误码类型参见Camera错误码

    function onPhotoOutputError(photoOutput: camera.PhotoOutput): void {
    photoOutput.on('error', (error: BusinessError) => {
    console.error(Photo output error code: ${error.code});
    });
    }

3.6 录像(ArkTS)

3.6.1 开发步骤

  1. 导入media模块。

创建录像输出流的SurfaceId以及录像输出的数据,都需要用到系统提供的media接口能力,导入media接口的方法如下。

复制代码
import { BusinessError } from '@kit.BasicServicesKit';
import { camera } from '@kit.CameraKit';
import { media } from '@kit.MediaKit';
  1. 创建Surface。

系统提供的media接口可以创建一个录像AVRecorder实例,通过该实例的getInputSurface方法获取SurfaceId,与录像输出流做关联,处理录像输出流输出的数据。

复制代码
async function getVideoSurfaceId(aVRecorderConfig: media.AVRecorderConfig): Promise<string | undefined> {  // aVRecorderConfig可参考下一章节。
  let avRecorder: media.AVRecorder | undefined = undefined;
  try {
    avRecorder = await media.createAVRecorder();
  } catch (error) {
    let err = error as BusinessError;
    console.error(`createAVRecorder call failed. error code: ${err.code}`);
  }
  if (avRecorder === undefined) {
    return undefined;
  }
  avRecorder.prepare(aVRecorderConfig, (err: BusinessError) => {
    if (err == null) {
      console.info('prepare success');
    } else {
      console.error('prepare failed and error is ' + err.message);
    }
  });
  let videoSurfaceId = await avRecorder.getInputSurface();
  return videoSurfaceId;
}
  1. 创建录像输出流。

通过CameraOutputCapability类中的videoProfiles属性,可获取当前设备支持的录像输出流。然后,定义创建录像的参数,通过createVideoOutput方法创建录像输出流。

说明

预览流与录像输出流的分辨率的宽高比要保持一致,如示例代码中宽高比为640:480 = 4:3,则需要预览流中的分辨率的宽高比也为4:3,如分辨率选择640:480,或960:720,或1440:1080,以此类推。

获取录像旋转角度的方法:通过VideoOutput类中的getVideoRotation方法获取rotation实际的值。

复制代码
async function getVideoOutput(cameraManager: camera.CameraManager, videoSurfaceId: string, cameraOutputCapability: camera.CameraOutputCapability): Promise<camera.VideoOutput | undefined> {
  let videoProfilesArray: Array<camera.VideoProfile> = cameraOutputCapability.videoProfiles;
  if (!videoProfilesArray) {
    console.error("createOutput videoProfilesArray == null || undefined");
    return undefined;
  }
  // AVRecorderProfile.
  let aVRecorderProfile: media.AVRecorderProfile = {
    fileFormat : media.ContainerFormatType.CFT_MPEG_4, // 视频文件封装格式,只支持MP4。
    videoBitrate : 100000, // 视频比特率。
    videoCodec : media.CodecMimeType.VIDEO_AVC, // 视频文件编码格式,支持avc格式。
    videoFrameWidth : 640,  // 视频分辨率的宽。
    videoFrameHeight : 480, // 视频分辨率的高。
    videoFrameRate : 30 // 视频帧率。
  };
  // 创建视频录制的参数,预览流与录像输出流的分辨率的宽(videoFrameWidth)高(videoFrameHeight)比要保持一致。
  let aVRecorderConfig: media.AVRecorderConfig = {
    videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV,
    profile: aVRecorderProfile,
    url: 'fd://35',
    rotation: 90 // rotation的值90,是通过getPhotoRotation接口获取到的值,具体请参考说明中获取录像旋转角度的方法。
  };
  // 创建avRecorder。
  let avRecorder: media.AVRecorder | undefined = undefined;
  try {
    avRecorder = await media.createAVRecorder();
  } catch (error) {
    let err = error as BusinessError;
    console.error(`createAVRecorder call failed. error code: ${err.code}`);
  }
  if (avRecorder === undefined) {
    return undefined;
  }
  // 设置视频录制的参数。
  avRecorder.prepare(aVRecorderConfig);
  // 创建VideoOutput对象。
  let videoOutput: camera.VideoOutput | undefined = undefined;
  // createVideoOutput传入的videoProfile对象的宽高需要和aVRecorderProfile保持一致。
  let videoProfile: undefined | camera.VideoProfile = videoProfilesArray.find((profile: camera.VideoProfile) => {
    return profile.size.width === aVRecorderProfile.videoFrameWidth && profile.size.height === aVRecorderProfile.videoFrameHeight;
  });
  if (!videoProfile) {
    console.error('videoProfile is not found');
    return;
  }
  try {
    videoOutput = cameraManager.createVideoOutput(videoProfile, videoSurfaceId);
  } catch (error) {
    let err = error as BusinessError;
    console.error('Failed to create the videoOutput instance. errorCode = ' + err.code);
  }
  return videoOutput;
}
  1. 开始录像。

先通过videoOutput的start方法启动录像输出流,再通过avRecorder的start方法开始录像。

复制代码
async function startVideo(videoOutput: camera.VideoOutput, avRecorder: media.AVRecorder): Promise<void> {
  videoOutput.start(async (err: BusinessError) => {
    if (err) {
      console.error(`Failed to start the video output ${err.message}`);
      return;
    }
    console.info('Callback invoked to indicate the video output start success.');
  });
  try {
    await avRecorder.start();
  } catch (error) {
    let err = error as BusinessError;
    console.error(`avRecorder start error: ${JSON.stringify(err)}`);
  }
}
  1. 停止录像。

先通过avRecorder的stop方法停止录像,再通过videoOutput的stop方法停止录像输出流。

复制代码
async function stopVideo(videoOutput: camera.VideoOutput, avRecorder: media.AVRecorder): Promise<void> {
  try {
    await avRecorder.stop();
  } catch (error) {
    let err = error as BusinessError;
    console.error(`avRecorder stop error: ${JSON.stringify(err)}`);
  }
  videoOutput.stop((err: BusinessError) => {
    if (err) {
      console.error(`Failed to stop the video output ${err.message}`);
      return;
    }
    console.info('Callback invoked to indicate the video output stop success.');
  });
}

3.6.2 状态监听

在相机应用开发过程中,可以随时监听录像输出流状态,包括录像开始、录像结束、录像流输出的错误。

  • 通过注册固定的frameStart回调函数获取监听录像开始结果,videoOutput创建成功时即可监听,录像第一次曝光时触发,有该事件返回结果则认为录像开始。

    function onVideoOutputFrameStart(videoOutput: camera.VideoOutput): void {
    videoOutput.on('frameStart', (err: BusinessError) => {
    if (err !== undefined && err.code !== 0) {
    return;
    }
    console.info('Video frame started');
    });
    }

  • 通过注册固定的frameEnd回调函数获取监听录像结束结果,videoOutput创建成功时即可监听,录像完成最后一帧时触发,有该事件返回结果则认为录像流已结束。

    function onVideoOutputFrameEnd(videoOutput: camera.VideoOutput): void {
    videoOutput.on('frameEnd', (err: BusinessError) => {
    if (err !== undefined && err.code !== 0) {
    return;
    }
    console.info('Video frame ended');
    });
    }

  • 通过注册固定的error回调函数获取监听录像输出错误结果,callback返回预览输出接口使用错误时对应的错误码,错误码类型参见Camera错误码

    function onVideoOutputError(videoOutput: camera.VideoOutput): void {
    videoOutput.on('error', (error: BusinessError) => {
    console.error(Video output error code: ${error.code});
    });
    }

3.7 元数据(ArkTS)

元数据(Metadata)是对相机返回的图像信息数据的描述和上下文,针对图像信息,提供的更详细的数据,如照片或视频中,识别人像的取景框坐标等信息。

Metadata主要是通过一个TAG(Key),去找对应的Data,用于传递参数和配置信息,减少内存拷贝操作。

3.7.1 开发步骤

  1. 导入相关接口,导入方法如下。

    import { camera } from '@kit.CameraKit';
    import { BusinessError } from '@kit.BasicServicesKit';

  2. 调用CameraOutputCapability类中的supportedMetadataObjectTypes属性,获取当前设备支持的元数据类型,并通过createMetadataOutput方法创建元数据输出流。

    function getMetadataOutput(cameraManager: camera.CameraManager, cameraOutputCapability: camera.CameraOutputCapability): camera.MetadataOutput | undefined {
    let metadataObjectTypes: Array<camera.MetadataObjectType> = cameraOutputCapability.supportedMetadataObjectTypes;
    let metadataOutput: camera.MetadataOutput | undefined = undefined;
    try {
    metadataOutput = cameraManager.createMetadataOutput(metadataObjectTypes);
    } catch (error) {
    let err = error as BusinessError;
    console.error(Failed to createMetadataOutput, error code: ${err.code});
    }
    return metadataOutput;
    }

  3. 调用Session.start方法开启metadata数据输出,再通过监听事件metadataObjectsAvailable回调拿到数据,接口调用失败时,会返回相应错误码,错误码类型参见Camera错误码

previewOutput获取方式请参考相机预览开发步骤

复制代码
async function startMetadataOutput(previewOutput: camera.PreviewOutput, metadataOutput: camera.MetadataOutput, cameraManager: camera.CameraManager): Promise<void> {
  let cameraArray: Array<camera.CameraDevice> = [];
  cameraArray = cameraManager.getSupportedCameras();
  if (cameraArray.length == 0) {
    console.error('no camera.');
    return;
  }
  // 获取支持的模式类型。
  let sceneModes: Array<camera.SceneMode> = cameraManager.getSupportedSceneModes(cameraArray[0]);
  let isSupportPhotoMode: boolean = sceneModes.indexOf(camera.SceneMode.NORMAL_PHOTO) >= 0;
  if (!isSupportPhotoMode) {
    console.error('photo mode not support');
    return;
  }
  let cameraInput: camera.CameraInput | undefined = undefined;
  cameraInput = cameraManager.createCameraInput(cameraArray[0]);
  if (cameraInput === undefined) {
    console.error('cameraInput is undefined');
    return;
  }
  // 打开相机。
  await cameraInput.open();
  let session: camera.PhotoSession = cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession;
  session.beginConfig();
  session.addInput(cameraInput);
  session.addOutput(previewOutput);
  session.addOutput(metadataOutput);
  await session.commitConfig();
  await session.start();
}
  1. 调用Session.stop方法停止输出metadata数据,接口调用失败会返回相应错误码,错误码类型参见Camera错误码

    function stopMetadataOutput(session: camera.Session): void {
    session.stop().then(() => {
    console.info('Callback returned with session stopped.');
    }).catch((err: BusinessError) => {
    console.error(Failed to session stop, error code: ${err.code});
    });
    }

3.7.2 状态监听

在相机应用开发过程中,可以随时监听metadata数据以及输出流的状态。

  • 通过注册监听获取metadata对象,监听事件固定为metadataObjectsAvailable。检测到有效metadata数据时,callback返回相应的metadata数据信息,metadataOutput创建成功时可监听。

    function onMetadataObjectsAvailable(metadataOutput: camera.MetadataOutput): void {
    metadataOutput.on('metadataObjectsAvailable', (err: BusinessError, metadataObjectArr: Array<camera.MetadataObject>) => {
    if (err !== undefined && err.code !== 0) {
    return;
    }
    console.info('metadata output metadataObjectsAvailable');
    });
    }

说明

当前的元数据类型仅支持人脸检测(FACE_DETECTION)功能。元数据信息对象为识别到的人脸区域的矩形信息(Rect),包含矩形区域的左上角x坐标、y坐标和矩形的宽高数据。

  • 通过注册回调函数,获取监听metadata流的错误结果,callback返回metadata输出接口使用错误时返回的错误码,错误码类型参见Camera错误码

    function onMetadataError(metadataOutput: camera.MetadataOutput): void {
    metadataOutput.on('error', (metadataOutputError: BusinessError) => {
    console.error(Metadata output error code: ${metadataOutputError.code});
    });
    }

3.8 对焦(ArkTS)

相机框架提供对设备对焦的能力,业务应用可以根据使用场景进行对焦模式和对焦点的设置。

3.8.1 开发步骤

  1. 导入相关接口,导入方法如下。

    import { camera } from '@kit.CameraKit';
    import { BusinessError } from '@kit.BasicServicesKit';

  2. 在设置对焦模式前,需要先调用isFocusModeSupported检查设备是否支持指定的焦距模式。

说明

需要在Session调用commitConfig完成配流之后调用。

复制代码
function isFocusModeSupported(photoSession: camera.PhotoSession): boolean {
  let status: boolean = false;
  try {
    // 以检查是否支持连续自动对焦模式为例
    status = photoSession.isFocusModeSupported(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO);
  } catch (error) {
    // 失败返回错误码error.code并处理
    let err = error as BusinessError;
    console.error(`The isFocusModeSupported call failed. error code: ${err.code}`);
  }
  return status;
}
  1. 调用setFocusMode设置对焦模式。

若设置为自动对焦模式,支持调用setFocusPoint设置对焦点,根据对焦点执行一次自动对焦。

说明

需要在Session调用commitConfig完成配流之后调用。

复制代码
function setFocusMode(photoSession: camera.PhotoSession): void {
  const focusPoint: camera.Point = {x: 1, y: 1};
  try {
    // 设置自动对焦模式
    photoSession.setFocusMode(camera.FocusMode.FOCUS_MODE_AUTO);
    // 设置对焦点
    photoSession.setFocusPoint(focusPoint);
  } catch (error) {
    // 失败返回错误码error.code并处理
    let err = error as BusinessError;
    console.error(`The setFocusMode and setFocusPoint call failed. error code: ${err.code}`);
  }
}

3.8.2 状态监听

在相机应用开发过程中,可以随时监听相机聚焦的状态变化。

  • 通过注册focusStateChange的回调函数获取监听结果,仅当自动对焦模式时,且相机对焦状态发生改变时触发该事件。

    function onFocusStateChange(photoSession: camera.PhotoSession): void {
    photoSession.on('focusStateChange', (err: BusinessError, focusState: camera.FocusState) => {
    if (err !== undefined && err.code !== 0) {
    console.error(focusStateChange error code: ${err.code});
    return;
    }
    console.info(focusStateChange focusState: ${focusState});
    // 为保证对焦功能的用户体验,在自动对焦成功后,可将对焦模式设置为连续自动对焦
    if (focusState === camera.FocusState.FOCUS_STATE_FOCUSED) {
    photoSession.setFocusMode(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO);
    }
    });
    }

3.9 手电筒使用(ArkTS)

手电筒模式的使用是通过操作手机启用手电筒功能,使设备的手电筒功能持续保持常亮状态。

在使用相机应用并操作手电筒功能时,存在以下几种情况说明:

  • 当使用后置摄像头并设置闪光灯模式FlashMode关闭时,手电筒功能无法启用。
  • 当使用前置摄像头时,手电筒可以正常启用并保持常亮状态。
  • 从前置摄像头切换至后置摄像头时,如果手电筒原本处于开启状态,它将会被自动关闭。

3.9.1 开发步骤

  1. 导入camera接口,接口中提供了相机相关的属性和方法,导入方法如下。

    import { camera } from '@kit.CameraKit';
    import { BusinessError } from '@kit.BasicServicesKit';

  2. 通过CameraManager类中的isTorchSupported方法,检测当前设备是否支持手电筒功能。

    function isTorchSupported(cameraManager: camera.CameraManager) : boolean {
    let torchSupport: boolean = false;
    try {
    torchSupport = cameraManager.isTorchSupported();
    } catch (error) {
    let err = error as BusinessError;
    console.error('Failed to torch. errorCode = ' + err.code);
    }
    console.info('Returned with the torch support status:' + torchSupport);
    return torchSupport;
    }

3 通过CameraManager类中的isTorchModeSupported方法,检测是否支持指定的手电筒模式TorchMode

复制代码
function isTorchModeSupported(cameraManager: camera.CameraManager, torchMode: camera.TorchMode) : boolean {
    let isTorchModeSupport: boolean = false;
    try {
        isTorchModeSupport = cameraManager.isTorchModeSupported(torchMode);
    } catch (error) {
        let err = error as BusinessError;
        console.error('Failed to set the torch mode. errorCode = ' + err.code);
    }
    return isTorchModeSupport;
}
  1. 通过CameraManager类中的setTorchMode方法,设置当前设备的手电筒模式。以及通过CameraManager类中的getTorchMode方法,获取当前设备的手电筒模式。

说明

在使用getTorchMode方法前,需要先注册监听手电筒的状态变化,请参考状态监听

复制代码
function setTorchModeSupported(cameraManager: camera.CameraManager, torchMode: camera.TorchMode) : void {
    cameraManager.setTorchMode(torchMode);
    let isTorchMode = cameraManager.getTorchMode();
    console.info(`Returned with the torch mode supportd mode: ${isTorchMode}`);
}

3.9.2 状态监听

在相机应用开发过程中,可以随时监听手电筒状态,包括手电筒打开、手电筒关闭、手电筒不可用、手电筒恢复可用。手电筒状态发生变化,可通过回调函数获取手电筒模式的变化。

通过注册torchStatusChange事件,通过回调返回监听结果,callback返回TorchStatusInfo参数,参数的具体内容可参考相机管理器回调接口实例TorchStatusInfo

复制代码
function onTorchStatusChange(cameraManager: camera.CameraManager): void {
    cameraManager.on('torchStatusChange', (err: BusinessError, torchStatusInfo: camera.TorchStatusInfo) => {
        if (err !== undefined && err.code !== 0) {
            console.error(`Callback Error, errorCode: ${err.code}`);
            return;
        }
        console.info(`onTorchStatusChange, isTorchAvailable: ${torchStatusInfo.isTorchAvailable}, isTorchActive: ${torchStatusInfo.
            isTorchActive}, level: ${torchStatusInfo.torchLevel}`);
    });
}

3.10 适配不同折叠状态的摄像头变更(ArkTS)

一台可折叠设备在不同折叠状态下,可使用不同的摄像头,应用可调用CameraManager.on('foldStatusChange')display.on('foldStatusChange')监听设备的折叠状态变化,并调用CameraManager.getSupportedCameras获取当前状态下可用摄像头,完成相应适配,确保应用在折叠状态变更时的用户体验。

3.10.1 创建XComponent

使用两个XComponent分别展示折叠态和展开态,防止切换折叠屏状态亮屏的时候上一个摄像头还未关闭,残留上一个摄像头的画面。

复制代码
 @Entry
 @Component
 struct Index {
   @State reloadXComponentFlag: boolean = false;
   @StorageLink('foldStatus') @Watch('reloadXComponent') foldStatus: number = 0;
   private mXComponentController: XComponentController = new XComponentController();
   private mXComponentOptions: XComponentOptions = {
     type: XComponentType.SURFACE,
     controller: this.mXComponentController
   }

   reloadXComponent() {
     this.reloadXComponentFlag = !this.reloadXComponentFlag;
   }

   async loadXComponent() {
     //初始化XComponent。
   }

   build() {
     Stack() {
       if (this.reloadXComponentFlag) {
         XComponent(this.mXComponentOptions)
           .onLoad(async () => {
             await this.loadXComponent();
           })
           .width(px2vp(1080))
           .height(px2vp(1920))
       } else {
         XComponent(this.mXComponentOptions)
           .onLoad(async () => {
             await this.loadXComponent();
           })
           .width(px2vp(1080))
           .height(px2vp(1920))
       }
     }
     .size({ width: '100%', height: '100%' })
     .backgroundColor(Color.Black)
   }
 }

3.10.2 获取设备折叠状态

此处提供两种方案供开发者选择。

  • 方案一:使用相机框架提供的CameraManager.on('foldStatusChange')监听设备折叠态变化。

    import { camera } from '@kit.CameraKit';
    import { BusinessError } from '@kit.BasicServicesKit';

    let cameraManager = camera.getCameraManager(getContext())

    function registerFoldStatusChanged(err: BusinessError, foldStatusInfo: camera.FoldStatusInfo) {
    // foldStatus 变量用来控制显示XComponent组件。
    AppStorage.setOrCreate<number>('foldStatus', foldStatusInfo.foldStatus);
    }

    cameraManager.on('foldStatusChange', registerFoldStatusChanged);
    //cameraManager.off('foldStatusChange', registerFoldStatusChanged);

  • 方案二:使用图形图像的display.on('foldStatusChange')监听设备折叠态变化。

    import { display } from '@kit.ArkUI';
    let preFoldStatus: display.FoldStatus = display.getFoldStatus();
    display.on('foldStatusChange', (foldStatus: display.FoldStatus) => {
    // 从半折叠态(FOLD_STATUS_HALF_FOLDED)和展开态(FOLD_STATUS_EXPANDED),相机框架返回所支持的摄像头是一致的,所以从半折叠态到展开态不需要重新配流,从展开态到半折叠态也是一样的。
    if ((preFoldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED &&
    foldStatus === display.FoldStatus.FOLD_STATUS_EXPANDED) ||
    (preFoldStatus === display.FoldStatus.FOLD_STATUS_EXPANDED &&
    foldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED)) {
    preFoldStatus = foldStatus;
    return;
    }
    preFoldStatus = foldStatus;
    // foldStatus 变量用来控制显示XComponent组件。
    AppStorage.setOrCreate<number>('foldStatus', foldStatus);
    })

3.11 分段式拍照(ArkTS)

分段式拍照是相机的重要功能之一,即应用下发拍照任务后,系统将分多阶段上报不同质量的图片。

  • 在第一阶段,系统快速上报轻量处理的图片,轻量处理的图片比全质量图低,出图速度快。应用通过回调会收到一个PhotoAsset对象,通过该对象可调用媒体库接口,读取图片或落盘图片。
  • 在第二阶段,相机框架会根据应用的请求图片诉求或者在系统闲时,进行图像增强处理得到全质量图,将处理好的图片传回给媒体库,替换轻量处理的图片。

通过分段式拍照,优化了系统的拍照响应时延,从而提升用户体验。

应用开发分段式拍照主要分为以下步骤:

  • 通过PhotoOutput,监听photoAssetAvailable回调,获取photoAccessHelper的PhotoAsset对象。

  • 通过PhotoAsset对象,调用媒体库相关接口,读取或落盘图片。
    说明

  • 分段式拍照能力是根据设备模式决定的,不同的设备支持不同的模式,不同的模式下分段式能力也各有不同,所以应用在切换设备或模式后分段式能力可能会发生变化。

  • 分段式拍照能力应用无需主动使能,相机框架会在配流期间判断设备和模式是否支持分段式,如果支持会使能分段式拍照。

3.11.1 开发步骤

  1. 导入依赖,需要导入相机框架、媒体库、图片相关领域依赖。

    import { camera } from '@kit.CameraKit';
    import { BusinessError } from '@kit.BasicServicesKit';
    import { common } from '@kit.AbilityKit';
    import { photoAccessHelper } from '@kit.MediaLibraryKit';

  2. 确定拍照输出流。

通过CameraOutputCapability类中的photoProfiles属性,可获取当前设备支持的拍照输出流,通过createPhotoOutput方法创建拍照输出流。

复制代码
function getPhotoOutput(cameraManager: camera.CameraManager, 
                        cameraOutputCapability: camera.CameraOutputCapability): camera.PhotoOutput | undefined {
  let photoProfilesArray: Array<camera.Profile> = cameraOutputCapability.photoProfiles;
  if (!photoProfilesArray) {
    console.error("createOutput photoProfilesArray == null || undefined");
  }
  let photoOutput: camera.PhotoOutput | undefined = undefined;
  try {
    photoOutput = cameraManager.createPhotoOutput(photoProfilesArray[0]);
  } catch (error) {
    let err = error as BusinessError;
    console.error(`Failed to createPhotoOutput. error: ${JSON.stringify(err)}`);
  }
  return photoOutput;
}
  1. 设置拍照photoAssetAvailable的回调。

注意

如果已经注册了photoAssetAvailable回调,并且在Session开始之后又注册了photoAvailable回调,photoAssetAvailable和photoAvailable同时注册,会导致流被重启,仅photoAssetAvailable生效。

不建议开发者同时注册photoAvailable和photoAssetAvailable。

复制代码
function photoAssetAvailableCallback(err: BusinessError, photoAsset: photoAccessHelper.PhotoAsset): void {
  if (err) {
    console.error(`photoAssetAvailable error: ${JSON.stringify(err)}.`);
    return;
  }
  console.info('photoOutPutCallBack photoAssetAvailable');
  // 开发者可通过photoAsset调用媒体库相关接口,自定义处理图片。
  // 处理方式一:调用媒体库落盘接口保存一阶段图,二阶段图就绪后媒体库会主动帮应用替换落盘图片。
  mediaLibSavePhoto(photoAsset);
  // 处理方式二:调用媒体库接口请求图片并注册一阶段图或二阶段图buffer回调,自定义使用。
  mediaLibRequestBuffer(photoAsset);
}

function onPhotoOutputPhotoAssetAvailable(photoOutput: camera.PhotoOutput): void {
  photoOutput.on('photoAssetAvailable', photoAssetAvailableCallback);
}

let context = getContext(this);
let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);

async function mediaLibSavePhoto(photoAsset: photoAccessHelper.PhotoAsset): Promise<void> {
  try {
    let assetChangeRequest: photoAccessHelper.MediaAssetChangeRequest = new photoAccessHelper.MediaAssetChangeRequest(photoAsset);
    assetChangeRequest.saveCameraPhoto();
    await phAccessHelper.applyChanges(assetChangeRequest);
    console.info('apply saveCameraPhoto successfully');
  } catch (err) {
    console.error(`apply saveCameraPhoto failed with error: ${err.code}, ${err.message}`);
  }
}

class MediaDataHandler implements photoAccessHelper.MediaAssetDataHandler<ArrayBuffer> {
  onDataPrepared(data: ArrayBuffer) {
    if (data === undefined) {
      console.error('Error occurred when preparing data');
      return;
    }
    // 应用获取到图片buffer后可自定义处理。
    console.info('on image data prepared');
  }
}

async function mediaLibRequestBuffer(photoAsset: photoAccessHelper.PhotoAsset) {
  let requestOptions: photoAccessHelper.RequestOptions = {
    // 按照业务需求配置回图模式。
    // FAST_MODE:仅接收一阶段低质量图回调。
    // HIGH_QUALITY_MODE:仅接收二阶段全质量图回调。
    // BALANCE_MODE:接收一阶段及二阶段图片回调。
    deliveryMode: photoAccessHelper.DeliveryMode.FAST_MODE,
  }
  const handler = new MediaDataHandler();
  await photoAccessHelper.MediaAssetManager.requestImageData(context, photoAsset, requestOptions, handler);
  console.info('requestImageData successfully');
}

落盘图片参考媒体库接口:saveCameraPhoto

请求图片参考媒体库接口:requestImageDataonDataPrepared

  1. 拍照时的会话配置及触发拍照的方式,与普通拍照相同,请参考拍照的步骤4-5。

3.11.2 状态监听

在相机应用开发过程中,可以随时监听拍照输出流状态,包括拍照流开始、拍照帧的开始与结束、拍照输出流的错误。

  • 通过注册固定的captureStart回调函数获取监听拍照开始结果,photoOutput创建成功时即可监听,相机设备已经准备开始这次拍照时触发,该事件返回此次拍照的captureId。

    function onPhotoOutputCaptureStart(photoOutput: camera.PhotoOutput): void {
    photoOutput.on('captureStartWithInfo', (err: BusinessError, captureStartInfo: camera.CaptureStartInfo) => {
    if (err !== undefined && err.code !== 0) {
    return;
    }
    console.info(photo capture started, captureId : ${captureStartInfo.captureId});
    });
    }

  • 通过注册固定的captureEnd回调函数获取监听拍照结束结果,photoOutput创建成功时即可监听,该事件返回结果为拍照完全结束后的相关信息CaptureEndInfo

    function onPhotoOutputCaptureEnd(photoOutput: camera.PhotoOutput): void {
    photoOutput.on('captureEnd', (err: BusinessError, captureEndInfo: camera.CaptureEndInfo) => {
    if (err !== undefined && err.code !== 0) {
    return;
    }
    console.info(photo capture end, captureId : ${captureEndInfo.captureId});
    console.info(frameCount : ${captureEndInfo.frameCount});
    });
    }

  • 通过注册固定的captureReady回调函数获取监听可拍下一张结果,photoOutput创建成功时即可监听,当下一张可拍时触发,该事件返回结果为下一张可拍的相关信息。

    function onPhotoOutputCaptureReady(photoOutput: camera.PhotoOutput): void {
    photoOutput.on('captureReady', (err: BusinessError) => {
    if (err !== undefined && err.code !== 0) {
    return;
    }
    console.info(photo capture ready);
    });
    }

  • 通过注册固定的error回调函数获取监听拍照输出流的错误结果。callback返回拍照输出接口使用错误时的对应错误码,错误码类型参见Camera错误码

    function onPhotoOutputError(photoOutput: camera.PhotoOutput): void {
    photoOutput.on('error', (error: BusinessError) => {
    console.error(Photo output error code: ${error.code});
    });
    }

3.12 动态照片(ArkTS)

相机框架提供动态照片拍摄能力,业务应用可以类似拍摄普通照片一样,一键式拍摄得到动态照片。

应用开发动态照片主要分为以下步骤:

  • 查询当前设备的当前模式是否支持拍摄动态照片。
  • 如果支持动态照片,可以调用相机框架提供的使能接口使能动态照片能力。
  • 监听照片回调,将照片存入媒体库。可参考MediaLibrary Kit-访问和管理动态照片资源

3.12.1 开发步骤

说明

  • 使能动态照片前需要使能分段式拍照能力。
  • 拍摄动态照片需要麦克风权限ohos.permission.MICROPHONE,权限申请和校验的方式请参考开发准备。否则拍摄的照片没有声音。
  1. 导入依赖,需要导入相机框架、媒体库、图片相关领域依赖。

    import { camera } from '@kit.CameraKit';
    import { photoAccessHelper } from '@kit.MediaLibraryKit';
    import { BusinessError } from '@kit.BasicServicesKit';

  2. 确定拍照输出流。

通过CameraOutputCapability类中的photoProfiles属性,可获取当前设备支持的拍照输出流,通过createPhotoOutput方法创建拍照输出流。

复制代码
function getPhotoOutput(cameraManager: camera.CameraManager, 
                        cameraOutputCapability: camera.CameraOutputCapability): camera.PhotoOutput | undefined {
  let photoProfilesArray: Array<camera.Profile> = cameraOutputCapability.photoProfiles;
  if (!photoProfilesArray) {
    console.error("createOutput photoProfilesArray == null || undefined");
  }
  let photoOutput: camera.PhotoOutput | undefined = undefined;
  try {
    photoOutput = cameraManager.createPhotoOutput(photoProfilesArray[0]);
  } catch (error) {
    let err = error as BusinessError;
    console.error(`Failed to createPhotoOutput. error: ${JSON.stringify(err)}`);
  }
  return photoOutput;
}
  1. 查询当前设备当前模式是否支持动态照片能力。

说明

查询是否支持动态照片前需要先完成相机会话配置、提交和启动会话,详细开发步骤请参考会话管理

复制代码
function isMovingPhotoSupported(photoOutput: camera.PhotoOutput): boolean {
  let isSupported: boolean = false;
  try {
    isSupported = photoOutput.isMovingPhotoSupported();
  } catch (error) {
    // 失败返回错误码error.code并处理。
    let err = error as BusinessError;
    console.error(`The isMovingPhotoSupported call failed. error code: ${err.code}`);
  }
  return isSupported;
}
  1. 使能动态照片拍照能力。

    function enableMovingPhoto(photoOutput: camera.PhotoOutput): void {
    try {
    photoOutput.enableMovingPhoto(true);
    } catch (error) {
    // 失败返回错误码error.code并处理。
    let err = error as BusinessError;
    console.error(The enableMovingPhoto call failed. error code: ${err.code});
    }
    }

  2. 触发拍照,与普通拍照方式相同

3.12.2 状态监听

在相机应用开发过程中,可以随时监听动态照片拍照输出流状态。通过注册photoAsset的回调函数获取监听结果,photoOutput创建成功时即可监听。

复制代码
let context = getContext(this);
let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);

async function mediaLibSavePhoto(photoAsset: photoAccessHelper.PhotoAsset): Promise<void> {
  try {
    let assetChangeRequest: photoAccessHelper.MediaAssetChangeRequest = new photoAccessHelper.MediaAssetChangeRequest(photoAsset);
    assetChangeRequest.saveCameraPhoto();
    await phAccessHelper.applyChanges(assetChangeRequest);
    console.info('apply saveCameraPhoto successfully');
  } catch (err) {
    console.error(`apply saveCameraPhoto failed with error: ${err.code}, ${err.message}`);
  }
}

function onPhotoOutputPhotoAssetAvailable(photoOutput: camera.PhotoOutput): void {
  photoOutput.on('photoAssetAvailable', (err: BusinessError, photoAsset: photoAccessHelper.PhotoAsset): void => {
    if (err) {
      console.info(`photoAssetAvailable error: ${JSON.stringify(err)}.`);
      return;
    }
    console.info('photoOutPutCallBack photoAssetAvailable');
    // 调用媒体库落盘接口保存一阶段图和动态照片视频。
    mediaLibSavePhoto(photoAsset);
  });
}

3.13 相机基础动效(ArkTS)

在使用相机过程中,如相机模式切换,前后置镜头切换等场景,不可避免出现预览流替换,为优化用户体验,可合理使用动效过渡。本文主要介绍如何使用预览流截图,并通过ArkUI提供的显示动画能力实现下方三种核心场景动效:

  • 模式切换动效,使用预览流截图做模糊动效过渡。

    图片为从录像模式切换为拍照模式的效果。

  • 前后置切换动效,使用预览流截图做翻转模糊动效过渡。

    图片为从前置摄像头切换为后置摄像头的效果。

  • 拍照闪黑动效,使用闪黑组件覆盖预览流实现闪黑动效过渡。

    图片为点击完成拍摄的效果。

3.13.1 闪黑动效

使用组件覆盖的形式实现闪黑效果。

  1. 导入依赖,需要导入相机框架、图片、ArkUI相关领域依赖。

    import { curves } from '@kit.ArkUI';

  2. 构建闪黑组件。

此处定义一个闪黑组件,在拍照闪黑及前后置切换时显示,用来遮挡XComponent组件。

属性定义:

复制代码
@State isShowBlack: boolean = false; // 是否显示闪黑组件。
@StorageLink('captureClick') @Watch('onCaptureClick') captureClickFlag: number = 0; // 拍照闪黑动效入口。
@State flashBlackOpacity: number = 1; // 闪黑组件透明度。

闪黑组件的实现逻辑参考:

复制代码
// 拍照闪黑及前后置切换时显示,用来遮挡XComponent组件。
if (this.isShowBlack) {
  Column()
    .key('black')
    .width(px2vp(1080)) // 与预览流XComponent宽高保持一致,图层在预览流之上,截图组件之下。
    .height(px2vp(1920))
    .backgroundColor(Color.Black)
    .opacity(this.flashBlackOpacity)
}
  1. 实现闪黑动效。

    function flashBlackAnim() {
    console.info('flashBlackAnim E');
    this.flashBlackOpacity = 1; // 闪黑组件不透明。
    this.isShowBlack = true; // 显示闪黑组件。
    animateToImmediately({
    curve: curves.interpolatingSpring(1, 1, 410, 38),
    delay: 50, // 延时50ms,实现黑屏。
    onFinish: () => {
    this.isShowBlack = false; // 闪黑组件下树。
    this.flashBlackOpacity = 1;
    console.info('flashBlackAnim X');
    }
    }, () => {
    this.flashBlackOpacity = 0; // 闪黑组件从不透明到透明。
    })
    }

  2. 触发闪黑动效。

点击或触控拍照按钮,更新StorageLink绑定CaptureClick的值,触发onCaptureClick方法,动效开始播放。

复制代码
onCaptureClick(): void {
  console.info('onCaptureClick');
    console.info('onCaptureClick');
    this.flashBlackAnim();
}

3.13.2 模糊动效

通过预览流截图,实现模糊动效,从而完成模式切换,或是前后置切换的动效。

  1. 导入依赖,需要导入相机框架、图片、ArkUI相关领域依赖。

    import { camera } from '@kit.CameraKit';
    import { image } from '@kit.ImageKit';
    import { curves } from '@kit.ArkUI';

  2. 获取预览流截图。

预览流截图通过图形提供的image.createPixelMapFromSurface接口实现,surfaceId为当前预览流的surfaceId,size为当前预览流profile的宽高。创建截图工具类(ts文件),导入依赖,导出获取截图方法供页面使用,截图工具类实现参考:

复制代码
import { image } from '@kit.ImageKit';

export class BlurAnimateUtil {
  public static surfaceShot: image.PixelMap;

  /**
   * 获取surface截图
   * @param surfaceId
   * @returns
   */
  public static async doSurfaceShot(surfaceId: string) {
    console.info(`doSurfaceShot surfaceId:${surfaceId}.`);
    if (surfaceId === '') {
      console.error('surface not ready!');
      return;
    }
    try {
      if (this.surfaceShot) {
        await this.surfaceShot.release();
      }
      this.surfaceShot = await image.createPixelMapFromSurface(surfaceId, {
        size: { width: 1920, height: 1080 }, // 取预览流profile的宽高。
        x: 0,
        y: 0
      });
      let imageInfo: image.ImageInfo = await this.surfaceShot.getImageInfo();
      console.info('doSurfaceShot surfaceShot:' + JSON.stringify(imageInfo.size));
    } catch (err) {
      console.error(JSON.stringify(err));
    }
  }

  /**
   * 获取doSurfaceShot得到的截图
   * @returns
   */
  public static getSurfaceShot(): image.PixelMap {
    return this.surfaceShot;
  }
}
  1. 构建截图组件。

此处定义一个截图组件,置于预览流XComponent组件之上,用来遮挡XComponent组件。

属性定义:

复制代码
@State isShowBlur: boolean = false; // 是否显示截图组件。
@StorageLink('modeChange') @Watch('onModeChange') modeChangeFlag: number = 0; // 模式切换动效触发入口。
@StorageLink('switchCamera') @Watch('onSwitchCamera') switchCameraFlag: number = 0;// 前后置切换动效触发入口。
@StorageLink('frameStart') @Watch('onFrameStart') frameStartFlag: number = 0; // 动效消失入口。
@State screenshotPixelMap: image.PixelMap | undefined = undefined; // 截图组件PixelMap。
@State surfaceId: string = ''; // 当前预览流XComponent的surfaceId。
@StorageLink('curPosition') curPosition: number = 0; // 当前镜头前后置状态。
@State shotImgBlur: number = 0; // 截图组件模糊度。
@State shotImgOpacity: number = 1; // 截图组件透明度。
@State shotImgScale: ScaleOptions = { x: 1, y: 1 }; // 截图组件比例。
@State shotImgRotation: RotateOptions = { y: 0.5, angle: 0 } // 截图组件旋转角度。

截图组件的实现参考:

复制代码
// 截图组件,置于预览流XComponent组件之上。
if (this.isShowBlur) {
  Column() {
    Image(this.screenshotPixelMap)
      .blur(this.shotImgBlur)
      .opacity(this.shotImgOpacity)
      .rotate(this.shotImgRotation)// ArkUI提供,用于组件旋转。
      .scale(this.shotImgScale)
      .width(px2vp(1080)) // 与预览流XComponent宽高保持一致,图层在预览流之上。
      .height(px2vp(1920))
      .syncLoad(true)
  }
  .width(px2vp(1080))
  .height(px2vp(1920))
}
  1. (按实际情况选择)实现模糊出现动效。

模式切换动效分两段实现,模糊出现动效和模糊消失动效。

模糊出现动效:用户点击或触控事件触发预览流截图,显示截图组件,截图清晰到模糊,覆盖旧预览流。

注意:由于图形提供的image.createPixelMapFromSurface接口是截取surface内容获取PixelMap,其内容和XComponent组件绘制逻辑不同,需要根据前后置 镜头做不同的图片内容旋转补偿组件旋转补偿

复制代码
async function showBlurAnim() {
  console.info('showBlurAnim E');
  // 获取已完成的surface截图。
  let shotPixel = BlurAnimateUtil.getSurfaceShot();
  // 后置。
  if (this.curPosition === 0) {
    console.info('showBlurAnim BACK');
    // 直板机后置截图初始内容旋转补偿90°。
    await shotPixel.rotate(90); //ImageKit提供,用于图片内容旋转。
    // 直板机后置截图初始组件旋转补偿0°。
    this.shotImgRotation = { y: 0.5, angle: 0 };
  } else {
    console.info('showBlurAnim FRONT');
    // 直板机前置截图内容旋转补偿270°。
    await shotPixel.rotate(270);
    // 直板机前置截图组件旋转补偿180°。
    this.shotImgRotation = { y: 0.5, angle: 180 };
  }
  this.screenshotPixelMap = shotPixel;
  // 初始化动效参数。
  this.shotImgBlur = 0; // 无模糊。
  this.shotImgOpacity = 1; // 不透明。
  this.isShowBlur = true;  // 显示截图组件。
  animateToImmediately(
    {
      duration: 200,
      curve: Curve.Friction,
      onFinish: async () => {
        console.info('showBlurAnim X');
      }
    },
    () => {
      this.shotImgBlur = 48; // 截图组件模糊度变化动效。
    }
  );
}
  1. 实现模糊消失动效。

模糊消失动效:由新模式预览流首帧回调on('frameStart')触发,截图组件模糊到清晰,显示新预览流。

复制代码
function hideBlurAnim(): void {
  this.isShowBlack = false;
  console.info('hideBlurAnim E');
  animateToImmediately({
    duration: 200,
    curve: Curve.FastOutSlowIn,
    onFinish: () => {
      this.isShowBlur = false; // 模糊组件下树。
      this.shotImgBlur = 0;
      this.shotImgOpacity = 1;
      console.info('hideBlurAnim X');
    }
  }, () => {
    // 截图透明度变化动效。
    this.shotImgOpacity = 0; // 截图组件透明度变化动效。
  });
}
  1. (按实际情况选择)实现模糊翻转动效。

模糊翻转动效分两段实现,模糊翻转动效和模糊消失动效,其中模糊消失动效同第5步。

模糊翻转动效:分两段组件翻转实现,先向外翻转90°再向内翻转90°,同时还执行了模糊度、透明度、比例缩放等动效。

为保证预览流在翻转时不露出,需要构建一个闪黑组件用于遮挡XComponent组件,构建方式参考闪黑动效-步骤2。

复制代码
/**
 * 先向外翻转90°,前后置切换触发
 */
async function rotateFirstAnim() {
  console.info('rotateFirstAnim E');
  // 获取已完成的surface截图。
  let shotPixel = BlurAnimateUtil.getSurfaceShot();
  // 后置切前置。
  if (this.curPosition === 1) {
    console.info('rotateFirstAnim BACK');
    // 直板机后置切前置截图初始内容旋转补偿90°。
    await shotPixel.rotate(90); //ImageKit提供,用于图片内容旋转。
    // 直板机后置切前置截图初始组件旋转补偿0°。
    this.shotImgRotation = { y: 0.5, angle: 0 };
  } else {
    console.info('rotateFirstAnim FRONT');
    // 直板机前置切后置截图初始内容旋转补偿270°。
    await shotPixel.rotate(270);
    // 直板机前置切后置截图初始组件旋转补偿180°。
    this.shotImgRotation = { y: 0.5, angle: 180 };
  }
  this.screenshotPixelMap = shotPixel;
  this.isShowBlack = true; // 显示闪黑组件,覆盖预览流保证视觉效果。
  this.isShowBlur = true; // 显示截图组件。
  animateToImmediately(
    {
      duration: 200,
      delay: 50, // 时延保证组件缩放模糊动效先行,再翻转,视觉效果更好。
      curve: curves.cubicBezierCurve(0.20, 0.00, 0.83, 1.00),
      onFinish: () => {
        console.info('rotateFirstAnim X');
        // 在onFinish后触发二段翻转。
        this.rotateSecondAnim();
      }
    },
    () => {
      // 截图向外翻转动效。
      if (this.curPosition === 1) {
        this.shotImgRotation = { y: 0.5, angle: 90 };
      } else {
        this.shotImgRotation = { y: 0.5, angle: 270 };
      }
    }
  )
}

/**
 * 再向内翻转90°
 */
async function rotateSecondAnim() {
  console.info('rotateSecondAnim E');
  // 获取已完成的surface截图。
  let shotPixel = BlurAnimateUtil.getSurfaceShot();
  // 后置。
  if (this.curPosition === 1) {
    // 直板机后置镜头内容旋转补偿90°。
    await shotPixel.rotate(90);
    // 组件旋转调整为-90°,保证二段翻转后,图片不是镜像的。
    this.shotImgRotation = { y: 0.5, angle: 90 };
  } else { // 前置。
    // 直板机前置截图内容旋转补偿270°。
    await shotPixel.rotate(270);
    // 直板机前置截图组件旋转补偿180°。
    this.shotImgRotation = { y: 0.5, angle: 180 };
  }
  this.screenshotPixelMap = shotPixel;
  animateToImmediately(
    {
      duration: 200,
      curve: curves.cubicBezierCurve(0.17, 0.00, 0.20, 1.00),
      onFinish: () => {
        console.info('rotateSecondAnim X');
      }
    },
    () => {
      // 截图向内翻转动效,翻转至初始状态。
      if (this.curPosition === 1) {
        this.shotImgRotation = { y: 0.5, angle: 0 };
      } else {
        this.shotImgRotation = { y: 0.5, angle: 180 };
      }
    }
  )
}

/**
 * 向外翻转90°同时
 */
function blurFirstAnim() {
  console.info('blurFirstAnim E');
  // 初始化动效参数。
  this.shotImgBlur = 0; //无模糊。
  this.shotImgOpacity = 1; //不透明。
  this.shotImgScale = { x: 1, y: 1 };
  animateToImmediately(
    {
      duration: 200,
      curve: Curve.Sharp,
      onFinish: () => {
        console.info('blurFirstAnim X');
        this.blurSecondAnim();
      }
    },
    () => {
      // 截图模糊动效。
      this.shotImgBlur = 48;
      // 截图比例缩小动效。
      this.shotImgScale = { x: 0.75, y: 0.75 };
    }
  );
}

/**
 * 向内翻转90°同时
 */
function blurSecondAnim() {
  console.info('blurSecondAnim E');
  animateToImmediately(
    {
      duration: 200,
      curve: Curve.Sharp,
      onFinish: () => {
        console.info('blurSecondAnim X');
      }
    },
    () => {
      // 截图比例恢复动效。
      this.shotImgScale = { x: 1, y: 1 };
    }
  )
}
  1. 按需触发动效。

模式切换动效触发:点击或触控模式按钮立即执行doSurfaceShot截图方法,更新StorageLink绑定modeChange的值,触发onModeChange方法,开始动效。

复制代码
onModeChange(): void {
  console.info('onModeChange');
  this.showBlurAnim();
}

前后置切换动效触发:点击或触控前后置切换按钮立即执行doSurfaceShot截图方法,更新StorageLink绑定switchCamera的值,触发onSwitchCamera方法,开始动效。

复制代码
onSwitchCamera(): void {
  console.info('onSwitchCamera');
  this.blurFirstAnim();
  this.rotateFirstAnim();
}

模糊消失动效触发:监听预览流首帧回调on('frameStart'),更新StorageLink绑定frameStart的值,触发onFrameStart方法,开始动效。

复制代码
onFrameStart(): void {
  console.info('onFrameStart');
  this.hideBlurAnim();
}

3.14 在Worker线程中使用相机(ArkTS)

Worker主要作用是为应用程序提供一个多线程的运行环境,可满足应用程序在执行过程中与主线程分离,在后台线程中运行一个脚本进行耗时操作,极大避免类似于计算密集型或高延迟的任务阻塞主线程的运行。

通常开发者使用相机功能需要创建相机会话,并持续接收处理预览流、拍照流、录像流等从而实现相关相机功能,这些密集型操作如果都放在主线程即UI线程,可能会阻塞UI绘制,推荐开发者在worker线程中实现相机功能。

3.14.1 开发步骤

  1. 创建worker线程文件,配置worker。

DevEco Studio支持一键生成Worker,在对应的{moduleName}目录下任意位置,点击鼠标右键 > New > Worker,即可自动生成Worker的模板文件及配置信息,无需再手动在build-profile.json5中进行相关配置 。

CameraWorker.ets实现参考:

复制代码
import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';
import CameraService from '../CameraService';

const workerPort: ThreadWorkerGlobalScope = worker.workerPort;

// 自定义消息格式。
interface MessageInfo {
  hasResolve: boolean;
  type: string;
  context: Context; // 注意worker线程中无法使用getContext()直接获取宿主线程context,需要通过消息从宿主线程通信到worker线程使用。
  surfaceId: string;
}

workerPort.onmessage = async (e: MessageEvents) => {
  const messageInfo: MessageInfo = e.data;
  console.info(`worker onmessage type:${messageInfo.type}`)
  if ('initCamera' === messageInfo.type) {
    // 在worker线程中收到宿主线程初始化相机的消息。
    console.info(`worker initCamera surfaceId:${messageInfo.surfaceId}`)
    // 在worker线程中初始化相机。
    await CameraService.initCamera(messageInfo.context, messageInfo.surfaceId);
  } else if ('releaseCamera' === messageInfo.type) {
    // 在worker线程中收到宿主线程释放相机的消息。
    console.info('worker releaseCamera.');
    // 在worker线程中释放相机。
    await CameraService.releaseCamera();
  }
}

workerPort.onmessageerror = (e: MessageEvents) => {
}

workerPort.onerror = (e: ErrorEvent) => {
}
  1. 创建相机服务代理类,调用CameraKit方法都放在这个类里执行。

    import { BusinessError } from '@kit.BasicServicesKit';
    import { camera } from '@kit.CameraKit';

    class CameraService {
    private imageWidth: number = 1920;
    private imageHeight: number = 1080;
    private cameraManager: camera.CameraManager | undefined = undefined;
    private cameras: Array<camera.CameraDevice> | Array<camera.CameraDevice> = [];
    private cameraInput: camera.CameraInput | undefined = undefined;
    private previewOutput: camera.PreviewOutput | undefined = undefined;
    private photoOutput: camera.PhotoOutput | undefined = undefined;
    private session: camera.PhotoSession | camera.VideoSession | undefined = undefined;

    // 初始化相机。
    async initCamera(context: Context, surfaceId: string): Promise<void> {
    console.info(initCamera surfaceId: ${surfaceId});
    try {
    await this.releaseCamera();
    // 获取相机管理器实例。
    this.cameraManager = camera.getCameraManager(context);
    if (this.cameraManager === undefined) {
    console.error('cameraManager is undefined');
    return;
    }
    this.cameras = this.cameraManager.getSupportedCameras();

    复制代码
       // 创建cameraInput输出对象。
       this.cameraInput = this.cameraManager.createCameraInput(this.cameras[0]);
       if (this.cameraInput === undefined) {
         console.error('Failed to create the camera input.');
         return;
       }
       // 打开相机。
       await this.cameraInput.open();
    
       let previewProfile: camera.Profile = {
         format: camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP,
         size: {
           width: this.imageWidth,
           height: this.imageHeight
         }
       };
       // 创建预览流输出。
       this.previewOutput = this.cameraManager.createPreviewOutput(previewProfile, surfaceId);
       if (this.previewOutput === undefined) {
         console.error('Failed to create the preview stream.');
         return;
       }
    
       let photoProfile: camera.Profile = {
         format: camera.CameraFormat.CAMERA_FORMAT_JPEG,
         size: {
           width: this.imageWidth,
           height: this.imageHeight
         }
       };
       // 创建拍照流输出。
       this.photoOutput = this.cameraManager.createPhotoOutput(photoProfile);
       if (this.photoOutput === undefined) {
         console.error('Failed to create the photoOutput.');
         return;
       }
    
       // 创建相机会话,启动会话。
       this.session = this.cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession;
       this.session.beginConfig();
       this.session.addInput(this.cameraInput);
       this.session.addOutput(this.previewOutput);
       this.session.addOutput(this.photoOutput);
       await this.session.commitConfig();
       await this.session.start();
     } catch (error) {
       let err = error as BusinessError;
       console.error(`initCamera fail: ${JSON.stringify(err)}`);
     }

    }

    // 释放相机资源。
    async releaseCamera(): Promise<void> {
    console.info('releaseCamera is called');
    try {
    await this.previewOutput?.release();
    await this.photoOutput?.release();
    await this.session?.release();
    await this.cameraInput?.close();
    } catch (error) {
    let err = error as BusinessError;
    console.error(releaseCamera fail: error: ${JSON.stringify(err)});
    } finally {
    this.previewOutput = undefined;
    this.photoOutput = undefined;
    this.cameraManager = undefined;
    this.session = undefined;
    this.cameraInput = undefined;
    }
    console.info('releaseCamera success');
    }
    }

    export default new CameraService();

  2. 创建组件,用于显示预览流,在页面相关生命周期中构造ThreadWorker实例,在worker线程中完成相机初始化和释放。

    import { worker } from '@kit.ArkTS';

    @Entry
    @Component
    struct Index {
    private mXComponentController: XComponentController = new XComponentController();
    private surfaceId: string = '';
    @State imageWidth: number = 1920;
    @State imageHeight: number = 1080;
    // 创建ThreadWorker对象获取worker实例。
    private workerInstance: worker.ThreadWorker = new worker.ThreadWorker('entry/ets/workers/CameraWorker.ets');

    onPageShow(): void {
    if ('' !== this.surfaceId) {
    // 通过worker实例向worker线程发送消息初始化相机。
    this.workerInstance.postMessage({
    type: 'initCamera',
    context: getContext(this),
    surfaceId: this.surfaceId,
    })
    }
    }

    onPageHide(): void {
    // 通过worker实例向worker线程发送消息销毁相机。
    this.workerInstance.postMessage({
    type: 'releaseCamera',
    })
    }

    build() {
    Column() {
    Column() {
    XComponent({
    id: 'componentId',
    type: XComponentType.SURFACE,
    controller: this.mXComponentController
    })
    .onLoad(async () => {
    console.info('onLoad is called');
    // 初始化XComponent获取预览流surfaceId。
    this.surfaceId = this.mXComponentController.getXComponentSurfaceId();
    let surfaceRect: SurfaceRect = {
    surfaceWidth: this.imageHeight,
    surfaceHeight: this.imageWidth
    };
    this.mXComponentController.setXComponentSurfaceRect(surfaceRect);
    console.info(onLoad surfaceId: ${this.surfaceId});
    if (!this.workerInstance) {
    console.error('create stage worker failed');
    return;
    }
    // 宿主线程向worker线程发送初始化相机消息。
    this.workerInstance.postMessage({
    type: 'initCamera',
    context: getContext(this), // 将宿主线程的context传给worker线程使用。
    surfaceId: this.surfaceId, // 将surfaceId传给worker线程使用。
    })
    })// The width and height of the surface are opposite to those of the XComponent.
    .width(px2vp(this.imageHeight))
    .height(px2vp(this.imageWidth))

    复制代码
       }.justifyContent(FlexAlign.Center)
       .height('90%')
    
       Text('WorkerDemo')
         .fontSize(36)
     }
     .justifyContent(FlexAlign.End)
     .height('100%')
     .width('100%')

    }
    }

3.14.2 trace对比

不使用worker:

使用woker:

3.15 适配相机旋转角度(ArkTS)

屏幕处于不同的屏幕状态时,原始图像需旋转不同的角度,以确保图像在合适的方向显示,效果如图所示。

开发者在预览、拍照、录像等不同场景下,如何适配相机的旋转角度。

3.15.1 创建会话

  1. 导入相机等相关模块。

    import { camera } from '@kit.CameraKit';
    import { BusinessError } from '@kit.BasicServicesKit';

  2. 创建Session会话。

相机使用预览等功能前,均需创建相机会话,调用CameraManager类中的createSession方法创建一个会话,创建会话时需指定创建SceneMode为NORMAL_PHOTO或NORMAL_VIDEO,创建的session处于拍照或者录像模式。

复制代码
function createPhotoSession(cameraManager: camera.CameraManager): camera.Session | undefined {
  let session: camera.Session | undefined = undefined;
  try {
    session = cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession;
  } catch (error) {
    let err = error as BusinessError;
    hilog.error(`Failed to create the session instance. error: ${JSON.stringify(err)}`);
  }
  return session;
}

function createVideoSession(cameraManager: camera.CameraManager): camera.Session | undefined {
  let session: camera.Session | undefined = undefined;
  try {
    session = cameraManager.createSession(camera.SceneMode.NORMAL_VIDEO) as camera.PhotoSession;
  } catch (error) {
    let err = error as BusinessError;
    hilog.error(`Failed to create the session instance. error: ${JSON.stringify(err)}`);
  }
  return session;
}

3.15.2 预览

完成会话创建后,可根据实际需求,配置输出流。

  1. 调用PreviewOutput类中的getPreviewRotation接口,获取预览旋转角度

displayRotation:显示设备的屏幕旋转角度,可通过display.getDefaultDisplaySync获取Display对象并读取其rotation属性值,并将对应角度填入。

例:Display.rotation = 1,表示显示设备屏幕顺时针旋转为90°,此处displayRotation填入90。

复制代码
import { display } from '@kit.ArkUI'; 

let initDisplayRotation = display.getDefaultDisplaySync().rotation;
let imageRotation = initDisplayRotation * camera.ImageRotation.ROTATION_90;

该接口需要在session调用commitConfig完成配流后调用,如果存在异步执行的情况,previewOutput未添加到session里或者已调用的session.release,导致两者关系未绑定,此时调用getPreviewRotation,则会调用失败,并抛出错误码CameraErrorCode.SERVICE_FATAL_ERROR

复制代码
function getPreviewRotation(previewOutput: camera.PreviewOutput, imageRotation : camera.ImageRotation): camera.ImageRotation {
  let previewRotation: camera.ImageRotation = camera.ImageRotation.ROTATION_0;
  try {
    previewRotation = previewOutput.getPreviewRotation(imageRotation);
    hilog.log(`Preview rotation is: ${previewRotation}`);
  } catch (error) {
    // 失败返回错误码error.code并处理
    let err = error as BusinessError;
    hilog.error(`The previewOutput.getPreviewRotation call failed. error code: ${err.code}`);
  }
  return previewRotation;
}
  1. 调用PreviewOutput类中的setPreviewRotation,设置图像的预览旋转角度。

该接口需要在session调用commitConfig完成配流后调用,如果多次。

复制代码
function setPreviewRotation(previewOutput: camera.PreviewOutput, previewRotation : camera.ImageRotation, isDisplayLocked: boolean): void {
  try {
    previewOutput.setPreviewRotation(previewRotation, isDisplayLocked);
  } catch (error) {
    // 失败返回错误码error.code并处理
    let err = error as BusinessError;
    hilog.error(`The previewOutput.setPreviewRotation call failed. error code: ${err.code}`);
  }
}

3.15.3 录像

完成会话创建后,开发者可根据实际需求,配置输出流。录像的旋转角度与重力方向(即设备旋转角度)相关。

  1. 调用VideoOutput类中的getVideoRotation可以获取到录像的旋转角度。

该接口需要在session调用commitConfig完成配流后调用。

deviceDegree:设备旋转角度。获取方式请见计算设备旋转角度

复制代码
function getVideoRotation(videoOutput: camera.VideoOutput, deviceDegree: number): camera.ImageRotation {
  let videoRotation: camera.ImageRotation = camera.ImageRotation.ROTATION_0;
  try {
    videoRotation = videoOutput.getVideoRotation(deviceDegree);
    hilog.info(`Video rotation is: ${videoRotation}`);
  } catch (error) {
    // 失败返回错误码error.code并处理
    let err = error as BusinessError;
    hilog.error(`The videoOutput.getVideoRotation call failed. error code: ${err.code}`);
  }
  return videoRotation;
}
  1. 将录像的旋转角度写入AVMetadata.videoOrientation。

  2. 其余参数的配置及启动录像。

3.15.4 计算设备旋转角度

当前可通过调用once(type: SensorId.GRAVITY, callback: Callback<GravityResponse>)获取一次重力传感器在x、y、z三个方向上的数据,计算得出设备旋转角度deviceDegree,示例如下所示。

如果无法获得重力传感器数据,需要申请重力传感器权限ohos.permission.ACCELEROMETER。权限申请请参考声明权限,如何获取传感器数据请参考传感器开发指导

复制代码
import { Decimal } from '@kit.ArkTS';
import { sensor } from '@kit.SensorServiceKit';
import { BusinessError } from '@ohos.base';

getRealData(data: sensor.GravityResponse): number {
  let getDeviceDegree: number = 0;
  hilog.info('Succeeded in invoki e. X-coordinate component: ' + data.x);
  hilog.info('Succeeded in invoking once. Y-coordinate component: ' + data.y);
  hilog.info('Succeeded in invoking once. Z-coordinate component: ' + data.z);
  let x = data.x;
  let y = data.y;
  let z = data.z;
  if ((x * x + y * y) * 3 < z * z) {
    return getDeviceDegree;
  } else {
    let sd: Decimal = Decimal.atan2(y, -x);
    let sc: Decimal = Decimal.round(Number(sd) / 3.141592653589 * 180)
    getDeviceDegree = 90 - Number(sc);
    getDeviceDegree = getDeviceDegree >= 0 ? getDeviceDegree % 360 : getDeviceDegree % 360 + 360;
  }
  return getDeviceDegree;
}

getGravity() : Promise<number> {
  sensor.getSensorList((error: BusinessError, data: Array<sensor.Sensor>) => {
    for (let i = 0; i < data.length; i++) {
      if (data[i].sensorId === sensor.SensorId.GRAVITY) {
        this.isSupported = true;
        break;
      }
    }});
  if (this.isSupported === true) {
    const promise: Promise<number> = new Promise((resolve, reject) => {
      sensor.once(sensor.SensorId.GRAVITY, (data: sensor.GravityResponse) => {
          resolve(this.getRealData(data));
      });
    })
    return promise;
  } else {
    const promise: Promise<number> = new Promise((resolve, reject) => {
      sensor.once(sensor.SensorId.ACCELEROMETER, (data: sensor.AccelerometerResponse) => {
        resolve(this.getRealData(data as sensor.GravityResponse));
      });
    })
    return promise;
  }
}

3.16 安全相机(ArkTS)

安全相机主要为银行等有活体检测等安全诉求的应用提供,安全相机的使用需要加密算法框架及可信应用服务。

应用具体使用步骤如下:

  • 通过Camera Kit打开安全摄像头,成功打开安全摄像头后,Camera Kit会返回给应用一个安全摄像头序列号
  • 通过DeviceSecurity Kit来创建证明密钥(安全摄像头序列号会作为入参)、初始化证明会话。DeviceSecurity Kit初始化证明会话完成后会返回给应用匿名证书链
  • 通过Camera Kit配置安全相机输入输出流,重点是配置安全数据流,注册安全数据流每帧安全图像回调监听。
  • 解析安全数据流每帧安全图像,在服务器侧完成安全图像的签名验证。

说明

当前文档主要说明通过Camera Kit完成的步骤,证明会话相关步骤需通过DeviceSecurity Kit完成,具体可参考可信应用服务-安全摄像头

3.16.1 开发步骤

  1. 导入依赖,需要导入相机框架相关领域依赖。

    import { camera } from '@kit.CameraKit';
    import { image } from '@kit.ImageKit';

  2. 选择支持安全相机的设备。

通过CameraManager类中的getSupportedSceneModes方法,可以获取当前设备支持的所有模式,如果当前设备支持安全相机模式,即可使用该设备做后续安全相机操作。

当前安全相机仅支持手机前置镜头。

复制代码
function isSecureCamera(cameraManager: camera.CameraManager, cameraDevice: camera.CameraDevice): boolean {
  let sceneModes: Array<camera.SceneMode> = cameraManager.getSupportedSceneModes(cameraDevice);
  const secureMode = sceneModes.find(mode => mode === camera.SceneMode.SECURE_PHOTO);
  if (secureMode) {
    console.info('current device support secure camera!');
    return true;
  } else {
    console.info('current device not support secure camera!');
    return false;
  }
}

let secureCamera: camera.CameraDevice;

function getSecureCamera(cameraManager: camera.CameraManager): void {
  let cameraArray: Array<camera.CameraDevice> = cameraManager.getSupportedCameras();
  for (let index = 0; index < cameraArray.length; index++) {
    if (isSecureCamera(cameraManager, cameraArray[index])) {
      secureCamera = cameraArray[index];
    }
  }
}
  1. 查询相机设备在安全模式下支持的输出能力。

通过CameraManagergetSupportedOutputCapability方法,可获取设备在安全模式下支持的输出能力。

当前安全相机仅支持输出预览流,推荐预览流使用640 * 480分辨率。

复制代码
function getSupportedOutputCapability(cameraManager: camera.CameraManager, secureCamera: camera.CameraDevice): void {
  let outputCap: camera.CameraOutputCapability =
    cameraManager.getSupportedOutputCapability(secureCamera, camera.SceneMode.SECURE_PHOTO);
  let previewProfilesArray: Array<camera.Profile> = outputCap.previewProfiles;
}
  1. 创建设备输入输出。

安全相机需要创建两路输出流:

安全数据流没有单独的数据类型,同属于预览流,输出能力与预览流保持一致,创建ImageReceiver仅支持JPEG格式。

复制代码
async function createInputAndOutputs(cameraManager: camera.CameraManager, 
                                     secureCamera: camera.CameraDevice,
                                     previewProfile: camera.Profile,
                                     previewSurfaceId: string): Promise<void> {
  // 创建输入流
  let cameraInput: camera.CameraInput = cameraManager.createCameraInput(secureCamera);
  // 创建普通预览输出流
  let previewOutput: camera.PreviewOutput = cameraManager.createPreviewOutput(previewProfile, previewSurfaceId);
  // 创建安全数据输出流
  const receiver: image.ImageReceiver =
    image.createImageReceiver({ width: previewProfile.size.width, height: previewProfile.size.height },
                              image.ImageFormat.JPEG, 8);
  const secureSurfaceId: string = await receiver.getReceivingSurfaceId();
  let secureOutput: camera.PreviewOutput = cameraManager.createPreviewOutput(previewProfile, secureSurfaceId);
}
  1. 打开安全设备。

CameraInput提供了open(isSecureEnabled)方法用于打开安全相机并返回安全摄像头序列号,该序列号是安全模块创建证明会话的必须参数。

仅当isSecureEnabled为true时,才会打开安全相机,并有安全序列号返回。

复制代码
async function openCamera(cameraInput: camera.CameraInput) {
  const seqId: bigint = await cameraInput.open(true);
}
  1. 使用Device Security Kit的能力,创建证明密钥、打开证明会话。请参考Device Security Kit(设备安全服务)的开发指导:可信应用服务-安全摄像头

  2. 创建安全相机会话,配流启流。

创建安全相机模式的会话,将输入流、输出流加入会话,需要将安全数据流通过SecureSessionaddSecureOutput方法标记成安全输出。

复制代码
async function openSession(cameraManager: camera.CameraManager,
                           cameraInput: camera.CameraInput,
                           previewOutput: camera.PreviewOutput,
                           secureOutput: camera.PreviewOutput): Promise<void> {
  try {
    let secureSession: camera.SecureSession = cameraManager.createSession(camera.SceneMode.SECURE_PHOTO);
    if (secureSession === undefined) {
      console.error('create secureSession failed!');
    }
    secureSession.beginConfig();
    secureSession.addInput(cameraInput);
    secureSession.addOutput(previewOutput);
    secureSession.addOutput(secureOutput);
    secureSession.addSecureOutput(secureOutput); // 把secureOutput标记成安全输出
    await secureSession.commitConfig();
    await secureSession.start();
  } catch (err) {
    console.error('openSession failed!');
  }
}
  1. 安全模式会话正常启动后,预览流和安全数据流数据逐帧上报,安全数据流每帧数据可以通过ImageReceiver注册imageArrival回调获取。

    function onBuffer(receiver: image.ImageReceiver): void {
    receiver.on('imageArrival', () => {
    // 从ImageReceiver读取下一张图片
    receiver.readNextImage().then((img: image.Image) => {
    // 从图像中获取组件缓存
    img.getComponent(image.ComponentType.JPEG).then((component: image.Component) => {
    // 安全数据流内容,应用通过解析该buffer内容完成签名认证
    const buffer = component.byteBuffer;
    console.info('Succeeded in getting component byteBuffer.');
    })
    })
    })
    }

解析安全数据流每帧安全图像,在服务器侧完成安全图像的签名验证。

如果有在端侧验证图像数据或地理位置数据签名的需求,可参考验证签名中与安全图像相关的部分。

  1. 释放安全相机,使用Sessionrelease方法。

3.17 动态调整预览帧率(ArkTS)

动态调整帧率是直播、视频等场景下控制预览效果的重要能力之一。应用可通过此能力,显性地控制流输出帧率,以适应不同帧率下的业务目标。

某些场景下降低帧率可在相机设备启用时降低功耗。

3.17.1 约束与限制

支持的帧率范围及帧率的设置依赖于硬件能力的实现,不同的硬件平台可能拥有不同的默认帧率。

3.17.2 开发流程

相机使用预览功能前,均需要创建相机会话。完成会话配置后,应用提交和开启会话,才可以开始调用相机相关功能。

流程图如下所示:

与普通的预览流程相比,动态调整预览帧率的注意点如图上标识:

  1. 调用createSession创建会话(Session)时,需要指定模式为NORMAL_PHOTO或NORMAL_VIDEO。

    仅当Session处于NORMAL_PHOTO或NORMAL_VIDEO模式时,支持调整预览流帧率。调整帧率的创建会话方式见创建Session会话并指定模式

  2. 动态调整帧率的操作,可在启动预览前后任意时刻调用。

  3. 动态调整帧率在预览里属于可选操作,可以完成:

    • 查询当前支持调整的帧率范围
    • 设置当前帧率
    • 获取当前生效的帧率设置

如何配置会话(Session)、释放资源,请参考会话管理 > 预览,或是完整流程示例。

3.17.3 创建Session会话并指定模式

相机使用预览等功能前,均需创建相机会话,调用CameraManagercreateSession创建一个会话。

创建会话时需指定SceneMode为NORMAL_PHOTO或NORMAL_VIDEO,创建出的Session处于拍照或录像模式。

以创建Session会话并指定为NORMAL_PHOTO模式为例:

复制代码
function createPhotoSession(cameraManager: camera.CameraManager): camera.Session | undefined {
  let session: camera.Session | undefined = undefined;
  try {
    // 创建Session会话并指定为NORMAL_PHOTO模式
    session = cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession;
  } catch (error) {
    let err = error as BusinessError;
    console.error(`Failed to create the session instance. error: ${JSON.stringify(err)}`);
  }
  return session;
}

3.17.4 调整帧率

  1. 调用PreviewOutputgetSupportedFrameRates,查询当前支持的帧率范围。

说明

需要在Session调用commitConfig完成配流之后调用。

复制代码
function getSupportedFrameRange(previewOutput: camera.PreviewOutput): Array<camera.FrameRateRange> {
// 获取支持的帧率范围,不同的硬件平台可能提供不同的帧率范围
  return previewOutput.getSupportedFrameRates();
}
  1. 根据实际开发需求,调用PreviewOutput类提供的setFrameRate接口对帧率进行动态调整。

说明

  • 需要在Session调用commitConfig完成配流之后调用。
  • 可在Session调用start启动预览前后任意时刻调用。
复制代码
function setFrameRate(previewOutput: camera.PreviewOutput, minFps: number, maxFps: number): void {
  try {
    previewOutput.setFrameRate(minFps, maxFps);
  } catch (error) {
    let err = error as BusinessError;
    console.error(`Failed to setFrameRate for previewOutput. error: ${JSON.stringify(err)}`);
  }
}
  1. (可选)通过PreviewOutput类提供的getActiveFrameRate接口查询已设置过并生效的帧率。

仅通过setFrameRate接口显性设置过帧率才可查询当前生效帧率信息。

复制代码
function getActiveFrameRange(previewOutput: camera.PreviewOutput): camera.FrameRateRange {
  return previewOutput.getActiveFrameRate();
}

3.18 使用相机预配置(ArkTS)

相机预配置(Preconfig),对常用的场景和分辨率进行了预配置集成,可简化开发相机应用流程,提高应用的开发效率。

系统针对拍照(PhotoSession)、录像(VideoSession)两类场景,提供了preconfig接口帮助开发者快速完成相机参数配置。推荐仅需要自定义拍照界面的无需开发专业相机应用的开发者,使用相机预配置功能快速开发应用。

以拍照(PhotoSession)为例,与遵循通用流程开发,有以下差异:

其他相关能力:

3.18.1 规格说明

系统提供了4种预配置类型(PreconfigType):

  • PRECONFIG_720P
  • PRECONFIG_1080P
  • PRECONFIG_4K
  • PRECONFIG_HIGH_QUALITY。

3种画幅比例规格(PreconfigRatio):

  • 1:1画幅(PRECONFIG_RATIO_1_1)
  • 4:3画幅(PRECONFIG_RATIO_4_3)
  • 16:9画幅(PRECONFIG_RATIO_16_9)。
    注意

由于不同的设备所支持的能力不同。使用相机预配置(preconfig)功能时,需要先调用canPreconfig检查对应的PreconfigType和PreconfigRatio的组合在当前设备上是否支持。

在不同的画幅比例下,其分辨率规格不同,详见下表。

  • 普通拍照模式下的预览输出

    预配置类型PreconfigType PRECONFIG_RATIO_1_1 PRECONFIG_RATIO_4_3 PRECONFIG_RATIO_16_9
    PRECONFIG_720P 720x720 960x720 1280x720
    PRECONFIG_1080P 1080x1080 1440x1080 1920x1080
    PRECONFIG_4K 1080x1080 1440x1080 1920x1080
    PRECONFIG_HIGH_QUALITY 1440x1440 1920x1440 2560x1440
  • 普通拍照模式下的拍照输出

    预配置类型PreconfigType PRECONFIG_RATIO_1_1 PRECONFIG_RATIO_4_3 PRECONFIG_RATIO_16_9
    PRECONFIG_720P 720x720 960x720 1280x720
    PRECONFIG_1080P 1080x1080 1440x1080 1920x1080
    PRECONFIG_4K 2160x2160 2880x2160 3840x2160
    PRECONFIG_HIGH_QUALITY 跟随Sensor(镜头)最大能力 跟随Sensor(镜头)最大能力 跟随Sensor(镜头)最大能力
  • 普通录像模式下的预览输出

    预配置类型PreconfigType PRECONFIG_RATIO_1_1 PRECONFIG_RATIO_4_3 PRECONFIG_RATIO_16_9
    PRECONFIG_720P 720x720 960x720 1280x720
    PRECONFIG_1080P 1080x1080 1440x1080 1920x1080
    PRECONFIG_4K 1080x1080 1440x1080 1920x1080
    PRECONFIG_HIGH_QUALITY 1080x1080 1440x1080 1920x1080
  • 普通录像模式下的录像输出

    预配置类型PreconfigType PRECONFIG_RATIO_1_1 PRECONFIG_RATIO_4_3 PRECONFIG_RATIO_16_9
    PRECONFIG_720P 720x720 960x720 1280x720
    PRECONFIG_1080P 1080x1080 1440x1080 1920x1080
    PRECONFIG_4K 2160x2160 2880x2160 3840x2160
    PRECONFIG_HIGH_QUALITY 2160x2160 2880x2160 3840x2160
  • 普通录像模式下的拍照输出

    预配置类型PreconfigType PRECONFIG_RATIO_1_1 PRECONFIG_RATIO_4_3 PRECONFIG_RATIO_16_9
    PRECONFIG_720P 跟随Sensor(镜头)最大能力 跟随Sensor(镜头)最大能力 跟随Sensor(镜头)最大能力
    PRECONFIG_1080P 跟随Sensor(镜头)最大能力 跟随Sensor(镜头)最大能力 跟随Sensor(镜头)最大能力
    PRECONFIG_4K 跟随Sensor(镜头)最大能力 跟随Sensor(镜头)最大能力 跟随Sensor(镜头)最大能力
    PRECONFIG_HIGH_QUALITY 跟随Sensor(镜头)最大能力 跟随Sensor(镜头)最大能力 跟随Sensor(镜头)最大能力

3.18.2 开发步骤

  1. 导入相关接口。

    import { camera } from '@kit.CameraKit';
    import { BusinessError } from '@kit.BasicServicesKit';

  2. 创建输出流Output。

此处以创建预览流和拍照流为例。

创建预览输出流时,涉及参数surfaceId。XComponent组件为预览流提供Surface

复制代码
// 创建预览输出流
let previewOutput: camera.PreviewOutput | undefined = undefined;
try {
  previewOutput = cameraManager.createPreviewOutput(surfaceId);
} catch (error) {
  let err = error as BusinessError;
  console.error(`Failed to create the PreviewOutput instance. error code: ${err.code}`);
}
if (previewOutput === undefined) {
  return;
}

// 创建拍照输出流
let photoOutput: camera.PhotoOutput | undefined = undefined;
try {
  photoOutput = cameraManager.createPhotoOutput();
} catch (error) {
  let err = error as BusinessError;
  console.error('Failed to createPhotoOutput errorCode = ' + err.code);
}
if (photoOutput === undefined) {
  return;
}
  1. 调用CameraManager类中的createCameraInput方法,创建输入流Input。

    let cameraInput: camera.CameraInput | undefined = undefined;
    try {
    cameraInput = cameraManager.createCameraInput(cameraArray[0]);
    } catch (error) {
    let err = error as BusinessError;
    console.error('Failed to createCameraInput errorCode = ' + err.code);
    }
    if (cameraInput === undefined) {
    return;
    }
    // 打开相机
    await cameraInput.open();

  2. 调用createSession创建会话(Session)。

说明

SceneMode需要指定为NORMAL_PHOTO或NORMAL_VIDEO,对应拍照场景PhotoSession和录像场景VideoSession

复制代码
//创建会话
let photoSession: camera.PhotoSession | undefined = undefined;
try {
  photoSession = cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession;
} catch (error) {
  let err = error as BusinessError;
  console.error('Failed to create the session instance. errorCode = ' + err.code);
}
if (photoSession === undefined) {
  return;
}
  1. 调用canPreconfig检查对应的PreconfigType和PreconfigRatio的组合在当前设备上是否支持。确认支持后,调用preconfig启用Preconfig配置。

    // 查询Preconfig能力
    try {
    let isPreconfigSupport = photoSession.canPreconfig(camera.PreconfigType.PRECONFIG_1080P);
    if (!isPreconfigSupport) {
    console.error('PhotoSession canPreconfig check fail.');
    return;
    }
    } catch (error) {
    let err = error as BusinessError;
    console.error('Failed to call canPreconfig. errorCode = ' + err.code);
    return;
    }

    // 配置Preconfig
    try {
    photoSession.preconfig(camera.PreconfigType.PRECONFIG_1080P);
    } catch (error) {
    let err = error as BusinessError;
    console.error('Failed to call preconfig. errorCode = ' + err.code);
    return;
    }

  2. Session添加Input和Output。

说明

Session调用preconfig接口成功之后,Session内部会将预置数据准备好,如果向Session中进行添加未配置Profile的Output,Session则会对相应的Output进行配置对应Profile。如果向Session中添加已配置Profile的Output,则Session的预配置数据不生效。

复制代码
// 开始配置会话
try {
  photoSession.beginConfig();
} catch (error) {
  let err = error as BusinessError;
  console.error('Failed to beginConfig. errorCode = ' + err.code);
}

// 向会话中添加相机输入流
try {
  photoSession.addInput(cameraInput);
} catch (error) {
  let err = error as BusinessError;
  console.error('Failed to addInput. errorCode = ' + err.code);
}

// 向会话中添加预览输出流
try {
  photoSession.addOutput(previewOutput);
} catch (error) {
  let err = error as BusinessError;
  console.error('Failed to addOutput(previewOutput). errorCode = ' + err.code);
}

// 向会话中添加拍照输出流
try {
  photoSession.addOutput(photoOutput);
} catch (error) {
  let err = error as BusinessError;
  console.error('Failed to addOutput(photoOutput). errorCode = ' + err.code);
}

// 提交会话配置
await photoSession.commitConfig();
  1. 启动Session。

    // 启动会话
    await photoSession.start().then(() => {
    console.info('Promise returned to indicate the session start success.');
    });

相关推荐
zzialx4 小时前
HarmonyOS:基于axios实现文件的下载以及下载进度的监听
harmonyos·鸿蒙
IT乐手5 小时前
3.5、HarmonyOS Next 文本显示(Text/Span)
harmonyos
IT乐手5 小时前
3.6、HarmonyOS Next 文本输入(TextInput/TextArea)
harmonyos
Georgewu5 小时前
【HarmonyOS Next】鸿蒙应用弹框和提示气泡详解(二)之浮层(OverlayManager),半模态页面(bindSheet),全模态页面(bindC
前端·华为·harmonyos
BensionLZ7 小时前
HO与OH差异之Navigation
harmonyos
CV工程师丁Sir7 小时前
《HarmonyOS Next状态栏动画实现案例与代码解析》
华为·harmonyos·harmonyos next
ChinaDragonDreamer8 小时前
HarmonyOS:通过键值型数据库实现数据持久化
harmonyos·鸿蒙
二川bro8 小时前
HarmonyOS NEXT(十) :系统集成与调试
华为·wpf·harmonyos
李游Leo11 小时前
HarmonyOS 之 @Require 装饰器自学指南
harmonyos