源码路径:
api/peer_connection_interface.h
pc/peer_connection.cc
pc/sdp_offer_answer.cc
pc/webrtc_session_description_factory.hchromium.googlesource.com/external/we… main 分支
对应规范:W3C WebRTC 1.0 §4.4.1
createOffer 在整个信令流程中有什么作用或者说为什么需要调用这个函数,刚开始了解 webrtc的人都会有这个疑问?
想象你要发起一场视频会议,CreateOffer 就是起草一份会议邀请函的过程。
这份"邀请函"里写了什么
-
我支持的视频格式:H.264、VP8、VP9我支持的音频格式:Opus、G711
-
我的网络入口地址:(等 ICE 收集)
-
我的身份证明:(DTLS 证书指纹)
-
我们用同一条通道传音视频吗:是(BUNDLE)
-
我打算:既发送也接收(sendrecv)
这就是 SDP Offer 的本质——一份本端能力的自我声明。
简单来说CreateOffer 干了四件事:
-
排队, 确保不和其他信令操作撞车
-
收集能力, 遍历所有 Track/Transceiver我要发什么?收什么?
-
等证书, DTLS 证书没好就排队 证书好了才能写入指纹
-
拼SDP, 把所有信息组装成文本通过 OnSuccess 回调返回
下面我们一起详细了解下CreateOffer。
一、W3C 规范怎么定义 CreateOffer
W3C 规范对 createOffer() 的定义非常简洁:
生成一个 SDP Offer,包含本端支持的媒体能力配置,用于发起一次新的 WebRTC 会话,或者修改一个已建立的会话。
但"简洁"的背后,规范要求实现方必须处理相当多的细节:
-
收集当前所有
RtpTransceiver的媒体描述 -
根据
RTCOfferAnswerOptions决定 m= section 的方向 -
携带本端的 ICE 凭据(ufrag / pwd)
-
携带 DTLS 证书指纹(
a=fingerprint) -
处理 ICE Restart 逻辑
-
整个操作必须串行化,不能与其他信令操作并发
这些细节,在 libwebrtc 里都有对应的实现,我们一层一层来看。
二、接口签名
// api/peer_connection_interface.h
// See: https://www.w3.org/TR/webrtc/#idl-def-rtcofferansweroptions
// Create a new offer.
// The CreateSessionDescriptionObserver callback will be called when done.
virtual void CreateOffer(CreateSessionDescriptionObserver* observer,
const RTCOfferAnswerOptions& options) = 0;
两个参数,设计非常清晰:
① CreateSessionDescriptionObserver
结果通过 Observer 异步回调,而不是直接返回。 原因是 SDP 生成可能涉及异步操作(最典型的是 DTLS 证书的异步生成),不能同步返回。回调只有两个接口:
// api/jsep.h
class CreateSessionDescriptionObserver {
// 成功:拿到 SDP,下一步调用 SetLocalDescription
virtual void OnSuccess(SessionDescriptionInterface* desc) = 0;
// 失败:收到错误原因
virtual void OnFailure(RTCError error) = 0;
};
② RTCOfferAnswerOptions 控制 SDP 生成行为的参数集:
// api/peer_connection_interface.h
// See: https://www.w3.org/TR/webrtc/#idl-def-rtcofferansweroptions
struct RTCOfferAnswerOptions {
// Plan B 遗留选项:是否在 Offer 中包含音/视频接收方向
// Unified Plan 用户应使用 AddTransceiver,不应设置这两个字段
int offer_to_receive_video = kUndefined;
int offer_to_receive_audio = kUndefined;
// 是否开启语音活动检测(VAD)
bool voice_activity_detection = true;
// 是否强制 ICE Restart(重新生成 ICE 凭据)
bool ice_restart = false;
// 是否开启 BUNDLE(音视频复用同一传输通道,推荐 true)
bool use_rtp_mux = true;
};
深度细节:offer_to_receive_audio/video 是 Plan B 语义的遗留字段。 在 Unified Plan 下,媒体方向由 RtpTransceiver::direction 控制, 这两个字段会触发 HandleLegacyOfferOptions 做兼容处理, 内部实际上是在操作 Transceiver 的 direction,而不是直接写 SDP。
三、调用链:一次 CreateOffer 经历了什么
完整调用链如下:
应用层调用
pc->CreateOffer(observer, options)
│
▼ [Signaling Thread]
PeerConnection::CreateOffer()
│ 直接转发给 sdp_handler_
▼
SdpOfferAnswerHandler::CreateOffer()
│ 封装进 OperationsChain 串行队列
▼
SdpOfferAnswerHandler::DoCreateOffer()
│ 前置校验 → GetOptionsForOffer()
▼
WebRtcSessionDescriptionFactory::CreateOffer()
│ 等待 DTLS 证书就绪(可能异步)
▼
MediaSessionDescriptionFactory::CreateOffer()
│ 真正生成 cricket::SessionDescription
▼
observer->OnSuccess(SessionDescriptionInterface*)
四、第一层:PeerConnection 调用CreateOffer
// pc/peer_connection.cc
void PeerConnection::CreateOffer(CreateSessionDescriptionObserver* observer,
const RTCOfferAnswerOptions& options) {
RTC_DCHECK_RUN_ON(signaling_thread());
sdp_handler_->CreateOffer(observer, options);
}
PeerConnection::CreateOffer 本身什么都不做, 只是做了一个线程断言(必须在 signaling_thread 上调用), 然后把工作全部委托给 SdpOfferAnswerHandler。 这是 libwebrtc 的典型设计模式:PeerConnection 是门面,复杂逻辑下沉到子组件。
五、第二层:OperationsChain —— 串行化保障
void SdpOfferAnswerHandler::CreateOffer(
CreateSessionDescriptionObserver* observer,
const PeerConnectionInterface::RTCOfferAnswerOptions& options) {
RTC_DCHECK_RUN_ON(signaling_thread());
// Chain this operation. If asynchronous operations are pending on the chain,
// this operation will be queued to be invoked, otherwise the contents of the
// lambda will execute immediately.
operations_chain_->ChainOperation(
[this_weak_ptr = weak_ptr_factory_.GetWeakPtr(),
observer_refptr =
rtc::scoped_refptr<CreateSessionDescriptionObserver>(observer),
options](std::function<void()> operations_chain_callback) {
// Abort early if |this_weak_ptr| is no longer valid.
if (!this_weak_ptr) {
observer_refptr->OnFailure(
RTCError(RTCErrorType::INTERNAL_ERROR,
"CreateOffer failed because the session was shut down"));
operations_chain_callback();
return;
}
// The operation completes asynchronously when the wrapper is invoked.
rtc::scoped_refptr<CreateSessionDescriptionObserverOperationWrapper>
observer_wrapper(new rtc::RefCountedObject<
CreateSessionDescriptionObserverOperationWrapper>(
std::move(observer_refptr),
std::move(operations_chain_callback)));
this_weak_ptr->DoCreateOffer(options, observer_wrapper);
});
}
这里有一个非常关键的设计:
OperationsChain。 W3C 规范要求所有信令操作(CreateOffer / CreateAnswer / SetLocalDescription / SetRemoteDescription)必须串行执行,不允许并发。 OperationsChain 是一个操作串行队列: 如果当前没有进行中的操作,新操作立即执行 如果有操作正在进行,新操作排队等待 每个操作完成后,必须调用 operations_chain_callback() 推进队列 这就是为什么代码里用了 weak_ptr + wrapper: CreateSessionDescriptionObserverOperationWrapper 在 OnSuccess/OnFailure 被调用时,自动触发 operations_chain_callback(),推进下一个排队操作。
六、第三层:DoCreateOffer —— 前置校验与选项收集
// pc/sdp_offer_answer.cc
void SdpOfferAnswerHandler::DoCreateOffer(
const RTCOfferAnswerOptions& options,
scoped_refptr<CreateSessionDescriptionObserver> observer) {
RTC_DCHECK_RUN_ON(signaling_thread());
// 校验1:PeerConnection 是否已关闭
if (pc_->IsClosed()) { /* 报错返回 */ }
// 校验2:是否处于 session error 状态
if (session_error() != SessionError::kNone) { /* 报错返回 */ }
// 校验3:options 参数合法性检查
if (!ValidateOfferAnswerOptions(options)) { /* 报错返回 */ }
// 兼容处理 Plan B 遗留选项
if (IsUnifiedPlan()) {
HandleLegacyOfferOptions(options); // 转换为 Transceiver direction 操作
}
// 核心:收集媒体会话选项
cricket::MediaSessionOptions session_options;
GetOptionsForOffer(options, &session_options);
// 交给工厂生成 SDP
webrtc_session_desc_factory_->CreateOffer(observer, options, session_options);
}
GetOptionsForOffer 是这一层最重要的函数。
它的职责是把 PeerConnection 当前的媒体状态(所有 Transceiver 的信息) 转换成 cricket::MediaSessionOptions,作为 SDP 生成的"输入规格"。 在 Unified Plan 下,它调用 GetOptionsForUnifiedPlanOffer, 遍历所有 RtpTransceiver,为每一个生成一条 m= section 的描述,包括: direction(sendrecv / sendonly / recvonly / inactive) mid(媒体标识) ICE Restart 标志 ICE renomination 策略 RTCP CNAME 加密选项(crypto_options) 预分配的 ICE 凭据池(pooled_ice_credentials)
七、第四层:WebRtcSessionDescriptionFactory —— DTLS 证书依赖
// pc/webrtc_session_description_factory.h
// This class is used to create offer/answer session description.
// Certificates for WebRtcSession/DTLS are either supplied at construction
// or generated asynchronously.
// It queues the create offer/answer request until the
// certificate generation has completed.
class WebRtcSessionDescriptionFactory : public rtc::MessageHandler {
// CreateOffer 请求入队,等待证书就绪
void CreateOffer(
CreateSessionDescriptionObserver* observer,
const RTCOfferAnswerOptions& options,
const cricket::MediaSessionOptions& session_options);
};
WebRtcSessionDescriptionFactory 解决了一个关键问题: DTLS 证书可能还没生成完。 SDP Offer 中必须包含 a=fingerprint 字段(DTLS 证书指纹), 但证书生成是异步的(调用系统加密 API,可能耗时数十毫秒)。 工厂的处理策略是: 证书已就绪 → 立即生成 SDP,同步回调 OnSuccess 证书生成中 → 将请求放入 create_session_description_requests_ 队列, 等 OnCertificateReady 信号触发后,批量处理队列中的请求
CreateOffer 请求
│
▼
证书是否就绪?
├── 是 → 直接调用 MediaSessionDescriptionFactory::CreateOffer()
└── 否 → 加入请求队列,等待 OnCertificateReady 信号
│
▼ (证书生成完毕)
处理队列中的所有请求
这就是为什么第一次调用 CreateOffer 有时会比后续调用稍慢—— 首次调用可能需要等待证书异步生成完成。
问题1:证书生成触发时机在哪里?见最后
八、MediaSessionDescriptionFactory::CreateOffer
这个函数是整个CreateOffer的核心,也是真正"写内容"的地方,总共干了六件事
MediaSessionDescriptionFactory::CreateOffer()
│
├── ① 收集全局编解码器列表(GetCodecsForOffer)
├── ② 收集全局 RTP Header Extension 列表
├── ③ 遍历每个 m= section,分别处理
│ ├── 音频 → AddAudioContentForOffer()
│ ├── 视频 → AddVideoContentForOffer()
│ └── 数据 → AddDataContentForOffer()
│
├── ④ 每个 m= section 内部又做了:
│ ├── 过滤编解码器(按方向/偏好)
│ ├── 分配 SSRC(流标识)
│ ├── 写入传输信息(AddTransportOffer)
│ │ └── ICE ufrag/pwd + DTLS 指纹
│ └── 写入 direction(sendrecv/sendonly...)
│
├── ⑤ 处理 BUNDLE(合并传输通道)
└── ⑥ 设置 MSID 信令方式(Unified Plan / Plan B 兼容)
① 收集全局编解码器列表
// 第一步:从当前已有描述(上次协商)里继承 payload type 映射
// 第二步:把本端所有支持的编解码器合并进来,避免 payload type 冲突
GetCodecsForOffer(current_active_contents,
&offer_audio_codecs,
&offer_video_codecs);
这一步解决了一个核心问题:payload type 全局唯一。 如果上次协商音频用了 PT=111 表示 Opus, 这次重新协商时必须继续用 111,不能换,否则对端会解码失败。
② 收集 RTP Header Extension(带 ID 分配)
AudioVideoRtpHeaderExtensions extensions_with_ids =
GetOfferedRtpHeaderExtensionsWithIds(
current_active_contents,
session_options.offer_extmap_allow_mixed,
session_options.media_description_options);
同样的逻辑:每个 Header Extension 有一个 extmap ID(1-14), 需要在多次协商中保持稳定,这里负责分配和复用。
③ 遍历每个 m= section 逐一填充
for (auto& media_description_options : session_options.media_description_options) {
switch (media_description_options.type) {
case MEDIA_TYPE_AUDIO: AddAudioContentForOffer(...); break;
case MEDIA_TYPE_VIDEO: AddVideoContentForOffer(...); break;
case MEDIA_TYPE_DATA: AddDataContentForOffer(...); break;
}
}
每个 m= section 的处理流程几乎一样,以音频为例:
AddAudioContentForOffer()
│
├── 按 direction 过滤编解码器
│ sendonly → 只保留编码器支持的 codec
│ recvonly → 只保留解码器支持的 codec
│ sendrecv → 两者取交集
│
├── 如果设置了 codec_preferences → 按偏好顺序排列
│
├── VAD 开关 → 是否保留 CN(舒适噪音)codec
│
├── CreateMediaContentOffer()
│ ├── 写入 codec 列表(带 PT 映射)
│ ├── 分配 SSRC(每路流的唯一标识)
│ └── 写入 RTP Header Extension
│
├── 设置传输协议(UDP/TLS/RTP/SAVPF,即 DTLS-SRTP)
│
├── 写入 direction(sendrecv / sendonly / ...)
│
└── AddTransportOffer() ← 写入 ICE + DTLS 信息
├── ice-ufrag / ice-pwd(ICE 凭据)
└── a=fingerprint(DTLS 证书指纹)
a=setup:actpass(DTLS 角色)
④ AddTransportOffer:写入 ICE 和 DTLS 信息
bool MediaSessionDescriptionFactory::AddTransportOffer(...) {
// 调用 TransportDescriptionFactory::CreateOffer()
// 生成该 m= section 的 TransportDescription
std::unique_ptr<TransportDescription> new_tdesc(
transport_desc_factory_->CreateOffer(
transport_options, current_tdesc, ice_credentials));
// 写入 offer
offer_desc->AddTransportInfo(TransportInfo(content_name, *new_tdesc));
}
TransportDescription 包含:
-
ice_ufrag / ice_pwd(每次协商或 ICE Restart 时重新生成)
-
identity_fingerprint(DTLS 证书的 SHA-256 指纹)
-
connection_role(actpass:发起方默认,可以主动也可以被动)
⑤ 处理 BUNDLE
if (session_options.bundle_enabled) {
ContentGroup offer_bundle(GROUP_TYPE_BUNDLE);
for (const ContentInfo& content : offer->contents()) {
offer_bundle.AddContentName(content.name); // 把所有 mid 加进去
}
offer->AddGroup(offer_bundle);
// BUNDLE 下多个 m= section 共用同一个 ICE/DTLS 传输
// 需要合并传输信息,确保只有第一个 m= section 的 ICE 凭据生效
UpdateTransportInfoForBundle(offer_bundle, offer.get());
UpdateCryptoParamsForBundle(offer_bundle, offer.get());
}
BUNDLE 开启后,SDP 里会出现:
a=group:BUNDLE audio video data
意味着音视频数据共用同一条 ICE/DTLS 通道,大幅减少候选对数量。
⑥ 设置 MSID 信令方式(兼容性处理)
if (is_unified_plan_) {
// 同时用 a=msid 和 a=ssrc 两种方式写 MSID
// 确保新旧端点都能识别
offer->set_msid_signaling(
kMsidSignalingMediaSection | kMsidSignalingSsrcAttribute);
} else {
// Plan B 只用 a=ssrc 方式
offer->set_msid_signaling(kMsidSignalingSsrcAttribute);
}
这一步的在整个CreateOffer里的整体位置大概如下图所示:
CreateOffer 的输入:MediaSessionOptions
(来自 GetOptionsForOffer 对 Transceiver 的遍历结果)
│
▼
MediaSessionDescriptionFactory::CreateOffer()
│
├── 全局 codec 列表(保持 PT 稳定)
├── 全局 Header Extension ID 分配
│
├── 音频 m= section
│ codec过滤 → SSRC分配 → ICE凭据 → DTLS指纹 → direction
│
├── 视频 m= section
│ codec过滤 → SSRC分配 → ICE凭据 → DTLS指纹 → direction
│
├── BUNDLE 合并传输通道
│
└── MSID 兼容性写入
│
▼
cricket::SessionDescription(内存对象)
│
▼
JsepSessionDescription(包装为 SDP 字符串接口)
│
▼
observer->OnSuccess() → 应用层拿到 SDP Offer
这一层是 CreateOffer 整个调用链里唯一真正"写内容"的地方,上面所有层次都是在做调度、排队、校验和参数收集,到这里才真正把一条条 SDP 属性组装成 cricket::SessionDescription 对象。
九、最终产物:SDP Offer 长什么样
经过以上四层处理,最终生成的 SDP Offer 大致如下:
v=0
o=- 123456 2 IN IP4 127.0.0.1
s=-
t=0 0
// BUNDLE:音视频复用同一 ICE/DTLS 通道
a=group:BUNDLE audio video
// 音频 m= section
m=audio 9 UDP/TLS/RTP/SAVPF 111
a=mid:audio
a=sendrecv ← Transceiver direction
a=rtpmap:111 opus/48000/2
a=ice-ufrag:abcd ← ICE 凭据
a=ice-pwd:efghijklmnopqrstuvwxyz
a=fingerprint:sha-256 XX:XX:... ← DTLS 证书指纹
a=setup:actpass ← DTLS 角色
// 视频 m= section
m=video 9 UDP/TLS/RTP/SAVPF 96
a=mid:video
a=sendrecv
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 nack ← NACK 重传能力声明
a=rtcp-fb:96 nack pli ← PLI 关键帧请求
a=ice-ufrag:abcd ← 复用同一 ICE 凭据(BUNDLE)
十、完整流程总结
应用层
pc->CreateOffer(observer, options)
│
▼ [Signaling Thread 断言]
PeerConnection::CreateOffer()
│ 委托 sdp_handler_
▼
SdpOfferAnswerHandler::CreateOffer()
│ 入 OperationsChain 串行队列(W3C 规范要求)
▼
DoCreateOffer()
│
├── 前置校验(PC是否关闭 / session_error / options合法性)
│
├── HandleLegacyOfferOptions()
│ └── Plan B 兼容:转换为 Transceiver direction 操作
│
├── GetOptionsForOffer()
│ └── 遍历所有 Transceiver,生成 MediaSessionOptions
│ 包含:direction / ICE凭据 / 加密选项 / RTCP CNAME
│
└── WebRtcSessionDescriptionFactory::CreateOffer()
│
├── DTLS 证书未就绪 → 排队等待
│
└── 证书就绪 → MediaSessionDescriptionFactory::CreateOffer()
│ 生成 cricket::SessionDescription
▼
observer->OnSuccess(SessionDescriptionInterface*)
│
└── 应用层收到 SDP Offer
→ 调用 SetLocalDescription()
十一、小结
CreateOffer 在应用层代码就一行调用,背后是线程安全、操作串行、证书异步、Transceiver 遍历 四重机制的协同运作。这也是 WebRTC 复杂性的一个缩影,这也是WebRTC让人痴迷的一个原因。
十二、问题1:证书生成触发时机
证书生成触发时机:
在 WebRtcSessionDescriptionFactory 构造时 答案不在 CreateOffer 里,而是更早——在 PeerConnection 创建时就已经触发了。
触发点:构造函数
pc/webrtc_session_description_factory.cc
WebRtcSessionDescriptionFactory::WebRtcSessionDescriptionFactory(
...
std::unique_ptr<rtc::RTCCertificateGeneratorInterface> cert_generator,
const rtc::scoped_refptr<rtc::RTCCertificate>& certificate,
...) {
if (!dtls_enabled) {
// DTLS 未开启,不需要证书,直接结束
return;
}
if (certificate) {
// 情况①:外部直接传入了证书(例如测试场景)
// 通过 PostMessage 异步设置,让调用方先连接信号槽
certificate_request_state_ = CERTIFICATE_WAITING;
signaling_thread_->Post(RTC_FROM_HERE, this,
MSG_USE_CONSTRUCTOR_CERTIFICATE, ...);
} else {
// 情况②:没有证书 → 立即异步生成
certificate_request_state_ = CERTIFICATE_WAITING;
cert_generator_->GenerateCertificateAsync(key_params,
absl::nullopt, callback);
}
}
三种状态流转
PeerConnection 创建
│
▼
WebRtcSessionDescriptionFactory 构造
│
├── dtls_enabled = false
│ → certificate_request_state_ = CERTIFICATE_NOT_NEEDED
│ → CreateOffer 直接走 InternalCreateOffer()
│
├── 外部传入 certificate
│ → certificate_request_state_ = CERTIFICATE_WAITING
│ → Post(MSG_USE_CONSTRUCTOR_CERTIFICATE)
│ → 异步回调 SetCertificate()
│
└── 无证书(默认情况)
→ certificate_request_state_ = CERTIFICATE_WAITING
→ cert_generator_->GenerateCertificateAsync() ← 真正的异步生成
→ 生成完成回调 SetCertificate()
CreateOffer 时如何判断证书状态
pc/webrtc_session_description_factory.cc
void WebRtcSessionDescriptionFactory::CreateOffer(...) {
// 证书生成失败 → 直接报错
if (certificate_request_state_ == CERTIFICATE_FAILED) {
PostCreateSessionDescriptionFailed(...);
return;
}
CreateSessionDescriptionRequest request(...);
if (certificate_request_state_ == CERTIFICATE_WAITING) {
// 证书还没好 → 请求入队等待
create_session_description_requests_.push(request);
} else {
// CERTIFICATE_SUCCEEDED 或 NOT_NEEDED → 直接执行
InternalCreateOffer(request);
}
}
证书就绪后:批量消费队列
pc/webrtc_session_description_factory.cc
void WebRtcSessionDescriptionFactory::SetCertificate(
const rtc::scoped_refptr<rtc::RTCCertificate>& certificate) {
certificate_request_state_ = CERTIFICATE_SUCCEEDED;
// 把证书设置到 TransportDescriptionFactory(用于写入 SDP 指纹)
transport_desc_factory_.set_certificate(certificate);
transport_desc_factory_.set_secure(cricket::SEC_ENABLED);
// 消费队列中所有排队的 CreateOffer / CreateAnswer 请求
while (!create_session_description_requests_.empty()) {
if (front.type == kOffer)
InternalCreateOffer(front);
else
InternalCreateAnswer(front);
create_session_description_requests_.pop();
}
}
以上就是 CreateOffer 从调用到产出的完整核心流程。
回顾整个链路: 从应用层一行 pc->CreateOffer() 出发,经过 OperationsChain 串行队列保障不与其他信令操作并发,经过 前置校验与选项收集,把所有 Transceiver 的媒体状态翻译成参数,经过 DTLS 证书依赖管理,确保指纹写入时证书已就绪,最终由 MediaSessionDescriptionFactory 逐一组装每个 m= section,写入编解码器、ICE 凭据、DTLS 指纹、SSRC、方向、BUNDLE 信息…… 一份完整的 SDP Offer 就此诞生,通过 OnSuccess 回调交到你手上。
拿到 SDP 之后,你还需要做三件事:
-
调用 SetLocalDescription(offer) —— 把这份 Offer 设置为本地描述,同时触发 ICE 候选收集
-
通过信令通道把 SDP 发送给对端
-
等待对端的 Answer 回来,调用 SetRemoteDescription(answer) 完成协商
CreateOffer 只是信令协商的第一步,它生产了一张"名片", 真正的连接建立,还要等 ICE 打通、DTLS 握手完成之后才算数。
下一篇,下一篇我们深入 SetLocalDescription SDP 落地的那一刻,libwebrtc 内部发生了什么?