源码路径:
api/peer_connection_interface.h
pc/peer_connection.cc
pc/sdp_offer_answer.cc对应规范:W3C WebRTC 1.0 §4.6.1
上一节我们深入分析了CreateOffer,libwebrtc 帮我们生成了本地能里的SDP描述,那为什么自己生成的SDP 还要 SetLocalDescription? 很多人第一次看到这个流程都会觉得奇怪: SDP 是我自己 CreateOffer 生成的,为什么还要再 SetLocalDescription 设置回去?
答案是:CreateOffer 只是"起草",SetLocalDescription 才是"生效"。
本质原因:职责分离
libwebrtc 把"生成描述"和"应用描述"刻意分成两步,原因有三:
① 给应用层修改 SDP 的机会(SDP Munging) CreateOffer 生成 SDP 后,应用层可以在调用 SetLocalDescription 之前修改 SDP 文本——比如强制指定某个 codec、修改带宽限制等。虽然 W3C 规范不推荐这么做,但现实中很多应用都有这个需求,两步设计天然支持了这种场景。
② SetLocalDescription 才真正触发底层动作 CreateOffer 什么都不启动,只是组装了一段文本。 真正发生的事情全在 SetLocalDescription 里:
SetLocalDescription 之后才发生的事:
├── IceTransport 创建
├── DtlsTransport 绑定
├── ICE 候选收集启动(MaybeStartGathering)
├── 媒体参数下推到编解码器
└── 信令状态机推进
③ W3C 规范本身就是两步设计
这不是 libwebrtc 的独创,W3C 规范明确规定了 createOffer 和 setLocalDescription 是两个独立操作,分别对应 JSEP 协议中的"生成描述"和"应用描述"两个阶段。
明白了为什么需要 SetLocalDescription 之后, 接下来我们就深入源码,看看SetLocalDescription到底做了哪些事情
一、W3C 规范怎么定义 SetLocalDescription
W3C 规范对 setLocalDescription() 的核心要求是:
将一个 SDP 描述设置为本端的本地描述,并根据描述类型(Offer/Answer)驱动信令状态机的状态转移。
规范还要求:
-
必须通过 Operations Chain 串行执行
-
设置成功后,信令状态(
SignalingState)必须按规则更新 -
如果是 Offer,
pending_local_description被更新 -
如果是 Answer,
current_local_description被更新,协商完成 -
设置成功后,ICE 候选收集必须立即启动
这最后一点非常关键,也是 SetLocalDescription 与 CreateOffer 最本质的区别:CreateOffer 只生产 SDP 文本,SetLocalDescription 才真正触发底层动作。
二、接口签名
libwebrtc 中 SetLocalDescription 有多个重载版本:
版本①:标准版,传入 desc + observer(推荐)
// api/peer_connection_interface.h
virtual void SetLocalDescription(
std::unique_ptr<SessionDescriptionInterface> desc,
rtc::scoped_refptr<SetLocalDescriptionObserverInterface> observer) {}
版本②:自动创建 SDP 并设置(免去手动 CreateOffer/CreateAnswer)
// api/peer_connection_interface.h
virtual void SetLocalDescription(
rtc::scoped_refptr<SetLocalDescriptionObserverInterface> observer) {}
版本③:旧版兼容接口(observer 回调延迟投递)
virtual void SetLocalDescription(
SetSessionDescriptionObserver* observer,
SessionDescriptionInterface* desc) = 0;
三个版本的核心逻辑相同,最终都收敛到 DoSetLocalDescription()。
三、完整调用链
应用层
pc->SetLocalDescription(desc, observer)
│
▼ [Signaling Thread 断言]
PeerConnection::SetLocalDescription()
│ 委托 sdp_handler_
▼
SdpOfferAnswerHandler::SetLocalDescription()
│ 入 OperationsChain 串行队列
▼
SdpOfferAnswerHandler::DoSetLocalDescription()
│
├── ① 校验(NULL / session_error / Rollback 处理)
├── ② ValidateSessionDescription(SDP 合法性检查)
├── ③ ApplyLocalDescription(核心落地逻辑)
│ ├── 更新描述槽位(pending / current)
│ ├── PushdownTransportDescription(ICE+DTLS 下推)
│ ├── UpdateTransceiversAndDataChannels(Transceiver 同步)
│ ├── UpdateSessionState(信令状态机推进)
│ │ ├── EnableSending(Answer 时开启媒体发送)
│ │ ├── ChangeSignalingState(状态机转移)
│ │ └── PushdownMediaDescription(媒体参数下推)
│ └── UseCandidatesInSessionDescription(应用远端候选)
│
├── ④ RemoveStoppedTransceivers(Answer 时清理)
├── ⑤ observer->OnSetLocalDescriptionComplete(OK)
└── ⑥ MaybeStartGathering() ← ICE 候选收集启动!
四、DoSetLocalDescription:入口与校验
// pc/sdp_offer_answer.cc
void SdpOfferAnswerHandler::DoSetLocalDescription(
std::unique_ptr<SessionDescriptionInterface> desc,
rtc::scoped_refptr<SetLocalDescriptionObserverInterface> observer) {
RTC_DCHECK_RUN_ON(signaling_thread());
// 校验1:desc 不为空
if (!desc) { /* 报错 */ }
// 校验2:无 session error
if (session_error() != SessionError::kNone) { /* 报错 */ }
// 校验3:Rollback 处理(仅 Unified Plan 支持)
if (desc->GetType() == SdpType::kRollback) {
if (IsUnifiedPlan()) {
observer->OnSetLocalDescriptionComplete(Rollback(desc->GetType()));
} else {
// Plan B 不支持 Rollback
observer->OnSetLocalDescriptionComplete(
RTCError(RTCErrorType::UNSUPPORTED_OPERATION, "..."));
}
return;
}
// 校验4:SDP 内容合法性(BUNDLE 组、codec、ICE 参数等)
RTCError error = ValidateSessionDescription(
desc.get(), cricket::CS_LOCAL, bundle_groups_by_mid);
if (!error.ok()) { /* 报错 */ }
// 核心:落地 SDP
error = ApplyLocalDescription(std::move(desc), bundle_groups_by_mid);
// Answer 时清理已停止的 Transceiver
if (local_description()->GetType() == SdpType::kAnswer) {
RemoveStoppedTransceivers();
port_allocator()->DiscardCandidatePool(); // 丢弃预分配候选池
}
observer->OnSetLocalDescriptionComplete(RTCError::OK());
// ★ 关键:通知 observer 之后才启动 ICE 候选收集
// 确保应用层先收到成功回调,再收到候选回调
transport_controller()->MaybeStartGathering();
}
深度细节:MaybeStartGathering() 必须在 observer 回调之后调用。 原因是:ICE 候选收集会立即触发 OnIceCandidate() 回调, 如果在 observer 回调之前就开始收集, 应用层可能在 SetLocalDescription 还未"完成"时就收到候选, 导致状态混乱。这是 W3C 规范明确要求的顺序。
五、ApplyLocalDescription:真正的"落地"
这是整个 SetLocalDescription 最核心的函数,按顺序做以下六件事:
5.1 更新描述槽位
// pc/sdp_offer_answer.cc
if (type == SdpType::kAnswer) {
// Answer:pending → current,协商完成
replaced_local_description = std::move(pending_local_description_);
current_local_description_ = std::move(desc);
pending_local_description_ = nullptr;
// 远端 pending 也同步变为 current
current_remote_description_ = std::move(pending_remote_description_);
} else {
// Offer/PrAnswer:存入 pending,等待 Answer 到来
replaced_local_description = std::move(pending_local_description_);
pending_local_description_ = std::move(desc);
}
libwebrtc 用两对槽位管理 SDP 状态,对应 W3C 规范:
5.2 PushdownTransportDescription:ICE + DTLS 下推
// 将本端 SDP 中的传输信息推送给 JsepTransportController
RTCError error = PushdownTransportDescription(cricket::CS_LOCAL, type);
PushdownTransportDescription 的实现:
RTCError SdpOfferAnswerHandler::PushdownTransportDescription(
cricket::ContentSource source, SdpType type) {
if (source == cricket::CS_LOCAL) {
return transport_controller()->SetLocalDescription(
type, local_description()->description());
}
// ...
}
这一步将 SDP 中的传输参数推送到 JsepTransportController,包括:
-
每个 m= section 的 ICE ufrag / pwd(凭据)
-
DTLS 证书指纹(a=fingerprint)
-
DTLS 角色(a=setup)
-
BUNDLE 信息
JsepTransportController 据此为每个 m= section 创建或更新 IceTransport 和 DtlsTransport 对象,为后续的 ICE 候选收集和 DTLS 握手做好准备。
5.3 UpdateTransceiversAndDataChannels:Transceiver 同步
// Unified Plan 下,将 SDP 内容同步到各 Transceiver
RTCError error = UpdateTransceiversAndDataChannels(
cricket::CS_LOCAL, *local_description(),
old_local_description, remote_description(),
bundle_groups_by_mid);
这一步遍历所有 Transceiver,做两件事:
① 绑定 DTLS Transport
这里的 transport是在 上面的PushdownTransportDescription ->JsepTransportController::SetLocalDescription->ApplyDescription_n->MaybeCreateJsepTransport 里创建的
// 将 DtlsTransport 绑定到 Sender 和 Receiver
if (transceiver->mid()) {
auto dtls_transport = LookupDtlsTransportByMid(
pc_->network_thread(), transport_controller(), *transceiver->mid());
transceiver->sender_internal()->set_transport(dtls_transport);
transceiver->receiver_internal()->set_transport(dtls_transport);
}
② Answer 时更新 direction 和 fired_direction
// 如果是 Answer,更新 CurrentDirection 和 FiredDirection
if (type == SdpType::kPrAnswer || type == SdpType::kAnswer) {
transceiver->set_current_direction(media_desc->direction());
transceiver->set_fired_direction(media_desc->direction());
// 如果 direction 变为不含 recv,触发 OnRemoveTrack 回调
}
5.4 UpdateSessionState:信令状态机推进
// pc/sdp_offer_answer.cc
RTCError SdpOfferAnswerHandler::UpdateSessionState(
SdpType type, cricket::ContentSource source, ...) {
// Answer 时允许媒体流动
if (type == SdpType::kPrAnswer || type == SdpType::kAnswer) {
EnableSending(); // ← 开启 RTP 发送通道
}
// 按 W3C 规范推进信令状态机
if (type == SdpType::kOffer) {
ChangeSignalingState(
source == CS_LOCAL
? kHaveLocalOffer // SetLocalDescription(offer)
: kHaveRemoteOffer); // SetRemoteDescription(offer)
} elseif (type == SdpType::kAnswer) {
ChangeSignalingState(kStable); // 协商完成,回到 Stable
transceivers()->DiscardStableStates(); // 清理中间状态
}
// 将媒体参数下推到各 Channel(Worker Thread)
return PushdownMediaDescription(type, source, bundle_groups_by_mid);
}
ChangeSignalingState 会触发 PeerConnectionObserver::OnSignalingChange() 回调,通知应用层信令状态已变化。
5.5 PushdownMediaDescription:媒体参数下推到 Worker Thread
这是 SDP 落地的最后一公里。SDP 中的媒体信息必须下推到编解码器层面才能真正生效:
// pc/sdp_offer_answer.cc
RTCError SdpOfferAnswerHandler::PushdownMediaDescription(...) {
// 更新 payload type demux 规则(决定收到的 RTP 包如何分发)
UpdatePayloadTypeDemuxingState(source, bundle_groups_by_mid);
// 遍历所有 Transceiver,收集 channel + media_desc 对
for (const auto& transceiver : rtp_transceivers) {
transceiver->OnNegotiationUpdate(type, content_desc); // 通知 Transceiver
channels.push_back({channel, content_desc});
}
// 切换到 Worker Thread 执行 SetLocalContent
// (避免阻塞信令线程,同时防止多个 channel 并发导致音频卡顿)
for (const auto& entry : channels) {
pc_->worker_thread()->Invoke<RTCError>(RTC_FROM_HERE, [&]() {
// 将 SDP media section 下推到底层 Voice/VideoChannel
// 触发编解码器参数更新、RTCP 配置、RTP Header Extension 配置等
entry.first->SetLocalContent(entry.second, type, &error);
return RTCError::OK();
});
}
}
SetLocalContent 最终会调用到 VoiceMediaChannel 或 VideoMediaChannel, 把 SDP 中的编解码器参数、SSRC、RTP Header Extension 等配置写入底层媒体引擎。
5.6 MaybeStartGathering:ICE 候选收集正式启动
// DoSetLocalDescription 末尾
transport_controller()->MaybeStartGathering();
这是 SetLocalDescription 最重要的副作用。 MaybeStartGathering 检查当前 transport 状态, 如果条件满足(已有本地描述,ICE 凭据已配置), 则正式启动 ICE 候选收集:
MaybeStartGathering()
│
├── 遍历所有 IceTransport
├── 调用 PortAllocator 开始枚举网络接口
│ ├── 收集 host 候选(本机 IP)
│ ├── 向 STUN 服务器发送 Binding Request(收集 srflx 候选)
│ └── 向 TURN 服务器分配(收集 relay 候选)
│
└── 每收集到一个候选 → OnIceCandidate() 回调 → 应用层通过信令发给对端
六、SetLocalDescription(Offer) vs SetLocalDescription(Answer)
两种 SDP 类型触发的动作有明显差异:
| |
SetLocalDescription(Offer)
|
SetLocalDescription(Answer)
|
| --- | --- | --- |
|
描述槽位
|
存入 pending_local_description_
|
存入 current_local_description_
|
|
信令状态
|
→ kHaveLocalOffer
|
→ kStable
|
|
EnableSending
|
不触发
|
✅ 触发,媒体可以开始发送
|
|
ICE 收集
|
✅ 立即启动
|
✅ 立即启动
|
|
RemoveStoppedTransceivers
|
不触发
|
✅ 清理已停止的 Transceiver
|
|
DiscardCandidatePool
|
不触发
|
✅ 丢弃预分配的 ICE 候选池
|
七、完整流程图
pc->SetLocalDescription(offer/answer, observer)
│
▼ [Signaling Thread]
DoSetLocalDescription()
│
├── 校验 + ValidateSessionDescription
│
├── ApplyLocalDescription()
│ │
│ ├── ① 更新描述槽位(pending/current)
│ │
│ ├── ② PushdownTransportDescription
│ │ → JsepTransportController 创建/更新
│ │ IceTransport + DtlsTransport
│ │
│ ├── ③ UpdateTransceiversAndDataChannels
│ │ → 绑定 DtlsTransport 到 Sender/Receiver
│ │ → 更新 CurrentDirection / FiredDirection
│ │
│ ├── ④ UpdateSessionState
│ │ → EnableSending(Answer 时)
│ │ → ChangeSignalingState → OnSignalingChange 回调
│ │ → PushdownMediaDescription
│ │ → Worker Thread: SetLocalContent
│ │ (编解码器参数、SSRC 写入底层媒体引擎)
│ │
│ └── ⑤ UseCandidatesInSessionDescription
│ (如果已有 remote description,应用远端候选)
│
├── observer->OnSetLocalDescriptionComplete(OK)
│
└── MaybeStartGathering() ← ICE 候选收集正式启动
→ OnIceCandidate() 回调开始触发
八、小结
SetLocalDescription 是 WebRTC 信令流程中触发动作最多的一步:
|
动作
|
触发条件
|
| --- | --- |
|
信令状态机推进
|
每次调用
|
|
ICE Transport 创建/更新
|
每次调用
|
|
DTLS Transport 绑定
|
每次调用
|
|
ICE 候选收集启动
|
每次调用(MaybeStartGathering)
|
|
媒体参数下推到编解码器
|
每次调用(PushdownMediaDescription)
|
|
媒体发送通道开启
|
仅 Answer 时(EnableSending)
|
|
Transceiver direction 确定
|
仅 Answer 时
|
CreateOffer 只是生产了一张"名片",SetLocalDescription 才是把这张名片正式递出去、同时拉开连接序幕的那一刻。
下一篇:SetRemoteDescription 深度解析——收到对端 SDP 时,libwebrtc 内部如何解析并驱动 Track 创建?