鸿蒙-自定义相机拍照

devtools/2025/2/26 2:40:39/

文章目录

    • 前言
    • 大致流程
    • 开发
      • 权限处理
      • 获取可用相机列表
      • 创建相机输入流并打开相机
      • 创建并配置会话
      • 拍照回调和启动会话
      • 其他配置
        • 闪光灯
        • 连续自动对焦
        • 缩放
      • 释放资源
      • 优化
        • 设备旋转
        • 在 Worker 线程中使用相机

前言

这个就没啥好说的,有需求就要搞定需求,搞不定需求就搞定提出需求的人嘛

大致流程

相机开发需要使用真机,模拟器目前还是不支持的。这就劝退了一部分开发者。
所需要的调用的接口大部分集中在@kit.CameraKit@kit.AbilityKit中。保存图片时需要用到@kit.ImageKit@kit.CoreFileKit@kit.MediaLibraryKit
接下来看下需要做哪些工作:

  1. 获取相机权限
  2. 获取可用相机列表
    1. 可以在这里监听相机状态(USB相机连接、断开连接、关闭、被占用等)
    2. 选择当前使用的相机
  3. 创建相机输入流并打开相机
    1. 可以创建相机输入流
    2. 可以监听预览输出流状态,包括预览流启动、预览流结束、预览流输出错误
    3. 可以获取相机支持的模式列表(NORMAL_PHOTO,NORMAL_VIDEO,SECURE_PHOTO)
    4. 可以获取当前相机设备支持的所有输出流,如预览流、拍照流、录像流等
  4. 会话(Session)管理
    1. 配置相机的输入流和输出流(分辨路等配置)
    2. 添加闪光灯、调整焦距等配置
    3. 会话切换控制:切换拍照或者录像
    4. 交和开启会话,可以开始调用相机相关功能
  5. 预览
    1. 创建Surface用于预览
    2. 将预览输出流通过SurfaceID与Surface关联
    3. 调用Session.start方法开始预览
  6. 拍照
    1. 创建拍照输出流
    2. 设置拍照photoAvailable的回调,并将拍照的buffer保存为图片。
    3. 参数配置(闪光灯、变焦、焦距等)
    4. 触发拍照

开发

权限处理

在进入拍照页面之前先申请权限,具体的流程看这里申请应用权限,本文不再赘述。

获取可用相机列表

首先要获取相机管理实例,这里为了代码看起来清晰,将各个步骤写到了单独的方法中。
另外多出使用camera.CameraManager实例,因此定义为了全局变量


@Entry
@Component
struct TakePhotoPage {//相机管理getCameraManager(context: common.BaseContext): camera.CameraManager {let cameraManager: camera.CameraManager = camera.getCameraManager(context);return cameraManager;}//获取可用相机列表getCameraDevices(cameraManager: camera.CameraManager): Array<camera.CameraDevice> {let cameraArray: Array<camera.CameraDevice> = cameraManager.getSupportedCameras();if (cameraArray != undefined && cameraArray.length > 0) {//在这里可以输出一些相机参数,比如相机位置(前置、后置)、相机类型(广角相机、长焦相机)等信息return cameraArray;} else {hilog.error(0x01,TAG,"cameraManager.getSupportedCameras error");return [];}}// 监听相机状态onCameraStatusChange(cameraManager: camera.CameraManager): void {cameraManager.on('cameraStatus', (err: BusinessError, cameraStatusInfo: camera.CameraStatusInfo) => {if (err !== undefined && err.code !== 0) {hilog.error(0x01,TAG,`Callback Error, errorCode: ${err.code}`);return;}// 如果当通过USB连接相机设备时,回调函数会返回新的相机出现状态if (cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_APPEAR) {hilog.info(0x01,TAG,`New Camera device appear.`);}// 如果当断开相机设备USB连接时,回调函数会返回相机被移除状态if (cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_DISAPPEAR) {hilog.info(0x01,TAG,`Camera device has been removed.`);}// 相机被关闭时,回调函数会返回相机可用状态if (cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_AVAILABLE) {hilog.info(0x01,TAG,`Current Camera is available.`);}// 相机被打开/占用时,回调函数会返回相机不可用状态if (cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_UNAVAILABLE) {hilog.info(0x01,TAG,`Current Camera has been occupied.`);}hilog.info(0x01,TAG,`camera: ${cameraStatusInfo.camera.cameraId}`);hilog.info(0x01,TAG,`status: ${cameraStatusInfo.status}`);});}
}

这样我们可以获取到所有可用的相机列表,并且可以根据相机类型、连接类型等过滤掉不适用的相机。
在获取到相机列表后,我们默认使用返回列表的第一个相机。

创建相机输入流并打开相机

在这一步我们主要是创建相机的输入流,为后面在XComponent中预览做准备。

