feat(soundboard): hotkeys fire outside calls with local-only playback
Lifts the `state.kind === 'connected'` guard from the soundboard hotkey useEffect so OS-level shortcuts are always registered. Inside a call the existing `playSoundboard` path routes audio into the LiveKit pipeline so peers hear; outside a call the new `playSoundboardLocal` helper fetches the blob via `getSoundBlob`, creates a short-lived object URL, and plays through a fresh HTMLAudioElement on the system default output. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -98,6 +98,7 @@ import {
|
|||||||
subscribeScreenShareVolumes,
|
subscribeScreenShareVolumes,
|
||||||
} from '../lib/screenShareVolumes';
|
} from '../lib/screenShareVolumes';
|
||||||
import { startSoundboardHotkeys } from '../lib/soundboardHotkeys';
|
import { startSoundboardHotkeys } from '../lib/soundboardHotkeys';
|
||||||
|
import { playSoundboardLocal } from '../lib/soundboardLocalPlay';
|
||||||
import { playEntry } from '../lib/soundboardPlayback';
|
import { playEntry } from '../lib/soundboardPlayback';
|
||||||
import {
|
import {
|
||||||
getPrefs as getSoundboardPrefs,
|
getPrefs as getSoundboardPrefs,
|
||||||
@@ -2257,12 +2258,22 @@ export function CallProvider({ children }: { children: ReactNode }) {
|
|||||||
pipelineRef.current?.setMonitorGain(prefs.monitorGain);
|
pipelineRef.current?.setMonitorGain(prefs.monitorGain);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Global soundboard hotkey registration — runs only while connected so the
|
// Global soundboard hotkey registration — always-on so the OS-level
|
||||||
// OS-level shortcuts don't fire when the user is outside of a call.
|
// shortcuts fire even outside a call (Stream-Deck-style local SFX). Inside
|
||||||
|
// a call we route through `playSoundboard` so peers hear; outside a call
|
||||||
|
// we fall back to `playSoundboardLocal` which plays through the system
|
||||||
|
// default output only.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.kind !== 'connected') return;
|
|
||||||
const teardown = startSoundboardHotkeys((id) => {
|
const teardown = startSoundboardHotkeys((id) => {
|
||||||
void playSoundboard(id);
|
if (state.kind === 'connected') {
|
||||||
|
void playSoundboard(id);
|
||||||
|
} else {
|
||||||
|
void (async () => {
|
||||||
|
const entries = await listSoundboard();
|
||||||
|
const entry = entries.find((e) => e.id === id);
|
||||||
|
if (entry) await playSoundboardLocal(entry);
|
||||||
|
})();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return teardown;
|
return teardown;
|
||||||
}, [state.kind, playSoundboard]);
|
}, [state.kind, playSoundboard]);
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// Plays a soundboard entry to the local default audio output. Used when no
|
||||||
|
// call pipeline is active (the in-call path routes via the LiveKit
|
||||||
|
// publishing pipeline so peers hear; this path is local-only). Fetches the
|
||||||
|
// blob via getSoundBlob and creates a short-lived object URL for the audio
|
||||||
|
// element.
|
||||||
|
|
||||||
|
import { getSoundBlob, type SoundboardEntry } from './soundboardStorage';
|
||||||
|
|
||||||
|
const activeAudios = new Set<HTMLAudioElement>();
|
||||||
|
|
||||||
|
export async function playSoundboardLocal(entry: SoundboardEntry): Promise<void> {
|
||||||
|
const blob = await getSoundBlob(entry.id);
|
||||||
|
if (!blob) return;
|
||||||
|
const src = URL.createObjectURL(blob);
|
||||||
|
const el = new Audio(src);
|
||||||
|
// Use the per-entry gain as local volume. SoundboardEntry exposes `gain`
|
||||||
|
// (0..1) which mirrors the value used in the in-call pipeline.
|
||||||
|
el.volume = Math.max(0, Math.min(1, entry.gain));
|
||||||
|
activeAudios.add(el);
|
||||||
|
const cleanup = () => {
|
||||||
|
activeAudios.delete(el);
|
||||||
|
URL.revokeObjectURL(src);
|
||||||
|
};
|
||||||
|
el.addEventListener('ended', cleanup);
|
||||||
|
el.addEventListener('error', cleanup);
|
||||||
|
try {
|
||||||
|
await el.play();
|
||||||
|
} catch (err) {
|
||||||
|
cleanup();
|
||||||
|
console.warn('soundboardLocalPlay failed', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopSoundboardLocal(): void {
|
||||||
|
for (const el of activeAudios) {
|
||||||
|
try {
|
||||||
|
el.pause();
|
||||||
|
el.currentTime = 0;
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activeAudios.clear();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user