From 26decc2c3ca1bc89f9b5f912ed5987a4f6bbe9b5 Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Mon, 23 Feb 2026 05:55:25 +0100 Subject: [PATCH] feat: Dynamically fetch TURN credentials, improve WebRTC connection stability with ICE restart logic, and configure TURN via environment variables. --- client/renderer.js | 48 ++++++++++++++++++++++++++++++++++++++++------ docker-compose.yml | 2 ++ public/viewer.js | 48 +++++++++++++++++++++++++++++++++++++--------- server.js | 7 +++++++ 4 files changed, 90 insertions(+), 15 deletions(-) diff --git a/client/renderer.js b/client/renderer.js index 85feb28..2fc4008 100644 --- a/client/renderer.js +++ b/client/renderer.js @@ -25,12 +25,24 @@ let selectedVideoSourceId = null; // Chart.js instance tracking let bitrateChart = null; -const config = { - iceServers: [ - { urls: "stun:localhost:3478" }, - { urls: "turn:localhost:3478", username: "myuser", credential: "mypassword" } - ] -}; +// Build ICE config dynamically based on server URL +function getIceConfig(serverUrl, turnUser = 'myuser', turnPass = 'mypassword') { + let turnHost = 'localhost'; + try { + const url = new URL(serverUrl); + turnHost = url.hostname; + } catch (e) {} + + return { + iceServers: [ + { urls: `stun:${turnHost}:3478` }, + { urls: `turn:${turnHost}:3478`, username: turnUser, credential: turnPass } + ], + iceCandidatePoolSize: 5 + }; +} + +let config = getIceConfig('http://localhost:3000'); // 1. Get Desktop Sources / Switch Video Source Mid-Stream getSourcesBtn.addEventListener('click', async () => { @@ -315,6 +327,16 @@ startBtn.addEventListener('click', async () => { }); function connectAndBroadcast(url, password) { + // Fetch TURN credentials from the server, then update ICE config + fetch(new URL('/turn-config', url).href) + .then(r => r.json()) + .then(turn => { + config = getIceConfig(url, turn.username, turn.credential); + }) + .catch(() => { + config = getIceConfig(url); // fallback to defaults + }); + socket = io(url); socket.on('connect', () => { @@ -355,6 +377,20 @@ function connectAndBroadcast(url, password) { } }; + // Monitor ICE state for stability + peerConnection.oniceconnectionstatechange = () => { + console.log(`Viewer ${id} ICE state:`, peerConnection.iceConnectionState); + if (peerConnection.iceConnectionState === 'failed') { + peerConnection.restartIce(); + } else if (peerConnection.iceConnectionState === 'disconnected') { + setTimeout(() => { + if (peerConnections[id] && peerConnections[id].iceConnectionState === 'disconnected') { + peerConnections[id].restartIce(); + } + }, 3000); + } + }; + peerConnection.createOffer().then(sdp => { if (window.RTCRtpSender && window.RTCRtpSender.getCapabilities) { const caps = window.RTCRtpSender.getCapabilities('video'); diff --git a/docker-compose.yml b/docker-compose.yml index d23f634..c8325a7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,8 @@ services: restart: always environment: - BROADCASTER_PASSWORD=MySuperSecurePassword123 + - TURN_USER=myuser + - TURN_PASSWORD=mypassword coturn: image: coturn/coturn:latest diff --git a/public/viewer.js b/public/viewer.js index 3228b5e..d3d74e2 100644 --- a/public/viewer.js +++ b/public/viewer.js @@ -3,17 +3,30 @@ const remoteVideo = document.getElementById('remoteVideo'); const overlay = document.getElementById('overlay'); let peerConnection; -const config = { + +// Fetch TURN credentials dynamically from the server +const turnHost = window.location.hostname; +let config = { iceServers: [ - { urls: "stun:localhost:3478" }, - { - urls: "turn:localhost:3478", - username: "myuser", - credential: "mypassword" - } - ] + { urls: `stun:${turnHost}:3478` } + ], + iceCandidatePoolSize: 5 }; +// Load TURN credentials before any connections +fetch('/turn-config') + .then(r => r.json()) + .then(turn => { + config = { + iceServers: [ + { urls: `stun:${turnHost}:3478` }, + { urls: `turn:${turnHost}:3478`, username: turn.username, credential: turn.credential } + ], + iceCandidatePoolSize: 5 + }; + }) + .catch(e => console.error('Failed to load TURN config:', e)); + socket.on('offer', (id, description) => { // Close any existing connection before creating a new one if (peerConnection) { @@ -31,10 +44,27 @@ socket.on('offer', (id, description) => { // Auto-unmute when the user interacts with the document to bypass browser autoplay restrictions document.addEventListener('click', () => { remoteVideo.muted = false; - // Set sink to max possible volume explicitly avoiding browser gain staging remoteVideo.volume = 1.0; }, {once: true}); + // Monitor ICE connection state for stability + peerConnection.oniceconnectionstatechange = () => { + console.log('ICE state:', peerConnection.iceConnectionState); + if (peerConnection.iceConnectionState === 'failed') { + // Try ICE restart before giving up + console.log('ICE failed, attempting restart...'); + peerConnection.restartIce(); + } else if (peerConnection.iceConnectionState === 'disconnected') { + // Wait briefly then check if it recovered + setTimeout(() => { + if (peerConnection && peerConnection.iceConnectionState === 'disconnected') { + console.log('ICE still disconnected, attempting restart...'); + peerConnection.restartIce(); + } + }, 3000); + } + }; + peerConnection.onicecandidate = event => { if (event.candidate) { socket.emit('candidate', id, event.candidate); diff --git a/server.js b/server.js index 2613423..ac42557 100644 --- a/server.js +++ b/server.js @@ -7,8 +7,15 @@ const io = new Server(server); // Password setting via environment variable, defaulting to "secret" const BROADCASTER_PASSWORD = process.env.BROADCASTER_PASSWORD; +const TURN_USER = process.env.TURN_USER || 'myuser'; +const TURN_PASSWORD = process.env.TURN_PASSWORD || 'mypassword'; let broadcasterSocketId = null; +// Serve TURN credentials to clients +app.get('/turn-config', (req, res) => { + res.json({ username: TURN_USER, credential: TURN_PASSWORD }); +}); + app.use(express.static("public")); io.on("connection", (socket) => {