DEV Community

Cover image for Bypassing the Server: WebRTC Architecture in Next.js 📡
Prajapati Paresh
Prajapati Paresh

Posted on Originally published at smarttechdevs.in

Bypassing the Server: WebRTC Architecture in Next.js 📡

The Bandwidth Bottleneck of Centralized Media

In traditional web architecture, all data flows through a central server. If you build a real-time chat application using WebSockets, Client A sends a message to the Node.js server, and the server forwards that message to Client B. For lightweight JSON text messages, this architecture is perfectly fine.

However, if you attempt to build a live video conferencing tool, a screen-sharing app, or a secure large-file transfer system, routing traffic through a central server becomes a catastrophic bottleneck. High-definition video streams consume massive amounts of bandwidth. If your central server is processing 500 concurrent 1080p video streams, your AWS egress bandwidth costs will bankrupt your company in a matter of days. Furthermore, routing a video stream from a user in London, up to a server in New York, and back down to another user in London introduces intolerable latency.

At Smart Tech Devs, we build ultra-low latency media and data streaming applications by bypassing the central server entirely. We achieve this using WebRTC (Web Real-Time Communication), a native browser API that establishes direct, Peer-to-Peer (P2P) connections between users.

Understanding the WebRTC Architecture

WebRTC is an architectural marvel, but it is notoriously complex to implement because computers cannot simply "find" each other on the open internet due to NATs (Network Address Translators) and corporate firewalls.

To establish a P2P connection, WebRTC requires a multi-step architectural flow:

  • The Signaling Server: Before a P2P connection can start, Client A and Client B need to exchange technical routing information (IP addresses, media formats). Because they can't talk directly yet, they use a lightweight, central server (usually WebSockets) to pass these "Signaling" messages.
  • STUN/TURN Servers: To discover their own public IP addresses and bypass firewalls, the clients query a STUN server. If the firewall is impenetrable, traffic falls back to a TURN server (a relay).
  • The P2P Connection: Once the technical negotiation (SDP Offer/Answer) is complete, the Signaling Server steps out of the way. The heavy video or data stream now flows directly between Client A and Client B, costing you zero server bandwidth.

Phase 1: Architecting the React WebRTC Hook

Because WebRTC relies heavily on mutable JavaScript objects (like RTCPeerConnection) that must persist across renders without triggering UI updates, we must architect our Next.js frontend using useRef heavily.

Let's build the core logic for the client who initiates the call (The Caller).


// hooks/useWebRTC.ts
import { useEffect, useRef, useState } from 'react';

export function useWebRTC(signalingSocket: any) {
    const peerConnection = useRef(null);
    const localVideoRef = useRef(null);
    const remoteVideoRef = useRef(null);
    const [callStatus, setCallStatus] = useState('Idle');

    useEffect(() => {
        // 1. Initialize the RTCPeerConnection with public STUN servers
        peerConnection.current = new RTCPeerConnection({
            iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
        });

        // 2. Listen for remote media tracks arriving from the peer
        peerConnection.current.ontrack = (event) => {
            if (remoteVideoRef.current) {
                remoteVideoRef.current.srcObject = event.streams[0];
                setCallStatus('Connected');
            }
        };

        // 3. Listen for ICE Candidates (network routing paths) and send them to the peer
        // via our lightweight Signaling Server
        peerConnection.current.onicecandidate = (event) => {
            if (event.candidate) {
                signalingSocket.send(JSON.stringify({
                    type: 'ice-candidate',
                    candidate: event.candidate,
                }));
            }
        };

        return () => peerConnection.current?.close();
    }, [signalingSocket]);

    return { peerConnection, localVideoRef, remoteVideoRef, callStatus, setCallStatus };
}

Phase 2: The Offer and Answer Handshake

To start the stream, the Caller must capture their local webcam, attach it to the P2P connection, and generate an "Offer" (a Session Description Protocol or SDP). They send this Offer to the Receiver via the Signaling Server.


// Inside the component utilizing the hook...

const startCall = async () => {
    setCallStatus('Starting local media...');
    
    // 1. Request access to the user's Webcam and Microphone
    const localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
    
    // 2. Display the local video on the screen
    if (localVideoRef.current) localVideoRef.current.srcObject = localStream;

    // 3. Attach the media tracks to the WebRTC connection
    localStream.getTracks().forEach((track) => {
        peerConnection.current?.addTrack(track, localStream);
    });

    // 4. Create the formal SDP Offer
    const offer = await peerConnection.current?.createOffer();
    await peerConnection.current?.setLocalDescription(offer);

    // 5. Send the Offer to the other user via the Signaling Server
    signalingSocket.send(JSON.stringify({
        type: 'video-offer',
        sdp: offer
    }));
    
    setCallStatus('Calling...');
};

Phase 3: The Receiver's Logic

When the Receiver gets the Offer via the Signaling Server, they must accept it, attach their own webcam, and generate an "Answer."


// Handling incoming messages from the Signaling Server

signalingSocket.onmessage = async (message) => {
    const data = JSON.parse(message.data);

    if (data.type === 'video-offer') {
        // 1. Accept the remote offer
        await peerConnection.current?.setRemoteDescription(new RTCSessionDescription(data.sdp));
        
        // 2. Get local media to send back
        const localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
        localStream.getTracks().forEach(t => peerConnection.current?.addTrack(t, localStream));
        
        // 3. Generate the Answer and send it back via Signaling
        const answer = await peerConnection.current?.createAnswer();
        await peerConnection.current?.setLocalDescription(answer);
        
        signalingSocket.send(JSON.stringify({ type: 'video-answer', sdp: answer }));
    }
    
    if (data.type === 'ice-candidate') {
        // Add the network routing paths discovered by the peer
        await peerConnection.current?.addIceCandidate(new RTCIceCandidate(data.candidate));
    }
};

The Engineering ROI and Data Channels

WebRTC is the undisputed king of real-time communication on the web. By shifting the heavy lifting from a centralized server architecture to a distributed, Peer-to-Peer architecture, you completely eradicate server bandwidth costs for rich media. The latency is mathematically minimized, as data takes the shortest possible physical path across the internet directly between the two users.

Furthermore, WebRTC is not limited to video. The RTCDataChannel API allows you to send arbitrary binary data (like 50GB files) directly between browsers using the exact same secure, encrypted P2P handshake. For enterprise collaboration tools, secure file sharing, and live broadcasting, mastering WebRTC architecture is the ultimate frontend superpower.

Top comments (0)