What is WebRTC?
WebRTC is an opensource project that provides webApplications and sites to establish a peer2peer connection and do a real-time communication. It allows to send video, audio and data sharing between browsers.
P2P
WebRTC is a peer to peer protocol. This means that you directly send your media over to the other person without the need of a central server.
NOTE:
You do need a central server for signaling and sometimes for sending media as well (turn). We’ll be discussing this later.
Latency:- latency is the time it takes for data to travel from the source to the destination.
You use WebRTC for applications that require Less latency.
Examples include
- Zoom/Google meet (Multi party call)
- Omegle, teaching (1:1 call)
- 30FPS games (WebRTC can also send data)
What is a Signaling Server?
To establish a connection, Both the server need to send each other their address in order to know whom they have to connect to. A signaling server is used for that. A signaling server itself does not handle the actual media or data transfer but facilitates the initial communication setup.
It is usually a WebSocket server but can be a (http) server as well.
What is a STUN server?
The primary purpose of a STUN server is to allow a client (or Browser) to determine its public-facing (which the world sees) IP address and the port that the NAT(explained below) has assigned to a communication session. This information is then used in the signaling process to facilitate direct communication between Browsers.
So what is NAT(Network Address Translation)??
Simple NAT Example
1.Your laptop (192.168.1.2) wants to visit a website.
2.The router changes the address to its public IP (e.g., 203.0.113.1) and sends the request.
3.The website sends data back to 203.0.113.1.
4.The router receives the data, knows it was for 192.168.1.2, and sends it to your laptop.
Coming back to STUN server:- This is How it Works
ICE Candidates :=>
ICE (Interactive Connectivity Establishment) candidates are network endpoints that WebRTC peers use to establish direct peer-to-peer connections. Each ICE candidate consists of:
IP Address: This is the address that identifies a device on a network. It can be either a local (private) IP address or a public IP address.
Port Number: Ports are numeric identifiers used by networking protocols to distinguish different types of traffic on the same IP address. They help direct data to the correct application or service running on a device.
In simple terms ICE candidates are potential IP address and Port through which two Browsers or peers can establish connection.
If two friends are trying to connect to each other in a hostel wifi , then they can connect via their private router ice candidates.
If two people from different countries are trying to connect to each other, then they would connect via their public IPs.
What is a TURN server?
A lot of times, your network doesn’t allow media to come in from browser2. This depends on how restrictive your network is.
So When direct peer-to-peer connections fail due to NAT traversal issues(or Router issues), a TURN server acts as a solution. It relays media and data between peers, ensuring that communication can still occur even if direct connections are not possible.
Offer
The process of the first browser (the one initiating connection) sending their ice candidates to the other side.
Answer
The other side returning their ice candidates is called the answer.
SDP - Session description protocol
A single file that contains all your ice candidates, what media you want to send, what protocols you’ve used to encode the media. This is the file that is sent in the offer and received in the answer.
SDP Look like this:-
(No need to understand this....senior dev things :))
Summary:-
1.You need a signaling server, stun server to initiate the webrtc conn b/w the parties. You can kill these once the conn is made.
2.You need to include a turn server incase any of the users are on a restrictive network so you can get back a turn ice candidate as well.
Now Lets Dive into some Coding Stuff:-
RTCPeerConnection (pc, peer connection)
This is a class that the browser provides you with which gives you access to the SDP, lets you create answers / offers , lets you send media.
This class hides all the complexity of webrtc from the developer.
To know more:-
https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection
Basic Steps:- (Don't worry if it feels jargony right now!)
Connecting the two sides
The steps to create a webrtc connection between 2 sides includes -
- Browser 1 creates an RTCPeerConnection
- Browser 1 creates an offer
- Browser 1 sets the local description to the offer
- Browser 1 sends the offer to the other side through the signaling server
- Browser 2 receives the offer from the signaling server
- Browser 2 sets the remote description to the offer
- Browser 2 creates an answer
- Browser 2 sets the local description to be the answer
- Browser 2 sends the answer to the other side through the signaling server
- Browser 1 receives the answer and sets the remote description
This is just to establish the p2p connection b/w the two parties
To actually send media, we have to
- Ask for camera /mic permissions
- Get the audio and video streams
- Call addTrack on the pc
- This would trigger a OnTrack callback on the other side
Implementation
- We will be writing the code in Node.js for the Signaling server. It will be a websocket server that supports 3 types of messages
- createOffer
- createAnswer
- addIceCandidate
- React + PeerConnectionObject on the frontend.
If you want to get an idea on how it works:- https://jsfiddle.net/rainzhao/3L9sfsvf/
**
Backend
**
- Create an empty TS project, add ws to it
npm init -y
npx tsc --init
npm install ws @types/ws
- Change rootDir and outDir in tsconfig
"rootDir": "./src",
"outDir": "./dist",
- Create a simple websocket server
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
let senderSocket: null | WebSocket = null;
let receiverSocket: null | WebSocket = null;
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data: any) {
const message = JSON.parse(data);
});
ws.send('something');
});
- Try running the server
tsc -b
node dist/index.js
- Add message handlers
import { WebSocket, WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
let senderSocket: null | WebSocket = null;
let receiverSocket: null | WebSocket = null;
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data: any) {
const message = JSON.parse(data);
if (message.type === 'sender') {
senderSocket = ws;
} else if (message.type === 'receiver') {
receiverSocket = ws;
} else if (message.type === 'createOffer') {
if (ws !== senderSocket) {
return;
}
receiverSocket?.send(JSON.stringify({ type: 'createOffer', sdp: message.sdp }));
} else if (message.type === 'createAnswer') {
if (ws !== receiverSocket) {
return;
}
senderSocket?.send(JSON.stringify({ type: 'createAnswer', sdp: message.sdp }));
} else if (message.type === 'iceCandidate') {
if (ws === senderSocket) {
receiverSocket?.send(JSON.stringify({ type: 'iceCandidate', candidate: message.candidate }));
} else if (ws === receiverSocket) {
senderSocket?.send(JSON.stringify({ type: 'iceCandidate', candidate: message.candidate }));
}
}
});
});
That is all that you need for a simple one way communication b/w two tabs.
**
Frontend
**
- Create a frontend repo
npm create vite@latest
- Add two routes, one for a sender and one for a receiver
import { useState } from 'react'
import './App.css'
import { Route, BrowserRouter, Routes } from 'react-router-dom'
import { Sender } from './components/Sender'
import { Receiver } from './components/Receiver'
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/sender" element={<Sender />} />
<Route path="/receiver" element={<Receiver />} />
</Routes>
</BrowserRouter>
)
}
export default App
Remove strict mode in main.tsx to get rid of a bunch of webrtc connections locally (not really needed).
Go to src folder and create a components folder inside which create two files Sender.tsx and Reciever.tsx
5.Create components for sender
import { useEffect, useState } from "react"
export const Sender = () => {
const [socket, setSocket] = useState<WebSocket | null>(null);
const [pc, setPC] = useState<RTCPeerConnection | null>(null);
useEffect(() => {
const socket = new WebSocket('ws://localhost:8080');
setSocket(socket);
socket.onopen = () => {
socket.send(JSON.stringify({
type: 'sender'
}));
}
}, []);
const initiateConn = async () => {
if (!socket) {
alert("Socket not found");
return;
}
socket.onmessage = async (event) => {
const message = JSON.parse(event.data);
if (message.type === 'createAnswer') {
await pc.setRemoteDescription(message.sdp);
} else if (message.type === 'iceCandidate') {
pc.addIceCandidate(message.candidate);
}
}
const pc = new RTCPeerConnection();
setPC(pc);
pc.onicecandidate = (event) => {
if (event.candidate) {
socket?.send(JSON.stringify({
type: 'iceCandidate',
candidate: event.candidate
}));
}
}
pc.onnegotiationneeded = async () => {
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
socket?.send(JSON.stringify({
type: 'createOffer',
sdp: pc.localDescription
}));
}
getCameraStreamAndSend(pc);
}
const getCameraStreamAndSend = (pc: RTCPeerConnection) => {
navigator.mediaDevices.getUserMedia({ video: true }).then((stream) => {
const video = document.createElement('video');
video.srcObject = stream;
video.play();
// this is wrong, should propogate via a component
document.body.appendChild(video);
stream.getTracks().forEach((track) => {
pc?.addTrack(track);
});
});
}
return <div>
Sender
<button onClick={initiateConn}> Send data </button>
</div>
}
- Create the component for receiver
import { useEffect } from "react"
export const Receiver = () => {
useEffect(() => {
const socket = new WebSocket('ws://localhost:8080');
socket.onopen = () => {
socket.send(JSON.stringify({
type: 'receiver'
}));
}
startReceiving(socket);
}, []);
function startReceiving(socket: WebSocket) {
const video = document.createElement('video');
document.body.appendChild(video);
const pc = new RTCPeerConnection();
pc.ontrack = (event) => {
video.srcObject = new MediaStream([event.track]);
video.play();
}
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === 'createOffer') {
pc.setRemoteDescription(message.sdp).then(() => {
pc.createAnswer().then((answer) => {
pc.setLocalDescription(answer);
socket.send(JSON.stringify({
type: 'createAnswer',
sdp: answer
}));
});
});
} else if (message.type === 'iceCandidate') {
pc.addIceCandidate(message.candidate);
}
}
}
return <div>
</div>
}
And You are good to go!!
Do like and share it if you found it useful.
Share your views below and comment if you have a doubt.
Thanks :)
Top comments (5)
Thanks for the appreciation :)
I might have to try this
Sure
Great project, thanks for sharing!
My pleasure :)