媒体流的发送(二):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数据包做了哪些准备。

相关推荐
换个昵称都难17 小时前
webrtc 拥塞控制GCC 和PCC
webrtc
Cxiaomu18 小时前
React接入WebRTC实时视频实践
react.js·音视频·webrtc
AndyHuang197621 小时前
WebRTC 强制 Relay 模式下 TCP 重连失败深度排查与优化实战
webrtc
换个昵称都难21 小时前
webrtc pacing 平滑发包模块
webrtc
换个昵称都难1 天前
webrtc 音频混音介绍
音视频·webrtc
换个昵称都难2 天前
webrtc QOS-RemoteBitrateEstimator接收端带宽估计(1)
webrtc
换个昵称都难2 天前
webrtc QOS-RemoteBitrateEstimator接收端带宽估计-四个实例(2)
webrtc
都在酒里2 天前
【极致低延时】香橙派部署 MediaMTX 实现 WebRTC 推流,延时仅 500-800ms,比局域网 ffmpeg 拉流快近 10 倍!(附踩坑全记录)
linux·arm开发·ffmpeg·webrtc·orangepi·嵌入式软件
换个昵称都难2 天前
WebRTC QoS 实战:从原理到弱网优化
开发语言·php·webrtc
小哈机器人2 天前
Phantom Bridge:一个基于WebRTC的ROS2远程可视化与遥操作工具
机器人·webrtc·数据可视化