What is this simple app?
I built a single static page app so that we can easily understand how to use the WebRTC API to establish communication between two peers.
Let me tell you in advance, it is not a complex or feature-rich app. It is just a static page and server made with around 100 lines of code, solely for learning purpose.
The page has two video elements: one for the user itself and another one for the remote peer, along with a button to start streaming.
Demo
I hope you don't judge this demo by my webcam quality.
Code walkthrough
File Structure
sample-webrtc-app/
└── public/
├── index.html
├── main.js
├── server.js
├── package.json
├── package-lock.json
├── LICENSE
├── server.js
├── .gitignore
public folder contains client-side code.
server.js contains code related to signaling server. We already learned what is signaling server in the previous blog: https://blog.denilgabani.com/signaling-in-webrtc
package.json contains the required dependency.
Client-side code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebRTC Example</title>
</head>
<body>
<h1>WebRTC Example</h1>
<div>
<video id="localVideo" autoplay></video>
<video id="remoteVideo" autoplay></video>
</div>
<button id="start">Start</button>
<script
src="https://cdn.socket.io/4.7.5/socket.io.min.js"
integrity="sha384-2huaZvOR9iDzHqslqwpR87isEmrfxqyWOF7hr7BY6KG0+hVKLoEXMPUJw3ynWuhO"
crossorigin="anonymous"
></script>
<script src="main.js"></script>
</body>
</html>
Two video elements are added in above static HTML file for peer1 and peer2.
One button with id "start" to start the stream.
Added socket.io script using cdn. Here we get a question why Socket.io is used?
Communication with signaling server is better with WebSocket because of two-way communication functionality.
Socket.IO is a library that enables low-latency, bidirectional, and event-based communication between a client and a server. It is easy to use Socket.io library instead of writing code for WebSocket from scratch. It gives us more focus on WebRTC.
Added our client-side script
main.js
.
const localVideo = document.getElementById("localVideo");
const remoteVideo = document.getElementById("remoteVideo");
let start = document.getElementById("start");
let localStream;
let pc;
const socket = io();
async function init() {
try {
localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
localVideo.srcObject = localStream;
pc = new RTCPeerConnection();
pc.ontrack = (event) => {
console.log("event: ", event);
if (event.streams && event.streams[0]) {
remoteVideo.srcObject = event.streams[0]; // Use event.streams[0] for remote stream
}
};
pc.onicecandidate = (event) => {
if (event.candidate) {
// Send the candidate to the remote peer
socket.emit("message", { candidate: event.candidate });
}
};
localStream
.getTracks()
.forEach((track) => pc.addTrack(track, localStream));
} catch (error) {
console.error("Error starting:", error);
}
}
async function startProcess() {
try {
// Create offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// Send the offer to the remote peer
socket.emit("message", { offer: pc.localDescription });
} catch (error) {
console.error("Error starting:", error);
}
}
async function receiveOfferAndCreateAnswer(offer) {
try {
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
// Send the answer to the remote peer
socket.emit("message", { answer: pc.localDescription });
} catch (error) {
console.error("Error receiving offer and creating answer:", error);
}
}
socket.on("message", (message) => {
console.log("Received from signaling server:", message);
if (message.offer) {
receiveOfferAndCreateAnswer(message.offer);
} else if (message.answer) {
pc.setRemoteDescription(message.answer);
} else if (message.candidate) {
pc.addIceCandidate(new RTCIceCandidate(message.candidate));
}
});
start.addEventListener("click", startProcess);
init();
Server-side code
// server.js
const express = require("express");
const http = require("http");
const socketIo = require("socket.io");
const app = express();
const server = http.createServer(app);
const io = socketIo(server);
// Serve the client-side code
app.use(express.static(__dirname + "/public"));
// Handle WebSocket connections
io.on("connection", (socket) => {
console.log("A user connected");
// Handle messages from clients
socket.on("message", (message) => {
console.log("Received message:", message);
// Broadcast the message to all other clients
socket.broadcast.emit("message", message);
});
// Handle disconnections
socket.on("disconnect", () => {
console.log("A user disconnected");
});
});
const PORT = process.env.PORT || 3000;
//starting the serve on PORT
server.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
Walkthrough
Let's go through this code step-by-step
Peer1 opens our hosted URL, in this case our local server, http://localhost:3000.
It accesses the webcam stream using
navigator.mediaDevices.getUserMedia()
and assigns it to thelocalVideo
element.Peer1 is now able to see their webcam stream on the page.
A peer connection object is created using
RTCPeerConnection()
.Two event functions are assigned to the
pc
(Peer Connection) object:ontrack
: When Peer2 adds a stream (which will be set in step 6), it shows it in the second video elementremoteVideo
.onicecandidate
: When an ICE candidate(provides the possible communication channels for WebRTC peers to explore, allowing them to overcome network barriers like NAT and firewalls to establish a connection) is received, it sends it to the signaling server over themessage
event using a socket.
After this stream is being added to the Peer Connection object using
pc.addTrack()
, which will invoke the callback function provided inontrack
on the Peer2 window. This is why the stream is being set in theremoteVideo
element.Peer2 opens a window, and the same process happens from steps 1 to 6 in their browser.
Clicking on the start button comes to the play.
When Peer1 clicks on the start button, it creates an offer:
It creates the offer using
createOffer()
.Sets it to the
localDescription
of the Peer Connection object.Sends it to the event named "message" with offer data, which will be consumed by the signaling server.
The "message" event is consumed by the signaling server (as seen in
server.js
), and it broadcasts this event. In this case, it will send the event to Peer2.On Peer2's browser, it executes the callback function provided on the "message" event.
Now, this "message" event contains offer data, so it starts executing
receiveOfferAndCreateAnswer()
:The received offer data is set to the remote description on the Peer Connection object using
setRemoteDescription()
.Answer data is created using
createAnswer()
, and then it is set to the local description as the answer data is local, and the offer data is remote for Peer2.
Peer2 emits answer data over the "message" event, which will be consumed by the signaling server, and it will broadcast this data to Peer1.
Peer1 sets the answer data as the remote description upon receiving it in the "message" event.
After that, ICE candidates are exchanged between Peer1 and Peer2 through the signaling server to find the best route to communicate.
Once the best route is found, communication is established, and the rest of the stream is passed directly between Peer1 and Peer2 without any intervention from the signaling server.
Yes, you guessed it right both streams of Peer1 and Peer2 are visible on the screen.
I don't know if you understand all the above steps or not but
I have not added many things in these like STUN server and all. I have kept it simple to understand Peer Connection. Don't worry We will also learn STUN server and other things in our upcoming blogs. Just take it easy.
Github repo link
If you find some corrections or something useful to mention then feel free to comment💬 under the blog.
To stay updated and learn together follow me on Twitter@denilgabani or subscribe to my newsletter.