Decode inlined sounds directly instead of fetching them, with the test.

This commit is contained in:
Timo K.
2026-09-30 11:22:15 +02:00
parent 81233e5cfb
commit 63ac1ba03e
2 changed files with 124 additions and 7 deletions
+47 -7
View File
@@ -32,6 +32,43 @@ function getPreferredAudioFormat(): "ogg" | "mp3" {
const preferredFormat = getPreferredAudioFormat();
/**
* Decode a `data:` URL into the bytes it carries.
*
* The component build inlines the sound files as `data:` URLs. Those could be
* `fetch`ed like any other URL, but that fetch is subject to the host page's
* Content Security Policy, and `connect-src` lists rarely allow `data:` (a `*`
* source does not cover it). Reading the bytes out of the URL directly needs no
* network access, so no policy can stop it.
* @param url The `data:` URL to decode.
* @returns The decoded bytes.
*/
function decodeDataUrl(url: string): ArrayBuffer {
const separator = url.indexOf(",");
if (!url.startsWith("data:") || separator === -1)
throw new Error("Not a valid data: URL");
const header = url.slice("data:".length, separator);
const payload = url.slice(separator + 1);
const decoded = header.split(";").includes("base64")
? atob(payload)
: decodeURIComponent(payload);
// `decoded` is a binary string: one char per byte
const bytes = Uint8Array.from(decoded, (c) => c.charCodeAt(0));
return bytes.buffer;
}
/**
* Load the bytes of a single sound file.
* @param url Where the sound is: an ordinary URL to fetch, or a `data:` URL.
* @returns The bytes, or null if the sound could not be loaded.
*/
async function loadSound(url: string): Promise<ArrayBuffer | null> {
if (url.startsWith("data:")) return decodeDataUrl(url);
const response = await fetch(url);
if (isFailure(response)) return null;
return await response.arrayBuffer();
}
/**
* Prefetch sounds to be used by the AudioContext. This can
* be called outside the scope of a component to ensure the
@@ -47,18 +84,21 @@ export async function prefetchSounds<S extends string>(
Object.entries(sounds).map(async ([name, file]) => {
const { mp3, ogg } = file as SoundDefinition;
// Use preferred format, fallback to ogg if no mp3 is provided.
// Load an audio file
const response = await fetch(
preferredFormat === "ogg" ? ogg : (mp3 ?? ogg),
);
if (isFailure(response)) {
const url = preferredFormat === "ogg" ? ogg : (mp3 ?? ogg);
let buffer: ArrayBuffer | null = null;
try {
buffer = await loadSound(url);
} catch (e) {
logger.warn(`Could not load sound ${name}`, e);
return;
}
if (buffer === null) {
// If the sound doesn't load, it's not the end of the world. We won't play
// the sound when requested, but it's better than failing the whole application.
logger.warn(`Could not load sound ${name}, response was not okay`);
return;
}
// Decode it
buffers[name] = await response.arrayBuffer();
buffers[name] = buffer;
}),
);
return buffers as Record<S, ArrayBuffer>;