PeerConnection 深度解析一:CreateOffer

0 阅读13分钟

源码路径:

对应规范:W3C WebRTC 1.0 §4.4.1

createOffer 在整个信令流程中有什么作用或者说为什么需要调用这个函数,刚开始了解 webrtc的人都会有这个疑问?

想象你要发起一场视频会议,CreateOffer 就是起草一份会议邀请函的过程。

这份"邀请函"里写了什么

  1. 我支持的视频格式:H.264、VP8、VP9我支持的音频格式:Opus、G711

  2. 我的网络入口地址:(等 ICE 收集)

  3. 我的身份证明:(DTLS 证书指纹)

  4. 我们用同一条通道传音视频吗:是(BUNDLE)

  5. 我打算:既发送也接收(sendrecv)

这就是 SDP Offer 的本质——一份本端能力的自我声明。

简单来说CreateOffer 干了四件事:

  1. 排队, 确保不和其他信令操作撞车

  2. 收集能力, 遍历所有 Track/Transceiver我要发什么?收什么?

  3. 等证书, DTLS 证书没好就排队 证书好了才能写入指纹

  4. 拼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 之后,你还需要做三件事:

  1. 调用 SetLocalDescription(offer) —— 把这份 Offer 设置为本地描述,同时触发 ICE 候选收集

  2. 通过信令通道把 SDP 发送给对端

  3. 等待对端的 Answer 回来,调用 SetRemoteDescription(answer) 完成协商

CreateOffer 只是信令协商的第一步,它生产了一张"名片", 真正的连接建立,还要等 ICE 打通、DTLS 握手完成之后才算数。

下一篇,下一篇我们深入 SetLocalDescription SDP 落地的那一刻,libwebrtc 内部发生了什么?

c1cbf8e09b18433224886f004bd0a404.png