Help us shape the future of Go! Take the 2026 Go Developer Survey

rtc

package
v0.93.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 21, 2026 License: MIT Imports: 27 Imported by: 0

Documentation

Overview

Package rtc is a WebRTC signaling server: rooms of peers over one core/stream.StateChannel WebSocket per room, addressed relay of offer/answer/ICE frames, ICE server configuration with per-peer time-limited TURN credentials, and cross-replica relay over core/fanout. Signaling only: SDP and ICE candidates cross the Go process as uninterpreted bytes, media never does. The browser half is the `rtc` runtime module (`__gofastr.connectRoom`).

Identity is server-derived. A client names nothing about itself on the wire; every Join field comes from the host's own authorization (cookies, sessions, handler.GetUser) through Config.Authorize. The TURN static-auth-secret never leaves the process: each peer's credential is minted at snapshot time and marshaled for that peer only.

Usage:

app.RegisterPlugin(rtc.New(rtc.Config{
    ICEServers: []rtc.ICEServer{{URLs: []string{"stun:stun.example.net:3478"}}},
    Authorize: func(r *http.Request) (rtc.Join, error) {
        // resolve the room and identity from the app's own auth
        return rtc.Join{Room: "support-17", Role: "caller"}, nil
    },
}))

Init mounts the endpoint (GET Config.Path, default /__gofastr/rtc) and wires Close into app shutdown; App.Mount wires SetFanout automatically when the app has WithFanout.

Serve skips Authorize when the caller has already authorized the request and passes its own Join.

Index

Constants

View Source
const DefaultPath = "/__gofastr/rtc"

DefaultPath is the WebSocket endpoint Mount registers when Config.Path is empty.

Variables

This section is empty.

Functions

func PermissionsPolicy

func PermissionsPolicy(features ...Feature) string

PermissionsPolicy returns a Permissions-Policy header value that closes geolocation, microphone, camera, and display-capture and opens the named features to the app's own origin ("(self)"). Every feature is written out: an omitted directive is not a closed one (the default allowlist for camera, microphone, and display-capture is self), so a header that only listed what it opened would open what it left out as well. Hand it to middleware.SecurityHeadersConfig.PermissionsPolicy. Example: PermissionsPolicy(Camera, Microphone) == "geolocation=(), microphone=(self), camera=(self), display-capture=()".

Types

type Config

type Config struct {
	// ICEServers is the static list every peer receives (STUN servers,
	// or TURN entries with long-lived credentials the host accepts).
	// Only stun:, stuns:, turn:, turns: URL schemes are allowed; New
	// refuses anything else.
	ICEServers []ICEServer

	// TURN, when set, appends a per-peer time-limited entry.
	TURN *TURN

	// Authorize decides who joins which room. Required for Mount and
	// ServeHTTP; Serve callers pass their own Join and may leave it nil.
	// A returned error refuses the upgrade with 403 (or the status
	// carried by a *HTTPError). The Join it returns is trusted.
	Authorize func(r *http.Request) (Join, error)

	// Path is the WebSocket endpoint Mount registers. Default
	// "/__gofastr/rtc". Validated like relay.Config.Path: absolute, no
	// trailing slash, no traversal, no percent-encoding.
	Path string

	// MaxPeers caps members per room, local plus remote; default 8. A
	// join past the cap is refused with 409 before the upgrade. Mesh
	// cost is O(n²) connections; an SFU is a different product.
	MaxPeers int

	// MaxSignalBytes caps one signaling payload (SDP, ICE candidate);
	// default 48 KiB. Larger frames are dropped and the peer closed.
	// A camera plus a data channel offers in 3-8 KiB; many codecs,
	// simulcast, and several tracks reach 15-25 KiB. The cap plus 1 KiB
	// of envelope must fit WS.ReadLimit (at most 64 KiB), or New panics
	// rather than accept a cap the socket would cut off first.
	MaxSignalBytes int

	// MaxStatusBytes caps a peer's status document; default 1 KiB.
	MaxStatusBytes int

	// MaxFramesPerSecond caps inbound frames per socket (token bucket,
	// burst = 2× rate); default 128. Over the cap the socket is closed.
	MaxFramesPerSecond int

	// MaxSocketsPerUser caps concurrent signaling sockets per principal
	// (Join.User) across every room; default 16, the core/stream seat
	// parity (SSE broker, CRUD event stream). A principal's next socket
	// past the cap displaces its oldest one — the replace-on-rejoin
	// policy, applied across rooms — so a reconnecting client always
	// wins and one principal can never park unbounded read loops and
	// per-room channel goroutines. Zero keeps the default; sockets whose
	// host derived no Join.User share one bucket.
	MaxSocketsPerUser int

	// RoomIdleTTL is how long an empty room's state survives before it
	// is dropped; default 1 minute. Rooms are created on first join.
	RoomIdleTTL time.Duration

	// WS is applied to every upgrade. ConnectionID and OnClose are
	// owned by the package and overwritten. Leave zero for defaults
	// (ReadLimit is forced to at most 64 KiB either way).
	WS stream.WSConfig

	Logger *slog.Logger // nil means slog.Default()
}

