查看: 111|回复: 3

OBS 鸿蒙硬编录制:共享 EGL context 与 NV12 偏红

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
背景:录制链路代码就位,真机一跑连环六坑

这是 OBS Studio 鸿蒙迁移系列的第七篇。预览能出画面之后,把画面录成一个视频文件这件事,代码上其实大部分已经就位:harmony-vcodec 硬编插件、obs-outputs 的 mp4 封装、桥接层的 Start/Stop 都写好了。但真机一点录制,问题一个接一个暴露出来,而且几乎每一个都是同一个母题的标本——桌面上成立的默认假设,在鸿蒙的沙箱、多线程和严格 GLES 语义下不成立。

下面按暴露顺序复盘六个坑的定位过程与修法,验证环境是 HUAWEI MatePad Edge(PC/2in1 模式),HarmonyOS 7.0.0.109,API 26,OBS Studio 32.2.2,分支 qingkouwei/obs-studio@harmonyos-port。

坑一:ffmpeg_muxer 要 fork 子进程,沙箱直接判死刑

点录制立刻启动失败。排查下来,OBS 默认录制走的是 ffmpeg_muxer 输出,它会 fork 一个名为 obs-ffmpeg-mux 的独立 ELF 进程来做封装。这是 OBS 的崩溃隔离设计:封装器崩了不会带走主程序。

而鸿蒙应用沙箱禁止 fork/exec 自带可执行文件,这个 helper 在上游本来就没有鸿蒙构建产物,即便有也不允许启动,子进程模型在这里完全没有生路。

好消息是 OBS 32 系里还有另一条实现:mp4_output,纯进程内的 encoded muxer,直接接收 h264/aac 包写 mp4,不依赖子进程。桥接层改一行即可:
  1. // 从 "ffmpeg_mux" 换成 "mp4_output"
  2. obs_output_t *recOutput = obs_output_create("mp4_output", "harmony_recording", settings, NULL);
复制代码

这里的经验值得记下:OBS 里看起来是同一个功能的录制,底层其实有 ffmpeg_muxer(子进程)和 mp4_output(进程内)两套实现,移植时要主动挑无进程依赖的那条。这也解释了为什么能力盘点里多格式封装(mkv/mov/flv)被标为"可补齐但工作量大"——那几种格式上游只在 ffmpeg muxer 里实现,要进程内化就得把 avformat 整体搬进来。

坑二:编码线程 SIGSEGV,因为它没有 GL context

切到 mp4_output 后点录制,应用秒崩,日志停在这样一行:
  1. signo(11) threadName(obs gpu encode)
复制代码

崩在名为 obs gpu encode 的线程上。查 libobs 源码可以确认,纹理编码跑在 obs-video-gpu-encode.c 创建的独立线程里。这个线程是为 Windows/D3D 设计的:D3D 纹理按 handle 跨线程共享,天然不需要 GL context。但在 EGL/GLES 下,要采样 libobs 创建的 GL 纹理,必须有一个与之共享资源的 context。插件的 venc_gl_init 里调用 eglGetCurrentContext(),在这条线程上自然拿到空值,用空 context 去建 surface,结果就是段错误。

这是所有 GL 平台纹理编码器的标准套路(NVENC、QSV 的 GL 版都这么处理):编码线程自建一个 share context。先在图形线程、context 有效的地方用 obs_add_main_render_callback 捕获 master 句柄与 config,再在编码线程里创建共享 context 并 make current:
  1. // 图形线程(context 有效处)用 obs_add_main_render_callback 捕获 master 句柄
  2. static void CaptureMaster(void *) {
  3. g_masterDisplay = eglGetCurrentDisplay();
  4. g_masterContext = eglGetCurrentContext();
  5. // config 也从当前 surface 查出来
  6. }
  7. // 编码线程里建共享 context
  8. EGLContext shared = eglCreateContext(g_masterDisplay, g_config, g_masterContext, attrs);
  9. eglMakeCurrent(g_masterDisplay, encodeSurface, encodeSurface, shared);
复制代码

一句话总结:libobs 的渲染和编码是两条线程,跨线程共享 GPU 资源在 GL 语义下必须显式 share context。这是从 D3D 世界过来的人最容易忽略的模型差异。

坑三:GLES 没有客户端顶点数组,传 CPU 指针必崩

共享 context 建好后,blit 第一帧仍然崩。查 blit 代码发现问题出在顶点上传方式:
  1. glVertexAttribPointer(loc, 2, GL_FLOAT, ..., venc_fullscreen_vertices); // CPU 数组指针
复制代码

桌面 GL 兼容模式允许在没有绑定 VBO 时直接传客户端数组指针,但 GLES 要求必须先绑定 GL_ARRAY_BUFFER,此时指针参数会被当作 VBO 内的字节偏移来解释,驱动把这个 CPU 地址当偏移去解引用,自然段错误。

修法是在共享 context 里建一个小 VBO,把全屏三角形顶点上传进去,blit 时先 glBindBuffer 再传偏移 0。GLES 是纯 VBO 世界,桌面 GL 里图方便传指针的写法在这里都是雷。

坑四:编码 0 帧,跨线程取纹理句柄被守卫挡住

不崩了,但日志报 source texture has no GL object,编码出来 0 帧。问题在 gs_texture_get_obj(拿 GL 纹理名)的 libobs 包装层:
  1. // graphics.c
  2. void *gs_texture_get_obj(gs_texture_t *tex) {
  3. gs_valid_p(tex); // ← 这个守卫要求 thread_graphics 非空
  4. return gs_texture_get_obj_internal(tex);
  5. }
复制代码

gs_valid_p 要求当前线程是图形线程,而编码线程从没 enter_context,thread_graphics 为 NULL,于是守卫直接让它返回 NULL。但这个函数本身只是读一个结构体字段、拿纹理名,属于纯元数据读取,根本不需要 GL context。

修法是给 graphics.c 加一个全局 gs_singleton(libobs 只创建一个 graphics 实例),让 gs_texture_get_obj 在 thread_graphics 为空时回退到它。纯元数据读取跨线程是合法的。这也提醒我们:libobs 的 gs_* 包装层默认所有调用都在图形线程,纹理编码器这类跨线程读句柄的场景必须专门开绿灯。

坑五:录出来了,但整幅偏红——NV12 双平面被当成了 RGBA

录制成功,文件可播,ffprobe 显示 h264 610 帧加 aac 音轨,但抽帧一看整幅是红的:画面内容和几何完全正确,只有颜色错。预览路径颜色是对的,只有录制偏红,说明问题在编码取帧路径而不是渲染路径。

看 obs_tex_frame 结构就清楚了:它带 tex 和 tex_uv 两个字段,是 NV12 双平面纹理。tf.tex 是 Y 平面(GL_R8 单通道,全分辨率),tf.tex_uv 是 UV 交错平面(GL_R8G8,半分辨率)。而当时的 blit shader 只做了 texture2D(u_texture, uv).r,把 Y 平面当 RGBA 用,于是 R 等于亮度、G 和 B 都是 0,结果就是整幅全红,症状完美对应。

补一个正经的 NV12→RGB 转换着色器,按 Rec.709 limited-range(与 OBS video 设置一致):
  1. float y = texture(u_texture, uv).r; // Y 平面
  2. float2 uv2 = texture(u_texture_uv, uv).rg - 0.5; // UV 平面,减 128
  3. // BT.709 limited range
  4. float r = y + 1.5748 * uv2.y;
  5. float g = y - 0.1873 * uv2.x - 0.4681 * uv2.y;
  6. float b = y + 1.8556 * uv2.x;
复制代码

Y 平面绑到纹理单元 0,UV 平面绑到纹理单元 1。重录后再抽帧,蓝色场景高亮、红色停止按钮、自然灰阶全部正确,偏红消失。

坑六:录制在跑,按钮却不翻牌

最后一个小问题:录制明明在执行(文件持续变大),按钮上还写着"开始录制",导致自动化脚本找不到"停止录制"去停止。根因是 toggleRecording 成功后依赖 stats 线程异步推回 recording=true 才刷新按钮,而 stats 首帧推送存在延迟。修法是成功后乐观置 model.stats.recording = true,不等异步回推。

端到端验证与复盘

六个坑走完,最终产出的 recording.mp4 经 ffprobe 核验,视频流为硬件 H.264(OH_VideoEncoder),1920x1080、60/1 fps;音频流为内录的 PlaybackCapture,aac stereo 48kHz。录制进行时的状态栏同样是健康的:红点"录制中"、约 0.62 Mbps、drop 0/294、计时在走,预览里是采集回来的真实桌面。抽帧画面里的 OBS 窗口、设置应用和混音器,色彩、几何、文字均正确。

