OBS Studio 32.2.2 的 HarmonyOS NEXT 移植记录,给桌面级 C/C++ 应用进入鸿蒙生态提供了一个较完整的样本。目标平台是 arm64、API 26,验证设备为 MatePad Edge,运行在 PC/2in1 模式,系统版本 HarmonyOS 7.0.0.109,GPU 为 Maleoon 916B。工程位于 fork 分支 qingkouwei/obs-studio@harmonyos-port,系列共 10 篇,第一篇先给出全景结论:预览、录制、推流三条核心链路均已在真机跑通。
真机结果比“能编译”更有说服力。桌面模式下,OBS 预览窗口里能看到浏览器窗口和 OBS 自身窗口,说明屏幕采集到预览的链路已经打通;录制产物是标准 MP4,编码为 h264 1080p60 加 AAC,经过 ffprobe 和抽帧双重验证;RTMP 推流到局域网 mediamtx 服务器后,再通过 HLS 回拉抽帧,画面中 OBS 状态栏显示 LIVE、码率 0.61 Mbps、丢帧 0。另有一项容易被忽略但产品上很关键的验证:把 OBS 切到后台、前台运行其他应用,录制继续执行,221 秒成片,无 ANR。鸿蒙应用生命周期与桌面系统差异很大,后台仍能持续采集和编码,本身就是移植过程中的一个里程碑。
为什么要移植 OBS
原因有三层。第一,HarmonyOS NEXT 已跑在平板、PC、2in1 形态上,但桌面级内容生产工具仍然稀缺,录屏、推流、虚拟摄像头几乎是空白,主播拿鸿蒙 PC 无法直接开播。第二,OBS 的架构意外地适合移植:内核 libobs 是纯 C,平台相关代码收敛在少数文件里,图形走 gs_* 抽象 API,采集、编码、输出都按插件组织。理论上只要替换图形后端和平台插件,前端 UI 可以重写。第三,这次移植相当于对鸿蒙原生能力做一次系统体检,涉及 AVScreenCapture 屏幕采集、OH_VideoEncoder 硬编、EGL/GLES3、napi 桥接、ArkTS UI、沙箱文件系统、SELinux 策略和 linker namespace。
总体架构与关键取舍
技术路线可以概括为:libobs 内核交叉编译后直接进入鸿蒙,图形层从桌面 GL 切到 EGL/GLES3,Qt 前端换成 ArkTS,采集和编码用鸿蒙 NDK 重写为原生插件。ArkTS UI 通过 napi 桥接到 obs_bridge.cpp,再调用 libobs 内核;libobs-opengl 通过 EGL 和 GLES3 对接 XComponent 提供的 OHNativeWindow;harmony-capture 使用 AVScreenCapture,harmony-audio 处理系统内录和麦克风,harmony-vcodec 使用 OH_VideoEncoder 硬编,obs-outputs 负责 MP4 封装和 RTMP 输出。
UI 没有继续用 Qt。Qt for OpenHarmony 虽然存在,但生态较浅,而且 OBS 的 Qt 前端与 libobs 之间纠缠了大量平台代码,包括系统托盘、全局热键、窗口管理等。用 ArkTS 重写一个够用的前端,覆盖场景、来源、混音器、推流录制控制,工作量反而小于维护 Qt 的鸿蒙移植层;最终这个前端只有几百行。
图形选择 GLES 而不是 Vulkan。HarmonyOS NEXT 的 NDK 层对外暴露的是 OpenGL ES,没有桌面 GL,Vulkan 不在本次范围内。libobs 的 GL 后端主体可以复用,但 shader 生成器按桌面 GLSL 方言编写,这里埋下了后续的典型问题。
录制封装选择 mp4_output,而不是传统 ffmpeg muxer。OBS 传统上把 ffmpeg 封装器跑在子进程里,用于崩溃隔离;鸿蒙应用沙箱禁止 fork/exec 自带 ELF,这条路直接走不通。OBS 32 系里较新的 mp4_output 是纯进程内封装器,功能上正好覆盖录屏场景。这是平台约束反推架构选型的典型例子。
静态全绿不等于能跑
第一阶段工作在 macOS 上完成:60 个依赖库鸿蒙化编译、libobs 平台层和构建脚本链,38 项契约检查全过,17MB 的 HAP 构建成功。装机后图形后端却起不来。复盘下来,从静态全绿到预览出画面填了 15 个坑,到录制出片又填了 6 个,推流稳定再填若干。共同点是:桌面上不可能发生,纸面推演也很难想到。
第一个坑是 GLES 并非桌面 GL 的子集,而是另一套更严格的方言。libobs 的 shader 解析器生成的 GLSL 在 GLES 下连环撞墙:out gl_PerVertex 接口块重声明被拒绝,因为 ES 里 gl_Position 是内置量;textureSize 返回 ivec,不会隐式转成 vec;textureLod 拒绝 int 型 LOD 参数;还揪出一个上游真实笔误,65535. / 4095 少了小数点。移植图形代码时,不能按“ES 是桌面 GL 子集”的直觉处理,类型系统严格度完全不是一个量级。
第二个坑是驱动 advertised 的能力和真正实现之间存在差距。Maleoon 916B 驱动声明支持 GL_EXT_texture_format_BGRA8888,但按 ES 规范 Table 3.2 完全合法的纹理组合 (SRGB8_ALPHA8, BGRA_EXT, UNSIGNED_BYTE) 却返回 GL_INVALID_OPERATION。试错三轮后才落到退让组合 (RGBA8, RGBA, UNSIGNED_BYTE)。扩展被 advertised,不等于该扩展的所有组合都被实现,这是国产 GPU 驱动合规性的一课。
第三个坑来自 SELinux 和 linker namespace 的双重门。libobs 启动时扫描插件目录,在鸿蒙上一行日志都不打,原因是目录 readdir 被 SELinux 策略禁止,返回 EACCES。改用绝对路径 dlopen 后,linker namespace 又报 check ns accessible failed。最终答案很朴素:使用裸 soname,例如 dlopen("harmony-capture.so"),走 namespace 的 permitted paths,而那里恰好就是 bundle libs。代价是桥接层要显式维护一份 12 插件清单。
第四个坑是预览黑屏,根因是一个枚举值撞车。排查中做过红帧实验、渐变实验和 attribute buffer 转储,最后发现手写的 glad shim 头文件里 GL_TEXTURE_RECTANGLE 被定义成 0x0DE1,而 0x0DE1 正是 GL_TEXTURE_2D。矩形纹理判定恒为真,UV 坐标按像素生成,CLAMP 采样只取到角落一个 texel,整个画布糊成单色。改成真值 0x84F5,画面立即出来。一个数字卡了一整天。
第五个坑是录出来的视频整幅偏红。编码链路打通后,成片发红。根因是 GPU 编码路径交付 NV12 双平面纹理,Y 一个平面、UV 一个平面,而 blit 着色器把 Y 平面当 RGBA 采样,得到 (Y, 0, 0),自然是一片红。补上正经的 NV12 到 RGB 的 BT.709 转换着色器后,颜色恢复正常。
调试方法论:把真机验证做成流水线
设备端验证的结构性成本很高:改一行代码、交叉编译、打包 HAP、装机、唤醒解锁、启动、点弹窗、抓日志、截图,每一环都可能因无线 hdc 掉线、自动锁屏、hvigor 打包缓存没带上改动而产生假象。作者的解法是把流程固化成一条命令:
- ./build-aux/harmony/on-device-verify.sh --record 8
- STREAM_SECS=45 ./build-aux/harmony/on-device-verify.sh --stream
复制代码
脚本自动完成装 HAP、唤醒并两段上滑解锁、清日志重启应用、轮询点掉允许弹窗、添加 Display 源、点击开始共享、触发录制或推流、抓 hilog、截图回传。窗口坐标每次运行期用 dumpLayout 现取,抵抗布局漂移。单轮改码到验证的循环从一小时压到几分钟。另一条铁律是日志先行:移植任何 C/C++ 库,第一件事把它的日志接进宿主系统,例如把 libobs 的 blog() 转发到 hilog。鸿蒙应用没有 stderr,不接日志等于瞎子摸象,后续坑能定位,全押在这一步上。
遗留项与社区推进
工程侧还有色彩精调,包括 8bit sRGB canvas 的双重编码问题;窗口和相机源的 Picker 交互流程;以及 AGC 受限权限 CUSTOM_SCREEN_RECORDING 的正式申请材料。社区侧按“以 PR 为工作轨道”推进,已在 fork 上切出两个可独立合入的小 PR:format_conversion.effect 笔误修复,以及 gl-shaderparser.c 的 GLSL ES 3.0 兼容三连。这两个改动对任何 GLES-only 目标都有独立价值。大的移植 PR 会先在 OBS dev forum 发 RFC 探路,harmonyos-port 分支作为公开进度台账持续更新。
对准备把桌面 C/C++ 库搬到鸿蒙的开发者,这次移植给出的可复用经验是:先接日志,尽早真机;图形代码按 GLES 的严格类型系统处理;封装器和子进程方案要优先考虑沙箱约束;真机验证必须自动化,否则排障成本会吞掉移植收益。 |