Start Recording
"use client"
import { useCallback, useEffect, useRef, useState } from "react"
import { Disc, Pause, Play, Trash2 } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { LiveWaveform } from "@/components/ui/live-waveform"
import { MicSelector } from "@/components/ui/mic-selector"
import { Separator } from "@/components/ui/separator"
type RecordingState = "idle" | "loading" | "recording" | "recorded" | "playing"
export function MicSelectorDemo() {
const [selectedDevice, setSelectedDevice] = useState<string>("")
const [isMuted, setIsMuted] = useState(false)
const [state, setState] = useState<RecordingState>("idle")
const [audioBlob, setAudioBlob] = useState<Blob | null>(null)
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
const audioChunksRef = useRef<Blob[]>([])
const audioElementRef = useRef<HTMLAudioElement | null>(null)
const startRecording = useCallback(async () => {
try {
setState("loading")
const stream = await navigator.mediaDevices.getUserMedia({
audio: selectedDevice ? { deviceId: { exact: selectedDevice } } : true,
})
const mediaRecorder = new MediaRecorder(stream)
mediaRecorderRef.current = mediaRecorder
audioChunksRef.current = []
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunksRef.current.push(event.data)
}
}
mediaRecorder.onstop = () => {
const blob = new Blob(audioChunksRef.current, { type: "audio/webm" })
setAudioBlob(blob)
stream.getTracks().forEach((track) => track.stop())
setState("recorded")
}
mediaRecorder.start()
setState("recording")
} catch (error) {
console.error("Error starting recording:", error)
setState("idle")
}
}, [selectedDevice])
const stopRecording = useCallback(() => {
if (mediaRecorderRef.current && state === "recording") {
mediaRecorderRef.current.stop()
}
}, [state])
const playRecording = useCallback(() => {
if (!audioBlob) return
const audio = new Audio(URL.createObjectURL(audioBlob))
audioElementRef.current = audio
audio.onended = () => {
setState("recorded")
}
audio.play()
setState("playing")
}, [audioBlob])
const pausePlayback = useCallback(() => {
if (audioElementRef.current) {
audioElementRef.current.pause()
setState("recorded")
}
}, [])
const restart = useCallback(() => {
if (audioElementRef.current) {
audioElementRef.current.pause()
audioElementRef.current = null
}
setAudioBlob(null)
audioChunksRef.current = []
setState("idle")
}, [])
// Stop recording when muted
useEffect(() => {
if (isMuted && state === "recording") {
stopRecording()
}
}, [isMuted, state, stopRecording])
// Cleanup on unmount
useEffect(() => {
return () => {
if (mediaRecorderRef.current) {
mediaRecorderRef.current.stop()
}
if (audioElementRef.current) {
audioElementRef.current.pause()
}
}
}, [])
const showWaveform = state === "recording" && !isMuted
const showProcessing = state === "loading" || state === "playing"
const showRecorded = state === "recorded"
return (
<div className="flex min-h-[200px] w-full items-center justify-center p-4">
<Card className="m-0 w-full max-w-2xl border p-0 shadow-lg">
<div className="flex items-center justify-between gap-2 p-2">
<div className="h-8 w-[120px] md:h-10 md:w-[200px]">
<div
className={cn(
"flex h-full items-center gap-2 rounded-md py-1",
"bg-foreground/5 text-foreground/70"
)}
>
<div className="h-full flex-1">
<div className="relative flex h-full w-full shrink-0 items-center justify-center overflow-hidden rounded-sm">
<LiveWaveform
key={state}
active={showWaveform}
processing={showProcessing}
deviceId={selectedDevice}
barWidth={3}
barGap={1}
barRadius={4}
fadeEdges={true}
fadeWidth={24}
sensitivity={1.8}
smoothingTimeConstant={0.85}
height={20}
mode="scrolling"
className={cn(
"h-full w-full transition-opacity duration-300",
state === "idle" && "opacity-0"
)}
/>
{state === "idle" && (
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-foreground/50 text-[10px] font-medium">
Start Recording
</span>
</div>
)}
{showRecorded && (
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-foreground/50 text-[10px] font-medium">
Ready to Play
</span>
</div>
)}
</div>
</div>
</div>
</div>
<div className="flex items-center">
<MicSelector
value={selectedDevice}
onValueChange={setSelectedDevice}
muted={isMuted}
onMutedChange={setIsMuted}
disabled={state === "recording" || state === "loading"}
/>
<Separator orientation="vertical" className="mx-1 -my-2.5" />
{state === "idle" && (
<Button
variant="ghost"
size="icon"
onClick={startRecording}
disabled={isMuted}
aria-label="Start recording"
>
<Disc className="h-5 w-5" />
</Button>
)}
{(state === "loading" || state === "recording") && (
<Button
variant="ghost"
size="icon"
onClick={stopRecording}
disabled={state === "loading"}
aria-label="Stop recording"
>
<Pause className="h-5 w-5" />
</Button>
)}
{showRecorded && (
<Button
variant="ghost"
size="icon"
onClick={playRecording}
aria-label="Play recording"
>
<Play className="h-5 w-5" />
</Button>
)}
{state === "playing" && (
<Button
variant="ghost"
size="icon"
onClick={pausePlayback}
aria-label="Pause playback"
>
<Pause className="h-5 w-5" />
</Button>
)}
<Separator orientation="vertical" className="mx-1 -my-2.5" />
<Button
variant="ghost"
size="icon"
onClick={restart}
disabled={
state === "idle" || state === "loading" || state === "recording"
}
aria-label="Delete recording"
>
<Trash2 className="h-5 w-5" />
</Button>
</div>
</div>
</Card>
</div>
)
}
Installation
pnpm dlx @elevenlabs/agents-cli@latest components add mic-selector
Usage
import { MicSelector } from "@/components/ui/mic-selector"
Basic Usage
<MicSelector />
Controlled
const [selectedDevice, setSelectedDevice] = useState("")
<MicSelector value={selectedDevice} onValueChange={setSelectedDevice} />
With Mute Control
const [selectedDevice, setSelectedDevice] = useState("")
const [isMuted, setIsMuted] = useState(false)
<MicSelector
value={selectedDevice}
onValueChange={setSelectedDevice}
muted={isMuted}
onMutedChange={setIsMuted}
/>
Custom Styling
<MicSelector className="w-full max-w-md" />
Using the Hook
import { useAudioDevices } from "@/components/ui/mic-selector"
const { devices, loading, error, hasPermission, loadDevices } =
useAudioDevices()
// Access available microphones
devices.map((device) => console.log(device.label, device.deviceId))
API Reference
MicSelector
A dropdown selector for choosing audio input devices with live waveform preview.
Props
Prop | Type | Description |
---|---|---|
value | string | Selected device ID (controlled) |
onValueChange | (deviceId: string) => void | Callback when device selection changes |
muted | boolean | Mute state (controlled) |
onMutedChange | (muted: boolean) => void | Callback when mute state changes |
disabled | boolean | Disables the selector dropdown |
className | string | Optional CSS classes for the container |
useAudioDevices
A hook for managing audio input devices.
Returns
Property | Type | Description |
---|---|---|
devices | AudioDevice[] | Array of available audio input devices |
loading | boolean | Whether devices are being loaded |
error | string | null | Error message if device loading failed |
hasPermission | boolean | Whether microphone permission was granted |
loadDevices | () => Promise<void> | Function to request permission and reload |
AudioDevice Type
interface AudioDevice {
deviceId: string
label: string
groupId: string
}
Features
- Device Management: Automatically detects and lists available microphones
- Live Preview: Real-time audio waveform visualization when dropdown is open
- Mute Toggle: Control preview audio on/off with controlled or uncontrolled state
- Permission Handling: Gracefully handles microphone permissions
- Auto-selection: Automatically selects first available device
- Device Changes: Listens for device connection/disconnection events
- Clean Labels: Automatically removes device metadata from labels
- Flexible Control: Works in both controlled and uncontrolled modes for device selection and mute state
Notes
- Uses the
LiveWaveform
component for audio visualization - Automatically requests microphone permissions when opening dropdown
- Preview shows scrolling waveform of microphone input
- Device list updates automatically when devices are connected/disconnected
- Works in both controlled and uncontrolled modes for device selection and mute state
- Mute state can be controlled from parent component for integration with recording controls
- Can be disabled during active recording or other operations
- Cleans up audio streams properly on unmount
Deploy and Scale Agents with ElevenLabs
ElevenLabs delivers the infrastructure and developer experience you need to ship reliable audio & agent applications at scale.
Talk to an expert