Keyboard shortcuts

Add keyboard shortcuts for common call actions like mute/unmute.

Best Practices

  • Use hotkeys-js library for cleaner shortcut registration.
  • Detect macOS with navigator.userAgent to use instead of Ctrl.
  • Place shortcut hook in same component as CallControls for logical grouping.
  • Always clean up event listeners in useEffect return function.
  • Common shortcuts: Ctrl/⌘ + D (toggle audio), Ctrl/⌘ + E (toggle video).

Define custom useKeyboardShortcuts hook

As this hook will introduce keyboard shortcuts related to CallControls (mute/unmute audio) we should ideally invoke it in the same component to group logical parts together for easier debugging later. The same goes for your other keyboard shortcuts you might introduce to your application. Learn how to adjust your CallControls component in the Replacing Call Controls guide.

import { useEffect } from "react";
import { useCallStateHooks } from "@stream-io/video-react-sdk";

const isMacOS = () => !!navigator.userAgent.match(/(Mac\s?OS)/g)?.length;

export const useKeyboardShortcuts = () => {
  const { useCameraState, useMicrophoneState } = useCallStateHooks();
  const { microphone } = useMicrophoneState();
  const { camera } = useCameraState();

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (
        e.key !== "d" ||
        (!e.metaKey && isMacOS()) ||
        (!e.ctrlKey && !isMacOS())
      )
        return;
      e.preventDefault();

      microphone.toggle().catch(console.error);
    };

    window.addEventListener("keydown", handleKeyDown);

    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [microphone]);

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (
        e.key !== "e" ||
        (!e.metaKey && isMacOS()) ||
        (!e.ctrlKey && !isMacOS())
      )
        return;
      e.preventDefault();

      camera.toggle().catch(console.error);
    };

    window.addEventListener("keydown", handleKeyDown);

    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [camera]);
};

Refactor with the use of hotkeys-js

The previous implementation looks a bit messy and hard to read so we'll refactor this hook with the help of the hotkeys-js which makes shortcut registering and handling much easier.

import { useEffect } from "react";
import hotkeys from "hotkeys-js";
import { useCallStateHooks } from "@stream-io/video-react-sdk";

const isMacOS = () => !!navigator.userAgent.match(/(Mac\s?OS)/g)?.length;

export const useKeyboardShortcuts = () => {
  const { useCameraState, useMicrophoneState } = useCallStateHooks();
  const { microphone } = useMicrophoneState();
  const { camera } = useCameraState();

  useEffect(() => {
    const shortcuts = "cmd+d,ctrl+d";
    hotkeys(shortcuts, (e, ke) => {
      if (isMacOS() && ke.shortcut !== "cmd+d") return;
      e.preventDefault();

      microphone.toggle().catch(console.error);
    });

    return () => hotkeys.unbind(shortcuts);
  }, [microphone]);

  useEffect(() => {
    const shortcuts = "cmd+e,ctrl+e";
    hotkeys(shortcuts, (e, ke) => {
      if (isMacOS() && ke.shortcut !== "cmd+e") return;
      e.preventDefault();

      camera.toggle().catch(console.error);
    });

    return () => hotkeys.unbind(shortcuts);
  }, [camera]);
};