HarmonyOS趣味相机实战第11篇:从单摄切换升级前后双摄同开、能力探测与降级设计

HarmonyOS趣味相机实战第11篇:从单摄切换升级前后双摄同开、能力探测与降级设计

摘要

当前 D:/APP/1quweixiangji 趣味相机已经完成前后镜头发现、单镜头预览、镜头切换和 PhotoOutput 拍照,但它一次只维护一个 CameraInput、一个 PhotoSession 和一个 XComponent Surface。若要升级为"前后双摄同开",不能简单地连续调用两次 createCameraInput():设备可能没有前后两个逻辑摄像头,也可能不支持并发;支持并发的机型还需要使用并发能力结果提供的输出配置,并以受限能力模式打开输入。

本文以当前单摄代码为起点,给出一套 API 18+ 前后双摄升级设计。文章明确区分"项目已经实现"和"下一阶段设计":当前版本 1.0.2 仍是前后镜头切换,尚未上线双摄 UI 与双路保存;本文提供的是可以落地的架构、代码骨架、降级策略和真机验收清单,不把设计稿描述成现成功能。

工程背景与源码定位

文件 当前作用 双摄升级后的责任
entry/src/main/ets/service/CameraDeviceService.ets 发现前后镜头并选择单个 activePosition 增加前后设备组合和并发能力探测
entry/src/main/ets/service/CameraPreviewService.ets 维护单路 CameraInput、PhotoSession、PreviewOutput、PhotoOutput 拆分成双路显式资源状态与统一释放
entry/src/main/ets/pages/Index.ets 单 XComponent 预览、镜头切换和拍照按钮 增加主画面与画中画双 Surface 布局
entry/src/main/ets/model/DecorationModels.ets 照片、水印、识别目标模型 增加双路照片关联和合成记录模型
entry/src/main/module.json5 声明 ohos.permission.CAMERA 拍照双摄仍只需要相机权限
entry/src/test/LocalUnit.test.ets 水印快照测试 增加能力路由、降级和双路结果聚合测试

环境、版本与官方边界

项目 当前值 说明
工程路径 D:/APP/1quweixiangji 当前源码为单摄实现
应用版本 1.0.2 来自 AppScope/app.json5
target SDK 6.0.2(22) 高于前后双摄并发所需的 API 18 起点
应用模型 HarmonyOS Stage 模型 EntryAbility 加载 pages/Index
当前相机模式 单 CameraInput + 单 PhotoSession 前后镜头之间重建预览
升级目标 前后双摄同时预览与分别拍照 必须以真机并发能力为准
受限模式 CAMERA_LIMITED_CAPABILITY 并发模式不是完整单摄控制能力集合

华为 CameraKit 官方入口:

  • Camera Kit 简介:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/camera-overview
  • 相机权限:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/camera-preparation
  • CameraManager API:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-camera-cameramanager
  • CameraInput API:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-camera-camerainput

前后双摄同开从 API 18 起提供相关能力,但最终是否可用取决于当前设备返回的并发信息。即使应用 target SDK 满足要求,也不能跳过真机能力探测。

一、当前项目是怎样完成单摄切换的

当前设备服务记录:

ts 复制代码
export type CameraPositionKey = 'front' | 'back';

export interface CameraDeviceState {
  available: boolean;
  totalCount: number;
  hasFront: boolean;
  hasBack: boolean;
  activePosition: CameraPositionKey;
  message: string;
}

页面只维护一个当前方向:

ts 复制代码
@State activeCameraPosition: CameraPositionKey = 'back';
@State previewSurfaceId: string = '';

切换时重新发现设备并启动单路预览:

ts 复制代码
const nextPosition: CameraPositionKey = CameraDeviceService.nextPosition(
  this.activeCameraPosition,
  this.hasFrontCamera,
  this.hasBackCamera
);
this.refreshCameraDevices(context, nextPosition);

CameraPreviewService.startPreview() 开头先释放旧会话,再按方向取一个摄像头:

ts 复制代码
await CameraPreviewService.stopPreview();
const cameraPosition: camera.CameraPosition = position === 'front' ?
  camera.CameraPosition.CAMERA_POSITION_FRONT :
  camera.CameraPosition.CAMERA_POSITION_BACK;
