HarmonyOS 相机开发从入门到放弃
一、背景引入:这玩意儿是干啥的?
今儿个咱聊聊 Camera Kit,中文名儿叫"相机服务"。听名字就知道,这玩意儿就是让你调用相机的。
你可能会问:“调用相机?我自己写个相机应用不就完了吗?”
嘿,您要真这么想,那我得给您点个赞——有这股劲儿,当年我写相机也是这么想的。但踩了几个坑之后,我就服了。
为啥要用 Camera Kit?
咱说个实际场景:
你在应用里想做个拍照功能,用户点了个"拍照"按钮,你得让人家能预览、能拍照、能录像吧?这时候你有几个选择:
- 自己写底层驱动:跟硬件打交道,ISP、HDI、缓存队列…您慢慢写,写完了叫我一声
- 用系统相机:拉起系统相机拍一张,简单,但定制性差
- 用 Camera Kit:系统提供的相机开发套件,能精确控制硬件
我选 3,为啥?
- 不用自己写驱动:预览、拍照、录像,系统都给你整明白了
- 能定制:闪光灯、曝光时间、对焦调焦,都能控制
- 多镜头适配:广角、长焦、TOF,多摄同开,香
说白了,Camera Kit 就是个"相机管家",你告诉它要干啥,它帮你调度硬件,完事儿。
这玩意儿能干啥?
咱列个表,您心里有数:
| 功能 | 说明 | 难度 |
|---|---|---|
| 预览 | 实时显示相机画面 | ⭐ 简单 |
| 拍照 | 拍摄并保存照片 | ⭐⭐ 中等 |
| 录像 | 录制视频(含音频) | ⭐⭐⭐ 有点麻烦 |
| 闪光灯控制 | 开关、自动、常亮 | ⭐ 简单 |
| 对焦调焦 | 自动对焦、手动对焦 | ⭐⭐ 中等 |
| 曝光控制 | 调整曝光时间、ISO | ⭐⭐⭐ 有点麻烦 |
| 多摄同开 | 同时开多个摄像头 | ⭐⭐⭐⭐ 难 |
注意啊:多摄同开这玩意儿,不是所有设备都支持,你得先检查。
二、整体架构:大概长啥样?
Camera Kit 的架构,说白了就三块:
┌─────────────────────────────────────────┐ │ 你的应用 │ │ (创建会话、配置输入输出) │ └─────────────────┬───────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ Camera Kit 服务 │ │ (会话管理、设备管理、输出管理) │ └─────────────────┬───────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ 相机硬件 + ISP │ │ (真正的采集和处理) │ └─────────────────────────────────────────┘ 工作流程:
- 你获取 CameraManager(相机管家)
- 创建 CameraInput(相机输入)—— 选哪个摄像头
- 创建 CameraSession(相机会话)—— 配置拍摄模式
- 添加 Output(输出流)—— 预览流、拍照流、录像流
- 提交配置,启动会话
- 开始拍照/录像
- 完事儿释放资源
几个概念先整明白:
- 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 能干啥?
- 预览、拍照、录像
- 闪光灯、对焦、曝光控制
- 多摄同开(部分设备支持)
开发流程?
- 申请权限(CAMERA、MICROPHONE)
- 获取 CameraManager
- 获取相机列表,选一个
- 创建 CameraInput
- 创建 CameraSession
- 添加输入输出流
- 提交配置,启动会话
- 拍照/录像
- 释放资源
有啥坑?
- 权限没申请就调用
- 相机被占用不处理
- 输出流添加顺序错了
- 不检查 canAddOutput
- 会话配置完不提交
- 释放资源不及时
- 在模拟器上测试
我的建议
如果你就是做个简单的拍照功能:
- 用 CameraPicker 就够了,不用自己整 Camera Kit
- CameraPicker 不用申请权限,直接拉起系统相机
如果你要做深度定制:
- 相机状态监听一定要加
- canAddOutput 一定要检查
- 释放资源一定要及时
- 真机测试,别用模拟器