Start Recording
<script setup lang="ts">
import { LiveWaveform } from '@/components/elevenlabs-ui/live-waveform'
import { MicSelector } from '@/components/elevenlabs-ui/mic-selector'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
import { cn } from '@/lib/utils'
import { Disc, Pause, Play, Trash2 } from 'lucide-vue-next'
import { computed, onUnmounted, ref, watch } from 'vue'
type RecordingState = 'idle' | 'loading' | 'recording' | 'recorded' | 'playing'
const selectedDevice = ref('')
const isMuted = ref(false)
const state = ref<RecordingState>('idle')
const audioBlob = ref<Blob | null>(null)
let mediaRecorder: MediaRecorder | null = null
let audioChunks: Blob[] = []
let audioElement: HTMLAudioElement | null = null
async function startRecording() {
try {
state.value = 'loading'
const stream = await navigator.mediaDevices.getUserMedia({
audio: selectedDevice.value ? { deviceId: { exact: selectedDevice.value } } : true,
})
mediaRecorder = new MediaRecorder(stream)
audioChunks = []
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunks.push(event.data)
}
}
mediaRecorder.onstop = () => {
const blob = new Blob(audioChunks, { type: 'audio/webm' })
audioBlob.value = blob
stream.getTracks().forEach(track => track.stop())
state.value = 'recorded'
}
mediaRecorder.start()
state.value = 'recording'
}
catch (error) {
console.error('Error starting recording:', error)
state.value = 'idle'
}
}
function stopRecording() {
if (mediaRecorder && state.value === 'recording') {
mediaRecorder.stop()
}
}
function playRecording() {
if (!audioBlob.value)
return
const audio = new Audio(URL.createObjectURL(audioBlob.value))
audioElement = audio
audio.onended = () => {
state.value = 'recorded'
}
audio.play()
state.value = 'playing'
}
function pausePlayback() {
if (audioElement) {
audioElement.pause()
state.value = 'recorded'
}
}
function restart() {
if (audioElement) {
audioElement.pause()
audioElement = null
}
audioBlob.value = null
audioChunks = []
state.value = 'idle'
}
// Stop recording when muted
watch([isMuted, state], ([muted, currentState]) => {
if (muted && currentState === 'recording') {
stopRecording()
}
})
onUnmounted(() => {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop()
}
if (audioElement) {
audioElement.pause()
}
})
const showWaveform = computed(() => state.value === 'recording' && !isMuted.value)
const showProcessing = computed(() => state.value === 'loading' || state.value === 'playing')
const showRecorded = computed(() => state.value === 'recorded')
</script>
<template>
<div class="flex min-h-[200px] w-full items-center justify-center p-4">
<Card class="m-0 w-full max-w-2xl border p-0 shadow-lg">
<div class="flex w-full flex-wrap items-center justify-between gap-2 p-2">
<div class="h-8 w-full min-w-0 flex-1 md:w-[200px] md:flex-none">
<div
:class="cn(
'flex h-full items-center gap-2 rounded-md py-1',
'bg-foreground/5 text-foreground/70',
)"
>
<div class="h-full min-w-0 flex-1">
<div class="relative flex h-full w-full shrink-0 items-center justify-center overflow-hidden rounded-sm">
<LiveWaveform
:key="state"
:active="showWaveform"
:processing="showProcessing"
:device-id="selectedDevice"
:bar-width="3"
:bar-gap="1"
:bar-radius="4"
:fade-edges="true"
:fade-width="24"
:sensitivity="1.8"
:smoothing-time-constant="0.85"
:height="20"
mode="scrolling"
:class="cn(
'h-full w-full transition-opacity duration-300',
state === 'idle' && 'opacity-0',
)"
/>
<div v-if="state === 'idle'" class="absolute inset-0 flex items-center justify-center">
<span class="text-foreground/50 text-xs font-medium">Start Recording</span>
</div>
<div v-if="showRecorded" class="absolute inset-0 flex items-center justify-center">
<span class="text-foreground/50 text-xs font-medium">Ready to Play</span>
</div>
</div>
</div>
</div>
</div>
<div class="flex w-full flex-wrap items-center justify-center gap-1 md:w-auto">
<MicSelector
v-model="selectedDevice"
v-model:muted="isMuted"
:disabled="state === 'recording' || state === 'loading'"
/>
<Separator orientation="vertical" class="mx-1 -my-2.5" />
<div class="flex">
<Button
v-if="state === 'idle'"
variant="ghost"
size="icon"
:disabled="isMuted"
aria-label="Start recording"
@click="startRecording"
>
<Disc class="size-5" />
</Button>
<Button
v-if="state === 'loading' || state === 'recording'"
variant="ghost"
size="icon"
:disabled="state === 'loading'"
aria-label="Stop recording"
@click="stopRecording"
>
<Pause class="size-5" />
</Button>
<Button
v-if="showRecorded"
variant="ghost"
size="icon"
aria-label="Play recording"
@click="playRecording"
>
<Play class="size-5" />
</Button>
<Button
v-if="state === 'playing'"
variant="ghost"
size="icon"
aria-label="Pause playback"
@click="pausePlayback"
>
<Pause class="size-5" />
</Button>
<Separator orientation="vertical" class="mx-1 -my-2.5" />
<Button
variant="ghost"
size="icon"
:disabled="['idle', 'loading', 'recording'].includes(state)"
aria-label="Delete recording"
@click="restart"
>
<Trash2 class="size-5" />
</Button>
</div>
</div>
</div>
</Card>
</div>
</template>Installation
pnpm dlx elevenlabs-ui-vue@latest add mic-selector
Usage
import { MicSelector } from "@/components/elevenlabs-ui/mic-selector"Basic Usage
<MicSelector />Controlled
<script setup lang="ts">
const selectedDevice = ref('')
</script>
<template>
<MicSelector :v-model="selectedDevice" />
</template>With Mute Control
<script setup lang="ts">
const selectedDevice = ref('')
const isMuted = ref(false)
</script>
<template>
<MicSelector
v-model="selectedDevice"
v-model:muted="isMuted"
/>
</template>Custom Styling
<MicSelector class="w-full max-w-md" />Using the Composable
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 |
|---|---|---|
| modelValue | string | Selected device ID (v-model) |
| muted | boolean | Mute state (v-model:muted) |
| disabled | boolean | Disables the selector dropdown |
| class | string | Optional CSS classes for the container |
Emits
| Event | Type | Description |
|---|---|---|
| update:modelValue | (deviceId: string) => void | Callback when device selection changes |
| update:muted | (mute: boolean) => void | Callback when nute state changes disconnects |
useAudioDevices
A composable 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
LiveWaveformcomponent 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