Backend Communication
Current: WebRTC

What is WebRTC?

WebRTC (Web Real-Time Communication) enables peer-to-peer audio, video, and data sharing directly between browsers and native applications. Unlike traditional client-server models, WebRTC allows clients to communicate directly with each other after establishing a connection through a signaling server.

Key characteristics:

  • Peer-to-peer - Direct communication between clients
  • Low latency - Minimal delay for real-time media
  • NAT traversal - STUN/TURN servers handle firewall traversal
  • Encryption - Built-in DTLS/SRTP security
  • Multiple data types - Audio, video, and arbitrary data channels

Architecture Overview

sequenceDiagram participant A as Peer A participant S as Signaling Server participant STUN as STUN Server participant TURN as TURN Server participant B as Peer B Note over A,B: Discovery & Signaling A->>S: Create Offer (SDP) S->>B: Forward Offer B->>S: Create Answer (SDP) S->>A: Forward Answer Note over A,B: ICE Candidate Exchange A->>STUN: Discover public IP STUN-->>A: Your public address A->>S: ICE Candidate S->>B: Forward Candidate B->>STUN: Discover public IP STUN-->>B: Your public address B->>S: ICE Candidate S->>A: Forward Candidate rect rgb(200, 220, 240) Note over A,B: Direct P2P Connection A<-->B: Media/Data (DTLS/SRTP) end rect rgb(240, 220, 200) Note over A,TURN,B: Fallback via TURN A<-->TURN: Relay TURN<-->B: Relay end

WebRTC Connection Lifecycle

  1. Signaling - Exchange connection metadata (SDP offers/answers)
  2. ICE Gathering - Discover network candidates
  3. ICE Checking - Test connectivity between candidates
  4. Connection - Establish peer-to-peer connection
  5. Communication - Exchange media and data
  6. Disconnection - Clean up resources

Real-World Use Cases

  • Video Conferencing - Zoom, Google Meet, Microsoft Teams
  • Live Streaming - Twitch-style broadcasts with low latency
  • File Sharing - P2P file transfer (ShareDrop, Snapdrop)
  • Screen Sharing - Remote collaboration tools
  • Online Gaming - Low-latency multiplayer games
  • IoT Communication - Direct device-to-device communication
  • Telemedicine - Doctor-patient video consultations

Implementation in Go with Pion

Project Structure

webrtc-server/
├── main.go
├── signaling/
   ├── server.go
   └── room.go
├── webrtc/
   ├── peer.go
   └── sfu.go
├── static/
   └── index.html
└── go.mod

Dependencies

go.mod

module webrtc-server

go 1.21

require (
    github.com/gorilla/websocket v1.5.1
    github.com/pion/webrtc/v3 v3.2.40
    github.com/pion/ice/v2 v2.3.11
)

1. WebRTC Peer Connection

webrtc/peer.go

package webrtc

import (
    "encoding/json"
    "fmt"
    "log"
    "sync"

    "github.com/pion/webrtc/v3"
)

// Peer represents a WebRTC peer connection
type Peer struct {
    ID         string
    PC         *webrtc.PeerConnection
    DataChannel *webrtc.DataChannel
    mu         sync.RWMutex
    OnICE      func(candidate *webrtc.ICECandidate)
    OnTrack    func(track *webrtc.TrackRemote)
    OnData     func(msg []byte)
}

// Config for WebRTC
var webrtcConfig = webrtc.Configuration{
    ICEServers: []webrtc.ICEServer{
        {
            URLs: []string{"stun:stun.l.google.com:19302"},
        },
        // Add TURN server for production
        // {
        //     URLs:       []string{"turn:turn.example.com:3478"},
        //     Username:   "username",
        //     Credential: "password",
        // },
    },
}

