Stabilize playback state and cursor feedback

This commit is contained in:
2026-05-18 20:57:20 +02:00
parent 6e908037fb
commit 751ac5f62b
13 changed files with 580 additions and 82 deletions
+45 -6
View File
@@ -32,6 +32,7 @@ class AudioManagerModule extends BaseModule {
this.ttsQueueEmpty = true;
this.pendingMusicPlayback = null;
this.currentMusicState = null;
this.mediaPreloadTimeoutMs = 60000;
this.assetRoots = {
images: '/images/',
music: '/music/',
@@ -493,6 +494,10 @@ class AudioManagerModule extends BaseModule {
.then(audio => {
this.setMediaVolume(audio, this.getSfxVolume());
return audio;
})
.catch(error => {
this.sfxCache.delete(url);
throw error;
});
this.sfxCache.set(url, promise);
return promise;
@@ -505,6 +510,10 @@ class AudioManagerModule extends BaseModule {
.then(audio => {
this.setMediaVolume(audio, this.getMusicVolume());
return audio;
})
.catch(error => {
this.musicCache.delete(url);
throw error;
});
this.musicCache.set(url, promise);
return promise;
@@ -517,14 +526,24 @@ class AudioManagerModule extends BaseModule {
const finish = (result, error = null) => {
if (settled) return;
settled = true;
clearTimeout(timeoutId);
audio.removeEventListener('canplaythrough', onReady);
audio.removeEventListener('loadeddata', onReady);
audio.removeEventListener('error', onError);
if (error) reject(error);
else resolve(result);
if (error) {
audio.pause();
audio.removeAttribute('src');
audio.load();
reject(error);
} else {
resolve(result);
}
};
const onReady = () => finish(audio);
const onError = () => finish(null, new Error(`Failed to preload ${label}: ${url}`));
const timeoutId = setTimeout(() => {
finish(null, new Error(`Timed out preloading ${label}: ${url}`));
}, this.mediaPreloadTimeoutMs);
audio.preload = 'auto';
audio.addEventListener('canplaythrough', onReady, { once: true });
audio.addEventListener('loadeddata', onReady, { once: true });
@@ -538,16 +557,36 @@ class AudioManagerModule extends BaseModule {
if (this.imageCache.has(url)) return this.imageCache.get(url);
const promise = new Promise((resolve, reject) => {
const image = new Image();
let settled = false;
const finish = (result, error = null) => {
if (settled) return;
settled = true;
clearTimeout(timeoutId);
image.onload = null;
image.onerror = null;
if (error) {
image.src = '';
reject(error);
} else {
resolve(result);
}
};
image.decoding = 'async';
image.onload = () => {
if (typeof image.decode === 'function') {
image.decode().catch(() => null).then(() => resolve(image));
image.decode().catch(() => null).then(() => finish(image));
} else {
resolve(image);
finish(image);
}
};
image.onerror = () => reject(new Error(`Failed to preload image: ${url}`));
image.onerror = () => finish(null, new Error(`Failed to preload image: ${url}`));
const timeoutId = setTimeout(() => {
finish(null, new Error(`Timed out preloading image: ${url}`));
}, this.mediaPreloadTimeoutMs);
image.src = url;
}).catch(error => {
this.imageCache.delete(url);
throw error;
});
this.imageCache.set(url, promise);
return promise;
@@ -571,7 +610,7 @@ class AudioManagerModule extends BaseModule {
throw error;
}));
await Promise.all(tasks);
return Promise.all(tasks);
}
handleMediaCue(cue) {