Fiber UI LogoFiberUI

useTrackToggle

A media control hook for muting and unmuting audio or video tracks. Toggles media availability without stopping the underlying stream, ideal for meeting controls.

Installation

npx shadcn@latest add https://r.fiberui.com/r/hooks/use-track-toggle.json

A React hook for controlling the enabled state of audio and video tracks. Provides mute/unmute functionality for media streams.

Source Code

View the full hook implementation in the Hook Source Code section below.

Instant Toggle

Unlike stopping tracks, toggling enabled is instant and doesn't require re-acquiring the stream.

Features

  • Instant Mute - Toggle track enabled state without stopping
  • Independent Controls - Separate audio and video toggles
  • Bulk Operations - muteAll() and unmuteAll() helpers
  • State Sync - Automatically syncs with actual track state

Keyboard Shortcuts

Control audio/video with keyboard shortcuts:

"use client";

import { useRef, useEffect, useState } from "react";
import { useUserMedia } from "@repo/hooks/webrtc/use-user-media";
import { useTrackToggle } from "@repo/hooks/webrtc/use-track-toggle";
import { Button } from "@repo/ui/components/button";
import { Mic, MicOff, Video, VideoOff, Keyboard } from "lucide-react";

/* TRACK TOGGLE WITH KEYBOARD - Keyboard Shortcut Controls */
export const Example1 = () => {
    const videoRef = useRef<HTMLVideoElement>(null);
    const [lastAction, setLastAction] = useState<string | null>(null);

    const { stream, isActive, start, stop } = useUserMedia();
    const {
        isAudioEnabled,
        isVideoEnabled,
        toggleAudio,
        toggleVideo,
        muteAll,
    } = useTrackToggle(stream);

    // Attach stream
    useEffect(() => {
        if (videoRef.current && stream) {
            videoRef.current.srcObject = stream;
        }
    }, [stream]);

    // Keyboard shortcuts
    useEffect(() => {
        const handleKeyDown = (e: KeyboardEvent) => {
            if (!isActive) return;

            if (e.key === "m" || e.key === "M") {
                toggleAudio();
                setLastAction(isAudioEnabled ? "Muted" : "Unmuted");
            } else if (e.key === "v" || e.key === "V") {
                toggleVideo();
                setLastAction(isVideoEnabled ? "Camera Off" : "Camera On");
            } else if (e.key === "Escape") {
                muteAll();
                setLastAction("All Muted");
            }

            // Clear action after 1.5s
            setTimeout(() => setLastAction(null), 1500);
        };

        window.addEventListener("keydown", handleKeyDown);
        return () => window.removeEventListener("keydown", handleKeyDown);
    }, [
        isActive,
        isAudioEnabled,
        isVideoEnabled,
        toggleAudio,
        toggleVideo,
        muteAll,
    ]);

    return (
        <div className="flex w-full max-w-md flex-col gap-4">
            {/* Video */}
            <div className="relative aspect-video overflow-hidden rounded-lg bg-zinc-900">
                <video
                    ref={videoRef}
                    autoPlay
                    playsInline
                    muted
                    className={`h-full w-full scale-x-[-1] object-cover ${
                        !isActive || !isVideoEnabled ? "hidden" : ""
                    }`}
                />
                {(!isActive || !isVideoEnabled) && (
                    <div className="flex h-full items-center justify-center">
                        <VideoOff className="h-12 w-12 text-zinc-600" />
                    </div>
                )}

                {/* Action Toast */}
                {lastAction && (
                    <div className="absolute left-1/2 top-4 -translate-x-1/2 animate-pulse rounded-full bg-black/80 px-4 py-2 text-sm font-medium text-white">
                        {lastAction}
                    </div>
                )}
            </div>

            {/* Keyboard Shortcuts */}
            {isActive && (
                <div className="rounded-lg border bg-zinc-50 p-3 dark:bg-zinc-900">
                    <div className="mb-2 flex items-center gap-2 text-sm font-medium">
                        <Keyboard className="h-4 w-4" />
                        Keyboard Shortcuts
                    </div>
                    <div className="text-muted-foreground grid grid-cols-3 gap-2 text-xs">
                        <div className="flex items-center gap-2">
                            <kbd className="rounded bg-zinc-200 px-1.5 py-0.5 font-mono dark:bg-zinc-800">
                                M
                            </kbd>
                            <span>Mute</span>
                        </div>
                        <div className="flex items-center gap-2">
                            <kbd className="rounded bg-zinc-200 px-1.5 py-0.5 font-mono dark:bg-zinc-800">
                                V
                            </kbd>
                            <span>Video</span>
                        </div>
                        <div className="flex items-center gap-2">
                            <kbd className="rounded bg-zinc-200 px-1.5 py-0.5 font-mono dark:bg-zinc-800">
                                Esc
                            </kbd>
                            <span>Mute All</span>
                        </div>
                    </div>
                </div>
            )}

            {/* Controls */}
            <div className="flex justify-center gap-2">
                {!isActive ? (
                    <Button onClick={() => start()}>Start</Button>
                ) : (
                    <>
                        <Button
                            variant={isAudioEnabled ? "outline" : "destructive"}
                            size="icon"
                            onClick={toggleAudio}
                        >
                            {isAudioEnabled ? (
                                <Mic className="h-5 w-5" />
                            ) : (
                                <MicOff className="h-5 w-5" />
                            )}
                        </Button>
                        <Button
                            variant={isVideoEnabled ? "outline" : "destructive"}
                            size="icon"
                            onClick={toggleVideo}
                        >
                            {isVideoEnabled ? (
                                <Video className="h-5 w-5" />
                            ) : (
                                <VideoOff className="h-5 w-5" />
                            )}
                        </Button>
                        <Button variant="ghost" onClick={stop}>
                            End
                        </Button>
                    </>
                )}
            </div>
        </div>
    );
};

