diff --git a/desktop/app.js b/desktop/app.js index 5900b47..a481e02 100644 --- a/desktop/app.js +++ b/desktop/app.js @@ -1003,6 +1003,29 @@ function ensurePeerConnection(peerId) { })); }; + pc.oniceconnectionstatechange = async () => { + const iceState = pc.iceConnectionState; + console.debug(`voice ice state (${peerId}):`, iceState); + if (iceState !== "failed" || pc.restartingIce) return; + pc.restartingIce = true; + try { + const offer = await pc.createOffer({ iceRestart: true }); + await pc.setLocalDescription(offer); + if (state.voice.ws && state.voice.ws.readyState === WebSocket.OPEN) { + state.voice.ws.send(JSON.stringify({ + type: "signal", + to_user_id: peerId, + kind: "offer", + data: pc.localDescription, + })); + } + } catch (err) { + console.warn("ICE restart failed", err); + } finally { + pc.restartingIce = false; + } + }; + pc.ontrack = (event) => { const stream = event.streams[0]; if (event.track.kind === "audio") { @@ -1014,7 +1037,20 @@ function ensurePeerConnection(peerId) { audio.playsInline = true; document.body.appendChild(audio); } - audio.srcObject = stream; + audio.muted = false; + audio.volume = 1; + audio.srcObject = new MediaStream([event.track]); + const tryPlay = () => { + const playPromise = audio.play(); + if (playPromise && typeof playPromise.catch === "function") { + playPromise.catch((err) => { + console.warn(`remote audio playback blocked for ${peerId}`, err); + }); + } + }; + audio.onloadedmetadata = tryPlay; + audio.oncanplay = tryPlay; + tryPlay(); } else if (event.track.kind === "video") { const peer = state.members.find(m => m.id === peerId) || { display_name: "Unknown" }; renderVideo(peerId, peer.display_name, stream, event.track.id); diff --git a/static/app.js b/static/app.js index bcf4501..b7a0a26 100644 --- a/static/app.js +++ b/static/app.js @@ -943,6 +943,29 @@ function ensurePeerConnection(peerId) { })); }; + pc.oniceconnectionstatechange = async () => { + const iceState = pc.iceConnectionState; + console.debug(`voice ice state (${peerId}):`, iceState); + if (iceState !== "failed" || pc.restartingIce) return; + pc.restartingIce = true; + try { + const offer = await pc.createOffer({ iceRestart: true }); + await pc.setLocalDescription(offer); + if (state.voice.ws && state.voice.ws.readyState === WebSocket.OPEN) { + state.voice.ws.send(JSON.stringify({ + type: "signal", + to_user_id: peerId, + kind: "offer", + data: pc.localDescription, + })); + } + } catch (err) { + console.warn("ICE restart failed", err); + } finally { + pc.restartingIce = false; + } + }; + pc.ontrack = (event) => { const stream = event.streams[0]; if (event.track.kind === "audio") { @@ -954,7 +977,20 @@ function ensurePeerConnection(peerId) { audio.playsInline = true; document.body.appendChild(audio); } - audio.srcObject = stream; + audio.muted = false; + audio.volume = 1; + audio.srcObject = new MediaStream([event.track]); + const tryPlay = () => { + const playPromise = audio.play(); + if (playPromise && typeof playPromise.catch === "function") { + playPromise.catch((err) => { + console.warn(`remote audio playback blocked for ${peerId}`, err); + }); + } + }; + audio.onloadedmetadata = tryPlay; + audio.oncanplay = tryPlay; + tryPlay(); } else if (event.track.kind === "video") { const peer = state.members.find(m => m.id === peerId) || { display_name: "Unknown" }; renderVideo(peerId, peer.display_name, stream, event.track.id); diff --git a/static/index.html b/static/index.html index 007c38e..fb34084 100644 --- a/static/index.html +++ b/static/index.html @@ -265,7 +265,7 @@ - +