feat: soundboard, account menu, and call timer
OpenSSF Scorecard / Scorecard analysis (push) Waiting to run
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Waiting to run
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
Soundboard: the trigger travels over the WebSocket and every client in the call plays the clip locally, instead of mixing it into the presser's microphone or publishing a LiveKit track. No upstream bandwidth, no media stack changes, and the clip is not degraded by voice processing. Fan-out uses a new sendToRoomParticipants rather than sendToRoom: the latter broadcasts a space room to the whole space, which is right for the presence the sidebar shows and wrong for anything audible. The cooldown is enforced server-side — a client-side one only slows down people not trying to abuse it, and a soundboard is the easiest thing here to turn into a weapon. Playing is open to anyone in the call; deciding what the buttons are needs MANAGE_SPACE. Account menu: the name in the user bar had cursor-pointer and no handler, so the interface was already promising a click that did nothing. Offers profile, status and copy-id — not the Clips or account switching the reference design shows, which would be dead UI here. Call timer: startedAt comes from the server, so a late joiner sees the call's age rather than their own arrival. Empty space rooms are destroyed already, which is what makes the next call start from zero — no reset logic needed.
This commit is contained in:
@@ -185,6 +185,34 @@ export class AudioManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays a sound from an arbitrary URL (soundboard clips, which live in the
|
||||
* upload directory rather than /sounds). Cached by URL like the built-in
|
||||
* effects, so repeats do not re-download.
|
||||
*/
|
||||
async playUrl(url: string, options: { volume?: number } = {}): Promise<void> {
|
||||
try {
|
||||
const ctx = this.ensureContext();
|
||||
await this.resumeContext();
|
||||
let buffer = this.soundBuffers.get(url);
|
||||
if (!buffer) {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) return;
|
||||
buffer = await ctx.decodeAudioData(await response.arrayBuffer());
|
||||
this.soundBuffers.set(url, buffer);
|
||||
}
|
||||
const source = ctx.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
const gain = ctx.createGain();
|
||||
gain.gain.value = options.volume ?? 1;
|
||||
source.connect(gain);
|
||||
gain.connect(this.getMasterOutput());
|
||||
source.start(0);
|
||||
} catch (err) {
|
||||
console.warn('[AudioManager] playUrl failed', err);
|
||||
}
|
||||
}
|
||||
|
||||
async playSound(name: string, options: { loop?: boolean; volume?: number } = {}): Promise<AudioBufferSourceNode | null> {
|
||||
await this.resumeContext();
|
||||
const buffer = await this.loadSound(name);
|
||||
|
||||
Reference in New Issue
Block a user