// NewPeer creates a new WebRTC peer connection
func NewPeer(id string) (*Peer, error) {
    // Create media engine
    mediaEngine := &webrtc.MediaEngine{}
    if err := mediaEngine.RegisterDefaultCodecs(); err != nil {
        return nil, err
    }

    // Create API with media engine
    api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine))

    // Create peer connection
    pc, err := api.NewPeerConnection(webrtcConfig)
    if err != nil {
        return nil, err
    }

    peer := &Peer{
        ID: id,
        PC: pc,
    }

    // Setup ICE candidate handler
    pc.OnICECandidate(func(candidate *webrtc.ICECandidate) {
        if candidate != nil && peer.OnICE != nil {
            peer.OnICE(candidate)
        }
    })

    // Handle connection state changes
    pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
        log.Printf("Peer %s: Connection state changed to %s", id, state.String())
    })

    // Handle ICE connection state
    pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
        log.Printf("Peer %s: ICE state changed to %s", id, state.String())
        if state == webrtc.ICEConnectionStateFailed {
            log.Printf("Peer %s: ICE connection failed", id)
        }
    })

    // Handle incoming tracks
    pc.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
        log.Printf("Peer %s: Received track: %s (type: %s)", id, track.ID(), track.Kind())
        if peer.OnTrack != nil {
            peer.OnTrack(track)
        }
    })

    return peer, nil
}

// CreateOffer creates an SDP offer
func (p *Peer) CreateOffer() (*webrtc.SessionDescription, error) {
    p.mu.Lock()
    defer p.mu.Unlock()

    offer, err := p.PC.CreateOffer(nil)
    if err != nil {
        return nil, err
    }

    if err = p.PC.SetLocalDescription(offer); err != nil {
        return nil, err
    }

    return &offer, nil
}

// CreateAnswer creates an SDP answer
func (p *Peer) CreateAnswer() (*webrtc.SessionDescription, error) {
    p.mu.Lock()
    defer p.mu.Unlock()

    answer, err := p.PC.CreateAnswer(nil)
    if err != nil {
        return nil, err
    }

    if err = p.PC.SetLocalDescription(answer); err != nil {
        return nil, err
    }

    return &answer, nil
}

// SetRemoteDescription sets remote SDP
func (p *Peer) SetRemoteDescription(sdp webrtc.SessionDescription) error {
    p.mu.Lock()
    defer p.mu.Unlock()

    return p.PC.SetRemoteDescription(sdp)
}

// AddICECandidate adds ICE candidate
func (p *Peer) AddICECandidate(candidate webrtc.ICECandidateInit) error {
    p.mu.Lock()
    defer p.mu.Unlock()

    return p.PC.AddICECandidate(candidate)
}

// CreateDataChannel creates a data channel
func (p *Peer) CreateDataChannel(label string) error {
    p.mu.Lock()
    defer p.mu.Unlock()

    dc, err := p.PC.CreateDataChannel(label, nil)
    if err != nil {
        return err
    }

    p.DataChannel = dc

    // Setup data channel handlers
    dc.OnOpen(func() {
        log.Printf("Peer %s: Data channel '%s' opened", p.ID, label)
    })

    dc.OnMessage(func(msg webrtc.DataChannelMessage) {
        log.Printf("Peer %s: Received data: %s", p.ID, string(msg.Data))
        if p.OnData != nil {
            p.OnData(msg.Data)
        }
    })

    dc.OnClose(func() {
        log.Printf("Peer %s: Data channel '%s' closed", p.ID, label)
    })

    return nil
}

// SendData sends data over data channel
func (p *Peer) SendData(data []byte) error {
    p.mu.RLock()
    defer p.mu.RUnlock()

    if p.DataChannel == nil {
        return fmt.Errorf("data channel not initialized")
    }

    return p.DataChannel.Send(data)
}

// AddTrack adds media track
func (p *Peer) AddTrack(track *webrtc.TrackLocalStaticRTP) (*webrtc.RTPSender, error) {
    p.mu.Lock()
    defer p.mu.Unlock()

    return p.PC.AddTrack(track)
}

