1. WebRTC SDP 生成机制深度解析

在 WebRTC 的实际开发中,SDP(Session Description Protocol)作为媒体协商的核心载体,其生成过程往往像一个黑盒子。很多开发者只知道调用 addTrack 后会自动生成 SDP,却不清楚 track 的具体参数是如何被映射到 SDP 中的各个字段的。本文将深入 WebRTC 源码,揭示从 addTrack 调用到 SDP 生成的完整数据流。

提示:阅读本文需要基本了解 WebRTC 的 PeerConnection、Transceiver 等核心概念,以及 SDP 的基础结构。

1.1 核心流程概览

当开发者调用 pc.addTrack(track, stream) 时,WebRTC 内部会触发以下关键操作链:

  1. 创建 RtpSender 实例并与传入的 track 和 stream 绑定
  2. 在后续的 createOffer 过程中,收集所有必要参数
  3. 通过多层级的选项传递和过滤,最终生成包含完整媒体描述的 SDP

这个过程中最关键的三个参数——SSRC、codec 和 RTP Header Extension——会通过不同的路径被写入最终的 SDP。让我们先看一个简化的数据流图:

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()     → 同上

2. SenderOptions 的构建与传递

2.1 RtpSender 的创建过程

当调用 addTrack 时,PeerConnection 会创建一个新的 RtpSender 实例。这个过程中最关键的是 track 和 stream 的信息会被封装成 SenderOptions 对象:

// sender_options 携带了这条 track 的全部信息
SenderOptions {
  track_id   = track->id(),        // track 的唯一标识
  stream_ids = {"stream-1"},       // addTrack 时传入的 stream
  num_sim_layers = 1,              // 默认单层,Simulcast 时 > 1
  rids = []                        // Simulcast RID(可选)
}

这个 SenderOptions 会被存储在 Transceiver 中,成为后续 SDP 生成的数据源头。

2.2 从 Transceiver 到 MediaDescriptionOptions

在 createOffer 阶段,WebRTC 会通过以下调用链将 SenderOptions 转换为 MediaDescriptionOptions:

GetOptionsForUnifiedPlanOffer()
  └─► GetMediaDescriptionOptionsForTransceiver()
        └─► 生成 MediaDescriptionOptions{
              sender_options,      // 来自 RtpSender
              direction,           // 发送/接收方向
              codec_preferences,   // 应用层设置的 codec 偏好
              ... 
            }

这个转换过程确保了 track 的元信息能够被传递到 SDP 生成的最终阶段。

3. Codec 列表的生成与处理

3.1 GetCodecsForOffer 的核心逻辑

Codec 信息是通过 MediaSessionDescriptionFactory::GetCodecsForOffer 方法收集的,其核心逻辑如下:

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);
}

这个过程确保了:

  1. 重协商时保持 payload type 的稳定性
  2. 新协商时包含所有支持的 codec

3.2 Codec 来源详解

来源 说明
MergeCodecsFromDescription 优先保留上次协商的 PT 映射,保持稳定
all_audio_codecs_ 本端支持的全量音频 codec(Opus、G.711 等)
all_video_codecs_ 本端支持的全量视频 codec(VP8、VP9、H264、AV1 等)
UsedPayloadTypes 已用 PT 集合,防止重复分配

3.3 按方向过滤 Codec

在 AddAudioContentForOffer 和 AddVideoContentForOffer 中,WebRTC 会根据 transceiver 的方向对 codec 列表进行过滤:

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) {
      filtered_codecs = GetCodecsFromDescription(current_content);
    } else {
      filtered_codecs = supported_audio_codecs;
    }
  }
  ...
}

这个过滤过程确保了只有符合当前方向的 codec 会被包含在最终的 SDP 中。

4. SSRC 与 Stream ID 的写入

4.1 AddStreamParams 的实现

SSRC 和 stream ID 是通过 AddStreamParams 方法写入 SDP 的。关键代码如下:

void AddStreamParams(
    const std::string& track_id,
    const std::vector<std::string>& stream_ids,
    MediaContentDescription* offer) {
  
  // 生成或复用 SSRC
  uint32_t ssrc = GenerateOrGetSsrc(track_id);
  
  // 创建 StreamParams 对象
  StreamParams stream;
  stream.id = track_id;
  stream.ssrcs.push_back(ssrc);
  stream.stream_ids_ = stream_ids;
  stream.cname = GenerateCname();
  
  // 添加到媒体描述
  offer->AddStream(stream);
  
  // 写入 a=ssrc 和 a=msid 属性
  offer->AddSsrc(ssrc, track_id, stream_ids);
}

4.2 SSRC 生成策略

WebRTC 采用以下策略管理 SSRC:

  1. 对于新 track,生成随机 SSRC
  2. 对于已有 track,复用之前的 SSRC(确保重协商时稳定)
  3. 对于 Simulcast,生成一组连续的 SSRC

4.3 a=msid 属性的生成

a=msid 属性将 stream ID 和 track ID 关联起来,其格式为:

a=msid:<stream_id> <track_id>

