import { useCallback, useEffect, useRef } from "react"; import { useComposeStore } from "@/stores/compose-store"; const VIDEO_PREFERRED_MIME = "video/webm;codecs=vp9,opus"; const VIDEO_FALLBACK_MIME = "video/webm"; const AUDIO_PREFERRED_MIME = "audio/webm;codecs=opus"; const AUDIO_FALLBACK_MIME = "audio/webm"; function getMediaMime(mode: "video" | "audio"): string { if (mode === "audio") { return MediaRecorder.isTypeSupported(AUDIO_PREFERRED_MIME) ? AUDIO_PREFERRED_MIME : AUDIO_FALLBACK_MIME; } return MediaRecorder.isTypeSupported(VIDEO_PREFERRED_MIME) ? VIDEO_PREFERRED_MIME : VIDEO_FALLBACK_MIME; } /** * Manages MediaRecorder lifecycle and writes results to compose-store. * * Does NOT handle uploads or particle creation — that responsibility * belongs to the configure step after the user finalizes stream metadata. */ export function useRecorder() { const recorderRef = useRef(null); const streamRef = useRef(null); const chunksRef = useRef([]); const startTimeRef = useRef(0); const recordingMode = useComposeStore((s) => s.recordingMode); const setMediaStream = useComposeStore((s) => s.setMediaStream); const setError = useComposeStore((s) => s.setError); const finishRecording = useComposeStore((s) => s.finishRecording); const stopTracks = useCallback(() => { streamRef.current?.getTracks().forEach((t) => t.stop()); streamRef.current = null; recorderRef.current = null; chunksRef.current = []; setMediaStream(null); }, [setMediaStream]); const startRecording = useCallback(async () => { try { const constraints = recordingMode === "video" ? { video: true, audio: true } : { audio: true }; const mediaStream = await navigator.mediaDevices.getUserMedia(constraints); streamRef.current = mediaStream; setMediaStream(mediaStream); chunksRef.current = []; startTimeRef.current = Date.now(); const mime = getMediaMime(recordingMode); const recorder = new MediaRecorder(mediaStream, { mimeType: mime }); recorderRef.current = recorder; recorder.ondataavailable = (e) => { if (e.data.size > 0) chunksRef.current.push(e.data); }; recorder.onstop = () => { const durationMs = Date.now() - startTimeRef.current; const blob = new Blob(chunksRef.current, { type: mime }); stopTracks(); if (blob.size > 0) { finishRecording(blob, durationMs); } }; recorder.start(); } catch (err) { stopTracks(); setError( err instanceof Error ? err.message : "Failed to start recording", ); } }, [recordingMode, setMediaStream, setError, finishRecording, stopTracks]); const stopRecording = useCallback(() => { if (recorderRef.current?.state === "recording") { recorderRef.current.stop(); } }, []); const cancelRecording = useCallback(() => { if (recorderRef.current) { recorderRef.current.ondataavailable = null; recorderRef.current.onstop = null; if (recorderRef.current.state === "recording") { recorderRef.current.stop(); } } stopTracks(); }, [stopTracks]); useEffect(() => { return () => stopTracks(); }, [stopTracks]); return { startRecording, stopRecording, cancelRecording }; }