WebRTC简介



WebRTC通信原理

WebRTC需要通过长链接查找到通信双方,然后通过 peer to peer 的方式传输音频数据。

PeerConnection

WebRTC中最主要的就是一个叫做​​PeerConnection​​的对象,这个是WebRTC中已经封装好的对象。每一路的音视频会话都会有唯一的一个​​PeerConnection​​对象,WebRTC通过这个​​PeerConnection​​对象进行视频的发起、传输、接收和挂断等操作。

PeerConnection中包含的属性如下:

  • localDescription:本地描述信息,类型:​​RTCSessionDescription​
  • remoteDescription:远端描述信息,类型:​​RTCSessionDescription​
  • onicecandidate:传入一个回调方法,该回调方法有一个返回参数,返回参数类型为:​​RTCIceCandidateEvent​​,
  • onaddstream:传入一个回调方法,该回调方法有一个返回参数,返回参数类型为:``,如果检测到有远程媒体流传输到本地之后便会调用该方法。
  • ondatachannel:(暂未用到)
  • oniceconnectionstatechange:(暂未用到)
  • onnegotiationneeded:(暂未用到)
  • onremovestream:(暂未用到)
  • onsignalingstatechange:(暂未用到)

​PeerConnection​​中还包含了一些方法:

  • setLocalDescription:设置本地offer,将自己的描述信息加入到​​PeerConnection​​中,参数类型:​​RTCSessionDescription​
  • setRemoteDescription:设置远端的​​answer​​,将对方的描述信息加入到​​PeerConnection​​中,参数类型:​​RTCSessionDescription​
  • createOffer:创建一个offer,需要传入两个参数,第一个参数是创建offer成功的回调方法,会返回创建好的offer,可以在这里将这个offer发送出去。第二个参数是创建失败的回调方法,会返回错误信息。
  • createAnswer:创建一个​​answer​​,需要传入两个参数,第一个参数是创建​​answer​​成功的回调方法,会返回创建好的​​answer​​,可以在这里将这个​​answer​​发送出去。第二个参数是创建失败的回调方法,会返回错误信息。
  • addIceCandidate:将打洞服务器加入到配置信息中,参数类型:​​RTCIceCandidate​
  • addStream:向​​PeerConnection​​中加入需要发送的数据流,参数类型:​​MediaStream​
  • close:
  • createDTMFSender:
  • createDataChannel:
  • getLocalStreams:
  • getRemoteStreams:
  • getStats:
  • getStreamById:
  • removeStream:
  • updateIce:
RTCSessionDescription

​RTCSessionDescription​​类型中包含了两个属性:

  • sdp:这个包含了所有的音视频的配置信息。
  • type:这个指明了是视频的接收方还是发起方,这个将在之后进行讨论。
通信过程:

A向B发起通信请求

  1. A链接socket;
  2. A获取音频数据;
  3. A创建一个​​Ice Candidate​​;
  4. A通过创建好的​​Ice Candidate​​创建一个​​PeerConnection​​;
  5. A创建一个​​offer​​,​​offer​​中包含了视频设置​​sdp​​,将创建好的​​offer​​设置为​​PeerConnection​​的​​localDescription​​;
  6. A同时将创建的​​offer​​和​​Ice Candidate​​通过socket发送给B;
  7. 将A获取到的音频数据存入​​PeerConnection​​;
  8. 如果B先接收到A发过来的​​offer​​,那么先将​​offer​​存起来,等到接收到A发过来的​​Ice Candidate​​后通过​​Ice Candidate​​创建一个​​PeerConnection​​,再将保存好的​​offer​​设置为​​PeerConnection​​的​​remoteDescription​​。
    如果B先接收到A发过来的​​Ice Candidate​​,那么通过A发过来的​​Ice Candidate​​创建一个​​PeerConnection​​,然后等待接收到A发过来的​​offer​​,再将A发过来的​​offer​​设置为​​PeerConnection​​的​​remoteDescription​​;
  9. B接收到A发过来的​​offer​​后要创建一个​​answer​​,将​​answer​​设置为​​PeerConnection​​的​​localDescription​​。并且将创建的​​answer​​通过socket返回给A。
  10. B开始获取音频数据,将音频数据存入​​PeerConnection​​中,WebRTC便会自动将音频数据发送给A。
  11. A接收到B返回的​​answer​​,将B返回的​​answer​​设置为​​PeerConnection​​的​​remoteDescription​​。
  12. 这个时候WebRTC会将音频数据自动发送给B,A和B就建立起了实时音频通信。


