Skip to content

harshithpabbati/realtime-edge

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Realtime Edge (Go + WebRTC SFU)

A minimal real-time video conferencing system built with Go and WebRTC using an SFU (Selective Forwarding Unit) architecture.


Features

  • WebRTC video + audio streaming
  • SFU (Selective Forwarding Unit) implementation
  • Multi-peer support (any number of participants)
  • Real-time signaling over WebSockets
  • Dynamic peer joins/leaves with automatic renegotiation
  • Multi-room support
  • Active speaker detection

Architecture

SFU

Clients send media to the server, which forwards it to all other participants.

A → Server → B
           → C
           → D

Each client maintains a single WebRTC connection to the server, regardless of participant count.


Tech Stack

  • Go
  • WebSockets (gorilla/websocket)
  • WebRTC (pion/webrtc)
  • HTML + Vanilla JS client

Project Structure

internal/
  client/     # WebSocket client abstraction
  sfu/        # SFU logic (tracks, fanout, peers)
  ws/         # WebSocket handler + signaling
cmd/
  server/     # Entry point
example/
  index.html  # Test client
  realtime-edge.js  # JavaScript client SDK

JavaScript Client SDK

The JavaScript client (example/realtime-edge.js) provides a high-level API for WebRTC video conferencing.

Constructor

const client = new RealtimeEdgeClient({ wsUrl, roomId });

Parameters:

Parameter Type Required Description
wsUrl string Yes WebSocket server URL (e.g., ws://localhost:3000/ws)
roomId string Yes Room identifier for the conference

Methods

join()

Initiates connection to the signaling server and joins the specified room. Automatically captures local media (video + audio).

await client.join();

leave()

Disconnects from the room and closes all WebRTC connections.

client.leave();

toggleMic()

Toggles the local microphone on/off.

const enabled = client.toggleMic();

Returns: boolean - true if microphone is now enabled, false if disabled.

toggleCamera()

Toggles the local camera on/off.

const enabled = client.toggleCamera();

Returns: boolean - true if camera is now enabled, false if disabled.

getLocalStream()

Returns the local media stream.

const stream = client.getLocalStream();

Returns: MediaStream | null - The local video/audio stream.

on(event, handler)

Registers an event listener.

client.on("participantJoined", ({ streamId, stream, displayName }) => {
  // Handle new participant
});

off(event, handler)

Removes an event listener.

client.off("participantJoined", myHandler);

Events

localStream

Fired when the local media stream is captured.

client.on("localStream", ({ stream }) => {
  localVideo.srcObject = stream;
});

Payload:

Property Type Description
stream MediaStream The local media stream

participantJoined

Fired when a new remote participant joins the room.

client.on("participantJoined", ({ streamId, stream, displayName }) => {
  addRemoteVideo(streamId, stream, displayName);
});

Payload:

Property Type Description
streamId string Unique identifier for the participant's stream
stream MediaStream The participant's media stream
displayName string Human-readable name for display

trackAdded

Fired when a new media track is received from a remote participant.

client.on("trackAdded", ({ streamId, track, stream, displayName }) => {
  // Handle incoming track
});

Payload:

Property Type Description
streamId string Unique identifier for the stream
track MediaStreamTrack The incoming media track
stream MediaStream The media stream containing the track
displayName string Human-readable name for display

trackRemoved

Fired when a media track is removed from a remote participant.

client.on("trackRemoved", ({ streamId, track }) => {
  // Handle track removal
});

Payload:

Property Type Description
streamId string Stream identifier
track MediaStreamTrack The removed media track

participantLeft

Fired when a remote participant leaves the room.

client.on("participantLeft", ({ streamId }) => {
  removeRemoteVideo(streamId);
});

Payload:

Property Type Description
streamId string ID of the departing participant

signalingStateChanged

Fired when the WebSocket signaling connection state changes.

client.on("signalingStateChanged", ({ state }) => {
  console.log("Signaling state:", state);
});

Payload:

Property Type Description
state string Connection state: "connected", "disconnected", "error", "left"

connectionStateChanged

Fired when the WebRTC PeerConnection state changes.

client.on("connectionStateChanged", ({ peerId, state }) => {
  console.log(`Peer ${peerId}: ${state}`);
});

Payload:

Property Type Description
peerId string Peer identifier (always "server" for SFU mode)
state string Connection state: "connecting", "connected", "disconnected", "failed", "closed"

micStateChanged

Fired when the local microphone state changes (via toggleMic()).

client.on("micStateChanged", ({ enabled }) => {
  micBtn.innerText = enabled ? "🎤 Mic On" : "🎤 Mic Off";
});

Payload:

Property Type Description
enabled boolean true if mic is on, false if off

cameraStateChanged

Fired when the local camera state changes (via toggleCamera()).

client.on("cameraStateChanged", ({ enabled }) => {
  camBtn.innerText = enabled ? "📷 Camera On" : "📷 Camera Off";
});

Payload:

Property Type Description
enabled boolean true if camera is on, false if off

activeSpeakerChanged

Fired when the active speaker changes (or clears during silence).

client.on(
  "activeSpeakerChanged",
  ({ streamId, previousStreamId, displayName, level, isLocal }) => {
    // streamId is "local", a remote streamId, or null
  },
);

Payload:

Property Type Description
streamId string | null Active speaker stream ID ("local" for self, or null when no active speaker)
previousStreamId string | null Previously active speaker stream ID
displayName string | null Display label for the active speaker
level number Current normalized audio level
isLocal boolean true when the active speaker is the local participant

WebSocket Protocol

The server uses JSON messages over WebSocket for signaling.

Client → Server Messages

Join Room

Join a room and initiate WebRTC connection.

{
  "type": "join",
  "roomId": "room-1",
  "data": "<SDP offer as JSON string>"
}

Leave Room

Leave the current room.

{
  "type": "leave"
}

ICE Candidate

Send an ICE candidate to the server.

{
  "type": "ice",
  "data": "<ICE candidate as JSON string>"
}

Answer

Send a WebRTC answer (for re-negotiation).

{
  "type": "answer",
  "data": "<SDP answer as JSON string>"
}

Server → Client Messages

Answer Event

WebRTC answer from server.

{
  "type": "event",
  "event": "answer",
  "data": "<SDP answer as JSON string>"
}

Ack Response

Acknowledgment response.

{
  "type": "ack",
  "message": "left room"
}

Error Response

Error message.

{
  "type": "error",
  "message": "error description"
}

Server API (Go)

Server

Main HTTP server that handles WebSocket connections and serves static files.

File: internal/server/server.go

New() *Server

Creates a new server instance.

Start() error

Starts the HTTP server on port 3000.

Routes:

Path Handler Description
/ Static file server Serves example/ directory
/health Health check Returns "ok"
/ws WebSocket handler WebRTC signaling

ws.Handler

WebSocket handler for WebRTC signaling.

File: internal/ws/handler.go

NewHandler() *Handler

Creates a new WebSocket handler with an SFU manager.

Handle(w http.ResponseWriter, r *http.Request)

HTTP handler that upgrades the connection to WebSocket.


ws.Message

WebSocket message structure.

File: internal/ws/message.go

Message Types

Type Value Description
Join "join" Join a room
Leave "leave" Leave a room
Offer "offer" WebRTC offer (reserved for future)
Answer "answer" WebRTC answer
ICE "ice" ICE candidate

Response Types

Type Value Description
Ack "ack" Acknowledgment
Error "error" Error response
Event "event" Event notification

sfu.Manager

Room manager that handles multiple rooms.

File: internal/sfu/manager.go

NewManager() *Manager

Creates a new SFU manager.

GetRoom(id string) *Room

Gets or creates a room by ID.

Parameters:

Parameter Type Description
id string Room identifier

Returns: *Room - The room instance.


sfu.Room

Represents a conference room with participants and media tracks.

File: internal/sfu/room.go

AddPeer(peer *Peer)

Adds a peer to the room and subscribes them to existing tracks.

RemovePeer(peerID string)

Removes a peer from the room and cleans up their tracks.

GetPeer(peerID string) *Peer

Gets a peer by ID.

PeerCount() int

Returns the number of peers in the room.

HasTracksFromOthers(peerID string) bool

Checks if there are tracks from other peers (used for negotiation).


sfu.Peer

Represents a participant's WebRTC connection.

File: internal/sfu/room.go

Close()

Closes the peer connection.

Negotiate()

Initiates or responds to WebRTC negotiation.

CheckPendingNegotiation()

Triggers deferred negotiation if needed.


sfu.TrackFanout

Manages media track distribution to subscribers.

File: internal/sfu/room.go

AddSubscriber(track *pion.TrackLocalStaticRTP)

Adds a subscriber to receive this track.

RequestKeyframeBurst()

Sends keyframe requests to reduce startup delay for new subscribers.

Write(buf []byte)

Forwards RTP packets to all active subscribers.


client.Client

WebSocket client wrapper.

File: internal/client/client.go

Fields

Field Type Description
ID string Unique client ID
Conn *websocket.Conn WebSocket connection
Send chan []byte Send channel
RoomID string Current room ID
Peer *pion.PeerConnection WebRTC connection

CloseSend()

Closes the send channel.

TrySend(msg []byte) bool

Non-blocking send attempt.


client.ReadPump()

Reads messages from WebSocket and calls callbacks.

c.ReadPump(func(msg []byte) {
    // handle message
}, func() {
    // handle close
})

client.WritePump()

Writes messages to WebSocket from the send channel.


Running Locally

1. Start server

go run ./cmd/server

2. Open client

Open in browser:

http://localhost:3000

Open multiple tabs to simulate multiple users.


Example Usage

// Initialize client
const client = new RealtimeEdgeClient({
  wsUrl: "ws://localhost:3000/ws",
  roomId: "my-room",
});

// Register event handlers
client.on("localStream", ({ stream }) => {
  document.getElementById("localVideo").srcObject = stream;
});

client.on("participantJoined", ({ streamId, stream, displayName }) => {
  console.log(`${displayName} joined with stream ${streamId}`);
  // Add video element for new participant
});

client.on("participantLeft", ({ streamId }) => {
  console.log(`Stream ${streamId} left`);
  // Remove video element
});

client.on("connectionStateChanged", ({ state }) => {
  console.log("Connection state:", state);
});

// Join the room
client.join();

// Toggle media controls
document.getElementById("micBtn").onclick = () => client.toggleMic();
document.getElementById("camBtn").onclick = () => client.toggleCamera();
document.getElementById("leaveBtn").onclick = () => client.leave();

Current Limitations

  • No authentication
  • No TURN server (may fail behind strict NAT)
  • No simulcast / bitrate control
  • Basic UI
  • Single-node (no horizontal scaling)

Next Steps

  • Better UI (video grid, controls)
  • Screen sharing
  • TURN/STUN configuration

About

Realtime Edge (Go + WebRTC SFU)

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages