背景:录制链路代码就位,真机一跑连环六坑
这是 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,不依赖子进程。桥接层改一行即可:
- // 从 "ffmpeg_mux" 换成 "mp4_output"
- 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 后点录制,应用秒崩,日志停在这样一行:
- 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:
- // 图形线程(context 有效处)用 obs_add_main_render_callback 捕获 master 句柄
- static void CaptureMaster(void *) {
- g_masterDisplay = eglGetCurrentDisplay();
- g_masterContext = eglGetCurrentContext();
- // config 也从当前 surface 查出来
- }
- // 编码线程里建共享 context
- EGLContext shared = eglCreateContext(g_masterDisplay, g_config, g_masterContext, attrs);
- eglMakeCurrent(g_masterDisplay, encodeSurface, encodeSurface, shared);
复制代码
一句话总结:libobs 的渲染和编码是两条线程,跨线程共享 GPU 资源在 GL 语义下必须显式 share context。这是从 D3D 世界过来的人最容易忽略的模型差异。
坑三:GLES 没有客户端顶点数组,传 CPU 指针必崩
共享 context 建好后,blit 第一帧仍然崩。查 blit 代码发现问题出在顶点上传方式:
- 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 包装层:
- // graphics.c
- void *gs_texture_get_obj(gs_texture_t *tex) {
- gs_valid_p(tex); // ← 这个守卫要求 thread_graphics 非空
- return gs_texture_get_obj_internal(tex);
- }
复制代码
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 设置一致):
- float y = texture(u_texture, uv).r; // Y 平面
- float2 uv2 = texture(u_texture_uv, uv).rg - 0.5; // UV 平面,减 128
- // BT.709 limited range
- float r = y + 1.5748 * uv2.y;
- float g = y - 0.1873 * uv2.x - 0.4681 * uv2.y;
- 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。 |