DEV Community

Cover image for WebRTC : Create Your First WebRTC connection!
Nirvan Jha
Nirvan Jha

Posted on

WebRTC : Create Your First WebRTC connection!

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.

Image description

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

  1. Zoom/Google meet (Multi party call)
  2. Omegle, teaching (1:1 call)
  3. 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.

Image description

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)??

Image description

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

Image description

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:

  1. 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.

  2. 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.

Image description

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 :))
Image description

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 -

  1. Browser 1 creates an RTCPeerConnection
  2. Browser 1 creates an offer
  3. Browser 1 sets the local description to the offer
  4. Browser 1 sends the offer to the other side through the signaling server
  5. Browser 2 receives the offer from the signaling server
  6. Browser 2 sets the remote description to the offer
  7. Browser 2 creates an answer
  8. Browser 2 sets the local description to be the answer
  9. Browser 2 sends the answer to the other side through the signaling server
  10. 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

  1. Ask for camera /mic permissions
  2. Get the audio and video streams
  3. Call addTrack on the pc
  4. This would trigger a OnTrack callback on the other side

Implementation

  1. 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
    1. createOffer
    2. createAnswer
    3. addIceCandidate
  2. React + PeerConnectionObject on the frontend.

If you want to get an idea on how it works:- https://jsfiddle.net/rainzhao/3L9sfsvf/

**

Backend

**

  1. Create an empty TS project, add ws to it
npm init -y
npx tsc --init
npm install ws @types/ws
Enter fullscreen mode Exit fullscreen mode
  1. Change rootDir and outDir in tsconfig
"rootDir": "./src",
"outDir": "./dist",
Enter fullscreen mode Exit fullscreen mode
  1. 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');
});
Enter fullscreen mode Exit fullscreen mode
  1. Try running the server
tsc -b
node dist/index.js
Enter fullscreen mode Exit fullscreen mode
  1. 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 }));
      }
    }
  });
});
Enter fullscreen mode Exit fullscreen mode

That is all that you need for a simple one way communication b/w two tabs.

**

Frontend

**

  1. Create a frontend repo
npm create vite@latest
Enter fullscreen mode Exit fullscreen mode
  1. 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
Enter fullscreen mode Exit fullscreen mode
  1. Remove strict mode in main.tsx to get rid of a bunch of webrtc connections locally (not really needed).

  2. 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>
}
Enter fullscreen mode Exit fullscreen mode
  1. 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>
}
Enter fullscreen mode Exit fullscreen mode

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 (6)

Collapse
 
matin_mollapur profile image
Matin Mollapur

great introduction to webrtc! you’ve done a fantastic job explaining the core concepts like signaling, stun, and turn servers in a clear and understandable way. the step-by-step guide on setting up a basic connection and the detailed code examples make it easy to follow along. really appreciate the practical approach and thorough explanations. this is a must-read for anyone looking to dive into webrtc. thanks for sharing!

Collapse
 
nirvanjha2004 profile image
Nirvan Jha

Thanks for the appreciation :)

Collapse
 
jyoung4242 profile image
Justin Young

I might have to try this

Collapse
 
nirvanjha2004 profile image
Nirvan Jha

Sure

Collapse
 
efpage profile image
Eckehard

Great project, thanks for sharing!

Collapse
 
nirvanjha2004 profile image
Nirvan Jha

My pleasure :)