查看: 99|回复: 3

鸿蒙OBS无线机位实践:Camera Kit零拷贝推流与同包分角色

[复制链接]
发表于 24 分钟前 | 显示全部楼层 |阅读模式
前十三篇解决的是“如何把桌面OBS搬到鸿蒙上”。从这篇开始,问题换了一个:搬上来之后,鸿蒙能给OBS哪些桌面给不了的能力?答案落在两台设备之间的关系上——手机不再是一个缩小版OBS,而是制作主机的镜头和导播台。下面这条链路全程用两部Mate真机验证,涉及同一个HAP分角色运行、自研RCTM帧协议、Camera Kit到AVCodec的零拷贝推流,以及手机把录制指令下发到平板、状态再回流到手机的过程。

一、同包分角色:一个HAP装出两个应用

两部设备安装同一个HAP,打开后却是两个完全不同的界面:

平板或2in1设备进入完整OBS控制台,场景、来源、混音器、推流按钮都在,来源列表里多出一个桌面版没有的“无线机位”选项;手机只显示“无线机位”角色页,填IP、点“开始机位”,后置摄像头画面就会出现在平板画布上。角色页下方还挂着“导播台”,连上平板后可以切换平板场景,录制和直播按钮跟随平板实况翻转。

架构上,ArkTS入口根据deviceInfo.deviceType分叉:手机分支直接return,RawFileExtractor、obs_startup、插件加载、混音轨一行都不执行;平板和2in1分支才启动libobs,并加载无线机位接收源。这样处理并非单纯为了省资源。真机验证过,手机端应用切到后台后,没有长时任务支撑,十几秒内就会被系统以signal:9回收。一个跑着libobs、攥着音视频采集的进程,在手机上天然活不长。再加上产品事实——做直播的人不会蹲在手机前剪多机位,制作主机需要大屏、续航和前台稳定性,手机的真实角色就是镜头和遥控器。

第一次验证分叉时,手机直接落进角色页,hilog里没有一行libobs日志。一个包里装着两个应用,靠设备形态各就各位。

二、Service Collaboration远程相机:一条被文档判死刑的捷径

做“手机相机进平板OBS”时,官方看起来留了一条直路:跨端协同服务,文档标题写着“PC/2in1跨端调用Phone相机”。第一版方案就是在相机插件里埋device_id="@remote"哨兵,枚举相机时优先挑CAMERA_CONNECTION_REMOTE类型,碰一碰信令obs-knock://camera一到就自动建远程相机源。

真机先把这条路堵死:两台同账号设备在超级终端里协同状态正常,但OBS的相机枚举里永远没有REMOTE条目。翻文档才看清,CAMERA_CONNECTION_REMOTE设备只有系统级应用挂得上,三方应用枚举不到。

退一步用协同服务对外接口createCollaborationServiceMenuItems也不行。逐字读完正文会发现,它的语义是拍照:拉起对端相机、拍一张、把照片ArrayBuffer回传。onState回调一次回一张静态图,没有连续帧流;旧的createCollaborationCameraMenuItems已标记deprecated。拍照语义不等于连续流,系统能力也不等于三方可用,@remote哨兵方案整体退役。

这个判定值得单独记住:官方文档里“支持跨端调用Phone相机”和“三方应用能把手机相机做成连续视频源”之间,隔着整整一个实现语义的距离。

三、自研RCTM:20字节帧头管够

排除系统捷径后,问题回到最朴素的状态:手机把H.264码流通过局域网TCP推给平板,平板边收边解边画。设备之间没有现成流协议,就自己定一个20字节帧头:
  1. magic   u32  'RCTM'
  2. flags   u32  bit0=关键帧提示 bit1=配置帧(SPS/PPS)提示
  3. pts     i64  采集时间戳(CLOCK_MONOTONIC 纳秒)
  4. size    u32  负载长度
  5. payload      Annex-B H.264 访问单元
复制代码

设计时纠结过是否把SPS/PPS外置成独立配置帧、是否兼容RTP、是否预留音频帧位。最后取舍服从一个原则:协议是给自己两端读的,可读可查可改即可;但字节序必须提前讲清楚,全用big-endian,时间戳必须带上,将来对音频、对多机位都往这个头里加flag,不用再动结构。

还有一个不显然的决定:大流量走socket,信令走软总线。碰一碰的协同文本通道很优雅,但拿它传视频是灾难,base64一进一出膨胀33%,吞吐也顶不住2Mbps的实时码流。所以软总线只负责“碰一下,开始传”这类控制语义,字节的洪水一律走TCP。

