73 lines
2.6 KiB
TypeScript
73 lines
2.6 KiB
TypeScript
import { MatrixClient } from 'matrix-js-sdk';
|
||||
|
|
import { downloadMedia, mxcUrlToHttp } from './matrix';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* [P5-15] A user-uploaded soundboard clip. Stored (as a list) in the
|
|||
|
|
* `io.lotus.soundboard` account data event, so clips sync across a user's
|
|||
|
|
* devices exactly like custom emoji / sticker packs.
|
|||
|
|
*/
|
|||
|
|
export type SoundboardClip = {
|
|||
|
|
/** Stable local id (not shared with peers). */
|
|||
|
|
id: string;
|
|||
|
|
/** Display name / shortcode shown on the tile. */
|
|||
|
|
name: string;
|
|||
|
|
/** mxc:// URI of the uploaded audio. */
|
|||
|
|
url: string;
|
|||
|
|
mimetype?: string;
|
|||
|
|
size?: number;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export type SoundboardContent = {
|
|||
|
|
clips?: SoundboardClip[];
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
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.
|
|||
|
|
*/
|
|||
|
|
export const playClipLocally = (objectUrl: string, volume: number): void => {
|
|||
|
|
try {
|
|||
|
|
const audio = new Audio(objectUrl);
|
|||
|
|
audio.volume = Math.max(0, Math.min(1, volume));
|
|||
|
|
audio.play().catch(() => undefined);
|
|||
|
|
} catch {
|
|||
|
|
/* best effort */
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export const readSoundboardClips = (mx: MatrixClient): SoundboardClip[] => {
|
|||
|
|
const content = mx.getAccountData('io.lotus.soundboard' as never)?.getContent() as
|
|||
|
|
| SoundboardContent
|
|||
|
|
| undefined;
|
|||
|
|
return Array.isArray(content?.clips) ? content.clips : [];
|
|||
|
|
};
|