const device: camera.CameraDevice = manager.getCameraDevice(
  cameraPosition,
  camera.CameraType.CAMERA_TYPE_DEFAULT
);

这是一套稳定的"二选一"架构,但不能直接承载双摄,因为服务内部资源都是单例字段。

二、双摄升级的第一条规则:先确认两个逻辑设备

不要只看 getSupportedCameras().length >= 2。设备列表里可能包含多个后置物理摄像头,不等于一定存在一前一后两个目标设备。

应按位置明确获取:

ts 复制代码
const manager: camera.CameraManager = camera.getCameraManager(context);
const frontDevice: camera.CameraDevice = manager.getCameraDevice(
  camera.CameraPosition.CAMERA_POSITION_FRONT,
  camera.CameraType.CAMERA_TYPE_DEFAULT
);
const backDevice: camera.CameraDevice = manager.getCameraDevice(
  camera.CameraPosition.CAMERA_POSITION_BACK,
  camera.CameraType.CAMERA_TYPE_DEFAULT
);

任意一个方向不可用,就不能进入前后并发路径。此时应回到项目已经稳定实现的单摄切换。

三、必须调用 getCameraConcurrentInfos

拿到两个设备后,继续查询:

ts 复制代码
const concurrentInfos: Array<camera.CameraConcurrentInfo> =
  manager.getCameraConcurrentInfos([frontDevice, backDevice]);

判断:

ts 复制代码
if (concurrentInfos.length === 0) {
  return {
    supported: false,
    reason: '当前设备不支持前后双摄同时预览'
  };
}

这个结果才是双摄入口的开关:

探测结果 产品行为
前置或后置缺失 保留单摄模式,不显示双摄入口
concurrentInfos 为空 提供顺序双拍或普通镜头切换
返回有效并发信息 继续匹配输出能力并建立双路会话
查询抛异常 记录错误,安全退回单摄

不能先以并发模式打开 CameraInput,再补查能力。能力查询必须在打开输入之前完成。

四、定义清晰的双摄状态模型

当前 CameraPreviewService 只有一套静态资源。升级时建议显式定义每一路:

ts 复制代码
export interface CameraStreamState {
  position: CameraPositionKey;
  surfaceId: string;
  device?: camera.CameraDevice;
  input?: camera.CameraInput;
  previewOutput?: camera.PreviewOutput;
  photoOutput?: camera.PhotoOutput;
  session?: camera.PhotoSession;
}

export interface DualCameraState {
  supported: boolean;
  front: CameraStreamState;
  back: CameraStreamState;
  concurrentInfos: Array<camera.CameraConcurrentInfo>;
  generation: number;
}

这样释放、重建、拍照和错误日志都能明确指出是哪一路,而不是依赖 activeCameraPosition 猜测。

五、使用受限能力模式打开两路输入

通过并发能力检查后,再创建输入:

ts 复制代码
const frontInput: camera.CameraInput = manager.createCameraInput(frontDevice);
const backInput: camera.CameraInput = manager.createCameraInput(backDevice);

await frontInput.open(camera.CameraConcurrentType.CAMERA_LIMITED_CAPABILITY);
await backInput.open(camera.CameraConcurrentType.CAMERA_LIMITED_CAPABILITY);

这里的 CAMERA_LIMITED_CAPABILITY 很重要。前后并发模式不是把两套完整单摄能力简单叠加,控制面和输出组合受到设备并发能力约束。

根据当前官方指南,并发受限模式应重点在以下基础控制范围内验证:

  • 闪光灯。
  • 曝光。
  • 变焦。
  • 曝光补偿。
  • 对焦。
  • 防抖。
  • 色彩空间。

即使某个控制在普通单摄会话可用,也不能默认它在并发会话同样可用。

六、输出 Profile 必须来自并发能力结果

当前单摄服务通过普通输出能力选择预览和拍照 Profile。双摄路径不能直接复用默认 Profile,因为并发模式下可用的分辨率、格式和输出组合可能更少。

