ZYB ARTICLES REPOS

WebRTC在SDP中携带Candidate的方法

普通的WebRTC交换SDP和Candidate的方式

普通的WebRTC双方交换SDPCandidate的信令流程是分开的。一般是由主叫先创建SDP.offer发给被叫,被叫再创建SDP.answer发给主叫。然后双方都有对方的SDP后就可以向TURN(或STUN)服务器请求自己的外网地址,也可同时向TURN服务器申请中继传输地址。得到这些信息之后就可以把它们包装成Candidate通过信令服务器发给对方。这样双方就完成了SDPCandidate的交换。

一般的代码流程如下(注:以下代码不可直接运行,只作示例):


// 初始化与信令服务器的WebSocket的Channel
channel = new SignalingChannel("wss://xxxx.xxx/xxxx");

// 初始化RTCPeerConnection
const configuration = { iceServers: [{ urls: 'turn:xxx.xxx:3478?transport=udp', username: 'xxx', credential: 'xxx' }], iceTransportPolicy: "relay" };
localPC = new RTCPeerConnection(configuration);

// 监听Candidate
localPC.addEventListener('icecandidate', e => onIceCandidate(e, localPC));

// 创建并发送sdp
localSDP = localPC.createOffer();
SendToSinglingServer("Invite", localSDP);

// 收到对方的sdp: remoteSDP
localPC.setLocalDescription(localSDP);
localPC.setRemoteDescription(remoteSDP);


// 对于被叫来说的流程
// localPC.setRemoteDescription(remoteSDP); // 在createAnswer前要调用setRemoteDescription
// localSDP = localPC.createAnswer();
// SendToSinglingServer("Answer", localSDP);
// localPC.setLocalDescription(localSDP);


function onIceCandidate(event, pc) {
    SendToSinglingServer("Candidate", event.candidate);
}

function SendToSinglingServer(cmd, payload) {
    data = Encode(cmd, payload);
    sData = window.btoa(data);
    channel.send(sData);
}

这样的流程倒也没有什么太大的问题,就是信令服务器要针对中转Candidate多一些设计实现。因此在想能不能把Candidate信息在客户端直接加入到SDP后双方再交换SDP,这样只要双方完成SDP交换就相当于完成了Candidate交换。

将Candidate合入SDP

实现这个目标有两个前提需要说明。

  1. 客户端何时获得自己的Candidate

    经过实验,只要Web的javascript代码调用了setLocalDescription接口,Web客户端就会开始请求TURNSTUN服务器来获得Candidate信息。

  2. 何时触发发送SDP

    这个问题变相就是问,如何确定Web客户端获取己方Candidate信息的结束时间。同样经过实验,WebRTC获取Candidate结束后会额外调用一次icecandidate回调函数,也就是此文中的onIceCandidate,只是这的event.candidate == null

  3. 如何获得带Candidate信息的SDP

    无论是createOffer()还是createAnswer返回的SDP都是不带Candidate的,因此有两条路,一是自己根据createOffer()createAnswer创建的SDP加上获得的Candidate信息自己手动构造一个;二是,利用WebRTC提供的Web接口。两种方法都是可以的,这里采用更简单的第二种方法。

    只需要每次调用RTCPeerConnectionicecandidate回调函数,也就是此文中的onIceCandidate时,都使用localPC.localDescription更新一遍localSDP就可以了。

基于以上信息我们可以对主叫的逻辑作如下修改

全局


var localSDP;
var SendSDP; // 发送SDP的回调函数

对于主叫的修改

// 创建sdp
localSDP = localPC.createOffer();

// 创建发送SDP回调函数
function sendInviteCmd() {
    SendToSinglingServer("Invite", localSDP);
}
SendSDP = sendInviteCmd;

// 设置本地sdp
localPC.setLocalDescription(localSDP);

// 等待onIceCandidate结束直接发送SDP信令
// ...

对于被叫的修改


// 设置对方的SDP
// 在createAnswer前必需要先调用setRemoteDescription
localPC.setRemoteDescription(remoteSDP); 

// 创建sdp
localSDP = localPC.createAnswer();

// 创建发送SDP回调函数
function sendAnswerCmd() {
    SendToSinglingServer("Answer", localSDP);
}
SendSDP = sendInviteCmd;

// 设置本地sdp
localPC.setLocalDescription(localSDP);

// 等待onIceCandidate结束直接发送SDP信令
// ...

onIceCandidate方法一

每收到一个CandidateonIceCandidate就会更新一次localSDP。当Candidate更新完成,也就是当event.candidate == null时,就触发发送localSDP到信令的逻辑。

function onIceCandidate(event, pc) {
    if(event.candidate != null) {
        // 更新SDP信息
        localSDP = localPC.localDescription;
    } else {
        // 发送SDP信息到信令服务器
        SendSDP();
    }
}

至此,就实现了将Candidate加进SDP一次性发送的功能了。

onIceCandidate方法二

该方法是利用初始化RTCPeerConnection时,指定只创建一个DTLS链路来收发所有WebRTC数据,且指定强制使用中转,且只有一个TURN服务器的情况下,只会生成一个Candidate的前提条件来实现的。

// 初始化RTCPeerConnection的config添加bundlePolicy
const configuration = { iceServers: [{ urls: 'turn:xxx.xxx:3478?transport=udp', username: 'xxx', credential: 'xxx' }], iceTransportPolicy: "relay", bundlePolicy: "max-bundle" };

关于bundlePolicy请参考: bundlePolicySDP BUNDLE

function onIceCandidate(event, pc) {
    // 如果RTCPeerConnection只使用一个DTLS链路
    // 且强制使用中转
    // 且只有一个TURN服务器
    // 则只会生成一个Candidate
    // 因此收到一个Candidate就可以直接认为Candidate收集结束
    // 就可以直接发送SDP了
    if(event.candidate) {
        localSDP = localPC.localDescription;
        SendSDP();
    }
}