WebRTC(十三) Web端實現遠程1對1視頻通話

前言

本文主要介紹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都已經調用。當接收到對方的流數據後即可正常顯示。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章