文章目錄
前言
本文主要介紹Web端基於Socket和STUN/TURN服務實現的視頻通話的功能。示例代碼是Web端部分,其他相關內容可以參考:
WebRTC(六) Node.js+Socket.io實現信令服務器
WebRTC(八) STUN協議
WebRTC(九) TURN協議
WebRTC(十一) Web端媒體協商
效果展示
如圖: mac電腦和ipad實現了遠程的視頻通話功能。爲了模擬真實的網絡場景,mac連接的是無線網,ipad連接的是手機用4G開的熱點。
實現流程
代碼示例
1 html部分代碼
<html>
<head>
<title>WebRTC Video Chat</title>
<link rel="stylesheet" href="./css/main.css">
</head>
<body>
<div>
<div>
<!-- socket連接 -->
<button id="connserver">Connect Sig Server</button>
<!-- 離開房間 -->
<button id="leave" disabled>Leave</button>
</div>
<div>
<input id="shareDesk" type="checkbox"/>
<label for="shareDesk"></label>
</div>
<!-- 帶寬限制設置 -->
<div>
<label>BandWidth:</label>
<select id="bandwidth" disabled>
<option value="unlimited" selected>unlimited</option>
<option value="2000">2000</option>
<option value="1000">1000</option>
<option value="500">500</option>
<option value="250">250</option>
<option value="125">125</option>
</select>
kbps
</div>
<div id="preview">
<div>
<h2>Local:</h2>
<!-- 本地畫面展示 -->
<video id="localvideo" autoplay playsinline muted></video>
<h2>Offer SDP:</h2>
<textarea id="offer"></textarea>
</div>
<div>
<h2>Romote:</h2>
<!-- 遠端畫面展示 -->
<video id="remotevideo" autoplay playsinline></video>
<h2>Answer SDP:</h2>
<textarea id="answer"></textarea>
</div>
</div>
<div class="graph-container" id="bitrateGraph">
<div>BitRate</div>
<canvas id="bitrateCanvas"></canvas>
</div>
<div class="graph-container" id="packetGraph">
<div>Packets send per second</div>
<canvas id="packetCanvas"></canvas>
</div>
</div>
<script src="third_party/graph.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js"></script>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="main.js"></script>
</body>
</html>
2 控件初始化及變量定義
'use strict'
var localVideo = document.querySelector('video#localvideo');
var remoteVideo = document.querySelector('video#remotevideo');
var btnConn = document.querySelector('button#connserver');
var btnLeave = document.querySelector('button#leave');
var optBw = document.querySelector('select#bandwidth');
var offer = document.querySelector('textarea#offer')
var answer = document.querySelector('textarea#answer');
var shareDeskBox = document.querySelector('input#shareDesk');
var bitrateGraph;
var bitrateSeries;
var packetGraph;
var packetSeries;
var lastResult;
var pcConfig = {
'iceServers':[{
'urls' : 'turn:182.92.70.244:3478',
'credential': "testpwd",
'username':"testuser"
}]
};
var localStream = null;
var remoteStream = null;
var pc = null;
var roomid;
var socket = null;
var offerdesc = null;
var state = 'init';
//按鈕監聽
btnConn.onclick = connSignalServer;
btnLeave.onclick = leave;
optBw.onchange = change_bw;
3 獲取本地視頻信息並展示
function connSignalServer(){
start();
return true;
}
配置媒體信息,並開始獲取本地視頻
function start(){
if(!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia){
console.error('the getUserMedia is not supported!');
return;
}else {
//1 ===============配置音視頻參數===============
var constraints;
if(shareDeskBox.checked && shareDesk()){
constraints = {
video : true,
audio : {
echoCancellation : true,
noiseSuppression : true,
autoGainControl : true
}
}
}else{
constraints = {
video : true,
audio : {
echoCancellation : true,
noiseSuppression : true,
autoGainControl : true
}
}
//獲取本地媒體流數據
navigator.mediaDevices.getUserMedia(constraints)
.then(getMediaStream)
.catch(handleError);
}
}
}
將獲取到的視頻數據賦值給顯示控件
function getMediaStream(stream){
if(localStream){
stream.getAudioTracks().forEach((track)=>{
localStream.addTrack(track);
stream.removeTrack(track);
})
} else {
localStream = stream;
}
//2 ===============顯示本地視頻===============
localVideo.srcObject = localStream;
//這個函數的調用時機特別重要 一定要在getMediaStream之後再調用,否則會出現綁定失敗的情況
conn();
bitrateSeries = new TimelineDataSeries();
bitrateGraph = new TimelineGraphView('bitrateGraph', 'bitrateCanvas');
bitrateGraph.updateEndDate();
packetSeries = new TimelineDataSeries();
packetGraph = new TimelineGraphView('bitrateGraph', 'bitrateCanvas');
packetGraph.updateEndDate();
}
4 開始socket連接並設置消息接收回調監聽
function conn(){
//1 觸發socke連接
socket = io.connect();
//2 加入房間後的回調
socket.on("joined", (roomid, id) => {
state = 'joined'
createPeerConnection();
bindTracks();
btnConn.disabled = true;
btnLeave.disabled = false;
console.log('receive joined message, state = ',state,",roomid = ",roomid, ",id = ",id);
});
//3 房間內其它人加入的回調
socket.on('otherjoin', (roomid) =>{
if(state === 'joined_unbind'){
createPeerConnection();
bindTracks();
}
state = 'joined_conn';
call();
console.log('receive other_joined message, state = ', state);
});
//4 房間內已滿回調
socket.on('full',(roomid, id) => {
console.log('receive full message ', roomid, id);
hangup();
closeLocalMedia();
state = 'leaved';
console.log('receive full message, state = ',state);
alert('the room is full! ');
})
//5 離開房間的回調
socket.on('leaved', (roomid, id) =>{
console.log('receive leaved message', roomid, id);
state = 'leaved';
socket.disconnect();
console.log('receive leaved message, state = ', state);
btnConn.disabled = false;
btnLeave.disabled = true;
optBw.disabled = true;
});
//6 其他人離開房間的回調
socket.on('bye', (room,id)=>{
console.log('receive bye message', roomid, id);
state = 'joined_unbind';
hangup();
offer.value = '';
answer.value = '';
console.log('receive bye message, state = ', state);
})
//7 斷開連接的回調
socket.on('disconnect', (socket)=>{
console.log('receive disconnect message!', roomid);
if(!(state === 'leaved')){
hangup();
closeLocalMedia();
}
state = 'leaved';
btnConn.disabled = false;
btnLeave.leaved = true;
optBw.disabled = true;
});
//8 消息接收的回調
socket.on('message', (roomid, data) => {
if(data === null || data === undefined){
console.err('the message is invalid!');
return;
}
//8.1 offer消息
if(data.hasOwnProperty('type') && data.type === 'offer'){
console.log('receive offer', roomid, data);
offer.value = data.sdp;
pc.setRemoteDescription(new RTCSessionDescription(data));
//create answer
pc.createAnswer()
.then(createAnswerSuc)
.catch(handleAnswerError);
//8.2 anser消息
}else if(data.hasOwnProperty('type') && data.type === 'answer'){
console.log('receive answer', roomid, data);
optBw.disabled = false;
answer.value = data.sdp;
pc.setRemoteDescription(new RTCSessionDescription(data));
//8.3 candidate消息
}else if(data.hasOwnProperty('type') && data.type === 'candidate'){
console.log('receive candidate', roomid, data);
var candidate = new RTCIceCandidate({
sdpMLineIndex: data.label,
candidate:data.candidate
});
pc.addIceCandidate(candidate);
}else{
console.log('the message is invalid', data);
}
});
roomid = getQueryVariable('room');
//9 發送加入房間的消息
socket.emit('join', roomid);
return true;
}
4.1 在加入房間的回調(joined方法)中,會調用createPeerConnection和bindTracks方法。
socket.on("joined", (roomid, id) => {
state = 'joined'
createPeerConnection();
bindTracks();
btnConn.disabled = true;
btnLeave.disabled = false;
console.log('receive joined message, state = ',state,",roomid = ",roomid, ",id = ",id);
4.2 在其他人加入房間的回調(otherjoin方法)中,當前如果是未綁定的狀態,也會觸發調用createPeerConnection和bindTracks方法。並調用call方法來觸發創建offer和發送offer。
socket.on('otherjoin', (roomid) =>{
if(state === 'joined_unbind'){
createPeerConnection();
bindTracks();
}
state = 'joined_conn';
call();
console.log('receive other_joined message, state = ', state);
});
4.4 幾種消息類型offer anser candidate
socket.on('message', (roomid, data) => {
if(data === null || data === undefined){
console.err('the message is invalid!');
return;
}
if(data.hasOwnProperty('type') && data.type === 'offer'){
console.log('receive offer', roomid, data);
offer.value = data.sdp;
pc.setRemoteDescription(new RTCSessionDescription(data));
pc.createAnswer()
.then(createAnswerSuc)
.catch(handleAnswerError);
}else if(data.hasOwnProperty('type') && data.type === 'answer'){
console.log('receive answer', roomid, data);
optBw.disabled = false;
answer.value = data.sdp;
pc.setRemoteDescription(new RTCSessionDescription(data));
}else if(data.hasOwnProperty('type') && data.type === 'candidate'){
console.log('receive candidate', roomid, data);
var candidate = new RTCIceCandidate({
sdpMLineIndex: data.label,
candidate:data.candidate
});
pc.addIceCandidate(candidate);
}else{
console.log('the message is invalid', data);
}
});
5 創建PeerConnection,並設置回調監聽
創建PeerConnection,
設置candidate生成的回調方法onicecandidate,並在此方法中將生成的cadidate發送至對端。
設置獲得流數據的回調方法ontrack,當接收到對方的流數據後,顯示視頻流數據。
function createPeerConnection(){
console.log('create RTCPeerConnection!');
if(!pc){
//1 創建 RTCPeerConnection
pc = new RTCPeerConnection(pcConfig);
//2 設置candidate生成的回調
pc.onicecandidate = (e)=>{
if(e.candidate){
//3 發送cadidate
sendMessage(roomid, {
type : 'candidate',
label : event.candidate.sdpMLineIndex,
id:event.candidate.sdpMid,
candidate:event.candidate.candidate
});
}else {
console.log('this is the end candidate');
}
}
pc.ontrack = getRemoteStream;
} else {
console.waring('the pc have be created!');
}
return;
}
6 將PeerConnection與Track綁定
function bindTracks(){
console.log('bind tracks into RTCPeerConnection!');
if(pc === null || pc === undefined){
console.error('pc is null or undefined!');
return;
}
if(localStream === null || localStream === undefined){
console.error('localStream is null or undefined!');
return;
}
localStream.getTracks().forEach((track)=>{
pc.addTrack(track, localStream);
});
}
7 創建offer
7.1 創建offer
function call(){
if(state === 'joined_conn'){
var offerOptions = {
offerToRecieveAudio : 1,
offerToRecieveVideo : 1
}
//創建offer
pc.createOffer(offerOptions)
.then(createOfferSuc)
.catch(createOfferFail);
}
}
7.2 創建成功後,調用發送方法
function createOfferSuc(desc){
pc.setLocalDescription(desc);
offer.value = desc.sdp;
offerdesc = desc;
//send offer sdp
sendMessage(roomid, offerdesc);
}
7.3 發送offer
function sendMessage(roomid, data){
console.log('send message to other end', "roomid = ",roomid, "data = ",data);
if(!socket){
console.log('socket is null');
}
socket.emit('message', roomid, data);
}
8 媒體協商setRemoteDescription
socket.on('message', (roomid, data) => {
if(data === null || data === undefined){
console.err('the message is invalid!');
return;
}
if(data.hasOwnProperty('type') && data.type === 'offer'){
console.log('receive offer', roomid, data);
offer.value = data.sdp;
pc.setRemoteDescription(new RTCSessionDescription(data));
//create answer
pc.createAnswer()
.then(createAnswerSuc)
.catch(handleAnswerError);
}else if(data.hasOwnProperty('type') && data.type === 'answer'){
console.log('receive answer', roomid, data);
optBw.disabled = false;
answer.value = data.sdp;
pc.setRemoteDescription(new RTCSessionDescription(data));
}else if(data.hasOwnProperty('type') && data.type === 'candidate'){
console.log('receive candidate', roomid, data);
var candidate = new RTCIceCandidate({
sdpMLineIndex: data.label,
candidate:data.candidate
});
pc.addIceCandidate(candidate);
}else{
console.log('the message is invalid', data);
}
});
8.1 接收到offer消息以後,遠端PeerConnection首先調用setRemoteDescription,來保存對方媒體信息。
然後遠端PeerConnection調用createAnswer方法來創建answer。
function createAnswerSuc(desc){
pc.setLocalDescription(desc);
answer.value = desc.sdp;
optBw.disabled = false;
sendMessage(roomid, desc);
}
answer創建成功後,首先遠端PeerConnection調用setLocalDescription來設置本地媒體信息,然後將此sdp發送到對端。
8.2 當發起端接收到answer以後,發起端PeerConnection首先調用setRemoteDescription,來保存對方的媒體信息。
if(data.hasOwnProperty('type') && data.type === 'answer'){
console.log('receive answer', roomid, data);
optBw.disabled = false;
answer.value = data.sdp;
pc.setRemoteDescription(new RTCSessionDescription(data));
}
此時發起端的setRemoteDescription和setLocalDescription都已經調用。當接收到對方的流數據後即可正常顯示。