type Feature

type Feature string

Feature names a browser feature the Permissions-Policy helper opens.

const (
	Camera         Feature = "camera"
	Microphone     Feature = "microphone"
	DisplayCapture Feature = "display-capture"
)

type HTTPError

type HTTPError struct {
	Status  int
	Message string // plain, short; written as the response body
}

HTTPError lets Config.Authorize choose the refusal status (401, 404, 410 …) instead of the default 403. ServeHTTP writes Status and Message as the response; anything else about the error is discarded so a refusal leaks nothing beyond what the host chose to say.

func (*HTTPError) Error

func (e *HTTPError) Error() string

type ICEServer

type ICEServer struct {
	URLs       []string `json:"urls"`
	Username   string   `json:"username,omitempty"`
	Credential string   `json:"credential,omitempty"`
}

ICEServer is one entry of the RTCConfiguration.iceServers list handed to a peer. JSON tags match the browser's RTCIceServer dictionary.

type Join

type Join struct {
	Room        string // required; 1-128 bytes, printable ASCII, no whitespace
	Role        string // optional app label ("caller", "viewer"); 0-32 bytes, same charset
	PeerID      string // optional stable id; empty means the server mints one per socket
	User        string // optional identity shown to the room (a user id); empty = anonymous
	DisplayName string // optional; 0-64 bytes; control bytes refused
}

Join is the server-derived identity of one connecting peer. Every field comes from the host's own authorization (cookies, sessions, handler.GetUser), never from the wire: a client names nothing about itself except through the host's Authorize.

type PeerInfo

type PeerInfo struct {
	ID          string          `json:"id"`
	Role        string          `json:"role,omitempty"`
	User        string          `json:"user,omitempty"`
	DisplayName string          `json:"name,omitempty"`
	Order       int64           `json:"order"`
	Status      json.RawMessage `json:"status,omitempty"`
}

