Spaces:
Running
Running
File size: 3,912 Bytes
bf04727 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | "use client";
import { useState, useRef, useCallback, useEffect } from "react";
export type RecorderState = "idle" | "requesting" | "recording" | "stopped" | "error";
export interface UseAudioRecorderReturn {
state: RecorderState;
seconds: number;
audioBlob: Blob | null;
analyserNode: AnalyserNode | null;
startRecording: () => Promise<void>;
stopRecording: () => void;
reset: () => void;
error: string | null;
}
export function useAudioRecorder(maxSeconds = 60): UseAudioRecorderReturn {
const [state, setState] = useState<RecorderState>("idle");
const [seconds, setSeconds] = useState(0);
const [audioBlob, setAudioBlob] = useState<Blob | null>(null);
const [analyserNode, setAnalyserNode] = useState<AnalyserNode | null>(null);
const [error, setError] = useState<string | null>(null);
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const chunksRef = useRef<Blob[]>([]);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const audioContextRef = useRef<AudioContext | null>(null);
const streamRef = useRef<MediaStream | null>(null);
const cleanup = useCallback(() => {
if (timerRef.current) clearInterval(timerRef.current);
if (audioContextRef.current && audioContextRef.current.state !== "closed") {
audioContextRef.current.close();
audioContextRef.current = null;
}
if (streamRef.current) {
streamRef.current.getTracks().forEach((t) => t.stop());
streamRef.current = null;
}
setAnalyserNode(null);
}, []);
const startRecording = useCallback(async () => {
setState("requesting");
setError(null);
chunksRef.current = [];
setAudioBlob(null);
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
streamRef.current = stream;
// Set up Web Audio API for visualisation
const audioContext = new AudioContext();
audioContextRef.current = audioContext;
const source = audioContext.createMediaStreamSource(stream);
const analyser = audioContext.createAnalyser();
analyser.fftSize = 256;
source.connect(analyser);
setAnalyserNode(analyser);
const mediaRecorder = new MediaRecorder(stream, {
mimeType: MediaRecorder.isTypeSupported("audio/webm;codecs=opus")
? "audio/webm;codecs=opus"
: "audio/webm",
});
mediaRecorderRef.current = mediaRecorder;
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) chunksRef.current.push(e.data);
};
mediaRecorder.onstop = () => {
const blob = new Blob(chunksRef.current, { type: "audio/webm" });
setAudioBlob(blob);
cleanup();
};
mediaRecorder.start(250);
setState("recording");
setSeconds(0);
timerRef.current = setInterval(() => {
setSeconds((prev) => {
if (prev + 1 >= maxSeconds) {
mediaRecorderRef.current?.stop();
if (timerRef.current) clearInterval(timerRef.current);
setState("stopped");
return maxSeconds;
}
return prev + 1;
});
}, 1000);
} catch (err) {
setError("Microphone access denied. Please allow microphone access and try again.");
setState("error");
}
}, [cleanup, maxSeconds]);
const stopRecording = useCallback(() => {
if (mediaRecorderRef.current && state === "recording") {
mediaRecorderRef.current.stop();
if (timerRef.current) clearInterval(timerRef.current);
setState("stopped");
}
}, [state]);
const reset = useCallback(() => {
cleanup();
setState("idle");
setSeconds(0);
setAudioBlob(null);
setError(null);
chunksRef.current = [];
}, [cleanup]);
useEffect(() => () => cleanup(), [cleanup]);
return { state, seconds, audioBlob, analyserNode, startRecording, stopRecording, reset, error };
}
|