OBS Studio 的鸿蒙迁移在技术实践篇收官时给出过 40% 的对齐度账单,这一天集中还账:滤镜面板与多场景、相机源、窗口 Picker、色准四项同时推进,其中三项落地,窗口采集卡在权限审批。下面按问题场景拆解其中可复用的适配与排障方法。
一、滤镜面板:FILTER 是私有源,不能按普通源创建
桥接层第一版照抄普通源的创建方式,调用 obs_source_create,返回 NULL,日志提示 type 'color_correction_filter' can't be created publically。这里叠了两个坑:一是注册名靠脑补,obs-filters 插件里色彩校正实际注册名是 color_filter,而且并没有 flip_filter,需要从插件源码里 grep '.id = "' 抄真实注册名;二是 libobs 里一切皆 source,但 source 分 INPUT/FILTER/TRANSITION 三类,FILTER 与 TRANSITION 属于私有源,必须依附宿主,创建要用 obs_source_create_private(),再用 obs_source_filter_add 挂到宿主上。
- // 错误示范
- obs_source_t *filter = obs_source_create("color_correction_filter", ...);
- // 返回 NULL,日志:type 'color_correction_filter' can't be created publically
复制代码- // 正确姿势:滤镜是私有源
- obs_source_t *filter = obs_source_create_private("color_filter", "color_filter",
- obs_get_source_defaults("color_filter"));
- obs_source_filter_add(source, filter);
复制代码
Qt 前端天然围绕这套语义构建滤镜 UI,自建 ArkTS 前端时就必须在第一次真机上把这条隐藏规则撞出来。
二、黑屏第一层:预览画布被布局挤成 43px
滤镜挂上后预览整块变黑。第一反应是 GPU 驱动:glGetError 返回 0x26D72A00(合法 GL 错误码只到 0x0505),EGL_BAD_NATIVE_WINDOW 刷屏,据此判断 Maleoon 驱动被息屏测试搞进了坏状态,重启设备后依旧黑。A/B 验证把前一天还能显示的旧 HAP 装回去,同样黑,排除了当天改动引入回归。真正定案的是一条日志:
- XComponent surface created: 2046 x 43
复制代码
预览画布被压成了 43 像素高的一条线。为塞进滤镜面板,侧栏高度从固定 300 调到 400,重启后窗口没有最大化、走了堆叠布局,固定高度侧栏加 TransportBar 和混音器把 layoutWeight(1) 的预览挤没了。修复办法是在 EntryAbility 的 loadContent 之后调用 window.maximize(),让桌面软件在 PC/2in1 上默认最大化,同时给预览加 constraintSize 兜底、把堆叠布局改为可滚动。面板够不着、预览被挤扁、自动化坐标漂移三个问题一并消失。
这段弯路的价值在于归因方式:要按实验证据链走,不能按日志里最吓眼的错误走。那串异常 GL 错误码真实存在,是 surface 尺寸异常引发的连锁反应,但不是根因。项目还曾基于错误归因提交过一次“已修复”,发现后用后续 commit 做了公开更正。
三、黑屏第二层:effect 资源从未进包
布局修好后,单独挂滤镜仍然黑。这次日志很干净:
- gs_effect_create_from_file: Null 'file' parameter
复制代码
obs_module_file() 返回 NULL,说明 obs-filters 的 data 目录(含 color_correction_filter.effect)从未被打进 HAP。回溯构建链发现,stage 脚本里“拷 libobs data”那段的条件路径在 OHOS 构建下不存在,等于该逻辑从未执行;插件 .so 进包了,但运行时读取的 effect/shader 资源一个都没进。libobs 的设计里,effect 加载失败不会阻止滤镜对象创建,它只会在画面上黑给你看。
修复分两半:stage 脚本改为遍历同步全部 7 个已加载插件的 data 目录进 rawfile/obs-plugins/<name>/;另外 rawfile 解压按 versionCode 幂等,versionCode 不变就整树跳过,新增资源不会重新解压,因此在 Index 中增加内容戳常量 ASSET_STAMP,资源树变化时递增。结论是资源进包要验货,光改脚本不够,还得让设备真的重新解压。
四、黑屏第三层:settings 传 nullptr 让 opacity 变 0
effect 进包后还是黑。把目光从 GPU 挪回 CPU,看 color_filter 源码里 opacity 怎么读设置:
- double opacity = obs_data_get_double(settings, SETTING_OPACITY);
- // settings 为 NULL → get_item(NULL) 返回 NULL → opacity = 0
- // 0/100 进入色彩矩阵 → 整行乘零 → 每个像素乘成黑
复制代码
桥接层创建滤镜时 settings 传的是 nullptr。libobs 的默认值机制是:创建时不传 settings,不会自动填充插件的 get_defaults,那是 Qt 前端在 UI 层做的事。桥接层应先用 obs_get_source_defaults(typeId) 跑一遍插件 get_defaults,拿到 opacity=100,再传入创建接口。
这类“宽容型”框架的典型故障形态是:滤镜创建成功、effect 加载成功、矩阵全乘零,每一层都不报错,错误只以最终观感呈现。对付它最有效的办法不是读渲染代码,而是先把数据域验证清楚:settings 里到底有没有 opacity、值是多少。
五、相机源:元数据可靠性按生产者分域
harmony-camera 插件首测时表面能用:源创建成功、video output frame start,但预览黑、录制也黑。日志刷的是:
- plane 0 stride 1 smaller than linesize 1920
复制代码
拷贝代码从 OH_NativeBuffer_MapPlanes 拿 rowStride,拿到的是 1。同一套代码在屏幕采集插件里跑了两天正常,对 AVScreenCapture 的 buffer,它报的 stride 是真的;对 ImageReceiver 相机 buffer,它是垃圾。正解是数据生产方自己的 API:OH_ImageNative_GetRowStride(image, componentType, &stride),探针验证得到 stride_y=1920。同时还发现结构差异:相机 NV12 的 GetComponentTypes 只返回 1 个组件,是半平面布局,Y 面和交织 CbCr 面在同一个 buffer 里,而代码假设了 2 个组件各一个 buffer,色度起点应为 stride_y × height。
拷贝修正后加内容探针,对 Y 平面按质数步长采样求和,得到 2080/2080 个采样点非零、均值约 91;再加时间戳探针,image 时间戳与 os_gettime_ns 差 -61ms,属于正常直播延迟量级。CPU 数据对、时钟对、拷贝成功、obs_source_output_video 也调了,画面依然黑。此时合成器每帧刷的错误就是全部真相:
- glDrawArrays failed, glGetError returned GL_INVALID_FRAMEBUFFER_OPERATION(0x506)
复制代码
追进 libobs 的异步帧上传路径,它把 NV12 拆成 GL_R8 加 GL_RG16 两张纹理上传,采样时靠 swizzle 拼回。Maleoon GLES 驱动不吃这套:上传不报错,采样帧缓冲就是 invalid,每帧渲染失败。旁证是 display-capture 一直只走 packed RGBA 上传路径,从没碰过这个雷。
三个选择:给 libobs 上传路径打补丁、换 EGLImage 外部纹理在 GPU 内转换、CPU 端直接 NV12→RGBA。最终选第三种,1080p30 每帧约 1ms,量级可接受,而且能把帧汇入已验证的 packed 快车道。转换用 BT.709 limited-range 系数乘 256 转整数运算:
- int32_t yc = 298 * (yy - 16);
- int32_t r = (yc + 459 * cr) >> 8;
- int32_t g = (yc - 55 * cb - 137 * cr) >> 8;
- int32_t b = (yc + 541 * cb) >> 8;
复制代码
奇数高度底行复制,full_range 标好,避免 libobs 二次展开。验证用三重证据:真机预览画面直出;相机源上再挂色彩校正,画面不黑,顺带证明滤镜链对相机帧同样生效;8 秒录制成片 34MB 拉回抽帧,像素统计 min=5 max=255 mean=166.6 unique=201,是真实曝光分布而非纯色或压缩噪点。
六、窗口 Picker:三条官方机制都接上,卡在受限权限
AVScreenCapture 的 OH_CAPTURE_SPECIFIED_WINDOW 模式要求传 missionIDs。文档说该字段可选,据此用空列表启动,Init/Start 都成功,但没有任何 UI 弹出,文档推断被证伪。随后把 API 26 sysroot 里找到的三条机制逐个接入:SetSelectionCallback(API 20,要求启动前注册,接上后从未触发);CaptureStrategy 加 StrategyForPickerPopUp(true)(API 20,SetCaptureStrategy 返回成功,仍不弹);PresentPicker(API 22,Init 后、Start 前各调一次,均返回 ERR_OPERATE_NOT_PERMIT(2),错误码释义为当前操作未获得必要权限或处于非法状态)。
线索拼起来后,嫌疑锁定 ohos.permission.CUSTOM_SCREEN_RECORDING:system_basic 级受限开放权限,需要 AGC 后台人工审批。没有它,系统在服务侧就把窗口选择能力关了。申请材料已备好,审批通过、签名 profile 加上后,这三行代码不需要改动就能复测。这类“审批依赖”的剩余工作不再是代码,而是流程,移植排期必须给它留位置。
七、色准闭环:一张 20KB 色卡关掉十天争论
账单上的色准精调长期停留在“大体正常”,谁也说不出差多少。方法是造一张可测量的卡:在 mac 上生成 16-patch sRGB 色卡 PNG,含黑、八档灰、白、六个饱和原色,数值精确、无 dither,用 HTTP 服务起来,设备浏览器全屏打开。先用系统截图 snapshot_display 证明“浏览器→合成器→屏幕→抓屏”这条恒等路径 16 项全中,再用 OBS 采集同一屏幕、录制、拉回抽帧比对。
结果很干净:八个非端点灰阶全部落在 sRGB EOTF 曲线上,饱和原色 0/255 端点全部无恙。端点不动正是“解码一次、缺最终编码”的指纹。根因是此前 Maleoon BGRA 退让把所有 GS_BGRA 纹理降成了线性 GL_RGBA8,把渲染目标也一并降了;而 libobs 的色彩管理 pass 写线性值进 canvas,指望的正是 sRGB FBO 的硬件写回编码。修复只有一行判断:GS_BGRA 且是渲染目标时,internal format 用 GL_SRGB8_ALPHA8;被驱动拒的从来只是 BGRA_EXT 那个三元组,(SRGB8_ALPHA8, GL_RGBA, UNSIGNED_BYTE) 是 GLES 3.0 核心组合,可以接受。可采样源纹理保持线性,解码在 shader 里显式做,不能二次解码。验证时重启清掉设备坏态后录 OBS 界面,同坐标 UI 颜色与恒等截图 Δ≤4。
两条教训:观感大体正常不是结论,可证伪的测量才是;驱动退让要退在最小面,退让过界会把别的语义一起埋掉,而且埋得无声无息。
八、可复用的排障结论
buffer 元数据的可靠性按数据生产者分域。同一个 MapPlanes,对 A 生产者报真 stride,对 B 生产者报垃圾,应优先用数据生产方自己的 API 取元数据,中转层 API 只作交叉验证。
“数据对但画面黑”用探针切 CPU/GPU 域。内容探针证明数据真实,时间戳探针证明时钟正常,两个探针各排除一半,剩下的合成器 GL 错误就是全部真相;这比读驱动源码快一个数量级。
错误归因要公开更正。基于“驱动坏状态”提交过错误修复,A/B 实验打脸后用后续 commit 明确标注这是布局回归。以 PR 为轨道的工作方式里,诚实的更正记录本身就是贡献的一部分。
资源进包要验货。改构建脚本只是第一步,还要处理 rawfile 解压的版本戳,让设备真正重新解压新增资源。
当天四项中,滤镜面板、多场景、相机源、色准落地,对齐度从 40% 走到约 60%(功能面口径),窗口 Picker 等 ACL 审批,另有一批反复启停采集会话后偶发 ANR 的独立遗留待专项。项目仓库为 qingkouwei/obs-studio@harmonyos-port,色准测量全记录在 docs/harmonyos-color-measurement.md。 |