4. WebRTC 视频引擎架构:VideoEngine、SendStream、ReceiveStream 与编码器接口
这一章,我们聊聊 WebRTC 的视频引擎架构。说白了,就是搞清楚视频数据从采集到发送、从接收到渲染,中间到底经过了哪些模块。我当年刚接触 WebRTC 时,也被这一堆类名绕晕过。后来踩了几个坑,才慢慢理清脉络。
4.1 VideoEngine 模块:视频引擎的“大脑”
VideoEngine 是 WebRTC 视频模块的顶层入口。它不直接干活,但负责协调所有子模块。你可以把它理解成一个调度中心。
它的核心职责包括:
- 创建和管理视频通道:每个视频流对应一个通道,通道内部维护编码器、解码器、RTP 发送/接收等资源。
- 配置编解码参数:比如分辨率、帧率、码率、编码器类型(H264/H265/VP8/VP9)。
- 启动和停止视频流:控制采集、编码、传输的生命周期。
- 统计与反馈:收集丢包率、延迟、编码耗时等数据,供拥塞控制和码率自适应使用。
关键点:VideoEngine 本身是单例的,一个 PeerConnection 对应一个 VideoEngine 实例。但每个视频流(比如屏幕共享和摄像头)会创建独立的通道。
我在项目中遇到过一个问题:两个视频流共用同一个 VideoEngine 实例,结果编码参数互相干扰。后来发现是通道 ID 没有隔离。嗯,这里要注意,通道 ID 必须全局唯一。
4.2 VideoSendStream 与 VideoReceiveStream:发送与接收的“管道”
这两个类,是视频数据流动的核心载体。我习惯把它们比作“水管”——一端接编码器,一端接网络。
4.2.1 VideoSendStream
VideoSendStream 负责将编码后的视频帧打包成 RTP 包,并发送到网络。它的工作流程大致如下:
- 从编码器拿到编码后的 H264/H265 NAL 单元。
- 按照 RTP 打包规范(比如 H264 的 STAP-A、FU-A)分片或聚合。
- 添加 RTP 头部(序列号、时间戳、SSRC 等)。
- 交给传输层发送。
它的内部结构,我画了一张图:
4.2.2 VideoReceiveStream
接收端是发送端的逆过程。VideoReceiveStream 从网络收到 RTP 包后,需要:
- 解析 RTP 头部,提取序列号、时间戳。
- 重组 NAL 单元(如果是 FU-A 分片,需要等待所有分片到齐)。
- 处理丢包重传(NACK)和参考帧请求(PLI)。
- 将完整的 NAL 单元交给解码器。
避坑指南:我曾经在接收端遇到花屏问题,排查了很久。最后发现是 FU-A 分片重组时,没有正确处理分片顺序。H264 的 FU-A 头部有一个 Start 位和 End 位,必须严格按照这两个标志位来拼接。少一个分片,画面就花了。
4.3 编码器接口设计:如何让 H264 和 H265 无缝切换
WebRTC 的编码器接口设计得非常巧妙。它定义了一套抽象接口,所有编码器(H264、H265、VP8、VP9)都实现同一套 API。这样上层代码就不需要关心底层是哪种编码器。
核心接口是 VideoEncoder 类。它的主要方法如下:
| 方法 | 作用 | 个人经验 |
|---|---|---|
InitEncode() |
初始化编码器,传入分辨率、帧率、码率等参数 | H265 的初始化参数比 H264 多一个档次参数(tier),容易漏掉 |
Encode() |
输入原始视频帧,输出编码后的码流 | 注意 H264 和 H265 的 NAL 单元起始码不同:H264 是 0x00000001,H265 也是 0x00000001,但 NAL 单元头结构不同 |
RegisterEncodeCompleteCallback() |
注册编码完成回调,用于获取编码后的数据 | 回调中要注意线程安全,不要在回调里做耗时操作 |
SetRates() |
动态调整码率、帧率 | H265 的码率控制算法比 H264 更复杂,调整后需要等待几个 GOP 才能稳定 |
Release() |
释放编码器资源 | 一定要在 PeerConnection 关闭前调用,否则内存泄漏 |
下面是一个简化的编码器接口实现示例:
class MyH264Encoder : public VideoEncoder {
public:
int32_t InitEncode(const VideoCodec* codec_settings,
int32_t number_of_cores,
size_t max_payload_size) override {
// 初始化 H264 编码器
// 注意:codec_settings->codecType 必须设置为 kVideoCodecH264
// 设置 SPS/PPS 参数
return WEBRTC_VIDEO_CODEC_OK;
}
int32_t Encode(const VideoFrame& input_frame,
const std::vector<VideoFrameType>* frame_types) override {
// 将 VideoFrame 转换为编码器输入格式
// 调用底层编码库(如 x264、openh264)
// 输出编码后的数据,通过回调返回
return WEBRTC_VIDEO_CODEC_OK;
}
int32_t RegisterEncodeCompleteCallback(
EncodedImageCallback* callback) override {
callback_ = callback;
return WEBRTC_VIDEO_CODEC_OK;
}
int32_t SetRates(const RateControlParameters& parameters) override {
// 根据网络反馈调整码率
// 注意:H264 的码率控制有 CBR、VBR 模式
return WEBRTC_VIDEO_CODEC_OK;
}
int32_t Release() override {
// 释放编码器资源
return WEBRTC_VIDEO_CODEC_OK;
}
private:
EncodedImageCallback* callback_ = nullptr;
// 编码器内部状态
};
重要提醒:H264 和 H265 的编码器实现,在 InitEncode 阶段有一个关键区别——H265 需要额外设置档次(Main/Main10)和档次参数(MainTier/HighTier)。如果设置不对,解码端可能无法正确解码。我见过有人把 H265 的档次参数设成 H264 的,结果解码器直接崩溃。
4.4 编码器适配层:让 H264 和 H265 共存
在实际项目中,我们经常需要同时支持 H264 和 H265。WebRTC 通过一个适配层来解决这个问题。这个适配层会根据 SDP 协商结果,动态选择编码器。
适配层的核心逻辑:
- 收到远端 SDP 后,解析支持的编码器列表。
- 按照优先级(通常是 H265 > H264 > VP9 > VP8)选择第一个双方都支持的编码器。
- 创建对应的编码器实例(通过工厂模式)。
我个人习惯在适配层加一个日志输出,记录最终选择了哪种编码器。这样排查问题时,一眼就能看出是 H264 还是 H265 在跑。
好了,这一章的内容就到这里。视频引擎架构是 WebRTC 视频能力的基石,理解了 VideoEngine、SendStream、ReceiveStream 和编码器接口,后面讲编码适配时你会觉得轻松很多。