網頁版WebRTC多人聊天Demo
本文基於Codelab中step7,在其基礎上做簡單修改,使其支持多人視頻通信,本文暫時只支持星狀結構三人聊天,多人聊天能夠在基礎上擴展,原理相同。
一.源碼分析
該工程包括三個文件:server.js,main.js,index.html。
1.server.js
if (numClients == 0){ socket.join(room); socket.emit('created', room); } else if (numClients == 1) { io.sockets.in(room).emit('join', room); socket.join(room); socket.emit('joined', room); } else { // max two clients socket.emit('full', room); } socket.emit('emit(): client ' + socket.id + ' joined room ' + room); socket.broadcast.emit('broadcast(): client ' + socket.id + ' joined room ' + room);
後臺服務代碼,負責異步消息通信。當有新用戶加入房間時,向客戶端發送消息,客戶端接收到消息後做相應的處理。
2.index.html
網站主頁,包括兩塊視頻區域和文本區域。
<!DOCTYPE html> <html> <head> <meta name='keywords' content='WebRTC, HTML5, JavaScript' /> <meta name='description' content='WebRTC Reference App' /> <meta name='viewport' content='width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1'> <base target='_blank'> <title>WebRTC client</title> <link rel='stylesheet' href='css/main.css' /> </head> <body> <div id='container' class='main' > <div id='videos' class='videos'> <video id='localVideo' class='localVideo' autoplay muted></video> <video id='remoteVideo' class='remoteVideo' autoplay></video> </div> <div id='textareas'> <textarea id="dataChannelSend" disabled placeholder="Press Start, enter some text, then press Send."></textarea> <textarea id="dataChannelReceive" disabled></textarea> </div> <button id="sendButton" disabled>Send</button> </div> <script src='/socket.io/socket.io.js'></script> <script src='js/lib/adapter.js'></script> <script src='js/main.js'></script> </body> </html>
3.main.js
核心代碼區域,包括房間的建立,RTCPeerConnection建立和兩點間的視頻通話。
3.1消息處理
socket.on('created', function (room){ console.log('Created room ' + room); isInitiator = true; }); socket.on('full', function (room){ console.log('Room ' + room + ' is full'); }); socket.on('join', function (room){ console.log('Another peer made a request to join room ' + room); console.log('This peer is the initiator of room ' + room + '!'); isChannelReady = true; }); socket.on('joined', function (room){ console.log('This peer has joined room ' + room); isChannelReady = true; }); socket.on('message', function (message){ console.log('Received message:', message); if (message === 'got user media') { maybeStart(); } else if (message.type === 'offer') { if (!isInitiator && !isStarted) { maybeStart(); } pc.setRemoteDescription(new RTCSessionDescription(message)); doAnswer(); } else if (message.type === 'answer' && isStarted) { pc.setRemoteDescription(new RTCSessionDescription(message)); } else if (message.type === 'candidate' && isStarted) { var candidate = new RTCIceCandidate({sdpMLineIndex:message.label, candidate:message.candidate}); pc.addIceCandidate(candidate); } else if (message === 'bye' && isStarted) { handleRemoteHangup(); } });
3.2peerconnection建立和通信
function createPeerConnection() { try { pc = new RTCPeerConnection(pc_config, pc_constraints); pc.onicecandidate = handleIceCandidate; console.log('Created RTCPeerConnnection with:\n' + ' config: \'' + JSON.stringify(pc_config) + '\';\n' + ' constraints: \'' + JSON.stringify(pc_constraints) + '\'.'); } catch (e) { console.log('Failed to create PeerConnection, exception: ' + e.message); alert('Cannot create RTCPeerConnection object.'); return; } pc.onaddstream = handleRemoteStreamAdded; pc.onremovestream = handleRemoteStreamRemoved; if (isInitiator) { try { // Reliable Data Channels not yet supported in Chrome sendChannel = pc.createDataChannel("sendDataChannel", {reliable: false}); sendChannel.onmessage = handleMessage; trace('Created send data channel'); } catch (e) { alert('Failed to create data channel. ' + 'You need Chrome M25 or later with RtpDataChannel enabled'); trace('createDataChannel() failed with exception: ' + e.message); } sendChannel.onopen = handleSendChannelStateChange; sendChannel.onclose = handleSendChannelStateChange; console.log('....................this is a initiator = true....................'); } else { pc.ondatachannel = gotReceiveChannel; console.log('....................this is not a initiator = false....................'); } }
3.3 視頻源的輸出展示
function handleRemoteStreamAdded(event) { console.log('Remote stream added.'); // reattachMediaStream(miniVideo, localVideo); attachMediaStream(remoteVideo, event.stream); remoteStream = event.stream; // waitForRemoteVideo(); }
二. 簡單工做流程介紹與修改思路
1. 工做過程以下:css
1.1.瀏覽器A訪問主頁,容許訪問攝像頭音頻設備,server接收到'create or join'消息,計算此時鏈接到服務器的客戶端數量,此時數量爲0,則向客戶端發送'created'消息。
1.2.瀏覽器A接收到'created'消息,將isInitiator設爲true,該值爲true表示該客戶斷是peerconnection的發起者。
1.3.瀏覽器B訪問主頁,容許訪問攝像頭音頻設備,server接收到'create or join'消息,計算此時鏈接到服務器的客戶端數量,此時數量爲1,則向客戶端發送join和joined消息。
1.4.瀏覽器A和瀏覽器B都接收到join和joined消息,設置isChannelReady=true,表示此時準備好創建鏈接。瀏覽器A發起peerconnection鏈接doCall,瀏覽器B迴應peerconnection鏈接doAnswer,A和B創建P2P鏈接。
1.5.A和B分別未來自本地和遠端的視頻stream顯示在頁面上。
注意:瀏覽器A和瀏覽器B都接受來自server相同的消息,而二者在接收到相同的消息後的處理卻不同(main.js代碼是同樣的),一個是發起者,一個是應答者。能夠使用狀態機來理解,程序所處狀態不同,雖然接收到相同的命令,但能夠作出不一樣的處理(經過isInitiator變量區分不一樣的狀態)。
2.三人聊天室的實現
簡單起見,咱們暫時先實現三人視頻通信,使用星狀結構。下面是修改思路:
a.A和B以及創建鏈接,此時如C加入,能夠將A和C創建鏈接,同時保持A和B以前的鏈接。此時,A能看到B和C,而B和C只能看到A。
b.若是A B C三者須要互相看到,則須要A將B的視頻傳給C,並將C的視頻傳給B。
本文暫時只實現A與B通信,A與C通信,BC之間不能通信。下面是具體的代碼修改步驟:
2.1server.js
if (numClients == 0){ socket.join(room); socket.emit('created', room); } else if (numClients <=2 ) { //第三個用戶加入後仍然發送join joined消息 io.sockets.in(room).emit('join', room); socket.join(room); socket.emit('joined', room); } else { // max two clients socket.emit('full', room); }
2.2index.html
能夠採用動態方式添加,這裏簡單起見直接增長一路視頻實現塊。
<div id='videos' class='videos'> <video id='localVideo' class='localVideo' autoplay muted></video> //本地視頻 A </div> <div > <video id='remoteVideo' class='remoteVideo' autoplay></video>// remote視頻B </div> <div > <video id='remoteVideo2' class='remoteVideo2' autoplay></video> //remote視頻c </div>
2.3 main.js
a.增長一個全局變量isPeerEstablished
用來表示該客戶端是否已經建立了PeerConnection。isPeerEstablished和isInitiator二者能夠區分發起者和應答者,由於具備超過2個客戶端,因此必須使用isPeerEstablished來選擇還沒有建立鏈接的客戶端做爲應答者。
var isPeerEstablished=false;
b.處理message機制修改
在判斷條件裏面加入(!isPeerEstablished||isInitiator),表示還沒有建立連接C和發起者A纔會執行peerconnection。保證新加入者C和A建立連接,同時保持A和B的鏈接。
socket.on('message', function (message){ console.log('Received message:', message); if (message === 'got user media'&&(!isPeerEstablished||isInitiator)) { maybeStart(); } else if (message.type === 'offer'&&(!isPeerEstablished||isInitiator)) { if (!isInitiator && !isStarted) { maybeStart(); } pc.setRemoteDescription(new RTCSessionDescription(message)); doAnswer(); } else if (message.type === 'answer' && isStarted&&(!isPeerEstablished||isInitiator)) { pc.setRemoteDescription(new RTCSessionDescription(message)); } else if (message.type === 'candidate' && isStarted&&(!isPeerEstablished||isInitiator)) { var candidate = new RTCIceCandidate({sdpMLineIndex:message.label, candidate:message.candidate}); pc.addIceCandidate(candidate); } else if (message === 'bye' && isStarted) { handleRemoteHangup(); } });
c.視頻流展示html
若是isInitiator和isPeerEstablished都爲true,說明此時A和B已經創建連接。此時,應該將新的視頻流顯示在remoteVideo2中。其餘狀況將視頻流展現在remoteVideo中。
function handleRemoteStreamAdded(event) { console.log('Remote stream added.'); // reattachMediaStream(miniVideo, localVideo); if(isInitiator&&isPeerEstablished){ attachMediaStream(remoteVideo2, event.stream); remoteStream2 = event.stream; }else{ attachMediaStream(remoteVideo, event.stream); remoteStream = event.stream; } isPeerEstablished=true; // waitForRemoteVideo(); }
d.其餘兩處修改
var remoteVideo2 = document.querySelector('#remoteVideo2'); ...... function handleRemoteHangup() { console.log('Session terminated.'); stop(); //isInitiator = false; //老是保持A的發起者角色 }
三人聊天效果圖: