8、SDP交换:SDP协议简介、创建Offer/Answer、设置本地与远端SDP、ICE Candidate处理
各位同学,咱们今天来聊聊WebRTC里最绕不开的一个环节——SDP交换。说实话,我刚开始接触WebRTC的时候,被SDP那一大串文本搞得一头雾水。什么m=行、a=行、c=行,看着就像天书。但后来做多了项目,慢慢就摸清了门道。
SDP,全称Session Description Protocol,会话描述协议。它不负责传输数据,只负责描述「这场会议该怎么开」。说白了,就是两个终端在握手阶段互相递名片——「我支持什么编码、我开在哪个端口、我有什么能力」。名片递对了,后面的音视频流才能跑起来。
核心要点:SDP不是传输协议,是描述协议。它描述的是媒体会话的格式、编码、传输地址等信息。
8.1 SDP协议简介
SDP的格式其实很简单,就是一行一行的键值对。每一行以单个字符开头,后面跟着等号和具体值。我整理了一下最常见的几类行:
| 行类型 | 含义 | 示例 |
|---|---|---|
| v= | 协议版本 | v=0 |
| o= | 会话发起者信息 | o=- 123456 2 IN IP4 192.168.1.1 |
| s= | 会话名称 | s=WebRTC Session |
| t= | 会话时间 | t=0 0 |
| m= | 媒体描述(音频/视频) | m=video 9 UDP/TLS/RTP/SAVPF 96 97 |
| a= | 属性行(核心信息都在这里) | a=rtpmap:96 VP8/90000 |
| c= | 连接信息 | c=IN IP4 0.0.0.0 |
嗯,这里要注意。WebRTC里的SDP和传统SIP里的SDP有些区别。WebRTC强制使用ICE、DTLS和SRTP,所以m=行里你会看到UDP/TLS/RTP/SAVPF这样的协议标识。我第一次看到这个的时候还以为是拼写错误,后来查了RFC才知道这是WebRTC的标准写法。
8.2 创建Offer/Answer
WebRTC使用Offer/Answer模型来交换SDP。流程很简单:
- 发起方(Caller)创建Offer SDP
- 发起方通过信令通道把Offer发给接收方
- 接收方(Callee)收到Offer后,创建Answer SDP
- 接收方把Answer发回给发起方
- 双方各自设置对方的SDP,连接建立
在Android上,创建Offer的代码是这样的:
// 创建PeerConnection
PeerConnection peerConnection = factory.createPeerConnection(
rtcConfig,
pcObserver
);
// 创建Offer
MediaConstraints constraints = new MediaConstraints();
constraints.mandatory.add(
new MediaConstraints.KeyValuePair(
"OfferToReceiveAudio", "true"
)
);
constraints.mandatory.add(
new MediaConstraints.KeyValuePair(
"OfferToReceiveVideo", "true"
)
);
peerConnection.createOffer(
new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sdp) {
// Offer创建成功,设置本地SDP
peerConnection.setLocalDescription(
new SdpObserver() { /* ... */ },
sdp
);
// 然后通过信令发送sdp.description
}
@Override
public void onCreateFailure(String error) {
Log.e("TAG", "创建Offer失败: " + error);
}
},
constraints
);
创建Answer的流程几乎一样,只是把createOffer换成createAnswer。我个人习惯在创建Offer时把OfferToReceiveAudio和OfferToReceiveVideo都设为true,这样对方可以自由选择是否发送音视频流。
小技巧:如果你只想做纯音频通话,可以把OfferToReceiveVideo设为false。这样生成的SDP里就不会包含video的m=行,能省下不少带宽。
8.3 设置本地与远端SDP
设置SDP有两个关键方法:setLocalDescription和setRemoteDescription。顺序很重要——先设本地,再设远端。
为什么?因为setLocalDescription会触发ICE代理开始收集Candidate。如果你先设了远端,ICE不知道往哪连,就会出问题。我在项目中遇到过这种情况:有个同事把顺序搞反了,结果ICE一直处于"checking"状态,连了半天都连不上。排查了一下午才发现是这个问题。
正确的流程:
// 1. 创建Offer
peerConnection.createOffer(sdpObserver, constraints);
// 2. 在onCreateSuccess回调中设置本地SDP
peerConnection.setLocalDescription(
new SdpObserver() {
@Override
public void onSetSuccess() {
// 本地SDP设置成功
// 此时可以获取localDescription并发送给对方
}
},
sdp
);
// 3. 收到远端的Answer后,设置远端SDP
peerConnection.setRemoteDescription(
new SdpObserver() {
@Override
public void onSetSuccess() {
// 远端SDP设置成功
// ICE开始尝试连接
}
},
remoteSdp
);
你想想看,如果远端SDP里包含的IP地址和端口不对,ICE Candidate再多也白搭。所以设置远端SDP之前,最好校验一下SDP的格式。我一般会检查m=行数是否匹配,a=rtpmap里是否有我们支持的编码。
8.4 ICE Candidate处理
ICE Candidate,说白了就是「我能被找到的地址」。每个终端可能有多个Candidate:本地局域网地址、公网地址(通过STUN打洞获得)、以及通过TURN中继的地址。
处理Candidate的流程:
- ICE代理收集到Candidate后,通过
OnIceCandidate回调通知应用层 - 应用层把Candidate通过信令通道发送给对方
- 对方收到后,调用
addIceCandidate添加到PeerConnection中
代码实现:
// 接收ICE Candidate的回调
private PeerConnection.Observer pcObserver = new PeerConnection.Observer() {
@Override
public void onIceCandidate(IceCandidate candidate) {
// 通过信令发送给远端
signalingClient.sendCandidate(
candidate.sdp,
candidate.sdpMid,
candidate.sdpMLineIndex
);
}
@Override
public void onIceCandidatesRemoved(IceCandidate[] candidates) {
// 某些情况下需要移除Candidate
for (IceCandidate c : candidates) {
peerConnection.removeIceCandidate(c);
}
}
};
// 收到远端的Candidate后添加
public void onRemoteCandidate(String sdp, String sdpMid, int sdpMLineIndex) {
IceCandidate candidate = new IceCandidate(sdpMid, sdpMLineIndex, sdp);
peerConnection.addIceCandidate(candidate);
}
注意:不要在setRemoteDescription完成之前添加ICE Candidate。我曾经踩过这个坑——在设置远端SDP之前就调了addIceCandidate,结果Candidate被丢弃了,导致连接失败。正确的做法是:先把收到的Candidate缓存起来,等远端SDP设置成功后再批量添加。
关于Candidate的类型,我简单说一下:
- host:本地地址,延迟最低,但只在同一局域网内有效
- srflx:通过STUN获取的公网地址,适合NAT穿透
- relay:通过TURN中继的地址,延迟最高,但最可靠
ICE的优先级是:host > srflx > relay。能直连就直连,直连不了就走中继。这个策略是WebRTC内置的,我们不需要手动干预。
最后,我画了一张图来总结整个SDP交换和ICE处理的流程:
这张图把整个流程串起来了。你看,从创建Offer到设置本地SDP,再到交换Candidate,每一步都有严格的先后顺序。跳过任何一步,或者顺序搞错,连接都建立不起来。
好了,关于SDP交换的核心内容就这些。记住一句话:SDP是会议的「蓝图」,ICE Candidate是「路标」。蓝图画好了,路标指明了,两个终端才能顺利「见面」。