媒体流的发送(四): 从摄像头采集到 RTP包发出

0 阅读14分钟

前几篇我们分析了信令层的 SDP 交换,本篇进入媒体层,聚焦发送端:一帧视频图像从摄像头采集出来,到最终作为 RTP 包离开本机,libwebrtc 内部经历了什么。


一、整体链路全景

摄像头硬件 / 软件 Capturer
    │ OnFrame(VideoFrame)
    ▼
rtc::VideoBroadcaster::OnFrame()         [广播给所有 Sink]
    │
    ▼
VideoTrackSource → VideoTrack            [pc 层封装]
    │ Sink 已注册:VideoStreamEncoder
    ▼
VideoStreamEncoder::OnFrame()            [video/video_stream_encoder.cc]
    │ 时间戳修正 + 投递 EncoderQueue
    ▼
EncoderQueue (独立线程)
    │
    ▼
MaybeEncodeVideoFrame()                  [码率/帧率控制、丢帧]
    │
    ▼
EncodeVideoFrame()
    │ encoder_->Encode()                 [VP8/VP9/H264/AV1 实际编码]
    ▼
OnEncodedImage()                         [编码器回调]
    │ sink_->OnEncodedImage()
    ▼
VideoSendStreamImpl::OnEncodedImage()    [video/video_send_stream_impl.cc]
    │
    ▼
RtpVideoSender::OnEncodedImage()         [call/rtp_video_sender.cc]
    │ 多流路由(Simulcast/SVC)
    ▼
RTPSenderVideo::SendVideo()              [modules/rtp_rtcp/source/rtp_sender_video.cc]
    │ RtpPacketizer 分包 + 扩展头写入
    │ AssignSequenceNumbers()
    ▼
PacedSender::EnqueuePackets()            [modules/pacing/paced_sender.cc]
    │ 按码率平滑出队
    ▼
PacketRouter::SendPacket()               [modules/pacing/packet_router.cc]
    │ TrySendPacket() → DTLS-SRTP 加密
    ▼
UDP Socket → 网络

二、第一段:采集 → VideoTrack

2.1 VideoBroadcaster:帧的广播中心

底层 Capturer(摄像头)采集到帧后,调用 VideoBroadcaster::OnFrame() 向所有已注册的 Sink 广播。VideoBroadcaster 继承自 VideoSourceBase,内部维护一个 sinks_ 列表。

// media/base/video_broadcaster.cc
void VideoBroadcaster::OnFrame(const webrtc::VideoFrame& frame) {
  webrtc::MutexLock lock(&sinks_and_wants_lock_);
  for (auto& sink_pair : sink_pairs()) {
    if (sink_pair.wants.black_frames) {
      // track 被 disable 时,给该 Sink 发黑帧而非原始帧
      webrtc::VideoFrame black_frame = ...;
      sink_pair.sink->OnFrame(black_frame);
    } else {
      sink_pair.sink->OnFrame(frame);   // 正常推帧给所有 Sink
    }
  }
}

VideoBroadcaster 还有一个关键能力 UpdateWants():每当有新 Sink 注册或注销时,它会把所有 Sink 的 VideoSinkWants 取最小值(分辨率取最小、帧率取最小)合并成一个 current_wants_,反馈给 Capturer,让 Capturer 按最保守的需求采集,避免资源浪费。

// media/base/video_broadcaster.cc
void VideoBroadcaster::UpdateWants() {
  VideoSinkWants wants;
for (auto& sink : sink_pairs()) {
    // max_pixel_count == MIN(所有 sink 的 max_pixel_count)
    if (sink.wants.max_pixel_count < wants.max_pixel_count)
      wants.max_pixel_count = sink.wants.max_pixel_count;
    // max_framerate_fps == MIN(所有 sink 的 max_framerate_fps)
    if (sink.wants.max_framerate_fps < wants.max_framerate_fps)
      wants.max_framerate_fps = sink.wants.max_framerate_fps;
    // resolution_alignment == LCM(所有 sink 的 resolution_alignment)
    wants.resolution_alignment = cricket::LeastCommonMultiple(
        wants.resolution_alignment, sink.wants.resolution_alignment);
  }
  current_wants_ = wants;
}

2.2 VideoTrackSource:中间转发层

VideoTrackSource 是对底层 Capturer(rtc::VideoSourceInterface)的封装,实现了 VideoTrackSourceInterface。它的 AddOrUpdateSink 只做一件事:把调用原封不动地透传给底层 source()(即真实 Capturer / VideoBroadcaster):