正确原则:

  1. 从匹配设备组合的 CameraConcurrentInfo 中读取输出能力。
  2. 为前后两路分别选择真实存在的 PreviewProfile。
  3. 只有在并发结果支持时才创建 PhotoOutput。
  4. 不要写死两路都使用单摄最高分辨率。

可以先实现"预览优先"的最小闭环:

text 复制代码
探测并发能力
  -> 选择两路低风险 PreviewProfile
  -> 双路预览稳定
  -> 再增加前后 PhotoOutput
  -> 最后增加合成与装饰

先上双路最高画质并同时开启人体识别,容易在真机上同时遇到带宽、内存和功耗问题。

七、页面需要两个独立 Surface

当前页面只有:

ts 复制代码
private previewController: XComponentController = new XComponentController();
@State previewSurfaceId: string = '';

双摄需要两个控制器和 Surface ID:

ts 复制代码
private backPreviewController: XComponentController = new XComponentController();
private frontPreviewController: XComponentController = new XComponentController();

@State backSurfaceId: string = '';
@State frontSurfaceId: string = '';

布局可以先采用主画面加画中画:

ts 复制代码
Stack() {
  XComponent({
    id: 'backPreview',
    type: XComponentType.SURFACE,
    controller: this.backPreviewController
  })
  .width('100%')
  .height('100%')

  XComponent({
    id: 'frontPreview',
    type: XComponentType.SURFACE,
    controller: this.frontPreviewController
  })
  .width(112)
  .height(148)
  .position({ x: 188, y: 18 })
}

只有两个 Surface 都完成 onLoad 后,才进入双摄启动。任一路 Surface 销毁,都应停止双摄会话,不能让 CameraInput 继续向失效 Surface 输出。

八、双 Surface 就绪要有统一闸门

推荐方法:

ts 复制代码
private canStartDualPreview(): boolean {
  return this.cameraPermissionStatus === 'granted' &&
    this.backSurfaceId.length > 0 &&
    this.frontSurfaceId.length > 0 &&
    !this.isDualStarting;
}

每个 Surface onLoad 后都调用同一个尝试启动方法:

ts 复制代码
private tryStartDualPreview(): void {
  if (!this.canStartDualPreview()) {
    return;
  }
  this.startDualPreview();
}

这样不依赖前置或后置 Surface 谁先加载,也不会启动两次。

九、双路拍照不要假设一个回调返回两张图

如果目标是分别保存前后两张照片,建议每一路维护自己的 PhotoOutput 和回调:

ts 复制代码
export interface DualCaptureResult {
  frontPixelMap?: image.PixelMap;
  backPixelMap?: image.PixelMap;
  frontReceived: boolean;
  backReceived: boolean;
  startedAt: number;
}

拍照入口:

text 复制代码
记录 captureStartedAt
  -> 清空前后上一轮结果
  -> 触发 frontPhotoOutput.capture()
  -> 触发 backPhotoOutput.capture()
  -> 分别等待两个 photoAvailable
  -> 两路成功后进入合成
  -> 任一路失败则按产品策略降级

不要因为后置先返回,就把上一轮前置结果配对进本次。每一路回调都要用时间边界或 capture generation 判断归属。

十、合成照片应在应用层完成

如果产品需要一张"前后同框"图片,推荐先拿到两张独立照片,再在应用层做画中画或上下拼接。这样资源归属清楚,也便于选择模板。

业务模型可以记录:

ts 复制代码
export interface DualCapturedPhoto {
  id: string;
  createdAt: string;
  frontPhotoId: string;
  backPhotoId: string;
  layout: 'pictureInPicture' | 'verticalSplit' | 'horizontalSplit';
  watermark?: WatermarkSnapshot;
}

合成时的关键点:

  • 两个 PixelMap 的方向和镜像分别处理。
  • 前置通常需要按产品预期镜像,后置通常不镜像。
  • 先统一到目标画布尺寸,再计算裁剪区域。
  • 合成完成后释放前后原始 PixelMap。
  • 如果用户取消合成,仍要释放两路结果。

十一、不支持双摄时的三级降级

双摄不是所有机型的基础能力,产品必须把降级设计放在入口,而不是失败后只弹"拍摄失败"。

降级一:普通前后切换

