媒体流的发送(二):Track 的参数是如何写进SDP的

承接上篇 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 缓存。

  1. 找不到(新 track) → 走生成路径

  2. 找得到(重协商) → 复用旧 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 的完整写入路径分五层:

  1. SenderOptionsaddTrack 的 track_id + stream_ids 打包进 Transceiver 的 sender_options

  2. GetCodecsForOffer:本端全量 codec 去重、保 PT 稳定,重协商优先复用旧 PT

  3. FilterCodecs :按 Transceiver direction 过滤,setCodecPreferences 可覆盖顺序

  4. CreateMediaContentOffer :三类写入------AddCodecs(codec/PT)、AddStreamParams(SSRC/msid)、set_rtp_header_extensions(extmap)

  5. AddTransportOffer:ICE ufrag/pwd + DTLS fingerprint + setup:actpass

其中 SSRC 是本端随机生成并缓存 的,首次创建后后续协商复用,对端通过 a=ssrc 行知道哪个 RTP 包属于哪条 track。

通过AddTrack添加到pc的 Track,在CreateOffer环节被写进SDP,并通过信令发给对端。下一节我们继续分析对端在SetRemoteDescription时是如何从sdp解析出来远端要发送的Track信息的,以及为接收远端的RTP数据包做了哪些准备。

相关推荐
TSINGSEE5 小时前
WebRTC/视频转码/RTMP推流EasyDSS何让每一类用户都能实现高效便捷操作
音视频·webrtc·实时音视频·语音转写·ai字幕
爱丽_5 小时前
WebRTC 从信令到 NAT 穿透(SDP / ICE / STUN / TURN)
webrtc
却道天凉_好个秋16 小时前
WebRTC(十五):NAT穿透机制深度解析
后端·webrtc·stun·turn·ice·net网络穿透
Arman_16 小时前
深入浅出 RTP 协议:从原理到 WebRTC 实践
webrtc·tcp
却道天凉_好个秋16 小时前
WebRTC(十四):Candidate
音视频·webrtc·candidate
简离13 天前
前端调试实战:基于 chrome://webrtc-internals/ 高效排查WebRTC问题
前端·chrome·webrtc
YYDataV数据可视化14 天前
【P2P音视频通信系统】之 WebRTC Android平台 aar 下载
webrtc·实时音视频
dazhong201215 天前
WebRTC信令简介
webrtc
YYDataV数据可视化15 天前
【P2P音视频通信系统】之TURN 服务详解
音视频·webrtc·实时音视频·ai编程