mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-10 23:25:52 +00:00
Decode inlined sounds directly instead of fetching them, with the test.
This commit is contained in:
+47
-7
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user