网页版WebRTC多人聊天Demo
网页版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. 工作过程如下:
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.视频流展现
如果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的发起者角色 }
三人聊天效果图: