網頁版WebRTC多人聊天Demo


網頁版WebRTC多人聊天Demo
本文基於Codelabstep7,在其基礎上作簡單修改,使其支持多人視頻通訊,本文暫時只支持星狀結構三人聊天,多人聊天可以在基礎上擴展,原理相同。
.源碼分析
該工程包括三個文件:server.jsmain.jsindex.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. 工作過程如下:

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,則向客戶端發送joinjoined消息。
1.4.瀏覽器A和瀏覽器B都接收到joinjoined消息,設置isChannelReady=true,表示此時准備好建立連接。瀏覽器A發起peerconnection連接doCall,瀏覽器B回應peerconnection連接doAnswer,AB建立P2P連接。
1.5.AB分別將來自本地和遠端的視頻stream顯示在頁面上。
注意:瀏覽器A和瀏覽器B都接受來自server相同的消息,而兩者在接收到相同的消息后的處理卻不一樣(main.js代碼是一樣的),一個是發起者,一個是應答者。可以使用狀態機來理解,程序所處狀態不一樣,雖然接收到相同的命令,但可以做出不同的處理(通過isInitiator變量區分不同的狀態)。
 
        
2.三人聊天室的實現
簡單起見,我們暫時先實現三人視頻通訊,使用星狀結構。下面是修改思路:
a.AB以及建立連接,此時如C加入,可以將AC建立連接,同時保持AB之前的連接。此時,A能看到BC,而BC只能看到A
b.如果A B C三者需要互相看到,則需要AB的視頻傳給C,並將C的視頻傳給B
 
        
本文暫時只實現AB通訊,AC通訊,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
用來表示該客戶端是否已經創建了PeerConnectionisPeerEstablishedisInitiator兩者可以區分發起者和應答者,因為具有超過2個客戶端,所以必須使用isPeerEstablished來選擇尚未創建連接的客戶端作為應答者。
     var isPeerEstablished=false;
   b.處理message機制修改
在判斷條件里面加入(!isPeerEstablished||isInitiator)表示尚未創建鏈接C和發起者A才會執行peerconnection。保證新加入者CA創建鏈接,同時保持AB的連接。
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.視頻流展現

如果isInitiatorisPeerEstablished都為true,說明此時AB已經建立鏈接。此時,應該將新的視頻流顯示在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的發起者角色


}
三人聊天效果圖:

 
       


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM