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
浏览器解析时会做这几件事:
- 按 m= 行拆分——每一条 m= 行对应一个媒体轨道或数据通道
- 提取编解码器列表——比如 audio 支持 opus 和 ISAC,video 支持 H264 和 VP8
- 解析媒体方向——sendrecv、recvonly、sendonly、inactive
- 提取 mid——用于和本地 Offer 的媒体线做匹配
我的经验:如果你在调试时发现媒体没通,第一步就是去浏览器控制台打印 pc.getTransceivers(),看看每个 Transceiver 的 mid 和方向对不对得上。我曾经花了一整天排查一个问题,最后发现是对端把 mid 写错了。
5.3 媒体轨道与收发器(Transceiver)的建立
好,重头戏来了。setRemoteDescription 之后,浏览器会做一件非常重要的事——为每一条远程媒体线创建或匹配 Transceiver。
什么是 Transceiver?你可以把它理解成「一个媒体通道的控制器」。每个 Transceiver 包含:
- 一个 sender(发送端)——负责发送本地媒体
- 一个 receiver(接收端)——负责接收远程媒体
- 一个 mid——唯一标识
- 一个 direction——方向(sendrecv / sendonly / recvonly / inactive)
我画了一张图,帮你理解这个过程:
你想想看,为什么要有 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 这些东西就会轻松很多。