SDP 与 JSEP · 会话怎么被描述和协商

SDP 是一段描述「这通电话怎么打」的文本,JSEP 是围绕它的提议/应答模型。这一章讲清 SDP 长什么样、SdpType 的四种取值(offer/pranswer/answer/rollback)、api 层的 SessionDescriptionInterface 与内部 SessionDescription 的区别,以及那个由 6 个状态构成的信令状态机。

作者 David更新于 第 9 篇(共 112 篇)

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 描述」在源码里有两副面孔,别混淆:

SessionDescriptionInterfaceSessionDescription
位置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:

图表加载中…

一次成功的协商,必须回到 kStable。 只有在 kStable,双方才算就「这通电话怎么打」达成完整一致,媒体才能真正开跑。中间态(kHaveLocalOffer 等)是「协商进行中」,此时若出岔子(抢话 glare 或配置冲突),可以用 kRollback 退回 kStable 重来。这台状态机的驱动者,正是下一章的主角 SdpOfferAnswerHandler——每次 setLocalDescription / setRemoteDescription,它都会检查「当前状态允不允许这个操作」,再推进状态。

小结与下一章

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」—— 协议原文。

本页目录