feat: add LiveKit infrastructure (Phase 0)

- Replace VoiceConfig STUN/TURN/MediaPort fields with LiveKit
  API key, secret, URL, and binary path
- Add livekit_process.go: companion process manager with
  auto-restart and config generation
- Add livekit.go: token generation, room service client,
  participant management, video track counting
- Add LiveKit server SDK dependency (server-sdk-go/v2)
- Update config tests for new VoiceConfig fields
This commit is contained in:
jevb
2026-03-20 05:14:52 +01:00
parent 7ebf2faecd
commit 80e880d536
6 changed files with 664 additions and 84 deletions
+22 -42
View File
@@ -29,18 +29,13 @@ type GitHubConfig struct {
Token string `koanf:"token"`
}
// VoiceConfig holds STUN/TURN server settings and SFU configuration.
// VoiceConfig holds LiveKit server connection and voice quality settings.
type VoiceConfig struct {
TURNSecret string `koanf:"turn_secret"` // HMAC-SHA1 secret; auto-generated if empty
STUNPort int `koanf:"stun_port"` // default 3478
TURNPort int `koanf:"turn_port"` // default 3478
TURNEnabled bool `koanf:"turn_enabled"` // default true
Quality string `koanf:"quality"` // low | medium | high
MixingThreshold int `koanf:"mixing_threshold"` // selective forwarding threshold
TopSpeakers int `koanf:"top_speakers"` // top-N speakers in selective mode
ExternalIP string `koanf:"external_ip"` // set if behind NAT
MediaPortMin int `koanf:"media_port_min"` // UDP port range start for WebRTC media
MediaPortMax int `koanf:"media_port_max"` // UDP port range end for WebRTC media
LiveKitAPIKey string `koanf:"livekit_api_key"` // LiveKit API key
LiveKitAPISecret string `koanf:"livekit_api_secret"` // LiveKit API secret
LiveKitURL string `koanf:"livekit_url"` // LiveKit server WebSocket URL (e.g. ws://localhost:7880)
LiveKitBinaryPath string `koanf:"livekit_binary"` // path to livekit-server binary; empty = don't auto-start
Quality string `koanf:"quality"` // low | medium | high
}
// ServerConfig holds HTTP server settings.
@@ -105,14 +100,10 @@ func defaults() Config {
StorageDir: "data/uploads",
},
Voice: VoiceConfig{
STUNPort: 3478,
TURNPort: 3478,
TURNEnabled: true,
Quality: "medium",
MixingThreshold: 10,
TopSpeakers: 3,
MediaPortMin: 10000,
MediaPortMax: 10100,
LiveKitAPIKey: "devkey",
LiveKitAPISecret: "secret",
LiveKitURL: "ws://localhost:7880",
Quality: "medium",
},
GitHub: GitHubConfig{},
}
@@ -148,13 +139,11 @@ upload:
storage_dir: "data/uploads"
voice:
# external_ip: "" # set to your public IP if behind NAT (required for voice over internet)
# stun_port: 3478 # UDP port for STUN
# turn_port: 3478 # UDP port for TURN relay
# turn_enabled: true
# quality: "medium" # low | medium | high
# media_port_min: 10000 # UDP port range for WebRTC media
# media_port_max: 10100
livekit_api_key: "devkey" # LiveKit API key
livekit_api_secret: "secret" # LiveKit API secret
livekit_url: "ws://localhost:7880" # LiveKit server WebSocket URL
# livekit_binary: "" # path to livekit-server binary; empty = don't auto-start
# quality: "medium" # low | medium | high
# github:
# token: "" # optional: GitHub API token for higher rate limits (5000 req/hr vs 60)
@@ -226,27 +215,18 @@ func Load(cfgPath string) (*Config, error) {
// overwrites struct defaults with Go zero values.
func applyVoiceDefaults(v *VoiceConfig) {
def := defaults().Voice
if v.STUNPort == 0 {
v.STUNPort = def.STUNPort
if v.LiveKitAPIKey == "" {
v.LiveKitAPIKey = def.LiveKitAPIKey
}
if v.TURNPort == 0 {
v.TURNPort = def.TURNPort
if v.LiveKitAPISecret == "" {
v.LiveKitAPISecret = def.LiveKitAPISecret
}
if v.LiveKitURL == "" {
v.LiveKitURL = def.LiveKitURL
}
if v.Quality == "" {
v.Quality = def.Quality
}
if v.MediaPortMin == 0 {
v.MediaPortMin = def.MediaPortMin
}
if v.MediaPortMax == 0 {
v.MediaPortMax = def.MediaPortMax
}
if v.MixingThreshold == 0 {
v.MixingThreshold = def.MixingThreshold
}
if v.TopSpeakers == 0 {
v.TopSpeakers = def.TopSpeakers
}
}
// validateYAML checks that raw bytes are valid YAML.
+12 -25
View File
@@ -241,14 +241,9 @@ func TestLoadVoiceConfigDefaults(t *testing.T) {
want any
}{
{"Voice.Quality", cfg.Voice.Quality, "medium"},
{"Voice.MixingThreshold", cfg.Voice.MixingThreshold, 10},
{"Voice.TopSpeakers", cfg.Voice.TopSpeakers, 3},
{"Voice.ExternalIP", cfg.Voice.ExternalIP, ""},
{"Voice.MediaPortMin", cfg.Voice.MediaPortMin, 10000},
{"Voice.MediaPortMax", cfg.Voice.MediaPortMax, 10100},
{"Voice.STUNPort", cfg.Voice.STUNPort, 3478},
{"Voice.TURNPort", cfg.Voice.TURNPort, 3478},
{"Voice.TURNEnabled", cfg.Voice.TURNEnabled, true},
{"Voice.LiveKitAPIKey", cfg.Voice.LiveKitAPIKey, "devkey"},
{"Voice.LiveKitAPISecret", cfg.Voice.LiveKitAPISecret, "secret"},
{"Voice.LiveKitURL", cfg.Voice.LiveKitURL, "ws://localhost:7880"},
}
for _, tc := range tests {
@@ -267,11 +262,9 @@ func TestLoadVoiceConfigFromYAML(t *testing.T) {
yaml := `
voice:
quality: high
mixing_threshold: 5
top_speakers: 4
external_ip: "1.2.3.4"
media_port_min: 20000
media_port_max: 20500
livekit_api_key: "mykey"
livekit_api_secret: "mysecret"
livekit_url: "ws://lk.example.com:7880"
`
if err := os.WriteFile(cfgPath, []byte(yaml), 0o644); err != nil {
t.Fatalf("failed to write yaml: %v", err)
@@ -285,20 +278,14 @@ voice:
if cfg.Voice.Quality != "high" {
t.Errorf("Voice.Quality = %q, want 'high'", cfg.Voice.Quality)
}
if cfg.Voice.MixingThreshold != 5 {
t.Errorf("Voice.MixingThreshold = %d, want 5", cfg.Voice.MixingThreshold)
if cfg.Voice.LiveKitAPIKey != "mykey" {
t.Errorf("Voice.LiveKitAPIKey = %q, want 'mykey'", cfg.Voice.LiveKitAPIKey)
}
if cfg.Voice.TopSpeakers != 4 {
t.Errorf("Voice.TopSpeakers = %d, want 4", cfg.Voice.TopSpeakers)
if cfg.Voice.LiveKitAPISecret != "mysecret" {
t.Errorf("Voice.LiveKitAPISecret = %q, want 'mysecret'", cfg.Voice.LiveKitAPISecret)
}
if cfg.Voice.ExternalIP != "1.2.3.4" {
t.Errorf("Voice.ExternalIP = %q, want '1.2.3.4'", cfg.Voice.ExternalIP)
}
if cfg.Voice.MediaPortMin != 20000 {
t.Errorf("Voice.MediaPortMin = %d, want 20000", cfg.Voice.MediaPortMin)
}
if cfg.Voice.MediaPortMax != 20500 {
t.Errorf("Voice.MediaPortMax = %d, want 20500", cfg.Voice.MediaPortMax)
if cfg.Voice.LiveKitURL != "ws://lk.example.com:7880" {
t.Errorf("Voice.LiveKitURL = %q, want 'ws://lk.example.com:7880'", cfg.Voice.LiveKitURL)
}
}