回头看,六个坑的共同点是同一个:桌面/移动单线程/宽松驱动的隐含假设,在鸿蒙的 EGL + 沙箱 + 严格 GLES 三者叠加下逐条失效。这也是录制段比采集段更硬核的原因——它同时踩了沙箱、多线程、GPU 语义三座山。

下一篇进入推流:rtmp_output 的进程内模型没有 fork 问题,但 obs_output_set_service 那个"不加引用计数"的 setter 语义陷阱比编码链更阴,会让 output 揣着一个悬空指针去 start。
回复

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: OBS 鸿蒙硬编录制:共享 EGL context 与 NV12 偏红

这篇复盘很实在,把“桌面能跑、鸿蒙真机连环崩”的根因串得很清楚。坑一主动切到进程内 mp4_output 很关键,沙箱下 fork helper 这条路基本没得选;坑二把编码线程必须自建共享 EGL context 讲透了,尤其强调在图形线程捕获 master handle 的时机,这个细节对后来移植 GL 纹理编码器的人很有用;坑三的 VBO 问题也很典型,GLES 里客户端顶点数组就是雷区;坑四的 gs_texture_get_obj 守卫问题更像是 libobs 默认假设与跨线程读元数据需求之间的冲突,如果只是拿纹理名,确实不该被图形线程守卫卡死。 不过正文到“修法是给 graphi”就截断了,坑四的具体修法和后面两坑还没看到,期待补全。整体上六个坑都围绕沙箱、多线程和严格 GLES 语义展开,这个归纳很有参考价值;后续如果能把这些修法收敛成一个编码线程初始化辅助层,可能会让其他硬编插件少踩一遍。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: OBS 鸿蒙硬编录制:共享 EGL context 与 NV12 偏红

这篇复盘看得很有共鸣,六个坑基本都踩在“桌面能跑、鸿蒙不一定”的默认假设上。几个点想补充交流: 坑一换 mp4_output 是正解,但进程内封装也意味着录制中崩溃或强杀时 moov 可能写不进去,文件直接废掉。建议评估一下 fragmented mp4,或者录完做一次自动 remux,至少别让用户丢整段素材。另外 mkv/mov 要补齐的话,把 avformat 进程内化确实是大工程,短期先保 mp4 比较现实。 坑二共享 context 那块,编码线程建议用 pbuffer surface,别依赖窗口 surface,后台或尺寸变化时更稳;EGLConfig 也要确认和 master 兼容,eglCreateContext 失败时最好有返回值检查,不然又是另一种崩法。 坑三补一个容易连带的细节:GLES3 下 VAO 不是共享对象,共享 context 里创建的 VAO 不要拿到渲染线程用,反之亦然,每个 context 各建各的 VAO,VBO 可以共享。这个和客户端顶点数组的坑经常一起出现。 坑四的守卫问题,短期可以在编码线程 obs_enter_graphics/obs_leave_graphics 包一下再取纹理名,但这是全局锁,和渲染线程抢锁可能影响帧率;如果共享 context 下纹理名稳定,更干净的做法是在图形线程侧缓存纹理名,或者按你的思路把纯元数据读取的守卫放
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: OBS 鸿蒙硬编录制:共享 EGL context 与 NV12 偏红

楼主这六连坑复盘得很实在,尤其把“桌面默认假设在鸿蒙沙箱、多线程和 GLES 严格语义下不成立”这条主线拎出来,很有参考价值。坑一换 mp4_output、坑二编码线程自建 share context、坑三 GLES 必须走 VBO,这几个点都是移植时容易踩又不容易一眼看出的。坑四那个 gs_valid_p 守卫也挺典型,读纹理名本质上是元数据读取却被图形线程校验拦住;如果不想动 libobs 核心,在桥接层包一个只取 obj 的接口可能更稳,但要注意别把真正需要 context 的调用也绕过去。标题里的 NV12 偏红还没展开,期待坑五坑六,尤其想看你最后怎么处理色彩空间和格式转换这一块。另外共享 context 和 VBO 的生命周期、编码线程退出时的清理,也值得在文里补一句,方便后来人避坑。
回复 支持 反对

使用道具 举报

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

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

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

官方核心成员

关注微信公众号

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

GMT+8, 2026-11-4 02:05 , Processed in 0.022981 second(s), 28 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部