相机预览不是把 CameraKit 打开就能显示出来。ArkUI 页面需要先提供 XComponent 的 surfaceIdCameraKit 才能创建 PreviewOutput把预览帧写进页面。学习目标理解 XComponent、surfaceId、PreviewOutput 的关系。能解释为什么 Surface 创建后要延迟准备相机能力。掌握 Surface 销毁时释放预览资源的原因。知道 frameStart 为什么是判断预览真正连上的关键事件。一、预览画面来自哪条链路很多人做相机页时会把注意力放在 CameraManager 和 PhotoSession 上但真正在屏幕上显示画面的入口是 Surface。ArkUI 侧 XComponent 创建出 surfaceId项目把这个 id 传给 CameraKit 的 createPreviewOutputCameraKit 后续输出的预览帧才有落点。所以第 25 篇关注的不是“拍照”而是“页面如何接住预览”。只要 Surface 生命周期没处理好后面的权限和会话都可能是正确的用户看到的仍然是黑屏。图 1 相机页 Surface 到 PreviewOutput 的运行链路二、PreviewSurfaceController只做生命周期转发项目里没有把 Surface 创建后的业务逻辑直接塞进 XComponent而是做了一个 PreviewSurfaceController。它继承 XComponentController内部只保存 createHandler 和 destroyHandler。当 onSurfaceCreated 到来时把 surfaceId 转交给页面状态当 onSurfaceDestroyed 到来时通知页面清理资源。这个设计很小但很重要控制器不关心相机权限、不创建 CameraInput也不操作 PhotoSession。它只负责把 ArkUI 生命周期转换成页面能处理的事件。这样后续如果要换预览组件或抽出通用控制器也不会牵动 CameraKit 主流程。图 2 PreviewSurfaceController 只转发 Surface 创建和销毁事件class PreviewSurfaceController extends XComponentController { private createHandler?: (surfaceId: string) void; private destroyHandler?: (surfaceId: string) void; setCreateHandler(handler: (surfaceId: string) void): void { this.createHandler handler; } setDestroyHandler(handler: (surfaceId: string) void): void { this.destroyHandler handler; } onSurfaceCreated(surfaceId: string): void { this.createHandler?.(surfaceId); } onSurfaceDestroyed(surfaceId: string): void { this.destroyHandler?.(surfaceId); } }三、Surface 创建后为什么要 schedule而不是马上 openaboutToAppear 中项目给 backSurfaceController 和 frontSurfaceController 分别设置回调。后摄 Surface 创建后会保存 backSurfaceId并调用 scheduleCameraCapabilityPrepare(80)。这里不是随手加延迟而是给页面布局、Surface 绑定和权限状态一个稳定窗口。Surface 销毁时项目会清空对应 id并调用 teardownDualPreview。原因也直接CameraKit 的输出还绑定着旧 surfaceId如果页面切走、组件重建或多窗口尺寸变化时不释放下一次创建会话很容易复用到已经失效的输出。图 3 Surface 创建保存 id销毁时释放预览资源this.backSurfaceController.setCreateHandler((surfaceId: string) { this.backSurfaceId surfaceId; this.scheduleCameraCapabilityPrepare(80); }); this.backSurfaceController.setDestroyHandler(() { this.backSurfaceId ; void this.teardownDualPreview(!this.shouldPreserveSequentialCaptureContext()); }); this.frontSurfaceController.setCreateHandler((surfaceId: string) { this.frontSurfaceId surfaceId; void this.ensureCameraPreview(); }); this.frontSurfaceController.setDestroyHandler(() { this.frontSurfaceId ; void this.teardownDualPreview(!this.shouldPreserveSequentialCaptureContext()); });四、PreviewOutput把 profile 和 surfaceId 绑定起来当单摄预览真正启动时项目先通过 capability 找到 previewProfiles再调用 createPreviewOutput把 preview profile 与 backSurfaceId 组合起来。随后给 PreviewOutput 绑定 frameStart 回调。frameStart 出现说明预览帧已经开始到达页面这比“start 方法调用成功”更接近用户真正看到画面的时刻。这也是 UI 状态更新的依据。创建会话成功但 frameStart 没来时页面仍可提示“预览连接中”frameStart 到来后再把预览状态切到 live。这样用户不会被一个已经 start 但还没有画面的页面误导。图 4 PreviewOutput 绑定 surfaceId 并监听 frameStartthis.singleCameraInput this.cameraManager.createCameraInput(this.singleCameraDevice); await this.singleCameraInput.open(); this.singlePreviewOutput this.cameraManager.createPreviewOutput( capability.previewProfiles[0], this.backSurfaceId ); this.singlePreviewOutput.on(frameStart, () { this.handleSinglePreviewFrameStart(activeRole); }); this.singlePhotoOutput this.cameraManager.createPhotoOutput(this.pickBestPhotoProfile(capability.photoProfiles)); this.bindPhotoOutput(activeRole, this.singlePhotoOutput, single); this.singlePhotoSession this.cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession; this.singlePhotoSession.beginConfig(); this.singlePhotoSession.addInput(this.singleCameraInput); this.singlePhotoSession.addOutput(this.singlePreviewOutput); this.singlePhotoSession.addOutput(this.singlePhotoOutput); await this.singlePhotoSession.commitConfig(); await this.singlePhotoSession.start(); this.cameraSessionActive true; this.syncZoomStateFromSession(); this.refreshCameraFlashState();五、预览稳定性的三个实操判断第一看 surfaceId 是否为空。只要 backSurfaceId 为空就不要创建 PreviewOutput应该等待 Surface 创建回调。第二看 previewProfiles 是否为空。能力集没有预览规格时不要硬传空数组。第三看销毁路径是否完整。Surface 销毁后不释放输入、输出和会话会让下一次进入相机页变成随机失败。项目把这些判断放在 prepareCameraCapability、ensureSinglePreview 和 teardownDualPreview 三个位置入口负责等条件会话负责连接输出释放负责把旧资源断干净。写相机预览时能把这三层分清楚代码就不会到处补 if。本篇检查清单PreviewSurfaceController 中没有混入 CameraKit 业务逻辑。Surface 创建时保存 surfaceId并延迟触发能力准备。Surface 销毁时会清空 id并释放旧预览资源。PreviewOutput 使用真实 surfaceId 创建并监听 frameStart。正文配图包含运行链路图、Surface 控制器源码、Surface 回调源码和 PreviewOutput 源码。今日练习在 Surface 创建回调中打印 backSurfaceId 长度确认页面切换时会重新创建。把 schedule 延迟临时改成 0对比部分机型预览连接时序是否更容易不稳定。在 frameStart 回调中记录第一次帧到达时间作为预览启动耗时指标。
第25篇|Surface 预览控制:ArkUI 页面如何接住相机画面
相机预览不是把 CameraKit 打开就能显示出来。ArkUI 页面需要先提供 XComponent 的 surfaceIdCameraKit 才能创建 PreviewOutput把预览帧写进页面。学习目标理解 XComponent、surfaceId、PreviewOutput 的关系。能解释为什么 Surface 创建后要延迟准备相机能力。掌握 Surface 销毁时释放预览资源的原因。知道 frameStart 为什么是判断预览真正连上的关键事件。一、预览画面来自哪条链路很多人做相机页时会把注意力放在 CameraManager 和 PhotoSession 上但真正在屏幕上显示画面的入口是 Surface。ArkUI 侧 XComponent 创建出 surfaceId项目把这个 id 传给 CameraKit 的 createPreviewOutputCameraKit 后续输出的预览帧才有落点。所以第 25 篇关注的不是“拍照”而是“页面如何接住预览”。只要 Surface 生命周期没处理好后面的权限和会话都可能是正确的用户看到的仍然是黑屏。图 1 相机页 Surface 到 PreviewOutput 的运行链路二、PreviewSurfaceController只做生命周期转发项目里没有把 Surface 创建后的业务逻辑直接塞进 XComponent而是做了一个 PreviewSurfaceController。它继承 XComponentController内部只保存 createHandler 和 destroyHandler。当 onSurfaceCreated 到来时把 surfaceId 转交给页面状态当 onSurfaceDestroyed 到来时通知页面清理资源。这个设计很小但很重要控制器不关心相机权限、不创建 CameraInput也不操作 PhotoSession。它只负责把 ArkUI 生命周期转换成页面能处理的事件。这样后续如果要换预览组件或抽出通用控制器也不会牵动 CameraKit 主流程。图 2 PreviewSurfaceController 只转发 Surface 创建和销毁事件class PreviewSurfaceController extends XComponentController { private createHandler?: (surfaceId: string) void; private destroyHandler?: (surfaceId: string) void; setCreateHandler(handler: (surfaceId: string) void): void { this.createHandler handler; } setDestroyHandler(handler: (surfaceId: string) void): void { this.destroyHandler handler; } onSurfaceCreated(surfaceId: string): void { this.createHandler?.(surfaceId); } onSurfaceDestroyed(surfaceId: string): void { this.destroyHandler?.(surfaceId); } }三、Surface 创建后为什么要 schedule而不是马上 openaboutToAppear 中项目给 backSurfaceController 和 frontSurfaceController 分别设置回调。后摄 Surface 创建后会保存 backSurfaceId并调用 scheduleCameraCapabilityPrepare(80)。这里不是随手加延迟而是给页面布局、Surface 绑定和权限状态一个稳定窗口。Surface 销毁时项目会清空对应 id并调用 teardownDualPreview。原因也直接CameraKit 的输出还绑定着旧 surfaceId如果页面切走、组件重建或多窗口尺寸变化时不释放下一次创建会话很容易复用到已经失效的输出。图 3 Surface 创建保存 id销毁时释放预览资源this.backSurfaceController.setCreateHandler((surfaceId: string) { this.backSurfaceId surfaceId; this.scheduleCameraCapabilityPrepare(80); }); this.backSurfaceController.setDestroyHandler(() { this.backSurfaceId ; void this.teardownDualPreview(!this.shouldPreserveSequentialCaptureContext()); }); this.frontSurfaceController.setCreateHandler((surfaceId: string) { this.frontSurfaceId surfaceId; void this.ensureCameraPreview(); }); this.frontSurfaceController.setDestroyHandler(() { this.frontSurfaceId ; void this.teardownDualPreview(!this.shouldPreserveSequentialCaptureContext()); });四、PreviewOutput把 profile 和 surfaceId 绑定起来当单摄预览真正启动时项目先通过 capability 找到 previewProfiles再调用 createPreviewOutput把 preview profile 与 backSurfaceId 组合起来。随后给 PreviewOutput 绑定 frameStart 回调。frameStart 出现说明预览帧已经开始到达页面这比“start 方法调用成功”更接近用户真正看到画面的时刻。这也是 UI 状态更新的依据。创建会话成功但 frameStart 没来时页面仍可提示“预览连接中”frameStart 到来后再把预览状态切到 live。这样用户不会被一个已经 start 但还没有画面的页面误导。图 4 PreviewOutput 绑定 surfaceId 并监听 frameStartthis.singleCameraInput this.cameraManager.createCameraInput(this.singleCameraDevice); await this.singleCameraInput.open(); this.singlePreviewOutput this.cameraManager.createPreviewOutput( capability.previewProfiles[0], this.backSurfaceId ); this.singlePreviewOutput.on(frameStart, () { this.handleSinglePreviewFrameStart(activeRole); }); this.singlePhotoOutput this.cameraManager.createPhotoOutput(this.pickBestPhotoProfile(capability.photoProfiles)); this.bindPhotoOutput(activeRole, this.singlePhotoOutput, single); this.singlePhotoSession this.cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession; this.singlePhotoSession.beginConfig(); this.singlePhotoSession.addInput(this.singleCameraInput); this.singlePhotoSession.addOutput(this.singlePreviewOutput); this.singlePhotoSession.addOutput(this.singlePhotoOutput); await this.singlePhotoSession.commitConfig(); await this.singlePhotoSession.start(); this.cameraSessionActive true; this.syncZoomStateFromSession(); this.refreshCameraFlashState();五、预览稳定性的三个实操判断第一看 surfaceId 是否为空。只要 backSurfaceId 为空就不要创建 PreviewOutput应该等待 Surface 创建回调。第二看 previewProfiles 是否为空。能力集没有预览规格时不要硬传空数组。第三看销毁路径是否完整。Surface 销毁后不释放输入、输出和会话会让下一次进入相机页变成随机失败。项目把这些判断放在 prepareCameraCapability、ensureSinglePreview 和 teardownDualPreview 三个位置入口负责等条件会话负责连接输出释放负责把旧资源断干净。写相机预览时能把这三层分清楚代码就不会到处补 if。本篇检查清单PreviewSurfaceController 中没有混入 CameraKit 业务逻辑。Surface 创建时保存 surfaceId并延迟触发能力准备。Surface 销毁时会清空 id并释放旧预览资源。PreviewOutput 使用真实 surfaceId 创建并监听 frameStart。正文配图包含运行链路图、Surface 控制器源码、Surface 回调源码和 PreviewOutput 源码。今日练习在 Surface 创建回调中打印 backSurfaceId 长度确认页面切换时会重新创建。把 schedule 延迟临时改成 0对比部分机型预览连接时序是否更容易不稳定。在 frameStart 回调中记录第一次帧到达时间作为预览启动耗时指标。