WebRTC — A Simple Video Chat With JavaScript (Part 1)

Jeffersonx Xavier
6 min readNov 11, 2020

--

The WebRTC (Web Real-Time Communications) is a technology with a set of features that allow an user get audio/video medias and transmit this information at a peer to peer communication. It’s also possible send any data like text or files with this connection.

This post provides a tutorial to implement a simple video sharing and whit chat without use any libraries or plugins beyond of resources from WebRTC.

Project Structure

This project consist of a server that works like an access point to clients start a web communication. WebSocket is used so that clients can know each other.

The client is a simple HTML to get a Video/Audio Stream and a input to send chat messages. The WebRTC communication is implemented in a Javascript file imported by this HTML.

The WebRTC Resources

  • MediaStream: Represents a stream of media content with tracks to audio and video. You can get a MediaStream object using the navigator.mediaDevices.getUserMedia() function.
  • RTCPeerConnection: Represents a connection between two peers. It’s used to send the stream between clients.
  • RTCDataChannel: Represents a bidirectional data channel between two pairs of a connection. It’s used to send chat messages between clients.

Show me the code

Let’s start with the server code. First we go start a NodeJS project.

yarn init -y

Install the necessary dependencies. Express to create a server and socket.io to enables the WebSocket communication.

yarn add express socket.io

Create server.js to start our server and put the follow code:

const express = require('express');
const socketio = require('socket.io');
const cors = require('cors');
const http = require('http');

// Create server
const app = express();
const server = http.Server(app);

// Enable Cors to Socket IO
app.use(cors());

// Init Socket IO Server
const io = socketio(server);

// Called whend a client start a socket connection
io.on('connection', (socket) => {

});

// Start server in port 3000 or the port passed at "PORT" env variable
server.listen(process.env.PORT || 3000,
() => console.log('Server Listen On: *:', process.env.PORT || 3000));

The initial project structure should be something like:]

The WebSocket Structure

The objective of websocket is make the client knows each other no WebRTC connection.

The WebRTC connection is established in some steps describe bellow. All this steps are explained in client implementation section.

  1. Create a RTCPeerConnection Instance;
  2. Create a Offer to connection;
  3. Send a Answer to offer request;
  4. Signaling between clients.

So, to implement this it’s necessary add some events to socket.

The first step is send to myself the others users connected to start the RTCPeerConnection with each them. After that, we have events to establish the connection with all steps describe above.

Below we have the complete code to this implementation.

// Array to map all clients connected in socket
let connectedUsers = [];

// Called whend a client start a socket connection
io.on('connection', (socket) => {
// It's necessary to socket knows all clients connected
connectedUsers.push(socket.id);

// Emit to myself the other users connected array to start a connection with each them
const otherUsers = connectedUsers.filter(socketId => socketId !== socket.id);
socket.emit('other-users', otherUsers);

// Send Offer To Start Connection
socket.on('offer', (socketId, description) => {
socket.to(socketId).emit('offer', socket.id, description);
});

// Send Answer From Offer Request
socket.on('answer', (socketId, description) => {
socket.to(socketId).emit('answer', description);
});

// Send Signals to Establish the Communication Channel
socket.on('candidate', (socketId, signal) => {
socket.to(socketId).emit('candidate', signal);
});

// Remove client when socket is disconnected
socket.on('disconnect', () => {
connectedUsers = connectedUsers.filter(socketId => socketId !== socket.id);
});
});

The Client Code

Fist create a folder with name public and add the files index.html and main.js. The final project structure should look like this:

  • HTML Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebRTC Example</title>

<style>
#video-grid {
display: none;
grid-template-columns: repeat(auto-fill, 400px);
grid-auto-rows: 400px;
}

video {
width: 100%;
height: 100%;
}
</style>

<script src="/socket.io/socket.io.js"></script>
<script src="/main.js" type="module"></script>
</head>
<body>
<h1>Hello!</h1>

<!-- My Video and Remote Video from connection -->
<div id="video-grid">
<video playsinline autoplay muted id="local-video"></video>
<video playsinline autoplay id="remote-video"></video>
</div>

<!-- Input to send messages -->
<div>
<span style="font-weight: bold">Message: </span>
<input type="text" id="message-input" title="Message to Send!">
<button id="message-button">Send</button>
</div>

<!-- Area to Print Images -->
<div class="messages"></div>
</body>
</html>

In main.js file the first step is start a MediaStream, like this:

console.log('Main JS!');

// Map All HTML Elements
const videoGrid = document.getElementById('video-grid');
const messagesEl = document.querySelector('.messages');
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('message-button');
const localVideo = document.getElementById('local-video');
const remoteVideo = document.getElementById('remote-video');

// Open Camera To Capture Audio and Video
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
// Show My Video
videoGrid.style.display = 'grid';
localVideo.srcObject = stream;

// Start a Peer Connection to Transmit Stream
initConnection(stream);
})
.catch(error => console.log(error));

The result is something like this, with your video in local-video area.

The next steps are start a socket connection and init RTCPeerConnectin to each other users connected. When receive other-users socket event, the client will initiate a connection with each them.

const initConnection = (stream) => {
const socket = io('/');
let localConnection;
let remoteConnection;

// Start a RTCPeerConnection to each client
socket.on('other-users', (otherUsers) => {
// Ignore when not exists other users connected
if (!otherUsers || !otherUsers.length) return;

const socketId = otherUsers[0];

// Ininit peer connection
localConnection = new RTCPeerConnection();

// Add all tracks from stream to peer connection
stream.getTracks().forEach(track => localConnection.addTrack(track, stream));

// Send Candidtates to establish a channel communication to send stream and data
localConnection.onicecandidate = ({ candidate }) => {
candidate && socket.emit('candidate', socketId, candidate);
};

// Receive stream from remote client and add to remote video area
localConnection.ontrack = ({ streams: [ stream ] }) => {
remoteVideo.srcObject = stream;
};

// Create Offer, Set Local Description and Send Offer to other users connected
localConnection
.createOffer()
.then(offer => localConnection.setLocalDescription(offer))
.then(() => {
socket.emit('offer', socketId, localConnection.localDescription);
});
});
}

IMPORTANT: In real world the RTCPeerConnection must be initalized with configurations to iceServers with STUN and TURN servers, this is necessary to get the real IP to internet connection and avoid NAT blocks in network. See more about this in RTCPeerConnection and WebRTC in real world

Continuing our tutorial, now the other client will receive the offer request and must create a RTCPeerConnection with your answer.

// Receive Offer From Other Client
socket.on('offer', (socketId, description) => {
// Ininit peer connection
remoteConnection = new RTCPeerConnection();

// Add all tracks from stream to peer connection
stream.getTracks().forEach(track => remoteConnection.addTrack(track, stream));

// Send Candidtates to establish a channel communication to send stream and data
remoteConnection.onicecandidate = ({ candidate }) => {
candidate && socket.emit('candidate', socketId, candidate);
};

// Receive stream from remote client and add to remote video area
remoteConnection.ontrack = ({ streams: [ stream ] }) => {
remoteVideo.srcObject = stream;
};

// Set Local And Remote description and create answer
remoteConnection
.setRemoteDescription(description)
.then(() => remoteConnection.createAnswer())
.then(answer => remoteConnection.setLocalDescription(answer))
.then(() => {
socket.emit('answer', socketId, remoteConnection.localDescription);
});
});

Lastly, the first client receive the answer and set the Remote Description. So, start the send candidates to create a communication channel to send the stream.

// Receive Answer to establish peer connection
socket.on('answer', (description) => {
localConnection.setRemoteDescription(description);
});

// Receive candidates and add to peer connection
socket.on('candidate', (candidate) => {
// GET Local or Remote Connection
const conn = localConnection || remoteConnection;
conn.addIceCandidate(new RTCIceCandidate(candidate));
});

The final result is something looks like the image below with showing Local and Remote videos.

Reference

https://developer.mozilla.org/pt-BR/docs/Web/API/WebRTC_API

https://www.html5rocks.com/en/tutorials/webrtc/infrastructure/

Next Steps

You can see all code in GitHub

Follow the next post to build sending chat messages and complete this tutorial.

Originally published at https://dev.to on November 11, 2020.

--

--

Jeffersonx Xavier

Software Engineer and Full-Stack Developer. A bigger Javascript enthusiast.