📜 ⬆️ ⬇️

Multiuser chat using WebRTC

image

WebRTC is an API provided by the browser that allows you to organize P2P connection and data transfer directly between browsers. There are quite a few tutorials on how to write your own video chat with WebRTC. For example, here is an article on Habré. However, they are all limited to connecting two clients. In this article I will try to talk about how to use WebRTC to connect and exchange messages between three or more users.

The RTCPeerConnection interface is a peer-to-peer connection between two browsers. To connect three or more users, we will have to organize a mesh network (a network in which each node is connected to all other nodes).
We will use the following scheme:
  1. When you open the page, check the availability of room ID in location.hash
  2. If no room ID is specified, generate a new one.
  3. We send a message to the signaling server that we want to join the specified room.
  4. The signaling server sends out a notification about a new user to the other clients in this room.
  5. Clients already in the room are sending to the newcomer SDP offer
  6. Newbie responds to offers

0. Signaling server


As you know, although WebRTC provides P2P connectivity between browsers, it still requires additional transport to exchange service messages. In this example, the WebSocket server written on Node.JS using socket.io is used as such a transport:

var socket_io = require("socket.io"); module.exports = function (server) { var users = {}; var io = socket_io(server); io.on("connection", function(socket) { //       socket.on("room", function(message) { var json = JSON.parse(message); //      users[json.id] = socket; if (socket.room !== undefined) { //      - ,    socket.leave(socket.room); } //     socket.room = json.room; socket.join(socket.room); socket.user_id = json.id; //            socket.broadcast.to(socket.room).emit("new", json.id); }); // ,   WebRTC (SDP offer, SDP answer  ICE candidate) socket.on("webrtc", function(message) { var json = JSON.parse(message); if (json.to !== undefined && users[json.to] !== undefined) { //          ,    ... users[json.to].emit("webrtc", message); } else { // ...    socket.broadcast.to(socket.room).emit("webrtc", message); } }); // -  socket.on("disconnect", function() { //   ,     socket.broadcast.to(socket.room).emit("leave", socket.user_id); delete users[socket.user_id]; }); }); }; 

1. index.html


The source code of the page itself is quite simple. I deliberately did not pay attention to the layout and other beautiful, as this article is not about that. If someone wants to make it beautiful, it will not make much effort.
')
 <html> <head> <title>WebRTC Chat Demo</title> <script src="/socket.io/socket.io.js"></script> </head> <body> <div>Connected to <span id="connection_num">0</span> peers</div> <div><textarea id="message"></textarea><br/><button onclick="sendMessage();">Send</button></div> <div id="room_link"></div> <div id="chatlog"></div> <script type="text/javascript" src="/javascripts/main.js"></script> </body> </html> 

2. main.js


2.0. Getting links to page elements and WebRTC interfaces

 var chatlog = document.getElementById("chatlog"); var message = document.getElementById("message"); var connection_num = document.getElementById("connection_num"); var room_link = document.getElementById("room_link"); 

We still have to use browser prefixes to access the WebRTC interfaces.

 var PeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection; var SessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription; var IceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate; 

2.1. Room ID Definition

Here we need a function to generate a unique identifier for the room and the user. We will use for these purposes UUID.

 function uuid () { var s4 = function() { return Math.floor(Math.random() * 0x10000).toString(16); }; return s4() + s4() + "-" + s4() + "-" + s4() + "-" + s4() + "-" + s4() + s4() + s4(); } 

Now let's try to get the room ID from the address. If not specified, generate a new one. We will display the link to the current room on the page, and in one, we will generate the identifier of the current user.

 var ROOM = location.hash.substr(1); if (!ROOM) { ROOM = uuid(); } room_link.innerHTML = "<a href='#"+ROOM+"'>Link to the room</a>"; var ME = uuid(); 

2.2. Websocket

Immediately upon opening the page, we connect to our signaling server, send a request to enter the room and specify message handlers.

 // ,           var socket = io.connect("", {"sync disconnect on unload": true}); socket.on("webrtc", socketReceived); socket.on("new", socketNewPeer); //        socket.emit("room", JSON.stringify({id: ME, room: ROOM})); //      ,   WebRTC function sendViaSocket(type, message, to) { socket.emit("webrtc", JSON.stringify({id: ME, to: to, type: type, data: message})); } 

2.3. PeerConnection Settings

Most providers provide internet connection through NAT. Because of this, a direct connection becomes not so trivial. When creating a connection, we need to specify a list of STUN and TURN servers that the browser will try to use to bypass NAT. We also indicate a couple of additional options for connecting.

 var server = { iceServers: [ {url: "stun:23.21.150.121"}, {url: "stun:stun.l.google.com:19302"}, {url: "turn:numb.viagenie.ca", credential: "your password goes here", username: "example@example.com"} ] }; var options = { optional: [ {DtlsSrtpKeyAgreement: true}, //     Chrome  Firefox {RtpDataChannels: true} //   Firefox   DataChannels API ] } 

2.4. Connect new user

When a new peer is added to the room, the server sends us a message new . According to the message handlers mentioned above, the socketNewPeer function is called .

 var peers = {}; function socketNewPeer(data) { peers[data] = { candidateCache: [] }; //    var pc = new PeerConnection(server, options); //   initConnection(pc, data, "offer"); //      peers[data].connection = pc; //  DataChannel        var channel = pc.createDataChannel("mychannel", {}); channel.owner = data; peers[data].channel = channel; //     bindEvents(channel); //  SDP offer pc.createOffer(function(offer) { pc.setLocalDescription(offer); }); } function initConnection(pc, id, sdpType) { pc.onicecandidate = function (event) { if (event.candidate) { //    ICE         peers[id].candidateCache.push(event.candidate); } else { //    ,     ,    //        SDP offer  SDP answer (    )... sendViaSocket(sdpType, pc.localDescription, id); // ...     ICE  for (var i = 0; i < peers[id].candidateCache.length; i++) { sendViaSocket("candidate", peers[id].candidateCache[i], id); } } } pc.oniceconnectionstatechange = function (event) { if (pc.iceConnectionState == "disconnected") { connection_num.innerText = parseInt(connection_num.innerText) - 1; delete peers[id]; } } } function bindEvents (channel) { channel.onopen = function () { connection_num.innerText = parseInt(connection_num.innerText) + 1; }; channel.onmessage = function (e) { chatlog.innerHTML += "<div>Peer says: " + e.data + "</div>"; }; } 

2.5. SDP offer, SDP answer, ICE Cand

When one of these messages is received, we call the handler of the corresponding message.
 function socketReceived(data) { var json = JSON.parse(data); switch (json.type) { case "candidate": remoteCandidateReceived(json.id, json.data); break; case "offer": remoteOfferReceived(json.id, json.data); break; case "answer": remoteAnswerReceived(json.id, json.data); break; } } 

2.5.0 SDP offer

 function remoteOfferReceived(id, data) { createConnection(id); var pc = peers[id].connection; pc.setRemoteDescription(new SessionDescription(data)); pc.createAnswer(function(answer) { pc.setLocalDescription(answer); }); } function createConnection(id) { if (peers[id] === undefined) { peers[id] = { candidateCache: [] }; var pc = new PeerConnection(server, options); initConnection(pc, id, "answer"); peers[id].connection = pc; pc.ondatachannel = function(e) { peers[id].channel = e.channel; peers[id].channel.owner = id; bindEvents(peers[id].channel); } } } 

2.5.1 SDP answer

 function remoteAnswerReceived(id, data) { var pc = peers[id].connection; pc.setRemoteDescription(new SessionDescription(data)); } 

2.5.2 ICE candidate

 function remoteCandidateReceived(id, data) { createConnection(id); var pc = peers[id].connection; pc.addIceCandidate(new IceCandidate(data)); } 

2.6. Posting a message

When you click on the Send button, the sendMessage function is called . All she does is go through the peer list and try to send the specified message to everyone.

 function sendMessage () { var msg = message.value; for (var peer in peers) { if (peers.hasOwnProperty(peer)) { if (peers[peer].channel !== undefined) { try { peers[peer].channel.send(msg); } catch (e) {} } } } chatlog.innerHTML += "<div>Peer says: " + msg + "</div>"; message.value = ""; } 

2.7. Disconnect

Well, in the end, when you close the page, it would be good to close all open connections.

 window.addEventListener("beforeunload", onBeforeUnload); function onBeforeUnload(e) { for (var peer in peers) { if (peers.hasOwnProperty(peer)) { if (peers[peer].channel !== undefined) { try { peers[peer].channel.close(); } catch (e) {} } } } } 

3. List of sources


  1. http://www.html5rocks.com/en/tutorials/webrtc/basics/
  2. https://www.webrtc-experiment.com/docs/WebRTC-PeerConnection.html
  3. https://developer.mozilla.org/en-US/docs/Web/Guide/API/WebRTC/WebRTC_basics

Source: https://habr.com/ru/post/255833/


All Articles