前一篇我们为视频流自建了 socket 通道,这一篇讲另一半:小内容走系统正规军。手机相册随手拍的一张图,两台设备顶端一碰,平板 OBS 的场景里就多一个图像源;反过来,平板录完的成片一碰发回手机。看起来是十行代码的事,实际一路撞了三个真 bug:一个 UDMF 类型体系的认知坑、一个进程生命周期的产品坑、还有一个色彩通道的老问题。下面把“碰一碰”从一个手势拆成一条可复制的工程路径。
一碰之后系统做了什么
10 月 3 日傍晚第一次双机端到端:手机相册里一张红底“福”字剪纸照片,两台设备顶部一贴,平板 OBS 日志依次滚出:
- KnockShare: record utd=general.jpeg
- KnockShare: image landed -> knock-images/knock-1-*.jpg, adding as source
- [obs] source 'image_source 1' (image_source) created
复制代码
画布上照片直接就位,来源面板挂上了 image_source 1。一碰之后自动缩放居中(fit-to-canvas)是顺手补的:不处理的话,手机拍的 4000×3000 原图会以 1:1 像素尺寸怼进 1920×1080 画布,只露一个角。用 obs_sceneitem_set_bounds(SCALE_INNER) 加居中,两行代码,观感差别很大。
机制拆解:应用只登记“愿意被碰”
碰一碰分享的链路可以拆成五步:两台设备顶部 NFC 区相贴 1~2 秒;系统识别接收方并拉起其前台应用的分享面板;发送方应用在 knockShare 回调里塞入 SharedData;接收方应用在 dataReceive 回调拿到 URI 和 utd 类型;最后拷贝进沙箱并按类型处置。
这里有三个关键认知:
其一,应用不实现“碰”,只登记“愿意被碰”。接收侧入口是窗口级的 on('dataReceive') 加一张接收能力表 RecvCapabilityRegistry,你声明自己收哪些 UDMF 类型(utd),系统才能在握手时把匹配内容路由给你。发送侧同理:应用在前台时用 arm('knockShare') 挂一个回调,系统真碰上了才调它。所以“碰一碰开播”要拆开看:碰是系统手势,应用提供的只是内容的收发两端。
其二,能力表是白名单,不是通配符。
其三,收发都要求前台。后台调 share() 是错误行为,接收方必须前台亮屏。这条限制表面是 API 约束,往下挖一层是鸿蒙对“用户可感知”的要求:一碰是用户主动发起的动作,内容必须落到用户看得见的窗口里。它直接决定了这套能力的产品形态,也埋了第三个坑的雷。相关能力在 API 12+ 可用,本机以 API 26 sysroot d.ts 核实;接收侧 PC/2in1 支持。
Bug 一:能力表少了 VIDEO,mp4 被路由给相册
第一版接收能力表声明的是 IMAGE + PLAIN_TEXT,收图片、收文本链接,看起来覆盖了需求。平板端还顺手做了反向链路:停止录制后 arm 成片分享,把 mp4 碰回手机。结果平板这边日志一切正常(KnockShareOut: shared),手机侧 OBS 却什么都没收到,mp4 被系统路由到了手机相册。
原因在能力表的语义:它不是“我能处理什么”的自述,而是系统路由的白名单。没声明 VIDEO 的应用,在系统眼里就没有收视频的资格,握手阶段直接出局,内容由系统兜底给相册。我们测试图片时一切正常(IMAGE 在表里),第一次测视频就翻车,典型的测试覆盖类型盲区。修复只需一行:能力表补 VIDEO。
这个坑的一般化结论是:声明式能力接口(UDMF、权限、syscap 都算),测试矩阵必须按“类型维度”穷举,而不是按“功能维度”顺手。功能上“收文件”测过了,类型上“收视频”一次都没测过,这就是 bug 的温床。
Bug 二:general.jpeg 不匹配 image,叶子类型的认知差
补了能力表,mp4 能进 OBS 了,又发现新问题:手机相册碰过来的图,文件落了盘,源却没建,静默得像没碰。查 isImage() 的实现,是很习惯的写法:
- // 错误示范
- return utd.indexOf('image') >= 0; // 以为 utd 都长 image/jpeg 这样
复制代码
直觉里 UTM 类型标识该是 MIME 风格 image/jpeg。真机日志打出的实际值是 general.jpeg。鸿蒙 UDMF 体系里大量内容类型走 general.* 命名空间,image.* 只是其中一族,而且相册传图用的是叶子类型而非父类。indexOf('image') 对 general.jpeg 恒为 -1,文件老老实实躺在沙箱里,没人认领。
修复是把匹配改成叶子名白名单(jpeg、jpg、png、heic、webp 等),并且把收到的 utd 原样打进日志。类型系统的坑,一半的解法在“先承认你不知道对面会来什么”。这和上一篇导播台的 msg 字段是同一个物种:接口文档没写、你凭常识补的字段或格式,就是 bug 的温床。两个案子最后都靠同一条纪律收服:边界处打原始数据。
Bug 三:红变蓝,色彩通道的老问题回家
福字照片第一次碰进画布时,红底变蓝紫。实测原图像素 (192,64,87),渲染成 (91,79,170),是 R/B 通道互换的典型指纹。这条线索一路往回捋,捋到了全系列最顽固的一块 GPU 路径上:
ffmpeg 解 JPEG 输出 BGRA,libobs 标记 GS_BGRA,Maleoon 拒绝 GL_BGRA 上传(第 13 篇的老朋友),OHOS 分支降级成 GL_RGBA,但只改了枚举、没翻字节,最终 R/B 静默互换。第 13 篇修相机源时立的规矩——“改纹理枚举必须同步改字节序”——在图像源这条此前没人走的路径上漏了。
ffmpeg 解出来的内存布局是 BGRA,你把上传枚举从 GL_BGRA 改成 GL_RGBA 却不翻转字节,驱动就按 RGBA 的读法去读 BGRA 的内存,红蓝互换,且全程零报错。修复在 graphics-ffmpeg.c 解码出口加 BGRA→RGBA 翻转,用 __OHOS__ 守卫,不碰其他平台。10 月 4 日晨复碰验证:渲染最红像素 (255,104,97),正了。
产品结论:谁当发起端,谁当接收端
反向链路(平板成片碰回手机 OBS)修完类型问题后仍然不通。这次不是代码的错:手机 OBS 收到内容时进程还活着,碰后十秒左右被系统以 signal:9 冻结回收。它没有申请长时任务,而一个“刚接收完文件、前台随时会被划走”的应用,在手机形态上就是保不住的。
这个现象把整套碰一碰工作流的正确形态逼了出来:发起端放系统应用(如相册),接收端放稳定前台(如平板 OBS)。这恰好也是官方最佳实践主推的方向。区别在于,这个结论我们是拿 signal:9 的尸体换的,不是读文档读来的。文档告诉你规则,真机告诉你规则的边界为什么长那样。
顺带交代物理姿势:两台设备顶部的 NFC 区相贴 1~2 秒,不是屏幕互蹭;接收方必须前台亮屏。
彩蛋:PLAIN_TEXT 当信令,给无线机位当扳机
能力表里的 PLAIN_TEXT 还能干更轻的事:当控制信令用。手机碰平板时不传任何文件,只传一段文本 obs-knock://camera。平板 OBS 识别到这个前缀,不建任何素材源,而是自动添加“无线机位”接收源(第 14 篇的 harmony_remote_camera)。这就是“碰一碰开播机位”的完整体验:手机往平板上一贴,平板画布空出一个机位坑,手机角色页点“开始机位”,画面进来。软总线传 20 字节信令,TCP 走 2 Mbps 视频,控制面走系统通道,数据面走自建通道,各回各的位。
收尾
碰一碰这套东西,演示时最讨彩的永远是“啪”那一下,但把它写成工程,剩下的全是类型、路由、生命周期这些不性感的词。系统级协同能力的“优雅”,都建立在应用把自己该声明的声明清楚之上:能力表白名单、utd 叶子类型、前台生命周期,三样都对了,那一碰才真的“一碰就有”。 |