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没有中心服务器来协调,只能靠两端自己商量。
流程其实很简单:
- 发起方创建PeerConnection,调用
createOffer() - 发起方调用
setLocalDescription(),把Offer设为本端描述 - 发起方通过信令通道把Offer发给远端
- 远端收到Offer,调用
setRemoteDescription() - 远端调用
createAnswer(),生成Answer - 远端调用
setLocalDescription(),把Answer设为本端描述 - 远端把Answer发回给发起方
- 发起方调用
setRemoteDescription(),完成协商
嗯,这里要注意:setLocalDescription和setRemoteDescription的顺序不能乱。我见过有人先设远端再设本端,结果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 知识体系总览
下面这张图,把媒体协商的完整流程串起来了:
这张图展示了完整的媒体协商流程。从左到右,从上到下,每一步都不能少。
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