Let's build a video conferencing app

Featured on Hashnode
Featured on daily.dev

Subscribe to my newsletter and never miss my upcoming articles

Hello Everyone👋,

In this article we will see how to build a video conferencing app.

Prerequisites: Basics of Webrtc

To implement this we will be using the following libraries:

  1. React
  2. Nodejs
  3. simple-peer
  4. socket.io
  5. chance

Setup Server:

touch app.js
yarn add express socket.io
const express = require('express');
const http = require('http');
const app = express();
const server = http.createServer(app);
const io = require('socket.io')(server);

const users = {};
const socketRoomMap = {};

io.on('connection', (socket) => {
  socket.on('join-room', (roomId, userDetails) => {
    // adding all user to a room so that we can broadcast messages
    socket.join(roomId);

    // adding map users to room
    if (users[roomId]) {
      users[roomId].push({ socketId: socket.id, ...userDetails });
    } else {
      users[roomId] = [{ socketId: socket.id, ...userDetails }];
    }

    // adding map of socketid to room
    socketRoomMap[socket.id] = roomId;
    const usersInThisRoom = users[roomId].filter(
      (user) => user.socketId !== socket.id
    );

    /* once a new user has joined sending the details of 
    users who are already present in room. */
    socket.emit('users-present-in-room', usersInThisRoom);
  });

  socket.on('initiate-signal', (payload) => {
    const roomId = socketRoomMap[socket.id];
    let room = users[roomId];
    let name = '';
    if (room) {
      const user = room.find((user) => user.socketId === socket.id);
      name = user.name;
    }

    /* once a peer wants to initiate signal, 
    To old user sending the user details along with signal */
    io.to(payload.userToSignal).emit('user-joined', {
      signal: payload.signal,
      callerId: payload.callerId,
      name,
    });
  });

  /* once the peer acknowledge signal sending the 
  acknowledgement back so that it can stream peer to peer. */
  socket.on('ack-signal', (payload) => {
    io.to(payload.callerId).emit('signal-accepted', {
      signal: payload.signal,
      id: socket.id,
    });
  });

  socket.on('disconnect', () => {
    const roomId = socketRoomMap[socket.id];
    let room = users[roomId];
    if (room) {
      room = room.filter((user) => user.socketId !== socket.id);
      users[roomId] = room;
    }
    // on disconnect sending to all users that user has disconnected
    socket.to(roomId).broadcast.emit('user-disconnected', socket.id);
  });
});

server.listen(3001);

Here we use socket to transmit the user details and webrtc signals between multiple peers.

Setup the Client:

npx create-react-app webrtc-video-call-react
cd webrtc-video-call-react
yarn add socket.io-client simple-peer chance
import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Home from './Home';
import Room from './Room';

const App = () => (
  <div className='App'>
    <BrowserRouter>
      <Switch>
        <Route path='/' exact component={Home} />
        <Route path='/room/:roomId' component={Room} />
      </Switch>
    </BrowserRouter>
  </div>
);

export default App;

Here we have 2 routes /home and /room/:roomId . In /home we will give the option to create room or join room. In /room/:roomId is where we will render the video streams from multiple users.

import React, { useState } from 'react';
import * as Chance from 'chance';

const chance = new Chance();

const Home = ({ history }) => {
  const [roomId, setRoomId] = useState('');
  return (
    <div style={{ marginTop: 10, marginLeft: 10 }}>
      <input
        type='text'
        value={roomId}
        onChange={(e) => setRoomId(e.target.value)}
      ></input>
      <button
        type='button'
        onClick={() => {
          if (!roomId) {
            alert('RoomId is required');
            return;
          }
          history.push(`/room/${roomId}`);
        }}
      >
        Join Room
      </button>
      <button
        type='button'
        onClick={() => {
          const id = chance.guid();
          history.push(`/room/${id}`);
        }}
      >
        Create Room
      </button>
    </div>
  );
};

export default Home;
import React, { useEffect, useRef, useState } from 'react';
import io from 'socket.io-client';
import Peer from 'simple-peer';
import * as Chance from 'chance';

import Video from './Video';

const chance = new Chance();