Meeting Controls UI

Video call style control bar:

U
"use client";

import { useRef, useEffect } from "react";
import { useUserMedia } from "@repo/hooks/webrtc/use-user-media";
import { useTrackToggle } from "@repo/hooks/webrtc/use-track-toggle";
import { Button } from "@repo/ui/components/button";
import { Mic, MicOff, Video, VideoOff, Phone, PhoneOff } from "lucide-react";

/* MEETING CONTROLS - Video Call Style Controls */
export const Example2 = () => {
    const videoRef = useRef<HTMLVideoElement>(null);
    const { stream, isActive, start, stop } = useUserMedia();
    const { isAudioEnabled, isVideoEnabled, toggleAudio, toggleVideo } =
        useTrackToggle(stream);

    // Attach stream
    useEffect(() => {
        if (videoRef.current && stream) {
            videoRef.current.srcObject = stream;
        }
    }, [stream]);

    return (
        <div className="flex w-full max-w-md flex-col gap-4">
            {/* Video */}
            <div className="relative aspect-video overflow-hidden rounded-xl bg-zinc-900">
                <video
                    ref={videoRef}
                    autoPlay
                    playsInline
                    muted
                    className={`h-full w-full scale-x-[-1] object-cover ${
                        !isActive || !isVideoEnabled ? "hidden" : ""
                    }`}
                />
                {(!isActive || !isVideoEnabled) && (
                    <div className="flex h-full items-center justify-center">
                        <div className="bg-linear-to-br flex h-20 w-20 items-center justify-center rounded-full from-blue-500 to-purple-600 text-2xl font-bold text-white">
                            U
                        </div>
                    </div>
                )}

                {/* Status Indicators */}
                {isActive && (
                    <div className="absolute left-3 top-3 flex gap-1.5">
                        {!isAudioEnabled && (
                            <div className="rounded-full bg-red-500 p-1.5">
                                <MicOff className="h-3 w-3 text-white" />
                            </div>
                        )}
                        {!isVideoEnabled && (
                            <div className="rounded-full bg-red-500 p-1.5">
                                <VideoOff className="h-3 w-3 text-white" />
                            </div>
                        )}
                    </div>
                )}

                {/* Floating Controls Bar */}
                {isActive && (
                    <div className="absolute bottom-4 left-1/2 flex -translate-x-1/2 gap-2 rounded-full bg-zinc-800/90 p-2 backdrop-blur">
                        <Button
                            variant="ghost"
                            size="icon"
                            onClick={toggleAudio}
                            className={`rounded-full ${
                                isAudioEnabled
                                    ? "hover:bg-zinc-700"
                                    : "bg-red-500 text-white hover:bg-red-600"
                            }`}
                        >
                            {isAudioEnabled ? (
                                <Mic className="h-5 w-5 text-white" />
                            ) : (
                                <MicOff className="h-5 w-5" />
                            )}
                        </Button>
                        <Button
                            variant="ghost"
                            size="icon"
                            onClick={toggleVideo}
                            className={`rounded-full ${
                                isVideoEnabled
                                    ? "hover:bg-zinc-700"
                                    : "bg-red-500 text-white hover:bg-red-600"
                            }`}
                        >
                            {isVideoEnabled ? (
                                <Video className="h-5 w-5 text-white" />
                            ) : (
                                <VideoOff className="h-5 w-5" />
                            )}
                        </Button>
                        <Button
                            variant="ghost"
                            size="icon"
                            onClick={stop}
                            className="rounded-full bg-red-500 text-white hover:bg-red-600"
                        >
                            <PhoneOff className="h-5 w-5" />
                        </Button>
                    </div>
                )}
            </div>

            {/* Join Button */}
            {!isActive && (
                <Button
                    onClick={() => start()}
                    className="mx-auto gap-2 bg-green-600 hover:bg-green-700"
                >
                    <Phone className="h-4 w-4" />
                    Join Meeting
                </Button>
            )}
        </div>
    );
};