// pc/video_track_source.cc
void VideoTrackSource::AddOrUpdateSink(
    rtc::VideoSinkInterface<VideoFrame>* sink,
    const rtc::VideoSinkWants& wants) {
  RTC_DCHECK(worker_thread_checker_.IsCurrent());
  source()->AddOrUpdateSink(sink, wants);  // 直接透传,不做任何处理
}

void VideoTrackSource::RemoveSink(rtc::VideoSinkInterface<VideoFrame>* sink) {
  RTC_DCHECK(worker_thread_checker_.IsCurrent());
  source()->RemoveSink(sink);              // 同样透传
}

source() 返回的是底层真实 Capturer,通常就是一个 VideoBroadcaster。因此 Sink 最终是被注册到 VideoBroadcaster 的 sinks_ 列表里的。

2.3 VideoTrack:双层 Sink 管理

VideoTrack 是 W3C MediaStreamTrack 的 C++ 实现,它持有 video_source_(即 VideoTrackSource)。在 Sink 注册这件事上,VideoTrack 做了两层操作

// pc/video_track.cc
void VideoTrack::AddOrUpdateSink(rtc::VideoSinkInterface<VideoFrame>* sink,
                                 const rtc::VideoSinkWants& wants) {
  RTC_DCHECK_RUN_ON(worker_thread_);

// 第一层:在 VideoSourceBaseGuarded 里记录一份 sink 列表(VideoTrack 自己持有)
  VideoSourceBaseGuarded::AddOrUpdateSink(sink, wants);

// 第二层:构造 modified_wants,根据 track 是否 enabled 决定是否发黑帧
  rtc::VideoSinkWants modified_wants = wants;
  modified_wants.black_frames = !enabled();  // track.enabled=false 时发黑帧

// 把 Sink 透传给底层 VideoTrackSource → VideoBroadcaster
  video_source_->AddOrUpdateSink(sink, modified_wants);
}

两层记录的原因

| 层级 | 存储位置 | 用途 | | --- | --- | --- | | VideoSourceBaseGuarded::sinks_ | VideoTrack 自身 | 当 set_enabled() 状态变化时,遍历所有 Sink 更新 black_frames 标志 | | VideoBroadcaster::sinks_ | 底层 Capturer | 实际帧推送的目标列表,OnFrame 时遍历 |

set_enabled() 的处理印证了这一点:

// pc/video_track.cc
bool VideoTrack::set_enabled(bool enable) {
  RTC_DCHECK_RUN_ON(worker_thread_);
  // 遍历 VideoTrack 自己记录的 sink 列表
  for (auto& sink_pair : sink_pairs()) {
    rtc::VideoSinkWants modified_wants = sink_pair.wants;
    modified_wants.black_frames = !enable;  // 用新的 enable 状态更新 black_frames
    // 重新通知底层 VideoBroadcaster 更新该 Sink 的 wants
    video_source_->AddOrUpdateSink(sink_pair.sink, modified_wants);
  }
  return MediaStreamTrack<VideoTrackInterface>::set_enabled(enable);
}

三、第二段:MediaChannel → VideoStreamEncoder

3.1 VideoRtpSender::SetSend —— Track 与 MediaChannel 的绑定

SetLocalDescription 完成后,VideoRtpSender::SetSend() 被调用,这是 pc 层连接 Track 与底层媒体引擎的关键函数:

// pc/rtp_sender.cc
void VideoRtpSender::SetSend() {
  RTC_DCHECK(!stopped_);
  RTC_DCHECK(can_send_track());
if (!media_channel_) {
    RTC_LOG(LS_ERROR) << "SetVideoSend: No video channel exists.";
    return;
  }
  cricket::VideoOptions options;
  VideoTrackSourceInterface* source = video_track()->GetSource();
if (source) {
    options.is_screencast = source->is_screencast();          // 是否屏幕共享
    options.video_noise_reduction = source->needs_denoising(); // 是否需要降噪
  }
  options.content_hint = cached_track_content_hint_;
// 切换到 worker_thread 调用 SetVideoSend
bool success = worker_thread_->Invoke<bool>(RTC_FROM_HERE, [&] {
    return video_media_channel()->SetVideoSend(ssrc_, &options, video_track());
    //                                         ↑SSRC   ↑编码选项  ↑VideoTrack 作为 Source
  });
  RTC_DCHECK(success);
}

