承接上篇
AddTrack分析。本文聚焦一个核心问题:调用pc.addTrack(track, stream)之后,这条 track 的 SSRC、codec、RTP Header Extension 是如何一步步被写入 SDP 的。
微信公众号: WebRTC 工程技术
一、整体数据流
bash
pc.addTrack(track, stream)
└─► RtpSender 创建,绑定 track + stream_ids
└─► Transceiver.sender_options 携带 SenderOptions{track_id, stream_ids}
pc.createOffer()
└─► GetOptionsForUnifiedPlanOffer()
└─► GetMediaDescriptionOptionsForTransceiver()
└─► MediaDescriptionOptions{sender_options, direction, ...}
└─► MediaSessionDescriptionFactory::CreateOffer()
├─► GetCodecsForOffer() → a=rtpmap / a=fmtp
├─► AddAudioContentForOffer()
│ ├─► FilterCodecs() → 按方向过滤 codec
│ ├─► CreateMediaContentOffer()
│ │ ├─► offer->AddCodecs() → 写入 codec 列表
│ │ ├─► AddStreamParams() → 写入 a=ssrc
│ │ ├─► set_rtp_header_extensions() → 写入 a=extmap
│ │ └─► AddSimulcastToMediaDescription()
│ └─► AddTransportOffer() → 写入 ICE/DTLS 参数
└─► AddVideoContentForOffer() → 同上
二、SenderOptions 的构建
addTrack 创建 RtpSender 后,在 GetMediaDescriptionOptionsForTransceiver 中,Transceiver 内部的 sender 信息被打包成 SenderOptions:
bash
// sender_options 携带了这条 track 的全部信息
SenderOptions {
track_id = track->id() // track 的唯一标识
stream_ids = {"stream-1"} // addTrack 时传入的 stream
num_sim_layers = 1 // 默认单层,Simulcast 时 > 1
rids = [] // Simulcast RID(可选)
}
这是后续 SSRC 分配和 a=msid 写入的数据源。
三、GetCodecsForOffer ------ codec 列表的来源
bash
void MediaSessionDescriptionFactory::GetCodecsForOffer(
const std::vector<const ContentInfo*>& current_active_contents,
AudioCodecs* audio_codecs, VideoCodecs* video_codecs) const {
UsedPayloadTypes used_pltypes;
// 1. 从已有协商结果里取(保持 PT 稳定)
MergeCodecsFromDescription(current_active_contents,
audio_codecs, video_codecs, &used_pltypes);
// 2. 补充本端支持但尚未出现的 codec
MergeCodecs<AudioCodec>(all_audio_codecs_, audio_codecs, &used_pltypes);
MergeCodecs<VideoCodec>(all_video_codecs_, video_codecs, &used_pltypes);
}
| 来源 | 说明 | | --- | --- | | MergeCodecsFromDescription | 优先保留上次协商的 PT 映射,保持稳定 | | all_audio_codecs_ | 本端支持的全量音频 codec(Opus、G.711 等) | | all_video_codecs_ | 本端支持的全量视频 codec(VP8、VP9、H264、AV1 等) | | UsedPayloadTypes | 已用 PT 集合,防止重复分配 |
结果:得到一份带有正确 PT 映射的全量 codec 列表,后续按方向过滤。
四、AddAudioContentForOffer ------ 按方向过滤 codec
bash
bool MediaSessionDescriptionFactory::AddAudioContentForOffer(...) {
// 按 Transceiver 方向过滤 codec
// sendrecv / sendonly → 包含发送 codec
// recvonly → 包含接收 codec
// inactive → 空列表
const AudioCodecs& supported_audio_codecs =
GetAudioCodecsForOffer(media_description_options.direction);
AudioCodecs filtered_codecs;
if (!media_description_options.codec_preferences.empty()) {
// 应用层手动设置了 codec 偏好(setCodecPreferences API)
filtered_codecs = MatchCodecPreference(
media_description_options.codec_preferences, supported_audio_codecs);
} else {
// 重协商时:优先保留上次已协商的 codec(保持 PT 稳定)
if (current_content && !current_content->rejected && ...) {
for (const AudioCodec& codec : acd->codecs()) {
if (FindMatchingCodec(...)) filtered_codecs.push_back(codec);
}
}
// 追加其他支持的 codec(新增的放后面)
for (const AudioCodec& codec : supported_audio_codecs) {
if (FindMatchingCodec(...) && !FindMatchingCodec(filtered_codecs,...)) {
filtered_codecs.push_back(found_codec); // 使用已有 PT 映射
}
}
}
// 不需要 CN(舒适噪声)时去掉
if (!session_options.vad_enabled) StripCNCodecs(&filtered_codecs);
// 调用核心函数写入 SDP
CreateMediaContentOffer(..., filtered_codecs, ..., audio.get());
audio->set_direction(media_description_options.direction);
desc->AddContent(mid, MediaProtocolType::kRtp, stopped, std::move(audio));
AddTransportOffer(mid, ...); // 写入 ICE/DTLS
}
| codec 过滤优先级 | 来源 | | --- | --- | | 最高:setCodecPreferences 手动设置 | MatchCodecPreference | | 次之:重协商时已有 codec(保稳定) | current_content->codecs() | | 最后:本端其他支持的 codec | supported_audio_codecs |
五、CreateMediaContentOffer ------ 三类参数写入 SDP
bash
static bool CreateMediaContentOffer(..., const std::vector<C>& codecs, ...) {
// [1] 写入 codec 列表 → 生成 a=rtpmap / a=fmtp
offer->AddCodecs(codecs);
// [2] 写入 Stream 参数 → 生成 a=ssrc / a=msid
AddStreamParams(media_description_options.sender_options,
session_options.rtcp_cname,
ssrc_generator, current_streams, offer);
// [3] 写入其他媒体参数
CreateContentOffer(..., rtp_extensions, ..., offer);
}
5.1 AddCodecs → a=rtpmap / a=fmtp
bash
offer->AddCodecs(codecs);
每个 codec 对应 SDP 中的一行或多行:
bash
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
5.2 AddStreamParams → a=ssrc / a=msid
bash
static bool AddStreamParams(
const std::vector<SenderOptions>& sender_options, ...) {
for (const SenderOptions& sender : sender_options) {
StreamParams* param = GetStreamByIds(*current_streams, sender.track_id);
if (!param) {
// 新 sender:分配新 SSRC
StreamParams stream_param =
sender.rids.empty()
? CreateStreamParamsForNewSenderWithSsrcs(sender, rtcp_cname,
include_rtx_streams, include_flexfec_stream, ssrc_generator)
: CreateStreamParamsForNewSenderWithRids(sender, rtcp_cname);
content_description->AddStream(stream_param);
current_streams->push_back(stream_param); // 缓存,下次复用
} else {
// 已有 sender:复用旧 SSRC,只更新 stream_ids
param->set_stream_ids(sender.stream_ids);
content_description->AddStream(*param);
}
}
}
对应 SDP 输出:
bash
a=ssrc:1234567890 cname:xxxx ← RTCP CNAME,用于同步
a=ssrc:1234567890 msid:stream-1 track-1 ← stream_id + track_id
a=ssrc:1234567890 mslabel:stream-1
a=ssrc:1234567890 label:track-1
a=ssrc:9876543210 cname:xxxx ← RTX SSRC(若有 RTX codec)
| SSRC 类型 | 说明 | | --- | --- | | 主 SSRC | 媒体数据的 RTP 流标识,首次随机生成,后续复用 | | RTX SSRC | 重传流(含 RTX codec 时自动生成) | | FlexFEC SSRC | 前向纠错流(含 FlexFEC codec 时生成) |
5.3 CreateContentOffer → a=extmap / a=rtcp-mux
bash
static bool CreateContentOffer(...) {
offer->set_rtcp_mux(session_options.rtcp_mux_enabled); // a=rtcp-mux
if (offer->type() == MEDIA_TYPE_VIDEO)
offer->set_rtcp_reduced_size(true); // a=rtcp-rsize
// 写入 RTP Header Extensions → a=extmap
RtpHeaderExtensions extensions;
for (auto extension_with_id : rtp_extensions) {
for (constauto& ext : media_description_options.header_extensions) {
if (extension_with_id.uri == ext.uri) {
extensions.push_back(extension_with_id); // URI + 分配好的 ID
}
}
}
offer->set_rtp_header_extensions(extensions);
// Simulcast 描述(若有)
AddSimulcastToMediaDescription(media_description_options, offer);
}
常见 RTP Header Extension:
bash
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
六、AddTransportOffer → ICE / DTLS 参数
bash
AddTransportOffer(media_description_options.mid,
media_description_options.transport_options,
current_description, desc, ice_credentials);
对应 SDP:
bash
a=ice-ufrag:xxxx
a=ice-pwd:xxxxxxxxxxxxxxxxxxxxxxxx
a=ice-options:trickle
a=fingerprint:sha-256 AA:BB:CC:... ← DTLS 证书指纹
a=setup:actpass ← Offer 方固定 actpass
bash
std::unique_ptr<SessionDescription> MediaSessionDescriptionFactory::CreateOffer(
const MediaSessionOptions& session_options,
const SessionDescription* current_description) const {
// 1. 收集本端全量 codec 列表(含已协商过的 PT 映射)
GetCodecsForOffer(current_active_contents,
&offer_audio_codecs, &offer_video_codecs);
// 2. 收集 RTP Header Extension,分配 ID
AudioVideoRtpHeaderExtensions extensions_with_ids =
GetOfferedRtpHeaderExtensionsWithIds(...);
// 3. 逐 m= section 生成内容
for (const MediaDescriptionOptions& opts : session_options...) {
switch (opts.type) {
case MEDIA_TYPE_AUDIO: AddAudioContentForOffer(...); break;
case MEDIA_TYPE_VIDEO: AddVideoContentForOffer(...); break;
case MEDIA_TYPE_DATA: AddDataContentForOffer(...); break;
}
}
// 4. 处理 BUNDLE group
if (session_options.bundle_enabled) {
ContentGroup offer_bundle(GROUP_TYPE_BUNDLE);
for (const ContentInfo& content : offer->contents()) {
if (!content.rejected) offer_bundle.AddContentName(content.name);
}
// 共享 ICE 凭证和加密参数
UpdateTransportInfoForBundle(offer_bundle, offer.get());
UpdateCryptoParamsForBundle(offer_bundle, offer.get());
}
}
每个 m= section 包含的关键内容
| SDP 字段 | 来源 | | --- | --- | | a=mid | mid_generator_ 生成或复用旧 MID | | a=rtpmap / a=fmtp | GetCodecsForOffer 本端全量 codec | | a=extmap | GetOfferedRtpHeaderExtensionsWithIds 分配 ID | | a=ssrc | StreamParams 里的 SSRC(已有则复用) | | a=ice-ufrag / a=ice-pwd | IceCredentialsIterator 新生成或复用 | | a=fingerprint | DTLS 证书的 SHA-256 指纹 | | a=setup:actpass | Offer 方固定为 actpass | | a=sendrecv 等 | Transceiver 的 direction | | a=group:BUNDLE | BUNDLE 策略开启时添加 |
七、一条 track 在 SDP 中的完整体现
以一个 addTrack 添加的视频 track 为例,最终生成的 SDP m= section:
bash
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 127 121 125 107 108 109 124 120 123
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:xxxx ← AddTransportOffer
a=ice-pwd:xxxxxxxxxxxxxxxxxxxxxxxx ← AddTransportOffer
a=ice-options:trickle
a=fingerprint:sha-256 AA:BB:CC:... ← DTLS 证书
a=setup:actpass
a=mid:video ← Transceiver MID
a=extmap:1 urn:ietf:params:rtp-hdrext:toffset
a=extmap:2 http://www.webrtc.org/...abs-send-time ← CreateContentOffer
a=extmap:3 http://www.ietf.org/...transport-wide-cc
a=sendrecv ← Transceiver direction
a=msid:stream-1 track-1 ← addTrack 时的 stream + track
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:96 VP8/90000 ← AddCodecs
a=rtcp-fb:96 goog-remb
a=rtcp-fb:96 transport-cc
a=rtpmap:97 rtx/90000 ← RTX codec
a=fmtp:97 apt=96
a=rtpmap:98 VP9/90000
a=rtpmap:99 H264/90000
a=fmtp:99 level-asymmetry-allowed=1;...
a=ssrc-group:FID 1234567890 9876543210 ← 主 SSRC + RTX SSRC 关联
a=ssrc:1234567890 cname:xxxx ← AddStreamParams
a=ssrc:1234567890 msid:stream-1 track-1
a=ssrc:9876543210 cname:xxxx ← RTX SSRC
a=ssrc:9876543210 msid:stream-1 track-1
八、关键设计:PT 稳定性保证
bash
// 重协商时,已用 PT 优先保留
UsedPayloadTypes used_pltypes;
MergeCodecsFromDescription(current_active_contents, ..., &used_pltypes);
MergeCodecs<VideoCodec>(all_video_codecs_, video_codecs, &used_pltypes);
| 场景 | PT 处理 | | --- | --- | | 首次协商 | 按 all_video_codecs_ 顺序分配,从 96 开始 | | 重协商(同一 track) | 复用上次的 PT,SSRC 不变 | | 新增 track 到已有 session | 不与已用 PT 冲突,从空闲 PT 分配 | | setCodecPreferences 限制 | 只输出指定的 codec,顺序以偏好为准 |
九、Track的 ssrc 是在哪里生成的
SSRC 的生成发生在 CreateOffer 过程中的 AddStreamParams → CreateStreamParamsForNewSenderWithSsrcs → StreamParams::GenerateSsrcs 这条链路上,具体分三层:
第一层:触发点 ------ AddStreamParams
bash
static bool AddStreamParams(
const std::vector<SenderOptions>& sender_options, ...) {
for (const SenderOptions& sender : sender_options) {
StreamParams* param =
GetStreamByIds(*current_streams, "" , sender.track_id);
if (!param) {
// 新 sender → 生成新 SSRC
StreamParams stream_param =
CreateStreamParamsForNewSenderWithSsrcs(
sender, rtcp_cname, include_rtx_streams,
include_flexfec_stream, ssrc_generator);
content_description->AddStream(stream_param);
current_streams->push_back(stream_param); // 缓存,下次复用
} else {
// 已有 sender → 复用旧 SSRC,不重新生成
param->set_stream_ids(sender.stream_ids);
content_description->AddStream(*param);
}
}
}
关键判断:GetStreamByIds 按 track_id 查找 current_streams 缓存。
-
找不到(新 track) → 走生成路径
-
找得到(重协商) → 复用旧 SSRC,PT 和 SSRC 都不变
第二层:生成入口 ------ CreateStreamParamsForNewSenderWithSsrcs
bash
static StreamParams CreateStreamParamsForNewSenderWithSsrcs(
const SenderOptions& sender, const std::string& rtcp_cname,
bool include_rtx_streams, bool include_flexfec_stream,
UniqueRandomIdGenerator* ssrc_generator) {
StreamParams result;
result.id = sender.track_id; // track_id 作为 stream id
result.GenerateSsrcs(
sender.num_sim_layers, // 普通单流 = 1,Simulcast > 1
include_rtx_streams, // 是否需要 RTX SSRC
include_flexfec_stream, // 是否需要 FlexFEC SSRC
ssrc_generator);
result.cname = rtcp_cname; // RTCP CNAME(会话级唯一)
result.set_stream_ids(sender.stream_ids);
return result;
}
第三层:实际随机生成 ------ StreamParams::GenerateSsrcs
bash
void StreamParams::GenerateSsrcs(int num_layers, bool generate_fid,
bool generate_fec_fr,
rtc::UniqueRandomIdGenerator* ssrc_generator) {
std::vector<uint32_t> primary_ssrcs;
for (int i = 0; i < num_layers; ++i) {
uint32_t ssrc = ssrc_generator->GenerateId(); // ← 随机生成主 SSRC
primary_ssrcs.push_back(ssrc);
add_ssrc(ssrc);
}
if (num_layers > 1) {
SsrcGroup simulcast(kSimSsrcGroupSemantics, primary_ssrcs); // SIM group
ssrc_groups.push_back(simulcast);
}
if (generate_fid) {
for (uint32_t ssrc : primary_ssrcs) {
AddFidSsrc(ssrc, ssrc_generator->GenerateId()); // ← 随机生成 RTX SSRC
}
}
if (generate_fec_fr) {
for (uint32_t ssrc : primary_ssrcs) {
AddFecFrSsrc(ssrc, ssrc_generator->GenerateId()); // ← 随机生成 FEC SSRC
}
}
}
底层 GenerateId() 的实现:
bash
uint32_t UniqueRandomIdGenerator::GenerateId() {
webrtc::MutexLock lock(&mutex_);
while (true) {
auto pair = known_ids_.insert(CreateRandomNonZeroId()); // 随机非零 uint32_t
if (pair.second) { // insert 成功说明不重复
return *pair.first;
}
// 重复则重试,直到唯一
}
}
保证唯一性:用 known_ids_(std::set)记录所有已用 SSRC,生成冲突则重试,全局唯一。
SSRC 生成完整路径图
bash
pc.addTrack(track, stream)
└─► RtpSender 创建,track_id 记录在 SenderOptions
pc.createOffer()
└─► GetMediaDescriptionOptionsForTransceiver()
└─► SenderOptions{track_id, stream_ids, num_sim_layers=1}
└─► MediaSessionDescriptionFactory::CreateOffer()
└─► AddAudioContentForOffer() / AddVideoContentForOffer()
└─► CreateMediaContentOffer()
└─► AddStreamParams()
├─ GetStreamByIds(track_id) → 未找到(首次)
└─► CreateStreamParamsForNewSenderWithSsrcs()
└─► StreamParams::GenerateSsrcs()
├─ ssrc_generator->GenerateId() → 主 SSRC(随机 uint32_t)
├─ ssrc_generator->GenerateId() → RTX SSRC(若有 RTX)
└─ ssrc_generator->GenerateId() → FEC SSRC(若有 FlexFEC)
生成几个 SSRC?
bash
| 场景 | 生成的 SSRC 数量 | 说明 |
|---|---|---|
| 普通单流(无 RTX) | 1 | 只有主 SSRC |
| 普通单流(含 RTX) | 2 | 主 SSRC + RTX SSRC,`a=ssrc-group:FID` 关联 |
| 普通单流(含 FlexFEC) | 2 | 主 SSRC + FEC SSRC,`a=ssrc-group:FEC-FR` 关联 |
| 普通单流(RTX + FlexFEC) | 3 | 主 + RTX + FEC |
| Simulcast 3 层(含 RTX) | 6 | 每层 主+RTX,`a=ssrc-group:SIM` 关联主 SSRC |
重协商时 SSRC 不变的原因 current_streams 是 跨次协商的缓存,存的是上次生成的 StreamParams。重协商时 GetStreamByIds(track_id) 能找到旧记录,直接复用,不会重新调用 GenerateSsrcs。这保证了同一个 track 在整个会话生命周期内 SSRC 稳定,对端不需要重新建立 SSRC → track 的映射。
十、小结
addTrack → SDP 的完整写入路径分五层:
-
SenderOptions :
addTrack的 track_id + stream_ids 打包进 Transceiver 的 sender_options -
GetCodecsForOffer:本端全量 codec 去重、保 PT 稳定,重协商优先复用旧 PT
-
FilterCodecs :按 Transceiver direction 过滤,
setCodecPreferences可覆盖顺序 -
CreateMediaContentOffer :三类写入------
AddCodecs(codec/PT)、AddStreamParams(SSRC/msid)、set_rtp_header_extensions(extmap) -
AddTransportOffer:ICE ufrag/pwd + DTLS fingerprint + setup:actpass
其中 SSRC 是本端随机生成并缓存 的,首次创建后后续协商复用,对端通过 a=ssrc 行知道哪个 RTP 包属于哪条 track。
通过AddTrack添加到pc的 Track,在CreateOffer环节被写进SDP,并通过信令发给对端。下一节我们继续分析对端在SetRemoteDescription时是如何从sdp解析出来远端要发送的Track信息的,以及为接收远端的RTP数据包做了哪些准备。