第24篇|相机权限和设备枚举:先判断能力再打开预览
双镜记忆相机的相机页不能只靠一个“打开相机”按钮。真正稳定的 CameraKit 入口需要把 CAMERA 权限、Surface 是否就绪、设备列表、前后摄能力和并发能力都串成一条清晰链路。学习目标知道 module.json5 与运行时授权分别解决什么问题。能从 CameraManager 获取设备列表并区分后摄、前摄和单摄兜底设备。理解为什么要在预览前先做能力探测而不是失败后再补救。把权限失败、设备为空、Surface 未就绪变成用户可理解的状态。一、先看相机入口应该长什么样第 24 篇先不急着拍照而是把相机页打开前的地基铺稳。双镜记忆相机的目标是同时服务“双拍”和“单拍回退”所以入口处要做三件事页面上的 Surface 已经创建用户已经允许 CAMERA 权限设备侧至少能找到一个可用 CameraDevice。只有这三个条件都满足后面的 PreviewOutput、PhotoSession、PhotoOutput 才有意义。如果直接在页面出现时调用 createCameraInput常见问题是黑屏、权限弹窗还没响应就初始化失败、部分机型只有一个摄像头时双摄逻辑反复重试。训练营的写法是先把能力探测做完整再决定进入双摄、单摄或提示用户处理权限。图 1 相机页运行效果与权限、设备探测链路二、清单权限module.json5 先声明 CAMERAHarmonyOS 的相机权限不是只在代码里请求就够了模块配置里必须先声明需要的权限和使用场景。项目的声明放在 entry/src/main/module.json5 的 requestPermissions 节点中CAMERA 权限限定在 EntryAbility 的 inuse 场景。这样系统权限弹窗有合法来源审核时也能看到权限用途。这一步解决的是“应用有没有资格申请权限”的问题下一步 requestPermissionsFromUser 解决的是“用户是否同意”。两个层次缺一不可。图 2 module.json5 中的 CAMERA 权限声明requestPermissions: [ { name: ohos.permission.CAMERA, reason: $string:camera_permission_reason, usedScene: { abilities: [ EntryAbility ], when: inuse } },三、运行时授权先查状态再触发请求进入相机页后项目先通过 checkPermissionGrant 判断当前是否已经授权。如果已经授权直接返回 true如果还没有授权再调用 requestPermissionsFromUser。这里还有一个细节权限请求和短超时用 Promise.race 包住避免用户停留在系统弹窗或系统返回异常时页面一直处于“正在申请权限”的等待态。这段逻辑的价值不只是拿到布尔值而是让相机入口具备可恢复能力。权限已经给过时不重复打扰用户权限没有给时页面能给出可见提示授权结果回来后再继续初始化 CameraKit。图 3 Index.ets 中运行时 CAMERA 权限请求逻辑private async requestCameraPermission(): Promiseboolean { const atManager: abilityAccessCtrl.AtManager abilityAccessCtrl.createAtManager(); const hostContext this.getUIContext().getHostContext() as common.UIAbilityContext; try { const currentGrant await this.checkPermissionGrant(ohos.permission.CAMERA); if (currentGrant abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) { console.info([superImage][capture] camera permission already granted); return true; } console.info([superImage][capture] request camera permission from user); const requestPromise: PromisePermissionRequestResult atManager.requestPermissionsFromUser( hostContext, this.cameraPermissionList ); const timeoutPromise: PromisePermissionRequestResult | undefined new Promise((resolve) { setTimeout(() { resolve(undefined); }, 1800); }); const result await Promise.race([requestPromise, timeoutPromise]); if (!result) {四、设备枚举从 CameraManager 推导可用路线权限通过后项目创建 CameraManager并调用 getSupportedCameras 获取设备列表。这个列表不是拿来展示数字的而是整个相机模式的决策输入能找到后摄和前摄就继续探测官方并发能力只找到一个设备就进入单摄预览设备为空就不要创建会话直接结束探测并保持页面可重试。注意这里没有把“双摄支持”当成默认成立。项目会先拿官方推荐的前后摄组合再看 getCameraConcurrentInfos 是否返回内容。返回为空并不代表应用坏了而是当前设备不支持前后摄同时工作这时应该优雅回退到单摄。图 4 权限通过后初始化 CameraKit 并枚举设备this.cameraStatusText 正在初始化 CameraKit...; const hostContext this.getUIContext().getHostContext() as common.UIAbilityContext; const cameraManager: camera.CameraManager camera.getCameraManager(hostContext); const cameras: Arraycamera.CameraDevice cameraManager.getSupportedCameras(); console.info([superImage][capture] camera devices${cameras.length}); this.cameraDeviceCount cameras.length; this.cameraProbeResultText this.buildConcurrentProbeReport(cameraManager, cameras); if (cameras.length 0) { this.cameraCapabilityChecked true; this.cameraStatusText ; return; } const officialPair this.getOfficialConcurrentCameraPair(cameraManager); const backDevice officialPair.backDevice ?? this.findCameraDeviceByPosition(cameras, camera.CameraPosition.CAMERA_POSITION_BACK); const frontDevice officialPair.frontDevice ?? this.findCameraDeviceByPosition(cameras, camera.CameraPosition.CAMERA_POSITION_FRONT); const fallbackSingleDevice backDevice ?? frontDevice ?? cameras[0]; this.cameraManager cameraManager; this.backCameraDevice backDevice; this.frontCameraDevice frontDevice; this.preferredBackSingleCameraDevice backDevice; this.singleCameraDevice fallbackSingleDevice; this.singleCameraRole this.getCameraRole(fallbackSingleDevice); this.singleCameraSupported true; this.refreshBackLensOptions(cameras, backDevice); if (!backDevice || !frontDevice) { this.cameraCapabilityChecked true; this.cameraStatusText ; await this.ensureCameraPreview(); return; } const concurrentInfos officialPair.concurrentInfos; this.concurrentInfos concurrentInfos; this.cameraCapabilityChecked true; this.cameraConcurrentProfileCount concurrentInfos.length; this.dualCameraSupported concurrentInfos.length 0; if (this.dualCameraSupported) { this.cameraStatusText ; await this.ensureCameraPreview(); } else { this.cameraStatusText ; await this.ensureCameraPreview();五、把入口状态设计成可解释而不是只看异常相机入口最容易被忽略的是“半成功状态”。例如 Surface 还没创建但权限已经允许设备列表有后摄没有前摄并发能力为空但单摄可用权限弹窗超时但用户稍后又打开系统设置授权。项目没有把这些都归成一个 catch而是拆成 cameraPermissionReady、cameraCapabilityChecked、dualCameraSupported、singleCameraSupported、cameraStatusText 等状态。这么做的好处是页面能知道下一步应该做什么。Surface 未就绪就稍后 scheduleCameraCapabilityPrepare权限未通过就展示提示双摄不支持就调用 ensureCameraPreview 进入单摄能力探测完成后不再重复请求。相机页由此从“碰运气打开”变成“按条件推进”。训练营后续的双摄、镜头、闪光灯和失败态都会复用这条入口链路。第 24 篇要记住的核心不是某一个 API而是顺序声明权限、请求授权、等待 Surface、枚举设备、探测并发、选择模式。本篇检查清单权限声明位于 module.json5并且 CAMERA 的 usedScene 指向 EntryAbility。requestCameraPermission 先查已有授权再请求用户授权。prepareCameraCapability 在 Surface 未就绪时不会强行创建相机会话。设备为空、只有单摄、双摄并发为空都能走到明确分支。正文配图包含运行截图、权限声明截图、运行时权限代码截图和 CameraKit 初始化截图。今日练习在真机上清除应用权限后重新进入相机页观察权限弹窗和页面提示。给 prepareCameraCapability 增加一条日志记录 cameras.length 与 concurrentInfos.length。把设备为空分支写成一条用户可读文案再验证不会继续创建 PhotoSession。