// Close closes the peer connection
func (p *Peer) Close() error {
    p.mu.Lock()
    defer p.mu.Unlock()

    if p.DataChannel != nil {
        p.DataChannel.Close()
    }

    return p.PC.Close()
}

2. Signaling Server with WebSocket

signaling/server.go

package signaling

import (
    "encoding/json"
    "log"
    "net/http"
    "sync"

    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        return true // In production, validate origin
    },
}

// MessageType represents signaling message types
type MessageType string

const (
    TypeOffer       MessageType = "offer"
    TypeAnswer      MessageType = "answer"
    TypeCandidate   MessageType = "candidate"
    TypeJoin        MessageType = "join"
    TypeLeave       MessageType = "leave"
    TypePeers       MessageType = "peers"
)

// Message represents a signaling message
type Message struct {
    Type   MessageType     `json:"type"`
    From   string          `json:"from,omitempty"`
    To     string          `json:"to,omitempty"`
    Room   string          `json:"room,omitempty"`
    Data   json.RawMessage `json:"data,omitempty"`
}

// Client represents a signaling client
type Client struct {
    ID   string
    Conn *websocket.Conn
    Room *Room
    Send chan *Message
}

// Room represents a signaling room
type Room struct {
    ID      string
    Clients map[string]*Client
    mu      sync.RWMutex
}

// Server manages signaling
type Server struct {
    rooms map[string]*Room
    mu    sync.RWMutex
}

// NewServer creates a new signaling server
func NewServer() *Server {
    return &Server{
        rooms: make(map[string]*Room),
    }
}

// HandleWebSocket handles WebSocket connections
func (s *Server) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Printf("Upgrade error: %v", err)
        return
    }

    client := &Client{
        Conn: conn,
        Send: make(chan *Message, 256),
    }

    go client.writePump()
    client.readPump(s)
}

func (c *Client) readPump(s *Server) {
    defer func() {
        if c.Room != nil {
            c.Room.Leave(c)
        }
        c.Conn.Close()
    }()

    for {
        var msg Message
        err := c.Conn.ReadJSON(&msg)
        if err != nil {
            if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
                log.Printf("WebSocket error: %v", err)
            }
            break
        }

        msg.From = c.ID
        s.handleMessage(c, &msg)
    }
}

func (c *Client) writePump() {
    for msg := range c.Send {
        if err := c.Conn.WriteJSON(msg); err != nil {
            log.Printf("Write error: %v", err)
            return
        }
    }
}

func (s *Server) handleMessage(client *Client, msg *Message) {
    switch msg.Type {
    case TypeJoin:
        var data struct {
            Room   string `json:"room"`
            UserID string `json:"userId"`
        }
        if err := json.Unmarshal(msg.Data, &data); err != nil {
            log.Printf("Invalid join data: %v", err)
            return
        }

        client.ID = data.UserID
        room := s.getOrCreateRoom(data.Room)
        room.Join(client)

    case TypeOffer, TypeAnswer, TypeCandidate:
        // Forward to specific peer
        if client.Room != nil && msg.To != "" {
            client.Room.SendTo(msg.To, msg)
        }

    case TypeLeave:
        if client.Room != nil {
            client.Room.Leave(client)
        }
    }
}

func (s *Server) getOrCreateRoom(id string) *Room {
    s.mu.Lock()
    defer s.mu.Unlock()

    room, exists := s.rooms[id]
    if !exists {
        room = &Room{
            ID:      id,
            Clients: make(map[string]*Client),
        }
        s.rooms[id] = room
        log.Printf("Created room: %s", id)
    }

    return room
}

// Room methods

func (r *Room) Join(client *Client) {
    r.mu.Lock()
    defer r.mu.Unlock()

    client.Room = r
    r.Clients[client.ID] = client

    log.Printf("Client %s joined room %s (total: %d)", client.ID, r.ID, len(r.Clients))

    // Send list of existing peers
    peers := make([]string, 0, len(r.Clients))
    for id := range r.Clients {
        if id != client.ID {
            peers = append(peers, id)
        }
    }

    peersData, _ := json.Marshal(map[string]interface{}{
        "peers": peers,
    })

    client.Send <- &Message{
        Type: TypePeers,
        Data: peersData,
    }

    // Notify others
    r.broadcastExcept(client.ID, &Message{
        Type: TypeJoin,
        From: client.ID,
    })
}

func (r *Room) Leave(client *Client) {
    r.mu.Lock()
    defer r.mu.Unlock()

    delete(r.Clients, client.ID)
    close(client.Send)

    log.Printf("Client %s left room %s (remaining: %d)", client.ID, r.ID, len(r.Clients))

    // Notify others
    r.broadcastExcept(client.ID, &Message{
        Type: TypeLeave,
        From: client.ID,
    })
}

func (r *Room) SendTo(clientID string, msg *Message) {
    r.mu.RLock()
    defer r.mu.RUnlock()

    if client, exists := r.Clients[clientID]; exists {
        select {
        case client.Send <- msg:
        default:
            log.Printf("Client %s send buffer full", clientID)
        }
    }
}

func (r *Room) broadcastExcept(exceptID string, msg *Message) {
    for id, client := range r.Clients {
        if id != exceptID {
            select {
            case client.Send <- msg:
            default:
                log.Printf("Client %s send buffer full", id)
            }
        }
    }
}

3. Main Server Application

main.go

package main

import (
    "log"
    "net/http"

    "webrtc-server/signaling"
)

func main() {
    // Create signaling server
    sigServer := signaling.NewServer()

    // Setup routes
    mux := http.NewServeMux()

    // WebSocket signaling endpoint
    mux.HandleFunc("/ws", sigServer.HandleWebSocket)

    // Serve static files
    mux.Handle("/", http.FileServer(http.Dir("./static")))

    // Health check
    mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("OK"))
    })

    log.Println("WebRTC signaling server starting on :8080")
    log.Println("Open http://localhost:8080 in your browser")

    if err := http.ListenAndServe(":8080", mux); err != nil {
        log.Fatal(err)
    }
}

4. Browser Client

static/index.html

