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。流程很简单:

  1. 发起方(Caller)创建Offer SDP
  2. 发起方通过信令通道把Offer发给接收方
  3. 接收方(Callee)收到Offer后,创建Answer SDP
  4. 接收方把Answer发回给发起方
  5. 双方各自设置对方的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时把OfferToReceiveAudioOfferToReceiveVideo都设为true,这样对方可以自由选择是否发送音视频流。

小技巧:如果你只想做纯音频通话,可以把OfferToReceiveVideo设为false。这样生成的SDP里就不会包含video的m=行,能省下不少带宽。

8.3 设置本地与远端SDP

设置SDP有两个关键方法:setLocalDescriptionsetRemoteDescription。顺序很重要——先设本地,再设远端。

为什么?因为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的流程:

  1. ICE代理收集到Candidate后,通过OnIceCandidate回调通知应用层
  2. 应用层把Candidate通过信令通道发送给对方
  3. 对方收到后,调用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处理的流程:

SDP交换与ICE处理流程 发起方 (Caller) 接收方 (Callee) ① 创建Offer SDP ② setLocalDescription(offer) 发送Offer ③ setRemoteDescription(offer) ④ 创建Answer SDP ⑤ setLocalDescription(answer) 发送Answer ⑥ setRemoteDescription(answer) ⑦ 收集并发送ICE Candidate ⑧ 接收并添加ICE Candidate 双向交换Candidate ✅ ICE连接建立,音视频开始传输

这张图把整个流程串起来了。你看,从创建Offer到设置本地SDP,再到交换Candidate,每一步都有严格的先后顺序。跳过任何一步,或者顺序搞错,连接都建立不起来。

好了,关于SDP交换的核心内容就这些。记住一句话:SDP是会议的「蓝图」,ICE Candidate是「路标」。蓝图画好了,路标指明了,两个终端才能顺利「见面」。

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