四、发送端:让相机直接长在编码器上

手机侧第一反应是复用平板已验证的相机采集插件:Camera Kit进ImageReceiver读回NV12,CPU转RGBA,再进libobs,最后接编码器。这个方案没有尝试就被否决了——它意味着每一帧都要GPU buffer回读CPU、转格式、再拷贝给编码器。手机只做采集、编码、发走三件事,中间任何一次不必要的CPU拷贝都在增加发热和丢帧概率。

HarmonyOS的AVCodec给了第二条路:surface输入模式的编码器自带一个可写入的NativeWindow,相机VideoOutput直接渲染到这个窗口上,帧就长在编码器输入队列里。发送链路可以概括为:Camera Kit的NORMAL_VIDEO会话通过surface直连OH_VideoEncoder的H.264输入,编码输出回调拿到Annex-B访问单元后进入深度30的内存队列,满则丢旧,再由专职socket线程加上RCTM帧头发出。

这里有个NDK接口细节:OH_VideoEncoder_GetSurface返回的是OHNativeWindow*,而相机CreateVideoOutput要的是字符串形式的surfaceId,中间需要用OH_NativeWindow_GetSurfaceId换算一次。这个“窗口到surfaceId字符串”的翻译层,是鸿蒙把图形生产者和消费者解耦后的粘合缝,第一次遇到容易愣住。

另一个关键约束:编码器输出回调里绝对不能碰socket。onNewOutputBuffer跑在codec工作线程上,网络一卡,整个编码器就停了。回调只做一件事——把码流字节拷进深度30的队列,通知专职发送线程;队列满就丢最旧的。直播语义下,新鲜画面永远比完整录像重要。

手机侧代码刻意不依赖libobs,整个是纯NDK加POSIX socket,编在napi桥模块里,由ArkTS角色页用三个函数唤起:startCameraSender(host, port, bitrate)、stopCameraSender()、cameraSenderStatus()。

五、接收端:Maleoon硬解、几何协商与那个熟悉的RGBA

平板侧是一个正经OBS插件harmony-remotecam,注册harmony_remote_camera异步视频源。它的世界分成三个线程,交接全用队列:accept线程监听TCP:25890,解析RCTM帧后进入深度4的pending队列,满则丢旧;解码器需要输入时从队列取;OH_VideoDecoder硬解输出NV12后,在回调里CPU转RGBA,再交给obs_source_output_video。

眼熟的读者会问:为什么又是NV12到RGBA的CPU转换?这是第13篇相机源的老毛病。Maleoon驱动拒绝libobs多平面路径的GL_R8加GL_RG16纹理上传,NV12直接交给libobs会整帧黑。同一条教训第二次生效:这块GPU上,packed RGBA是唯一见过光的色彩路径。连转换用的BT.709系数矩阵(298/459/55/137/541那组乘256整数)都是从相机插件原样搬来的。工程做到后面,复用不是优雅,是纪律。

解码器这边有个AVCodec特性很贴心:onStreamChanged回调会把真实几何随流带出来,包括宽高、stride和slice_height。发送端编码1920x1080时,stride可能比宽度大、slice_height比高度大,这是硬件对齐的结果。按几何画布盲猜偏移必然花屏,用回调里的权威值一次就对。这个回调签名是(codec, format, userData),比另外两个少一个index参数,写的时候编译器会提醒一遍。

验收策略也值得一提:接收端不依赖发送端就能单独验证。写了一个200行的mac脚本rctm-test-sender.py,用ffmpeg造testsrc画面,按起始码切NAL,打上RCTM帧头往平板TCP一推。解码渲染链路的技术风险——Maleoon硬解能否打开、色彩是否正确、帧能否进画布——当天中午全部排掉,手机发送端晚上才上场。双端功能,先造假对手,把真手的风险拆单。

六、导播台:一个字母的bug与状态自己流回来

机位打通后,手机角色页顺势长出导播台。这次跨设备通道不再自建,视频洪水才需要socket,控制指令量太小,直接走应用协同连接abilityConnectionManager:手机作为source发起connect,平板OBS作为sink在onCollaborate里acceptConnect,之后双向只发小JSON。

链路一次就通,平板日志打出DirectorLink: accepted connect (sessionId=100)。但手机界面卡死在“连接中”,场景列表怎么都不出来。双向打日志排查,平板侧一行warn把真相原样打在脸上:receiveMessage without text payload,后面跟着{"sessionId":101,"eventType":"receiveMessage","msg":"{\"action\":\"status\",\"index\":-1}"}。命令明明送到了,载荷就在info对象里,但字段名是msg,双端代码读的都是message。于是每一条到达的消息都在messageOf()里被读成空串并丢弃,静默得像什么都没发生。一个字母,双端同犯。

教训压成两句话:EventCallbackInfo的文本字段是msg,SDK声明文件第255行白纸黑字;所有“读不到就当没有”的else分支,必须打warn并带原始载荷。本次5分钟破案,全靠这条日志把对象整个吐了出来。静默丢弃是分布式调试的头号大敌。

修完后,那个夜晚最漂亮的一幕出现了:手机点“开始录制”,平板真的开始录,第8篇那套output代码原样复用;一秒后手机按钮自己翻成“停止录制”。这不是前端乐观更新,而是平板的stats.recording=true坐上了每秒一次的状态心跳,流回来把按钮刷成了真。再点一次,平板停录,手机按钮翻回。命令下行、状态上行,双向都验真。

七、顺手抓的两个真bug

第一个是悬空指针。手机首推失败,日志报GetOutputCapability failed。查FindBackCamera:从GetSupportedCameras拿到设备数组,*out = cameras做结构体浅拷贝,然后DeleteSupportedCameras释放数组。Camera_Device.cameraId是个const char*,指向刚被释放的字符串,下游CreateCameraInput拿着野指针查能力,不挂才怪。修法与第13篇相机洋葱同族:跨释放边界的字符串必须自己own一份。发送器结构里加char device_id_buf[64]做深拷贝,生命周期跟pipeline同始同终。鸿蒙NDK里成对的Get*/Delete*全家桶,都是这种雷,见一个排一个。

第二个是测试脚本的4:4:4。rctm-test-sender.py第一推零帧,原因是ffmpeg里testsrc默认yuv444p,撞上baseline profile后编码器拒绝打开。加-pix_fmt yuv420p解决。工具链的坑不在主链路上,但一样能让人误判“功能没通”。

八、离生产级还差什么

这套“无线机位加导播台”目前是演示级成立,生产级还差几件事,原文列出了任务清单和闭环状态:

机位断线自动重连已闭环:指数退避1/2/4/8/16/30秒,断流期间相机和编码器不重启,重连成功清积压帧并重发SPS/PPS配置帧,真机实证制作端被杀后第6次重试自动恢复解码。

机位带声音已闭环:RCTM帧头兑现预留的flags位AUDIO=0x4,手机OH_AudioCapturer采48kHz立体声,OH_AudioEncoder编AAC-ADTS,与视频交错推同一条socket,平板侧硬解AAC进libobs混音器,录成片验到手机麦克风声,峰值-17dB。这一路排掉六个连环坑,包括ADTS/raw封装、喂帧门控死锁、跨设备时钟域、会话锚点复位、空推热循环饿死编码器、僵尸peer卡accept。

机位进录制出口的实证已闭环:testsrc模拟机位流进画布后开始录制,成片h264加aac抽帧验到测试图案,采集、解码、合成、编码、封装全出口打通;RTMP推流口同一composite下游,还待补一次人工验流。

多机位并发尚未实测:两个手机等于两个源、两个端口,架构天然支持,但“分布式机位”的叙事要站住,这一验跑不掉。

九、API锚定与结论

原文对照本机SDK(API 26 sysroot)核实了关键API:Camera Kit NORMAL_VIDEO会话(API 12+)、OH_VideoEncoder surface输入(API 11+)、OH_NativeWindow_GetSurfaceId(API 12+)、OH_VideoDecoder硬解加onStreamChanged几何协商(API 11+)、deviceInfo.deviceType形态分叉(API 10+)、abilityConnectionManager应用协同连接(API 18+,SystemCapability.DistributedSched.AppCollaboration)、distributedDeviceManager可信设备发现(API 10+)。EventCallbackInfo.msg字段名以SDK声明文件第255行为准。