API Reference

Hook Signature

function useTrackToggle(stream: MediaStream | null): UseTrackToggleReturn;

Return Value

PropertyTypeDescription
isAudioEnabledbooleanAudio track enabled state
isVideoEnabledbooleanVideo track enabled state
toggleAudio() => voidToggle audio on/off
toggleVideo() => voidToggle video on/off
setAudioEnabled(enabled) => voidSet audio state directly
setVideoEnabled(enabled) => voidSet video state directly
muteAll() => voidMute both audio and video
unmuteAll() => voidUnmute both audio and video

Hook Source Code

import { useState, useCallback, useEffect } from "react";

/**
 * Options for the useTrackToggle hook
 */
export interface UseTrackToggleOptions {
    /**
     * Mode for toggling tracks:
     * - 'mute': Sets track.enabled (default, keeps hardware on, allows instant toggle)
     * - 'stop': Stops track entirely (turns hardware off, requires restart via callback)
     */
    mode?: "mute" | "stop";
    /** Callback to restart video when mode is 'stop' (must return Promise) */
    onRestartVideo?: () => Promise<boolean>;
    /** Callback to restart audio when mode is 'stop' */
    onRestartAudio?: () => Promise<boolean>;
    /** Callback when video is stopped in 'stop' mode */
    onStopVideo?: () => void;
    /** Callback when audio is stopped in 'stop' mode */
    onStopAudio?: () => void;
}

/**
 * Return type for the useTrackToggle hook
 */
export interface UseTrackToggleReturn {
    /** Whether audio is currently enabled */
    isAudioEnabled: boolean;
    /** Whether video is currently enabled */
    isVideoEnabled: boolean;
    /** Toggle audio on/off */
    toggleAudio: () => void;
    /** Toggle video on/off */
    toggleVideo: () => void;
    /** Set audio enabled state directly */
    setAudioEnabled: (enabled: boolean) => void;
    /** Set video enabled state directly */
    setVideoEnabled: (enabled: boolean) => void;
    /** Mute all tracks (audio and video) */
    muteAll: () => void;
    /** Unmute all tracks (audio and video) */
    unmuteAll: () => void;
}

/**
 * A React hook for controlling the enabled state of audio/video tracks.
 * Provides mute/unmute functionality for media streams.
 *
 * @param stream - The MediaStream to control
 * @returns UseTrackToggleReturn object with toggle states and methods
 *
 * @example
 * ```tsx
 * const { stream } = useUserMedia();
 * const { isAudioEnabled, toggleAudio, toggleVideo } = useTrackToggle(stream);
 *
 * return (
 *     <>
 *         <button onClick={toggleAudio}>
 *             {isAudioEnabled ? "Mute" : "Unmute"}
 *         </button>
 *         <button onClick={toggleVideo}>
 *             {isVideoEnabled ? "Hide" : "Show"}
 *         </button>
 *     </>
 * );
 * ```
 */
