第28篇|相机失败态:没有权限、没有设备、会话失败时如何提示

第 28 篇专门处理失败态。相机失败不能只靠 catch 打日志,而要把权限失败、设备为空、会话失败、Surface 销毁和资源释放都变成可恢复流程。

学习目标

  • 把相机失败拆成权限、能力、会话、资源四类。
  • 知道哪些状态字段负责 UI 提示,哪些字段负责资源释放。
  • 理解为什么失败后要先清状态再释放资源。
  • 能设计一个可以重新进入的相机页,而不是失败一次就卡死。

一、失败态是相机页质量的分水岭

相机功能最容易在演示时正常,在真实用户手里出问题。用户可能拒绝权限,设备可能没有前摄,Surface 可能因为页面切换销毁,PhotoSession 可能在 commitConfig 或 start 时失败。第 28 篇要做的不是列异常码,而是把失败态写成产品流程。

双镜记忆相机的原则是:任何失败都不能留下黑屏和脏资源。页面要么能说明当前缺什么,要么能释放干净,下一次进入时重新探测。

图 1 失败态处理目标:状态可见、资源可释放、流程可重试

二、状态字段先分层,失败时才不会乱

项目把相机状态拆成权限、能力、会话、预览、拍摄中的 pending 数据。cameraPermissionReady 表示是否能继续 CameraKit 初始化;cameraCapabilityChecked 表示本轮能力探测是否结束;dualCameraSupported 和 singleCameraSupported 决定 UI 走双摄还是单摄;cameraStatusText 负责把关键问题显示给用户。

另一组 pendingCapture 字段用于拍照保存流程。失败释放时,如果这些字段不清,下一次拍照可能误以为上一轮还有未完成输出。状态字段越细,失败处理越能精确。

图 2 相机失败态相关状态字段与 pending 拍摄字段

ts 复制代码
@State private cameraPermissionReady: boolean = false;
  @State private cameraCapabilityChecked: boolean = false;
  @State private dualCameraSupported: boolean = false;
  @State private cameraStatusText: string = '拍照准备中';
  @State private cameraDeviceCount: number = 0;
  @State private cameraConcurrentProfileCount: number = 0;
  @State private cameraProbeResultText: string = '拍照能力检测完成';
  @State private singleCameraSupported: boolean = false;
  @State private selectedCaptureMode: CaptureMode = 'dual';
  @State private singleCameraRole: CameraLensRole = 'back';
  @State private singlePreviewLive: boolean = false;
  @State private backPreviewLive: boolean = false;
  @State private frontPreviewLive: boolean = false;
  @State private cameraFlashAvailable: boolean = false;
  @State private cameraFlashMode: camera.FlashMode = camera.FlashMode.FLASH_MODE_CLOSE;
  @State private cameraZoomMin: number = 1;
  @State private cameraZoomMax: number = 1;
  @State private cameraZoomCurrent: number = 1;
  @State private cameraZoomReady: boolean = false;
  @State private backLensChoiceKey: string = '';
  @State private captureBusy: boolean = false;
  private pendingCaptureId: string = '';
  @State private pendingCaptureMode: 'dual' | 'single' | 'sequence' = 'dual';
  @State private pendingSingleCaptureRole: CameraLensRole = 'back';
  private pendingBackCapturePath: string = '';
  private pendingFrontCapturePath: string = '';
  @State private backCaptureDelivered: boolean = false;
  @State private frontCaptureDelivered: boolean = false;
  private pendingCaptureLatitude: number = 0;
  private pendingCaptureLongitude: number = 0;
  private pendingCapturePlace: string = '';
  private pendingCaptureTitle: string = '';
  private pendingWatermarkStyle: WatermarkStyleKey = 'none';

三、初始化失败:不要继续创建会话

prepareCameraCapability 里有几个关键 return。权限没通过,设置 cameraCapabilityChecked 并返回;设备列表为空,结束探测并返回;只有单摄设备时,不再探测双摄,而是进入单摄预览;CameraKit 初始化抛异常时,把 dualCameraSupported 和 singleCameraSupported 都置为 false,并写入状态文本。

