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
This commit is contained in:
jevb
2026-03-20 06:27:43 +01:00
parent bf3fada16b
commit 682e6cbae9
4 changed files with 37 additions and 25 deletions
@@ -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": {
+31 -2
View File
@@ -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
+4 -18
View File
@@ -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)
+1 -4
View File
@@ -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()))
}
}