PeerInfo is a room member as every other member sees it. Order is the join instant in Unix nanoseconds and decides negotiation politeness (see the wire protocol's politeness rule); ties break on ID.

type Signaler

type Signaler struct {
	// contains filtered or unexported fields
}

Signaler is one signaling server: the room set, its sockets, and the optional cross-replica fanout lane. Construct with New, register with App.RegisterPlugin. Implements framework.Plugin.

func New

func New(cfg Config) *Signaler

New validates cfg and constructs the Signaler. It panics on invalid configuration with a message prefixed "rtc:": a bad rtc Config is a construction-time programmer error (same posture as relay.New and framework.NewApp's registration panics), not a runtime condition the process should limp along with. Call Close when done, or let Init wire it to app shutdown.

func (*Signaler) Close

func (s *Signaler) Close()

Close stops every room, closes every socket, stops the fanout subscription. Safe to call twice.

func (*Signaler) Init

func (s *Signaler) Init(app *framework.App) error

Init is the lifecycle path: Mount on the App (which registers the route and, when the app has WithFanout, wires SetFanout) and Close into app shutdown. A nil Config.Authorize is returned as an error, not a panic: plugin wiring is a runtime step the host can react to. Mount keeps its panic for the direct-mount spelling of the same rule.

func (*Signaler) Kick

func (s *Signaler) Kick(room, peerID string) bool

Kick closes the local socket of one peer. Reports whether a local peer with that id was found; a peer held by another replica is not reached. The leave event is published when the socket's read loop exits. Kick is a nudge, not a revocation: the browser module reconnects with backoff, so a peer that must stay out is refused by Authorize on that reconnect, and a TURN credential it already holds keeps working until TURN.TTL runs out.

func (*Signaler) Mount

func (s *Signaler) Mount(rt *router.Router)

Mount registers GET cfg.Path → ServeHTTP on the router. It panics if Config.Authorize is nil: a mounted signaling endpoint with no gate is a bug, not a default. The Signaler also satisfies the framework's Mountable seam, so App.Mount wires SetFanout automatically when the app has WithFanout.

func (*Signaler) Name

func (s *Signaler) Name() string

Name implements framework.Plugin.

func (*Signaler) Peers

func (s *Signaler) Peers(room string) []PeerInfo

Peers returns the merged local plus live-remote roster of a room, sorted by Order then ID. Empty for an unknown room.

func (*Signaler) ReservedEmbedPrefixes added in v0.87.0

func (s *Signaler) ReservedEmbedPrefixes() []string

ReservedEmbedPrefixes reports the path this Signaler actually mounted. Authorize reads the request's user, and an embed grant installs its subject as that user, so a grant living in a stranger's page would join rooms as the subject. No embed surface may reach it. See framework.EmbedReserving.

func (*Signaler) Rooms

func (s *Signaler) Rooms() []string

Rooms returns the names of rooms with at least one local member.

func (*Signaler) Serve

func (s *Signaler) Serve(w http.ResponseWriter, r *http.Request, join Join)

Serve upgrades r to a WebSocket, joins the peer to join.Room, and runs the read loop until the socket closes. The caller has already authorized the request; join is trusted. Blocks. A refused join (400 bad Join, 409 room full, 503 closed) is an HTTP status for a plain request and, for an upgrade request, an accepted handshake closed with code 4000+status (see refuse).

func (*Signaler) ServeHTTP

func (s *Signaler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP runs Authorize then Serve.

func (*Signaler) SetFanout

func (s *Signaler) SetFanout(f fanout.Fanout) (func(), error)

SetFanout wires cross-replica relay. Duck-typed to the framework's Mountable wiring (see App.Mount): with WithFanout attached, mounting the Signaler calls this automatically. Returns the cancel that stops the subscription and heartbeat. An error if a fanout is already attached, f is nil, or the Signaler is closed.

type TURN

type TURN struct {
	URLs   []string      // "turn:host:3478?transport=udp", "turns:host:5349"
	Secret string        // static-auth-secret configured on the TURN server
	TTL    time.Duration // credential lifetime; 0 means 1h; minimum 1m
}

TURN mints per-peer, time-limited TURN credentials in the coturn static-auth-secret convention (also called the TURN REST API): username is "<unix-expiry>:<peer id>", credential is base64(HMAC-SHA1(secret, username)). The secret is shared with the TURN server and never leaves the Go process; each peer receives only its own credential, inside its own snapshot.

func (TURN) Credentials

func (t TURN) Credentials(peerID string, now time.Time) ICEServer

Credentials returns the ICEServer entry for one peer, valid until now+TTL, in the coturn static-auth-secret convention (the "TURN REST API"): username is "<unix-expiry>:<peer id>", credential is base64(HMAC-SHA1(secret, username)). The secret is shared with the TURN server and never leaves the Go process; each peer receives only its own credential, inside its own snapshot.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL