当前位置: 首页 > article >正文

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

目录

1 Camera Kit简介

1.1 开发模型

2 开发准备

2.1 申请权限

2.2 开发指导

3. 相机开发

3.1 相机管理(ArkTS)

3.1.1 开发步骤

3.1.2 状态监听

3.2 设备输入(ArkTS)

3.2.1 开发步骤

3.3 会话管理(ArkTS)

3.3.1 开发步骤

3.4 预览(ArkTS)

3.4.1 开发步骤

3.4.2 状态监听

3.5 拍照(ArkTS)

3.5.1 开发步骤

3.5.2 状态监听

3.6 录像(ArkTS)

3.6.1 开发步骤

3.6.2 状态监听

3.7 元数据(ArkTS)

3.7.1 开发步骤

3.7.2 状态监听

3.8 对焦(ArkTS)

3.8.1 开发步骤

3.8.2 状态监听

3.9 手电筒使用(ArkTS)

3.9.1 开发步骤

3.9.2 状态监听

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

3.10.1 创建XComponent

3.10.2 获取设备折叠状态

3.11 分段式拍照(ArkTS)

3.11.1 开发步骤

3.11.2 状态监听

3.12 动态照片(ArkTS)

3.12.1 开发步骤

3.12.2 状态监听

3.13 相机基础动效(ArkTS)

3.13.1 闪黑动效

3.13.2 模糊动效

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

3.14.1 开发步骤

3.14.2 trace对比

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

3.15.1 创建会话

3.15.2 预览

3.15.3 录像

3.15.4 计算设备旋转角度

3.16 安全相机(ArkTS)

3.16.1 开发步骤

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

3.17.1 约束与限制

3.17.2 开发流程

3.17.3 创建Session会话并指定模式

3.17.4 调整帧率

3.18 使用相机预配置(ArkTS)

3.18.1 规格说明

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,来访问用户媒体文件中的地理位置信息。

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

  • 当需要读取图片或视频文件时,请优先使用媒体库Picker选择媒体资源。
  • 当需要保存图片或视频文件时,请优先使用安全控件保存媒体资源。

说明

仅应用需要克隆、备份或同步用户公共目录的图片、视频类文件时,可申请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;
}

说明

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

 3. 通过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类中的commitConfig和start方法提交相关配置,并启动会话。

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)}`);
  }
}

5. 会话控制。调用PhotoSession类中的stop方法可以停止当前会话。调用removeOutput和addOutput方法可以完成会话切换控制。以下示例代码以移除拍照流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,以此类推。

3. 通过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;
}

 4. 使能。通过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;
}

3. 设置拍照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(); 
     });
   });
}

4. 参数配置。

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

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)}`);
  }
}

5. 触发拍照。

通过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';

2. 创建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;
}

3. 创建录像输出流。

通过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;
}

 4. 开始录像。

先通过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)}`);
  }
}

5. 停止录像。

先通过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();
}

4. 调用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;
}

3. 调用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;
}

4. 通过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;
}

3. 设置拍照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

请求图片参考媒体库接口:requestImageData 和 onDataPrepared

4. 拍照时的会话配置及触发拍照的方式,与普通拍照相同,请参考拍照的步骤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;
}

3. 查询当前设备当前模式是否支持动态照片能力。

说明

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

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;
}

 4. 使能动态照片拍照能力。

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}`);
  }
}

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

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)
}

3. 实现闪黑动效。

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; // 闪黑组件从不透明到透明。
  })
}

4. 触发闪黑动效。

点击或触控拍照按钮,更新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;
  }
}

3. 构建截图组件。

此处定义一个截图组件,置于预览流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))
}

4. (按实际情况选择)实现模糊出现动效。

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

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

注意:由于图形提供的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; // 截图组件模糊度变化动效。
    }
  );
}

5. 实现模糊消失动效。

模糊消失动效:由新模式预览流首帧回调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; // 截图组件透明度变化动效。
  });
}

6. (按实际情况选择)实现模糊翻转动效。

模糊翻转动效分两段实现,模糊翻转动效和模糊消失动效,其中模糊消失动效同第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 };
    }
  )
}

7. 按需触发动效。

模式切换动效触发:点击或触控模式按钮立即执行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) => {
}

2. 创建相机服务代理类,调用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();

