Queue overlay messages instead of interrupting the current one
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 31s

Messages now play one at a time for their full configured duration
before the next one appears, instead of a new message immediately
replacing whatever was on screen. TTS audio is tagged with the same
message id as its text so it stays in sync even though it's
generated and delivered asynchronously after the text broadcast.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
vrubelroman 2026-07-01 17:05:01 +00:00
parent f4d023c695
commit 03a3a4c4af
2 changed files with 50 additions and 22 deletions

View file

@ -5,7 +5,10 @@ const textEl = document.getElementById('text');
const token = location.pathname.split('/').filter(Boolean).pop();
const socket = io({ query: { role: 'overlay', token } });
let hideTimer = null;
const FADE_MS = 400; // matches the CSS transition duration on #box
const queue = [];
let current = null; // { id, text, from, durationMs, audio }
let audioCtx = null;
function getAudioCtx() {
@ -42,34 +45,53 @@ function playNotificationSound() {
}
}
function showMessage(text, from, durationMs) {
clearTimeout(hideTimer);
fromEl.textContent = from || '';
fromEl.style.display = from ? 'block' : 'none';
textEl.textContent = text;
// force reflow so the transition re-triggers even if a message is already visible
void box.offsetWidth;
box.classList.add('visible');
playNotificationSound();
hideTimer = setTimeout(() => {
box.classList.remove('visible');
}, durationMs || 10000);
}
function playSpeech(base64Audio, mimeType) {
const audio = new Audio(`data:${mimeType};base64,${base64Audio}`);
audio.play().catch((err) => console.error('Could not play TTS audio:', err));
}
function advanceQueue() {
if (current || queue.length === 0) return;
current = queue.shift();
fromEl.textContent = current.from || '';
fromEl.style.display = current.from ? 'block' : 'none';
textEl.textContent = current.text;
// force reflow so the transition re-triggers even if a message was already visible
void box.offsetWidth;
box.classList.add('visible');
playNotificationSound();
if (current.audio) {
playSpeech(current.audio.data, current.audio.mimeType);
}
setTimeout(() => {
box.classList.remove('visible');
current = null;
setTimeout(advanceQueue, FADE_MS);
}, current.durationMs || 10000);
}
socket.on('display_message', (payload) => {
showMessage(payload.text, payload.from, payload.durationMs);
queue.push({
id: payload.id,
text: payload.text,
from: payload.from,
durationMs: payload.durationMs,
audio: null,
});
advanceQueue();
});
socket.on('display_audio', (payload) => {
playSpeech(payload.audio, payload.mimeType);
const audio = { data: payload.audio, mimeType: payload.mimeType };
if (current && current.id === payload.id) {
playSpeech(audio.data, audio.mimeType);
return;
}
const queued = queue.find((item) => item.id === payload.id);
if (queued) queued.audio = audio;
});
socket.on('invalid_link', () => {