Stabilize screen audio playback/reduce race conditions
Some checks are pending
build-docker / Build Image (push) Waiting to run

This commit is contained in:
mrkmntal 2026-04-12 17:42:18 -04:00
commit b0d9c95bf1
3 changed files with 87 additions and 18 deletions

View file

@ -385,6 +385,10 @@ const screenAudioMap = {
}; };
let screenAudioPlayer = null; 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) // Helper function to check if screen audio is enabled (always reads from localStorage)
const isScreenAudioEnabled = () => { const isScreenAudioEnabled = () => {
@ -394,17 +398,35 @@ const isScreenAudioEnabled = () => {
// Play screen audio // Play screen audio
const playScreenAudio = async (screenId) => { const playScreenAudio = async (screenId) => {
console.log(`[AUDIO] playScreenAudio called for ${screenId}`);
// Always check localStorage to ensure setting is current // 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]; 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 // Stop any existing screen audio first
stopScreenAudio(); stopScreenAudio();
// Don't play if alert tone is active // 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% // Duck background music to 10%
if (player && !player.paused) { if (player && !player.paused) {
@ -414,17 +436,22 @@ const playScreenAudio = async (screenId) => {
// Create and play audio // Create and play audio
screenAudioPlayer = new Audio(withBasePath(`alert/${fileName}`)); screenAudioPlayer = new Audio(withBasePath(`alert/${fileName}`));
screenAudioPlayer.type = 'audio/mpeg'; screenAudioPlayer.type = 'audio/mpeg';
currentScreenId = screenId;
screenAudioPlayer.addEventListener('ended', () => { screenAudioPlayer.addEventListener('ended', () => {
console.log(`[AUDIO] 'ended' event fired for ${currentScreenId}`);
screenAudioPlayer = null; screenAudioPlayer = null;
currentScreenId = null;
// Only restore if alert isn't playing // Only restore if alert isn't playing
if (!alertToneActive && !alertTonePending) { if (!alertToneActive && !alertTonePending) {
restoreMediaAfterAlert(); restoreMediaAfterAlert();
} }
}); });
screenAudioPlayer.addEventListener('error', () => { screenAudioPlayer.addEventListener('error', (e) => {
console.log(`[AUDIO] 'error' event fired for ${currentScreenId}:`, e.message || e);
screenAudioPlayer = null; screenAudioPlayer = null;
currentScreenId = null;
if (!alertToneActive && !alertTonePending) { if (!alertToneActive && !alertTonePending) {
restoreMediaAfterAlert(); restoreMediaAfterAlert();
} }
@ -432,21 +459,69 @@ const playScreenAudio = async (screenId) => {
try { try {
await screenAudioPlayer.play(); await screenAudioPlayer.play();
audioStartTime = Date.now();
console.log(`[AUDIO] Audio started for ${screenId} at ${audioStartTime}`);
} catch (e) { } catch (e) {
console.log(`[AUDIO] Failed to play for ${screenId}:`, e.message);
screenAudioPlayer = null; screenAudioPlayer = null;
currentScreenId = null;
if (!alertToneActive && !alertTonePending) { if (!alertToneActive && !alertTonePending) {
restoreMediaAfterAlert(); restoreMediaAfterAlert();
} }
} }
}; };
// Stop screen audio immediately // Actually stop the audio (internal helper)
const stopScreenAudio = () => { const actuallyStopAudio = () => {
console.log(`[AUDIO] Actually stopping audio for ${currentScreenId}`);
if (screenAudioPlayer) { if (screenAudioPlayer) {
screenAudioPlayer.pause(); screenAudioPlayer.pause();
screenAudioPlayer.currentTime = 0; screenAudioPlayer.currentTime = 0;
screenAudioPlayer = null; 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 () => { const playerCanPlay = async () => {

View file

@ -6,6 +6,7 @@ import { safeJson } from './utils/fetch.mjs';
import { getPoint, getOpenMeteoForecast, aggregateWeatherForecastData } from './utils/weather.mjs'; import { getPoint, getOpenMeteoForecast, aggregateWeatherForecastData } from './utils/weather.mjs';
import { debugFlag } from './utils/debug.mjs'; import { debugFlag } from './utils/debug.mjs';
import settings from './settings.mjs'; import settings from './settings.mjs';
import { stopScreenAudio } from './media.mjs';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
init(); init();
@ -386,17 +387,13 @@ const handleNavButton = (button) => {
case 'next': case 'next':
setPlaying(false); setPlaying(false);
// Stop screen audio immediately when navigating // Stop screen audio immediately when navigating
import('./media.mjs').then((media) => { stopScreenAudio();
media.stopScreenAudio();
});
navTo(msg.command.nextFrame); navTo(msg.command.nextFrame);
break; break;
case 'previous': case 'previous':
setPlaying(false); setPlaying(false);
// Stop screen audio immediately when navigating // Stop screen audio immediately when navigating
import('./media.mjs').then((media) => { stopScreenAudio();
media.stopScreenAudio();
});
navTo(msg.command.previousFrame); navTo(msg.command.previousFrame);
break; break;
case 'menu': case 'menu':

View file

@ -9,6 +9,7 @@ import { parseQueryString } from './utils/setting.mjs';
import settings from './settings.mjs'; import settings from './settings.mjs';
import { elemForEach } from './utils/elem.mjs'; import { elemForEach } from './utils/elem.mjs';
import { debugFlag } from './utils/debug.mjs'; import { debugFlag } from './utils/debug.mjs';
import { playScreenAudio, stopScreenAudio } from './media.mjs';
class WeatherDisplay { class WeatherDisplay {
constructor(navId, elemId, name, defaultEnabled) { constructor(navId, elemId, name, defaultEnabled) {
@ -228,9 +229,7 @@ class WeatherDisplay {
// Play screen-specific audio only if display was not already active // Play screen-specific audio only if display was not already active
// This prevents audio restart on frame changes (e.g., Local Radar animation) // This prevents audio restart on frame changes (e.g., Local Radar animation)
if (!wasActive) { if (!wasActive) {
import('./media.mjs').then((media) => { playScreenAudio(this.elemId);
media.playScreenAudio(this.elemId);
});
} }
} }
@ -241,9 +240,7 @@ class WeatherDisplay {
document.querySelector('#divTwc').classList.remove(this.elemId); document.querySelector('#divTwc').classList.remove(this.elemId);
// Stop screen audio when leaving // Stop screen audio when leaving
import('./media.mjs').then((media) => { stopScreenAudio();
media.stopScreenAudio();
});
} }
get active() { get active() {