<!DOCTYPE html>
<html>
<head>
    <title>WebRTC Demo</title>
    <style>
        body { font-family: Arial, sans-serif; padding: 20px; }
        video { width: 400px; height: 300px; background: #000; }
        .container { display: flex; gap: 20px; }
        .controls { margin: 20px 0; }
        button { padding: 10px 20px; margin: 5px; }
        #messages { border: 1px solid #ccc; padding: 10px; height: 200px; overflow-y: auto; }
    </style>
</head>
<body>
    <h1>WebRTC Peer-to-Peer Demo</h1>

    <div class="controls">
        <input type="text" id="roomId" placeholder="Room ID" value="room1">
        <input type="text" id="userId" placeholder="Your User ID" value="user1">
        <button onclick="joinRoom()">Join Room</button>
        <button onclick="startVideo()">Start Video</button>
        <button onclick="stopVideo()">Stop Video</button>
    </div>

    <div class="container">
        <div>
            <h3>Local Video</h3>
            <video id="localVideo" autoplay muted></video>
        </div>
        <div>
            <h3>Remote Video</h3>
            <video id="remoteVideo" autoplay></video>
        </div>
    </div>

    <h3>Data Channel Messages</h3>
    <div class="controls">
        <input type="text" id="messageInput" placeholder="Type a message">
        <button onclick="sendMessage()">Send</button>
    </div>
    <div id="messages"></div>

    <script>
        let ws;
        let peerConnection;
        let localStream;
        let dataChannel;
        let roomId;
        let userId;
        const peers = {};

        const config = {
            iceServers: [
                { urls: 'stun:stun.l.google.com:19302' }
            ]
        };

        function joinRoom() {
            roomId = document.getElementById('roomId').value;
            userId = document.getElementById('userId').value;

            ws = new WebSocket('ws://localhost:8080/ws');

            ws.onopen = () => {
                console.log('Connected to signaling server');
                ws.send(JSON.stringify({
                    type: 'join',
                    data: { room: roomId, userId: userId }
                }));
            };

            ws.onmessage = async (event) => {
                const msg = JSON.parse(event.data);
                await handleSignalingMessage(msg);
            };

            ws.onerror = (error) => {
                console.error('WebSocket error:', error);
            };

            ws.onclose = () => {
                console.log('Disconnected from signaling server');
            };
        }

        async function handleSignalingMessage(msg) {
            console.log('Received:', msg);

            switch (msg.type) {
                case 'peers':
                    // Create offers to all existing peers
                    const peers = JSON.parse(msg.data).peers;
                    for (const peerId of peers) {
                        await createPeerConnection(peerId, true);
                    }
                    break;

                case 'join':
                    // New peer joined - wait for their offer
                    console.log('Peer joined:', msg.from);
                    break;

                case 'offer':
                    await handleOffer(msg);
                    break;

                case 'answer':
                    await handleAnswer(msg);
                    break;

                case 'candidate':
                    await handleCandidate(msg);
                    break;

                case 'leave':
                    handleLeave(msg);
                    break;
            }
        }

        async function createPeerConnection(peerId, createOffer) {
            const pc = new RTCPeerConnection(config);
            peers[peerId] = pc;

            // Add local tracks
            if (localStream) {
                localStream.getTracks().forEach(track => {
                    pc.addTrack(track, localStream);
                });
            }

            // Create data channel
            if (createOffer) {
                dataChannel = pc.createDataChannel('chat');
                setupDataChannel();
            } else {
                pc.ondatachannel = (event) => {
                    dataChannel = event.channel;
                    setupDataChannel();
                };
            }

            // Handle ICE candidates
            pc.onicecandidate = (event) => {
                if (event.candidate) {
                    ws.send(JSON.stringify({
                        type: 'candidate',
                        to: peerId,
                        data: JSON.stringify(event.candidate)
                    }));
                }
            };

            // Handle remote tracks
            pc.ontrack = (event) => {
                console.log('Received remote track');
                const remoteVideo = document.getElementById('remoteVideo');
                remoteVideo.srcObject = event.streams[0];
            };

            pc.onconnectionstatechange = () => {
                console.log('Connection state:', pc.connectionState);
            };

            if (createOffer) {
                const offer = await pc.createOffer();
                await pc.setLocalDescription(offer);

                ws.send(JSON.stringify({
                    type: 'offer',
                    to: peerId,
                    data: JSON.stringify(offer)
                }));
            }

            return pc;
        }

        async function handleOffer(msg) {
            const pc = await createPeerConnection(msg.from, false);
            const offer = JSON.parse(msg.data);

            await pc.setRemoteDescription(new RTCSessionDescription(offer));
            const answer = await pc.createAnswer();
            await pc.setLocalDescription(answer);

            ws.send(JSON.stringify({
                type: 'answer',
                to: msg.from,
                data: JSON.stringify(answer)
            }));
        }

        async function handleAnswer(msg) {
            const pc = peers[msg.from];
            if (pc) {
                const answer = JSON.parse(msg.data);
                await pc.setRemoteDescription(new RTCSessionDescription(answer));
            }
        }

        async function handleCandidate(msg) {
            const pc = peers[msg.from];
            if (pc) {
                const candidate = JSON.parse(msg.data);
                await pc.addIceCandidate(new RTCIceCandidate(candidate));
            }
        }

        function handleLeave(msg) {
            const pc = peers[msg.from];
            if (pc) {
                pc.close();
                delete peers[msg.from];
            }
        }

        function setupDataChannel() {
            dataChannel.onopen = () => {
                console.log('Data channel opened');
            };

            dataChannel.onmessage = (event) => {
                const messages = document.getElementById('messages');
                messages.innerHTML += `<div><strong>Peer:</strong> ${event.data}</div>`;
                messages.scrollTop = messages.scrollHeight;
            };

            dataChannel.onclose = () => {
                console.log('Data channel closed');
            };
        }

        async function startVideo() {
            try {
                localStream = await navigator.mediaDevices.getUserMedia({
                    video: true,
                    audio: true
                });

                document.getElementById('localVideo').srcObject = localStream;

                // Add tracks to existing peer connections
                for (const pc of Object.values(peers)) {
                    localStream.getTracks().forEach(track => {
                        pc.addTrack(track, localStream);
                    });
                }

                console.log('Local video started');
            } catch (error) {
                console.error('Error accessing media devices:', error);
            }
        }

        function stopVideo() {
            if (localStream) {
                localStream.getTracks().forEach(track => track.stop());
                document.getElementById('localVideo').srcObject = null;
                localStream = null;
            }
        }

        function sendMessage() {
            const input = document.getElementById('messageInput');
            const message = input.value;

            if (dataChannel && dataChannel.readyState === 'open') {
                dataChannel.send(message);

                const messages = document.getElementById('messages');
                messages.innerHTML += `<div><strong>You:</strong> ${message}</div>`;
                messages.scrollTop = messages.scrollHeight;

                input.value = '';
            } else {
                alert('Data channel is not open');
            }
        }
    </script>
</body>
</html>

Selective Forwarding Unit (SFU) for Scaling

For multi-party video conferencing, use an SFU to forward streams efficiently:

webrtc/sfu.go

package webrtc

import (
    "io"
    "log"
    "sync"

    "github.com/pion/webrtc/v3"
)

// SFU manages media routing between multiple peers
type SFU struct {
    peers map[string]*Peer
    mu    sync.RWMutex
}

// NewSFU creates a new SFU
func NewSFU() *SFU {
    return &SFU{
        peers: make(map[string]*Peer),
    }
}

// AddPeer adds a peer to the SFU
func (s *SFU) AddPeer(peer *Peer) {
    s.mu.Lock()
    defer s.mu.Unlock()

    s.peers[peer.ID] = peer

    // Handle incoming tracks
    peer.OnTrack = func(track *webrtc.TrackRemote) {
        s.forwardTrack(peer.ID, track)
    }

    log.Printf("SFU: Added peer %s (total: %d)", peer.ID, len(s.peers))
}

// RemovePeer removes a peer from the SFU
func (s *SFU) RemovePeer(peerID string) {
    s.mu.Lock()
    defer s.mu.Unlock()

    if peer, exists := s.peers[peerID]; exists {
        peer.Close()
        delete(s.peers, peerID)
        log.Printf("SFU: Removed peer %s (remaining: %d)", peerID, len(s.peers))
    }
}

// forwardTrack forwards a track to all other peers
func (s *SFU) forwardTrack(senderID string, track *webrtc.TrackRemote) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    // Create a local track to forward
    localTrack, err := webrtc.NewTrackLocalStaticRTP(
        track.Codec().RTPCodecCapability,
        track.ID(),
        track.StreamID(),
    )
    if err != nil {
        log.Printf("Error creating local track: %v", err)
        return
    }

    // Add track to all other peers
    for id, peer := range s.peers {
        if id != senderID {
            _, err := peer.AddTrack(localTrack)
            if err != nil {
                log.Printf("Error adding track to peer %s: %v", id, err)
            }
        }
    }

    // Forward packets
    go func() {
        buffer := make([]byte, 1500)
        for {
            n, _, err := track.Read(buffer)
            if err == io.EOF {
                return
            }
            if err != nil {
                log.Printf("Error reading track: %v", err)
                return
            }

            if _, err := localTrack.Write(buffer[:n]); err != nil {
                log.Printf("Error writing track: %v", err)
                return
            }
        }
    }()

    log.Printf("SFU: Forwarding track %s from peer %s to %d peers", track.ID(), senderID, len(s.peers)-1)
}

STUN/TURN Server Setup

For production, you need TURN servers for NAT traversal:

coturn.conf

listening-port=3478
external-ip=YOUR_PUBLIC_IP
fingerprint
lt-cred-mech
user=username:password
realm=your.domain.com

Start coturn:

docker run -d --network=host \
  coturn/coturn \
  -n \
  --log-file=stdout \
  --external-ip=YOUR_PUBLIC_IP \
  --listening-port=3478 \
  --fingerprint \
  --lt-cred-mech \
  --user=username:password \
  --realm=your.domain.com

Best Practices

1. Always Implement Signaling Timeouts

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// Wait for ICE gathering with timeout
select {
case <-gatherComplete:
    // Proceed
case <-ctx.Done():
    return errors.New("ICE gathering timeout")
}

2. Handle Connection Failures

pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
    if state == webrtc.PeerConnectionStateFailed {
        // Attempt ICE restart
        pc.RestartICE()
    }
})

3. Clean Up Resources

defer func() {
    if localStream != nil {
        localStream.getTracks().forEach(track => track.stop())
    }
    if peerConnection != nil {
        peerConnection.close()
    }
}()

4. Monitor Media Quality

ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()

for range ticker.C {
    stats := pc.GetStats()
    // Monitor packet loss, jitter, bitrate
    log.Printf("Stats: %+v", stats)
}