video_track() 作为 VideoSourceInterface 被传入 SetVideoSend,后续媒体引擎会通过它的 AddOrUpdateSink() 把 VideoStreamEncoder 注册进来。

3.2 VideoSourceSinkController::SetSource —— 编码器注册为 Sink

VideoStreamEncoder 通过内部的 VideoSourceSinkController 完成 Sink 注册。SetSource 的逻辑非常清晰:

// video/video_source_sink_controller.cc
void VideoSourceSinkController::SetSource(
    rtc::VideoSourceInterface<VideoFrame>* source) {
  RTC_DCHECK_RUN_ON(&sequence_checker_);

  rtc::VideoSourceInterface<VideoFrame>* old_source = source_;
  source_ = source;

// 先把自己从旧 Source 里移除
if (old_source != source && old_source)
    old_source->RemoveSink(sink_);   // sink_ 就是 VideoStreamEncoder

if (!source)
    return;

// 把自己(VideoStreamEncoder)注册到新 Source(VideoTrack)
  source->AddOrUpdateSink(sink_, CurrentSettingsToSinkWants());
//      ↑VideoTrack       ↑VideoStreamEncoder    ↑分辨率/帧率限制
}

CurrentSettingsToSinkWants() 把当前的编码约束(最大分辨率、最大帧率、对齐要求等)转换成 VideoSinkWants,让 VideoBroadcaster 的 UpdateWants() 能感知到编码器的诉求,进而反馈给 Capturer 调整采集参数。


3.3、Sink 注册完整关系图

以下是从 addTrack 到编码器注册完成的完整 Sink 注册路径:

应用层调用:
  pc->addTrack(local_video_track, ...)
  local_video_track->AddOrUpdateSink(local_sink_, wants)  ← 本地预览 Sink 注册

pc->SetLocalDescription(offer) 触发:
  VideoRtpSender::SetSend()
    └─ video_media_channel()->SetVideoSend(ssrc_, options, video_track())
          └─ WebRtcVideoSendStream::SetVideoSend()
                └─ VideoSendStream::SetSource(video_track, degradation_pref)
                      └─ VideoStreamEncoder::SetSource(video_track, ...)
                            └─ VideoSourceSinkController::SetSource(video_track)
                                  └─ video_track->AddOrUpdateSink(VideoStreamEncoder, wants)
                                         ↓
                                  VideoTrack::AddOrUpdateSink()
                                    ├─ VideoSourceBaseGuarded::AddOrUpdateSink()  ← 记录在 VideoTrack.sinks_
                                    └─ video_source_->AddOrUpdateSink()           ← 透传
                                         ↓
                                  VideoTrackSource::AddOrUpdateSink()
                                    └─ source()->AddOrUpdateSink()                ← 再透传
                                         ↓
                                  VideoBroadcaster::AddOrUpdateSink()
                                    ├─ sinks_.push_back({VideoStreamEncoder, wants})  ← 最终注册
                                    └─ UpdateWants()  ← 合并所有 Sink wants,反馈给 Capturer

注册完成后,VideoBroadcaster::sinks_ 里同时存在:

| Sink | 注册时机 | 用途 | | --- | --- | --- | | local_sink_ (本地预览) | 应用层手动调用 AddOrUpdateSink | 本地显示原始帧 | | VideoStreamEncoder | SetLocalDescription  完成后自动注册 | 编码后发送 |

Capturer 每次 OnFrame 时,VideoBroadcaster 会同时把帧推给所有 Sink,两条链路完全独立互不干扰。

第四章:编码帧如何变成 RTP 包发出去

核心源码文件:

  • video/video_stream_encoder.cc

  • video/video_send_stream_impl.cc

  • call/rtp_video_sender.cc

  • modules/rtp_rtcp/source/rtp_sender_video.cc

  • modules/rtp_rtcp/source/rtp_format_h264.cc

  • modules/pacing/paced_sender.cc

  • modules/pacing/packet_router.cc


4.1 编码完成回调:VideoStreamEncoder::OnEncodedImage

x264(或硬件编码器)编码完一帧后,通过回调进入:

// video/video_stream_encoder.cc:1825
EncodedImageCallback::Result VideoStreamEncoder::OnEncodedImage(
    const EncodedImage& encoded_image,
    const CodecSpecificInfo* codec_specific_info) {
// 填充时序元数据(encode timing、QP 信息)
  frame_encode_metadata_writer_.FillTimingInfo(spatial_idx, &image_copy);
  frame_encode_metadata_writer_.UpdateBitstream(codec_specific_info, &image_copy);

// 如果 encoder 没有提供 QP,libwebrtc 自行解析码流
if (image_copy.qp_ < 0 && qp_parsing_allowed_) {
    image_copy.qp_ = qp_parser_.Parse(...).value_or(-1);
  }

// 转发给 sink_(即 VideoSendStreamImpl)
  EncodedImageCallback::Result result =
      sink_->OnEncodedImage(image_copy, codec_specific_info);  // line:1912
}

sink_ 是 VideoSendStreamImpl,完成从编码线程到发送链路的交接。


4.2 VideoSendStreamImpl::OnEncodedImage:转发至 RtpVideoSender

// video/video_send_stream_impl.cc:539
EncodedImageCallback::Result VideoSendStreamImpl::OnEncodedImage(
    const EncodedImage& encoded_image,
    const CodecSpecificInfo* codec_specific_info) {
  // 标记编码器仍然活跃,防止 padding 被误关闭
  activity_ = true;
  // 转发给 RtpVideoSender
  result = rtp_video_sender_->OnEncodedImage(encoded_image, codec_specific_info);
}

4.3 RtpVideoSender::OnEncodedImage:Simulcast 路由 + RTP 时间戳计算

// call/rtp_video_sender.cc:517
EncodedImageCallback::Result RtpVideoSender::OnEncodedImage(
    const EncodedImage& encoded_image,
    const CodecSpecificInfo* codec_specific_info) {

// 根据 SpatialIndex 路由到对应的 Simulcast 流
size_t stream_index = 0;
if (codec_specific_info &&
      (codec_specific_info->codecType == kVideoCodecVP8 ||
       codec_specific_info->codecType == kVideoCodecH264 ||
       codec_specific_info->codecType == kVideoCodecGeneric)) {
    stream_index = encoded_image.SpatialIndex().value_or(0);  // line:534
  }

// RTP Timestamp = 编码帧 timestamp + 该流的起始偏移(StartTimestamp)
uint32_t rtp_timestamp =
      encoded_image.Timestamp() +
      rtp_streams_[stream_index].rtp_rtcp->StartTimestamp();  // line:538-540

// 调用对应流的 RTPSenderVideo 完成打包
bool send_result = rtp_streams_[stream_index].sender_video->SendEncodedImage(
      rtp_config_.payload_type, codec_type_, rtp_timestamp, encoded_image,
      params_[stream_index].GetRtpVideoHeader(...),
      expected_retransmission_time_ms);  // line:578-582
}

关键点:

  • stream_index 对应 Simulcast 不同分辨率层,每层有独立的 RTPSenderVideo 和 SSRC;

  • rtp_timestamp 在 90kHz 时钟下,最终写入 RTP 包头。


4.4 RTPSenderVideo::SendVideo:构造 RTP 包模板 + 填充头信息

核心入口 modules/rtp_rtcp/source/rtp_sender_video.cc:459

Step 1:分配 RTP 包模板

由于一帧会被拆成多个 RTP 包,首包/中间包/尾包携带的 Header Extension 不同,预先分配 4 个包模板:

// rtp_sender_video.cc:517-550
std::unique_ptr<RtpPacketToSend> single_packet = rtp_sender_->AllocatePacket();
auto first_packet  = std::make_unique<RtpPacketToSend>(*single_packet);
auto middle_packet = std::make_unique<RtpPacketToSend>(*single_packet);
auto last_packet   = std::make_unique<RtpPacketToSend>(*single_packet);

AllocatePacket() 分配时已写入 SSRCCreateOffer 阶段由 UniqueRandomIdGenerator 生成,绑定到该 RTPSender)。

Step 2:写入 RTP 基础字段

// rtp_sender_video.cc:520-522
single_packet->SetPayloadType(payload_type);   // PT:SDP 协商的 H264 编码 PT
single_packet->SetTimestamp(rtp_timestamp);    // 90kHz RTP 时间戳
single_packet->set_capture_time_ms(capture_time_ms);

RTP Timestamp 换算路径video_stream_encoder.cc:1260):

const int kMsToRtpTimestamp = 90;
incoming_frame.set_timestamp(
    kMsToRtpTimestamp * static_cast<uint32_t>(incoming_frame.ntp_time_ms()));

视频使用 90kHz 时钟,1 ms = 90 ticks。

Step 3:差异化填充 RTP Header Extension

调用 AddRtpHeaderExtensionsrtp_sender_video.cc:304),按包位置区别填充:

| Extension | 填充包位置 | 触发条件 | 作用 | | --- | --- | --- | --- | | VideoOrientation | last_packet | 方向变化或关键帧 | 告知接收端旋转角度 | | VideoTimingExtension | last_packet | timing.flags 有效 | 端到端时延打点 | | PlayoutDelayLimits | 所有包 | playout_delay_pending_  为 true | 接收端播放延迟控制 | | AbsoluteCaptureTimeExtension | first_packet | 始终填充 | 绝对采集时间(多流同步) | | ColorSpaceExtension | last_packet | 颜色空间变化或关键帧 | HDR/SDR 元数据 | | RtpDependencyDescriptorExtension | first/last packet | SVC/VP9 模式 | 帧依赖关系描述 |

Step 4:计算 Payload Size Limits

// rtp_sender_video.cc:556-569
RtpPacketizer::PayloadSizeLimits limits;
limits.max_payload_len            = packet_capacity - middle_packet->headers_size();
limits.single_packet_reduction_len = single_packet->headers_size() - middle_packet->headers_size();
limits.first_packet_reduction_len  = first_packet->headers_size()  - middle_packet->headers_size();
limits.last_packet_reduction_len   = last_packet->headers_size()   - middle_packet->headers_size();

首尾包因携带更多 Extension,可用 Payload 空间比中间包小,limits 精确描述这种差异,供 Packetizer 均等切片时使用。


4.5 H264 NALU 解析与 RFC 3984 三种打包模式

分配完模板后,计算 PayloadSizeLimits(留出 Header Extension 占用的空间),然后调用:

// rtp_sender_video.cc:615-616
std::unique_ptr<RtpPacketizer> packetizer =
    RtpPacketizer::Create(codec_type, payload, limits, video_header);

工厂方法 RtpPacketizer::Create(rtp_format.cc:28)根据编码类型实例化对应 Packetizer:

// rtp_format.cc:40-44
case kVideoCodecH264: {
  const auto& h264 = absl::get<RTPVideoHeaderH264>(rtp_video_header.video_type_header);
  return std::make_unique<RtpPacketizerH264>(payload, limits, h264.packetization_mode);
}

RtpPacketizerH264 构造阶段(rtp_format_h264.cc:48)

首先用 H264::FindNaluIndices 从裸码流中找出每一个 NALU 的边界(通过 start code 0x00 0x00 0x00 0x01 或 0x00 0x00 0x01 定位),填充 input_fragments_:

// rtp_format_h264.cc:56-60
for (const auto& nalu : H264::FindNaluIndices(payload.data(), payload.size())) {
  input_fragments_.push_back(
      payload.subview(nalu.payload_start_offset, nalu.payload_size));
}

FindNaluIndices 扫描 0x00 0x00 0x01 或 0x00 0x00 0x00 0x01 start code,切割出 SPS、PPS、SEI、IDR slice 等独立 NALU。

随后调用 GeneratePackets(rtp_format_h264.cc:79),对每个 NALU 按以下策略打包:

NALU 大小 > MTU 剩余容量  →  FU-A 分片
NALU 大小 ≤ MTU 且可聚合  →  STAP-A 聚合
NALU 大小 ≤ MTU 且只有一个 →  Single NAL Unit

1. FU-A 分片(rtp_format_h264.cc:111)

大 NALU(如 IDR 帧)超出单包 MTU 时,剥去原始 NAL header,均分为多个片段。每个 FU-A 包头格式:

Byte 0: FU Indicator = (F|NRI) | 0x1C (FU-A type)
Byte 1: FU Header   = S(start) | E(end) | R | nal_type
// rtp_format_h264.cc:298-312
uint8_t fu_indicator = (packet->header & (kFBit | kNriMask)) | H264::NaluType::kFuA;
uint8_t fu_header = 0;
fu_header |= (packet->first_fragment ? kSBit : 0);  // 第一片置 S 位
fu_header |= (packet->last_fragment ? kEBit : 0);   // 最后一片置 E 位
fu_header |= type;  // 原始 NAL type

2. STAP-A 聚合(rtp_format_h264.cc:159)

多个小 NALU(如 SPS + PPS + SEI)可以聚合进一个 RTP 包,每个 NALU 前加 2 字节长度前缀:

// rtp_format_h264.cc:270-281
buffer[0] = (packet->header & (kFBit | kNriMask)) | H264::NaluType::kStapA;
// 依次写入: [2字节长度][NALU数据][2字节长度][NALU数据]...
ByteWriter<uint16_t>::WriteBigEndian(&buffer[index], fragment.size());
memcpy(&buffer[index], fragment.data(), fragment.size());