例如:

a=msid:stream-1 video-track-1

这个属性是由 AddSsrc 方法根据 StreamParams 中的信息自动生成的。

5. RTP Header Extensions 的处理

5.1 扩展头的注册与协商

RTP Header Extensions 通过以下路径被写入 SDP:

void set_rtp_header_extensions(
    const RtpHeaderExtensions& extensions,
    MediaContentDescription* description) {
  
  for (const auto& ext : extensions) {
    if (ext.direction == direction ||
        ext.direction == RtpExtension::Direction::kSendReceive) {
      description->AddRtpHeaderExtension(ext);
    }
  }
}

5.2 常见的 RTP Header Extensions

扩展名 URI 用途
abs-send-time http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time 绝对发送时间
transport-cc http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 传输层拥塞控制
video-orientation urn:3gpp:video-orientation 视频方向
mid urn:ietf:params:rtp-hdrext:sdes:mid Media ID

5.3 扩展头方向性处理

RTP Header Extensions 具有方向性属性,WebRTC 会根据 transceiver 的方向进行过滤:

  • 仅包含方向匹配(send 或 sendrecv)的扩展头
  • 确保不泄露不必要的信息(如 recvonly 的扩展头)

6. 实际案例分析

6.1 音频 track 的 SDP 生成

假设我们添加一个音频 track 并生成 offer,最终 SDP 可能包含如下部分:

m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104
a=ssrc:12345678 cname:abcd1234
a=ssrc:12345678 msid:stream-1 audio-track-1
a=rtpmap:111 opus/48000/2
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
a=extmap:1 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=sendrecv

6.2 视频 track 的 SDP 生成

对于视频 track,SDP 可能如下:

m=video 9 UDP/TLS/RTP/SAVPF 96 97 98
a=ssrc:87654321 cname:abcd1234
a=ssrc:87654321 msid:stream-1 video-track-1
a=rtpmap:96 VP8/90000
a=rtpmap:97 VP9/90000
a=rtpmap:98 H264/90000
a=extmap:2 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:3 urn:3gpp:video-orientation
a=sendrecv

7. 高级主题:Simulcast 处理

7.1 Simulcast 的 SSRC 分配

当启用 Simulcast 时,WebRTC 会为同一 track 分配多个 SSRC:

void AddSimulcastStreams(
    const std::string& track_id,
    const std::vector<std::string>& stream_ids,
    int num_layers,
    MediaContentDescription* offer) {
  
  for (int i = 0; i < num_layers; ++i) {
    uint32_t ssrc = GenerateSsrc();
    StreamParams stream;
    stream.id = track_id;
    stream.ssrcs.push_back(ssrc);
    stream.stream_ids_ = stream_ids;
    stream.rid = GenerateRid(i);  // 例如 "1", "2", "3"
    offer->AddStream(stream);
  }
}

7.2 Simulcast 的 SDP 表示

Simulcast 会在 SDP 中生成额外的属性:

a=simulcast:send 1;2;3
a=rid:1 send
a=rid:2 send
a=rid:3 send
a=ssrc-group:SIM 12345678 23456789 34567890

8. 调试技巧与常见问题

8.1 调试 SDP 生成

  1. 查看完整调用栈 :在 MediaSessionDescriptionFactory::CreateOffer 设置断点
  2. 检查中间状态 :
    • sender_options 是否包含正确的 track/stream 信息
    • codec 列表是否按预期过滤
    • SSRC 是否按预期生成或复用

8.2 常见问题排查

问题现象 可能原因 解决方案
SDP 中缺少预期的 codec 方向过滤过严或 codec 未正确注册 检查 transceiver 方向和 all_audio_codecs_ / all_video_codecs_
SSRC 在重协商时变化 SSRC 生成逻辑错误 确保 GenerateOrGetSsrc 正确复用已有 SSRC
a=msid 缺失 stream_ids 为空 检查 addTrack 时是否传入了有效的 stream
RTP 扩展头未出现 方向不匹配或未注册 检查扩展头的注册和方向设置

8.3 性能考量

  1. Codec 排序 :将最可能的 codec 放在前面,减少协商开销
  2. SSRC 复用 :避免不必要的 SSRC 变化,影响 QoS 统计
  3. 扩展头精简 :只包含必要的扩展头,减少 RTP 头部开销

9. 从源码学习的建议

要深入理解 WebRTC 的 SDP 生成机制,建议重点阅读以下源码文件:

  1. pc/rtp_sender.cc - RtpSender 的实现
  2. pc/rtp_transceiver.cc - Transceiver 的逻辑
  3. pc/media_session.cc - SDP 生成的核心
  4. media/sdp/media_description.cc - 媒体描述的表示

关键点关注:

  • 数据如何在各组件间传递
  • 各种选项(direction, codec preferences等)如何影响最终结果
  • 状态(如 SSRC)如何在多次协商间保持

理解这些底层机制,将帮助开发者更好地调试 WebRTC 应用,并在需要时实现自定义的 SDP 生成逻辑。

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