import { useCallback, useEffect, useRef, useState } from "react"; import { Mic, MicOff, AlertCircle } from "lucide-react"; export interface AudioRecorderProps { onChunk: (chunk: ArrayBuffer) => void; onEndUtterance: () => void; disabled?: boolean; } type RecorderState = "idle" | "recording" | "unsupported"; const PREFERRED_MIME = [ "audio/webm;codecs=opus", "audio/webm", "audio/ogg;codecs=opus", "audio/mp4", ]; function getSupportedMimeType(): string | null { if (typeof MediaRecorder === "undefined") return null; for (const mime of PREFERRED_MIME) { if (MediaRecorder.isTypeSupported(mime)) return mime; } return null; } export default function AudioRecorder({ onChunk, onEndUtterance, disabled }: AudioRecorderProps) { const [state, setState] = useState("idle"); const [permissionDenied, setPermissionDenied] = useState(false); const mediaRecorderRef = useRef(null); const streamRef = useRef(null); const mimeType = useRef(null); // Check support on mount useEffect(() => { if ( typeof navigator.mediaDevices?.getUserMedia === "undefined" || getSupportedMimeType() === null ) { setState("unsupported"); } mimeType.current = getSupportedMimeType(); }, []); const startRecording = useCallback(async () => { if (state !== "idle" || disabled) return; try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); streamRef.current = stream; const recorder = new MediaRecorder(stream, { mimeType: mimeType.current ?? undefined, audioBitsPerSecond: 16000, }); recorder.ondataavailable = async (e) => { if (e.data.size > 0) { const buf = await e.data.arrayBuffer(); onChunk(buf); } }; recorder.start(100); // 100ms chunks mediaRecorderRef.current = recorder; setState("recording"); } catch (err: unknown) { if (err instanceof DOMException && err.name === "NotAllowedError") { setPermissionDenied(true); } } }, [state, disabled, onChunk]); const stopRecording = useCallback(() => { if (state !== "recording") return; const recorder = mediaRecorderRef.current; if (recorder && recorder.state !== "inactive") { recorder.onstop = () => { onEndUtterance(); }; recorder.stop(); } streamRef.current?.getTracks().forEach((t) => t.stop()); streamRef.current = null; mediaRecorderRef.current = null; setState("idle"); }, [state, onEndUtterance]); // Cleanup on unmount useEffect(() => { return () => { streamRef.current?.getTracks().forEach((t) => t.stop()); }; }, []); if (state === "unsupported") { return (
Audio tidak didukung browser ini
); } if (permissionDenied) { return (
Izin mikrofon ditolak
); } const isRecording = state === "recording"; return ( ); }