直接复用项目当前 nextPosition() 和单路预览,是最稳定的兜底。

降级二:顺序双拍

用户先拍后置,应用自动切前置,再拍第二张,最后合成。它不是同时拍摄,UI 文案必须写"顺序双拍",不能误导为双摄同开。

降级三:单路拍照加历史图片

当前摄像头拍一张,另一块区域从本地相册选择历史照片。此路径不依赖双摄并发,但需要额外设计图库选择和隐私说明。

能力路由示例:

ts 复制代码
export type DualCameraMode = 'concurrent' | 'sequential' | 'single';

export function resolveDualCameraMode(
  hasFront: boolean,
  hasBack: boolean,
  concurrentCount: number
): DualCameraMode {
  if (hasFront && hasBack && concurrentCount > 0) {
    return 'concurrent';
  }
  if (hasFront && hasBack) {
    return 'sequential';
  }
  return 'single';
}

十二、权限与隐私控制

前后双摄拍照仍使用:

json5 复制代码
"name": "ohos.permission.CAMERA"

如果只拍照片,不应因为"双摄"额外申请麦克风权限。只有加入带声音的视频录制时才需要重新评估 ohos.permission.MICROPHONE

隐私说明需要明确:

  • 双摄模式会同时处理前后画面。
  • 是否保存两张原图,还是只保存合成图。
  • 人体关键点或分割轮廓是否落盘。
  • 用户关闭双摄后是否立即停止并释放两路资源。

当前项目不把人体目标持久化,升级时应延续最小留存原则。

十三、双路资源释放顺序

建议为每一路统一释放:

text 复制代码
取消回调和采样任务
  -> session.stop()
  -> session.release()
  -> photoOutput.release()
  -> previewOutput.release()
  -> cameraInput.close()

双摄总释放要做到:

  1. 先把静态引用或状态引用置空,防止新操作拿到旧对象。
  2. 前后两路分别 try/catch,某一路失败不阻止另一条释放。
  3. 页面退出、任一 Surface 销毁、权限撤销和模式降级都走同一入口。
  4. 迟到的前后 PixelMap 按 generation 判断,失效结果立即释放。

十四、错误码与恢复策略

双摄比单摄更容易遇到资源冲突。需要重点识别:

错误信号 含义方向 建议恢复
7400102 操作不允许 检查调用顺序、会话状态和能力边界
7400107 相机冲突 释放旧会话,确认没有其他应用占用
7400108 安全原因禁用相机 提示用户检查系统安全设置
7400201 相机服务致命错误 完整释放后重建,仍失败则退回单摄

不要在 concurrentInfos 为空时反复重试打开双摄。空结果是能力不支持,不是瞬时网络错误。

十五、建议补充的测试

纯函数测试

ts 复制代码
expect(resolveDualCameraMode(true, true, 1)).assertEqual('concurrent');
expect(resolveDualCameraMode(true, true, 0)).assertEqual('sequential');
expect(resolveDualCameraMode(false, true, 0)).assertEqual('single');

真机集成测试

场景 操作 验收点
支持并发机型 打开双摄入口 两路 Surface 都有稳定预览
不支持并发机型 打开双摄入口 自动进入顺序双拍,不报崩溃
缺少前置 启动应用 只显示单摄模式
连续双拍 连续执行 10 次 前后结果不会跨轮次配对
快速切后台 双摄预览时切后台 两路会话都释放,返回可重建
Surface 销毁 关闭画中画组件 双摄立即停止,不保留悬空输入
前后合成 保存画中画 方向、镜像、水印和裁剪正确
内存验证 连续拍摄并取消 PixelMap 不持续增长

十六、常见问题排查

