边界:类是 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