MICHAEL COSTAÑOS.

January 15, 2026

The WebRTC handbook: Go Fiber & Javascript for real time video

In the world of web development, we are mostly trained to think in terms of Request/Response. But for high-bandwidth video data with sub-second latency, standard HTTP won't cut it. To build a modern video call app, you need a powerful duo: WebSockets for the handshake and WebRTC for the peer-to-peer (P2P) data flow.

The Architecture: The "Signaling" Handshake

Because WebRTC is peer-to-peer, browsers need a way to find each other's IP addresses and agree on media formats. Since they can't talk directly yet, they use a Signaling Server. Think of this as the "digital matchmaker" that introduces two users before they start talking directly.

The Backend: High-Speed Signaling with Go Fiber

Using the Fiber framework in Go allows us to handle WebSocket upgrades with minimal overhead. The server's only job is to route JSON packets between Peer A and Peer B.

1. The Fiber WebSocket Handler

This snippet demonstrates how Fiber upgrades a connection and handles the message routing logic.

app.Get("/ws/:id", websocket.New(func(c *websocket.Conn) {
    id := c.Params("id")
    clients[id] = c // Register the user locally
    
    defer func() {
        delete(clients, id)
        c.Close()
    }()

    for {
        var msg map[string]interface{}
        if err := c.ReadJSON(&msg); err != nil {
            break
        }
        
        // Extract recipient and route the WebRTC handshake data
        if targetID, ok := msg["to"].(string); ok {
            if targetClient, ok := clients[targetID]; ok {
                targetClient.WriteJSON(msg)
            }
        }
    }
}))

The Frontend: The WebRTC State Machine

The core of WebRTC is the RTCPeerConnection. To get it working, both sides must follow a strict "Offer-Answer" dance.

1. Peer A: Initiating the Call

Peer A captures their camera, creates an Offer, and sets it as their LocalDescription.

const localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
const peerConnection = new RTCPeerConnection({
    iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] // Public STUN server
});

// Add camera tracks to the connection
localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));

// Create and send Offer
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
socket.send(JSON.stringify({ type: "offer", data: offer, to: "peer-b-id" }));

2. Peer B: Receiving and Answering

When Peer B receives the offer via the Go WebSocket, they must set it as their RemoteDescription, create an Answer, and send it back.

socket.onmessage = async (msg) => {
    const { type, data, from } = JSON.parse(msg.data);

    if (type === "offer") {
        await peerConnection.setRemoteDescription(new RTCSessionDescription(data));
        
        const answer = await peerConnection.createAnswer();
        await peerConnection.setLocalDescription(answer);
        
        socket.send(JSON.stringify({ type: "answer", data: answer, to: from }));
    } else if (type === "answer") {
        // Peer A receives this and completes the loop
        await peerConnection.setRemoteDescription(new RTCSessionDescription(data));
    }
};

3. The Vital Link: ICE Candidates

Even with descriptions set, browsers need to find a path through firewalls. This is handled via ICE Candidates. Both peers must listen for these and exchange them immediately.

peerConnection.onicecandidate = (event) => {
    if (event.candidate) {
        socket.send(JSON.stringify({ type: "candidate", data: event.candidate, to: "other-peer-id" }));
    }
};

// When a candidate is received:
if (type === "candidate") {
    await peerConnection.addIceCandidate(new RTCIceCandidate(data));
}

Displaying the Stream

Once the handshake is complete and ICE candidates are exchanged, the ontrack event fires. The video data now flows directly between browsers, bypassing your Go server entirely.

peerConnection.ontrack = (event) => {
    const remoteVideo = document.getElementById('remoteVideo');
    remoteVideo.srcObject = event.streams[0];
};

Conclusion

By combining Go Fiber for lightweight, high-speed signaling and WebRTC for P2P media, you create an architecture that is both performant and private. The server handles the "introduction," while the heavy lifting of video processing is distributed across the users' hardware.