7、媒体协商:SDP协议、Offer/Answer模型、编解码器选择

媒体协商,说白了就是两个设备在通话前先“对一下暗号”。

你想想看,你的手机要跟对方的手机视频通话,总得先知道对方支持什么格式吧?是用H.264还是VP8?音频是Opus还是AAC?这些信息怎么传递、怎么达成一致,就是媒体协商要解决的问题。

我个人习惯把媒体协商比作两个人相亲——先交换简历(SDP),然后你提条件我回应(Offer/Answer),最后决定用哪种语言聊天(编解码器)。

核心要点:没有媒体协商,两个设备就像鸡同鸭讲。WebRTC的媒体协商机制,保证了跨平台、跨浏览器的音视频通信成为可能。

7.1 SDP协议:媒体的“简历”

SDP,全称Session Description Protocol,会话描述协议。它不负责传输媒体数据,只负责描述媒体。

我在项目中遇到过不少新手,以为SDP是传输协议,其实它就是个文本格式的描述文件。你打开一看,里面全是键值对。

一个典型的SDP长这样:

v=0
o=- 123456 2 IN IP4 192.168.1.100
s=-
t=0 0
m=audio 49170 RTP/AVP 111 103 104
c=IN IP4 192.168.1.100
a=rtpmap:111 opus/48000/2
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
m=video 49172 RTP/AVP 96 98
a=rtpmap:96 H264/90000
a=rtpmap:98 VP8/90000
a=fmtp:96 profile-level-id=42e01f;packetization-mode=1

我来拆解一下这个“简历”里的关键字段:

字段 含义 我的理解
v= 协议版本 目前就是0,不用管
o= 会话发起者信息 谁发的、会话ID、版本号、地址类型
m= 媒体描述 音频还是视频?端口多少?用什么传输?
a= 属性行 这里才是重点——编解码器、参数、方向等
c= 连接信息 IP地址和网络类型

避坑指南:我曾经调试一个跨网络通话问题,折腾了两天,最后发现是SDP里的c=行IP地址写错了。记住,SDP里的IP不一定是你本机的IP,在NAT环境下,它可能是ICE协商后的候选地址。

7.2 Offer/Answer模型:你提需求,我回应

Offer/Answer模型是WebRTC媒体协商的核心机制。它定义了两个角色:

  • Offerer(发起方):先发出SDP,列出自己支持的媒体能力
  • Answerer(应答方):收到Offer后,匹配双方能力,返回一个Answer

为什么会这样设计?因为WebRTC没有中心服务器来协调,只能靠两端自己商量。

流程其实很简单:

  1. 发起方创建PeerConnection,调用createOffer()
  2. 发起方调用setLocalDescription(),把Offer设为本端描述
  3. 发起方通过信令通道把Offer发给远端
  4. 远端收到Offer,调用setRemoteDescription()
  5. 远端调用createAnswer(),生成Answer
  6. 远端调用setLocalDescription(),把Answer设为本端描述
  7. 远端把Answer发回给发起方
  8. 发起方调用setRemoteDescription(),完成协商

嗯,这里要注意:setLocalDescriptionsetRemoteDescription的顺序不能乱。我见过有人先设远端再设本端,结果ICE状态一直卡着不动。

代码示例:

// 发起方
const pc = new RTCPeerConnection(configuration);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// 通过信令通道发送 offer 给远端

// 应答方
const pc = new RTCPeerConnection(configuration);
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
// 通过信令通道发送 answer 给发起方

// 发起方收到 answer
await pc.setRemoteDescription(answer);

重要提醒:Offer和Answer的交换必须通过信令通道完成。WebRTC不规定信令怎么传,你可以用WebSocket、HTTP轮询,甚至用鸽子送信都行。但一定要保证顺序——先Offer后Answer,不能乱。

7.3 编解码器选择:谁更合适?

编解码器选择,是媒体协商中最实际的问题。你想想看,两个设备支持的编解码器可能完全不同——Android手机可能硬解H.264,iPhone也支持H.264,但某些浏览器默认用VP8。

WebRTC的编解码器选择遵循一个原则:双方取交集,按优先级排序

具体来说:

  • Offer方列出所有支持的编解码器,按偏好排序
  • Answer方从Offer的列表里,挑出自己也支持的
  • Answer方按自己的偏好重新排序,返回给Offer方
  • 最终使用的编解码器,是Answer方列表里的第一个

我个人的经验是:移动端优先选H.264。为什么?因为大部分手机都有H.264的硬件编码器,省电、发热低、性能好。VP8虽然开源免费,但软编软解对CPU压力大,手机发烫严重。

不过也有例外。我记得有一次做跨平台项目,Android端用H.264,但某个国产浏览器只支持VP8。最后只能在Offer里同时带上H.264和VP8,让浏览器自己选。

编解码器优先级调整代码:

// 把H.264提到最前面
const pc = new RTCPeerConnection();
const transceivers = pc.getTransceivers();
for (const t of transceivers) {
  if (t.kind === 'video') {
    const codecs = RTCRtpSender.getCapabilities('video').codecs;
    const h264Codecs = codecs.filter(c => c.mimeType.includes('H264'));
    const otherCodecs = codecs.filter(c => !c.mimeType.includes('H264'));
    const sortedCodecs = [...h264Codecs, ...otherCodecs];
    t.setCodecPreferences(sortedCodecs);
  }
}

避坑指南:我曾经在项目中遇到一个问题——明明双方都支持H.264,但协商结果却是VP8。查了半天发现,Offer方把VP8排在了H.264前面,Answer方直接选了第一个。所以,如果你想让H.264优先,一定要在Offer里手动调整编解码器顺序。

7.4 知识体系总览

下面这张图,把媒体协商的完整流程串起来了:

媒体协商核心流程 发起方 (Offerer) createOffer() setLocalDescription() 信令通道 Offer 应答方 (Answerer) setRemoteDescription() createAnswer() setLocalDescription() Answer setRemoteDescription() ✅ 媒体协商完成 编解码器:H.264 / VP8 / Opus ...

这张图展示了完整的媒体协商流程。从左到右,从上到下,每一步都不能少。

7.5 实际项目中的经验总结

做了这么多年WebRTC开发,我总结了几条关于媒体协商的实战经验:

  • SDP不是一成不变的:ICE协商过程中,SDP里的候选地址会动态更新。不要以为设置完一次就完事了。
  • 编解码器优先级要主动控制:默认顺序不一定适合你的场景。移动端优先H.264,桌面端可以优先VP8或VP9。
  • Offer/Answer可以多次触发:比如中途添加流、改变分辨率,都需要重新协商。这时候会触发新的Offer/Answer周期。
  • 注意SDP的版本号:每次重新协商,SDP的版本号(o=行里的数字)必须递增,否则对端可能忽略你的新Offer。

避坑指南:我曾经遇到一个线上问题——用户切换摄像头后视频卡住不动。排查发现,切换摄像头后没有重新触发协商,导致对端还在用旧的SDP。记住,任何媒体参数的变更,都要走一遍完整的Offer/Answer流程。

媒体协商是WebRTC的基石。理解了SDP、Offer/Answer模型和编解码器选择,你就掌握了WebRTC通信的“语言”。


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