WebRTC簡介
WebRTC通信原理
WebRTC需要通過長鏈接查找到通信雙方,然后通過 peer to peer 的方式傳輸音頻數(shù)據(jù)。
PeerConnection
WebRTC中最主要的就是一個叫做PeerConnection的對象,這個是WebRTC中已經(jīng)封裝好的對象。每一路的音視頻會話都會有唯一的一個PeerConnection對象,WebRTC通過這個PeerConnection對象進行視頻的發(fā)起、傳輸、接收和掛斷等操作。
PeerConnection中包含的屬性如下:
- localDescription:本地描述信息,類型:
RTCSessionDescription - remoteDescription:遠端描述信息,類型:
RTCSessionDescription - onicecandidate:傳入一個回調(diào)方法,該回調(diào)方法有一個返回參數(shù),返回參數(shù)類型為:
RTCIceCandidateEvent, - onaddstream:傳入一個回調(diào)方法,該回調(diào)方法有一個返回參數(shù),返回參數(shù)類型為:``,如果檢測到有遠程媒體流傳輸?shù)奖镜刂蟊銜{(diào)用該方法。
- ondatachannel:(暫未用到)
- oniceconnectionstatechange:(暫未用到)
- onnegotiationneeded:(暫未用到)
- onremovestream:(暫未用到)
- onsignalingstatechange:(暫未用到)
PeerConnection中還包含了一些方法:
- setLocalDescription:設置本地offer,將自己的描述信息加入到
PeerConnection中,參數(shù)類型:RTCSessionDescription - setRemoteDescription:設置遠端的
answer,將對方的描述信息加入到PeerConnection中,參數(shù)類型:RTCSessionDescription - createOffer:創(chuàng)建一個offer,需要傳入兩個參數(shù),第一個參數(shù)是創(chuàng)建offer成功的回調(diào)方法,會返回創(chuàng)建好的offer,可以在這里將這個offer發(fā)送出去。第二個參數(shù)是創(chuàng)建失敗的回調(diào)方法,會返回錯誤信息。
- createAnswer:創(chuàng)建一個
answer,需要傳入兩個參數(shù),第一個參數(shù)是創(chuàng)建answer成功的回調(diào)方法,會返回創(chuàng)建好的answer,可以在這里將這個answer發(fā)送出去。第二個參數(shù)是創(chuàng)建失敗的回調(diào)方法,會返回錯誤信息。 - addIceCandidate:將打洞服務器加入到配置信息中,參數(shù)類型:
RTCIceCandidate - addStream:向
PeerConnection中加入需要發(fā)送的數(shù)據(jù)流,參數(shù)類型:MediaStream - close:
- createDTMFSender:
- createDataChannel:
- getLocalStreams:
- getRemoteStreams:
- getStats:
- getStreamById:
- removeStream:
- updateIce:
RTCSessionDescription
RTCSessionDescription類型中包含了兩個屬性:
- sdp:這個包含了所有的音視頻的配置信息。
- type:這個指明了是視頻的接收方還是發(fā)起方,這個將在之后進行討論。
通信過程:
A向B發(fā)起通信請求
- A鏈接socket;
- A獲取音頻數(shù)據(jù);
- A創(chuàng)建一個
Ice Candidate; - A通過創(chuàng)建好的
Ice Candidate創(chuàng)建一個PeerConnection; - A創(chuàng)建一個
offer,offer中包含了視頻設置sdp,將創(chuàng)建好的offer設置為PeerConnection的localDescription; - A同時將創(chuàng)建的
offer和Ice Candidate通過socket發(fā)送給B; - 將A獲取到的音頻數(shù)據(jù)存入
PeerConnection; - 如果B先接收到A發(fā)過來的
offer,那么先將offer存起來,等到接收到A發(fā)過來的Ice Candidate后通過Ice Candidate創(chuàng)建一個PeerConnection,再將保存好的offer設置為PeerConnection的remoteDescription。
如果B先接收到A發(fā)過來的Ice Candidate,那么通過A發(fā)過來的Ice Candidate創(chuàng)建一個PeerConnection,然后等待接收到A發(fā)過來的offer,再將A發(fā)過來的offer設置為PeerConnection的remoteDescription; - B接收到A發(fā)過來的
offer后要創(chuàng)建一個answer,將answer設置為PeerConnection的localDescription。并且將創(chuàng)建的answer通過socket返回給A。 - B開始獲取音頻數(shù)據(jù),將音頻數(shù)據(jù)存入
PeerConnection中,WebRTC便會自動將音頻數(shù)據(jù)發(fā)送給A。 - A接收到B返回的
answer,將B返回的answer設置為PeerConnection的remoteDescription。 - 這個時候WebRTC會將音頻數(shù)據(jù)自動發(fā)送給B,A和B就建立起了實時音頻通信。
WebRTC實現(xiàn)
1.信令服務器
首先WebRTC需要一個信令服務器,也就是一個socket鏈接用來發(fā)起視頻通信,發(fā)送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.創(chuàng)建PeerConnection
WebRTC由于是未來的一種即時通信的標準,所以目前在Chrome、Firefox和Opera瀏覽器中有內(nèi)置插件,均提供一個全局的PeerConnection類。
- Chrome瀏覽器中為
webkitRTCPeerConnection - FireFox瀏覽器中為
mozRTCPeerConnection - Opera瀏覽器中暫時沒有特殊名稱
創(chuàng)建PeerConnection的對象:
var peerConnection = new webkitRTCPeerConnection(iceServer)
創(chuàng)建時需要傳入打洞服務器的配置信息,如果不穿入打洞服務器的配置信息,則只可以在內(nèi)網(wǎng)中使用實時音頻通訊。
由于PeerConnection是全局的,所以我們可以通過另外的一種方式進行創(chuàng)建:
window.RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
var peerConnection = new RTCPeerConnection(iceServer)
如何判斷瀏覽器是否支持WebRTC:
if (RTCPeerConnection) (function () {
console.log("瀏覽器支持實時音頻通訊");
// 這里面可以做其他操作
})();else {
console.log("您使用的瀏覽器暫不支持實時音頻通訊。");
}
4.獲取本地音視頻數(shù)據(jù)
WebRTC也提供了一個全局單例來獲取本地的音視頻信息:
- Chrome瀏覽器中為
webkitGetUserMedia - Firefox瀏覽器中為
mozGetUserMedia - Opera瀏覽器中為
msGetUserMedia
GetUserMedia需要傳入三個參數(shù),第一個參數(shù)為配置信息,第二個參數(shù)為獲取成功的回調(diào),第三個參數(shù)為獲取失敗的回調(diào)。
獲取到視頻流之后
navigator.getMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia
navigator.getMedia({
audio: true, // 是否開啟麥克風
video: true // 是否開啟攝像頭,這里還可以進行更多的配置
}, function(stream){
// 獲取到視頻流stream
// 綁定本地媒體流到video標簽用于輸出
document.getElementById('localVideo').src = URL.createObjectURL(stream);
// 向PeerConnection中加入需要發(fā)送的流
peerConnection.addStream(stream);
}, function(error){
// 獲取本地視頻流失敗
})
5.發(fā)起音頻通話請求
創(chuàng)建一個offer并發(fā)送給指定的對象:
peerConnection.createOffer(function(desc){
console.log("創(chuàng)建offer成功");
// 將創(chuàng)建好的offer設置為本地offer
peerConnection.setLocalDescription(desc);
// 通過socket發(fā)送offer
}, function(error){
// 創(chuàng)建offer失敗
console.log("創(chuàng)建offer失敗");
})
創(chuàng)建offer時要同時發(fā)送打洞服務器配置信息,WebRTC給了一個監(jiān)聽:
peerConnection.onicecandidate = function (event) {
console.log("發(fā)送打洞服務器配置信息");
}
返回的參數(shù)中有一個candidate屬性,便是打洞服務器的配置信息。
6.收到音頻通話請求
音頻通話請求是通過socket發(fā)來的,需要通過socket去監(jiān)聽。
如果收到了offer,那么需要將offer存到自己的peerConnection中,并且創(chuàng)建一個answer發(fā)送回對方。
將offer存入peerConnection中:
peerConnection.setRemoteDescription(new RTCSessionDescription(json.data.sdp));
創(chuàng)建一個answer并返回給對方:
peerConnection.createAnswer(function(desc){
console.log("創(chuàng)建answer成功");
// 將創(chuàng)建好的answer設置為本地offer
peerConnection.setLocalDescription(desc);
// 通過socket發(fā)送answer
}, function(error){
// 創(chuàng)建answer失敗
console.log("創(chuàng)建answer失敗");
})
如果收到了打洞服務器的配置信息,那么需要將打洞服務器的配置信息存入到peerConnection中:
peerConnection.addIceCandidate(new RTCIceCandidate(json.data.candidate));
發(fā)送給對方answer后便可以等待接受對方的數(shù)據(jù)流了:
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("開始");
// 僅僅用于控制哪一端的瀏覽器發(fā)起offer,#號后面有值的一方發(fā)起
// #號后面加true的為發(fā)起者
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"
}]
};
// 創(chuàng)建PeerConnection實例 (參數(shù)為null則沒有iceserver,即使沒有stunserver和turnserver,仍可在局域網(wǎng)下通訊)
window.RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
var peerConnection = new RTCPeerConnection(iceServer)
// 發(fā)送offer的函數(shù)
var sendOfferFn = function (desc) {
// 設置本地Offer
peerConnection.setLocalDescription(desc);
// 發(fā)送offer
socket.send(JSON.stringify({
"event": "_offer",
"data": {
"sdp": desc
}
}));
};
// 發(fā)送answer的函數(shù),發(fā)送本地session描述
var sendAnswerFn = function(desc){ // 發(fā)送answer
peerConnection.setLocalDescription(desc); // 設置本地Offer
socket.send(JSON.stringify({ // 發(fā)送answer
"event": "_answer",
"data": {
"sdp": desc
}
}));
};
// 獲取本地音頻數(shù)據(jù)
navigator.getMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia
navigator.getMedia({
audio: true, // 是否開啟麥克風
video: true // 是否開啟攝像頭,這里還可以進行更多的配置
}, function(stream){
// 獲取到視頻流stream
// 綁定本地媒體流到video標簽用于輸出
document.getElementById('localVideo').src = URL.createObjectURL(stream);
// 向PeerConnection中加入需要發(fā)送的流
peerConnection.addStream(stream);
// 如果是發(fā)起方則發(fā)送一個offer信令
if(isCaller){
peerConnection.createOffer(sendOfferFn, function (error) {
console.log('Failure callback: ' + error);
});
}
}, function(error){
// 獲取本地視頻流失敗
console.log("獲取本地視頻流失敗");
})
// 發(fā)送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>