Marker bit 标记帧结束 每帧的最后一个 RTP 包,NextPacket 会置 Marker bit:

// rtp_format_h264.cc:256
rtp_packet->SetMarker(packets_.empty());  // 最后一包 Marker=1

接收端靠 Marker bit 判断"一帧数据已全部到达",触发解码。

三种打包策略决策

// rtp_format_h264.cc:79-108
bool RtpPacketizerH264::GeneratePackets(H264PacketizationMode packetization_mode) {
  for (size_t i = 0; i < input_fragments_.size();) {
    if (fragment_len > single_packet_capacity)
      PacketizeFuA(i++);      // 大 NALU → FU-A 分片
    else
      i = PacketizeStapA(i);  // 小 NALU → STAP-A 聚合
  }
}

4.5.1 FU-A 分片(大 NALU,如 IDR 帧)

当一个 NALU 超过 MTU 可用空间时,PacketizeFuArtp_format_h264.cc:111)将其切分:

// rtp_format_h264.cc:136-156
// 剥去原始 1 字节 NAL header,均等切分 payload
limits.max_payload_len -= kFuAHeaderSize;  // kFuAHeaderSize = 2
size_t payload_left = fragment.size() - kNalHeaderSize;
std::vector<int> payload_sizes = SplitAboutEqually(payload_left, limits);

for (size_t i = 0; i < payload_sizes.size(); ++i) {
  packets_.push(PacketUnit(fragment.subview(offset, packet_length),
                           /*first_fragment=*/i == 0,
                           /*last_fragment=*/i == payload_sizes.size() - 1,
                           false, fragment[0]));  // fragment[0] 保存原始 NAL header
}

NextFragmentPacketrtp_format_h264.cc:293)写入 FU-A 包头:

// rtp_format_h264.cc:298-312
uint8_t fu_indicator = (packet->header & (kFBit | kNriMask)) | H264::NaluType::kFuA;
uint8_t fu_header = 0;
fu_header |= (packet->first_fragment ? kSBit : 0);  // 起始片:S=1
fu_header |= (packet->last_fragment  ? kEBit : 0);  // 结束片:E=1
fu_header |= type;  // 原始 NAL type(如 5=IDR, 1=non-IDR)
buffer[0] = fu_indicator;
buffer[1] = fu_header;
memcpy(buffer + kFuAHeaderSize, fragment.data(), fragment.size());

FU-A 包结构:

+---------------+---------------+-------------------------------+
| FU indicator  |   FU Header   |        NAL 数据片段            |
| F|NRI|0x1C    | S|E|R|NALtype |   原始 NALU payload 的一部分   |
+---------------+---------------+-------------------------------+

4.5.2 STAP-A 聚合(多个小 NALU,如 SPS + PPS + SEI)

NextAggregatePacketrtp_format_h264.cc:261)将多个小 NALU 聚合进一包:

// rtp_format_h264.cc:270-281
buffer[0] = (packet->header & (kFBit | kNriMask)) | H264::NaluType::kStapA;
// 依次写入:[2字节长度][NALU数据][2字节长度][NALU数据]...
ByteWriter<uint16_t>::WriteBigEndian(&buffer[index], fragment.size());
memcpy(&buffer[index], fragment.data(), fragment.size());

STAP-A 包结构:

+---------------+------+-----------+------+-----------+
| STAP-A HDR    | Size |  NALU 1   | Size |  NALU 2   |
| F|NRI|0x18    |  2B  |    ...    |  2B  |    ...    |
+---------------+------+-----------+------+-----------+

4.5.3 Marker bit:标记帧边界

// rtp_format_h264.cc:256
rtp_packet->SetMarker(packets_.empty());  // 包队列为空 → 这是帧的最后一包

接收端通过 Marker bit 判断"一帧的全部 RTP 包已到达",触发解码器。


4.6 序列号分配

分包完成后统一分配序列号:

// rtp_sender_video.cc:700
if (!rtp_sender_->AssignSequenceNumbersAndStoreLastPacketState(rtp_packets)) {
  return false;
}
LogAndSendToNetwork(std::move(rtp_packets), payload.size());

序列号在 RTPSender 内以原子方式递增(16-bit,0xFFFF 后回绕到 0)。同一帧的多个包连续递增,接收端靠序列号检测丢包和乱序。

