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 包,并发送到网络。它的工作流程大致如下:

  1. 从编码器拿到编码后的 H264/H265 NAL 单元。
  2. 按照 RTP 打包规范(比如 H264 的 STAP-A、FU-A)分片或聚合。
  3. 添加 RTP 头部(序列号、时间戳、SSRC 等)。
  4. 交给传输层发送。

它的内部结构,我画了一张图:

VideoSendStream 内部结构 编码器 (H264/H265) NAL 单元 RTP 打包器 (STAP-A / FU-A) RTP 包 发送队列 网络 说明: H264 的 NAL 单元类型包括 IDR、非 IDR、SPS、PPS 等。 RTP 打包器根据 NAL 单元大小选择 STAP-A(聚合)或 FU-A(分片)。

4.2.2 VideoReceiveStream

接收端是发送端的逆过程。VideoReceiveStream 从网络收到 RTP 包后,需要:

  1. 解析 RTP 头部,提取序列号、时间戳。
  2. 重组 NAL 单元(如果是 FU-A 分片,需要等待所有分片到齐)。
  3. 处理丢包重传(NACK)和参考帧请求(PLI)。
  4. 将完整的 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 和编码器接口,后面讲编码适配时你会觉得轻松很多。