Video Chatting and Screen Sharing with React, Node, WebRTC(peerjs)

To create a video chatting and screen sharing application requires three major setup

1. Basic React setup for handling UI.

2. Needs Backend (Nodejs) for maintaining socket connection.

3. Needs a peer server to maintain create peer-to-peer connection and to maintain it.

 1) React basic setup with join button which makes an API call to backend and gets a unique id and redirects the user to join the room (React running at the port 3000)

Frontend - /Home.js

import Axios from 'axios';
import React from 'react';

function Home(props) {
    const handleJoin = () => {
        Axios.get(`http://localhost:5000/join`).then(res => {
            props.history?.push(`/join/${res.data.link}?quality=${quality}`);
        })
    }

    return (
        <React.Fragment>
            <button onClick={handleJoin}>join</button>
        </React.Fragment>
    )
}

export default Home;

Here our backend is running at port localhost 5000, as a response will be getting a unique id that will be used as a room id with upcoming steps.

2) Backend - Node basic setup with a server listening in port 5000 and defining router with "/join" to generate a unique id and return it to frontend

Backend - /server.js


import express from 'express';
import cors from 'cors';
import server from 'http';
import { v4 as uuidV4 } from 'uuid';

const app = express();
const serve = server.Server(app);
const port = process.env.PORT || 5000;

// Middlewares
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get('/join', (reqres=> {
    res.send({ link: uuidV4() });
});

serve.listen(port, () => {
    console.log(`Listening on the port ${port}`);
}).on('error'e => {
    console.error(e);
});

Here using uuid package to generate a unique string.

3) At the frontend creating a new route with the id got in the response(looks something like this "http://localhost:3000/join/a7dc3a79-858b-420b-a9c3-55eec5cf199b"). A new component - RoomComponent is created with the disconnect button and having a div container with id="room-container" to hold our video elements

Frontend  - ./RoomComponent.js

const RoomComponent = (props=> {
    const handleDisconnect = () => {
        socketInstance.current?.destoryConnection();
        props.history.push('/');
    }
    return (
        <React.Fragment>
            <div id="room-container"></div>
            <button onClick={handleDisconnect}>Disconnect</button>
        </React.Fragment>
    )
}

export default RoomComponent;

4) Now we need our stream from our device cam and mic, we can use the navigator to get the device stream data. For this, we can use a helper class (Connection) to maintain all the incoming and outgoing stream data and to maintain the socket connection with the backend.

Frontend - ./connection.js

