HarmonyOS 相机开发从入门到放弃

一、背景引入:这玩意儿是干啥的?

今儿个咱聊聊 Camera Kit,中文名儿叫"相机服务"。听名字就知道,这玩意儿就是让你调用相机的。

你可能会问:“调用相机?我自己写个相机应用不就完了吗?”

嘿,您要真这么想,那我得给您点个赞——有这股劲儿,当年我写相机也是这么想的。但踩了几个坑之后,我就服了。

为啥要用 Camera Kit?

咱说个实际场景:

你在应用里想做个拍照功能,用户点了个"拍照"按钮,你得让人家能预览、能拍照、能录像吧?这时候你有几个选择:

  1. 自己写底层驱动:跟硬件打交道,ISP、HDI、缓存队列…您慢慢写,写完了叫我一声
  2. 用系统相机:拉起系统相机拍一张,简单,但定制性差
  3. 用 Camera Kit:系统提供的相机开发套件,能精确控制硬件

我选 3,为啥?

  • 不用自己写驱动:预览、拍照、录像,系统都给你整明白了
  • 能定制:闪光灯、曝光时间、对焦调焦,都能控制
  • 多镜头适配:广角、长焦、TOF,多摄同开,香

说白了,Camera Kit 就是个"相机管家",你告诉它要干啥,它帮你调度硬件,完事儿。

这玩意儿能干啥?

咱列个表,您心里有数:

功能说明难度
预览实时显示相机画面⭐ 简单
拍照拍摄并保存照片⭐⭐ 中等
录像录制视频(含音频)⭐⭐⭐ 有点麻烦
闪光灯控制开关、自动、常亮⭐ 简单
对焦调焦自动对焦、手动对焦⭐⭐ 中等
曝光控制调整曝光时间、ISO⭐⭐⭐ 有点麻烦
多摄同开同时开多个摄像头⭐⭐⭐⭐ 难

注意啊:多摄同开这玩意儿,不是所有设备都支持,你得先检查。


二、整体架构:大概长啥样?

Camera Kit 的架构,说白了就三块:

┌─────────────────────────────────────────┐ │ 你的应用 │ │ (创建会话、配置输入输出) │ └─────────────────┬───────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ Camera Kit 服务 │ │ (会话管理、设备管理、输出管理) │ └─────────────────┬───────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ 相机硬件 + ISP │ │ (真正的采集和处理) │ └─────────────────────────────────────────┘ 

工作流程

  1. 你获取 CameraManager(相机管家)
  2. 创建 CameraInput(相机输入)—— 选哪个摄像头
  3. 创建 CameraSession(相机会话)—— 配置拍摄模式
  4. 添加 Output(输出流)—— 预览流、拍照流、录像流
  5. 提交配置,启动会话
  6. 开始拍照/录像
  7. 完事儿释放资源

几个概念先整明白

  • CameraManager:相机管家,管所有相机设备的
  • CameraDevice:单个相机设备,比如前置、后置
  • CameraInput:相机输入流,就是选哪个摄像头拍摄
  • CameraSession:相机会话,配置拍摄模式和输出
  • Output:输出流,预览、拍照、录像都靠它

三、开发准备:先整权限

来,上代码之前,先申请权限。这玩意儿没权限,后面都白搭。

3.1 需要啥权限?

module.json5 里加:

"requestPermissions": [ { "name": "ohos.permission.CAMERA", "reason": "$string:camera_reason", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }, { "name": "ohos.permission.MICROPHONE", "reason": "$string:mic_reason", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } } ] 

说明

  • CAMERA:相机权限,必须的
  • MICROPHONE:麦克风权限,录像的时候需要
  • MEDIA_LOCATION:如果你要在照片里记录地理位置,加这个

3.2 向用户申请授权

光在配置文件里声明还不行,你得弹窗问用户:

