Fiber UI LogoFiberUI

useTrackToggle

A hook for muting/unmuting audio and video tracks

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">
                {isActive && isVideoEnabled ? (
                    <video
                        ref={videoRef}
                        autoPlay
                        playsInline
                        muted
                        className="h-full w-full scale-x-[-1] object-cover"
                    />
                ) : (
                    <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">
                {isActive && isVideoEnabled ? (
                    <video
                        ref={videoRef}
                        autoPlay
                        playsInline
                        muted
                        className="h-full w-full scale-x-[-1] object-cover"
                    />
                ) : (
                    <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";

/**
 * 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,
): UseTrackToggleReturn {
    const [isAudioEnabled, setIsAudioEnabled] = useState(true);
    const [isVideoEnabled, setIsVideoEnabled] = useState(true);

    // 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);
        }
        if (videoTrack) {
            setIsVideoEnabled(videoTrack.enabled);
        }
    }, [stream]);

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

            const audioTracks = stream.getAudioTracks();
            audioTracks.forEach((track) => {
                track.enabled = enabled;
            });
            setIsAudioEnabled(enabled);
        },
        [stream],
    );

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

            const videoTracks = stream.getVideoTracks();
            videoTracks.forEach((track) => {
                track.enabled = enabled;
            });
            setIsVideoEnabled(enabled);
        },
        [stream],
    );

    // Toggle audio
    const toggleAudio = useCallback(() => {
        setAudioEnabled(!isAudioEnabled);
    }, [isAudioEnabled, setAudioEnabled]);

    // Toggle video
    const toggleVideo = useCallback(() => {
        setVideoEnabled(!isVideoEnabled);
    }, [isVideoEnabled, setVideoEnabled]);

    // 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;