SDP 与 JSEP · 会话怎么被描述和协商
SDP 是一段描述「这通电话怎么打」的文本,JSEP 是围绕它的提议/应答模型。这一章讲清 SDP 长什么样、SdpType 的四种取值(offer/pranswer/answer/rollback)、api 层的 SessionDescriptionInterface 与内部 SessionDescription 的区别,以及那个由 6 个状态构成的信令状态机。
SDP 与 JSEP · 会话怎么被描述和协商
会话建立的核心,是两个端点就「这通电话怎么打」达成一致——用什么编解码器、走什么传输、密钥指纹是什么。承载这份「约定」的,是一段叫 SDP 的文本;而围绕它进行提议 / 应答的规则,叫 JSEP。读懂下一章的 SdpOfferAnswerHandler 之前,得先弄明白这两个词。
SDP:一段描述会话的文本
SDP(Session Description Protocol,会话描述协议) 是一段纯文本,一行一个 键=值,描述一次媒体会话。WebRTC 的 SDP 大致长这样(极简片段):
v=0
o=- 46... 2 IN IP4 127.0.0.1
m=audio 9 UDP/TLS/RTP/SAVPF 111 ← 一条媒体行:音频,用 payload type 111
a=rtpmap:111 opus/48000/2 ← 111 是 Opus,48kHz 立体声
a=fingerprint:sha-256 AB:CD:... ← DTLS 证书指纹(防中间人)
a=ice-ufrag:F7g... ← ICE 用户名片段
a=ice-pwd:x9k... ← ICE 密码
a=setup:actpass ← DTLS 角色(谁当客户端)
a=sendrecv ← 收发方向每条 m= 行(media line,简称 m-line) 描述一路媒体(一路音频、一路视频……),下面的 a=(attribute)行给出细节:编解码器(rtpmap)、DTLS 指纹(第 3 部分验证身份用)、ICE 凭据(第 2 部分连通性检查用)、收发方向(sendrecv/sendonly/recvonly/inactive)。一句话:SDP 把「我这端能做什么、想怎么做」编码成文本,发给对端,对端据此回应。
JSEP:提议 / 应答的模型
WebRTC 不自己规定怎么交换 SDP,而是遵循 IETF 的 JSEP(JavaScript Session Establishment Protocol,RFC 8829)。JSEP 的核心,就是我们见过的提议 / 应答(offer/answer):一端生成 offer 描述,另一端回 answer 描述,双方各自「设本地 / 设远端」,协商即成。
JSEP 有意做了一个关键切分:
JSEP 把「协商媒体」和「传递信令」分开。 WebRTC 负责生成和消化 SDP(createOffer、setLocalDescription、setRemoteDescription);而把 SDP 从一端送到另一端,是应用的责任(信令服务器)。这个切分让 WebRTC 不绑定任何信令协议——你用 WebSocket、SIP、还是自定义协议传 SDP,它都不关心。JSEP 只定义「描述的语义」和「状态怎么变」,不定义「怎么传」。
SdpType:描述的四种身份
一段 SDP 描述,拿到手要当成什么来处理?api/jsep.h 里的 SdpType 枚举给出四种真实取值:
// api/jsep.h
enum class SdpType {
kOffer, // 当作 SDP 提议
kPrAnswer, // 当作「临时应答」(provisional,还不是最终)
kAnswer, // 当作最终应答
kRollback // 回滚:撤销待定的提议,信令状态退回上一步
};kOffer:提议——「我想这么打,你看行不行」。kAnswer:最终应答——「行,就这么定」。协商到此敲定。kPrAnswer:临时应答(provisional answer)——「大概行,但还没最终确认」,用于需要提前建立部分状态的场景,后面还会跟一个最终kAnswer。kRollback:回滚——把一个还没敲定的提议撤掉,信令状态退回kStable。这是「协商反悔」的机制,在双方几乎同时发 offer(glare,抢话)时尤其有用。
两个 SessionDescription:api 层 vs 内部
「一段 SDP 描述」在源码里有两副面孔,别混淆:
SessionDescriptionInterface | SessionDescription | |
|---|---|---|
| 位置 | api/jsep.h(公共 API) | pc/session_description.h(内部) |
| 是什么 | API 层的包装:类型 + SDP 文本 + 候选 | 解析后的结构化媒体描述 |
| 你拿到的 | createOffer 回调给你的就是它 | 内部协商操作的对象 |
| 关键内容 | type()、ToString()、AddCandidate() | 各条 m-line 的 ContentInfo、编解码器列表等 |
简单说:SessionDescriptionInterface 是「给外面看的信封」(带着类型标签和可序列化的 SDP 文本),SessionDescription 是「拆开信封后结构化的内容」(下一章和媒体协商真正操作的是它)。createOffer 给你的是前者,你 ToString() 拿到文本发给对端;对端 setRemoteDescription 又把文本解析回结构。
信令状态机:6 个状态
JSEP 的协商过程,是一台状态机。api/peer_connection_interface.h 里的 SignalingState 定义了它的 6 个真实状态:
// api/peer_connection_interface.h:SignalingState
kStable, // 稳定态:没有进行中的协商
kHaveLocalOffer, // 已设本地 offer,等对端 answer
kHaveLocalPrAnswer, // 已设本地临时应答
kHaveRemoteOffer, // 已设远端 offer,待本端 answer
kHaveRemotePrAnswer, // 已设远端临时应答
kClosed, // 连接已关闭典型的一轮 offer/answer,就是在这些状态间迁移。发起方(设本地 offer)和应答方(设远端 offer)走两条对称的路,最后双双回到 kStable:
小结与下一章
SDP 是描述「这通电话怎么打」的文本(m-line 列媒体、a= 行给编解码器 / DTLS 指纹 / ICE 凭据 / 方向);JSEP(RFC 8829)是围绕它的提议 / 应答模型,把「协商媒体」(WebRTC 负责)与「传递信令」(应用负责)分开。SdpType 四种:offer / pranswer / answer / rollback(回滚)。描述有两副面孔:api 层 SessionDescriptionInterface(信封)与内部 SessionDescription(结构化内容)。协商是一台 6 状态的信令状态机,一轮成功后回到 kStable。
这台状态机的司机,是 PeerConnection 那个 sdp_handler_ 成员——SdpOfferAnswerHandler。下一章读它:createOffer / setLocalDescription / setRemoteDescription 到底做了什么。👉 SdpOfferAnswerHandler:协商的驾驶舱。
参考资料
- [1] libwebrtc 源码.
api/jsep.h(SdpType、SessionDescriptionInterface)、pc/session_description.h(内部结构)。 - [2] libwebrtc 源码.
api/peer_connection_interface.h(SignalingState六状态)。 - [3] IETF RFC 8829「JSEP」、RFC 8866「SDP」—— 协议原文。