HarmonyOS趣味相机实战第6篇:相机权限申请、设备发现与前后镜头切换降级

HarmonyOS趣味相机实战第6篇:相机权限申请、设备发现与前后镜头切换降级
HarmonyOS趣味相机实战第6篇相机权限申请、设备发现与前后镜头切换降级摘要相机 App 的第一道稳定性门槛不是拍照 API而是“能不能正确进入相机能力”。用户可能拒绝权限设备可能没有前置摄像头模拟器可能没有真实相机某些机型只暴露后置切换镜头时旧预览会话可能还没释放权限申请还可能抛异常。如果这些状态没有拆清楚页面就会出现按钮可点但没反应、提示不准确、镜头切换失败、预览黑屏等问题。本文继续基于D:/APP/1quweixiangjiHarmonyOS ArkTS 水印相机项目复盘第 6 条工程主线相机权限、设备发现和前后摄像头切换。重点文件是module.json5、CameraPermissionService.ets、CameraDeviceService.ets和Index.ets。本文覆盖module.json5为什么要声明ohos.permission.CAMERA和usedScene。CameraPermissionService.requestCamera()如何封装运行时授权。CameraDeviceService.discover()如何发现前置、后置和总设备数。resolveActivePosition()如何处理偏好镜头不可用的降级。nextPosition()如何避免切到不存在的镜头。页面如何把权限、设备、预览和拍照提示串成闭环。工程背景与源码定位文件作用entry/src/main/module.json5EntryAbility、页面入口和相机权限声明entry/src/main/ets/service/CameraPermissionService.ets运行时相机权限申请返回 granted/denied/errorentry/src/main/ets/service/CameraDeviceService.ets摄像头发现、前后镜头可用性判断、默认镜头选择entry/src/main/ets/service/CameraPreviewService.ets根据 active camera position 启动 CameraKit 预览entry/src/main/ets/pages/Index.ets页面权限弹窗、设备刷新、镜头切换、预览启动和状态提示entry/src/main/resources/base/element/string.json权限用途说明资源entry/src/test/LocalUnit.test.ets当前已有照片快照测试后续可补设备选择纯函数测试环境与版本边界项目当前值说明当前复盘日期2026-07-14文章按当前工程源码与本地配置复盘工程路径D:/APP/1quweixiangji本文只引用该项目已有源码工程类型HarmonyOS Stage 模型页面入口为pages/Indextarget SDK6.0.2(22)是否 EOL 或存在 Breaking Change要以上架时 DevEco/SDK 发布说明为准compatible SDK6.0.2(22)真机验证要覆盖目标机型bundleNamecom.fun.quweixiangji当前应用包名versionName1.0.1当前复盘版本相机能力kit.CameraKit设备发现、预览、拍照权限能力kit.AbilityKitabilityAccessCtrl运行时授权构建命令cd D:\APP\1quweixiangji$env:JAVA_HOMED:\Program Files\Huawei\DevEco Studio\jbr$env:Path$env:JAVA_HOME\bin;$env:PathD:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.bat--mode module-p moduleentrydefault-p productdefault assembleHap--no-daemon一、权限声明是运行时授权的前置条件相机权限首先要在entry/src/main/module.json5中声明。项目里使用的是{requestPermissions:[{name:ohos.permission.CAMERA,reason:$string:camera_permission_reason,usedScene:{abilities:[EntryAbility],when:inuse}}]}这段配置有三个关键点字段说明name声明相机权限ohos.permission.CAMERAreason指向权限用途说明资源给用户解释为什么需要相机usedScene.when当前是前台拍照所以使用inuse如果只写运行时申请、不在module.json5声明权限链路是不完整的。用户看不到清晰用途系统授权也可能不符合预期。二、运行时授权只返回业务状态不直接操作 UICameraPermissionService.ets把权限请求封装成服务方法constCAMERA_PERMISSION:Permissionsohos.permission.CAMERA;exporttypeCameraPermissionStatusidle|granted|denied|error;exportinterfaceCameraPermissionResult{status:CameraPermissionStatus;message:string;}申请权限staticasyncrequestCamera(context:common.UIAbilityContext):PromiseCameraPermissionResult{try{constatManager:abilityAccessCtrl.AtManagerabilityAccessCtrl.createAtManager();constresultawaitatManager.requestPermissionsFromUser(context,[CAMERA_PERMISSION]);constgranted:booleanresult.authResults.length0result.authResults[0]0;if(granted){return{status:granted,message:相机权限已开启可以接入真实预览};}return{status:denied,message:相机权限未开启请开启后使用真实取景};}catch(error){hilog.error(DOMAIN,TAG,request camera permission failed: %{public}s,JSON.stringify(error));return{status:error,message:相机权限申请失败请稍后重试};}}这个服务层没有直接显示弹窗也没有改页面状态。它只负责把系统授权结果转换为业务状态系统结果业务状态页面动作authResults[0] 0granted继续发现摄像头用户拒绝denied展示权限未开启提示API 抛异常error展示申请失败提示页面和服务解耦后权限逻辑更容易复用和测试。三、页面先给业务说明再触发系统授权页面里点击“启用相机”时先打开自定义权限说明而不是直接弹系统授权Button(启用相机).onClick((){this.openCameraPermissionPrompt();})确认后再请求系统权限privateasyncrequestCameraPermission():Promisevoid{this.cameraPermissionPromptVisiblefalse;constcontext:common.UIAbilityContextthis.getUIContext().getHostContext()ascommon.UIAbilityContext;constresultawaitCameraPermissionService.requestCamera(context);this.cameraPermissionStatusresult.status;this.cameraStatusTextresult.message;if(result.statusgranted){this.refreshCameraDevices(context,this.activeCameraPosition);}else{this.cameraAvailablefalse;this.captureStatusTextresult.message;}}这条链路的好处是用户先理解为什么要开相机。授权成功后立即发现设备。授权失败后页面不会继续启动 CameraKit。权限异常也有明确提示不会静默失败。四、设备发现不要假设前后摄像头都存在CameraDeviceService.discover()负责发现设备staticdiscover(context:common.UIAbilityContext,preferredPosition:CameraPositionKey):CameraDeviceState{try{constmanager:camera.CameraManagercamera.getCameraManager(context);constsupportedDevices:camera.CameraDevice[]manager.getSupportedCameras();consthasFront:booleanCameraDeviceService.hasCamera(manager,camera.CameraPosition.CAMERA_POSITION_FRONT);consthasBack:booleanCameraDeviceService.hasCamera(manager,camera.CameraPosition.CAMERA_POSITION_BACK);constactivePosition:CameraPositionKeyCameraDeviceService.resolveActivePosition(preferredPosition,hasFront,hasBack);constmessage:stringCameraDeviceService.buildMessage(supportedDevices.length,hasFront,hasBack,activePosition);return{available:supportedDevices.length0,totalCount:supportedDevices.length,hasFront,hasBack,activePosition,message};}catch(error){hilog.error(DOMAIN,TAG,discover camera failed: %{public}s,JSON.stringify(error));return{available:false,totalCount:0,hasFront:false,hasBack:false,activePosition:preferredPosition,message:摄像头检测失败请确认设备相机可用};}}这里没有假设“手机一定有前后双摄”而是显式记录字段说明available是否有任意可用摄像头totalCount系统返回的支持设备数量hasFront前置是否可用hasBack后置是否可用activePosition最终选中的镜头message给页面展示的状态文案对真机适配来说这比只存一个cameraAvailable更可靠。五、单个镜头探测要捕获异常判断某个方向是否可用privatestatichasCamera(manager:camera.CameraManager,position:camera.CameraPosition):boolean{try{constdevice:camera.CameraDevicemanager.getCameraDevice(position,camera.CameraType.CAMERA_TYPE_DEFAULT);returndevice!undefined;}catch(error){hilog.warn(DOMAIN,TAG,camera position unavailable: %{public}s,JSON.stringify(error));returnfalse;}}getCameraDevice()可能因为设备不支持该方向而抛异常。这里捕获后返回 false避免整个设备发现失败。这种处理对多机型很重要设备情况处理方式只有后置hasFrontfalse默认后置只有前置hasBackfalse默认前置没有真实相机availablefalse页面提示某方向 API 抛异常该方向不可用不影响另一个方向六、偏好镜头不可用时自动降级镜头选择逻辑privatestaticresolveActivePosition(preferredPosition:CameraPositionKey,hasFront:boolean,hasBack:boolean):CameraPositionKey{if(preferredPositionfronthasFront){returnfront;}if(preferredPositionbackhasBack){returnback;}if(hasBack){returnback;}returnfront;}这个策略按优先级执行用户偏好前置且前置可用使用前置。用户偏好后置且后置可用使用后置。偏好不可用但后置可用使用后置。后置不可用退到前置。它避免了一个常见问题用户上次选择前置但这台设备没有前置页面仍然拿前置启动预览结果启动失败。七、切换镜头时不要切到不存在的方向下一镜头选择staticnextPosition(current:CameraPositionKey,hasFront:boolean,hasBack:boolean):CameraPositionKey{if(currentfronthasBack){returnback;}if(currentbackhasFront){returnfront;}returncurrent;}页面切换privateswitchCameraPosition():void{if(!this.isCameraGranted()){this.openCameraPermissionPrompt();return;}constnextPosition:CameraPositionKeyCameraDeviceService.nextPosition(this.activeCameraPosition,this.hasFrontCamera,this.hasBackCamera);constcontext:common.UIAbilityContextthis.getUIContext().getHostContext()ascommon.UIAbilityContext;this.refreshCameraDevices(context,nextPosition);}这里有两个细节没授权时切换镜头按钮会打开权限说明不会静默失败。如果另一侧镜头不存在nextPosition()返回当前镜头不会强行切换。八、设备刷新后自动启动预览设备刷新逻辑privaterefreshCameraDevices(context:common.UIAbilityContext,preferredPosition:CameraPositionKey):void{conststate:CameraDeviceStateCameraDeviceService.discover(context,preferredPosition);this.cameraAvailablestate.available;this.cameraTotalCountstate.totalCount;this.hasFrontCamerastate.hasFront;this.hasBackCamerastate.hasBack;this.activeCameraPositionstate.activePosition;this.cameraDeviceTextstate.message;this.captureStatusTextstate.available?state.message:相机权限已开但没有检测到可用摄像头;if(state.availablethis.previewSurfaceId.length0){this.startCameraPreview();}}它把服务层状态同步到页面服务状态页面状态availablecameraAvailabletotalCountcameraTotalCounthasFronthasFrontCamerahasBackhasBackCameraactivePositionactiveCameraPositionmessagecameraDeviceText和captureStatusText如果 Surface 已经准备好就直接启动预览。否则等XComponent.onLoad()后再启动。九、预览启动也有三重前置条件启动预览前privateasyncstartCameraPreview():Promisevoid{if(!this.isCameraGranted()||!this.cameraAvailable||this.previewSurfaceId.length0){return;}this.previewStatusstarting;this.previewStatusText正在启动相机预览;constcontext:common.UIAbilityContextthis.getUIContext().getHostContext()ascommon.UIAbilityContext;constresult:CameraPreviewStateawaitCameraPreviewService.startPreview(context,this.previewSurfaceId,this.activeCameraPosition);}三重条件条件缺失时后果权限已授予不能启动 CameraKit有可用摄像头启动会话失败Surface ID 已存在预览输出没有承载面这三个条件任何一个不满足都不应该进入CameraPreviewService.startPreview()。十、状态文案要同时服务用户和调试CameraDeviceService.buildMessage()privatestaticbuildMessage(totalCount:number,hasFront:boolean,hasBack:boolean,activePosition:CameraPositionKey):string{if(totalCount0){return未检测到可用摄像头;}constactiveText:stringactivePositionfront?前置:后置;constfrontText:stringhasFront?前置可用:前置不可用;constbackText:stringhasBack?后置可用:后置不可用;return${activeText}摄像头已准备${frontText}${backText};}这类文案不只是给用户看的也是调试线索文案说明未检测到可用摄像头权限可能已开但设备层没有摄像头前置摄像头已准备前置可用后置不可用后置不可切换后置摄像头已准备前置不可用后置可用前置按钮不应切换摄像头检测失败CameraKit 查询阶段异常好的状态文案能少看一半日志。十一、权限、设备、Surface 的完整时序完整链路可以整理为用户点击启用相机 - 打开业务权限说明 - requestPermissionsFromUser - granted - CameraDeviceService.discover - 解析 hasFront / hasBack / activePosition - 如果 XComponent Surface 已就绪 - CameraPreviewService.startPreview - PhotoSession.startSurface 另一条链路XComponent.onLoad - previewController.getXComponentSurfaceId() - 如果权限已开且设备可用 - CameraPreviewService.startPreview这两个链路谁先完成都可以。权限先完成就等 SurfaceSurface 先完成就等权限和设备。十二、建议补充的单元测试CameraDeviceService.resolveActivePosition()当前是 private后续可以把纯选择逻辑抽出来测试。建议覆盖preferredhasFronthasBack期望fronttruetruefrontfrontfalsetruebackbacktruefalsefrontbackfalsetruebackfronttruefalsefrontnextPosition()已经是 public可以直接测试expect(CameraDeviceService.nextPosition(front,true,true)).assertEqual(back);expect(CameraDeviceService.nextPosition(back,true,true)).assertEqual(front);expect(CameraDeviceService.nextPosition(back,false,true)).assertEqual(back);expect(CameraDeviceService.nextPosition(front,true,false)).assertEqual(front);权限服务则更适合集成测试或手工真机测试因为它依赖系统授权弹窗。十三、常见问题排查现象可能原因排查方式点击启用相机无反应没拿到UIAbilityContext或权限请求异常查requestCameraPermission()用户拒绝后仍启动预览页面没有判断result.status确认 denied 时设置cameraAvailablefalse模拟器提示没有摄像头getSupportedCameras()返回空看totalCount和cameraDeviceText只有后置仍能点前置切换逻辑没判断hasFront查nextPosition()偏好前置但启动失败没做resolveActivePosition()降级查 activePositionSurface 已加载但不预览权限或设备未就绪查三重条件授权成功但没有自动预览Surface ID 为空查onPreviewSurfaceReady()切换镜头后黑屏旧会话没释放干净查CameraPreviewService.startPreview()是否先stopPreview()状态文案误导用户文案没有包含前后镜头可用性查buildMessage()上架审核质疑权限权限用途说明不清楚检查reason和隐私政策十四、上线前验收清单module.json5已声明ohos.permission.CAMERA。权限reason指向有效资源字符串。未授权时展示业务权限说明。用户拒绝后不启动 CameraKit。权限申请异常时有错误提示。授权成功后立即执行摄像头发现。能正确识别前置可用、后置可用、双摄可用和无摄像头。偏好镜头不可用时能自动降级。切换镜头不会切到不存在的方向。Surface ID 为空时不启动预览。Surface 就绪后能根据当前权限和设备状态启动预览。设备发现失败时不影响页面其他功能。切换镜头前后能释放并重启预览会话。状态文案能显示当前 activePosition 和前后镜头可用性。真机覆盖前置、后置、拒绝权限、二次授权、后台返回、模拟器无摄像头等场景。2026-07-14 之后如果升级 SDK要复核 CameraKit 和权限 API 是否有 Breaking Change。总结1quweixiangji的相机入口设计把权限、设备和预览拆成三层module.json5负责声明权限CameraPermissionService负责运行时授权CameraDeviceService负责发现前后摄像头和选择可用镜头Index.ets负责把授权结果、设备状态、Surface 就绪和预览启动串起来。这套拆分的价值在于降级清晰没权限就不发现设备没设备就不启动预览偏好镜头不可用就自动换可用方向Surface 没就绪就等待。相机项目能稳定跑起来靠的不是一个拍照按钮而是这些边界状态都被认真处理。