From 682e6cbae9828e348b9f98ce5de1c99d70541ac7 Mon Sep 17 00:00:00 2001 From: jevb Date: Fri, 20 Mar 2026 06:27:43 +0100 Subject: [PATCH] fix: resolve LiveKit connection issues - Add http://localhost:* and ws://localhost:* to CSP connect-src (WebView2 was blocking LiveKit signal connection) - Add connection retry (3 attempts, 2s delay) for LiveKit server startup race condition ("could not find any available nodes") - Remove broken TURN TLS config from generated livekit.yaml - Send direct LiveKit URL instead of proxy path (localhost is treated as secure context in Chromium/WebView2) - Set LiveKit server host from API config for URL resolution --- Client/tauri-client/src-tauri/tauri.conf.json | 2 +- Client/tauri-client/src/lib/livekitSession.ts | 33 +++++++++++++++++-- Server/ws/livekit_process.go | 22 +++---------- Server/ws/voice_handlers.go | 5 +-- 4 files changed, 37 insertions(+), 25 deletions(-) diff --git a/Client/tauri-client/src-tauri/tauri.conf.json b/Client/tauri-client/src-tauri/tauri.conf.json index f75e6c8f..269ba2cf 100644 --- a/Client/tauri-client/src-tauri/tauri.conf.json +++ b/Client/tauri-client/src-tauri/tauri.conf.json @@ -22,7 +22,7 @@ } ], "security": { - "csp": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' https: wss:; img-src 'self' https: data:; frame-src https://www.youtube.com https://youtube.com" + "csp": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' https: wss: http://localhost:* ws://localhost:* http://127.0.0.1:* ws://127.0.0.1:*; img-src 'self' https: data:; frame-src https://www.youtube.com https://youtube.com" } }, "bundle": { diff --git a/Client/tauri-client/src/lib/livekitSession.ts b/Client/tauri-client/src/lib/livekitSession.ts index 099fe5a5..069d762f 100644 --- a/Client/tauri-client/src/lib/livekitSession.ts +++ b/Client/tauri-client/src/lib/livekitSession.ts @@ -320,9 +320,38 @@ export async function handleVoiceToken( room.on(RoomEvent.ActiveSpeakersChanged, handleActiveSpeakersChanged); room.on(RoomEvent.Disconnected, handleDisconnected); - // Connect to LiveKit server (resolve proxy URL if relative path) + // Connect to LiveKit server with retry (LiveKit may still be initializing) const resolvedUrl = resolveLiveKitUrl(url); - await room.connect(resolvedUrl, token); + const MAX_RETRIES = 3; + const RETRY_DELAY_MS = 2000; + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + await room.connect(resolvedUrl, token); + break; + } catch (connectErr) { + if (attempt < MAX_RETRIES) { + log.warn("LiveKit connect failed, retrying", { attempt, maxRetries: MAX_RETRIES, error: connectErr }); + await new Promise((r) => setTimeout(r, RETRY_DELAY_MS)); + // Recreate room for fresh connection state + room.removeAllListeners(); + room = new Room({ + adaptiveStream: true, + dynacast: true, + audioCaptureDefaults: { + echoCancellation: loadPref("echoCancellation", true), + noiseSuppression: loadPref("noiseSuppression", true), + autoGainControl: loadPref("autoGainControl", true), + }, + }); + room.on(RoomEvent.TrackSubscribed, handleTrackSubscribed); + room.on(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed); + room.on(RoomEvent.ActiveSpeakersChanged, handleActiveSpeakersChanged); + room.on(RoomEvent.Disconnected, handleDisconnected); + } else { + throw connectErr; + } + } + } log.info("Connected to LiveKit room", { channelId, url: resolvedUrl }); // Enable microphone: use RNNoise if Enhanced Noise Suppression is on diff --git a/Server/ws/livekit_process.go b/Server/ws/livekit_process.go index 629cf0c9..74a49f67 100644 --- a/Server/ws/livekit_process.go +++ b/Server/ws/livekit_process.go @@ -46,22 +46,8 @@ func NewLiveKitProcess(cfg *config.VoiceConfig, tlsCfg *config.TLSConfig, dataDi func (p *LiveKitProcess) generateConfig() (string, error) { cfgPath := filepath.Join(p.dataDir, "livekit.yaml") - // Build TLS section if OwnCord has TLS configured with cert files. - tlsSection := "" - if p.tlsCfg != nil && p.tlsCfg.CertFile != "" && p.tlsCfg.KeyFile != "" { - // Resolve cert/key paths relative to the data directory's parent - // (same working directory as chatserver). - certFile := p.tlsCfg.CertFile - keyFile := p.tlsCfg.KeyFile - tlsSection = fmt.Sprintf(` -turn: - enabled: true - tls_port: 5349 - udp_port: 3478 - cert_file: %s - key_file: %s -`, certFile, keyFile) - } + // No TURN TLS config — LiveKit signaling is proxied through OwnCord's + // HTTPS server at /livekit/*, so no separate TLS is needed on LiveKit. content := fmt.Sprintf(`# Auto-generated by OwnCord — do not edit manually. port: 7880 @@ -70,10 +56,10 @@ rtc: port_range_end: 60000 use_external_ip: true keys: - %s: %s%s + %s: %s logging: level: info -`, p.cfg.LiveKitAPIKey, p.cfg.LiveKitAPISecret, tlsSection) +`, p.cfg.LiveKitAPIKey, p.cfg.LiveKitAPISecret) if err := os.MkdirAll(p.dataDir, 0o755); err != nil { return "", fmt.Errorf("creating data dir: %w", err) diff --git a/Server/ws/voice_handlers.go b/Server/ws/voice_handlers.go index c6a9a522..98410d8f 100644 --- a/Server/ws/voice_handlers.go +++ b/Server/ws/voice_handlers.go @@ -103,10 +103,7 @@ func (h *Hub) handleVoiceJoin(c *Client, payload json.RawMessage) { slog.Error("ws handleVoiceJoin GenerateToken", "err", tokenErr, "user_id", c.userID) // Non-fatal: voice join still succeeds at the DB/state level. } else { - // Send "/livekit" as URL — client constructs the full wss:// URL - // from its server connection. Proxied through OwnCord's HTTPS - // to avoid mixed-content blocks in WebView2. - c.sendMsg(buildVoiceToken(channelID, token, "/livekit")) + c.sendMsg(buildVoiceToken(channelID, token, h.livekit.URL())) } }