5、接收Answer:setRemoteDescription方法、远程SDP解析、媒体轨道与收发器(Transceiver)的建立

好,咱们接着聊。上一节我们把本地 Offer 发出去了,现在轮到对端回复 Answer。你作为发起方,收到这个 Answer 之后要做什么?

说白了就三步:收下来、解析它、把媒体通道搭起来。这三步在 WebRTC 里对应一个核心方法——setRemoteDescription。我个人习惯把它叫做「远程 SDP 落地」。

5.1 setRemoteDescription 到底干了什么?

先看代码,最典型的调用方式是这样的:

const answer = {
  type: 'answer',
  sdp: 'v=0\r\no=- ...'
};

await pc.setRemoteDescription(new RTCSessionDescription(answer));

你可能会想:这不就是把对端发来的 SDP 字符串塞进去吗?有什么好讲的?

嗯,表面上看确实是这样。但底层发生的事情,比你想象的多得多。我简单列一下:

  • 验证 SDP 合法性——检查 type 是不是 answer,检查格式对不对
  • 匹配 Offer/Answer——把远程 SDP 和本地 Offer 做对照,确保媒体线能对上
  • 触发 Transceiver 建立——这是最核心的一步,后面细说
  • 更新 ICE 候选收集状态——告诉 ICE 层:对端信息到了,可以开始连通性检查

关键点:setRemoteDescription 不是简单的赋值操作。它是一次状态机迁移。调用成功后,SignalingState 会从 "have-local-offer" 变为 "stable"。

5.2 远程 SDP 解析——浏览器在背后做了什么?

你传进去的是一个 RTCSessionDescription 对象,但浏览器拿到之后会做深度解析。我当年刚接触 WebRTC 时,以为 SDP 就是个文本,浏览器直接拿去用就完了。后来踩了坑才明白——浏览器会把 SDP 拆成一个个媒体描述(Media Description)

举个例子,假设远程 Answer 的 SDP 长这样:

m=audio 49170 UDP/TLS/RTP/SAVPF 111 103
a=rtpmap:111 opus/48000/2
a=rtpmap:103 ISAC/16000
a=mid:0
a=sendrecv

m=video 49172 UDP/TLS/RTP/SAVPF 96 97
a=rtpmap:96 H264/90000
a=rtpmap:97 VP8/90000
a=mid:1
a=recvonly

浏览器解析时会做这几件事:

  1. 按 m= 行拆分——每一条 m= 行对应一个媒体轨道或数据通道
  2. 提取编解码器列表——比如 audio 支持 opus 和 ISAC,video 支持 H264 和 VP8
  3. 解析媒体方向——sendrecv、recvonly、sendonly、inactive
  4. 提取 mid——用于和本地 Offer 的媒体线做匹配

我的经验:如果你在调试时发现媒体没通,第一步就是去浏览器控制台打印 pc.getTransceivers(),看看每个 Transceiver 的 mid 和方向对不对得上。我曾经花了一整天排查一个问题,最后发现是对端把 mid 写错了。

5.3 媒体轨道与收发器(Transceiver)的建立

好,重头戏来了。setRemoteDescription 之后,浏览器会做一件非常重要的事——为每一条远程媒体线创建或匹配 Transceiver

什么是 Transceiver?你可以把它理解成「一个媒体通道的控制器」。每个 Transceiver 包含:

  • 一个 sender(发送端)——负责发送本地媒体
  • 一个 receiver(接收端)——负责接收远程媒体
  • 一个 mid——唯一标识
  • 一个 direction——方向(sendrecv / sendonly / recvonly / inactive)

我画了一张图,帮你理解这个过程:

远程 Answer SDP m=audio ... (mid:0) sendrecv m=video ... (mid:1) recvonly m=application ... (mid:2) 解析 SDP 解析器 • 按 m= 行拆分 • 提取编解码器 • 解析 direction • 提取 mid • 匹配本地 Offer • 创建/匹配 Transceiver 生成 Transceiver 列表 Transceiver #0 mid:0 | audio | sendrecv Transceiver #1 mid:1 | video | recvonly Transceiver #2 mid:2 | data | —

你想想看,为什么要有 Transceiver 这个东西?直接搞个 sender 和 receiver 不行吗?

嗯,原因其实很实际——WebRTC 需要支持动态的媒体流增删。比如你正在视频通话,突然想共享屏幕,这时候就需要新增一个视频轨道。如果没有 Transceiver 这种抽象层,你就要重新走一遍 Offer/Answer 流程,非常麻烦。

有了 Transceiver,你可以直接调用 pc.addTrack()pc.addTransceiver(),浏览器会自动帮你管理 SDP 的协商。

注意:setRemoteDescription 之后,Transceiver 的 direction 可能会被对端覆盖。比如你本地设置的是 sendrecv,但对端 Answer 里写的是 recvonly,那最终生效的是 recvonly。我曾经在这个问题上吃过亏——本地摄像头明明开着,但对方就是看不到画面,查了半天发现是对端把方向写错了。

5.4 实际开发中的几个关键点

聊了这么多原理,咱们落地到实际开发。我总结几个你在写代码时一定会遇到的情况:

场景 做法 说明
收到 Answer 后获取远程视频 pc.ontrack = (e) => { video.srcObject = e.streams[0]; } track 事件会在 setRemoteDescription 之后触发
查看当前所有 Transceiver pc.getTransceivers() 返回数组,每个元素包含 sender、receiver、mid、direction
修改 Transceiver 方向 transceiver.direction = 'sendonly' 需要在 createOffer 之前修改才有效
停止某个 Transceiver transceiver.stop() 停止后不可恢复,慎用

一个小技巧:如果你想知道远程 SDP 里到底有哪些媒体线,可以在 setRemoteDescription 之前先打印一下 SDP 字符串。我经常用这个方法来排查「为什么对方的声音没传过来」这类问题。

5.5 总结一下

setRemoteDescription 接收 Answer,说白了就是一次「对端能力确认」的过程。浏览器拿到远程 SDP 后,会解析出每一条媒体线,然后和本地已经存在的 Transceiver 做匹配。匹配上了,媒体通道就建立起来了;匹配不上,那就等着报错吧。

我个人觉得,理解 Transceiver 是理解 WebRTC 媒体协商的关键。你只要搞懂了「一条媒体线对应一个 Transceiver,每个 Transceiver 管一对 sender/receiver」,后面再学重协商、Simulcast 这些东西就会轻松很多。


公众号:蓝海资料掘金营,微信deep3321