news 2026/7/15 11:12:23

HarmonyOS趣味相机实战第12篇:CameraKit错误分层、相机占用恢复与用户可重试状态机

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HarmonyOS趣味相机实战第12篇:CameraKit错误分层、相机占用恢复与用户可重试状态机

HarmonyOS趣味相机实战第12篇:CameraKit错误分层、相机占用恢复与用户可重试状态机

摘要

CameraKit 相机项目最难排查的故障往往都被用户描述成一句“相机打不开”。实际原因可能完全不同:相机权限被拒绝、设备没有可用镜头、XComponent Surface 尚未创建、旧 PhotoSession 没释放、相机被其他应用占用、photoAvailable没在超时窗口内返回,甚至只是页面拿到了旧任务的迟到 PixelMap。如果所有错误都显示“请重试”,用户无法恢复,开发者也无法从日志定位。

本文继续基于D:/APP/1quweixiangjiHarmonyOS ArkTS 趣味相机项目,把相机故障拆成权限、设备、Surface、预览、拍照和释放六层,复盘现有服务如何把系统异常转换成业务状态,并给出可直接落地的错误码映射、恢复动作、任务代际和真机回归方案。

工程背景与源码定位

文件主要错误边界
entry/src/main/ets/service/CameraPermissionService.ets运行时权限 granted、denied、error
entry/src/main/ets/service/CameraDeviceService.ets摄像头发现、方向不可用和默认镜头降级
entry/src/main/ets/service/CameraPreviewService.etsSurface、CameraInput、PhotoSession、输出和拍照回调
entry/src/main/ets/pages/Index.ets页面状态、用户提示、重试入口和结果所有权
entry/src/main/module.json5ohos.permission.CAMERA声明
entry/src/test/LocalUnit.test.ets当前水印快照测试,可扩展错误路由纯函数测试

环境与版本边界

项目当前值说明
工程路径D:/APP/1quweixiangji本文按当前源码分析
应用版本1.0.2来自AppScope/app.json5
target SDK6.0.2(22)SDK 升级后要回归错误码和会话行为
应用模型HarmonyOS Stage 模型EntryAbility加载单页工作台
相机能力@kit.CameraKit设备、输入、会话、预览和拍照
日志能力@kit.PerformanceAnalysisKit使用 hilog 记录可公开诊断信息
图像对象@ohos.multimedia.imagePixelMap、ImageSource 与照片组件

本地构建命令:

cd D:\APP\1quweixiangji$env:JAVA_HOME='D:\Program Files\Huawei\DevEco Studio\jbr'$env:Path="$env:JAVA_HOME\bin;$env:Path"&'D:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.bat'--mode module-p module=entry@default-p product=default assembleHap--no-daemon

一、先定义错误发生在哪一层

建议把相机链路看成六层闸门:

权限 -> 设备 -> Surface -> 预览会话 -> 拍照回调 -> 资源释放

每一层的责任不同:

层级成功条件失败后是否应该继续
权限用户授予 CAMERA否,不能发现或打开相机
设备找到至少一个可用镜头否,展示设备不可用
SurfaceXComponent 返回有效 Surface ID等待,不应提前启动预览
预览PhotoSession 成功 start否,释放半成品并允许重试
拍照本次收到真实 photoAvailable 与 PixelMap预览可保持,允许再次拍照
释放所有对象 stop/release/close失败也要继续释放其他对象

只有先确定层级,恢复动作才不会错。

二、权限错误:拒绝和 API 异常不是一回事

权限服务定义:

exporttypeCameraPermissionStatus='idle'|'granted'|'denied'|'error';exportinterfaceCameraPermissionResult{status:CameraPermissionStatus;message:string;}

申请逻辑:

constresult=awaitatManager.requestPermissionsFromUser(context,['ohos.permission.CAMERA']);constgranted:boolean=result.authResults.length>0&&result.authResults[0]===0;

拒绝返回:

return{status:'denied',message:'相机权限未开启,请开启后使用真实取景'};

API 异常返回:

catch(error){hilog.error(DOMAIN,TAG,'request camera permission failed: %{public}s',JSON.stringify(error));return{status:'error',message:'相机权限申请失败,请稍后重试'};}

两种状态的恢复不同:

状态用户动作
denied再次阅读权限用途,前往系统设置或重新授权
error稍后重试,必要时重启应用

页面在非 granted 时必须把cameraAvailable设为 false,不能继续走设备发现。

三、设备错误:不要把没有镜头误判成权限问题

设备服务先读取:

constmanager:camera.CameraManager=camera.getCameraManager(context);constsupportedDevices:camera.CameraDevice[]=manager.getSupportedCameras();

然后分别探测前后方向:

