口子开在工厂上,不动它的代码
WebRTC 每出一版都要重新合代码,改动做进它本体,合并就越做越吃力,项目又大,编一次也久。它留的口子是工厂:建 PeerConnectionFactory 时,解码与编码的工厂都可以指定成自己的实现。
我的接法是在建工厂时挂上自己的视频解码工厂,里面的解码器按编码类型从播放器自己的注册表取名:平台硬解优先,取不到 FFmpeg 软解兜底;选型机制是解码接入那一篇讲过的那套。Windows 上取到 D3D11VA 那个实现,Android 是 MediaCodec,Apple 是 VideoToolbox。H265 这么就有了能用的解码路——它本体对 H265 只做到协议层、没有解码器,而这一路从头到尾用的都是播放器自己的解码链,WebRTC 侧只做 RTP 解包和码流解析。配套的重编参数在编译那一篇讲过:关掉它内置的 H264,省得两边各链一份 ffmpeg。
视频解码器:进出的两个转换
适配器实现 WebRTC 的解码器接口,同时实现播放器侧的解码观察者,是两头之间的桥。初始化藏在里面:Configure 时按 H264 还是 H265 建码流解析器(顺手把 QP 取出来,那是参与判断图像质量的量化参数),按编码类型从注册表取名建解码器;输入线程看到配置帧齐了自己会完成初始化,不用额外挂一个初始化接口。
WebRTC 交过来的编码帧和 ffmpeg 的包很像,固定 annexb、四字节起始码。有一处要拆开看:AUD、SEI 这些非数据单元后面可能跟着配置帧,也就是参数集,得把包拆开认一遍,配置帧收走单独喂给解码器,剩下的整包照常交给解码器,由它自己处理包内的合并与拆分——配置帧和别的帧合在一个包里是常事,不拆开,MediaCodec 这类要一个一个收参数集的实现就会卡住。
解码结果要翻回它的返回码:成功、数据没就绪、出错、超时、不支持或没打开,各有各的对应;配置帧还没配齐时来的包,回的是「成功但需要关键帧」,WebRTC 收到这个会去向对端要一个 I 帧。

时间戳两头都要对
解码出来的帧转回 WebRTC 的 VideoFrame 时,pts 和 RTP 时间戳都得设。这个我实测过:不设或者设错,日志要么报解码队列里数据太多要丢弃,要么报渲染延迟太大——它拿时间戳判断解码跟没跟得上。
做法是编码帧进来时,把它的呈现时间与 RTP 时间戳一起记进一个队列(带上 QP,限了长度防止越积越多);解码出来拿到 pts,回队列里查出对应的 RTP 时间戳填回去。偶尔查不到,就按 pts 乘 90 兜底——RTP 时钟是 90kHz,这个换算是固定的。
接管到的帧有两种形态
信令那头:本地 SDP 发给服务器换成对端的 answer,SetRemoteDescription 之后双方都知道彼此支持什么;轨道建好的回调里把远端视频轨挂上 sink,帧就从 sink 回调进来。

进来的帧分两种。软件解出来的,CPU 缓冲,按 yuv420p 组装成播放器的帧;硬解出来的,类型标成原生,缓冲里裹的是平台对象——Apple 上是 CVPixelBuffer 一族的图像缓冲,Android 上是 SurfaceTexture 那条外部纹理。原生帧不落 CPU,直接进渲染层,这与普通播放里硬解帧那两条去处是同一个认识:共享的是那块显存,谁做呈现另说。
接口上的对应是:要把帧翻成 WebRTC 的 I420 时,CPU 帧真给数据——yuv420p 直接拷,Windows 的 NV12 用 libyuv 转一道;GPU 帧返回空:渲染全在播放器自己的管线里做,不经这一步。首帧还会建出视频轨道描述、触发源就绪,下游按描述把渲染对象配起来。

AAC:插进它原有的工厂,不整条换
音频这边不能照搬视频的做法。它自带的 opus、G711 后面还挂着音频数据检测那一层,整条换成自己的,这些就丢了。做法是仿照它现成的解码器写一个 AAC 的,挂进工厂的类型列表——列表里除了它,还是原有的 Opus、G722、G711。
这个适配器做两件事:对外声明支持哪些 SDP 格式,按协商结果生成解码器。声明的组合按采样率与声道列了六组:8000 单声道、16000 单双声道、32000 单双声道、48000 双声道,名字都是 mpeg4-generic;44100 不放进声明里,它对采样率有检查。解码内部转发给项目自己的 AAC 实现,走 fdk-aac、FFmpeg 兜底,输出固定 S16。
两个坑:一是头,AAC 的包装有 adts 与 asc 两种,直播流那条路来的带 adts,WebRTC 的 RTP 包不带,要自己生成两字节的 asc 当配置喂进去,两头必须对上,不然一直报错;二是分包,一帧 AAC 可能拆在多个 RTP 包里,包头里写的是这一帧总共多长,收到的包只是其中一段,所以解析是攒积式的——不够就接着收,够了才合成完整一帧送进解码器,缓冲清空重来。网络一抖,一帧拆四五个包是常事,不凑齐,解出来的就是半帧噪声。还有一处小的:它那边返回的音频长度按 16 位样本数计,换算成字节要除以 2。
顶层不进播放器的轨道
普通播放是 MediaPlayer 领着 source、轨道、解码、渲染一条链;WebRTC 这条路不进那套,顶层是一个独立的播放器对象,自己持有渲染器:远端画面直接渲在原生窗口上,声音走音频渲染那一层(开 3A 的开关就是音频前处理那一篇说的那个)。本地那半边——自己的画面预览——复用同一套渲染,推流侧的接法在下一篇讲。
引擎 avox 在 GitHub 开源:github.com/xxxzhou/avox,解码工厂、帧适配与 AAC 工厂都在里面。
音视频开发 WebRTC 解码器 H265 AAC 硬解 C++