Apple 的硬解接入:VideoToolbox 与 ObjC 边界

从零做一个全平台播放器·约 1510 字

Apple 平台上的硬解接口是 C 的,但周边全是 Objective-C 与 CoreMedia 的对象。边界划得清不清,决定了这一层是「一个 C++ 类顺手调了几个系统函数」,还是「一半逻辑散在 ObjC 里」。这篇写这条边界怎么划、会话建起来要备哪些东西、输出格式怎么挑,以及那些看起来像渲染问题的黑屏花屏,根因其实在层与格式这一层。

边界:类是 C++ 的,只有实现文件是 ObjC++

会话的建与配,含参数集按编码分流

解码器主体是一个纯 C++ 类:声明里没有任何 Objective-C 对象,成员就是几个 CoreMedia、CoreVideo 的句柄加上标准库的容器。实现文件后缀是 ObjC++,用它是为了能在类的方法里直接写系统调用。

真正用到 ObjC 语法的地方只有一处:建会话时的属性字典,那是文件里唯一一个 Objective-C 对象字面量。其余句柄(数据块、样本、字典)都是 C 的类型。

回调这一层反过来处理:解压会话要求一个 C 函数指针,这里传的是类的静态函数,把自己的指针当不透明参数带进去,回调里再转回来。这样回调也能落在类里,不用全局变量。

帧的持有则手工管:回调拿到的图像缓冲先加一次引用,放进重排缓冲里;投递出去之后把自己的那一份放掉,长期持有的地方自己再加。关流与异常路径上也各放一次。

头文件用宏把 Objective-C 的声明隔开,日志走标准输出而不是系统日志——整条链上不引入 ObjC 依赖,除了那个属性字典。

出口本来留了两条:交 CPU 缓冲,或交带 IOSurface 的图像缓冲。这一版从构造起就把「走 Metal」固定为真,也没有设置入口,CPU 那条等于封着——后面写的出口都是图像缓冲那条,帧的持有与释放也按图像缓冲的规矩来。

会话的建与配

建之前先把旧会话退场:等异步帧处理完,再让它失效并释放。换片、换分辨率都要走这一步。

参数集换成格式描述这一步按编码分岔:H264 用 SPS 与 PPS 建;H265 是三个;VP9 只给编码类型与宽高;AV1 的参数集是一段配置记录,包在扩展里送;ProRes 从容器的编码标识取。

会话本身建的时候带一个属性字典,里面只挂两样:像素格式,以及一个「与 Metal 兼容」的开关。不带与 GL 兼容的那个键是有意的:较新的系统版本里 GL 那一侧已经移除,带上它会把会话建在一条废弃的路上。

喂包这一段有一步不能省:数据要拷进一个系统缓冲块里,因为送进去之后数据的所有权就交出去了,帧的时效也不由这边定。样本带上显示与解码两个时间戳,再带两个标志:允许异步解压、允许时间处理。

多 slice 的流还有一个前置动作:解码器要的是「整幅图一个样本」,所以喂进去之前要把同一个访问单元的多个切片聚合起来——一个访问单元,就是一幅图的全部切片。

出帧回调里做重排:按时间戳把帧排好再往下放,免得出来的是乱序。

输出像素格式:按编码与位深定

默认给的是 8bit 的平面格式。10bit 要看编码:高压缩比的那两种且是 10bit 时,给 10bit 的平面格式;ProRes 的 10bit 给另一种色度不缩半的格式。

有两个「宁可放弃这条车道」的判断:一种是编码规格要求 10bit 输出、而这个格式拿不到,那就按打开失败上报,让上层换下一条候选;另一种是 10bit 的输出格式不被支持,也一样放弃——这里不静默折成 8bit。

前面那句「与 Metal 兼容」是这个实现的前提:帧的默认出口是带 IOSurface 的图像缓冲,给渲染那一侧直接映射成纹理。

黑屏与花屏:成因在层和格式上

那一族黑屏与花屏各自的成因与处理

这一族问题看起来都在渲染,根因却分几种,分开说:

每帧开始先自愈一次:重建图、管线与目标,防止窗口面重建或直通态翻转之后残下空对象,整段黑屏。

每一个必要条件缺失都点名记一条日志:图像缓冲、缓存、管线、目标,以及层的尺寸与设备。排查时先看这一组。

层的格式会漂移:宿主重新挂视图会把层重置回 8bit,而管线可能还在按 16bit 画——画进 8bit 目标就是花屏。处理是每帧核对目标的格式,不符就把层配置写回去并丢掉这一帧。

直通态翻转必须让层与管线在同一帧里一起换格式,否则校验会直接拦下来。

层的尺寸也不归这一侧管:宿主视图在主线程挂到层上,尺寸按视图尺寸乘缩放得出。显示器更换没有监听,宿主不动视图,这一侧就无从知道尺寸变过。

窗口不可见时取不到可绘制的对象,渲染那一侧跳过这一帧——桌面端因此不注册后台标志,移动端则由应用生命周期通知停管线。

与软件解码怎么共存:预检在打开前,自愈在解码中

这个实现按编码注册进表,标成硬件。能不能用这件事分两层管,位置不同。

预检在打开之前:先看系统版本够不够,再看这台机器对当前编码支不支持硬解。不通过就按打开失败上报——上层不必知道是版本问题还是硬件问题,候选链会换下一条。

坏流自愈在解码过程中:同一轮里连续多次拿到坏数据时,先扣掉一些帧、等下一个关键帧重新对齐;如果重建多次之后仍然一个好帧都没有,才按打开失败上报、退回软解。

引擎 avox 在 GitHub 开源:github.com/xxxzhou/avox,这条边界、会话的配置与那几处格式守卫都在里面。

音视频开发 播放器 硬件解码 VideoToolbox Metal iOS