WebRTC SDP生成机制与媒体协商详解
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 内部会触发以下关键操作链:
- 创建 RtpSender 实例并与传入的 track 和 stream 绑定
-
在后续的
createOffer过程中,收集所有必要参数 - 通过多层级的选项传递和过滤,最终生成包含完整媒体描述的 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);
}
这个过程确保了:
- 重协商时保持 payload type 的稳定性
- 新协商时包含所有支持的 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:
- 对于新 track,生成随机 SSRC
- 对于已有 track,复用之前的 SSRC(确保重协商时稳定)
- 对于 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 生成
-
查看完整调用栈
:在
MediaSessionDescriptionFactory::CreateOffer设置断点 -
检查中间状态
:
-
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 性能考量
- Codec 排序 :将最可能的 codec 放在前面,减少协商开销
- SSRC 复用 :避免不必要的 SSRC 变化,影响 QoS 统计
- 扩展头精简 :只包含必要的扩展头,减少 RTP 头部开销
9. 从源码学习的建议
要深入理解 WebRTC 的 SDP 生成机制,建议重点阅读以下源码文件:
-
pc/rtp_sender.cc- RtpSender 的实现 -
pc/rtp_transceiver.cc- Transceiver 的逻辑 -
pc/media_session.cc- SDP 生成的核心 -
media/sdp/media_description.cc- 媒体描述的表示
关键点关注:
- 数据如何在各组件间传递
- 各种选项(direction, codec preferences等)如何影响最终结果
- 状态(如 SSRC)如何在多次协商间保持
理解这些底层机制,将帮助开发者更好地调试 WebRTC 应用,并在需要时实现自定义的 SDP 生成逻辑。
火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。
更多推荐
所有评论(0)