第35篇|双预览会话:后摄主画面和前摄小窗如何同时准备
第 35 篇拆双预览。能力探测通过以后,项目还要准备两个 Surface、两个 CameraInput、两个 PreviewOutput、两个 PhotoOutput 和两组回调。只有后摄主画面和前摄小窗都开始出帧,页面才能认为双预览可用。 本文是 21 天「智能相机开发实战」训练营中的一篇实操记录。所有代码片段都来自当前项目,配图围绕运行页面和源码关键路径展开,读完以后可以直
第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 状态共同确认
二、入口校验:Surface、设备、并发信息缺一不可
ensureDualPreview 先判断页面是否已有会话,再检查权限、前后摄设备、并发信息和 SurfaceId。任一条件不满足,立即切到顺序单摄预览。这样页面不会停在“正在打开双摄”却没有画面的状态。

图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
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 记录后摄或前摄是否已经开始出帧。只有 backPreviewLive 和 frontPreviewLive 都为 true,才清理 watchdog 并清空状态提示。

图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 状态,避免半成功。
- 双预览失败时进入顺序单摄路径,用户仍可完成作品。
今日练习
- 在代码中搜索
backPreviewSurfaceId和frontPreviewSurfaceId,确认它们何时写入。 - 真机打开拍照页,观察后摄和前摄 frameStart 日志是否都出现。
- 尝试遮挡或延迟一个 Surface,思考 watchdog 应该怎样提示用户。
下一篇会继续沿着同一条工程链路往下拆:先看用户能看到的效果,再回到源码确认状态、文件和服务边界是否闭合。
更多推荐



所有评论(0)