Windows 的硬编:编码器选型与输入这条路

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

硬编这件事在两端的说法不一样:苹果与安卓各自有系统给的编码器接口,Windows 这边则是先在一张表里挑一个编码器,再决定帧怎么进去。挑的部分与解码那侧同构,但这一版真正打开的是 FFmpeg 带的那几个硬件编码器,帧进去的形态是内存里的 NV12——也就是说渲染完的图要先回读一次。这篇写这条候选链怎么走、那个分岔在哪,以及纹理直接喂进去那条路现在到哪一步。

编码器这一层:一条候选链,硬编只是排在前面

编码器与解码器用同一套机制:一张注册表,每个平台把实现挂进去,每项带一个硬件标记。选型的时候先给一个首选名,其余按注册顺序追加在后面,首帧打不开才往下换一条候补。

这套机制在解码那一篇写过,编码这一侧是照它搭的,不重复。

开着硬编开关时,首选名给的是这个平台自研的那两个——ff_h264_dx11 与 ff_hevc_dx11。但这两个名字底下还没有实现:D3D11 那条路在这两个类里没接上编码,表里取不到这个首选名。

取不到首选名,候选链就落到注册表其余项上。这是选型链本来的兜底:首选打不开就按顺序往下。

这一版实际打开的是谁

候选链怎么排,以及打开编码器时那个分岔

注册表里除自研那两项之外,还有 FFmpeg 批量注册进来的它自带的编码器。打开时按名字取到之后,Windows 这一侧还有一个分岔:如果输入是 NV12,先按能不能建出硬件设备上下文来挑——依次试 QSV、AMF、NVENC 三家,哪家能建出设备就用哪家的编码器名;三家都建不出来,才回落到渠道注入的那个名字。

渠道注入的名字在 Windows 的包里是 MF 那两个,也就是走系统自带的媒体基础变换器。所以这一版在 Windows 上开硬编,打开的是三家硬件编码器之一,或者那个 MF 封装——两者都在 FFmpeg 这一支里。

这个分岔不查硬件能力表,直接试着把设备建出来,建得出来就算硬件可用。设备建得出来但编码器用不了的情况,由往下换候补来解决。

这一步在日志里能对上:先在表里找首选名,找不到就是一条警告;再按注册序取,真正打开的那个名字会出现在日志里。所以「这一版跑的是谁」不用猜,看日志第一行就能对上。

打开时配的几样,与各编码器不同的那套词

不管是哪一支里的编码器,打开时共通的那几样是同一套:

像素格式跟着上层描述走;颜色这一组有三样要一起给——颜色范围、色彩空间,以及原色与传输特性。这三样不给的后果很具体:编码器写出来的码流里这组信息是未指定,播放器默认按有限范围解释,饱和度高的画面会被裁掉一截、颜色偏掉。

时间基给的是毫秒,与整条链上的时间戳取法一致;关键帧间隔按倍数折算成帧,也就是间隔乘以帧率;双向预测帧关掉;码率按分辨率乘帧率再乘一个系数算出来;线程数与线程类型也一起给,帧级与切片级都开。

往下就看具体是哪个编码器了,它们调参的词不一样:x264 那一族走质量系数加最快的档,并且把平均码率置零;QSV 用一档较快的预设并保留码率;NVENC 用最快那一档再加一个低延迟的调优项;AMF 用速度优先。MF 那两个不下发档次——它的档次是数值枚举,传字符串会让打开直接失败。

这套分发按名字里带的标记认。所以新接一个编码器时,配参那一段要跟着补一条分支,否则它会用上一家的词。

输入形态:纹理直喂只有设计,现在走内存上传

编码器选出来之后,还有一个输入形态的问题。纹理直喂那条路暂时没做完,现在走的是内存上传。

输入是 CPU 平面帧:三个重载里最终交给编码器的是内存里的 Y、U、V 三个平面指针,编码器自己把数据传上去。也就是说,渲染或者解码出来的帧要先回读成内存,再交给编码器——编码器本身是硬的,但这条路上没有零拷贝。

两条输入路:纹理直喂与内存上传,现在走的是后者

纹理直喂那条路对应的是自研那个实现:它接的是 GPU 帧,进来先用计算着色器把 RGBA 纹理转成 NV12 纹理,再把这张纹理塞进给编码器的数据结构里;设备借 FFmpeg 的 D3D11 硬解设备上下文建出来,与解码那个实现借设备是同一套。这条路暂时没做完。

为什么这条路没那么好走:D3D11 那条硬解设备的上下文是解码侧的东西,编码要拿同一台设备,得走媒体基础那套设备管理器,或者让编码器自己暴露一个能收纹理的输入面。设备建出来了不等于编码器愿意收纹理,这一步是这条路真正的关节。

与解码侧共用的那套设施

解码那边为了把帧直接交给渲染,已经建起了一套设施:借 FFmpeg 建一台 D3D11 设备、交给渲染侧复用同一台,再把解码产物转成的 RGBA 共享纹理与一道共享栅栏交出去。

纹理直喂那条编码路要用的是同一套东西:帧对象里的上下文与缓冲本来就是为这个留的——上下文的取纹理接口给出那张纹理,缓冲字段在硬解帧上装的就是平台纹理指针。编码那个留壳的实现正是从这里取纹理。所以这套设施在解码与渲染之间是通的,在编码这一侧还没有接上。

同一张表在别的平台上是什么样

把三个平台摆在一起看,这张表的用法是一致的,差别在谁真的填了实现。

Linux 上同类的那一支是接通的:它按硬件加速那套接口实现、注册进表,选型时的首选名就是它,走的是同一条候选链。Vulkan 那两个名字也还没做,是留给「在 Vulkan 里做编码」这条路线的位置。

所以这件事不是机制缺一块,而是这一支实现没做:表、候选链、首帧前换道、失败处置全都齐,缺的只是那两个类里最后一步——把帧以纹理形态交给编码器。这一点与硬解接入那一篇里这与硬解接入那一篇那两条搁下的自研解码实现是一类情况:机制同一套,实现没有做完。

Windows 上「硬编」在这个项目里目前意味着编码器是硬的,输入还是内存;纹理直喂那条路因为时间关系还没做完。

引擎 avox 在 GitHub 开源:github.com/xxxzhou/avox,编码器的注册与选型、这台共用的 D3D11 设备都在里面。

音视频开发 播放器 硬件编码 Media Foundation FFmpeg C++