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()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">
{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
| 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";
/**
* 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;