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.jsonA 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()andunmuteAll()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:
"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
| Property | Type | Description |
|---|---|---|
isAudioEnabled | boolean | Audio track enabled state |
isVideoEnabled | boolean | Video track enabled state |
toggleAudio | () => void | Toggle audio on/off |
toggleVideo | () => void | Toggle video on/off |
setAudioEnabled | (enabled) => void | Set audio state directly |
setVideoEnabled | (enabled) => void | Set video state directly |
muteAll | () => void | Mute both audio and video |
unmuteAll | () => void | Unmute 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;
useScreenShare
A simplified hook for capturing screen content. Provides easy access to display media streams for sharing screens, windows, or browser tabs in your application.
useUserMedia
A hook for accessing and managing the user's camera and microphone. Handles permission requests, device constraints, and stream lifecycle management effortlessly.