diff --git a/assets/js/main.js b/assets/js/main.js index 8256e19..2849f3e 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -224,85 +224,174 @@ document.addEventListener('DOMContentLoaded', () => { } }; + const announcementQueue = []; + const queuedAnnouncementKeys = new Set(); + let announcementPlaying = false; + let activeAnnouncementAudio = null; + let activeSpeechUtterance = null; + let audioLoadTimeoutId = null; + + const clearAnnouncementPlayback = () => { + if (audioLoadTimeoutId) { + window.clearTimeout(audioLoadTimeoutId); + audioLoadTimeoutId = null; + } + + if (activeAnnouncementAudio) { + activeAnnouncementAudio.onended = null; + activeAnnouncementAudio.onerror = null; + activeAnnouncementAudio.pause(); + activeAnnouncementAudio.removeAttribute('src'); + activeAnnouncementAudio.load(); + activeAnnouncementAudio = null; + } + + if (activeSpeechUtterance) { + activeSpeechUtterance.onend = null; + activeSpeechUtterance.onerror = null; + activeSpeechUtterance = null; + } + }; + + const queueAnnouncement = (card) => { + if (!card) return; + const key = card.dataset.announcementKey || ''; + const text = locale === 'ar' ? (card.dataset.announcementAr || '') : (card.dataset.announcementEn || ''); + if (!key || !text || queuedAnnouncementKeys.has(key)) return; + announcementQueue.push({ key, text }); + queuedAnnouncementKeys.add(key); + playNextAnnouncement(); + }; + + const playNextAnnouncement = () => { + if (announcementPlaying || announcementQueue.length === 0) { + return; + } + + const nextAnnouncement = announcementQueue.shift(); + if (!nextAnnouncement) { + return; + } + + announcementPlaying = true; + const videoPlayer = document.getElementById('adsVideoPlayer'); + if (videoPlayer) videoPlayer.volume = 0.1; + + const finishAnnouncement = () => { + clearAnnouncementPlayback(); + if ('speechSynthesis' in window && (window.speechSynthesis.speaking || window.speechSynthesis.pending)) { + window.speechSynthesis.cancel(); + } + if (videoPlayer) videoPlayer.volume = 1.0; + queuedAnnouncementKeys.delete(nextAnnouncement.key); + announcementPlaying = false; + playNextAnnouncement(); + }; + + playChime(); + + window.setTimeout(() => { + const tl = locale === 'ar' ? 'ar' : 'en'; + const ttsUrl = window.location.pathname.replace(/\/[^\/]*$/, '/api/tts.php') + '?lang=' + tl + '&text=' + encodeURIComponent(nextAnnouncement.text); + const audioObj = new Audio(ttsUrl); + audioObj.preload = 'auto'; + activeAnnouncementAudio = audioObj; + + let finished = false; + const done = () => { + if (finished) return; + finished = true; + finishAnnouncement(); + }; + + const handleFallback = (err) => { + if (finished) return; + console.warn('External TTS failed, falling back to built-in speech', err); + + if (activeAnnouncementAudio === audioObj) { + audioObj.onended = null; + audioObj.onerror = null; + audioObj.pause(); + audioObj.removeAttribute('src'); + audioObj.load(); + activeAnnouncementAudio = null; + } + + if ('speechSynthesis' in window) { + const utterance = new SpeechSynthesisUtterance(nextAnnouncement.text); + utterance.lang = locale === 'ar' ? 'ar-SA' : 'en-US'; + const voices = availableVoices.length > 0 ? availableVoices : window.speechSynthesis.getVoices(); + const langPrefix = locale === 'ar' ? 'ar' : 'en'; + const langVoices = voices.filter((voice) => voice.lang.toLowerCase().startsWith(langPrefix)); + + if (langVoices.length > 0) { + const bestVoice = langVoices.find((voice) => + voice.name.includes('Google') || voice.name.includes('Natural') || voice.name.includes('Premium') || voice.name.includes('Online') + ) || langVoices.find((voice) => voice.name.includes('Microsoft')) || langVoices[0]; + if (bestVoice) utterance.voice = bestVoice; + } + + activeSpeechUtterance = utterance; + utterance.onend = done; + utterance.onerror = done; + if (window.speechSynthesis.speaking || window.speechSynthesis.pending) { + window.speechSynthesis.cancel(); + } + window.speechSynthesis.speak(utterance); + } else { + done(); + } + }; + + audioObj.onended = done; + audioObj.onerror = handleFallback; + + const playPromise = audioObj.play(); + if (playPromise !== undefined) { + playPromise.catch(handleFallback); + } + + audioLoadTimeoutId = window.setTimeout(() => { + if (!finished && (audioObj.networkState === HTMLMediaElement.NETWORK_NO_SOURCE || audioObj.error)) { + handleFallback(new Error('Audio load timeout')); + } + }, 2500); + }, 500); + }; + const checkAnnouncements = () => { const cards = Array.from(document.querySelectorAll('.announcement-card')); const latest = cards[0]; const audioEnabled = window.localStorage.getItem('hospitalQueue:audioEnabled') !== 'false'; - - if (latest) { - const announcementKey = latest.dataset.announcementKey || ''; - const storageKey = `hospitalQueue:lastAnnouncement:${locale}`; - const storedKey = window.localStorage.getItem(storageKey) || ''; - - if (announcementKey && announcementKey !== storedKey) { - window.localStorage.setItem(storageKey, announcementKey); - - if (audioEnabled) { - const videoPlayer = document.getElementById('adsVideoPlayer'); - if (videoPlayer) videoPlayer.volume = 0.1; - - const text = locale === 'ar' ? (latest.dataset.announcementAr || '') : (latest.dataset.announcementEn || ''); - let audioObj = null; - if (text) { - const tl = locale === 'ar' ? 'ar' : 'en'; - const ttsUrl = window.location.pathname.replace(/\/[^\/]*$/, '/api/tts.php') + '?lang=' + tl + '&text=' + encodeURIComponent(text); - audioObj = new Audio(ttsUrl); - audioObj.preload = 'auto'; // Force browser to start downloading - } - - playChime(); - - setTimeout(() => { - if (audioObj) { - audioObj.onended = () => { if (videoPlayer) videoPlayer.volume = 1.0; }; - - let fallbackPlayed = false; - const handleFallback = (err) => { - if (fallbackPlayed) return; - fallbackPlayed = true; - console.warn("External TTS failed, falling back to built-in speech", err); - if ('speechSynthesis' in window) { - const utterance = new SpeechSynthesisUtterance(text); - utterance.lang = locale === 'ar' ? 'ar-SA' : 'en-US'; - const voices = availableVoices.length > 0 ? availableVoices : window.speechSynthesis.getVoices(); - const langPrefix = locale === 'ar' ? 'ar' : 'en'; - const langVoices = voices.filter(v => v.lang.toLowerCase().startsWith(langPrefix)); - - if (langVoices.length > 0) { - const bestVoice = langVoices.find(v => - v.name.includes('Google') || v.name.includes('Natural') || v.name.includes('Premium') || v.name.includes('Online') - ) || langVoices.find(v => v.name.includes('Microsoft')) || langVoices[0]; - if (bestVoice) utterance.voice = bestVoice; - } - - utterance.onend = () => { if (videoPlayer) videoPlayer.volume = 1.0; }; - utterance.onerror = () => { if (videoPlayer) videoPlayer.volume = 1.0; }; - window.speechSynthesis.cancel(); // Clear any stuck queue - window.speechSynthesis.speak(utterance); - } else { - if (videoPlayer) videoPlayer.volume = 1.0; - } - }; - audioObj.onerror = handleFallback; - - const playPromise = audioObj.play(); - if (playPromise !== undefined) { - playPromise.catch(handleFallback); - } - - // Failsafe timeout in case audioObj hangs (e.g., blocked by adblocker, bad network) - setTimeout(() => { - if (audioObj.networkState === HTMLMediaElement.NETWORK_NO_SOURCE || audioObj.error) { - handleFallback(new Error("Audio load timeout")); - } - }, 2500); - - } else { - if (videoPlayer) videoPlayer.volume = 1.0; - } - }, 500); // reduced timeout from 1200ms to 500ms for faster playback - } + + if (!latest) { + return; + } + + const storageKey = `hospitalQueue:lastAnnouncement:${locale}`; + const storedKey = window.localStorage.getItem(storageKey) || ''; + const latestKey = latest.dataset.announcementKey || ''; + if (!latestKey || latestKey === storedKey) { + return; + } + + let newCards = []; + if (!storedKey) { + newCards = [latest]; + } else { + for (const card of cards) { + const key = card.dataset.announcementKey || ''; + if (!key) continue; + if (key === storedKey) break; + newCards.push(card); } + newCards.reverse(); + } + + window.localStorage.setItem(storageKey, latestKey); + + if (audioEnabled) { + newCards.forEach(queueAnnouncement); } };