3. 创建组件,用于显示预览流,在页面相关生命周期中构造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)

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

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

  • 在预览时,图像旋转角度与屏幕显示旋转角度(Display.rotation)相关。具体开发指导:创建会话 > 预览
  • 在拍照、录像时,图像旋转角度与设备重力方向(即设备旋转角度)相关。

    拍照开发指导:创建会话 > 计算设备旋转角度 > 拍照

    录像开发指导:创建会话 > 计算设备旋转角度 > 录像

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;
}

2. 调用PreviewOutput类中的setPreviewRotation,设置图像的预览旋转角度。

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

  • previewRotation:预览旋转角度,取上一步getPreviewRotation的返回值。
  • isDisplayLocked:可选入参,默认为false。当设置为false,即屏幕方向未锁定,预览旋转角度将根据相机镜头角度+屏幕显示旋转角度的值计算;当设置为true,Surface旋转锁定,不跟随窗口变化,旋转角度仅取相机镜头角度计算。
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;
}

2. 将录像的旋转角度写入AVMetadata.videoOrientation。

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

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];
    }
  }
}

3. 查询相机设备在安全模式下支持的输出能力。

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

当前安全相机仅支持输出预览流,推荐预览流使用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;
}

4. 创建设备输入输出。

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

  • 一路是普通的预览流,用于界面显示,普通预览流的创建流程请参考预览开发指导。
  • 一路是安全数据流,用于安全服务校验,安全数据流需要通过image.createImageReceiver创建图像接收类ImageReceiver,再通过其getReceivingSurfaceId方法获取surfaceId。

说明

安全数据流没有单独的数据类型,同属于预览流,输出能力与预览流保持一致,创建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);
}

 5. 打开安全设备。

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

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

async function openCamera(cameraInput: camera.CameraInput) {
  const seqId: bigint = await cameraInput.open(true);
}

6. 使用Device Security Kit的能力,创建证明密钥、打开证明会话。请参考Device Security Kit(设备安全服务)的开发指导:可信应用服务-安全摄像头。

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

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

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!');
  }
}

8. 安全模式会话正常启动后,预览流和安全数据流数据逐帧上报,安全数据流每帧数据可以通过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.');
      })
    })
  })
}

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

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

9. 释放安全相机,使用Session的release方法。

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会话并指定模式

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

创建会话时需指定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. 调用PreviewOutput的getSupportedFrameRates,查询当前支持的帧率范围。

说明

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

function getSupportedFrameRange(previewOutput: camera.PreviewOutput): Array<camera.FrameRateRange> {
// 获取支持的帧率范围,不同的硬件平台可能提供不同的帧率范围
  return previewOutput.getSupportedFrameRates();
}

 2. 根据实际开发需求,调用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)}`);
  }
}

 3. (可选)通过PreviewOutput类提供的getActiveFrameRate接口查询已设置过并生效的帧率。

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

function getActiveFrameRange(previewOutput: camera.PreviewOutput): camera.FrameRateRange {
  return previewOutput.getActiveFrameRate();
}

3.18 使用相机预配置(ArkTS)

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

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

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

其他相关能力:

  • CameraPicker:无需开发相机功能,拉起系统相机获取照片或视频。
  • 调用全量相机接口开发:可开发自定义界面、分辨率、图像效果的专业相机应用。

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;
}

3. 调用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();

4. 调用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;
}

5. 调用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;
}

6. 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();

 7. 启动Session。

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


http://www.kler.cn/a/601183.html

相关文章:

  • ragflow安装es报错怎么办
  • 云原生进化:架构现代化的核心引擎
  • 优先级与环境变量的艺术:驾驭 Linux 系统的核心
  • 鸿蒙入门——ArkUI 跨页面数据同步和应用全局单例的UI状态存储AppStorage 小结(三)
  • 贪心算法——思路与例题
  • 华为云助力数字化转型
  • 第一天学爬虫
  • 【干货,实战经验】nginx缓存问题
  • [GHCTF 2025]ez_readfile
  • LabVIEW 与 PLC 通讯的常见方式
  • 分级反爬虫是什么?JAVA实现反爬虫策略
  • K8S学习之基础五十:k8s中pod时区问题并通过kibana查看日志
  • uniapp中$emit的使用方法
  • RWEQ 模型深度讲解:结合 Python、ArcGIS 等实现土壤风蚀归因分析
  • GitHub和Gitee上的一些AI项目
  • Zbrush插件安装
  • HarmonyOS NEXT 实现拖动卡片背景模糊效果
  • 独立组网和非独立组网
  • Linux 多线程-生产消费者模型线程池线程单例模式其他
  • 力扣刷题39. 组合总和