Pantalla TV: el sonido arranca encendido, sin esperar que alguien la toque

sonidoActivo empezaba en false y solo un clic lo encendía, así que cualquier
recarga —corte de red, reinicio, refresco— dejaba el televisor mudo hasta que
una persona fuera físicamente a tocarlo. Peor: con esa lógica ni siquiera
poniendo --autoplay-policy=no-user-gesture-required habría sonado, porque la
página se bloqueaba a sí misma antes de que el navegador opinara.

Ahora arranca encendido e intenta abrir el audio al cargar. Si el navegador
lo permite, habla sin que nadie intervenga. Si lo bloquea, se muestra el
letrero para que alguien toque una vez, que es el único camino que dejan las
políticas de reproducción automática.

El pito de confirmación queda solo para el clic humano: al arrancar sola, la
pantalla no tiene por qué pitar cada vez que se recarga. Y el letrero ya no
depende de localStorage sino de si el audio quedó realmente suspendido,
atado a onstatechange porque resume() es asíncrono y antes parpadeaba.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-12 07:45:52 -05:00
co-authored by Claude Opus 5
parent db133d9b5d
commit 24c05a40c2
+48 -17
View File
@@ -378,7 +378,7 @@ $_hasVideo = (bool)$_tvPlaylist;
<div class="dot"></div>
</div>
<div id="sonido-banner" onclick="activarSonido()" style="display:none;position:fixed;bottom:0;left:0;right:0;z-index:9999;background:rgba(239,68,68,.92);color:#fff;padding:.6rem 1.2rem;font-size:1rem;font-weight:600;cursor:pointer;align-items:center;justify-content:center;gap:.6rem">
<div id="sonido-banner" onclick="activarSonido(true)" style="display:none;position:fixed;bottom:0;left:0;right:0;z-index:9999;background:rgba(239,68,68,.92);color:#fff;padding:.6rem 1.2rem;font-size:1rem;font-weight:600;cursor:pointer;align-items:center;justify-content:center;gap:.6rem">
<i class="fas fa-volume-mute"></i> Haz clic aquí para activar el sonido
</div>
@@ -404,7 +404,7 @@ $_hasVideo = (bool)$_tvPlaylist;
<div class="hd-right">
<div class="live-dot"></div>
<div class="reloj" id="reloj">--:--:--</div>
<button class="btn-fs" id="btn-sonido" onclick="activarSonido()" title="Activar sonido">
<button class="btn-fs" id="btn-sonido" onclick="activarSonido(true)" title="Activar sonido">
<i class="fas fa-volume-mute"></i>
</button>
<button class="btn-fs" onclick="toggleFullscreen()" title="Pantalla completa">
@@ -499,14 +499,22 @@ function tick() {
tick(); setInterval(tick, 1000);
/* ── Audio ── */
let audioCtx = null, sonidoActivo = false;
// Arranca encendido. Esta pantalla vive en un televisor que nadie toca: si el
// sonido esperara un clic, cualquier recarga —corte de red, reinicio, refresco—
// la dejaba muda hasta que alguien fuera físicamente a tocarla, que es lo que
// venía pasando. Si el navegador termina bloqueándolo, se avisa con el letrero.
let audioCtx = null, sonidoActivo = true;
function activarSonido() {
function activarSonido(porGesto) {
try {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
} else if (audioCtx.state === 'suspended') {
audioCtx.resume();
// resume() es asíncrono: sin esto el letrero parpadeaba al abrir,
// porque el estado todavía decía "suspended" cuando ya iba a sonar.
audioCtx.onstatechange = revisarBloqueo;
}
if (audioCtx.state === 'suspended') {
audioCtx.resume().catch(() => {});
}
const g = audioCtx.createGain(); g.gain.setValueAtTime(0.001, audioCtx.currentTime);
const o = audioCtx.createOscillator(); o.connect(g); g.connect(audioCtx.destination);
@@ -516,13 +524,39 @@ function activarSonido() {
localStorage.setItem('turneroSonido', '1');
const btn = document.getElementById('btn-sonido');
if (btn) { btn.innerHTML = '<i class="fas fa-volume-up"></i>'; btn.classList.add('on'); }
const banner = document.getElementById('sonido-banner');
if (banner) banner.style.display = 'none';
setTimeout(playBeep, 100);
// El pito de confirmación solo cuando alguien tocó: al arrancar sola, la
// pantalla no tiene por qué pitar cada vez que se recarga.
if (porGesto) setTimeout(playBeep, 100);
revisarBloqueo();
}
// Sin once:true para que cualquier clic reactive si la página recargó
document.addEventListener('click', () => { if (!sonidoActivo) activarSonido(); });
document.addEventListener('touchstart', () => { if (!sonidoActivo) activarSonido(); });
/**
* El navegador puede negarse a sonar sin un gesto humano. No hay forma de
* saberlo preguntando, solo mirando si el contexto quedó suspendido: si es así
* se muestra el letrero para que alguien toque la pantalla una vez.
*
* Se evita del todo abriendo Chrome en el televisor con
* --autoplay-policy=no-user-gesture-required
*/
function revisarBloqueo() {
const banner = document.getElementById('sonido-banner');
if (!banner) return;
const bloqueado = !audioCtx || audioCtx.state === 'suspended';
banner.style.display = bloqueado ? 'flex' : 'none';
}
// Intento de arranque automático, en cuanto la página está lista
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => activarSonido(false));
} else {
activarSonido(false);
}
// Reintento: Chrome a veces deja el contexto suspendido un instante al abrir
setTimeout(() => { if (audioCtx && audioCtx.state === 'suspended') activarSonido(false); }, 2000);
// Cualquier gesto sirve de respaldo si el navegador bloqueó el arranque
document.addEventListener('click', () => activarSonido(true));
document.addEventListener('touchstart', () => activarSonido(true));
function playBeep() {
if (!sonidoActivo || !audioCtx) return;
@@ -568,11 +602,8 @@ if ('speechSynthesis' in window) {
window.speechSynthesis.resume();
}, 10000);
}
// Auto-restore sonido si estaba activo antes de un reload
if (localStorage.getItem('turneroSonido') === '1') {
const banner = document.getElementById('sonido-banner');
if (banner) banner.style.display = 'flex';
}
// El letrero ya no depende de localStorage sino de si el navegador dejó sonar:
// lo decide revisarBloqueo(), que corre al arrancar y en cada intento.
function anunciarTurno(codigo, destino, paciente) {
playBeep();