import{ abilityAccessCtrl, bundleManager }from'@kit.AbilityKit';asyncfunctionrequestCameraPermission():Promise<boolean>{let atManager = abilityAccessCtrl.createAtManager();let token =await bundleManager.getAccessToken();// 先检查有没有权限let grantStatus =await atManager.checkAccessToken( token,'ohos.permission.CAMERA');if(grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED){console.info("已经有相机权限了");returntrue;}// 没有就申请let result =await atManager.requestPermissionsFromUser(['ohos.permission.CAMERA','ohos.permission.MICROPHONE']);// 检查结果for(let permResult of result.authResults){if(permResult !==0){console.error("用户拒绝了权限");returnfalse;}}console.info("权限申请成功");returntrue;}

建议:在应用启动时就申请,别等用户点拍照了才问,体验不好。


四、核心功能:咋整?

4.1 获取相机管家

来,第一步,获取 CameraManager:

import{ camera }from'@kit.CameraKit';import{ common }from'@kit.AbilityKit';functiongetCameraManager(context: common.BaseContext): camera.CameraManager |undefined{let cameraManager: camera.CameraManager;try{ cameraManager = camera.getCameraManager(context);}catch(error){console.error(`获取相机管家失败:${error}`);returnundefined;}return cameraManager;}

注意:如果获取失败,说明相机可能被占用或者设备没相机,别继续了。

4.2 获取支持的相机列表

拿到管家之后,得看看设备上有几个摄像头:

functiongetSupportedCameras(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(`相机 ID: ${cameraArray[index].cameraId}`);console.info(`相机位置:${cameraArray[index].cameraPosition}`);// 前置/后置console.info(`相机类型:${cameraArray[index].cameraType}`);// 广角/长焦等console.info(`连接类型:${cameraArray[index].connectionType}`);}return cameraArray;}else{console.error("设备没有可用相机");return[];}}

建议:把相机列表缓存起来,后面选摄像头的时候用。

4.3 监听相机状态

相机可能会被占用、被移除,你得监听状态:

functiononCameraStatusChange(cameraManager: camera.CameraManager):void{ cameraManager.on('cameraStatus',(err, cameraStatusInfo)=>{if(err !==undefined&& err.code !==0){console.error(`相机状态监听错误:${err.code}`);return;}// 新相机出现(比如 USB 外接摄像头)if(cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_APPEAR){console.info("新相机设备出现");}// 相机被移除if(cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_DISAPPEAR){console.info("相机设备被移除");}// 相机可用(之前被占用,现在释放了)if(cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_AVAILABLE){console.info("相机当前可用");}// 相机被占用if(cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_UNAVAILABLE){console.info("相机被占用");}console.info(`相机 ID: ${cameraStatusInfo.camera.cameraId}`);console.info(`状态:${cameraStatusInfo.status}`);});}

建议:在应用启动时就注册监听,相机状态变化时好处理。

4.4 创建相机输入流

选好了摄像头,得创建输入流:

asyncfunctioncreateInput( 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(`创建相机输入失败:${err.code}`);}if(cameraInput ===undefined){returnundefined;}// 监听输入流错误 cameraInput.on('error', cameraDevice,(error: BusinessError)=>{console.error(`相机输入错误:${error.code}`);});// 打开相机await cameraInput.open();return cameraInput;}

注意:创建输入流之前,确保相机没被占用。

4.5 获取支持的模式

不同的拍摄模式,支持的输出流不一样:

functiongetSupportedSceneModes( 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(`支持的模式:${sceneModeArray[index]}`);}return sceneModeArray;}else{console.error("获取支持的模式失败");return[];}}

常见模式

  • NORMAL_PHOTO:普通拍照
  • NORMAL_VIDEO:普通录像
  • HIGH_QUALITY_PHOTO:高质量拍照
  • 等等…

4.6 获取输出能力

不同的模式,支持的输出流不一样:

asyncfunctiongetSupportedOutputCapability( 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("获取输出能力失败");returnundefined;}console.info(`输出能力:${JSON.stringify(cameraOutputCapability)}`);// 以 NORMAL_PHOTO 模式为例,需要预览流和拍照流let previewProfilesArray:Array<camera.Profile>= cameraOutputCapability.previewProfiles;// 预览流let photoProfilesArray:Array<camera.Profile>= cameraOutputCapability.photoProfiles;// 拍照流if(!previewProfilesArray){console.error("不支持预览流");}if(!photoProfilesArray){console.error("不支持拍照流");}return cameraOutputCapability;}

注意:在创建输出流之前,先检查设备支不支持。


五、会话管理:重头戏来了

好了,前面都是准备工作,现在进入正题——会话管理。

5.1 创建会话

会话是相机开发的核心,所有配置都在会话里:

functiongetSession(cameraManager: camera.CameraManager): camera.VideoSession |undefined{let videoSession: camera.VideoSession |undefined=undefined;try{// 以录像会话为例 videoSession = cameraManager.createSession( camera.SceneMode.NORMAL_VIDEO)as camera.VideoSession;}catch(error){let err = error as BusinessError;console.error(`创建会话失败:${err.code}`);}return videoSession;}

注意:会话类型要跟场景模式匹配,录像模式创建 VideoSession。

5.2 配置会话

创建完会话,得配置:

functionbeginConfig(videoSession: camera.VideoSession):void{try{ videoSession.beginConfig();}catch(error){let err = error as BusinessError;console.error(`开始配置失败:${err.code}`);}}

这步就是告诉会话:“我要开始配置了啊”。

5.3 添加输入输出流

配置会话的核心就是添加输入输出流:

asyncfunctionstartSession( videoSession: camera.VideoSession, cameraInput: camera.CameraInput, previewOutput: camera.PreviewOutput, photoOutput: camera.PhotoOutput ):Promise<void>{// 1. 添加输入流try{ videoSession.addInput(cameraInput);}catch(error){let err = error as BusinessError;console.error(`添加输入流失败:${err.code}`);}// 2. 添加预览输出流(先检查能不能添加)let canAddPreviewOutput:boolean=false;try{ canAddPreviewOutput = videoSession.canAddOutput(previewOutput);}catch(error){let err = error as BusinessError;console.error(`检查预览流失败:${err.code}`);}if(!canAddPreviewOutput){console.error("无法添加预览输出流");return;}try{ videoSession.addOutput(previewOutput);}catch(error){let err = error as BusinessError;console.error(`添加预览流失败:${err.code}`);}// 3. 添加拍照输出流let canAddPhotoOutput:boolean=false;try{ canAddPhotoOutput = videoSession.canAddOutput(photoOutput);}catch(error){let err = error as BusinessError;console.error(`检查拍照流失败:${err.code}`);}if(!canAddPhotoOutput){console.error("无法添加拍照输出流");return;}try{ videoSession.addOutput(photoOutput);}catch(error){let err = error as BusinessError;console.error(`添加拍照流失败:${err.code}`);}// 4. 提交配置try{await videoSession.commitConfig();}catch(error){let err = error as BusinessError;console.error(`提交配置失败:${err.code}`);return;}// 5. 启动会话try{await videoSession.start();}catch(error){let err = error as BusinessError;console.error(`启动会话失败:${err.code}`);}}

注意

  • 添加输出流之前,先用 canAddOutput 检查一下
  • 提交配置之后才能启动会话
  • 顺序不能乱

5.4 会话切换

比如从拍照模式切换到录像模式:

asyncfunctionswitchOutput( videoSession: camera.VideoSession, videoOutput: camera.VideoOutput, photoOutput: camera.PhotoOutput ):Promise<void>{// 1. 停止当前会话try{await videoSession.stop();}catch(error){let err = error as BusinessError;console.error(`停止会话失败:${err.code}`);}// 2. 开始重新配置try{ videoSession.beginConfig();}catch(error){let err = error as BusinessError;console.error(`开始配置失败:${err.code}`);}// 3. 移除拍照输出流try{ videoSession.removeOutput(photoOutput);}catch(error){let err = error as BusinessError;console.error(`移除拍照流失败:${err.code}`);}// 4. 添加视频输出流try{ videoSession.canAddOutput(videoOutput);}catch(error){let err = error as BusinessError;console.error(`检查视频流失败:${err.code}`);}try{ videoSession.addOutput(videoOutput);}catch(error){let err = error as BusinessError;console.error(`添加视频流失败:${err.code}`);}// 5. 提交配置try{await videoSession.commitConfig();}catch(error){let err = error as BusinessError;console.error(`提交配置失败:${err.code}`);}// 6. 启动会话try{await videoSession.start();}catch(error){let err = error as BusinessError;console.error(`启动会话失败:${err.code}`);}}

建议:切换模式的时候,给用户一个 loading 提示,不然用户以为卡死了。


六、避坑指南 ⚠️

好了,重头戏来了。下面是我踩过的坑,你们别踩。

坑 1:权限没申请就调用相机

翻车现场

// 直接获取 CameraManagerlet cameraManager = camera.getCameraManager(context);// 结果:报错"权限不足"

正确姿势

// 先申请权限let hasPermission =awaitrequestCameraPermission();if(!hasPermission){ promptAction.showToast({ message:"需要相机权限"});return;}// 再获取 CameraManagerlet cameraManager = camera.getCameraManager(context);

坑 2:相机被占用不处理

翻车现场

// 直接创建输入流let cameraInput = cameraManager.createCameraInput(cameraDevice);// 结果:报错"相机被占用"

正确姿势

// 先监听相机状态 cameraManager.on('cameraStatus',(err, info)=>{if(info.status === camera.CameraStatus.CAMERA_STATUS_AVAILABLE){// 相机可用,再创建let cameraInput = cameraManager.createCameraInput(cameraDevice);}});

坑 3:输出流添加顺序错了

翻车现场

// 先添加输出流,再添加输入流 session.addOutput(previewOutput); session.addInput(cameraInput);// 结果:配置失败

正确姿势

// 先输入,后输出 session.addInput(cameraInput); session.addOutput(previewOutput); session.addOutput(photoOutput);

坑 4:不检查 canAddOutput

翻车现场

// 直接添加输出流 session.addOutput(previewOutput);// 结果:某些设备不支持,报错

正确姿势

// 先检查let canAdd = session.canAddOutput(previewOutput);if(!canAdd){console.error("设备不支持此输出流");return;}// 再添加 session.addOutput(previewOutput);

坑 5:会话配置完不提交

翻车现场

session.addInput(cameraInput); session.addOutput(previewOutput);// 忘了 commitConfig session.start();// 结果:启动失败

正确姿势

session.addInput(cameraInput); session.addOutput(previewOutput);await session.commitConfig();// 提交配置await session.start();// 启动会话

坑 6:释放资源不及时

翻车现场

// 用完相机直接返回functioncloseCamera(){// 忘了释放return;}

正确姿势

asyncfunctioncloseCamera( session: camera.Session, cameraInput: camera.CameraInput ){// 1. 停止会话await session.stop();// 2. 关闭输入流await cameraInput.close();// 3. 释放会话 session.release();}

坑 7:模拟器上测相机

翻车现场

// 在模拟器上测试let cameras = cameraManager.getSupportedCameras();// 结果:空列表,以为代码写错了

真相

模拟器没有相机硬件,获取不到相机列表。

正确姿势

真机测试,别在模拟器上浪费时间。


七、完整实战案例

来,整个完整的例子,你直接抄作业。

场景:简单的拍照应用

// CameraManager.tsimport{ camera }from'@kit.CameraKit';import{ common }from'@kit.AbilityKit';import{ BusinessError }from'@kit.BasicServicesKit';exportclassSimpleCameraManager{private cameraManager: camera.CameraManager |undefined;private cameraDevice: camera.CameraDevice |undefined;private cameraInput: camera.CameraInput |undefined;private session: camera.PhotoSession |undefined;private previewOutput: camera.PreviewOutput |undefined;private photoOutput: camera.PhotoOutput |undefined;// 初始化asyncinit(context: common.BaseContext):Promise<boolean>{// 1. 获取相机管家this.cameraManager = camera.getCameraManager(context);if(!this.cameraManager){console.error("获取相机管家失败");returnfalse;}// 2. 获取相机列表let cameras =this.cameraManager.getSupportedCameras();if(cameras.length ===0){console.error("没有可用相机");returnfalse;}// 3. 选后置相机this.cameraDevice = cameras.find( c => c.cameraPosition === camera.CameraPosition.BACK)|| cameras[0];// 4. 创建输入流this.cameraInput =this.cameraManager.createCameraInput(this.cameraDevice);awaitthis.cameraInput.open();returntrue;}// 创建会话asynccreateSession():Promise<boolean>{if(!this.cameraManager ||!this.cameraDevice){returnfalse;}// 1. 创建会话this.session =this.cameraManager.createSession( camera.SceneMode.NORMAL_PHOTO)as camera.PhotoSession;// 2. 开始配置this.session.beginConfig();// 3. 添加输入流this.session.addInput(this.cameraInput);// 4. 创建并添加输出流(这里省略创建过程)// this.previewOutput = await this.createPreviewOutput();// this.photoOutput = await this.createPhotoOutput();// this.session.addOutput(this.previewOutput);// this.session.addOutput(this.photoOutput);// 5. 提交配置awaitthis.session.commitConfig();// 6. 启动会话awaitthis.session.start();returntrue;}// 拍照asynctakePhoto():Promise<string|undefined>{if(!this.photoOutput){console.error("拍照输出流未创建");returnundefined;}// 触发拍照let photoPath =awaitthis.photoOutput.capture();console.info(`照片保存路径:${photoPath}`);return photoPath;}// 释放资源asyncrelease():Promise<void>{// 1. 停止会话if(this.session){awaitthis.session.stop();this.session.release();}// 2. 关闭输入流if(this.cameraInput){awaitthis.cameraInput.close();}}}// 使用let cameraManager =newSimpleCameraManager();// 初始化await cameraManager.init(context);// 创建会话await cameraManager.createSession();// 拍照let photoPath =await cameraManager.takePhoto();// 释放await cameraManager.release();

八、总结

好了,唠了这么多,总结一下:

Camera Kit 能干啥?

  • 预览、拍照、录像
  • 闪光灯、对焦、曝光控制
  • 多摄同开(部分设备支持)

开发流程?

  1. 申请权限(CAMERA、MICROPHONE)
  2. 获取 CameraManager
  3. 获取相机列表,选一个
  4. 创建 CameraInput
  5. 创建 CameraSession
  6. 添加输入输出流
  7. 提交配置,启动会话
  8. 拍照/录像
  9. 释放资源

有啥坑?

  1. 权限没申请就调用
  2. 相机被占用不处理
  3. 输出流添加顺序错了
  4. 不检查 canAddOutput
  5. 会话配置完不提交
  6. 释放资源不及时
  7. 在模拟器上测试

我的建议

如果你就是做个简单的拍照功能:

  • 用 CameraPicker 就够了,不用自己整 Camera Kit
  • CameraPicker 不用申请权限,直接拉起系统相机

如果你要做深度定制:

  • 相机状态监听一定要加
  • canAddOutput 一定要检查
  • 释放资源一定要及时
  • 真机测试,别用模拟器

Read more

《算法题讲解指南:优选算法-模拟》--38.替换所有问号,39.提莫攻击,40.Z 字形变换

《算法题讲解指南:优选算法-模拟》--38.替换所有问号,39.提莫攻击,40.Z 字形变换

🔥小叶-duck:个人主页 ❄️个人专栏:《Data-Structure-Learning》 《C++入门到进阶&自我学习过程记录》《算法题讲解指南》--从优选到贪心 ✨未择之路,不须回头 已择之路,纵是荆棘遍野,亦作花海遨游 目录 38.替换所有问号 题目链接: 题目描述: 题目示例: 解法(模拟): 算法思路: C++算法代码: 算法总结及流程解析: 39.提莫攻击 题目链接: 题目描述: 题目示例: 解法(模拟+分情况讨论): 算法思路: C++算法代码: 算法总结及流程解析: 40.Z 字形变换 题目链接: 题目描述: 题目示例: 解法(模拟+找规律): 算法思路: C+

By Ne0inhk
HDFS核心组件深度解析:分布式文件系统的架构基石

HDFS核心组件深度解析:分布式文件系统的架构基石

HDFS核心组件深度解析:分布式文件系统的架构基石 * 引言:HDFS——大数据的存储基石 * 一、HDFS架构全景 * 1.1 主从架构设计 * 1.2 核心组件概览 * 二、NameNode:HDFS的"大脑" * 2.1 核心职责 * 2.2 元数据存储结构 * 2.3 内存与持久化 * 2.4 单点故障问题 * 三、DataNode:HDFS的"数据仓库" * 3.1 核心职责 * 3.2 工作流程 * 3.3 数据存储结构 * 四、Secondary NameNode:NameNode的&

By Ne0inhk
【动态规划篇】专题(六):子序列问题——不连续的艺术

【动态规划篇】专题(六):子序列问题——不连续的艺术

文章目录 * LIS 模型及其衍生:回头看,全是风景 * 一、 前言:从 O(N) 到 O(N²) * 二、 最长递增子序列 (Medium) * 2.1 题目描述 * 2.2 核心思路:LIS 模型 * 2.3 代码实现 * 三、 摆动序列 (Medium) * 3.1 题目描述 * 3.2 状态定义:波峰与波谷 * 3.3 代码实现 * 四、 最长递增子序列的个数 (Medium) * 4.1 题目描述 * 4.2 双重状态 * 4.

By Ne0inhk
Flutter for OpenHarmony: Flutter 三方库 directed_graph 在鸿蒙应用中优雅处理复杂的拓扑排序与依赖关系(算法级工具)

Flutter for OpenHarmony: Flutter 三方库 directed_graph 在鸿蒙应用中优雅处理复杂的拓扑排序与依赖关系(算法级工具)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在进行 OpenHarmony 的复杂业务逻辑设计时,我们经常会遇到“依赖关联”问题。例如: 1. 任务调度:任务 A 依赖于任务 B 和 C,任务 B 依赖于 D。你应该按什么顺序运行它们? 2. 数据流建模:在鸿蒙分布式节点中,数据是如何从一个端点流向另一个端点的?是否存在循环引用(Cycle)? 3. 资源加载器:一个大型鸿蒙 HAP 包内的资源加载优先级排序。 directed_graph 是一款纯粹的、算法级别的 Dart 库。它提供了标准的数据结构模型,能帮你极其高效地处理这些复杂的拓扑(Topology)关系。 一、有向图逻辑模型 该库支持对图节点进行深度遍历、

By Ne0inhk