createInput(): camera.CameraInput | undefined {
// 创建相机输入流
let cameraInput: camera.CameraInput | undefined = undefined;
try {cameraInput = this.cameraManager.createCameraInput(this.currentCamera);
} catch (error) {let err = error as BusinessError;console.error('Failed to createCameraInput errorCode = ' + err.code);
}
if (cameraInput === undefined) {return undefined;
}
// 监听cameraInput错误信息
cameraInput.on('error', this.currentCamera, (error: BusinessError) => {console.error(`Camera input error code: ${error.code}`);
});return cameraInput;
}

最重要的就一行代码:调用cameraManager.createCameraInput(camera: CameraDevice)创建一个输入流并返回,之后调用返回的输入流的open()方法打开相机,注意该方法是异步的。
同样的,我们可以调用cameraManager.getSupportedSceneModes(camera: CameraDevice)来获取相机支持的模式,一般情况下都会支持拍照和录像。
之后我们获取设备支持的输出流能力

getSupportedOutputCapability(): camera.CameraOutputCapability | undefined {
// 获取相机设备支持的输出流能力
let cameraOutputCapability: camera.CameraOutputCapability =this.cameraManager.getSupportedOutputCapability(this.currentCamera, camera.SceneMode.NORMAL_PHOTO)if (!cameraOutputCapability) {console.error("cameraManager.getSupportedOutputCapability error");return undefined;
}
return cameraOutputCapability;
}

我们拿到cameraOutputCapability之后,可以从该对象的previewProfilesphotoProfiles属性中获取到设备支持的分辨率大小。这里我们直接使用1920*1080的分辨率。
需要注意的是 previewProfilesphotoProfiles所支持的分辨率不一定是一致的。预览的话只要宽高比一致,分辨率别差的太离谱就可以。

之后我们使用选择好的Profile对象来创建拍照输出流和预览输出流

    try {this.photoOutput = this.cameraManager.createPhotoOutput(this.currentPhotoProfile);this.previewOutput = this.cameraManager.createPreviewOutput(previewProfile, this.surfaceId);} catch (error) {let err = error as BusinessError;console.error('Failed to createPhotoOutput errorCode = ' + err.code);}if (this.photoOutput === undefined) {return;}

这里需要注意的是,创建预览输出流的时候需要传入 surfaceID,该值来源于组件XComponent

private mXComponentController: XComponentController = new XComponentController;
XComponent({
id: 'componentId',
type: XComponentType.SURFACE,
controller: this.mXComponentController,
})
.onLoad(async () => {this.surfaceId = this.mXComponentController.getXComponentSurfaceId();
})

所以这里我们需要注意一下创建预览输出流的时机

创建并配置会话