import openSocket from 'socket.io-client';
import Peer from 'peerjs';
const { websocketpeerjsEndpoint } = env_config;
const initializePeerConnection = () => {
    return new Peer('', {
        host: peerjsEndpoint// need to provide peerjs server endpoint 
                              // (something like localhost:9000)
        secure: true
    });
}
const initializeSocketConnection = () => {
    return openSocket.connect(websocket, {// need to provide backend server endpoint 
                              // (ws://localhost:5000) if ssl provided then
                              // (wss://localhost:5000)
        secure: true
        reconnection: true
        rejectUnauthorized: false,
        reconnectionAttempts: 10
    });
}
class Connection {
    videoContainer = {};
    message = [];
    settings;
    streaming = false;
    myPeer;
    socket;
    myID = '';
    constructor(settings) {
        this.settings = settings;
        this.myPeer = initializePeerConnection();
        this.socket = initializeSocketConnection();
        this.initializeSocketEvents();
        this.initializePeersEvents();
    }
    initializeSocketEvents = () => {
        this.socket.on('connect', () => {
            console.log('socket connected');
        });
        this.socket.on('user-disconnected', (userID=> {
            console.log('user disconnected-- closing peers'userID);
            peers[userID] && peers[userID].close();
            this.removeVideo(userID);
        });
        this.socket.on('disconnect', () => {
            console.log('socket disconnected --');
        });
        this.socket.on('error', (err=> {
            console.log('socket error --'err);
        });
    }
    initializePeersEvents = () => {
        this.myPeer.on('open', (id=> {
            this.myID = id;
            const roomID = window.location.pathname.split('/')[2];
            const userData = {
                userID: idroomID
            }
            console.log('peers established and joined room'userData);
            this.socket.emit('join-room'userData);
            this.setNavigatorToStream();
        });
        this.myPeer.on('error', (err=> {
            console.log('peer connection error'err);
            this.myPeer.reconnect();
        })
    }
    setNavigatorToStream = () => {
        this.getVideoAudioStream().then((stream=> {
            if (stream) {
                this.streaming = true;
                this.createVideo({ id: this.myIDstream });
                this.setPeersListeners(stream);
                this.newUserConnection(stream);
            }
        })
    }
    getVideoAudioStream = (video=trueaudio=true=> {
        let quality = this.settings.params?.quality;
        if (qualityquality = parseInt(quality);
        const myNavigator = navigator.mediaDevices.getUserMedia || 
        navigator.mediaDevices.webkitGetUserMedia || 
        navigator.mediaDevices.mozGetUserMedia || 
        navigator.mediaDevices.msGetUserMedia;
        return myNavigator({
            video: video ? {
                frameRate: quality ? quality : 12,
                noiseSuppression: true,
                width: {min: 640ideal: 1280max: 1920},
                height: {min: 480ideal: 720max: 1080}
            } : false,
            audio: audio,
        });
    }
    createVideo = (createObj=> {
        if (!this.videoContainer[createObj.id]) {
            this.videoContainer[createObj.id] = {
                ...createObj,
            };
            const roomContainer = document.getElementById('room-container');
            const videoContainer = document.createElement('div');
            const video = document.createElement('video');
            video.srcObject = this.videoContainer[createObj.id].stream;
            video.id = createObj.id;
            video.autoplay = true;
            if (this.myID === createObj.idvideo.muted = true;
            videoContainer.appendChild(video)
            roomContainer.append(videoContainer);
        } else {
            // @ts-ignore
            document.getElementById(createObj.id)?.srcObject = createObj.stream;
        }
    }
    setPeersListeners = (stream=> {
        this.myPeer.on('call', (call=> {
            call.answer(stream);
            call.on('stream', (userVideoStream=> {console.log('user stream data'
            userVideoStream)
                this.createVideo({ id: call.metadata.idstream: userVideoStream });
            });
            call.on('close', () => {
                console.log('closing peers listeners'call.metadata.id);
                this.removeVideo(call.metadata.id);
            });
            call.on('error', () => {
                console.log('peer error ------');
                this.removeVideo(call.metadata.id);
            });
            peers[call.metadata.id] = call;
        });
    }
    newUserConnection = (stream=> {
        this.socket.on('new-user-connect', (userData=> {
            console.log('New User Connected'userData);
            this.connectToNewUser(userDatastream);
        });
    }
    connectToNewUser(userDatastream) {
        const { userID } = userData;
        const call = this.myPeer.call(userIDstream, { metadata: { id: this.myID }});
        call.on('stream', (userVideoStream=> {
            this.createVideo({ id: userIDstream: userVideoStreamuserData });
        });
        call.on('close', () => {
            console.log('closing new user'userID);
            this.removeVideo(userID);
        });
        call.on('error', () => {
            console.log('peer error ------')
            this.removeVideo(userID);
        })
        peers[userID] = call;
    }
    removeVideo = (id=> {
        delete this.videoContainer[id];
        const video = document.getElementById(id);
        if (videovideo.remove();
    }
    destoryConnection = () => {
        const myMediaTracks = this.videoContainer[this.myID]?.stream.getTracks();
        myMediaTracks?.forEach((track:any=> {
            track.stop();
        })
        socketInstance?.socket.disconnect();
        this.myPeer.destroy();
    }
}

export function createSocketConnectionInstance(settings={}) {
    return socketInstance = new Connection(settings);
}

Here we have created a Connection class to maintain all our socket and peer connection, Don't worry we will walk through all the functions above.

  1. we have a constructor that gets a settings object (optional) that can be used to send some data from our component for setting up our connection class like (sending video frame to be used)
  2. Inside constructor we are invoking two methods initializeSocketEvents() and initializePeersEvents()
    • initializeSocketEvents() - Will start socket connection with our backend.
    • initializePeersEvents() - Will start peer connection with our peer server.

  1. Then we have setNavigatorToStream() which has getVideoAndAudio() function which will get the audio and video stream from navigator. We can specify the video frame rate in the navigator.
  2. If the stream is available then we will be resolving in .then(streamObj) and now we can create a video element to display our stream bypassing the stream object to createVideo().
  3. Now after getting our own stream it's time to listen to the peer events in function setPeersListeners() where we will be listening for any incoming video stream from another user and will stream our data in peer.answer(ourStream).
  4. And we will be setting newUserConnection(), where we will be sending our stream, if we are connecting to the existing room and also keeping track of the current peer connection by userID in peers Object.
  5. Finally, we have removeVideo to remove the video element from dom when any user disconnected.

5) Now the backend needs to listen to the socket connection. Using socket "socket.io" to make the socket connection easy.

Backend - ./server.js

import socketIO from 'socket.io';
io.on('connection'socket => {
    console.log('socket established')
    socket.on('join-room', (userData=> {
        const { roomIDuserID } = userData;
        socket.join(roomID);
        socket.to(roomID).broadcast.emit('new-user-connect'userData);
        socket.on('disconnect', () => {
            socket.to(roomID).broadcast.emit('user-disconnected'userID);
        });
    });
});

Now we have added a socket connection to the backend to listen to the join room, which will be triggered from the frontend with userData containing roomID and userID. The userID is available when creating a peer connection.

Then the socket has now connected a room with the roomID (From unique id got as response in frontend) and now we can dispatch message to all the users in the room

Now socket.to(roomID).broadcast.emit('new-user-connect'userData);  with this we can dispatch message to all the user's connected except us. And this 'new-user-connect listens at the frontend so all the users connected in the room will receive the new user data.

6) Now you need to create a peerjs server by using the following commands

  • npm i -g peerjs
  • peerjs --port 9000

7) Now in Room Component we need to invoke the Connection class to start the call. In Room Component add this functionality.

Frontend - ./RoomComponent.js

    let socketInstance = useRef(null);    
    useEffect(() => {
        startConnection();
    }, []);
    const startConnection = () => {
        params = {quality: 12}
        socketInstance.current = createSocketConnectionInstance({
            params
        });
    }

Now you will be able to see that after creating a room, when a new user joins the user will be peer-to-peer connected.

8) Now for Screen Sharing You need to replace the current stream with the new screen sharing stream.

Frontend - ./connection.js

    reInitializeStream = (videoaudiotype='userMedia'=> {
        const media = type === 'userMedia' ? this.getVideoAudioStream(videoaudio) : 
        navigator.mediaDevices.getDisplayMedia();
        return new Promise((resolve=> {
            media.then((stream=> {
                if (type === 'displayMedia') {
                    this.toggleVideoTrack({audiovideo});
                }
                this.createVideo({ id: this.myIDstream });
                replaceStream(stream);
                resolve(true);
            });
        });
    }
    toggleVideoTrack = (status=> {
        const myVideo = this.getMyVideo();
        if (myVideo && !status.video
            myVideo.srcObject?.getVideoTracks().forEach((track=> {
                if (track.kind === 'video') {
                    !status.video && track.stop();
                }
            });
        else if (myVideo) {
            this.reInitializeStream(status.videostatus.audio);
        }
    }
    replaceStream = (mediaStream=> {
        Object.values(peers).map((peer=> {
            peer.peerConnection?.getSenders().map((sender=> {
                if(sender.track.kind == "audio") {
                    if(mediaStream.getAudioTracks().length > 0){
                        sender.replaceTrack(mediaStream.getAudioTracks()[0]);
                    }
                }
                if(sender.track.kind == "video") {
                    if(mediaStream.getVideoTracks().length > 0){
                        sender.replaceTrack(mediaStream.getVideoTracks()[0]);
                    }
                }
            });
        })
    }

Now the current stream needs to reInitializeStream() will be checking the type it needs to replace, if it is userMedia then it will be streaming from cam and mic, if its display media it gets the display stream object from getDisplayMedia() and then it will toggle the track to stop or start the cam or mic.

Then the new stream video element is created based on the userID and then it will place the new stream by replaceStream(). By getting the current call object store previosly will contain the curretn stream data will be replaced with the new stream data in replaceStream().

9) At roomConnection we need to create a button to toggle the video and screen sharing.

Frontend - ./RoomConnection.js

    const [mediaTypesetMediaType] = useState(false);    
    const toggleScreenShare = (displayStream ) => {
        const { reInitializeStreamtoggleVideoTrack } = socketInstance.current;
        displayStream === 'displayMedia' && toggleVideoTrack({
            video: falseaudio: true
        });
        reInitializeStream(falsetruedisplayStream).then(() => {
            setMediaType(!mediaType)
        });
    }
    return (
        <React.Fragment>
            <div id="room-container"></div>
            <button onClick={handleDisconnect}>Disconnect</button>
            <button 
                onClick={() => reInitializeStream(mediaType ? 
                'userMedia' : 'displayMedia')}
            >
            {mediaType ? 'screen sharing' : 'stop sharing'}</button>
        </React.Fragment>
    )


That's all you have to Create an application with video chatting and screen sharing.

Good Luck !!!

Here's my working demo - vichat



Comments

Post a Comment