WebRTC实现

1.信令服务器

首先WebRTC需要一个信令服务器,也就是一个socket链接用来发起视频通信,发送WebRTC中的​​offer​​和回复​​answer​​。

如何搭建一个简单的socket服务器,可以找我的这篇文章《》,也可以是用webSocket搭建信令服务器。

2.打洞服务器

WebRTC需要打洞服务器(一个​​stun​​,一个​​turn​​)来穿透防火墙等,我们需要配置打洞服务器:



var iceServer = {
"iceServers": [{
"urls" : ["stun:stun.l.google.com:19302"]
}, {
"urls" : ["turn:numb.viagenie.ca"],
"username" : "webrtc@live.com",
"credential" : "muazkh"
}]
};


3.创建PeerConnection

WebRTC由于是未来的一种即时通信的标准,所以目前在Chrome、Firefox和Opera浏览器中有内置插件,均提供一个全局的PeerConnection类。

  • Chrome浏览器中为​​webkitRTCPeerConnection​
  • FireFox浏览器中为​​mozRTCPeerConnection​
  • Opera浏览器中暂时没有特殊名称

创建PeerConnection的对象:



var peerConnection = new webkitRTCPeerConnection(iceServer) 


创建时需要传入打洞服务器的配置信息,如果不穿入打洞服务器的配置信息,则只可以在内网中使用实时音频通讯。

由于PeerConnection是全局的,所以我们可以通过另外的一种方式进行创建:



window.RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
var peerConnection = new RTCPeerConnection(iceServer)


如何判断浏览器是否支持WebRTC:



if (RTCPeerConnection) (function () {
console.log("浏览器支持实时音频通讯");
// 这里面可以做其他操作
})();else {
console.log("您使用的浏览器暂不支持实时音频通讯。");
}


4.获取本地音视频数据

WebRTC也提供了一个全局单例来获取本地的音视频信息:

  • Chrome浏览器中为​​webkitGetUserMedia​
  • Firefox浏览器中为​​mozGetUserMedia​
  • Opera浏览器中为​​msGetUserMedia​

GetUserMedia需要传入三个参数,第一个参数为配置信息,第二个参数为获取成功的回调,第三个参数为获取失败的回调。

获取到视频流之后



navigator.getMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia
navigator.getMedia({
audio: true, // 是否开启麦克风
video: true // 是否开启摄像头,这里还可以进行更多的配置
}, function(stream){
// 获取到视频流stream
// 绑定本地媒体流到video标签用于输出
document.getElementById('localVideo').src = URL.createObjectURL(stream);
// 向PeerConnection中加入需要发送的流
peerConnection.addStream(stream);
}, function(error){
// 获取本地视频流失败
})


5.发起音频通话请求

创建一个offer并发送给指定的对象:



peerConnection.createOffer(function(desc){
console.log("创建offer成功");
// 将创建好的offer设置为本地offer
peerConnection.setLocalDescription(desc);
// 通过socket发送offer
}, function(error){
// 创建offer失败
console.log("创建offer失败");
})


创建offer时要同时发送打洞服务器配置信息,WebRTC给了一个监听:



peerConnection.onicecandidate = function (event) {
console.log("发送打洞服务器配置信息");
}


返回的参数中有一个​​candidate​​属性,便是打洞服务器的配置信息。

6.收到音频通话请求

音频通话请求是通过socket发来的,需要通过socket去监听。

如果收到了offer,那么需要将offer存到自己的peerConnection中,并且创建一个answer发送回对方。

将offer存入peerConnection中:



peerConnection.setRemoteDescription(new RTCSessionDescription(json.data.sdp));


创建一个answer并返回给对方:



peerConnection.createAnswer(function(desc){
console.log("创建answer成功");
// 将创建好的answer设置为本地offer
peerConnection.setLocalDescription(desc);
// 通过socket发送answer
}, function(error){
// 创建answer失败
console.log("创建answer失败");
})


如果收到了打洞服务器的配置信息,那么需要将打洞服务器的配置信息存入到​​peerConnection​​中:



peerConnection.addIceCandidate(new RTCIceCandidate(json.data.candidate));


发送给对方answer后便可以等待接受对方的数据流了:



peerConnection.onaddstream = function(event){
console.log("检测到媒体流连接到本地");
// 绑定远程媒体流到video标签用于输出
document.getElementById('remoteVideo').src = URL.createObjectURL(event.stream);
};


至此,整个简单的WebRTC的流程就完成了

WebRTC例子



<html>
<body>
Local: <br>
<video id="localVideo" autoplay></video><br>
Remote: <br>
<video id="remoteVideo" autoplay></video>

<script>
console.log("开始");
// 仅仅用于控制哪一端的浏览器发起offer,#号后面有值的一方发起
// #号后面加true的为发起者
var isCaller = window.location.href.split('#')[1];

// 与信令服务器的WebSocket连接
var socket = new WebSocket("ws://127.0.0.1:3000");

// stun和turn服务器,打洞服务器设置
var iceServer = {
"iceServers": [{
"url": "stun:stun.l.google.com:19302"
}, {
"url": "turn:numb.viagenie.ca",
"username": "webrtc@live.com",
"credential": "muazkh"
}]
};

// 创建PeerConnection实例 (参数为null则没有iceserver,即使没有stunserver和turnserver,仍可在局域网下通讯)
window.RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
var peerConnection = new RTCPeerConnection(iceServer)

// 发送offer的函数
var sendOfferFn = function (desc) {
// 设置本地Offer
peerConnection.setLocalDescription(desc);
// 发送offer
socket.send(JSON.stringify({
"event": "_offer",
"data": {
"sdp": desc
}
}));
};
// 发送answer的函数,发送本地session描述
var sendAnswerFn = function(desc){ // 发送answer
peerConnection.setLocalDescription(desc); // 设置本地Offer
socket.send(JSON.stringify({ // 发送answer
"event": "_answer",
"data": {
"sdp": desc
}
}));
};

// 获取本地音频数据
navigator.getMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia
navigator.getMedia({
audio: true, // 是否开启麦克风
video: true // 是否开启摄像头,这里还可以进行更多的配置
}, function(stream){
// 获取到视频流stream
// 绑定本地媒体流到video标签用于输出
document.getElementById('localVideo').src = URL.createObjectURL(stream);
// 向PeerConnection中加入需要发送的流
peerConnection.addStream(stream);
// 如果是发起方则发送一个offer信令
if(isCaller){
peerConnection.createOffer(sendOfferFn, function (error) {
console.log('Failure callback: ' + error);
});
}
}, function(error){
// 获取本地视频流失败
console.log("获取本地视频流失败");
})

// 发送ICE候选到其他客户端
peerConnection.onicecandidate = function(event){
if (event.candidate !== null) {
socket.send(JSON.stringify({
"event": "_ice_candidate",
"data": {
"candidate": event.candidate
}
}));
}
};

// 如果检测到媒体流连接到本地,将其绑定到一个video标签上输出
peerConnection.onaddstream = function(event){
document.getElementById('remoteVideo').src = URL.createObjectURL(event.stream);
};

//处理到来的信令
socket.onmessage = function(event){
var json = JSON.parse(event.data);
//如果是一个ICE的候选,则将其加入到PeerConnection中,否则设定对方的session描述为传递过来的描述
if( json.event === "_ice_candidate" ){
peerConnection.addIceCandidate(new RTCIceCandidate(json.data.candidate));
} else {
peerConnection.setRemoteDescription(new RTCSessionDescription(json.data.sdp));
// 如果是一个offer,那么需要回复一个answer
if(json.event === "_offer") {
peerConnection.createAnswer(sendAnswerFn, function (error) {
console.log('Failure callback: ' + error);
});
}
}
};
</script>
</body>
</html>