WebRTC在SDP中携带Candidate的方法
普通的WebRTC交换SDP和Candidate的方式
普通的WebRTC双方交换SDP
和Candidate
的信令流程是分开的。一般是由主叫先创建SDP.offer
发给被叫,被叫再创建SDP.answer
发给主叫。然后双方都有对方的SDP
后就可以向TURN
(或STUN
)服务器请求自己的外网地址,也可同时向TURN
服务器申请中继传输地址。得到这些信息之后就可以把它们包装成Candidate
通过信令服务器发给对方。这样双方就完成了SDP
和Candidate
的交换。
一般的代码流程如下(注:以下代码不可直接运行,只作示例):
// 初始化与信令服务器的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
实现这个目标有两个前提需要说明。
客户端何时获得自己的Candidate
经过实验,只要Web的javascript代码调用了
setLocalDescription
接口,Web客户端就会开始请求TURN
或STUN
服务器来获得Candidate
信息。何时触发发送SDP
这个问题变相就是问,如何确定Web客户端获取己方
Candidate
信息的结束时间。同样经过实验,WebRTC获取Candidate
结束后会额外调用一次icecandidate
回调函数,也就是此文中的onIceCandidate
,只是这的event.candidate == null
。如何获得带
Candidate
信息的SDP
无论是
createOffer()
还是createAnswer
返回的SDP
都是不带Candidate
的,因此有两条路,一是自己根据createOffer()
或createAnswer
创建的SDP
加上获得的Candidate
信息自己手动构造一个;二是,利用WebRTC提供的Web接口。两种方法都是可以的,这里采用更简单的第二种方法。只需要每次调用
RTCPeerConnection
的icecandidate
回调函数,也就是此文中的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方法一
每收到一个Candidate
,onIceCandidate
就会更新一次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
请参考: bundlePolicy 和 SDP BUNDLE
function onIceCandidate(event, pc) {
// 如果RTCPeerConnection只使用一个DTLS链路
// 且强制使用中转
// 且只有一个TURN服务器
// 则只会生成一个Candidate
// 因此收到一个Candidate就可以直接认为Candidate收集结束
// 就可以直接发送SDP了
if(event.candidate) {
localSDP = localPC.localDescription;
SendSDP();
}
}