如何通过 WebRTC 传输预编码视频流

如何将外置编码器输出的预编译视频流交给 WebRTC 进行推流转发

场景

本人在参与公司自研图传服务的工作中,希望通过 WebRTC 将视频流推送到 SFU 服务器,在集成到已有的软件架构的过程中只能从 Deepstream pipeline 中获取到 H264 预编码的视频流,无法直接接入 WebRTC 原生的数据链路,因此我们需要使其具备直接使用预编码视频流进行推流的能力。

WebRTC Pipeline

如果要在 WebRTC 的数据链路上动手脚,我们必须稍微熟悉一下它(当然在实际开发中有更多的细节)。

简而言之,摄像头的原始数据由 Video Capturer 进行捕获,经由 Video Encoder 编码后进行 RTP 打包,最终通过 PeerConnection 发送出去。

这样看数据链路其实非常简单,接下来的问题就是如何将预编码的视频流放到这个 Pipeline 里面了,显然我们无法把预编码的视频流通过 Video Capturer 进行捕获,因为 Video Capturer 获取的都是 YUV 格式的原始数据,我们也不需要 Video Encoder 进行编码,因为原始数据已经是编码后的 H264 格式视频数据,我们希望的是直接将预编码的数据放到 Video Encoder 后面的流程中。

这个时候很容易产生一个符合直觉的想法,把预编码的视频流伪装成原始的 YUV 数据放到 Video Capturer 里面,在 Video Encoder 中再将它还原出来不就行了吗?乍一看非常贴合原生的 Pipeline,但是仔细想想实现过程中一定有非常多复杂的格式转换过程和影响性能的数据拷贝过程,一定有更好的方法。

其实我们要做的只是将整个 Pipeline 驱动起来即可,好比在水管的一端注水,另一端就能出水一样,WebRTC 内部使用了非常多的回调函数进行 Pipeline 的驱动,我们只要让它动起来即可。

将 Video Capturer 看作水管的一端,Video Encoder 看作水管的另一端,向 Video Capturer 中放入数据不就能驱动 Pipeline 了吗?我们并不关心放入的数据如何,因此只要创建黑帧即可,这样我们在 Video Encoder 中原本要对黑帧进行编码的阶段,将预编码的帧替换掉之前创建的黑帧就大工告成了。

实现

思路还是比较简单的,但是对于第一次接触 WebRTC 的人来说实现流程可没有那么简单。

上面我们主要讨论了如何将预编译的视频流放到 WebRTC Pipeline 中,而实现上绕不开的是 WebRTC 的连接流程,对于开发者来说,需要面对的就是 PeerConnection,而 PeerConnection 由 PeerConnectionFactory 创建,我们要做的第一步就是创建 PeerConnectionFactory。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
peer_connection_factory_ = webrtc::CreatePeerConnectionFactory(
      network_thread_.get(), worker_thread_.get(), signaling_thread_.get(),
      webrtc::AudioDeviceModule::Create(
          webrtc::AudioDeviceModule::kDummyAudio,
          webrtc::CreateDefaultTaskQueueFactory().get()),
      webrtc::CreateBuiltinAudioEncoderFactory(),
      webrtc::CreateBuiltinAudioDecoderFactory(),
      std::make_unique<webrtc::VideoEncoderFactoryTemplate<
          CustomH264EncoderTemplateAdapter>>(),
      std::make_unique<webrtc::VideoDecoderFactoryTemplate<
          webrtc::OpenH264DecoderTemplateAdapter>>(),
      nullptr /* audio_mixer */, nullptr /* audio_processing */);

其中 CustomH264EncoderTemplateAdapter 用于创建我们自定义的 Video Encoder,实际上就是告诉上层不同格式,具体用什么编码器,具体实现我放到后面再说。

使用 PeerConnectionFactory 创建完 PeerConnection 后,我们就要为连接添加视频轨道了,前面提到为了让 Pipeline 跑起来,我们需要在 Video Track 中放入黑帧,为此我们需要实现自己的 VideoTrackSource 类。

当预编译视频流产生视频帧时,我们需要创建一帧黑帧来推动 Pipeline,并将视频帧暂存起来以便 Video Encoder 的回调函数被执行时能拿到。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class DummyVideoTrackSource : public webrtc::VideoTrackSource {
 public:
  static rtc::scoped_refptr<DummyVideoTrackSource> Create(int id) {
    return rtc::make_ref_counted<DummyVideoTrackSource>(id);
  }

  void OnFrame(const char *data, int width, int height, int size,
               bool is_idr_frame) {
    // 暂存预编码的 H264 帧
    EncodedFrameQueue::Instance()->Push(id_, data, width, height, size,
                                        is_idr_frame);
    OnDummyFrame(width, height);
  }

 protected:
  DummyVideoTrackSource(int id) : VideoTrackSource(/*remote=*/false), id_(id) {}

 private:
  void OnDummyFrame(int width, int height) {
    // 构造黑帧用于触发下游编码器的 Encode 函数
    rtc::scoped_refptr<webrtc::I420Buffer> buffer =
        webrtc::I420Buffer::Create(width, height);
    webrtc::I420Buffer::SetBlack(buffer.get());
    uint64_t timestamp_us = rtc::TimeMicros();
    uint32_t rtp_timestamp = static_cast<uint32_t>(timestamp_us / 1000000 * 90);

    auto dummy_frame = webrtc::VideoFrame::Builder()
                           .set_id(id_)
                           .set_video_frame_buffer(buffer)
                           .set_timestamp_us(timestamp_us)
                           .set_rtp_timestamp(rtp_timestamp)
                           .build();

    broadcaster_.OnFrame(dummy_frame);
  }

  rtc::VideoSourceInterface<webrtc::VideoFrame> *source() override {
    return &broadcaster_;
  }

  int id_;
  rtc::VideoBroadcaster broadcaster_;
};

