Pantalla TV: el cartel se quedaba pegado cuando el navegador no dejaba hablar
Regresión del cambio anterior. Al arrancar con sonidoActivo=true, anunciarTurno() devolvía el objeto de voz aunque el navegador estuviera bloqueando el audio; el cartel esperaba el evento 'end', que en ese caso no llega nunca, y quedaba fijo hasta la red de seguridad de 25 segundos. Antes devolvía null y cerraba a los 5. Ahora avisa por callback tanto al terminar de hablar como al quedar claro que no va a hablar, detectado porque 'start' no ocurre. speak() no lanza nada ni emite 'error' cuando lo bloquean: el silencio es la única señal. Además, si la voz elegida no arranca —el equipo la lista pero no la puede usar— se reintenta una vez con la del navegador en vez de quedarse mudo. Comprobado con tres escenarios: equipo normal (cierra a 4,7 s), audio bloqueado (3,1 s) y voz que no suena (5,9 s, hablando en el reintento). Aparte: la expresión de mayúsculas usaba \w, que no cuenta letras acentuadas y trataba la tilde como separador. "maría fernanda gómez" se pronunciaba "MaríA Fernanda GóMez". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7764f88df3
commit
8f94a8e012
@@ -629,32 +629,72 @@ if ('speechSynthesis' in window) {
|
||||
// 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) {
|
||||
// Espera antes de hablar: el pito dura 0,65 s y hablando encima tapaba
|
||||
// "Turno X", que desde lejos sonaba como si el anuncio empezara ya empezado.
|
||||
// A nivel de archivo porque showAnnouncement() también lo necesita.
|
||||
const BEEP_MS = 700;
|
||||
|
||||
/**
|
||||
* Dice el llamado en voz alta.
|
||||
*
|
||||
* Avisa por `onFin` cuando terminó de hablar, o cuando quedó claro que no va a
|
||||
* hablar. Lo segundo importa tanto como lo primero: el cartel se cierra con esa
|
||||
* señal, y si nunca llegaba se quedaba pegado en pantalla.
|
||||
*
|
||||
* Si la voz elegida no arranca —pasa cuando el equipo la lista pero no la puede
|
||||
* usar— se reintenta una vez con la del navegador, en vez de quedarse mudo.
|
||||
*
|
||||
* @return bool si se intentó hablar. Falso significa pantalla sin sonido.
|
||||
*/
|
||||
function anunciarTurno(codigo, destino, paciente, onFin) {
|
||||
playBeep();
|
||||
if (!sonidoActivo || !('speechSynthesis' in window)) return null;
|
||||
if (!sonidoActivo || !('speechSynthesis' in window)) return false;
|
||||
|
||||
const letras = codigo.split('').join(' ');
|
||||
const nomVoz = paciente ? paciente.toLowerCase().replace(/\b\w/g, c => c.toUpperCase()) : null;
|
||||
// \w no cuenta las letras acentuadas, así que trataba la tilde como
|
||||
// separador: "maría" salía "MaríA" y la voz lo pronunciaba raro.
|
||||
const nomVoz = paciente
|
||||
? paciente.toLowerCase().replace(/(^|\s)(\p{L})/gu, (_, sep, c) => sep + c.toUpperCase())
|
||||
: null;
|
||||
let texto = `Turno ${letras}`;
|
||||
if (nomVoz) texto += `, ${nomVoz}`;
|
||||
texto += `, pase a ${destino}`;
|
||||
const utt = new SpeechSynthesisUtterance(texto);
|
||||
utt.lang = 'es-CO';
|
||||
const voz = getVozES();
|
||||
if (voz) utt.voice = voz;
|
||||
utt.rate = 0.95; utt.pitch = 1.05; utt.volume = 1;
|
||||
|
||||
// Se espera a que el pito termine (dura 0,65 s) antes de hablar. Hablando
|
||||
// encima, el tono tapaba "Turno X" y desde lejos parecía que el anuncio
|
||||
// empezaba ya empezado.
|
||||
const BEEP_MS = 700;
|
||||
let avisado = false;
|
||||
const avisar = () => { if (!avisado) { avisado = true; onFin(); } };
|
||||
|
||||
// Solo cuando hay algo en curso hace falta cancelar: Chrome ignora un
|
||||
// speak() encadenado a un cancel() en el mismo ciclo.
|
||||
if (window.speechSynthesis.speaking || window.speechSynthesis.pending) {
|
||||
window.speechSynthesis.cancel();
|
||||
}
|
||||
setTimeout(() => window.speechSynthesis.speak(utt), BEEP_MS);
|
||||
return utt;
|
||||
const decir = (voz, esReintento) => {
|
||||
const utt = new SpeechSynthesisUtterance(texto);
|
||||
utt.lang = 'es-CO';
|
||||
if (voz) utt.voice = voz;
|
||||
utt.rate = 0.95; utt.pitch = 1.05; utt.volume = 1;
|
||||
|
||||
let arranco = false;
|
||||
utt.addEventListener('start', () => { arranco = true; });
|
||||
utt.addEventListener('end', avisar);
|
||||
utt.addEventListener('error', () => esReintento ? avisar() : decir(null, true));
|
||||
|
||||
// Que speak() no lance nada no significa que vaya a sonar: si el
|
||||
// navegador bloquea el audio, o la voz no sirve, no pasa absolutamente
|
||||
// nada y 'error' tampoco llega. Solo se nota porque 'start' no ocurre.
|
||||
const margen = (esReintento ? 0 : BEEP_MS) + 1200;
|
||||
setTimeout(() => {
|
||||
if (arranco) return;
|
||||
if (esReintento) { avisar(); return; }
|
||||
window.speechSynthesis.cancel();
|
||||
decir(null, true); // segunda oportunidad, con la voz del navegador
|
||||
}, margen);
|
||||
|
||||
// Solo cuando hay algo en curso hace falta cancelar: Chrome ignora un
|
||||
// speak() encadenado a un cancel() en el mismo ciclo.
|
||||
if (window.speechSynthesis.speaking || window.speechSynthesis.pending) {
|
||||
window.speechSynthesis.cancel();
|
||||
}
|
||||
setTimeout(() => window.speechSynthesis.speak(utt), esReintento ? 0 : BEEP_MS);
|
||||
};
|
||||
|
||||
decir(getVozES(), false);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ── Fullscreen ── */
|
||||
@@ -699,8 +739,6 @@ function showAnnouncement({ codigo, destino, paciente, color }) {
|
||||
|
||||
ov.classList.add('visible');
|
||||
|
||||
const utt = anunciarTurno(codigo, destino, paciente);
|
||||
|
||||
bar.style.transition = 'none';
|
||||
bar.style.width = '100%';
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
@@ -725,11 +763,15 @@ function showAnnouncement({ codigo, destino, paciente, color }) {
|
||||
setTimeout(dismiss, Math.max(0, MIN_VISIBLE - (Date.now() - abiertoEn)));
|
||||
};
|
||||
|
||||
if (utt) {
|
||||
utt.addEventListener('end', () => setTimeout(cerrarCuandoToque, 400));
|
||||
utt.addEventListener('error', cerrarCuandoToque);
|
||||
// Red de seguridad: Chrome a veces no emite 'end' y el cartel quedaría fijo
|
||||
setTimeout(dismiss, 25000);
|
||||
// anunciarTurno avisa tanto si terminó de hablar como si quedó claro que no
|
||||
// va a hablar. Esperar solo el fin de la voz dejaba el cartel pegado en las
|
||||
// pantallas donde el navegador bloquea el audio.
|
||||
const intentoHablar = anunciarTurno(codigo, destino, paciente,
|
||||
() => setTimeout(cerrarCuandoToque, 400));
|
||||
|
||||
if (intentoHablar) {
|
||||
// Red de seguridad: Chrome a veces no emite 'end' a mitad de una frase
|
||||
setTimeout(dismiss, 20000);
|
||||
} else {
|
||||
setTimeout(dismiss, MIN_VISIBLE);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user