2026-06-30 22:34:17 -04:00
|
|
|
|
import { MatrixClient } from 'matrix-js-sdk';
|
|
|
|
|
|
import { downloadMedia, mxcUrlToHttp } from './matrix';
|
|
|
|
|
|
|
2026-07-01 23:21:50 -04:00
|
|
|
|
// [P5-15 v2] Shared media helpers for the soundboard. Clip storage/metadata now
|
|
|
|
|
|
// lives in the soundboard pack plugin (plugins/soundboard); this module only
|
|
|
|
|
|
// handles resolving an mxc clip for playback + local preview.
|
2026-06-30 22:34:17 -04:00
|
|
|
|
|
|
|
|
|
|
export const SOUNDBOARD_NAME_MAX = 24;
|
|
|
|
|
|
/** Keep clips short: they publish to every peer and hold a track open. */
|
|
|
|
|
|
export const SOUNDBOARD_MAX_CLIP_BYTES = 1024 * 1024; // 1 MB
|
|
|
|
|
|
export const SOUNDBOARD_MAX_CLIPS = 40;
|
|
|
|
|
|
export const SOUNDBOARD_ACCEPT = 'audio/mpeg,audio/ogg,audio/wav,audio/webm,audio/mp4,audio/aac';
|
|
|
|
|
|
|
|
|
|
|
|
// Cache resolved object URLs per mxc so re-triggering a clip doesn't re-download
|
|
|
|
|
|
// it. Object URLs live for the page session; the set is tiny (<= MAX_CLIPS).
|
|
|
|
|
|
const objectUrlCache = new Map<string, string>();
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Resolve an mxc clip to a `blob:` object URL the Element Call widget can fetch
|
|
|
|
|
|
* without credentials. Authenticated media (MSC3916) can't be fetched from the
|
|
|
|
|
|
* widget's realm, so the host downloads it (auth handled by the service worker)
|
|
|
|
|
|
* and hands the widget a same-session blob URL instead.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const resolveClipObjectUrl = async (mx: MatrixClient, mxcUrl: string): Promise<string> => {
|
|
|
|
|
|
const cached = objectUrlCache.get(mxcUrl);
|
|
|
|
|
|
if (cached) return cached;
|
|
|
|
|
|
|
|
|
|
|
|
const httpUrl = mxcUrlToHttp(mx, mxcUrl, true);
|
|
|
|
|
|
if (!httpUrl) throw new Error('invalid mxc url');
|
|
|
|
|
|
const blob = await downloadMedia(httpUrl);
|
|
|
|
|
|
const objectUrl = URL.createObjectURL(blob);
|
|
|
|
|
|
objectUrlCache.set(mxcUrl, objectUrl);
|
|
|
|
|
|
return objectUrl;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Play a resolved clip locally so the person who pressed it gets immediate
|
|
|
|
|
|
* feedback — LiveKit doesn't loop a participant's own published track back to
|
|
|
|
|
|
* them, so without this the presser would hear nothing. `volume` is 0–1.
|
2026-07-01 23:21:50 -04:00
|
|
|
|
* Returns the audio element so callers can track when it ends (or undefined if
|
|
|
|
|
|
* playback couldn't start).
|
2026-06-30 22:34:17 -04:00
|
|
|
|
*/
|
2026-07-01 23:21:50 -04:00
|
|
|
|
export const playClipLocally = (
|
|
|
|
|
|
objectUrl: string,
|
|
|
|
|
|
volume: number,
|
|
|
|
|
|
): HTMLAudioElement | undefined => {
|
2026-06-30 22:34:17 -04:00
|
|
|
|
try {
|
|
|
|
|
|
const audio = new Audio(objectUrl);
|
|
|
|
|
|
audio.volume = Math.max(0, Math.min(1, volume));
|
|
|
|
|
|
audio.play().catch(() => undefined);
|
2026-07-01 23:21:50 -04:00
|
|
|
|
return audio;
|
2026-06-30 22:34:17 -04:00
|
|
|
|
} catch {
|
2026-07-01 23:21:50 -04:00
|
|
|
|
return undefined;
|
2026-06-30 22:34:17 -04:00
|
|
|
|
}
|
|
|
|
|
|
};
|