Let's build a video conferencing app

Let's build a video conferencing app

·

11 min read

Featured on Hashnode
Featured on daily.dev

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.

Did you find this article valuable?

Support Kannan by becoming a sponsor. Any amount is appreciated!