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

Top comments (42)

Collapse
 
xandris profile image
Alexandra Parker

The Connection class is defined in connection.js, but it is not used in connection.js and it is not exported from connection.js. What part of the code uses it?

The Connection class is an ES6 class, but all its methods are assigned as instance fields. This is surprising when using a class because you would normally expect a class to have prototype methods. What I mean is I would normally expect to see this:

class Example {
  method() { body; }
}
Enter fullscreen mode Exit fullscreen mode

And instead you have this:

class Example {
  method = ()=>{ body; }
}
Enter fullscreen mode Exit fullscreen mode

Explaining why you are doing it this way would help the reader :)

Putting a language tag on your code fences would make it much easier to read!

'''javascript
Enter fullscreen mode Exit fullscreen mode

(I couldn't figure out how to escape code fences here on dev.to, so I used single quotes place of backticks.)

Collapse
 
arjhun777 profile image
Arjhun777 • Edited

connection class is used in step-7, by calling createSocketConnectionInstance() - also this just rough code with all functionalities to create video and screen sharing application

Collapse
 
xandris profile image
Alexandra Parker

The code as written says this:

export function createSocketConnectionInstance(settings={}) {
    return socketInstance = new SocketConnection(settings);
}
Enter fullscreen mode Exit fullscreen mode

Should that be new Connection(settings) instead?

Thread Thread
 
arjhun777 profile image
Arjhun777

yes

Collapse
 
deepaksai1919 profile image
Deepaksai1919

new SocketConnection(settings) is used in the code instead of new Connection(settings) in createSocketConnectionInstance()

Collapse
 
avj2352_85 profile image
PRAMOD A JINGADE

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

Collapse
 
kvvaradha profile image
kvvaradha

This is great one.

I am facing an issue while handling peerjs in cpanel based vps.

It gives me handshake failed with additionally wss://site url. Com××××× like that.

Do you have any solution for this situation?

Collapse
 
anirudh711 profile image
Anirudh Madhavan

are you running the peerjs server with sslkey and sslcert as args?

Collapse
 
kvvaradha profile image
kvvaradha

hi thank you for the prompt reply, Sorry i didnt know, how to do that, give me a guidance to do it inside cpanel, to peerjs.

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
 
supertiger profile image
Supertiger

Everyone is making tutorials about p2p which exposes IP to everyone. I want a tutorial for a different topology (one discord uses)
Good job though!

Collapse
 
avj2352_85 profile image
PRAMOD A JINGADE

You can look into APIs that Twilio / Frozen Mountain / Pexip provides for Network topologies other than P2P. Twilio & FM supports SFU and MCU for handling larger attendees.

Not sure what you mean by IP exposing...

Collapse
 
supertiger profile image
Supertiger

Still a shame there is no full tutorial for beginners. peer to peer exposes everyone's IP address to each other. I will take a look thanks.

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
 
krishan111 profile image
Krishan111

This is awxm...

Collapse
 
sandeepbot profile image
Sandeep-bot

If anyone could post working demo... That would be great👍

Collapse
 
arjhun777 profile image
Arjhun777

vichat.netlify.app/
check this working demo

Collapse
 
sandeepbot profile image
Sandeep-bot

That's working... Little bit of ui design and that is good to go.. Great work. I'll probably build it when I'm free...good work. ✌

Collapse
 
surajdpatel profile image
Suraj D. Patel

Screen sharing is not implemented

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
 
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
 
noa131 profile image
noa131

is it works with more than two users?

Collapse
 
arjhun777 profile image
Arjhun777

yes

Collapse
 
vishdrck profile image
Vishwajith Weerasinghe

fantastic

Collapse
 
suruchikatiyar profile image
Suruchi Katiyar

excellent work. I found it helpfull.

Collapse
 
kesavamuthu profile image
k5@^!

An EC2 micro with 1 GB ram can handle how many concurrent socket connection load 😊

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.