完整 RTP 包头结构:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X|  CC   |M|     PT      |       Sequence Number         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Timestamp(90kHz 时钟)                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           SSRC                                 |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|        RTP Header Extensions(one-byte / two-byte 格式)        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   H264 Payload(Single NAL / FU-A 分片 / STAP-A 聚合)         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| RTP 字段 | 来源 | 代码位置 | | --- | --- | --- | | PT | SDP 协商的 H264 payload type | rtp_sender_video.cc:520 | | Sequence Number | RTPSender  原子递增,16-bit 回绕 | rtp_sender_video.cc:700 | | Timestamp | 采集时间 ms × 90(90kHz 时钟) | video_stream_encoder.cc:1261 | | SSRC | CreateOffer  阶段 UniqueRandomIdGenerator 生成 | AllocatePacket()  时写入 | | Marker bit | 帧最后一包为 1 | rtp_format_h264.cc:256 |


4.7 PacedSender:令牌桶平滑发送,防止 burst

LogAndSendToNetworkrtp_sender_video.cc:190)将包交给 PacedSender

// rtp_sender_video.cc:210
rtp_sender_->EnqueuePackets(std::move(packets));

进入 PacedSender::EnqueuePacketspaced_sender.cc:115):

// paced_sender.cc:115-131
void PacedSender::EnqueuePackets(
    std::vector<std::unique_ptr<RtpPacketToSend>> packets) {
  MutexLock lock(&mutex_);
  for (auto& packet : packets) {
    pacing_controller_.EnqueuePacket(std::move(packet));  // 入队
  }
  MaybeWakupProcessThread();  // 唤醒 Pacer 处理线程
}

PacingController 内部按优先级维护多个发送队列:

| 优先级 | 包类型 | 说明 | | --- | --- | --- | | 最高 | 音频包 | 延迟敏感,优先发送 | | 高 | 视频关键帧 | IDR 帧不可丢 | | 中 | 普通视频包 | P/B 帧 | | 低 | RTX 重传包 | 丢包重传 | | 最低 | Padding 包 | 带宽探测填充 |

令牌桶机制:

  • 令牌按 BWE(带宽估计)下发的目标码率匀速补充;

  • ProcessPacketspaced_sender.cc:183)由专属线程定时驱动,每次消耗令牌发送数据;

  • 若 TWCC 反馈拥塞,BWE 降低目标码率,Pacer 自动缓存包减缓发送速率。


4.8 PacketRouter:注入 TransportCC 序列号,交给 DTLS-SRTP

Pacer 放行后调用 PacketRouter::SendPacketpacket_router.cc:131):

// packet_router.cc:131-168
void PacketRouter::SendPacket(std::unique_ptr<RtpPacketToSend> packet,
                              const PacedPacketInfo& cluster_info) {
MutexLock lock(&modules_mutex_);

// 注入 TransportSequenceNumber(全局递增,所有流共用同一计数器)
if (packet->HasExtension<TransportSequenceNumber>()) {
    packet->SetExtension<TransportSequenceNumber>((++transport_seq_) & 0xFFFF);  // line:141
  }

// 按 SSRC 找到对应的 RtpRtcpInterface 模块
uint32_t ssrc = packet->Ssrc();
  RtpRtcpInterface* rtp_module = send_modules_map_.find(ssrc)->second;

// 交给 DTLS-SRTP 加密,最终通过 UDP 发出
  rtp_module->TrySendPacket(packet.get(), cluster_info);  // line:155

// 收集本次触发产生的 FEC 包
for (auto& fec_pkt : rtp_module->FetchFecPackets()) {
    pending_fec_packets_.push_back(std::move(fec_pkt));
  }
}

TransportSequenceNumber 的意义:

  • 与 RTP 序列号不同,它是跨所有媒体流(含音频)共用的递增序号;

  • 对端通过 TWCC RTCP 反馈每包的到达时间,libwebrtc 据此估计链路带宽,回调给 BWE → Pacer 形成拥塞控制闭环。

TrySendPacket 进入 DTLS-SRTP 层,由 SrtpTransport 对 RTP 载荷加密,最终通过 P2PTransportChannel 选定的 ICE candidate pair,以 UDP 发出。


4.9 完整数据流总结

encoder_->Encode(VideoFrame)
  │  编码完成
  ▼
VideoStreamEncoder::OnEncodedImage          // video_stream_encoder.cc:1825
  │  填充 timing/QP 元数据,转发给 sink_
  ▼
