DEV Community

Cover image for Video Chatting and Screen Sharing with React, Node, WebRTC(peerjs)
Arjhun777
Arjhun777

Posted on • Updated on • Originally published at arjhun777.blogspot.com

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

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', (req, res) => {
    res.send({ link: uuidV4() });
});

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

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

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 { websocket, peerjsEndpoint } = 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: id, roomID
            }
            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.myID, stream });
                this.setPeersListeners(stream);
                this.newUserConnection(stream);
            }
        })
    }
    getVideoAudioStream = (video=true, audio=true) => {
        let quality = this.settings.params?.quality;
        if (quality) quality = 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: 640, ideal: 1280, max: 1920},
                height: {min: 480, ideal: 720, max: 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.id) video.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.id, stream: 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(userData, stream);
        });
    }
    connectToNewUser(userData, stream) {
        const { userID } = userData;
        const call = this.myPeer.call(userID, stream, { metadata: { id: this.myID }});
        call.on('stream', (userVideoStream) => {
            this.createVideo({ id: userID, stream: userVideoStream, userData });
        });
        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 (video) video.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);
}
Enter fullscreen mode Exit fullscreen mode

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.
  3. Then we have setNavigatorToStream() which has getVideoAndAudio() function which will get the audio and video stream from the navigator. We can specify the video frame rate in the navigator.
  4. 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 stream object to createVideo().
  5. 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).
  6. And the 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.
  7. Finally we have removeVideo to remove the video element from dom when any user dissconnected.

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 { roomID, userID } = userData;
        socket.join(roomID);
        socket.to(roomID).broadcast.emit('new-user-connect', userData);
        socket.on('disconnect', () => {
            socket.to(roomID).broadcast.emit('user-disconnected', userID);
        });
    });
});
Enter fullscreen mode Exit fullscreen mode

Now we have added socket connection to backend to listen to join room, which will be triggred from frontend with userData containing roomID and userID. The userID is available when creating the 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 is listened at the frontend so all the user's connected in the room will receive the new user data.

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

npm i -g peerjs
peerjs --port 9000
Enter fullscreen mode Exit fullscreen mode

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

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 = (video, audio, type='userMedia') => {
        const media = type === 'userMedia' ? this.getVideoAudioStream(video, audio) : 
        navigator.mediaDevices.getDisplayMedia();
        return new Promise((resolve) => {
            media.then((stream) => {
                if (type === 'displayMedia') {
                    this.toggleVideoTrack({audio, video});
                }
                this.createVideo({ id: this.myID, stream });
                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.video, status.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]);
                    }
                }
            });
        })
    }
Enter fullscreen mode Exit fullscreen mode

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 [mediaType, setMediaType] = useState(false);    
    const toggleScreenShare = (displayStream ) => {
        const { reInitializeStream, toggleVideoTrack } = socketInstance.current;
        displayStream === 'displayMedia' && toggleVideoTrack({
            video: false, audio: true
        });
        reInitializeStream(false, true, displayStream).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>
    )
Enter fullscreen mode Exit fullscreen mode

Thats all you have Create a application with video chatting and screen sharing.

Good Luck !!!
Here's my working demo - vichah
Check out my blog - https://arjhun777.blogspot.com/
Github link
Frontend - https://github.com/Arjhun777/VChat-FrontEnd
Backend - https://github.com/Arjhun777/VChat-BackEnd

Latest comments (42)

Collapse
 
pascuflow profile image
pascuflow

You have a typo on that last picture, the onclick should call your toggleScreenShare but its calling reInitializeStream.

Collapse
 
hsureka profile image
hsureka

Hey , can you please help me with screen share in my project?

Collapse
 
vijayabalansujan profile image
vijayabalansujan

how to play this live chatting in any web site.

Collapse
 
gorayaa66 profile image
gorayaa66

you havent specified of stun and turn servers, your app is not working over different networks.

Collapse
 
noa131 profile image
noa131

is it works with more than two users?

Collapse
 
arjhun777 profile image
Arjhun777

yes

Collapse
 
gorayaa66 profile image
gorayaa66 • Edited

Hey i have just cloned you both repo's, but i am getting peer id error.
My peer is running on port 5002.
Can you please help me sort out this.
this is your deployed request: wss://vichat-peerjs.herokuapp.com/peerjs?key=peerjs&id=a6bb940c-bf96-4251-a8a3-55b358f91a69&token=1ga5gbsgsn7
but in my case it is different even if peer is running
undefined/peerjs/id?ts=16191558869..., this is what i am receiving

Collapse
 
arjhun777 profile image
Arjhun777

Try to use endpoint directly when initializing the peers connection

Collapse
 
gorayaa66 profile image
gorayaa66

here is the error screenshot.

Collapse
 
arjhun777 profile image
Arjhun777

Try to use the endpoint directly when initializing peer connection

Collapse
 
suruchikatiyar profile image
Suruchi Katiyar

excellent work. I found it helpfull.

Collapse
 
tchpowdog profile image
tchpowdog • Edited

I came across this post while searching for a way to handle an issue. I implemented a video chat for my web app using peerjs. But I've ran into a bit of an aggravating issue - if a peer connects WITHOUT video. In other words, they do not have a camera, there are two tracks created for video and audio, but the video track is null. If that person wants to share their screen, simply replacing the null track with the new video track does not work.

I am trying to figure out a solution without having to re-establish the connection between all peers.

Has anyone else ran into this? I see a lot of these examples of video chat apps, but they all assume all peers have a camera. Well, as we all know, that's never the case in real life situations.

Collapse
 
arjhun777 profile image
Arjhun777

Check out the "replaceStream" function, it might give you some idea

Collapse
 
loadero profile image
Loadero

Awesome content, thanks for posting! I thought this post about writing an easy automated test for a WebRTC application would be a nice add to it: dev.to/loadero/how-to-set-up-an-au...

Collapse
 
mohammadarik profile image
Mohammad Arik

Hello brother, your article is awesome. But can you help how to deploy it??

Collapse
 
arjhun777 profile image
Arjhun777

sure, for deploying your frontend(React) you can use netlify, and for peerjs server and socket backend you can use heroku.

Collapse
 
mohammadarik profile image
Mohammad Arik

If you don't mind can I have the complete source code of this project for production.. It will be great help...

Collapse
 
tomeraitz profile image
Tomer Raitz

Excellent article, thank you for this.

I have a question if you know (or anybody else), why we need WebSocket? I understand that webRtc doesn't use SIP (signaling protocol), and we need the WebSocket for the connection. But I saw this RTCPeerConnection() in the webRtc API, and I don't fully understand what it means? Can't we accomplice connection with RTCPeerConnection()?

Collapse
 
gconhub profile image
gconhub

I doubt anyone can make it work with this tutorial, the code is all over the place with missing import and stuff. It would be much easier if you just share it on git hub so people can check around.

Collapse
 
singhanuj620 profile image
Anuj Singh

The demo made me save the post. Great Article 👌👌👌

Collapse
 
kothariji profile image
Dhruv Kothari

Can you please tell me, why you choose peerjs over simple-peer. Any pros ?

Collapse
 
arjhun777 profile image
Arjhun777

No, just familiar and easy for me to implement

Collapse
 
kothariji profile image
Dhruv Kothari

great.

Collapse
 
avj2352_85 profile image
PRAMOD A JINGADE

Would be great if you could share your working demo on GitHub