第35篇|双预览会话:后摄主画面和前摄小窗如何同时准备

第 35 篇拆双预览。能力探测通过以后,项目还要准备两个 Surface、两个 CameraInput、两个 PreviewOutput、两个 PhotoOutput 和两组回调。只有后摄主画面和前摄小窗都开始出帧,页面才能认为双预览可用。

本文是 21 天「智能相机开发实战」训练营中的一篇实操记录。所有代码片段都来自当前项目,配图围绕运行页面和源码关键路径展开,读完以后可以直接回到工程里按函数名定位。

本篇目标

  • 理解双预览比单预览多出的 Surface 和输出对象。
  • 读懂 ensureDualPreview 的校验顺序。
  • 知道为什么 CameraInput 要以 CAMERA_LIMITED_CAPABILITY 打开。
  • 用 frameStart 判断双路画面是否真正开始工作。

代码位置

  • entry/src/main/ets/pages/Index.ets

一、双预览不是两个组件摆在页面上

运行效果里,后摄是主画面,前摄是小窗。工程里必须分别拿到后摄和前摄的 SurfaceId,并确认并发信息已经存在。两个预览视图只是最终承载,真正决定是否可用的是 CameraKit 会话是否成功启动并持续出帧。

图1 双预览由两个 Surface、两路输出和 frameStart 状态共同确认

图1 双预览由两个 Surface、两路输出和 frameStart 状态共同确认

二、入口校验:Surface、设备、并发信息缺一不可

ensureDualPreview 先判断页面是否已有会话,再检查权限、前后摄设备、并发信息和 SurfaceId。任一条件不满足,立即切到顺序单摄预览。这样页面不会停在“正在打开双摄”却没有画面的状态。

图2 ensureDualPreview 校验双摄预览所需的关键条件

图2 ensureDualPreview 校验双摄预览所需的关键条件

  private async ensureDualPreview(): Promise<void> {
    if (this.cameraSessionPreparing || this.cameraSessionActive) {
      return;
    }
    if (this.activeTab !== 'camera' || !this.cameraPermissionReady || !this.dualCameraSupported) {
      return;
    }
    if (!this.cameraManager || !this.backCameraDevice || !this.frontCameraDevice) {
      return;
    }
    if (this.backSurfaceId.length === 0 || this.frontSurfaceId.length === 0) {
      return;
    }

    const backCapability = this.getConcurrentPhotoCapability(camera.CameraPosition.CAMERA_POSITION_BACK);
    const frontCapability = this.getConcurrentPhotoCapability(camera.CameraPosition.CAMERA_POSITION_FRONT);
    if (!backCapability || !frontCapability) {
      await this.fallbackToSequentialSinglePreview('当前设备双摄配置不可用,已切换为顺序双拍');
      return;
    }
    if (backCapability.previewProfiles.length === 0 || frontCapability.previewProfiles.length === 0 ||
      backCapability.photoProfiles.length === 0 || frontCapability.photoProfiles.length === 0) {
      await this.fallbackToSequentialSinglePreview('当前设备双摄配置不可用,已切换为顺序双拍');
      return;
    }

这里的校验顺序很适合排查黑屏:先看 SurfaceId,再看设备,再看并发 profile,最后看会话启动。

三、输出创建:两路 PreviewOutput 和两路 PhotoOutput

双摄预览会分别创建后摄和前摄的 CameraInput,并使用 CAMERA_LIMITED_CAPABILITY 打开,这是并发模式下对能力范围的约束。随后项目创建两路预览输出和拍照输出,并把后续拍照回调绑定到对应 role。

图3 双摄预览中创建两路 PreviewOutput 与 PhotoOutput

图3 双摄预览中创建两路 PreviewOutput 与 PhotoOutput

      this.frontCameraInput = this.cameraManager.createCameraInput(this.frontCameraDevice);
      await this.backCameraInput.open(camera.CameraConcurrentType.CAMERA_LIMITED_CAPABILITY);
      await this.frontCameraInput.open(camera.CameraConcurrentType.CAMERA_LIMITED_CAPABILITY);

      this.backPreviewOutput = this.cameraManager.createPreviewOutput(
        backCapability.previewProfiles[0],
        this.backSurfaceId
      );
      this.frontPreviewOutput = this.cameraManager.createPreviewOutput(
        frontCapability.previewProfiles[0],
        this.frontSurfaceId
      );

      this.backPreviewOutput.on('frameStart', () => {
        this.handlePreviewFrameStart('back');
      });
      this.frontPreviewOutput.on('frameStart', () => {
        this.handlePreviewFrameStart('front');
      });

      this.backPhotoOutput = this.cameraManager.createPhotoOutput(this.pickBestPhotoProfile(backCapability.photoProfiles));
      this.frontPhotoOutput = this.cameraManager.createPhotoOutput(this.pickBestPhotoProfile(frontCapability.photoProfiles));
      this.bindPhotoOutput('back', this.backPhotoOutput, 'dualBack');
      this.bindPhotoOutput('front', this.frontPhotoOutput, 'dualFront');

      this.backPhotoSession = this.cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession;
      this.backPhotoSession.beginConfig();
      this.backPhotoSession.addInput(this.backCameraInput);

两个 PhotoOutput 后面会分别交付后摄图和前摄图。现在把 role 绑定清楚,后续保存路径、合成和入库才不会混。

四、frameStart:两路都亮才算双预览完成

预览启动不是 start() 返回就结束。项目通过 handlePreviewFrameStart 记录后摄或前摄是否已经开始出帧。只有 backPreviewLivefrontPreviewLive 都为 true,才清理 watchdog 并清空状态提示。

图4 handlePreviewFrameStart 等待后摄和前摄两路画面都开始输出

图4 handlePreviewFrameStart 等待后摄和前摄两路画面都开始输出

  private handlePreviewFrameStart(role: 'back' | 'front'): void {
    if (role === 'back') {
      this.backPreviewLive = true;
      this.syncZoomStateFromSession();
    } else {
      this.frontPreviewLive = true;
    }

    if (this.backPreviewLive && this.frontPreviewLive) {
      this.clearDualPreviewWatchdog();
      this.cameraStatusText = '';
    } else if (this.backPreviewLive || this.frontPreviewLive) {
      this.cameraStatusText = '副图预览连接中...';
    }
  }

这个判断能避免“会话启动了但小窗没画面”的半成功状态。对于真机调试,frameStart 比单纯看 Promise 返回更有参考价值。

工程检查清单

  • 双预览必须分别维护后摄 SurfaceId 和前摄 SurfaceId。
  • 并发模式打开 CameraInput 时使用受限并发能力类型。
  • 预览输出和拍照输出都要按 role 绑定,避免路径错位。
  • 用 frameStart 更新 live 状态,避免半成功。
  • 双预览失败时进入顺序单摄路径,用户仍可完成作品。

今日练习

  1. 在代码中搜索 backPreviewSurfaceIdfrontPreviewSurfaceId,确认它们何时写入。
  2. 真机打开拍照页,观察后摄和前摄 frameStart 日志是否都出现。
  3. 尝试遮挡或延迟一个 Surface,思考 watchdog 应该怎样提示用户。

下一篇会继续沿着同一条工程链路往下拆:先看用户能看到的效果,再回到源码确认状态、文件和服务边界是否闭合。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