privatestatichasCamera(manager:camera.CameraManager,position:camera.CameraPosition):boolean{try{constdevice:camera.CameraDevice=manager.getCameraDevice(position,camera.CameraType.CAMERA_TYPE_DEFAULT);returndevice!==undefined;}catch(error){hilog.warn(DOMAIN,TAG,'camera position unavailable: %{public}s',JSON.stringify(error));returnfalse;}}

返回状态包含设备总数、前后镜头和最终方向。这样可以区分:

  • 权限已开但模拟器没有真实相机。
  • 只有后置,没有前置。
  • 用户偏好前置,但当前设备只能降级到后置。
  • CameraManager 查询本身抛异常。

没有设备时,提示应是“未检测到可用摄像头”,而不是让用户反复授权。

四、Surface 错误:尚未就绪应该等待

预览服务入口先检查:

if(surfaceId.length===0){return{status:'error',message:'预览画面尚未就绪'};}

页面则通过 XComponent 获取:

privateonPreviewSurfaceReady():void{this.previewSurfaceId=this.previewController.getXComponentSurfaceId();if(this.isCameraGranted()&&this.cameraAvailable){this.startCameraPreview();}}

严格来说,“Surface 尚未就绪”通常是等待状态,不是不可恢复错误。页面应该使用三重闸门:

if(!this.isCameraGranted()||!this.cameraAvailable||this.previewSurfaceId.length===0){return;}

权限、设备和 Surface 谁最后完成,谁触发统一的tryStartPreview(),避免重复启动。

五、预览失败必须释放半成品资源

预览服务启动前先调用:

awaitCameraPreviewService.stopPreview();

之后依次创建 CameraManager、Device、CameraInput、PreviewOutput、PhotoOutput 和 PhotoSession。任何一步失败,catch 中执行:

catch(error){hilog.error(DOMAIN,TAG,'start preview failed: %{public}s',JSON.stringify(error));awaitCameraPreviewService.stopPreview();return{status:'error',message:'相机预览启动失败,请检查权限或相机占用'};}

这里的核心不是提示文案,而是再次调用统一释放。否则可能留下已经 open 的 CameraInput 或只配置了一半的 PhotoSession,下一次重试就更容易报相机冲突。

六、把 CameraKit 错误码映射成恢复动作

相机输入和会话相关场景需要重点关注以下错误信号:

错误码方向用户提示程序恢复
7400102操作不允许当前相机状态不可用检查调用顺序,释放后重建
7400107相机冲突相机可能被占用释放本应用旧会话,提示关闭其他相机应用
7400108安全原因禁用相机被系统安全策略禁用引导检查系统设置,不循环重试
7400201相机服务致命错误相机服务异常完整释放,限次重建,仍失败则停止

可以定义业务错误:

exporttypeCameraErrorKind='permissionDenied'|'deviceUnavailable'|'surfaceNotReady'|'cameraConflict'|'securityDisabled'|'serviceFatal'|'captureTimeout'|'unknown';

错误对象只负责分类,页面根据 kind 选择按钮和文案。

七、避免无限自动重试

相机冲突和服务异常不适合无上限重建。推荐:

constMAX_PREVIEW_RETRY:number=2;privatepreviewRetryCount:number=0;

恢复策略:

  1. 第一次失败,完整释放并等待短暂退避。
  2. 第二次失败,允许再重建一次。
  3. 超过上限,停止自动重试,显示“重新启用相机”按钮。
  4. 用户主动点击后把计数清零,开始新一轮。

安全禁用和权限拒绝不应该自动重试,因为外部状态没有改变。

八、用会话代际淘汰旧启动结果

镜头切换、页面退出和 Surface 重建可能同时触发异步启动。建议:

privatepreviewGeneration:number=0;privateasyncstartCameraPreview():Promise<void>{constgeneration:number=++this.previewGeneration;this.previewStatus='starting';constresult:CameraPreviewState=awaitCameraPreviewService.startPreview(context,this.previewSurfaceId,this.activeCameraPosition);if(generation!==this.previewGeneration){return;}this.previewStatus=result.status;this.previewStatusText=result.message;}

页面退出或 Surface 销毁时递增 generation。旧启动即使晚到,也不能覆盖新状态。

九、拍照错误不应该摧毁可用预览

服务在photoOutput为空时返回:

return{success:false,message:'相机拍照输出尚未就绪',realPhotoReceived:false,photoEventCount:CameraPreviewService.photoEventCount,visionTargetCount:0};

页面调用后继续检查:

if(!captureState.realPhotoReceived||captureState.photoPixelMap===undefined){this.isCapturing=false;return;}

拍照失败时,预览会话可能仍然可用。正确行为通常是:

  • 不创建CapturedPhoto
  • 不进入结果弹层。
  • 保持预览运行。
  • 恢复快门按钮。
  • 给用户一次明确重试机会。

除非 CameraKit 返回会话级或服务级错误,否则不要因为单次拍照超时就重建整个预览。