其中 VideoBroadcaster 就是那个被我们用来推动 Pipeline 的工具。

自定义 VideoTrackSource 实现完成之后我们通过 AddTrack 将其加入到 PeerConnection 中。

上面我们提到实现了自己的 CustomH264EncoderTemplateAdapter,当需要 H264 编码器的时候,就可以将编码器替换为我们自己实现的 PassThough 编码器,重点在于其中的 Encode 函数,当黑帧被放入 Pipeline 中时,Encode 函数在未来会被调用,在它被调用的时刻,我们取出在 VideoTrackSource 中暂存的 H264 帧,包装为 EncodedImage 后,交给下游。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class DummyH264Encoder : public webrtc::VideoEncoder {
 public:

  ......

  int32_t Encode(
      const webrtc::VideoFrame &frame,
      const std::vector<webrtc::VideoFrameType> *frame_types) override {
    if (!encoded_image_callback_) {
      return WEBRTC_VIDEO_CODEC_UNINITIALIZED;
    }

    EncodedFrameQueue::EncodedFrame encoded_frame =
        EncodedFrameQueue::Instance()->Pop(frame.id());
    auto buffer = webrtc::EncodedImageBuffer::Create(encoded_frame.data.data(),
                                                     encoded_frame.data.size());

    webrtc::EncodedImage encoded_image;
    encoded_image.SetEncodedData(buffer);
    encoded_image._encodedWidth = encoded_frame.width;
    encoded_image._encodedHeight = encoded_frame.height;
    encoded_image.capture_time_ms_ = frame.timestamp_us() / 1000;
    encoded_image.SetRtpTimestamp(frame.rtp_timestamp());
    encoded_image.SetColorSpace(frame.color_space());
    encoded_image._frameType = encoded_frame.is_idr_frame
                                   ? webrtc::VideoFrameType::kVideoFrameKey
                                   : webrtc::VideoFrameType::kVideoFrameDelta;
    h264_bitstream_parser_.ParseBitstream(encoded_image);
    encoded_image.qp_ = h264_bitstream_parser_.GetLastSliceQp().value_or(-1);

    webrtc::CodecSpecificInfo codec_specific;
    codec_specific.codecType = webrtc::kVideoCodecH264;
    codec_specific.codecSpecific.H264.packetization_mode =
        webrtc::H264PacketizationMode::NonInterleaved;
    codec_specific.codecSpecific.H264.temporal_idx = webrtc::kNoTemporalIdx;
    codec_specific.codecSpecific.H264.idr_frame = encoded_frame.is_idr_frame;
    codec_specific.codecSpecific.H264.base_layer_sync = false;

    encoded_image_callback_->OnEncodedImage(encoded_image, &codec_specific);

    if (!frame_types->empty() &&
        frame_types->back() == webrtc::VideoFrameType::kVideoFrameKey) {
      ExternalVideoEncoderCallbacks::Instance()->TriggerEncodeFrameInfoCallback(
          nullptr, frame.id(), 0, 0);
    }

    return WEBRTC_VIDEO_CODEC_OK;
  }

  webrtc::VideoEncoder::EncoderInfo GetEncoderInfo() const override {
    // 关闭码流控制
    webrtc::VideoEncoder::EncoderInfo info;
    info.scaling_settings = webrtc::VideoEncoder::ScalingSettings::kOff;
    info.supports_native_handle = false;
    info.implementation_name = "DummyH264";
    info.has_trusted_rate_controller = true;
    info.is_hardware_accelerated = true;
    info.supports_simulcast = false;
    info.preferred_pixel_formats = {webrtc::VideoFrameBuffer::Type::kI420};
    info.is_qp_trusted = true;
    return info;
  }

 private:
  webrtc::H264BitstreamParser h264_bitstream_parser_;
  webrtc::EncodedImageCallback *encoded_image_callback_;
};

这边省略了部分代码,重点在于 Encode 函数,我们不需要关心 VideoFrame,也就是我们创建的黑帧,我们在这里将 H264 视频帧传给下游即可。

这里需要注意的是:

  1. 在实际测试过程中如果不关闭码流控制,由非常大的丢帧风险,由于黑帧需求的码率和我们实际需求的码率是有很大差别的,因此在这里我们选择关闭码流控制以规避这个问题,后续码率控制通过直接通知外置编码器实现。
  2. VideoFrameType 是我们需要关心的,当用户拉流时,SFU 服务器会要求推流端发送 IDR 帧以便以快速出图,因此我们需要将这个信息同步给外置编码器。
  3. 在析构时,需要注意析构顺序为 pc->pcf->thread,并且需要手动将指针赋值为 nullptr 以触发其内部的析构逻辑。

这里没有对 PeerConnection 的 SDP 协商做描述,因为这涉及到业务所以暂且省略,将目光集中于预编译视频流与 WebRTC 本身的实现上。

参考

  1. TzuHuanTai/RaspberryPi-WebRTC
  2. WebRTC
  3. C/C++ 现场设备 SDK API
  4. PeerConnection 连接流程
updatedupdated2025-10-302025-10-30