VideoSendStreamImpl::OnEncodedImage         // video_send_stream_impl.cc:539
  │  标记 activity_,转发给 rtp_video_sender_
  ▼
RtpVideoSender::OnEncodedImage              // call/rtp_video_sender.cc:517
  │  按 SpatialIndex 路由到对应 Simulcast 流
  │  rtp_timestamp = encoded.Timestamp() + StartTimestamp()
  ▼
RTPSenderVideo::SendVideo                   // rtp_sender_video.cc:459
  ├─ AllocatePacket()        → 写入 SSRC(CreateOffer 阶段生成)
  ├─ SetPayloadType()        → PT(SDP 协商值)
  ├─ SetTimestamp()          → 90kHz RTP 时间戳
  ├─ AddRtpHeaderExtensions() → 差异化填充各包 Extension
  ├─ RtpPacketizer::Create() → 工厂实例化 RtpPacketizerH264
  │     ├─ FindNaluIndices()  → 按 start code 切割 NALU 列表
  │     ├─ FU-A              → 大 NALU 均等分片(S/E 标志位标识首尾)
  │     ├─ STAP-A            → 多小 NALU 聚合(2 字节长度前缀)
  │     └─ SetMarker()        → 帧最后一包 Marker=1
  └─ AssignSequenceNumbers() → RTP 序列号原子递增
  │
  ▼  LogAndSendToNetwork
PacedSender::EnqueuePackets                 // paced_sender.cc:115
  │  令牌桶限速,按优先级排队,防 burst
  ▼
PacketRouter::SendPacket                    // packet_router.cc:131
  │  注入 TransportSequenceNumber(TWCC 用)
  │  按 SSRC 找到 RtpRtcpInterface 模块
  ▼
RtpRtcpInterface::TrySendPacket
  │  DTLS-SRTP 加密(SrtpTransport)
  ▼
P2PTransportChannel → UDP Socket → 网络

| 步骤 | 核心动作 | 关键代码位置 | | --- | --- | --- | | OnEncodedImage | 填充 timing/QP,转发给发送链 | video_stream_encoder.cc:1825 | | Simulcast 路由 | 按 SpatialIndex 选 stream | rtp_video_sender.cc:534 | | RTP Timestamp | NTP ms × 90(90kHz 时钟) | video_stream_encoder.cc:1261 | | SSRC 写入 | CreateOffer  阶段生成,AllocatePacket 写入 | rtp_sender_video.cc:517 | | PT 写入 | SDP 协商的 H264 payload type | rtp_sender_video.cc:520 | | NALU 切割 | FindNaluIndices  按 start code 定位 | rtp_format_h264.cc:56 | | FU-A 分片 | 大 NALU 均等切片,S/E 标志位标识首尾 | rtp_format_h264.cc:111 | | STAP-A 聚合 | 多小 NALU 塞一包,2 字节长度前缀 | rtp_format_h264.cc:159 | | Marker bit | 帧最后一包置 1 | rtp_format_h264.cc:256 | | 序列号分配 | RTPSender  原子递增,16-bit 回绕 | rtp_sender_video.cc:700 | | PacedSender | 令牌桶平滑发送,防 burst | paced_sender.cc:115 | | TransportCC SN | 全局递增,供 TWCC 带宽估计 | packet_router.cc:141 | | DTLS-SRTP 加密 | TrySendPacket  → UDP 发出 | packet_router.cc:155 |

五、小结

从摄像头到 RTP 包,libwebrtc 经历了五个关键阶段:

  1. 采集:Capturer 产生 VideoFrame,通过 VideoBroadcaster 广播,VideoTrack 传递到注册的 Sink

  2. 连接SetLocalDescription 触发 VideoRtpSender::SetSend(),将 Track 与编码器打通

  3. 编码VideoStreamEncoder::OnFrame() 异步投入 EncoderQueue,经过帧率/码率控制后调用 encoder_->Encode()

  4. 打包RTPSenderVideo::SendVideo() 使用 RtpPacketizer 按 MTU 分包,写入扩展头,分配序列号

  5. 发送PacedSender 按目标码率平滑出队,PacketRouter 写入 TransportSequenceNumber,经 DTLS-SRTP 加密后从 UDP Socket 发出

每个阶段都有独立线程,通过消息队列解耦,保证采集不阻塞编码,编码不阻塞发送。

下一篇:从 RTP 包收到到视频渲染——接收端解包、解码、渲染全链路解析

c1cbf8e09b18433224886f004bd0a404.png