8、SDP协商:SDP结构解析、Offer/Answer模型、SDP媒体行修改技巧
SDP协商,说白了就是两个浏览器在互相喊话:「我能发视频,你能收吗?」「我这边网络环境是这样的,你那边呢?」。这个过程是WebRTC建立连接的核心环节,搞不懂它,屏幕共享就根本跑不起来。
我刚开始做WebRTC的时候,觉得SDP就是一串又臭又长的字符串,直接拿来用就行。直到有一次屏幕共享死活连不上,排查了半天才发现是SDP里媒体行顺序不对。嗯,从那以后我再也不敢小看这个「文本协议」了。
8.1 SDP长什么样?
SDP全称Session Description Protocol,会话描述协议。它不负责传输数据,只负责描述「这场会话该怎么开」。你想想看,就像两个人约见面,总得先说好在哪见、几点见、穿什么颜色的衣服吧?SDP干的就是这个活。
一个典型的SDP长这样:
v=0
o=- 1234567890 1234567890 IN IP4 0.0.0.0
s=-
t=0 0
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98
c=IN IP4 0.0.0.0
a=rtpmap:96 H264/90000
a=rtpmap:97 VP8/90000
a=rtpmap:98 VP9/90000
a=fmtp:96 packetization-mode=1
m=audio 9 UDP/TLS/RTP/SAVPF 111 63
a=rtpmap:111 opus/48000/2
a=rtpmap:63 red/48000/2
每一行都有特定含义。我习惯把SDP分成三块来看:
- 会话级信息:v=、o=、s=、t= 这些,描述整个会话的基本属性
- 媒体级信息:m= 开头,描述每一路媒体流(视频、音频、数据通道)
- 属性行:a= 开头,给媒体行补充细节,比如编码格式、传输参数
核心要点:SDP里最关键的就是 m= 行和 a= 行。m=行告诉对方「我开了几路媒体」,a=行告诉对方「每路媒体支持什么编码」。屏幕共享主要操作的就是视频那一路的 m= 和 a=。
8.2 Offer/Answer模型
Offer/Answer模型是SDP协商的「对话规则」。流程其实很简单:
- A方创建Offer,把自己的能力告诉B方
- B方收到Offer,对比自己的能力,生成Answer
- Answer返回给A方,双方达成一致
为什么会需要这个模型?因为WebRTC是P2P的,没有中心服务器来协调。双方必须自己商量好用什么编码、什么分辨率、走哪个端口。Offer/Answer就是这套「商量机制」。
我在项目中遇到过一个问题:A方发了Offer,B方也回了Answer,但连接就是建立不起来。后来发现是B方在Answer里把A方Offer里的某些编码给删了,而A方又不支持B方选的编码。说白了,就是双方没商量到一块去。
避坑指南:我曾经在Answer里擅自修改了Offer中的编码顺序,结果导致视频花屏。记住一个原则:Answer只能从Offer提供的编码里选,不能自己加新的。除非你明确知道对方支持。
8.3 SDP媒体行修改技巧
屏幕共享和普通视频通话最大的区别在哪?屏幕共享的分辨率通常很高,可能是1920x1080甚至4K。而摄像头采集的视频一般只有720p。所以SDP里视频那一路的参数需要针对性调整。
我常用的几个修改技巧:
| 修改目标 | SDP行 | 说明 |
|---|---|---|
| 限制分辨率 | a=fmtp:96 x-google-max-bitrate=2000 | 防止屏幕共享占用太多带宽 |
| 指定编码优先级 | 调整m=行中编码编号的顺序 | 把VP8或H264放在前面 |
| 开启Simulcast | a=simulcast:send rid=high;mid;low | 同时发送多路不同分辨率的流 |
| 关闭音频 | 直接删除m=audio那行 | 纯屏幕共享场景常用 |
举个例子,如果你只想做纯屏幕共享,不要音频,可以在创建Offer之前把音频轨道去掉:
// 创建Offer前,只添加视频轨道
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
stream.getAudioTracks().forEach(track => track.stop()); // 干掉音频
pc.addTrack(stream.getVideoTracks()[0], stream);
const offer = await pc.createOffer();
// 这里生成的SDP里就不会有m=audio行了
如果你需要在SDP创建之后手动修改,可以这样操作:
pc.createOffer().then(offer => {
let sdp = offer.sdp;
// 删除所有音频行
sdp = sdp.replace(/m=audio.*\r\n/g, '');
sdp = sdp.replace(/a=rtpmap:\d+.*\r\n/g, '');
// 限制视频码率
sdp = sdp.replace(
/a=rtpmap:(\d+) H264/g,
'a=rtpmap:$1 H264\r\na=fmtp:$1 x-google-max-bitrate=2000'
);
offer.sdp = sdp;
pc.setLocalDescription(offer);
});
注意:手动修改SDP是个双刃剑。改对了能解决很多问题,改错了可能直接导致连接失败。我建议你只在确实需要的时候才动SDP,而且一定要在修改前后做完整的测试。
8.4 SDP协商的完整流程
为了让你更直观地理解整个SDP协商过程,我画了一张流程图:
这张图展示的是最标准的协商流程。实际项目中,你可能还需要处理ICE候选信息的交换,那个是跟SDP并行进行的。但核心逻辑不变:Offer发起、Answer回应、双方达成一致。
我的经验:调试SDP问题时,最有效的方法就是把两端的SDP打印出来,逐行对比。我经常用Chrome的chrome://webrtc-internals页面,那里能看到完整的SDP内容和协商过程。你想想看,与其瞎猜,不如直接看数据。
最后说一句,SDP协商虽然看起来复杂,但只要你理解了它的核心逻辑——「描述能力、交换能力、达成一致」——剩下的就是具体字段的细节了。多动手、多调试,慢慢就会形成自己的套路。
公众号:蓝海资料掘金营,微信deep3321