现象 可能原因 排查方式
两个摄像头都存在却不能同开 设备组合不支持并发 getCameraConcurrentInfos()
打开第二路报冲突 未使用受限并发模式或旧会话未释放 查 open 模式与资源释放
双路预览一黑一亮 某个 Surface 未就绪或 Profile 不匹配 查两个 Surface ID 和并发输出能力
单摄 Profile 可用,双摄失败 错用了普通输出能力 从 CameraConcurrentInfo 选择 Profile
前置方向反了 没有单独处理镜像 前后 PixelMap 分别处理方向
两张照片错配 没有 capture generation 以同一轮次聚合前后回调
退出页面仍占相机 只释放一路资源 前后流统一 stop/release/close
不支持机型反复报错 把空并发信息当瞬时失败 直接进入顺序双拍降级
双摄卡顿严重 两路分辨率或识别负载过高 先降低预览 Profile,再逐步增加能力
闪光灯在双摄失效 并发受限控制面不同 重新探测目标模式支持
模拟器能跑真机失败 模拟器不能代表并发硬件能力 必须真机覆盖
UI 宣称同时拍但实际顺序拍 降级文案不诚实 明确标注"顺序双拍"

十七、上线前验收清单

  • 当前单摄实现与双摄升级边界已明确。
  • 前后设备使用位置 API 分别获取。
  • 打开输入前调用 getCameraConcurrentInfos()
  • 空并发结果直接进入降级,不盲目重试。
  • 两路输入使用 CAMERA_LIMITED_CAPABILITY 打开。
  • PreviewProfile 和 PhotoProfile 来自并发能力结果。
  • 页面拥有两个独立 XComponent Surface。
  • 两个 Surface 都就绪后只启动一次双摄。
  • 任一 Surface 销毁都会停止双摄。
  • 前后 PhotoOutput 回调按 capture generation 聚合。
  • 前置镜像和后置方向分别验证。
  • 合成取消、失败和成功路径都释放 PixelMap。
  • 不支持并发时提供顺序双拍或单摄模式。
  • 降级 UI 不把顺序拍摄描述为同时拍摄。
  • 拍照双摄不额外申请麦克风权限。
  • 双摄原图和合成图保存策略写入隐私说明。
  • 7400102740010774001087400201 有明确恢复路径。
  • 支持机型、不支持机型和单摄设备都完成真机回归。

总结

从单摄切换升级到前后双摄同开,真正的难点不是多放一个 XComponent,而是把能力探测、输出 Profile、两路资源所有权、异步回调配对和不支持设备降级设计清楚。当前 1quweixiangji 的单摄服务已经提供权限、设备发现、预览、拍照和释放底座;双摄升级应在此基础上新增并发能力路由,而不是破坏稳定的单摄主流程。

可复用的核心顺序是:先确认前后设备,再查询 getCameraConcurrentInfos(),根据并发结果选择输出能力,以 CAMERA_LIMITED_CAPABILITY 打开输入,双 Surface 稳定后再接双路拍照,最后做应用层合成。任何一步不满足,都应诚实退回顺序双拍或普通单摄。这样双摄能力才是可用的产品功能,而不是只在少数设备偶然运行的演示代码。

相关推荐
拥抱太阳06162 小时前
HarmonyOS 应用开发《掌上英语》第12篇:组件通信模式:从父子传参到跨层级状态共享
华为·harmonyos
最爱老式锅包肉2 小时前
HarmonyOS技术精讲-Camera Kit(相机服务)第9篇:自动对焦与手动调焦
数码相机·华为·harmonyos
2501_919749032 小时前
华为鸿蒙免费口算闯关APP—小羊口算
华为·harmonyos
春卷同学2 小时前
HarmonyOS掌上记账APP开发实践第14篇:@Extend 扩展函数 — 告别重复样式代码
华为·harmonyos
蓝速科技2 小时前
蓝速科技鸿蒙广告机替代安卓实测:信创系统下的性能与边界评估
android·科技·harmonyos
2301_768103493 小时前
HarmonyOS趣味相机实战第12篇:CameraKit错误分层、相机占用恢复与用户可重试状态机
harmonyos·arkts·稳定性·错误处理·camerakit
最爱老式锅包肉5 小时前
HarmonyOS技术精讲-Camera Kit(相机服务)第8篇:曝光控制深入解析
数码相机·华为·harmonyos
Helen_cai6 小时前
OpenHarmony 路由封装 RouterUtil 页面跳转统一管理(API Version23+)
运维·服务器·nginx·华为·harmonyos
爸爸6196 小时前
鸿蒙应用开发实战【08】— AppStorage 全局状态与跨页面通信
ubuntu·华为·harmonyos·鸿蒙系统