From ad81b9791a54af26f4aa9c12ab24708c6c08282d Mon Sep 17 00:00:00 2001 From: "every.channel" Date: Tue, 24 Feb 2026 23:21:19 -0800 Subject: [PATCH] web: prefer video tag with gesture audio unlock --- apps/web/app.js | 44 ++++++++++++++++--- ...-live-video-tag-first-with-audio-unlock.md | 26 +++++++++++ 2 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 evolution/proposals/ECP-0078-live-video-tag-first-with-audio-unlock.md diff --git a/apps/web/app.js b/apps/web/app.js index 43086c8..ac2e288 100644 --- a/apps/web/app.js +++ b/apps/web/app.js @@ -169,10 +169,16 @@ function mountPlayer(relayUrl, name) { watch.connection.websocket = { enabled: false }; } - const canvas = document.createElement("canvas"); - canvas.className = "canvas"; - watch.appendChild(canvas); - + // Prefer a video element for native controls/audio routing. + // Start muted to satisfy autoplay policy, then unlock audio on user gesture. + const video = document.createElement("video"); + video.className = "archiveVideo"; + video.controls = true; + video.autoplay = true; + video.muted = true; + video.volume = 1; + video.playsInline = true; + watch.appendChild(video); mount.appendChild(watch); const forceAudioOn = () => { try { @@ -182,9 +188,33 @@ function mountPlayer(relayUrl, name) { // Best effort only. } }; - forceAudioOn(); - window.setTimeout(forceAudioOn, 1000); - window.setTimeout(forceAudioOn, 4000); + const unlockAudio = () => { + forceAudioOn(); + watch.backend?.paused?.set?.(true); + watch.backend?.paused?.set?.(false); + video.muted = false; + video.volume = 1; + void video.play().catch(() => {}); + setHint(`Live: subscribed to ${name} (audio unlocked)`, "ok"); + }; + document.addEventListener("pointerdown", unlockAudio, { once: true }); + video.addEventListener("pointerdown", unlockAudio, { once: true }); + void video.play().catch(() => {}); + + // If video-element rendering stalls, fall back to canvas rendering. + window.setTimeout(() => { + const stalled = video.readyState < 2 || (video.currentTime === 0 && !video.paused); + if (!stalled) return; + const canvas = document.createElement("canvas"); + canvas.className = "canvas"; + try { + video.remove(); + watch.appendChild(canvas); + setHint(`Live: subscribed to ${name} (canvas fallback)`, "warn"); + } catch (_) { + // Ignore fallback errors. + } + }, 9000); bindPlayerSignals(watch, name); } diff --git a/evolution/proposals/ECP-0078-live-video-tag-first-with-audio-unlock.md b/evolution/proposals/ECP-0078-live-video-tag-first-with-audio-unlock.md new file mode 100644 index 0000000..e64ec91 --- /dev/null +++ b/evolution/proposals/ECP-0078-live-video-tag-first-with-audio-unlock.md @@ -0,0 +1,26 @@ +# ECP-0078: Live `