PeerConnection 深度解析三:SetRemoteDescription

0 阅读8分钟

源码路径:

  • api/peer_connection_interface.h

  • pc/peer_connection.cc

  • pc/sdp_offer_answer.cc

对应规范:W3C WebRTC 1.0 §4.6.2

一、W3C 规范怎么定义 SetRemoteDescription

W3C 规范对 setRemoteDescription() 的核心要求:

将对端的 SDP 描述设置为远端描述,根据描述内容驱动本端 Transceiver 状态更新、Track 创建/移除、信令状态机推进。

与 SetLocalDescription 相比,SetRemoteDescription 多了一项关键职责: 触发 OnTrack / OnAddTrack 回调,通知应用层对端的媒体轨道已就绪。

规范要求:

  • 同样必须通过 OperationsChain 串行执行

  • 设置成功后信令状态按规则推进

  • Offer 时创建/更新 Transceiver,并触发 ontrack 事件

  • Answer 时确认方向,绑定 DtlsTransport

  • 支持 Implicit Rollback(Unified Plan 下收到新 Offer 时自动回滚旧 Offer)

二、接口签名

// api/peer_connection_interface.h
virtual void SetRemoteDescription(
    std::unique_ptr<SessionDescriptionInterface> desc,
    rtc::scoped_refptr<SetRemoteDescriptionObserverInterface> observer) = 0;

与 SetLocalDescription 对称,同样是 Observer 异步回调。

三、完整调用链

应用层
  pc->SetRemoteDescription(desc, observer)
          │
          ▼  [Signaling Thread 断言]
  PeerConnection::SetRemoteDescription()
          │  委托 sdp_handler_
          ▼
  SdpOfferAnswerHandler::SetRemoteDescription()
          │  入 OperationsChain 串行队列
          ▼
  SdpOfferAnswerHandler::DoSetRemoteDescription()
          │
          ├── ① Implicit Rollback 处理
          ├── ② FillInMissingRemoteMids(兼容旧端点)
          ├── ③ ValidateSessionDescription(SDP 合法性校验)
          ├── ④ ApplyRemoteDescription(核心落地)
          │       ├── 更新描述槽位(pending/current)
          │       ├── PushdownTransportDescription(ICE+DTLS 下推)
          │       ├── UpdateTransceiversAndDataChannels(Transceiver 同步)
          │       ├── UpdateSessionState(状态机推进 + 媒体参数下推)
          │       ├── UseCandidatesInSessionDescription(应用内嵌候选)
          │       ├── CheckForRemoteIceRestart(ICE Restart 检测)
          │       ├── IceConnectionState → Checking
          │       └── OnTrack / OnAddTrack / OnRemoveTrack 回调触发
          │
          ├── ⑤ Answer 时 RemoveStoppedTransceivers
          └── ⑥ observer->OnSetRemoteDescriptionComplete(OK)

四、DoSetRemoteDescription:入口与前置处理

void SdpOfferAnswerHandler::DoSetRemoteDescription(
    std::unique_ptr<SessionDescriptionInterface> desc,
    rtc::scoped_refptr<SetRemoteDescriptionObserverInterface> observer) {
  RTC_DCHECK_RUN_ON(signaling_thread());
  // ...
if (IsUnifiedPlan()) {
    if (pc_->configuration()->enable_implicit_rollback) {
      if (desc->GetType() == SdpType::kOffer &&
          signaling_state() == PeerConnectionInterface::kHaveLocalOffer) {
        Rollback(desc->GetType());   // 隐式回滚本端 Offer
      }
    }
    if (desc->GetType() == SdpType::kRollback) {
      observer->OnSetRemoteDescriptionComplete(Rollback(desc->GetType()));
      return;
    }
  }
  // 兼容没有 a=mid 的旧端
  FillInMissingRemoteMids(desc->description());

  std::map<std::string, const cricket::ContentGroup*> bundle_groups_by_mid =
      GetBundleGroupsByMid(desc->description());
  RTCError error = ValidateSessionDescription(desc.get(), cricket::CS_REMOTE,
                                              bundle_groups_by_mid);
  // ...
  error = ApplyRemoteDescription(std::move(desc), bundle_groups_by_mid);

4.1 Implicit Rollback(隐式回滚)

触发条件:enable_implicit_rollback = true + 收到远端 Offer + 本端当前是 have-local-offer

libwebrtc 直接调用 Rollback(SdpType::kOffer),将本端的 pending_local_description_ 清空,SignalingState 退回 stable,再接着处理远端 Offer。

// Explicit rollback.
    if (desc->GetType() == SdpType::kRollback) {
      observer->OnSetRemoteDescriptionComplete(Rollback(desc->GetType()));
      return;
    }

这是 W3C 规范 §4.5.1 中定义的"完美协商(Perfect Negotiation)"模式的基础支撑。 当双端同时发起 Offer 产生冲突时,礼貌方(polite peer) 通过 Implicit Rollback 自动回滚自己的 Offer,接受对端的 Offer。

4.2 FillInMissingRemoteMids(兼容旧端点)

FillInMissingRemoteMids(desc->description());

部分老版本 WebRTC 实现不在 SDP 里写 a=mid,此函数按 m= section 顺序自动补全 MID,保证后续所有按 MID 查找的逻辑不出错。

4.3 ValidateSessionDescription

校验内容包括:

  • SDP 类型与当前 SignalingState 是否匹配

  • BUNDLE 策略与 SDP 里 BUNDLE group 是否一致

  • Unified Plan 下 m= section 数量兼容性

五、ApplyRemoteDescription:核心落地

5.1 更新描述槽位

// pc/sdp_offer_answer.cc
if (type == SdpType::kAnswer) {
    // Answer:pending → current,协商完成
    current_remote_description_ = std::move(desc);
    pending_remote_description_ = nullptr;
    // 本端 pending local 也同步变为 current
    current_local_description_ = std::move(pending_local_description_);
} else {
    // Offer:存入 pending,等待 Answer
    pending_remote_description_ = std::move(desc);
    pending_remote_description_ = std::move(desc);
}

关键:Answer 阶段 pending_local_description_ 同时晋升为 current,意味着本端 Offer 与远端 Answer 同时固化,协商完成。

5.2 PushdownTransportDescription:ICE+DTLS 下推

RTCError error = PushdownTransportDescription(cricket::CS_REMOTE, type);

将 SDP 里的传输参数(ICE ufrag/pwd、DTLS fingerprint、加密套件)下推给 JsepTransportController。

  • Offer 阶段:建立 JsepTransport 对象(ICE + DTLS),但 DTLS 角色尚未确定(等 Answer)

  • Answer 阶段:确定 DTLS 角色(active/passive),握手正式开始

  • 对端的 ICE ufrag / pwd(用于 Connectivity Check 时验证对端身份)

  • 对端的 DTLS 证书指纹(a=fingerprint,用于 DTLS 握手时验证对端证书)

  • 对端的 DTLS 角色(a=setup,与本端角色互补)

5.3 UpdateTransceiversAndDataChannels:

Transceiver 同步 Offer 到来时,根据SDP中的 m= section 创建或匹 配 Transceiver:

RTCError error = UpdateTransceiversAndDataChannels(
    cricket::CS_REMOTE, *remote_description(),
    local_description(), old_remote_description,
    bundle_groups_by_mid);

具体逻辑:

  • 遍历 remote SDP 的每个 m= section

  • 按 mid 查找已有 Transceiver,找不到则新建 Transceiver

  • 更新 Transceiver 的 mid、mline_index

  • Answer 时绑定 DtlsTransport 到 Sender/Receiver

const ContentInfos& new_contents = new_session.description()->contents();
for (size_t i = 0; i < new_contents.size(); ++i) {
  const cricket::ContentInfo& new_content = new_contents[i];
  cricket::MediaType media_type = new_content.media_description()->type();
  // ...
  auto transceiver_or_error =
      AssociateTransceiver(source, new_session.GetType(), i, new_content,
                           old_local_content, old_remote_content);
  // ...
  RTCError error =
      UpdateTransceiverChannel(transceiver, new_content, bundle_group);

AssociateTransceiver 的匹配规则(CS_REMOTE 时):

  } else {
    RTC_DCHECK_EQ(source, cricket::CS_REMOTE);
    // 优先按 MID 查找已有 transceiver
    if (!transceiver &&
        RtpTransceiverDirectionHasRecv(media_desc->direction()) &&
        !media_desc->HasSimulcast()) {
      transceiver = FindAvailableTransceiverToReceive(media_desc->type());
    }
    // 还找不到 → 新建一个 recvonly transceiver
    if (!transceiver) {
      // ...
      transceiver = rtp_manager()->CreateAndAddTransceiver(sender, receiver);
      transceiver->internal()->set_direction(
          RtpTransceiverDirection::kRecvOnly);
      if (type == SdpType::kOffer) {
        transceivers()->StableState(transceiver)->set_newly_created();
      }
    }
  }

匹配优先级(远端 source):

| 优先级 | 查找方式 | 条件 | | --- | --- | --- | | 1 | 按 MID 精确匹配 | transceivers()->FindByMid(content.name) | | 2 | 按方向查空闲 transceiver | 远端方向含 Recv + 无 Simulcast | | 3 | 新建 recvonly transceiver | 以上都找不到 |

UpdateTransceiverChannel:创建/销毁媒体 Channel

RTCError SdpOfferAnswerHandler::UpdateTransceiverChannel(
    rtc::scoped_refptr<...> transceiver,
    const cricket::ContentInfo& content,
    ...) {
  cricket::ChannelInterface* channel = transceiver->internal()->channel();
if (content.rejected) {
    if (channel) {
      transceiver->internal()->SetChannel(nullptr);
      DestroyChannelInterface(channel);   // m= section 被拒绝,销毁 channel
    }
  } else {
    if (!channel) {
      if (transceiver->media_type() == cricket::MEDIA_TYPE_AUDIO) {
        channel = CreateVoiceChannel(content.name);  // 新建音频 channel
      } else {
        channel = CreateVideoChannel(content.name);  // 新建视频 channel
      }
      transceiver->internal()->SetChannel(channel);
    }
  }
return RTCError::OK();
}

这一步意味着:收到远端 Offer 后,libwebrtc 立刻为每个非 rejected 的 m= section 创建对应的 VoiceChannel 或 VideoChannel,媒体传输通道的骨架在此建立。

5.4 UpdateSessionState:状态机推进

// 注意:candidates 收集由 SetLocalDescription 触发,不是这里
// NOTE: Candidates allocation will be initiated only when
//       SetLocalDescription is called.
error = UpdateSessionState(type, cricket::CS_REMOTE, ...);

对 Offer 而言:

  • SignalingState 切换到 have-remote-offer

  • ICE 不在这里启动(ICE 收集在 SetLocalDescription 时由本端触发)

  • 为后续 Answer 的 DTLS 角色协商准备上下文

对 Answer 而言:

  • SignalingState 切换回 stable

  • DTLS 角色确定(active/passive),DTLS 握手正式开始

  • ICE 进入 checking 状态

SDP 类型 信令状态变化

| SDP 类型 | 调用前状态 | 调用后状态 | | --- | --- | --- | | kOffer | stable | have-remote-offer | | kAnswer | have-local-offer | stable (协商完成) |

Answer 时同样调用 EnableSending() 开启媒体发送, 并通过 PushdownMediaDescription 将媒体参数下推到底层编解码器。

5.5 UseCandidatesInSessionDescription:应用内嵌候选

if (local_description() &&
    !UseCandidatesInSessionDescription(remote_description())) {
  LOG_AND_RETURN_ERROR(...);
}

条件:必须已有 local_description()(即本端已 SetLocalDescription)。

如果 Offer SDP 里内嵌了 ICE candidates(非 Trickle ICE 场景,整包发送),此函数将这些 candidates 一次性推入 ICE agent。这也是为什么必须先有 local description 才能应用 candidates —— ICE transport 是在 local description 设置时创建的。 这是非 Trickle ICE 模式下对端候选应用的唯一入口。

5.6 CheckForRemoteIceRestart:检测 ICE Restart

if (CheckForRemoteIceRestart(old_remote_description,
                              remote_description(), content.name)) {
    // 对端 Offer 携带了新 ICE 凭据,标记需要 ICE Restart
    pending_ice_restarts_.insert(content.name);
} else {
    // 没有 ICE Restart,把旧候选复制到新描述(保留历史候选)
    CopyCandidatesFromSessionDescription(...);
}

通过比较新旧 SDP 的 ICE ufrag/pwd 来判断是否发生了 ICE Restart(对端重新协商网络路径时会更换这两个值)。

5.7 OnTrack / OnAddTrack:触发 Track 相关回调

这是 SetRemoteDescription 独有的、SetLocalDescription 没有的逻辑:

for (const auto& transceiver_ext : transceivers()->List()) {
  // ...
  RtpTransceiverDirection local_direction =
      RtpTransceiverDirectionReversed(media_desc->direction());
  // 远端 sendrecv/sendonly → 本端 local_direction 含 Recv → 触发 OnTrack
if (RtpTransceiverDirectionHasRecv(local_direction)) {
    // 关联 RemoteStream
    SetAssociatedRemoteStreams(transceiver->receiver_internal(), stream_ids,
                               &added_streams, &removed_streams);
    // 首次出现 Recv 方向,加入 now_receiving_transceivers
    if (!transceiver->fired_direction() ||
        !RtpTransceiverDirectionHasRecv(*transceiver->fired_direction())) {
      now_receiving_transceivers.push_back(transceiver);
    }
  }
  // 远端变成 sendonly/inactive → 本端不再 Recv → 触发 OnRemoveTrack
if (!RtpTransceiverDirectionHasRecv(local_direction) &&
      (transceiver->fired_direction() &&
       RtpTransceiverDirectionHasRecv(*transceiver->fired_direction()))) {
    ProcessRemovalOfRemoteTrack(...);
  }
  // 更新 FiredDirection 记录
  transceiver->set_fired_direction(local_direction);
  // Answer 阶段才固化 CurrentDirection + 绑定 DTLS transport
if (type == SdpType::kPrAnswer || type == SdpType::kAnswer) {
    transceiver->set_current_direction(local_direction);
    // 绑定 DtlsTransport 给 sender/receiver
    transceiver->sender_internal()->set_transport(dtls_transport);
    transceiver->receiver_internal()->set_transport(dtls_transport);
  }
  // 设置 SSRC,打通 RTP 接收路径
  transceiver->receiver_internal()->SetupMediaChannel(ssrc);
}
// 统一触发回调
for (const auto& transceiver : now_receiving_transceivers) {
  observer->OnTrack(transceiver);
  observer->OnAddTrack(transceiver->receiver(), ...);
}

OnTrack 触发的精确时机:

| 条件 | 触发行为 | | --- | --- | | 远端方向含 sendrecv 或 sendonly | local_direction  含 Recv → 进入候选列表 | | fired_direction  为空或之前不含 Recv | 首次 Recv,触发 OnTrack + OnAddTrack | | fired_direction  已含 Recv | 重复协商,不重复触发 | | 远端变为 sendonly/inactive | OnRemoveTrack |

关键点:OnTrack 触发的条件是 FiredDirection 的变化, 不是每次 SetRemoteDescription 都触发, 只有从"不含 recv"变为"含 recv"时才触发,避免重复回调。

六、SetRemoteDescription(Offer) vs SetRemoteDescription(Answer)

| 操作 | SetRemoteDescription(Offer) | SetRemoteDescription(Answer) | | --- | --- | --- | | 描述槽位 | → pending_remote_description_ | → current_remote_description_ | | 信令状态 | → kHaveRemoteOffer | → kStable | | Transceiver 创建 | ✅ 按 SDP m= section 创建新 Transceiver | 不创建,只更新 | | EnableSending | 不触发 | ✅ 触发,媒体可以发送 | | DtlsTransport 绑定 | 不绑定 | ✅ 绑定到 Sender / Receiver | | OnTrack 回调 | ✅ 触发(新增 recv 方向时) | ✅ 触发(方向发生变化时) | | ICE Restart 检测 | ✅ 检测并标记 | 不检测 | | RemoveStoppedTransceivers | 不触发 | ✅ 清理已停止的 Transceiver |

七、与 SetLocalDescription 的核心差异

| 对比维度 | SetLocalDescription | SetRemoteDescription | | --- | --- | --- | | 处理的 SDP 来源 | 本端生成(CreateOffer / CreateAnswer) | 对端发来 | | ICE 候选收集 | ✅ 触发(MaybeStartGathering) | ❌ 不触发(注释明确说明) | | OnTrack 回调 | ❌ 不触发 | ✅ 触发 | | Transceiver 创建 | 不创建 | ✅ Offer 时创建 | | Implicit Rollback | ❌ 不支持 | ✅ 支持 | | 应用内嵌候选 | ✅(remote desc 已存在时) | ✅(local desc 已存在时) |

八、小结

SetRemoteDescription 是 WebRTC 接收侧的核心入口, 它完成了从"收到一段 SDP 文本"到"本端媒体管道准备就绪"的全部转化: 解析对端能力,创建匹配的 Transceiver 把对端的 ICE/DTLS 参数注入传输层 驱动信令状态机推进 把对端的 Track 通过 OnTrack 通知给应用层 如果说 SetLocalDescription 是"亮出自己的名片", 那么 SetRemoteDescription 就是"读懂对方的名片,并据此调整自己的行为"。

下一篇:CreateAnswer 深度解析

  • 为什么 CreateAnswer 必须在 SetRemoteDescription(Offer) 之后才能调用?

  • Answer 的 codec 是如何从 Offer 与本端能力中取交集的?

  • m= section 数量、方向、DTLS 角色是如何由 Offer 约束的?

  • MediaSessionDescriptionFactory::CreateAnswer 内部都发生了什么?

关注我,下篇继续。

c1cbf8e09b18433224886f004bd0a404.png