110 lines
4.0 KiB
TypeScript
110 lines
4.0 KiB
TypeScript
import React from 'react';
|
|
import { Track } from 'livekit-client';
|
|
import { useLocalParticipant, useTrackToggle } from '@livekit/components-react';
|
|
import { useSettingsState } from './SettingsContext';
|
|
import { KeyCommand } from './types';
|
|
|
|
export function KeyboardShortcuts() {
|
|
const { state } = useSettingsState();
|
|
const { localParticipant, isMicrophoneEnabled } = useLocalParticipant();
|
|
const { toggle: toggleMic, pending: pendingMicChange } = useTrackToggle({ source: Track.Source.Microphone });
|
|
const { toggle: toggleCamera, pending: pendingCameraChange } = useTrackToggle({ source: Track.Source.Camera });
|
|
|
|
const pttHeldRef = React.useRef(false);
|
|
|
|
React.useEffect(() => {
|
|
const handlers = Object.entries(state.keybindings)
|
|
.flatMap(([command, binding]) => {
|
|
switch (command) {
|
|
case KeyCommand.PTT:
|
|
if (!state.enablePTT || !Array.isArray(binding)) return [];
|
|
|
|
const [enable, disable] = binding;
|
|
const t = getEventTarget(enable.target);
|
|
if (!t) return null;
|
|
|
|
const on = async (event: KeyboardEvent) => {
|
|
if (enable.guard(event)) {
|
|
event.preventDefault();
|
|
if (!isMicrophoneEnabled) {
|
|
pttHeldRef.current = true;
|
|
localParticipant?.setMicrophoneEnabled(true);
|
|
}
|
|
}
|
|
};
|
|
|
|
const off = async (event: KeyboardEvent) => {
|
|
if (disable.guard(event)) {
|
|
event.preventDefault();
|
|
if (pttHeldRef.current && isMicrophoneEnabled) {
|
|
pttHeldRef.current = false;
|
|
localParticipant?.setMicrophoneEnabled(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
t.addEventListener(enable.eventName, on as any);
|
|
t.addEventListener(disable.eventName, off as any);
|
|
return [
|
|
{ eventName: enable.eventName, target: t, handler: on },
|
|
{ eventName: disable.eventName, target: t, handler: off },
|
|
];
|
|
case KeyCommand.ToggleMic:
|
|
if (!Array.isArray(binding)) {
|
|
const t = getEventTarget(binding.target);
|
|
if (!t) return null;
|
|
|
|
const handler = async (event: KeyboardEvent) => {
|
|
if (binding.guard(event) && !pendingMicChange) {
|
|
event.preventDefault();
|
|
toggleMic?.().catch(console.error);
|
|
}
|
|
};
|
|
t.addEventListener(binding.eventName, handler as any);
|
|
return { eventName: binding.eventName, target: t, handler };
|
|
}
|
|
case KeyCommand.ToggleCamera:
|
|
if (!Array.isArray(binding)) {
|
|
const t = getEventTarget(binding.target);
|
|
if (!t) return null;
|
|
|
|
const handler = async (event: KeyboardEvent) => {
|
|
if (binding.guard(event) && !pendingCameraChange) {
|
|
event.preventDefault();
|
|
toggleCamera?.().catch(console.error);
|
|
}
|
|
};
|
|
t.addEventListener(binding.eventName, handler as any);
|
|
return { eventName: binding.eventName, target: t, handler };
|
|
}
|
|
default:
|
|
return [];
|
|
}
|
|
})
|
|
.filter(Boolean) as Array<{
|
|
target: EventTarget;
|
|
eventName: string;
|
|
handler: (event: KeyboardEvent) => void;
|
|
}>;
|
|
|
|
return () => {
|
|
handlers.forEach(({ target, eventName, handler }) => {
|
|
target.removeEventListener(eventName, handler as any);
|
|
});
|
|
};
|
|
}, [state, toggleCamera, pendingCameraChange, toggleMic, pendingMicChange, localParticipant, isMicrophoneEnabled]);
|
|
|
|
return null;
|
|
}
|
|
|
|
function getEventTarget(
|
|
target: Window | Document | HTMLElement | string = window,
|
|
): EventTarget | null {
|
|
const targetElement = typeof target === 'string' ? document.querySelector(target) : target;
|
|
if (!targetElement) {
|
|
console.warn(`Target element not found for ${target}`);
|
|
return null;
|
|
}
|
|
return targetElement;
|
|
}
|