export function useTrackToggle(
    stream: MediaStream | null,
    options: UseTrackToggleOptions = {},
): UseTrackToggleReturn {
    const {
        mode = "mute",
        onRestartVideo,
        onRestartAudio,
        onStopVideo,
        onStopAudio,
    } = options;

    const [isAudioEnabled, setIsAudioEnabled] = useState(true);
    const [isVideoEnabled, setIsVideoEnabled] = useState(true);
    const [isTogglingVideo, setIsTogglingVideo] = useState(false);
    const [isTogglingAudio, setIsTogglingAudio] = useState(false);

    // Sync with actual track states when stream changes
    useEffect(() => {
        if (!stream) {
            setIsAudioEnabled(true);
            setIsVideoEnabled(true);
            return;
        }

        const audioTrack = stream.getAudioTracks()[0];
        const videoTrack = stream.getVideoTracks()[0];

        if (audioTrack) {
            setIsAudioEnabled(audioTrack.enabled);
        } else {
            // No audio track means it's paused/stopped
            setIsAudioEnabled(false);
        }
        if (videoTrack) {
            setIsVideoEnabled(videoTrack.enabled);
        } else {
            // No video track means it's paused/stopped
            setIsVideoEnabled(false);
        }
    }, [stream]);

    // Set audio enabled state
    const setAudioEnabled = useCallback(
        async (enabled: boolean) => {
            if (!stream) return;

            if (mode === "stop") {
                // Stop mode: actually stop/restart tracks
                if (!enabled) {
                    const audioTracks = stream.getAudioTracks();
                    audioTracks.forEach((track) => {
                        track.stop();
                    });
                    onStopAudio?.();
                    setIsAudioEnabled(false);
                } else if (onRestartAudio) {
                    setIsTogglingAudio(true);
                    const success = await onRestartAudio();
                    setIsAudioEnabled(success);
                    setIsTogglingAudio(false);
                }
            } else {
                // Mute mode: just toggle enabled property
                const audioTracks = stream.getAudioTracks();
                audioTracks.forEach((track) => {
                    track.enabled = enabled;
                });
                setIsAudioEnabled(enabled);
            }
        },
        [stream, mode, onRestartAudio, onStopAudio],
    );

    // Set video enabled state
    const setVideoEnabled = useCallback(
        async (enabled: boolean) => {
            if (!stream) return;

            if (mode === "stop") {
                // Stop mode: actually stop/restart tracks
                if (!enabled) {
                    const videoTracks = stream.getVideoTracks();
                    videoTracks.forEach((track) => {
                        track.stop();
                    });
                    onStopVideo?.();
                    setIsVideoEnabled(false);
                } else if (onRestartVideo) {
                    setIsTogglingVideo(true);
                    const success = await onRestartVideo();
                    setIsVideoEnabled(success);
                    setIsTogglingVideo(false);
                }
            } else {
                // Mute mode: just toggle enabled property
                const videoTracks = stream.getVideoTracks();
                videoTracks.forEach((track) => {
                    track.enabled = enabled;
                });
                setIsVideoEnabled(enabled);
            }
        },
        [stream, mode, onRestartVideo, onStopVideo],
    );

    // Toggle audio
    const toggleAudio = useCallback(() => {
        if (isTogglingAudio) return; // Prevent double-toggle during async restart
        setAudioEnabled(!isAudioEnabled);
    }, [isAudioEnabled, setAudioEnabled, isTogglingAudio]);

    // Toggle video
    const toggleVideo = useCallback(() => {
        if (isTogglingVideo) return; // Prevent double-toggle during async restart
        setVideoEnabled(!isVideoEnabled);
    }, [isVideoEnabled, setVideoEnabled, isTogglingVideo]);

    // Mute all tracks
    const muteAll = useCallback(() => {
        setAudioEnabled(false);
        setVideoEnabled(false);
    }, [setAudioEnabled, setVideoEnabled]);

    // Unmute all tracks
    const unmuteAll = useCallback(() => {
        setAudioEnabled(true);
        setVideoEnabled(true);
    }, [setAudioEnabled, setVideoEnabled]);

    return {
        isAudioEnabled,
        isVideoEnabled,
        toggleAudio,
        toggleVideo,
        setAudioEnabled,
        setVideoEnabled,
        muteAll,
        unmuteAll,
    };
}

export default useTrackToggle;