Common Pitfalls

1. Not Handling ICE Restart

// Handle ICE failures with restart
pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
    if state == webrtc.ICEConnectionStateFailed {
        pc.RestartICE()
    }
})

2. Missing TURN Servers

// Always include TURN for production
ICEServers: []webrtc.ICEServer{
    {URLs: []string{"stun:stun.l.google.com:19302"}},
    {
        URLs:       []string{"turn:turn.example.com:3478"},
        Username:   "user",
        Credential: "pass",
    },
}

3. Not Implementing Bandwidth Management

// Set bandwidth limits
pc.SetConfiguration(webrtc.Configuration{
    SDPSemantics: webrtc.SDPSemanticsUnifiedPlan,
})

When to Use WebRTC

✅ Use WebRTC When:

  • Low latency critical (< 500ms)
  • Peer-to-peer preferred - Reduce server bandwidth
  • Real-time audio/video required
  • Interactive applications - Gaming, collaboration
  • Direct file transfer needed
  • Screen sharing required

❌ Avoid WebRTC When:

  • One-to-many broadcasting - Use HLS/DASH instead
  • Recorded playback - Use traditional streaming
  • Simple data exchange - WebSockets simpler
  • No NAT traversal support - Firewall restrictions
  • Wide device support needed - Limited on older devices

Advantages

  1. Low Latency - Sub-second delay
  2. Peer-to-Peer - Reduces server bandwidth
  3. Built-in Security - DTLS/SRTP encryption
  4. NAT Traversal - STUN/TURN support
  5. Browser Native - No plugins required
  6. Multiple Streams - Audio, video, and data

Disadvantages

  1. Complex Setup - Signaling, STUN/TURN required
  2. Bandwidth Intensive - Each peer connection uses bandwidth
  3. Scaling Challenges - Mesh doesn’t scale beyond ~4-6 peers
  4. NAT/Firewall Issues - TURN servers needed
  5. Browser Compatibility - Not all features universally supported
  6. Debugging Difficulty - Complex protocol stack

Architecture Patterns

graph TB subgraph "Mesh (2-4 peers)" P1[Peer 1] <--> P2[Peer 2] P1 <--> P3[Peer 3] P2 <--> P3 end subgraph "SFU (4+ peers)" P4[Peer A] <--> SFU[SFU Server] P5[Peer B] <--> SFU P6[Peer C] <--> SFU P7[Peer D] <--> SFU end subgraph "MCU (high quality)" P8[Peer 1] --> MCU[MCU Server] P9[Peer 2] --> MCU P10[Peer 3] --> MCU MCU --> P8 MCU --> P9 MCU --> P10 end style P1 fill:#e1f5ff style SFU fill:#ffe1e1 style MCU fill:#fff4e1

Backend Communication
Current: WebRTC