const Room = (props) => {
  const [userDetails, setUserDetails] = useState({
    id: chance.guid(),
    name: chance.name(),
  });
  const [peers, setPeers] = useState([]);

  const socketRef = useRef();
  const refVideo = useRef();
  const peersRef = useRef([]);

  const roomId = props.match.params.roomId;

  useEffect(() => {
    navigator.mediaDevices
      .getUserMedia({ video: true, audio: true })
      .then((stream) => {
        refVideo.current.srcObject = stream;

        socketRef.current = io.connect('http://localhost:3001');

        // sending the user details and roomid to join in the room
        socketRef.current.emit('join-room', roomId, userDetails);

        socketRef.current.on('users-present-in-room', (users) => {
          const peers = [];

          // To all users who are already in the room initiating a peer connection
          users.forEach((user) => {
            const peer = createPeer(
              user.socketId,
              socketRef.current.id,
              stream
            );

            peersRef.current.push({
              peerId: user.socketId,
              peer,
              name: user.name,
            });

            peers.push({
              peerId: user.socketId,
              peerObj: peer,
            });
          });

          setPeers(peers);
        });

        // once the users initiate signal we will call add peer
        // to acknowledge the signal and send the stream
        socketRef.current.on('user-joined', (payload) => {
          const peer = addPeer(payload.signal, payload.callerId, stream);
          peersRef.current.push({
            peerId: payload.callerId,
            peer,
            name: payload.name,
          });

          setPeers((users) => [
            ...users,
            { peerId: payload.callerId, peerObj: peer },
          ]);
        });

        // once the signal is accepted calling the signal with signal
        // from other user so that stream can flow between peers
        socketRef.current.on('signal-accepted', (payload) => {
          const item = peersRef.current.find((p) => p.peerId === payload.id);
          item.peer.signal(payload.signal);
        });

        // if some user is disconnected removing his references.
        socketRef.current.on('user-disconnected', (payload) => {
          const item = peersRef.current.find((p) => p.peerId === payload);
          if (item) {
            item.peer.destroy();
            peersRef.current = peersRef.current.filter(
              (p) => p.peerId !== payload
            );
          }
          setPeers((users) => users.filter((p) => p.peerId !== payload));
        });
      });
  }, []);

  function createPeer(userToSignal, callerId, stream) {
    const peer = new Peer({
      initiator: true,
      trickle: false,
      stream,
    });

    peer.on('signal', (signal) => {
      socketRef.current.emit('initiate-signal', {
        userToSignal,
        callerId,
        signal,
      });
    });

    return peer;
  }

  function addPeer(incomingSignal, callerId, stream) {
    const peer = new Peer({
      initiator: false,
      trickle: false,
      stream,
    });

    peer.on('signal', (signal) => {
      socketRef.current.emit('ack-signal', { signal, callerId });
    });

    peer.signal(incomingSignal);

    return peer;
  }

  return (
    <div style={{ display: 'flex', flexWrap: 'wrap' }}>
      <div style={{ display: 'flex', flexDirection: 'column' }}>
        <video muted ref={refVideo} autoPlay playsInline />
        <span>{userDetails.name}</span>
      </div>
      {peers.map((peer, index) => {
        return (
          <Video
            key={peersRef.current[index].peerId}
            peer={peer.peerObj}
            name={peersRef.current[index].name}
          />
        );
      })}
    </div>
  );
};

export default Room;

This room component will take care of connecting to the peers, sending and receiving signal with the help of our socket server and simple-peer.

Start the server, client and open the url in multiple devices to see it in action.

Output

Please feel free fork and play with the source code.

Source: Client Server

Please like and share if you find this interesting.

Ezenwankwo Ugochukwu's photo

Thank you for this! This will surely come handy in the future for me! Bookmarked!

Kannan's photo

Ezenwankwo Ugochukwu I am glad its helpful

Vivek Tiwari's photo

Awesome tutorial. I always wanted to understand how this works. 👏

Kannan's photo

Thanks Vivek

Victoria Lo's photo

This is such an awesome article! Thanks for sharing!

Kannan's photo

Thanks Victoria

Tapas Adhikary's photo

Kannan, Great tutorial. Thanks for sharing. I'm going to use it in my DemoLab soon.

Kannan's photo

Thanks Tapas

Murewa Ashiru's photo

Awesome! Can't wait to try it out.

Kannan's photo

Thanks Murewa

Astha Srivastava's photo

Wonderful tutorial. I will surely try it out.. :-)

Kannan's photo

Thanks Astha

N. M. Ali Hayder's photo

Hi Kannan, I've hosted the server into Heroku and the client into firebase hosting. Now, If I test on my own network using multiple devices then it's working fine. But on another network, the video becomes black when I test it with a friend. An error shows in the console is like: Uncaught Error: Connection failed. at d.value (index.js:658) at RTCPeerConnection.t._pc.onconnectionstatechange (index.js:116)

Show +1 replies
N. M. Ali Hayder's photo

Hi, Kannan I solved the issue by configuring the ice servers. The snippet looks like this now.

const peer = new Peer({
      initiator: true,
      trickle: false,
      config: {
        iceServers: [
          {
            urls: "stun:numb.viagenie.ca",
            username: "sultan1640@gmail.com",
            credential: "98376683"
          },
          {
            urls: "turn:numb.viagenie.ca",
            username: "sultan1640@gmail.com",
            credential: "98376683"
          }
        ]
      },
      stream,
    });
Juliano Costa's photo

Hey buddy.

I'm trying to use a local ip like 192.168.0.49, but it does not work. Do u know what could be happen ?

Kannan's photo

Is the application loading in your other device ?. If it is not loading I think you have to allow inbound traffic for the port.

If it is loading and you just dont see the video stream then it might be due to the browser which blocks the web rtc stream if the website does not have https associated with it. It will work only in localhost with http.

In either these case you can use ngrok npm to publish the port (aka proxy) from local server to internet.

just install ngrok globally via npm / yarn

In terminal you can run ngrok by ngrok http <ClientAppPort>

In another window ngrok http <ServerAppPort>

Once you get the server proxy url make sure you replace it in the client application.