DEV Community

Cover image for WebRTC - A Simple Video Chat With JavaScript (Part 1)
Jefferson Xavier
Jefferson Xavier

Posted on • Updated on

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

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
Enter fullscreen mode Exit fullscreen mode

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

yarn add express socket.io
Enter fullscreen mode Exit fullscreen mode

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));

Enter fullscreen mode Exit fullscreen mode

The initial project structure should be something like:

Alt Text

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);
  });
});
Enter fullscreen mode Exit fullscreen mode

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:

Alt Text

  • HML 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>
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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

Alt Text

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);
      });
  });
}
Enter fullscreen mode Exit fullscreen mode

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);
      });
  });
Enter fullscreen mode Exit fullscreen mode

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));
});
Enter fullscreen mode Exit fullscreen mode

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

Alt Text

Reference

WebRTC API

WebRTC in real world

Next Steps

You can see all code in GitHub

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

Thanks for your reading. Please, leave your comment with your contribution.

Top comments (9)

Collapse
 
streammer profile image
Streammer • Edited

Hi, where is <script src="/socket.io/socket.io.js"></script>

Collapse
 
jeffersonxavier profile image
Jefferson Xavier

Hello, is in node_modules folder.

Collapse
 
denisroot profile image
Denis-root

Thank you very much for your help, you have helped me understand new things.
Just a doubt, I did it step by step, I used visual code, it works locally for me. I uploaded it to heroku, as long as I use it locally I can make video calls, however trying to connect between different networks does not work. How can I make it work remotely.

Collapse
 
jeffersonxavier profile image
Jefferson Xavier

Hi, thanks for your comment. It's necessary configure a turn server to make video calls in internet. Read more about that here webrtc.org/getting-started/turn-se... and html5rocks.com/en/tutorials/webrtc....

Collapse
 
muhammadsaleh profile image
Muhammad Saleh

Thanks for the great tutorial.
I wonder if there is a way to answer offers without sending stream in return. The purpose of that is to allow some users to just watching without broadcasting, some joined users wouldn't allow media permissions. So you might consider it as a one-way call.

Collapse
 
jeffersonxavier profile image
Jefferson Xavier

Hi, thanks for your comment. Yes, that's possible, you can just disable audio and video in getUserMedia to customer users and to broadcast enable this features.

Collapse
 
dcsan profile image
dc

I assume this is using p2p web RTC? how easy is it to get it working for say a four-way chat even with voice only

Collapse
 
jeffersonxavier profile image
Jefferson Xavier

Hi, thanks for your comment.

It's simple, to open stream with voice only just pass the option video with false to getUserMedia() function like

{video: false, audio: true}

To open connection with multiple users the dynamic is same. Use all array received in other-users socket event calling the new RTCPeerConnection to any user and change the variables localConnection and remoteConnection to store a array with all connections initiated or received.

Collapse
 
teofi28 profile image
Joe

Hello Jefferson,
Thanks for this tutorial... I´d really appreciate your help. I just got an iceserver with stun and turn servers on twilio. Where should this configuration be done? I see signaling is already set in your code. Thanks and Regards..
Joe Nassar