回到最初的问题:把OBS搬上鸿蒙到底为了什么?如果只是“能在鸿蒙上跑”,前十三篇已经够了。这篇给出的答案是,鸿蒙重新分配了“一台直播工作站”的构成。在桌面,机位意味着采集卡或UVC摄像头加一根线;在这里,同一个局域网里的同账号设备,点两下屏幕就能成为一个机位、一块遥控屏。在桌面,多设备协同是第三方软件用私有协议互猜;在这里,设备发现、协同连接、触碰传文件是系统级地基,人人可取。手机不是OBS的缩小版,它是制作主机的镜头和导播台——这个形态,桌面给不了。
回复

使用道具 举报

发表于 14 分钟前 | 显示全部楼层

Re: 鸿蒙OBS无线机位实践:Camera Kit零拷贝推流与同包分角色

楼主这篇信息量很大,几个取舍讲得很实在。同包分角色那段尤其有启发,手机端直接跳过 libobs 和采集链路,不是省资源那么简单,而是顺着设备形态和真实使用场景做产品边界,这个思路比硬把桌面 OBS 塞进手机更合理。跨端相机那条“文档支持”和“三方可用连续流”之间的落差也很有价值,能少踩很多坑。 RCTM 帧头设计我比较认同,20 字节够用,大流量走 TCP、信令走软总线也符合直觉。发送端让相机 surface 直连编码器、输出回调只入队不碰 socket,这个约束很关键,直播场景下丢旧保新也合理。想请教两个点:接收端 pending 队列满丢旧时,如果弱网下丢掉的是关键帧,解码器参考链断裂后有没有关键帧请求或快速恢复机制?另外手机端深度 30 的队列满丢旧,是全部帧一视同仁,还是会给关键帧/配置帧留一点优先级?如果后续还有接收端线程交接和几何协商的内容,期待继续更新。
回复 支持 反对

使用道具 举报

发表于 14 分钟前 | 显示全部楼层

Re: 鸿蒙OBS无线机位实践:Camera Kit零拷贝推流与同包分角色

这篇信息量挺大,几个点看完很有共鸣。同包分角色那段说得很实在,手机后台十几秒被 signal:9 回收这个坑,凡是真机跑过采集编码的都懂,所以一开始就按设备形态分叉、手机端不启动 libobs,不是省资源,而是认清产品事实——制作主机就该在大屏上,手机是镜头加遥控器,这个定位很清醒。 远程相机那条“被文档判死刑的捷径”也值得单独拎出来说。文档标题看着像直路,实际枚举不到 CAMERA_CONNECTION_REMOTE,协同服务接口又是拍照语义、onState 一次回一张静态图,这个“支持跨端调用”和“三方应用能拿到连续帧流”之间的语义差,确实容易让人白花时间,能写出来对后来人帮助很大。 RCTM 那个 20 字节头我挺认同,自用协议没必要背 RTP 的包袱,big-endian 和时间戳提前定死,后面加音频、多机位只动 flag 就行。大流量走 TCP、信令走软总线的分工也对,base64 膨胀加吞吐瓶颈,拿协同文本通道传视频确实是灾难。 发送端 surface 直连编码器这个决定很关键,绕开 GPU 回读、CPU 转格式那一串拷贝,手机端只做采集编码发送,思路干净。OH_NativeWindow_GetSurfaceId 那层“窗口到 surfaceId 字符串”的翻译,第一次碰到确实会愣一下,这细节写出来很实用。编码回调里不碰 socket、队列满丢旧,也是直播语义下的正解。
回复 支持 反对

使用道具 举报

发表于 4 分钟前 | 显示全部楼层

Re: 鸿蒙OBS无线机位实践:Camera Kit零拷贝推流与同包分角色

这篇实践信息量很大,几个点特别有收获。同包分角色那段很实在,手机端直接不进 libobs,既是资源考虑也符合真实使用场景,比强行把完整 OBS 塞进手机合理得多。远程相机那条“文档看着有路、真机走不通”的坑很有价值,尤其是把拍照语义和连续帧流区分开,这个判断能帮不少人少走弯路。RCTM 的 20 字节帧头设计也很克制,big-endian、pts、关键帧/配置帧 flag 先定清楚,后面扩音频和多机位确实会轻松很多。大流量走 TCP、信令走软总线这个分工也很务实,base64 传视频想想就顶不住。发送端 surface 直连编码器、输出回调只入队不碰 socket、队列满丢旧保新鲜,这几条都是直播链路里很关键的经验。接收端好像写到“解码器需要输入时从”就截断了,期待后续补全 Maleoon 硬解、几何协商以及录制状态回流那部分。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-11-11 00:24 , Processed in 0.026897 second(s), 29 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部