这些分支的共同点是"不让失败继续扩大"。只要前置条件不满足,就不要创建 input、output 和 session。这样失败只停留在入口层,用户可以处理权限或换设备,页面也可以重新触发 prepare。

图 3 CameraKit 初始化中的失败分支与可重试状态

ts 复制代码
this.cameraStatusText = '正在申请相机权限...';
    try {
      this.cameraPermissionReady = await this.requestCameraPermission();
      console.info(`[superImage][capture] camera permission ready=${this.cameraPermissionReady}`);
      if (!this.cameraPermissionReady) {
        this.cameraCapabilityChecked = true;
        if (this.cameraStatusText.length === 0) {
          this.cameraStatusText = '请允许相机权限后重试';
        }
        return;
      }

      this.cameraStatusText = '正在初始化 CameraKit...';
      const hostContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
      const cameraManager: camera.CameraManager = camera.getCameraManager(hostContext);
      const cameras: Array<camera.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();
      }
    } catch (error) {
      const err = error as BusinessError;
      this.cameraCapabilityChecked = true;
      this.dualCameraSupported = false;
      this.singleCameraSupported = false;
      this.singlePreviewLive = false;
      this.cameraStatusText = `CameraKit 初始化失败 ${err.code}`;
    } finally {

四、释放资源:先断开引用,再逐层 release

teardownDualPreview 是失败态的核心收口。它先把当前 session、output、input 保存到局部变量,再把组件字段置空。这样做可以让 UI 状态立即回到"没有活动会话",同时保留旧资源的引用用于后续 release。

随后依次释放 PhotoSession、PreviewOutput、PhotoOutput,最后关闭 CameraInput。这个顺序符合资源依赖:会话先停,输出再放,输入最后关。resetCaptureState 为 true 时,还会清空 pendingCapture、缩略图和水印状态,避免一次失败污染下一次拍摄。

图 4 teardownDualPreview 统一释放相机会话、输出和输入

ts 复制代码
private async teardownDualPreview(resetCaptureState: boolean = true): Promise<void> {
    this.logCaptureTrace('teardown-preview-start', `reset=${resetCaptureState}`);
    this.clearDualPreviewWatchdog();
    const backPhotoSession = this.backPhotoSession;
    const frontPhotoSession = this.frontPhotoSession;
    const singlePhotoSession = this.singlePhotoSession;
    const backPreviewOutput = this.backPreviewOutput;
    const frontPreviewOutput = this.frontPreviewOutput;
    const singlePreviewOutput = this.singlePreviewOutput;
    const backPhotoOutput = this.backPhotoOutput;
    const frontPhotoOutput = this.frontPhotoOutput;
    const singlePhotoOutput = this.singlePhotoOutput;
    const backCameraInput = this.backCameraInput;
    const frontCameraInput = this.frontCameraInput;
    const singleCameraInput = this.singleCameraInput;

    this.backPhotoSession = undefined;
    this.frontPhotoSession = undefined;
    this.singlePhotoSession = undefined;
    this.backPreviewOutput = undefined;
    this.frontPreviewOutput = undefined;
    this.singlePreviewOutput = undefined;
    this.backPhotoOutput = undefined;
    this.frontPhotoOutput = undefined;
    this.singlePhotoOutput = undefined;
    this.backCameraInput = undefined;
    this.frontCameraInput = undefined;
    this.singleCameraInput = undefined;
    this.cameraSessionActive = false;
    this.cameraSessionPreparing = false;
    this.singlePreviewLive = false;
    this.backPreviewLive = false;
    this.frontPreviewLive = false;
    this.resetCameraZoomState();
    this.cameraFlashAvailable = false;
    this.cameraFlashMode = camera.FlashMode.FLASH_MODE_CLOSE;
    this.cameraFlashSupportedModes = [];
    if (resetCaptureState) {
      this.captureBusy = false;
      this.sequentialCaptureQueued = false;
      this.markAllPhotoOutputsReady();
      this.pendingCaptureMode = 'dual';
      this.pendingSingleCaptureRole = 'back';
      this.backCaptureDelivered = false;
      this.frontCaptureDelivered = false;
      this.pendingCaptureId = '';
      this.pendingBackCapturePath = '';
      this.pendingFrontCapturePath = '';
      this.pendingCaptureLatitude = 0;
      this.pendingCaptureLongitude = 0;
      this.pendingCapturePlace = '';
      this.pendingCaptureTitle = '';
      this.clearPendingWatermark();
      this.cameraSequentialThumbnailUri = '';
      this.cameraSequentialThumbnailLabel = '';
    }

    await this.releasePhotoSession(backPhotoSession);
    await this.releasePhotoSession(frontPhotoSession);
    await this.releasePhotoSession(singlePhotoSession);
    await this.releasePreviewOutput(backPreviewOutput);
    await this.releasePreviewOutput(frontPreviewOutput);
    await this.releasePreviewOutput(singlePreviewOutput);
    await this.releasePhotoOutput(backPhotoOutput);
    await this.releasePhotoOutput(frontPhotoOutput);
    await this.releasePhotoOutput(singlePhotoOutput);
    await this.closeCameraInput(backCameraInput);
    await this.closeCameraInput(frontCameraInput);
    await this.closeCameraInput(singleCameraInput);
    this.logCaptureTrace('teardown-preview-finished', `reset=${resetCaptureState}`);

五、用户看到的应该是路径,而不是异常

失败态最终要落到用户体验。权限失败时提示允许权限;Surface 未就绪时提示正在准备画面并稍后重试;设备为空时不进入会话;预览失败时释放并允许重新进入。开发时可以看日志,用户不能看日志,所以 cameraStatusText 和按钮可用性必须替用户说明下一步。

这篇完成后,day06 的相机基础链路就闭合了:第 24 篇做入口探测,第 25 篇接住预览,第 26 篇建立单摄会话,第 27 篇控制镜头和参数,第 28 篇处理失败与释放。后面继续做双摄拍照、图库保存和端云同步时,这条链路会承担稳定底座。

本篇检查清单

  • 失败态按权限、设备、会话、资源释放拆分,而不是只写一个 catch。
  • 初始化失败时不会继续创建 PhotoSession。
  • teardownDualPreview 先清字段,再按 session、output、input 顺序释放旧资源。
  • pendingCapture 状态在重置时被统一清理。
  • 正文配图包含失败态运行目标、状态字段、初始化失败分支和资源释放源码。

今日练习

  • 拒绝 CAMERA 权限后进入相机页,确认页面可见提示而不是黑屏。
  • 切换页面或旋转窗口后检查 Surface 销毁是否触发 teardown。
  • 在 ensureSinglePreview 的 catch 中加入一次日志,确认失败后可以再次进入相机页。
相关推荐
不羁的木木2 小时前
Form Kit(卡片开发服务)学习笔记05-进阶实战与性能优化
笔记·学习·harmonyos
G_dou_2 小时前
# Flutter+OpenHarmony 实战:note_app 笔记应用
flutter·harmonyos
想你依然心痛2 小时前
HarmonyOS 6(API 23)智能体驱动的沉浸式AR脑机接口神经调控中心
华为·ar·harmonyos·智能体
技术路线图2 小时前
鸿蒙系统小红书内存占用太大怎么办?(2026全面清理指南)
华为·harmonyos
Goway_Hui2 小时前
【鸿蒙原生应用开发--ArkUI--011】Flashcard-app 单词卡应用开发教程
华为·harmonyos
Swift社区3 小时前
鸿蒙游戏中的手势系统详解
游戏·华为·harmonyos
不羁的木木3 小时前
Form Kit(卡片开发服务)学习笔记02-环境搭建与基础配置
笔记·学习·harmonyos
G_dou_3 小时前
# Flutter+OpenHarmony 实战:ToDo待办清单
flutter·harmonyos
不羁的木木3 小时前
Form Kit(卡片开发服务)学习笔记04-交互事件与跳转处理
笔记·学习·交互·harmonyos