创建会话也只是一行的就可以搞定,但可能会有异常出现

  createSession() {//创建会话try {this.photoSession = this.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 (this.photoSession === undefined) {return;}// 监听session错误信息this.photoSession.on('error', (error: BusinessError) => {console.error(`Capture session error code: ${error.code}`);});}

配置会话主要是添加相机输入流、预览输出流和拍照输出流,最后提交配置

async configSession(cameraInput: camera.CameraInput, previewOutput: camera.PreviewOutput) {if (!this.photoSession) {return}// 开始配置会话try {this.photoSession.beginConfig();} catch (error) {let err = error as BusinessError;console.error('Failed to beginConfig. errorCode = ' + err.code);}// 向会话中添加相机输入流try {this.photoSession.addInput(cameraInput);} catch (error) {let err = error as BusinessError;console.error('Failed to addInput. errorCode = ' + err.code);}// 向会话中添加预览输出流try {this.photoSession.addOutput(previewOutput);} catch (error) {let err = error as BusinessError;console.error('Failed to addOutput(previewOutput). errorCode = ' + err.code);}// 向会话中添加拍照输出流try {this.photoSession.addOutput(this.photoOutput);} catch (error) {let err = error as BusinessError;console.error('Failed to addOutput(photoOutput). errorCode = ' + err.code);}// 提交会话配置await this.photoSession.commitConfig();}

拍照回调和启动会话

我们先启动会话

await this.photoSession.start().then(() => {console.info('Promise returned to indicate the session start success.');});

会话启动之后我们就可以进行拍照了。拍照的话需要调用拍照输出流的capture方法

takePhoto() {
if (!this.photoOutput) {return
}
let photoCaptureSetting: camera.PhotoCaptureSetting = {quality: camera.QualityLevel.QUALITY_LEVEL_HIGH, // 设置图片质量高rotation: camera.ImageRotation.ROTATION_0 // 设置图片旋转角度0
}
// 使用当前拍照设置进行拍照
this.photoOutput.capture(photoCaptureSetting, (err: BusinessError) => {if (err) {console.error(`Failed to capture the photo ${err.message}`);return;}console.info('Callback invoked to indicate the photo capture request success.');
});
}

但照片内容确不是在该方法中返回,而是需要我们在拍照输出流中添加photoAvailable事件监听,该监听可以在创建拍照输出流之后就添加

setPhotoOutputCb(): void {if (!this.photoOutput) {return}//设置回调之后,调用photoOutput的capture方法,就会将拍照的buffer回传到回调中this.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 = 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;let filePath = getContext().cacheDir + '/' + systemDateTime.getTime() + '.jpg'let file = fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE)fileIo.writeSync(file.fd, buffer)fileIo.closeSync(file)let fileUrl = fileUri.getUriFromPath(filePath)promptAction.showToast({message:fileUrl})let id: number = 0promptAction.openCustomDialog({builder: () => {this.saveImageToAlbumDialog(fileUrl, () => {promptAction.closeCustomDialog(id)})}}).then((dialogID) => {id = dialogID})} else {console.error('byteBuffer is null');return;}imageObj.release();});});}

这里就简单写了一下处理:拿到 ArrayBuffer 之后写入沙箱文件,然后在弹窗中展示

其他配置

我们创建会话(camera.PhotoSession)之后,可以通过该对象配置闪光灯模式、对焦模式、缩放等

闪光灯

首先判断设备是否支持闪光灯,然后再判断支持的闪光灯模式。

 getSupportFlashMode() {this.supportFlashMode = []if (!this.photoSession) {return}// 判断设备是否支持闪光灯let flashStatus: boolean = false;try {flashStatus = this.photoSession.hasFlash();} catch (error) {let err = error as BusinessError;console.error('Failed to hasFlash. errorCode = ' + err.code);}console.info('Returned with the flash light support status:' + flashStatus);if (flashStatus) {// 判断支持的闪光灯模式try {let status: boolean = this.photoSession.isFlashModeSupported(camera.FlashMode.FLASH_MODE_CLOSE);if (status) {this.supportFlashMode.push(camera.FlashMode.FLASH_MODE_CLOSE)}} catch (error) {let err = error as BusinessError;console.error('Failed to check whether the flash mode is supported. errorCode = ' + err.code);}try {let status: boolean = this.photoSession.isFlashModeSupported(camera.FlashMode.FLASH_MODE_OPEN);if (status) {this.supportFlashMode.push(camera.FlashMode.FLASH_MODE_OPEN)}} catch (error) {let err = error as BusinessError;console.error('Failed to check whether the flash mode is supported. errorCode = ' + err.code);}try {let status: boolean = this.photoSession.isFlashModeSupported(camera.FlashMode.FLASH_MODE_AUTO);if (status) {this.supportFlashMode.push(camera.FlashMode.FLASH_MODE_AUTO)}} catch (error) {let err = error as BusinessError;console.error('Failed to check whether the flash mode is supported. errorCode = ' + err.code);}try {let status: boolean = this.photoSession.isFlashModeSupported(camera.FlashMode.FLASH_MODE_ALWAYS_OPEN);if (status) {this.supportFlashMode.push(camera.FlashMode.FLASH_MODE_ALWAYS_OPEN)}} catch (error) {let err = error as BusinessError;console.error('Failed to check whether the flash mode is supported. errorCode = ' + err.code);}}}
连续自动对焦

也是需要先判断是否支持自动连续对焦,不支持的话只能手动对焦

  setAutoContinuousFocus() {if (!this.photoSession) {return}// 判断是否支持连续自动变焦模式let focusModeStatus: boolean = false;try {let status: boolean = this.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. errorCode = ' + err.code);}if (focusModeStatus) {// 设置连续自动变焦模式try {this.photoSession.setFocusMode(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO);} catch (error) {let err = error as BusinessError;console.error('Failed to set the focus mode. errorCode = ' + err.code);}}}

手动对焦则是获取到用户点击的位置,然后调用this.photoSession.setFocusPoint(point: camera.Point)方法进行对焦

缩放

同样的,需要先获取到支持的缩放范围

  getZoomRatioRange() {if (!this.photoSession) {return}// 获取相机支持的可变焦距比范围let zoomRatioRange: Array<number> = [];try {zoomRatioRange = this.photoSession.getZoomRatioRange();} catch (error) {let err = error as BusinessError;console.error('Failed to get the zoom ratio range. errorCode = ' + err.code);}if (zoomRatioRange.length <= 0) {return;}this.zoomRatioRangeStart = zoomRatioRange[0]this.zoomRatioRangeEnd = zoomRatioRange[1]}

然后调用this.photoSession.setZoomRatio(zoom);设置缩放比

释放资源

在拍照结束后需要释放相应的资源

await photoSession.stop();// 释放相机输入流
await cameraInput.close();// 释放预览输出流
await previewOutput.release();// 释放拍照输出流
await photoOutput.release();// 释放会话
await photoSession.release();// 会话置空
photoSession = undefined;

优化

设备旋转

上面的代码中我们并没有考虑设备旋转问题

import { display } from '@kit.ArkUI';   let initDisplayRotation = display.getDefaultDisplaySync().rotation;
let initPreviewRotation = previewOutput.getPreviewRotation(initDisplayRotation * camera.ImageRotation.ROTATION_90);
previewOutput.setPreviewRotation(initPreviewRotation, false);
display.off('change');
display.on('change', () => {initDisplayRotation = display.getDefaultDisplaySync().rotation;let imageRotation = initDisplayRotation * camera.ImageRotation.ROTATION_90;let previewRotation = previewOutput.getPreviewRotation(imageRotation);previewOutput.setPreviewRotation(previewRotation, false);
});
在 Worker 线程中使用相机

一般情况下,设备的性能足以支持我们直接使用相机,但如果要追求极致性能,可以将拍照的一系列流程都放在 Worker 线程中完成,通过宿主线程的即时消息通信完成线程间交互


具体的代码在https://github.com/huangyuanlove/HelloArkUI/blob/main/entry/src/main/ets/pages/playground/take_photo/TakePhotoPage.ets
就不再贴一遍了。
上面代码中并没有实现切换摄像头、切换闪光灯、切换分辨率功能,只是做了展示。


http://www.ppmy.cn/devtools/162705.html

相关文章

解决idea一个非常坑的问题

dea中经常会遇到这样问题&#xff0c;明明maven的pom中已经添加了依赖&#xff0c;总是提示jar包找不到&#xff0c; 于是双击clean &#xff0c;或者 点击 reload maven &#xff0c;都是不好使。如 javax.crypto.spec.IvParameterSpec;这个包&#xff0c;竟然飘红了。我clean…

ArcGIS Pro制作人口三维地图教程

数据可视化是现代数据分析领域的一项重要技术&#xff0c;它通过图表、地图等视觉形式将数据转化为易于理解的信息&#xff0c;使得人们能够更直观地把握数据的内涵与趋势。 在众多可视化手段中&#xff0c;三维地图因其能够展现数据在空间中的立体分布而备受青睐。 本文将详…

git中的merge和rebase的区别

在 Git 中&#xff0c;git merge 和 git rebase 都是用于整合分支变更的核心命令&#xff0c;但它们的实现方式和结果有本质区别。以下是两者的详细对比&#xff1a; 一、核心区别 特性git mergegit rebase历史记录保留分支拓扑&#xff0c;生成新的合并提交线性化历史&#x…

SmartMediakit之音视频直播技术的极致体验与广泛应用

引言 在数字化时代&#xff0c;音视频直播技术已经深入到各个行业和领域&#xff0c;成为信息传递和交流的重要手段。视沃科技自2015年成立以来&#xff0c;一直致力于为传统行业提供极致体验的音视频直播技术解决方案&#xff0c;其旗下的大牛直播SDK凭借强大的功能和卓越的性…

智慧后勤的消防管理:豪越科技为安全护航

智慧后勤消防管理难题大揭秘&#xff01; 在智慧后勤发展得如火如荼的当下&#xff0c;消防管理却暗藏诸多难题。传统模式下&#xff0c;消防设施分布得那叫一个散&#xff0c;就像一盘散沙&#xff0c;管理起来超费劲。人工巡检不仅效率低&#xff0c;还容易遗漏&#xff0c;不…

网络运维学习笔记 017 HCIA-Datacom综合实验01

文章目录 综合实验1实验需求总部特性 分支8分支9 配置一、 基本配置&#xff08;IP二层VLAN链路聚合&#xff09;ACC_SWSW-S1SW-S2SW-Ser1SW-CoreSW8SW9DHCPISPGW 二、 单臂路由GW 三、 vlanifSW8SW9 四、 OSPFSW8SW9GW 五、 DHCPDHCPGW 六、 NAT缺省路由GW 七、 HTTPGW 综合实…

支持向量机 (Support Vector Machine, SVM)

支持向量机 (Support Vector Machine, SVM) 支持向量机&#xff08;SVM&#xff09;是一种广泛应用于分类、回归分析以及异常检测的监督学习算法。它基于结构风险最小化&#xff08;Structural Risk Minimization&#xff0c;SRM&#xff09;原则&#xff0c;通过寻找一个最优…

DeepSeek 与其他大语言模型相比,优势和劣势

DeepSeek 与其他大语言模型相比,优势和劣势主要体现在以下方面: 优势 性能卓越:在多项权威测试中展现出强大的语言理解能力,能准确理解复杂语句含义。语言生成方面,文本自然流畅、逻辑连贯,生成速度可达每秒 60 个 tokens。成本优势:训练成本仅为同级别模型的几分之一,…