十、区分 capture 调用成功和真实照片到达

PhotoOutput.capture()没抛异常,只能说明触发调用被接受。项目继续等待本次photoAvailable

constrealPhotoReceived:boolean=awaitCameraPreviewService.waitForPhotoAvailable(captureStartedAt,350);

若没有真实回调,返回:

message:'未获取到真实照片,请重试'

页面不能只看success,还要看realPhotoReceivedphotoPixelMap。这避免把旧预览画面或空记录保存到相册。

十一、迟到回调要验证任务归属

当前服务使用captureStartedAt区分本次照片:

if(CameraPreviewService.lastPhotoAvailableAt>=captureStartedAt){returnPromise.resolve(true);}

页面层还应增加 capture generation。若用户已经退出页面、重新拍摄或切换镜头,迟到结果要立即释放:

if(generation!==this.captureGeneration||!this.pageActive){if(captureState.photoPixelMap!==undefined){awaitcaptureState.photoPixelMap.release();}return;}

“不再展示”不代表“可以不释放”。PixelMap 所有权已经交给页面,失效结果仍由页面收口。

十二、释放错误要逐项隔离

统一释放中通常包含:

取消视觉采样 -> 移除 photoAvailable/metadata 回调 -> session.stop() -> session.release() -> metadataOutput.release() -> photoOutput.release() -> previewOutput.release() -> cameraInput.close()

某个 release 失败,不能阻断后续对象释放。推荐每一步独立 try/catch,并记录对象类型。释放入口要保持幂等:多次调用不会因为静态引用仍指向旧对象而重复操作。

十三、页面提示必须带下一步动作

对用户有用的错误提示至少包含“发生了什么”和“现在能做什么”:

错误不推荐文案推荐文案
权限拒绝相机失败相机权限未开启,请授权后重试
无设备相机失败未检测到可用摄像头
相机冲突请重试相机可能被占用,请关闭其他相机应用后重试
安全禁用请重试相机被系统禁用,请检查安全设置
拍照超时拍照成功未获取到真实照片,请保持预览后重试
服务异常未知错误相机服务异常,已停止自动重试

错误文案不要暴露完整异常栈、文件路径或系统内部对象。

十四、日志字段设计

建议每次预览与拍照记录:

hilog.info(DOMAIN,TAG,'preview generation=%{public}d position=%{public}s status=%{public}s',generation,this.activeCameraPosition,result.status);

推荐字段:

字段用途
previewGeneration区分预览重建轮次
captureGeneration区分拍照轮次
activeCameraPosition定位前置或后置差异
previewStatus判断失败阶段
surfaceReady判断 Surface 闸门
photoEventCount判断回调是否到达
errorCode映射 CameraKit 恢复动作

不要记录照片内容、PixelMap 字节、水印地点、用户备注或人体关键点。

十五、建议补充的纯函数测试

错误码路由可以抽成纯函数:

exportfunctionmapCameraError(code:number):CameraErrorKind{if(code===7400107){return'cameraConflict';}if(code===7400108){return'securityDisabled';}if(code===7400201){return'serviceFatal';}return'unknown';}

测试矩阵:

expect(mapCameraError(7400107)).assertEqual('cameraConflict');expect(mapCameraError(7400108)).assertEqual('securityDisabled');expect(mapCameraError(7400201)).assertEqual('serviceFatal');expect(mapCameraError(-1)).assertEqual('unknown');

还可以测试哪些错误允许自动重试:

kind自动重试
permissionDenied
deviceUnavailable
surfaceNotReady等待 Surface,不计为失败
cameraConflict限次重试
securityDisabled
serviceFatal完整释放后限次重试
captureTimeout保持预览,允许用户再次拍照

十六、真机故障注入流程

权限路径

  1. 首次启动拒绝权限。
  2. 确认不发现设备、不启动预览。
  3. 再次授权后确认能恢复。

相机占用

  1. 让另一相机应用持有摄像头。
  2. 返回趣味相机启动预览。
  3. 确认识别冲突、限次重试并给出明确提示。

Surface 生命周期

  1. 预览运行时销毁 XComponent。
  2. 确认旧会话释放。
  3. Surface 重建后只启动一次新会话。

拍照超时

  1. 连续拍摄并快速切后台。
  2. 验证迟到回调不会创建旧照片。
  3. 迟到 PixelMap 被释放。

服务错误

  1. 记录错误码、镜头方向和 generation。
  2. 完整释放后限次重建。
  3. 超过重试上限后停止循环。

十七、常见问题排查

