From 2250cb0a8f01bc9e704de150e316d01e80a08dd3 Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Fri, 17 Apr 2026 08:44:30 -0400 Subject: [PATCH] feat(webrtc): add Config with defaults and validation --- core/webrtc/config.go | 59 ++++++++++++++++++++++++++++++++++++++ core/webrtc/config_test.go | 48 +++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 core/webrtc/config.go create mode 100644 core/webrtc/config_test.go diff --git a/core/webrtc/config.go b/core/webrtc/config.go new file mode 100644 index 0000000..521e009 --- /dev/null +++ b/core/webrtc/config.go @@ -0,0 +1,59 @@ +package webrtc + +import "fmt" + +// PortRange represents an inclusive UDP port range. +type PortRange struct { + Low, High int +} + +// Config controls the WebRTC egress module. +type Config struct { + // Enabled toggles the entire module. When false, no endpoints are served. + Enabled bool + + // WHEPListen is the address the WHEP HTTP endpoint binds to (e.g. ":8787"). + WHEPListen string + + // PublicIP is the server's externally-reachable IP, advertised in ICE + // candidates via NAT1To1. Empty means rely on STUN discovery. + PublicIP string + + // UDPPortRange bounds the local UDP ports allocated for FFmpeg→Pion RTP. + UDPPortRange PortRange + + // ICEServers is the list of STUN/TURN URIs given to each PeerConnection. + ICEServers []string + + // MaxPeersTotal is a hard safety cap on concurrent subscribers. + MaxPeersTotal int +} + +// DefaultConfig returns production-reasonable defaults. +func DefaultConfig() Config { + return Config{ + Enabled: true, + WHEPListen: ":8787", + PublicIP: "", + UDPPortRange: PortRange{Low: 10000, High: 10100}, + ICEServers: []string{"stun:stun.cloudflare.com:3478", "stun:stun.l.google.com:19302"}, + MaxPeersTotal: 32, + } +} + +// Validate returns an error if the config is internally inconsistent. +func (c Config) Validate() error { + if c.WHEPListen == "" { + return fmt.Errorf("webrtc: WHEPListen must not be empty") + } + if c.UDPPortRange.Low <= 0 || c.UDPPortRange.High <= 0 { + return fmt.Errorf("webrtc: UDPPortRange must have positive bounds, got %v", c.UDPPortRange) + } + if c.UDPPortRange.Low > c.UDPPortRange.High { + return fmt.Errorf("webrtc: UDPPortRange.Low > High (%d > %d)", c.UDPPortRange.Low, c.UDPPortRange.High) + } + if c.MaxPeersTotal <= 0 { + return fmt.Errorf("webrtc: MaxPeersTotal must be positive, got %d", c.MaxPeersTotal) + } + return nil +} diff --git a/core/webrtc/config_test.go b/core/webrtc/config_test.go new file mode 100644 index 0000000..95bb5f7 --- /dev/null +++ b/core/webrtc/config_test.go @@ -0,0 +1,48 @@ +package webrtc + +import ( + "testing" +) + +func TestConfig_Defaults(t *testing.T) { + c := DefaultConfig() + if !c.Enabled { + t.Error("default Enabled should be true") + } + if c.WHEPListen != ":8787" { + t.Errorf("default WHEPListen = %q, want :8787", c.WHEPListen) + } + if c.UDPPortRange.Low != 10000 || c.UDPPortRange.High != 10100 { + t.Errorf("default UDPPortRange = %v, want 10000-10100", c.UDPPortRange) + } + if c.MaxPeersTotal != 32 { + t.Errorf("default MaxPeersTotal = %d, want 32", c.MaxPeersTotal) + } + if len(c.ICEServers) == 0 { + t.Error("default ICEServers should have at least one STUN entry") + } +} + +func TestConfig_Validate(t *testing.T) { + tests := []struct { + name string + mutate func(*Config) + wantErr bool + }{ + {"defaults are valid", func(c *Config) {}, false}, + {"empty listen", func(c *Config) { c.WHEPListen = "" }, true}, + {"inverted port range", func(c *Config) { c.UDPPortRange.Low = 20000; c.UDPPortRange.High = 10000 }, true}, + {"zero max peers", func(c *Config) { c.MaxPeersTotal = 0 }, true}, + {"negative max peers", func(c *Config) { c.MaxPeersTotal = -1 }, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := DefaultConfig() + tt.mutate(&c) + err := c.Validate() + if (err != nil) != tt.wantErr { + t.Errorf("Validate() err = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}