Peer-to-peer video call with Next.js, Socket.io and Native WebRTC APIs
July 6, 2022
8 min
Note: The app we are building cannot be deployed on Vercel. The reason is that Vercel takes any code put into the api
folder and deploys it as serverless functions, and websockets aren't supported. This app would have to be built and served traditionally to work. If you want to deploy something like this to Vercel, check out our WebRTC implementation with Pusher.
In this example, we'll set up a Next.js application with Socket.io, and build a simple video chat app!
Create new Next app:
1npx create-next-app next-webrtc-socket-io
Install dependencies socket.io
and socket.io-client
.
socket.io
will be used to create a socket server and used server side and socket.io-client
will be used on the client side.
1yarn add socket.io socket.io-client
Let's pause a second and see what we need before we keep going. If you read The Ultimate Beginner's Guide To WebRTC there were many times that Rick and Morty were talking to the signaling server. In this example, our socket server created by socket.io will be used as our signaling server.
To start, lets create a socket.js
file inside of our pages/api
folder:
1// pages/api/socket.js
2import { Server } from 'socket.io';
3
4const SocketHandler = (req, res) => {
5 if (res.socket.server.io) {
6 console.log('Socket is already attached');
7 return res.end();
8 }
9
10 const io = new Server(res.socket.server);
11 res.socket.server.io = io;
12
13 io.on("connection", (socket) => {
14 console.log(`User Connected : ${socket.id}`);
15 });
16
17 return res.end();
18};
19
20export default SocketHandler;
21
When the user joins, we make a GET request to /api/socket
endpoint.
Building off of our Beginner's Guide to WebRTC example, these are the socket events that we'll need:
- When Rick joins a room or when Morty joins the same room. Event:
join
- Morty tells the socket he's ready. Event:
ready
- Rick will eventually create an Offer and send it to the socket. Event:
offer
- Morty will then create an Answer and send it to the socket. Event:
answer
- Rick and Morty will then be sending ICE Candidates to the socket. Event:
ice-candidate
- Rick or Morty might leave the room. Event:
leave
1// pages/api/socket.js
2import { Server } from 'socket.io';
3
4const SocketHandler = (req, res) => {
5 if (res.socket.server.io) {
6 console.log('Socket is already attached');
7 return res.end();
8 }
9
10 const io = new Server(res.socket.server);
11 res.socket.server.io = io;
12
13 io.on("connection", (socket) => {
14 console.log(`User Connected :$socket.id}`);
15
16 // Triggered when a peer hits the join room button.
17 socket.on("join", (roomName) => {
18 });
19
20 // Triggered when the person who joined the room is ready to communicate.
21 socket.on("ready", (roomName) => {
22 });
23
24 // Triggered when server gets an icecandidate from a peer in the room.
25 socket.on("ice-candidate", (candidate, roomName: string) => {
26 });
27
28 // Triggered when server gets an offer from a peer in the room.
29 socket.on("offer", (offer, roomName) => {
30 });
31
32 // Triggered when server gets an answer from a peer in the room
33 socket.on("answer", (answer, roomName) => {
34 });
35
36 socket.on("leave", (roomName) => {
37 });
38
39 });
40
41 return res.end();
42};
43
44export default SocketHandler;
45
Our socket will also need to react to the events above, so let's set those up too:
- When the socket receives a
join
event, it can go one of three different ways- If no one is in the room, we will emit a
created
event - If someone is already in the room, it will emit a
joined
event - Since we only support 2 people in this room, if a third person joins, the socket will emit a
full
event
- If no one is in the room, we will emit a
- When the socket receives a
ready
event, it broadcastsready
to the room - When the socket receives an
offer
event, it broadcastsoffer
to the room with the offer data - When the socket receives an
answer
event, it broadcastsanswer
to the room with the answer data - When it receives the
ice-candidate
event, it broadcastsice-candidate
to the room with the ice-candidates it receives - When it receives the
leave
event, it broadcasts theleave
event to the room
1// pages/api/socket.js
2
3import { Server } from 'socket.io';
4
5const SocketHandler = (req, res) => {
6 if (res.socket.server.io) {
7 console.log('Socket is already attached');
8 return res.end();
9 }
10
11 const io = new Server(res.socket.server);
12 res.socket.server.io = io;
13
14 io.on("connection", (socket) => {
15 console.log(`User Connected :${ socket.id}`);
16
17 // Triggered when a peer hits the join room button.
18 socket.on("join", (roomName) => {
19 const {rooms} = io.sockets.adapter;
20 const room = rooms.get(roomName);
21
22 // room == undefined when no such room exists.
23 if (room === undefined) {
24 socket.join(roomName);
25 socket.emit("created");
26 } else if (room.size === 1) {
27 // room.size == 1 when one person is inside the room.
28 socket.join(roomName);
29 socket.emit("joined");
30 } else {
31 // when there are already two people inside the room.
32 socket.emit("full");
33 }
34 console.log(rooms);
35 });
36
37 // Triggered when the person who joined the room is ready to communicate.
38 socket.on("ready", (roomName) => {
39 socket.broadcast.to(roomName).emit("ready"); // Informs the other peer in the room.
40 });
41
42 // Triggered when server gets an icecandidate from a peer in the room.
43 socket.on("ice-candidate", (candidate: RTCIceCandidate, roomName: string) => {
44 console.log(candidate);
45 socket.broadcast.to(roomName).emit("ice-candidate", candidate); // Sends Candidate to the other peer in the room.
46 });
47
48 // Triggered when server gets an offer from a peer in the room.
49 socket.on("offer", (offer, roomName) => {
50 socket.broadcast.to(roomName).emit("offer", offer); // Sends Offer to the other peer in the room.
51 });
52
53 // Triggered when server gets an answer from a peer in the room.
54 socket.on("answer", (answer, roomName) => {
55 socket.broadcast.to(roomName).emit("answer", answer); // Sends Answer to the other peer in the room.
56 });
57
58 socket.on("leave", (roomName) => {
59 socket.leave(roomName);
60 socket.broadcast.to(roomName).emit("leave");
61 });
62
63 });
64 return res.end();
65};
66
67export default SocketHandler;
68
That's all we need to do from our signaling server
!
Now let's create a useful hook for our front end. This hook will initialize the socket on any component that calls it. Create a hooks
folder, and create a file called useSocket.js
1// hooks/useSocket.js
2import { useEffect, useRef } from "react";
3
4const useSocket = () => {
5 const socketCreated = useRef(false)
6 useEffect(() =>{
7 if (!socketCreated.current) {
8 const socketInitializer = async () => {
9 await fetch ('/api/socket')
10 }
11 try {
12 socketInitializer()
13 socketCreated.current = true
14 } catch (error) {
15 console.log(error)
16 }
17 }
18 }, []);
19};
20
21export default useSocket
Let's update the pages/index.js
with some boilerplate code:
1import Head from 'next/head'
2import { useRouter } from 'next/router'
3import { useState } from 'react'
4import styles from '../styles/Home.module.css'
5
6export default function Home() {
7 const router = useRouter()
8 const [roomName, setRoomName] = useState('')
9
10 const joinRoom = () => {
11 router.push(`/room/${roomName || Math.random().toString(36).slice(2)}`)
12 }
13
14 return (
15 <div className={styles.container}>
16 <Head>
17 <title>Native WebRTC API with NextJS</title>
18 <meta name="description" content="Use Native WebRTC API for video conferencing" />
19 <link rel="icon" href="/favicon.ico" />
20 </Head>
21
22 <main className={styles.main}>
23 <h1>Lets join a room!</h1>
24 <input onChange={(e) => setRoomName(e.target.value)} value={roomName} className={styles['room-name']} />
25 <button onClick={joinRoom} type="button" className={styles['join-room']}>Join Room</button>
26 </main>
27 </div>
28 )
29}
30
Here we are storing the room name in state, and we also will send the user to /room/{roomName}
using Next's Router. If the user doesn't type a room name, the joinRoom
function will simply pick a random string.
You can now start up Next:
1yarn run dev
Let's create a room
folder and create a file called [id].js
. This is where the bulk of our application code will go.
First, let's import a few things that we will need later. We are going to instantiate our socket via useSocket
. We will also create a bunch of refs.
Understanding the refs:
userVideREf
,peerVideoRef
- both are refs forvideo
elementsrtcConnectionRef
- this will store the ref to the WebRTC connection that will be created latersocketRef
- will store the ref of the socketuserStreamRef
- will keep a reference to the user media streams that we get from the camera and mic
1import { useRouter } from 'next/router';
2import { useEffect, useRef, useState } from 'react';
3import { io } from 'socket.io-client';
4import useSocket from '../../hooks/useSocket';
5
6const Room = () => {
7 useSocket();
8
9 const router = useRouter();
10 const userVideoRef = useRef();
11 const peerVideoRef = useRef();
12 const rtcConnectionRef = useRef(null);
13 const socketRef = useRef();
14 const userStreamRef = useRef();
15 const hostRef = useRef(false);
16
17 return (
18 <div>
19 <video autoPlay ref={userVideoRef} />
20 <video autoPlay ref={peerVideoRef} />
21 </div>
22 );
23};
24
25export default Room;
26
Let's start by working on our socketRef. The first thing we want to know is the roomName
that the user has joined. This is done with the help of next/router
:
const{id:roomName}=router.query;
Inside the useEffect
, we first store a reference to the socket. This works because we called our useSocket
hook to create the socket.
socketRef.current = io();
When a user joins a room, the client would emit a join
event with the roomName
. When joining, the server will check if they are the first to join and emit created
if they were or emit joined
if they were second user. It would emit full
if they were the third person to join. We have to handle those 3 events via callbacks.
We also want to clean up or disconnect from the socket when we leave the component with a return statement in the useEffect
return()=>socketRef.current.disconnect();
We will receive a few other events from the server: first when a user leaves a room (leave
event). The other events are webRTC related, which include offer
, answer
and ice-candidate
. We'll listen for each of these in a related callback as well:
1import { useRouter } from 'next/router';
2import { useEffect, useRef, useState } from 'react';
3import { io } from 'socket.io-client';
4import useSocket from '../../hooks/useSocket';
5
6const Room = () => {
7 useSocket();
8
9 const router = useRouter();
10 const userVideoRef = useRef();
11 const peerVideoRef = useRef();
12 const rtcConnectionRef = useRef(null);
13 const socketRef = useRef();
14 const userStreamRef = useRef();
15 const hostRef = useRef(false);
16
17 const { id: roomName } = router.query;
18 useEffect(() => {
19 socketRef.current = io();
20 // First we join a room
21 socketRef.current.emit('join', roomName);
22
23 socketRef.current.on('created', handleRoomCreated);
24
25 socketRef.current.on('joined', handleRoomJoined);
26 // If the room didn't exist, the server would emit the room was 'created'
27
28 // Whenever the next person joins, the server emits 'ready'
29 socketRef.current.on('ready', initiateCall);
30
31 // Emitted when a peer leaves the room
32 socketRef.current.on('leave', onPeerLeave);
33
34 // If the room is full, we show an alert
35 socketRef.current.on('full', () => {
36 window.location.href = '/';
37 });
38
39 // Events that are webRTC speccific
40 socketRef.current.on('offer', handleReceivedOffer);
41 socketRef.current.on('answer', handleAnswer);
42 socketRef.current.on('ice-candidate', handlerNewIceCandidateMsg);
43
44 // clear up after
45 return () => socketRef.current.disconnect();
46 }, [roomName]);
47
48 return (
49 <div>
50 <video autoPlay ref={userVideoRef} />
51 <video autoPlay ref={peerVideoRef} />
52 </div>
53 );
54};
55
56export default Room;
57
With our useEffect complete, let's handle each of the event callbacks.
The first person to join will call the handleRoomCreated
method, and the second person will call handleRoomJoined
. They are similar methods with two differences:
- When
handleRoomCreated
is called, it will set the caller as the host (i.e. the first user to join the room) - When
handleRoomJoined
is called, it will emit theready
event to the server to tell the first user that they are ready to start the call
Without the ready
event, none of the downstream events will fire and the call won't start. These two callbacks (handleRoomCreated
and handleRoomJoined
) both request and capture the user camera and mic and store it to the userStreamRef
, and then assign it for playback to the userVideoRef.current.srcObject
.
1 const handleRoomCreated = () => {
2 hostRef.current = true;
3 navigator.mediaDevices
4 .getUserMedia({
5 audio: true,
6 video: { width: 500, height: 500 },
7 })
8 .then((stream) => {
9 /* use the stream */
10 userStreamRef.current = stream;
11 userVideoRef.current.srcObject = stream;
12 userVideoRef.current.onloadedmetadata = () => {
13 userVideoRef.current.play();
14 };
15 })
16 .catch((err) => {
17 /* handle the error */
18 console.log(err);
19 });
20 };
21
22 const handleRoomJoined = () => {
23 navigator.mediaDevices
24 .getUserMedia({
25 audio: true,
26 video: { width: 500, height: 500 },
27 })
28 .then((stream) => {
29 /* use the stream */
30 userStreamRef.current = stream;
31 userVideoRef.current.srcObject = stream;
32 userVideoRef.current.onloadedmetadata = () => {
33 userVideoRef.current.play();
34 };
35 socketRef.current.emit('ready', roomName);
36 })
37 .catch((err) => {
38 /* handle the error */
39 console.log('error', err);
40 });
41 };
Now once hanldeRoomJoined
emits ready
, the first user or host
will call the initiateCall
function. The initiateCall
function will do three things:
- Call
createPeerConnection
, which returns aRTCPeerConnection
and set it to thertcConnectionRef
- The stream in
userStreamsRef
that was captured duringhandleCreateRoom
has an audio and video track. We take these tracks and add them to the connection so that the peer can have access to it - Create an offer and emit the offer event along with the roomName, and then set the offer to the local description of the
RTCPeerConnection
that we just created
The RTCPeerConnection
subscribes to a bunch of events, but we only care about two: onicecandidate
and ontrack
. For now, let's create two functions (handleICECandidateEvent
and handleTrackEvent
) to handle these events.
1 const initiateCall = () => {
2 if (hostRef.current) {
3 rtcConnectionRef.current = createPeerConnection();
4 rtcConnectionRef.current.addTrack(
5 userStreamRef.current.getTracks()[0],
6 userStreamRef.current,
7 );
8 rtcConnectionRef.current.addTrack(
9 userStreamRef.current.getTracks()[1],
10 userStreamRef.current,
11 );
12 rtcConnectionRef.current
13 .createOffer()
14 .then((offer) => {
15 rtcConnectionRef.current.setLocalDescription(offer);
16 socketRef.current.emit('offer', offer, roomName);
17 })
18 .catch((error) => {
19 console.log(error);
20 });
21 }
22 };
23
24 const ICE_SERVERS = {
25 iceServers: [
26 {
27 urls: 'stun:openrelay.metered.ca:80',
28 }
29 ],
30 };
31
32 const createPeerConnection = () => {
33 // We create a RTC Peer Connection
34 const connection = new RTCPeerConnection(ICE_SERVERS);
35
36 // We implement our onicecandidate method for when we received a ICE candidate from the STUN server
37 connection.onicecandidate = handleICECandidateEvent;
38
39 // We implement our onTrack method for when we receive tracks
40 connection.ontrack = handleTrackEvent;
41 return connection;
42
43 };
Once an offer
is emitted by the host, the server receives and emits it to the other peer who is listening for it. The other peer will call handleReceivedOffer
. This function is similar to initiateCall
. It calls createPeerConnection
and stores the returning RTCPeerConnection
to the rtcConnectionRef
and it adds the audio and video tracks to this RTCPeerConnection
or ref. The offer
is also set as the RemoteDescription
of the peer (as opposed to this same offer were stored as the LocalDescription
of the host, since it is local to the host). Then an answer
is created, which is emitted to the server with the roomName.
1 const handleReceivedOffer = (offer) => {
2 if (!hostRef.current) {
3 rtcConnectionRef.current = createPeerConnection();
4 rtcConnectionRef.current.addTrack(
5 userStreamRef.current.getTracks()[0],
6 userStreamRef.current,
7 );
8 rtcConnectionRef.current.addTrack(
9 userStreamRef.current.getTracks()[1],
10 userStreamRef.current,
11 );
12 rtcConnectionRef.current.setRemoteDescription(offer);
13
14 rtcConnectionRef.current
15 .createAnswer()
16 .then((answer) => {
17 rtcConnectionRef.current.setLocalDescription(answer);
18 socketRef.current.emit('answer', answer, roomName);
19 })
20 .catch((error) => {
21 console.log(error);
22 });
23 }
24 };
The host will now receive the answer
from the server and has to do something with it. In this case, the RTCPeerConnection
that was created during the initiateCall
function will set the answer as its RemoteDescription.
1 const handleAnswer = (answer) => {
2 rtcConnectionRef.current
3 .setRemoteDescription(answer)
4 .catch((err) => console.log(err));
5 };
So far, we've handled 2 out of the 3 WebRTC related events, offer
and answer
. Before we tackle the socket event for ice-candidate
, we have to first receive these ice candidates from the handleIceCandidateEvent
event. It receives candidates from the STUN server as soon as we set the offer
(if host) and answer
(if peer) is set to LocalDescription. As soon as the ice candidate events come in, it is emitted to the server for the other peers to receive.
1 const handleICECandidateEvent = (event) => {
2 if (event.candidate) {
3 socketRef.current.emit('ice-candidate', event.candidate, roomName);
4 }
5 };
We can finally handle the ice-candidate event that is received from the socket. Here the incoming ice candidate is casted into the right format via RTCIceCandidate
and added to the RTCPeerConnection. This event will get called many times and multiple ice candidates will get stored in the RTCPeerConnection.
1 const handlerNewIceCandidateMsg = (incoming) => {
2 // We cast the incoming candidate to RTCIceCandidate
3 const candidate = new RTCIceCandidate(incoming);
4 rtcConnectionRef.current
5 .addIceCandidate(candidate)
6 .catch((e) => console.log(e));
7 };
At this point, the two peers know how to reach each other, via the offer
and answer
and now they know what are the supported ways to communicate with their peer via the ice candidates. A negotiation will happen to find the best way to communicate based on codec support, bandwidth, etc.
Once the negotiation is successful, the audio and video tracks are sent. Here we are storing the stream coming into the peerVideoRef
's srcObject:
1 const handleTrackEvent = (event) => {
2 peerVideoRef.current.srcObject = event.streams[0];
3 };
And with this, we have successfully created a video call between two users!
There is one more socket event to handle, which is when one of the users decides to leave the room.
For this workflow, let's first create a button in the JSX that we are returning that a user can click which calls a leaveRoom
function.
1 return (
2 <div>
3 <video autoPlay ref={userVideoRef} />
4 <video autoPlay ref={peerVideoRef} />
5 <button onClick={leaveRoom} type="button">
6 Leave
7 </button>
8 </div>
9 );
This function will emit the leave
event and also do a bunch of cleanup such as stoping all tracks its receiving and closing our the RTCPeerConnection, and then it finally sends the user back to the main view.
1 const leaveRoom = () => {
2 socketRef.current.emit('leave', roomName); // Let's the server know that user has left the room.
3
4 if (userVideoRef.current.srcObject) {
5 userVideoRef.current.srcObject.getTracks().forEach((track) => track.stop()); // Stops receiving all track of User.
6 }
7 if (peerVideoRef.current.srcObject) {
8 peerVideoRef.current.srcObject
9 .getTracks()
10 .forEach((track) => track.stop()); // Stops receiving audio track of Peer.
11 }
12
13 // Checks if there is peer on the other side and safely closes the existing connection established with the peer.
14 if (rtcConnectionRef.current) {
15 rtcConnectionRef.current.ontrack = null;
16 rtcConnectionRef.current.onicecandidate = null;
17 rtcConnectionRef.current.close();
18 rtcConnectionRef.current = null;
19 }
20 router.push('/')
21 };
So now, once one peer leaves, the peer remaining will receive the leave
event from the server and call the onPeerLeave
method. Let's do some cleanup for the remaining user on the call as well:
1 const onPeerLeave = () => {
2 // This person is now the creator because they are the only person in the room.
3 hostRef.current = true;
4 if (peerVideoRef.current.srcObject) {
5 peerVideoRef.current.srcObject
6 .getTracks()
7 .forEach((track) => track.stop()); // Stops receiving all track of Peer.
8 }
9
10 // Safely closes the existing connection established with the peer who left.
11 if (rtcConnectionRef.current) {
12 rtcConnectionRef.current.ontrack = null;
13 rtcConnectionRef.current.onicecandidate = null;
14 rtcConnectionRef.current.close();
15 rtcConnectionRef.current = null;
16 }
17 }
And that's it! Below is the complete [id].js
page.
1import { useRouter } from 'next/router';
2import { useEffect, useRef, useState } from 'react';
3import { io } from 'socket.io-client';
4import useSocket from '../../hooks/useSocket';
5
6const ICE_SERVERS = {
7 iceServers: [
8 {
9 urls: 'stun:openrelay.metered.ca:80',
10 }
11 ],
12};
13
14const Room = () => {
15 useSocket();
16 const [micActive, setMicActive] = useState(true);
17 const [cameraActive, setCameraActive] = useState(true);
18
19 const router = useRouter();
20 const userVideoRef = useRef();
21 const peerVideoRef = useRef();
22 const rtcConnectionRef = useRef(null);
23 const socketRef = useRef();
24 const userStreamRef = useRef();
25 const hostRef = useRef(false);
26
27 const { id: roomName } = router.query;
28 useEffect(() => {
29 socketRef.current = io();
30 // First we join a room
31 socketRef.current.emit('join', roomName);
32
33 socketRef.current.on('joined', handleRoomJoined);
34 // If the room didn't exist, the server would emit the room was 'created'
35 socketRef.current.on('created', handleRoomCreated);
36 // Whenever the next person joins, the server emits 'ready'
37 socketRef.current.on('ready', initiateCall);
38
39 // Emitted when a peer leaves the room
40 socketRef.current.on('leave', onPeerLeave);
41
42 // If the room is full, we show an alert
43 socketRef.current.on('full', () => {
44 window.location.href = '/';
45 });
46
47 // Event called when a remote user initiating the connection and
48 socketRef.current.on('offer', handleReceivedOffer);
49 socketRef.current.on('answer', handleAnswer);
50 socketRef.current.on('ice-candidate', handlerNewIceCandidateMsg);
51
52 // clear up after
53 return () => socketRef.current.disconnect();
54 }, [roomName]);
55
56 const handleRoomJoined = () => {
57 navigator.mediaDevices
58 .getUserMedia({
59 audio: true,
60 video: { width: 500, height: 500 },
61 })
62 .then((stream) => {
63 /* use the stream */
64 userStreamRef.current = stream;
65 userVideoRef.current.srcObject = stream;
66 userVideoRef.current.onloadedmetadata = () => {
67 userVideoRef.current.play();
68 };
69 socketRef.current.emit('ready', roomName);
70 })
71 .catch((err) => {
72 /* handle the error */
73 console.log('error', err);
74 });
75 };
76
77
78
79 const handleRoomCreated = () => {
80 hostRef.current = true;
81 navigator.mediaDevices
82 .getUserMedia({
83 audio: true,
84 video: { width: 500, height: 500 },
85 })
86 .then((stream) => {
87 /* use the stream */
88 userStreamRef.current = stream;
89 userVideoRef.current.srcObject = stream;
90 userVideoRef.current.onloadedmetadata = () => {
91 userVideoRef.current.play();
92 };
93 })
94 .catch((err) => {
95 /* handle the error */
96 console.log(err);
97 });
98 };
99
100 const initiateCall = () => {
101 if (hostRef.current) {
102 rtcConnectionRef.current = createPeerConnection();
103 rtcConnectionRef.current.addTrack(
104 userStreamRef.current.getTracks()[0],
105 userStreamRef.current,
106 );
107 rtcConnectionRef.current.addTrack(
108 userStreamRef.current.getTracks()[1],
109 userStreamRef.current,
110 );
111 rtcConnectionRef.current
112 .createOffer()
113 .then((offer) => {
114 rtcConnectionRef.current.setLocalDescription(offer);
115 socketRef.current.emit('offer', offer, roomName);
116 })
117 .catch((error) => {
118 console.log(error);
119 });
120 }
121 };
122
123 const onPeerLeave = () => {
124 // This person is now the creator because they are the only person in the room.
125 hostRef.current = true;
126 if (peerVideoRef.current.srcObject) {
127 peerVideoRef.current.srcObject
128 .getTracks()
129 .forEach((track) => track.stop()); // Stops receiving all track of Peer.
130 }
131
132 // Safely closes the existing connection established with the peer who left.
133 if (rtcConnectionRef.current) {
134 rtcConnectionRef.current.ontrack = null;
135 rtcConnectionRef.current.onicecandidate = null;
136 rtcConnectionRef.current.close();
137 rtcConnectionRef.current = null;
138 }
139 }
140
141 /**
142 * Takes a userid which is also the socketid and returns a WebRTC Peer
143 *
144 * @param {string} userId Represents who will receive the offer
145 * @returns {RTCPeerConnection} peer
146 */
147
148 const createPeerConnection = () => {
149 // We create a RTC Peer Connection
150 const connection = new RTCPeerConnection(ICE_SERVERS);
151
152 // We implement our onicecandidate method for when we received a ICE candidate from the STUN server
153 connection.onicecandidate = handleICECandidateEvent;
154
155 // We implement our onTrack method for when we receive tracks
156 connection.ontrack = handleTrackEvent;
157 return connection;
158
159 };
160
161 const handleReceivedOffer = (offer) => {
162 if (!hostRef.current) {
163 rtcConnectionRef.current = createPeerConnection();
164 rtcConnectionRef.current.addTrack(
165 userStreamRef.current.getTracks()[0],
166 userStreamRef.current,
167 );
168 rtcConnectionRef.current.addTrack(
169 userStreamRef.current.getTracks()[1],
170 userStreamRef.current,
171 );
172 rtcConnectionRef.current.setRemoteDescription(offer);
173
174 rtcConnectionRef.current
175 .createAnswer()
176 .then((answer) => {
177 rtcConnectionRef.current.setLocalDescription(answer);
178 socketRef.current.emit('answer', answer, roomName);
179 })
180 .catch((error) => {
181 console.log(error);
182 });
183 }
184 };
185
186 const handleAnswer = (answer) => {
187 rtcConnectionRef.current
188 .setRemoteDescription(answer)
189 .catch((err) => console.log(err));
190 };
191
192 const handleICECandidateEvent = (event) => {
193 if (event.candidate) {
194 socketRef.current.emit('ice-candidate', event.candidate, roomName);
195 }
196 };
197
198 const handlerNewIceCandidateMsg = (incoming) => {
199 // We cast the incoming candidate to RTCIceCandidate
200 const candidate = new RTCIceCandidate(incoming);
201 rtcConnectionRef.current
202 .addIceCandidate(candidate)
203 .catch((e) => console.log(e));
204 };
205
206 const handleTrackEvent = (event) => {
207 // eslint-disable-next-line prefer-destructuring
208 peerVideoRef.current.srcObject = event.streams[0];
209 };
210
211 const toggleMediaStream = (type, state) => {
212 userStreamRef.current.getTracks().forEach((track) => {
213 if (track.kind === type) {
214 // eslint-disable-next-line no-param-reassign
215 track.enabled = !state;
216 }
217 });
218 };
219
220 const toggleMic = () => {
221 toggleMediaStream('audio', micActive);
222 setMicActive((prev) => !prev);
223 };
224
225 const toggleCamera = () => {
226 toggleMediaStream('video', cameraActive);
227 setCameraActive((prev) => !prev);
228 };
229
230 const leaveRoom = () => {
231 socketRef.current.emit('leave', roomName); // Let's the server know that user has left the room.
232
233 if (userVideoRef.current.srcObject) {
234 userVideoRef.current.srcObject.getTracks().forEach((track) => track.stop()); // Stops receiving all track of User.
235 }
236 if (peerVideoRef.current.srcObject) {
237 peerVideoRef.current.srcObject
238 .getTracks()
239 .forEach((track) => track.stop()); // Stops receiving audio track of Peer.
240 }
241
242 // Checks if there is peer on the other side and safely closes the existing connection established with the peer.
243 if (rtcConnectionRef.current) {
244 rtcConnectionRef.current.ontrack = null;
245 rtcConnectionRef.current.onicecandidate = null;
246 rtcConnectionRef.current.close();
247 rtcConnectionRef.current = null;
248 }
249 router.push('/')
250 };
251
252 return (
253 <div>
254 <video autoPlay ref={userVideoRef} />
255 <video autoPlay ref={peerVideoRef} />
256 <button onClick={toggleMic} type="button">
257 {micActive ? 'Mute Mic' : 'UnMute Mic'}
258 </button>
259 <button onClick={leaveRoom} type="button">
260 Leave
261 </button>
262 <button onClick={toggleCamera} type="button">
263 {cameraActive ? 'Stop Camera' : 'Start Camera'}
264 </button>
265 </div>
266 );
267};
268
269export default Room;
270
You should be able to open two tabs in your browser and have a video call with yourself to test the functioning app end-to-end!