现象可能原因排查方式
反复授权仍黑屏实际是设备或 Surface 层错误分层检查权限、设备、Surface
切镜头后旧文案覆盖新状态异步启动结果过期增加 previewGeneration
预览失败后一直冲突半成品资源未释放catch 中统一 stopPreview
点击拍照无结果photoAvailable 未在窗口内返回查 captureStartedAt 与事件计数
拍照超时却生成照片页面只检查 success同时检查 realPhotoReceived 和 PixelMap
页面退出后弹出旧照片迟到结果未校验代际检查 captureGeneration
内存持续增长失效 PixelMap 未释放所有取消路径显式 release
release 抛错后仍占相机一步失败阻断后续释放每个对象独立 try/catch
相机冲突无限重试没有重试上限设置 MAX_PREVIEW_RETRY
安全禁用仍循环打开错误码路由错误7400108 不自动重试
用户不知道如何恢复文案只有“失败”文案加入下一步动作
日志泄露用户内容记录了照片或水印正文只记录状态、代际和错误码

十八、上线前验收清单

  • 权限 denied 与 permission API error 分开处理。
  • 未授权时不进入设备发现。
  • 无设备时不提示用户重复授权。
  • Surface 未就绪时等待,不启动 PhotoSession。
  • 预览启动失败时释放全部半成品资源。
  • 7400102740010774001087400201有业务映射。
  • 自动重试有次数上限。
  • 权限拒绝和安全禁用不自动重试。
  • 预览异步结果按 generation 校验。
  • 拍照失败不默认销毁仍可用的预览。
  • 页面检查 realPhotoReceived 和 photoPixelMap。
  • 迟到 PixelMap 在失效路径释放。
  • 每个系统资源独立 stop/release/close。
  • 释放入口可重复调用。
  • 错误文案包含可执行的恢复动作。
  • 日志包含镜头、状态、代际和错误码。
  • 日志不记录照片、水印或人体数据。
  • 拒绝权限、相机占用、切后台、Surface 重建和拍照超时完成真机验证。
  • SDK 升级后重新确认错误码和 CameraKit 生命周期行为。

总结

相机稳定性不是给每个 catch 加一句“请重试”,而是先判断错误属于权限、设备、Surface、预览、拍照还是释放层,再为这一层选择正确恢复动作。1quweixiangji当前已经通过服务层状态对象、预览失败统一释放、真实照片确认和 PixelMap 所有权控制打好了基础。

进一步完善时,应加入错误码业务映射、预览与拍照 generation、限次重试、迟到结果释放和带下一步动作的用户文案。这样“相机打不开”就能被拆成可观测、可恢复、可测试的状态机;相机冲突不会无限循环,拍照超时不会生成假记录,页面退出也不会接收旧任务结果。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/15 11:11:31

S-Cart插件开发实战:如何创建自定义支付和物流插件

S-Cart插件开发实战&#xff1a;如何创建自定义支付和物流插件 【免费下载链接】s-cart Free Laravel e-commerce for business: shopping cart, cms content, and more... 项目地址: https://gitcode.com/gh_mirrors/sc/s-cart S-Cart是一款基于Laravel框架的免费电子商…

作者头像 李华
网站建设 2026/7/15 11:09:25

从地心到图纸:四大坐标系在GIS与工程测量中的角色与转换实战

1. 坐标系&#xff1a;从卫星定位到施工放样的语言桥梁 第一次用GPS接收机采集数据时&#xff0c;我盯着屏幕上的数字彻底懵了——为什么同一地点会显示三组完全不同的坐标&#xff1f;这个困惑直到参与某高速公路勘测项目才真正解开。当时施工队拿着设计院的图纸放样&#xff…

作者头像 李华
网站建设 2026/7/15 11:09:16

MATLAB图像处理:ROI技术详解与应用实践

1. MATLAB中ROI的基本概念与应用场景在图像处理领域&#xff0c;感兴趣区域&#xff08;Region of Interest&#xff0c;简称ROI&#xff09;是指图像中需要特别关注或进行特定操作的部分区域。这个概念最早来源于医学影像处理&#xff0c;医生需要重点关注CT或MRI图像中的特定…

作者头像 李华
网站建设 2026/7/15 11:09:02

技术提问与总结的黄金法则:提升开发效率的关键技能

1. 为什么我们需要学习提问和总结在技术社区混迹多年&#xff0c;我发现一个令人深思的现象&#xff1a;同样的问题&#xff0c;有人三言两语就能获得详细解答&#xff0c;有人长篇大论却得不到回应。这背后的差异往往不在于问题的难度&#xff0c;而在于提问的方式。记得刚入行…

作者头像 李华
网站建设 2026/7/15 11:09:01

单片机软硬件设计核心要点与工程实践

1. 单片机设计的核心考量维度当我在2013年第一次接触STC89C52单片机时&#xff0c;曾天真地认为只要写好代码就能让系统跑起来。直到实际焊接电路才发现&#xff0c;电源滤波电容少焊了一个&#xff0c;导致ADC采样值跳变超过30%。这个教训让我明白&#xff1a;单片机设计是软硬…

作者头像 李华