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 @@
-
+