当前位置: 首页 > news >正文

第25篇|Surface 预览控制:ArkUI 页面如何接住相机画面

相机预览不是把 CameraKit 打开就能显示出来。ArkUI 页面需要先提供 XComponent 的 surfaceId,CameraKit 才能创建 PreviewOutput,把预览帧写进页面。

学习目标

  • 理解 XComponent、surfaceId、PreviewOutput 的关系。
  • 能解释为什么 Surface 创建后要延迟准备相机能力。
  • 掌握 Surface 销毁时释放预览资源的原因。
  • 知道 frameStart 为什么是判断预览真正连上的关键事件。

一、预览画面来自哪条链路

很多人做相机页时会把注意力放在 CameraManager 和 PhotoSession 上,但真正在屏幕上显示画面的入口是 Surface。ArkUI 侧 XComponent 创建出 surfaceId,项目把这个 id 传给 CameraKit 的 createPreviewOutput,CameraKit 后续输出的预览帧才有落点。

所以第 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,而不是马上 open

aboutToAppear 中,项目给 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 并监听 frameStart

this.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 回调中记录第一次帧到达时间,作为预览启动耗时指标。
http://www.gsyq.cn/news/1429075.html

相关文章:

  • APP攻防-资产收集篇反代理反证书反模拟器MsgiskLSP模块系统证书
  • 猫抓Cat-Catch:浏览器视频下载神器,一键嗅探网页媒体资源完整指南
  • 解锁小说离线阅读新可能:novel-downloader重新定义数字阅读体验
  • 如何用SMUDebugTool解锁AMD Ryzen处理器的终极性能:完全指南
  • 别再死记硬背了!用Kettle+MySQL手把手还原一个‘客户忠诚度分级’复杂存储过程
  • COM3D2.MaidFiddler:如何用实时编辑器快速修改COM3D2女仆属性
  • 横向辅助驾驶及人机共驾控制策略优化【附仿真】
  • 终极指南:使用msoffcrypto-tool轻松解锁加密Office文档
  • 5分钟搞定200+小说网站:novel-downloader离线阅读终极指南
  • 5步实现加密音频格式转换:开源工具深度解析与应用指南
  • UniApp + Painter实战:从‘社交裂变’到‘数据报告’,解锁小程序图片生成的3个高级应用场景
  • HS2-HF Patch终极指南:如何轻松优化你的Honey Select 2游戏体验
  • 基于SCARA机械臂的DIY写字钟:从运动学算法到嵌入式实现
  • 基于Arduino与游戏手柄的机器人手臂糖果分发系统设计与实现
  • 2026石家庄手表回收真实成交 全套附件价更高 - 薛定谔的梨花猫
  • 专业级直播间数据抓取工具:Live Room Watcher 完整实战指南
  • 机器人基础模型:从预训练到部署的技术演进与应用挑战
  • 基于Arduino与PID控制的自平衡机器人设计与实现
  • 告别‘天书’公式:用动画和Tanner图轻松理解LDPC码的译码原理
  • TinkerCAD仿真入门:三按钮控制RGB LED混色电路设计与实践
  • 2026年上海家装十大品牌靠谱榜单,多维测评优选本地装企 - 商业新知
  • 告别闭集检测:用Open-Vocabulary Detection(OVD)让YOLO也能识别训练集外的物体
  • 算力拉满,GPU 却在摸鱼:深度学习里的访存瓶颈
  • 从RAII设计模式看C++11锁管理:手把手教你实现一个简易版的lock_guard
  • 全品类宠品售卖|活体猫狗、品牌粮品、用品玩具一站式配齐 - 余生黄金回收
  • 用Python的Pulp库搞定NDDF模型:一个环境经济学研究生的效率测算实战笔记
  • 2018技术趋势盘点:AI伦理、数据隐私与平台治理的反思与应对
  • beweb目录结构审视
  • Arduino节奏训练器:状态机与时间精度在嵌入式交互中的实践
  • 如何用AntiDupl.NET免费开源工具智能清理重复图片:完整指南