diff --git a/server/scripts/modules/media.mjs b/server/scripts/modules/media.mjs index 986ce7f..27ea856 100644 --- a/server/scripts/modules/media.mjs +++ b/server/scripts/modules/media.mjs @@ -385,6 +385,10 @@ const screenAudioMap = { }; let screenAudioPlayer = null; +let audioStartTime = 0; +let audioStopTimeout = null; +const AUDIO_MIN_PLAY_TIME = 500; // 500ms minimum play time +let currentScreenId = null; // Helper function to check if screen audio is enabled (always reads from localStorage) const isScreenAudioEnabled = () => { @@ -394,17 +398,35 @@ const isScreenAudioEnabled = () => { // Play screen audio const playScreenAudio = async (screenId) => { + console.log(`[AUDIO] playScreenAudio called for ${screenId}`); + // Always check localStorage to ensure setting is current - if (!isScreenAudioEnabled()) return; + if (!isScreenAudioEnabled()) { + console.log(`[AUDIO] Aborting - screen audio disabled`); + return; + } const fileName = screenAudioMap[screenId]; - if (!fileName) return; + if (!fileName) { + console.log(`[AUDIO] No audio file mapped for ${screenId}`); + return; + } + + // Cancel any pending stop timeout from previous audio + if (audioStopTimeout) { + console.log(`[AUDIO] Cancelling pending stop timeout for new audio`); + clearTimeout(audioStopTimeout); + audioStopTimeout = null; + } // Stop any existing screen audio first stopScreenAudio(); // Don't play if alert tone is active - if (alertToneActive || alertTonePending) return; + if (alertToneActive || alertTonePending) { + console.log(`[AUDIO] Aborting - alert tone active/pending`); + return; + } // Duck background music to 10% if (player && !player.paused) { @@ -414,17 +436,22 @@ const playScreenAudio = async (screenId) => { // Create and play audio screenAudioPlayer = new Audio(withBasePath(`alert/${fileName}`)); screenAudioPlayer.type = 'audio/mpeg'; + currentScreenId = screenId; screenAudioPlayer.addEventListener('ended', () => { + console.log(`[AUDIO] 'ended' event fired for ${currentScreenId}`); screenAudioPlayer = null; + currentScreenId = null; // Only restore if alert isn't playing if (!alertToneActive && !alertTonePending) { restoreMediaAfterAlert(); } }); - screenAudioPlayer.addEventListener('error', () => { + screenAudioPlayer.addEventListener('error', (e) => { + console.log(`[AUDIO] 'error' event fired for ${currentScreenId}:`, e.message || e); screenAudioPlayer = null; + currentScreenId = null; if (!alertToneActive && !alertTonePending) { restoreMediaAfterAlert(); } @@ -432,21 +459,69 @@ const playScreenAudio = async (screenId) => { try { await screenAudioPlayer.play(); + audioStartTime = Date.now(); + console.log(`[AUDIO] Audio started for ${screenId} at ${audioStartTime}`); } catch (e) { + console.log(`[AUDIO] Failed to play for ${screenId}:`, e.message); screenAudioPlayer = null; + currentScreenId = null; if (!alertToneActive && !alertTonePending) { restoreMediaAfterAlert(); } } }; -// Stop screen audio immediately -const stopScreenAudio = () => { +// Actually stop the audio (internal helper) +const actuallyStopAudio = () => { + console.log(`[AUDIO] Actually stopping audio for ${currentScreenId}`); if (screenAudioPlayer) { screenAudioPlayer.pause(); screenAudioPlayer.currentTime = 0; screenAudioPlayer = null; } + currentScreenId = null; + audioStartTime = 0; + audioStopTimeout = null; +}; + +// Stop screen audio (with minimum play time protection) +const stopScreenAudio = () => { + if (!screenAudioPlayer) { + console.log(`[AUDIO] stopScreenAudio called but no audio playing`); + return; + } + + const elapsed = Date.now() - audioStartTime; + console.log(`[AUDIO] stopScreenAudio called for ${currentScreenId}, elapsed: ${elapsed}ms`); + + // If we haven't played for the minimum time, delay the stop + if (elapsed < AUDIO_MIN_PLAY_TIME) { + const delay = AUDIO_MIN_PLAY_TIME - elapsed; + console.log(`[AUDIO] Delaying stop for ${delay}ms (minimum play time: ${AUDIO_MIN_PLAY_TIME}ms)`); + + // Clear any existing timeout first + if (audioStopTimeout) { + clearTimeout(audioStopTimeout); + } + + audioStopTimeout = setTimeout(() => { + // Check if this audio is still the current one (might have been replaced) + if (screenAudioPlayer && Date.now() - audioStartTime >= AUDIO_MIN_PLAY_TIME) { + console.log(`[AUDIO] Executing delayed stop for ${currentScreenId}`); + actuallyStopAudio(); + // Restore music volume after delayed stop + if (!alertToneActive && !alertTonePending) { + restoreMediaAfterAlert(); + } + } else { + console.log(`[AUDIO] Delayed stop cancelled - audio already changed or stopped`); + } + }, delay); + } else { + // Minimum play time met, stop immediately + console.log(`[AUDIO] Minimum play time met, stopping immediately`); + actuallyStopAudio(); + } }; const playerCanPlay = async () => { diff --git a/server/scripts/modules/navigation.mjs b/server/scripts/modules/navigation.mjs index 4a5d27a..2e36a57 100644 --- a/server/scripts/modules/navigation.mjs +++ b/server/scripts/modules/navigation.mjs @@ -6,6 +6,7 @@ import { safeJson } from './utils/fetch.mjs'; import { getPoint, getOpenMeteoForecast, aggregateWeatherForecastData } from './utils/weather.mjs'; import { debugFlag } from './utils/debug.mjs'; import settings from './settings.mjs'; +import { stopScreenAudio } from './media.mjs'; document.addEventListener('DOMContentLoaded', () => { init(); @@ -386,17 +387,13 @@ const handleNavButton = (button) => { case 'next': setPlaying(false); // Stop screen audio immediately when navigating - import('./media.mjs').then((media) => { - media.stopScreenAudio(); - }); + stopScreenAudio(); navTo(msg.command.nextFrame); break; case 'previous': setPlaying(false); // Stop screen audio immediately when navigating - import('./media.mjs').then((media) => { - media.stopScreenAudio(); - }); + stopScreenAudio(); navTo(msg.command.previousFrame); break; case 'menu': diff --git a/server/scripts/modules/weatherdisplay.mjs b/server/scripts/modules/weatherdisplay.mjs index 53d682e..694c9c3 100644 --- a/server/scripts/modules/weatherdisplay.mjs +++ b/server/scripts/modules/weatherdisplay.mjs @@ -9,6 +9,7 @@ import { parseQueryString } from './utils/setting.mjs'; import settings from './settings.mjs'; import { elemForEach } from './utils/elem.mjs'; import { debugFlag } from './utils/debug.mjs'; +import { playScreenAudio, stopScreenAudio } from './media.mjs'; class WeatherDisplay { constructor(navId, elemId, name, defaultEnabled) { @@ -228,9 +229,7 @@ class WeatherDisplay { // Play screen-specific audio only if display was not already active // This prevents audio restart on frame changes (e.g., Local Radar animation) if (!wasActive) { - import('./media.mjs').then((media) => { - media.playScreenAudio(this.elemId); - }); + playScreenAudio(this.elemId); } } @@ -241,9 +240,7 @@ class WeatherDisplay { document.querySelector('#divTwc').classList.remove(this.elemId); // Stop screen audio when leaving - import('./media.mjs').then((media) => { - media.stopScreenAudio(); - }); + stopScreenAudio(); } get active() {