前几篇我们分析了信令层的 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() 分配时已写入 SSRC(CreateOffer 阶段由 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
调用 AddRtpHeaderExtensions(rtp_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 可用空间时,PacketizeFuA(rtp_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
}
NextFragmentPacket(rtp_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)
NextAggregatePacket(rtp_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
LogAndSendToNetwork(rtp_sender_video.cc:190)将包交给 PacedSender:
// rtp_sender_video.cc:210
rtp_sender_->EnqueuePackets(std::move(packets));
进入 PacedSender::EnqueuePackets(paced_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(带宽估计)下发的目标码率匀速补充;
-
ProcessPackets(paced_sender.cc:183)由专属线程定时驱动,每次消耗令牌发送数据; -
若 TWCC 反馈拥塞,BWE 降低目标码率,Pacer 自动缓存包减缓发送速率。
4.8 PacketRouter:注入 TransportCC 序列号,交给 DTLS-SRTP
Pacer 放行后调用 PacketRouter::SendPacket(packet_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 经历了五个关键阶段:
-
采集:Capturer 产生
VideoFrame,通过VideoBroadcaster广播,VideoTrack传递到注册的 Sink -
连接:
SetLocalDescription触发VideoRtpSender::SetSend(),将 Track 与编码器打通 -
编码:
VideoStreamEncoder::OnFrame()异步投入EncoderQueue,经过帧率/码率控制后调用encoder_->Encode() -
打包:
RTPSenderVideo::SendVideo()使用RtpPacketizer按 MTU 分包,写入扩展头,分配序列号 -
发送:
PacedSender按目标码率平滑出队,PacketRouter写入 TransportSequenceNumber,经 DTLS-SRTP 加密后从 UDP Socket 发出
每个阶段都有独立线程,通过消息队列解耦,保证采集不阻塞编码,编码不阻塞发送。
下一篇:从 RTP 包收到到视频渲染——接收端解包、解码、渲染全链路解析