fix: preserve recorder blob without object-url refetch
All checks were successful
Build & Deploy to K3s / build-and-deploy (push) Successful in 35s

This commit is contained in:
ordinarthur 2026-04-11 16:45:23 +02:00
parent 6dd7d2c4e5
commit 800214976f
2 changed files with 23 additions and 42 deletions

View File

@ -9,12 +9,12 @@ import { useState, useCallback, useEffect, useRef } from "react";
export function useAudioRecorder() { export function useAudioRecorder() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isRecording, setIsRecording] = useState(false); const [isRecording, setIsRecording] = useState(false);
const [recordings, setRecordings] = useState<string[]>([]); const [currentRecording, setCurrentRecording] = useState<Blob | null>(null);
const [currentRecording, setCurrentRecording] = useState<string | null>(null);
const mediaRecorderRef = useRef<MediaRecorder | null>(null); const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const chunksRef = useRef<Blob[]>([]); const chunksRef = useRef<Blob[]>([]);
const streamRef = useRef<MediaStream | null>(null); const streamRef = useRef<MediaStream | null>(null);
const stopResolverRef = useRef<((value: { blob: Blob }) => void) | null>(null);
/** Choisit le meilleur mimeType supporté par le navigateur. */ /** Choisit le meilleur mimeType supporté par le navigateur. */
const getMimeType = useCallback(() => { const getMimeType = useCallback(() => {
@ -54,7 +54,6 @@ export function useAudioRecorder() {
const mimeType = getMimeType(); const mimeType = getMimeType();
const recorder = new MediaRecorder(stream, { const recorder = new MediaRecorder(stream, {
mimeType: mimeType || undefined, mimeType: mimeType || undefined,
audioBitsPerSecond: 128000,
}); });
recorder.ondataavailable = (e) => { recorder.ondataavailable = (e) => {
@ -66,10 +65,11 @@ export function useAudioRecorder() {
recorder.onstop = () => { recorder.onstop = () => {
const actualMime = recorder.mimeType || mimeType || "audio/webm"; const actualMime = recorder.mimeType || mimeType || "audio/webm";
const blob = new Blob(chunksRef.current, { type: actualMime }); const blob = new Blob(chunksRef.current, { type: actualMime });
const url = URL.createObjectURL(blob); setCurrentRecording(blob);
stopResolverRef.current?.({ blob });
setRecordings((prev) => [...prev, url]); stopResolverRef.current = null;
setCurrentRecording(url); mediaRecorderRef.current = null;
chunksRef.current = [];
// Arrête les pistes micro // Arrête les pistes micro
stream.getTracks().forEach((t) => t.stop()); stream.getTracks().forEach((t) => t.stop());
@ -92,26 +92,15 @@ export function useAudioRecorder() {
if (!isRecording || !mediaRecorderRef.current) return; if (!isRecording || !mediaRecorderRef.current) return;
setIsLoading(true); setIsLoading(true);
return new Promise<{ blob: Blob; url: string }>((resolve) => { return new Promise<{ blob: Blob }>((resolve) => {
const recorder = mediaRecorderRef.current!; const recorder = mediaRecorderRef.current!;
const mimeType = recorder.mimeType || getMimeType() || "audio/webm"; stopResolverRef.current = resolve;
const ext = getExtension(mimeType); recorder.requestData();
const originalOnStop = recorder.onstop;
recorder.onstop = (ev) => {
// Appelle le handler de base (qui crée le blob + currentRecording)
if (originalOnStop) originalOnStop.call(recorder, ev);
const blob = new Blob(chunksRef.current, { type: mimeType });
const url = URL.createObjectURL(blob);
setIsRecording(false);
setIsLoading(false);
resolve({ blob, url });
};
recorder.stop(); recorder.stop();
setIsRecording(false);
setIsLoading(false);
}); });
}, [isRecording, getMimeType, getExtension]); }, [isRecording]);
const toggleRecording = useCallback(async () => { const toggleRecording = useCallback(async () => {
if (isRecording) { if (isRecording) {
@ -122,25 +111,21 @@ export function useAudioRecorder() {
}, [isRecording, startRecording, stopRecording]); }, [isRecording, startRecording, stopRecording]);
const clearRecordings = useCallback(() => { const clearRecordings = useCallback(() => {
recordings.forEach((url) => URL.revokeObjectURL(url));
setRecordings([]);
setCurrentRecording(null); setCurrentRecording(null);
}, [recordings]); }, []);
// Cleanup au démontage // Cleanup au démontage
useEffect(() => { useEffect(() => {
return () => { return () => {
recordings.forEach((url) => URL.revokeObjectURL(url));
if (streamRef.current) { if (streamRef.current) {
streamRef.current.getTracks().forEach((t) => t.stop()); streamRef.current.getTracks().forEach((t) => t.stop());
} }
}; };
}, [recordings]); }, []);
return { return {
isLoading, isLoading,
isRecording, isRecording,
recordings,
currentRecording, currentRecording,
startRecording, startRecording,
stopRecording, stopRecording,

View File

@ -109,20 +109,16 @@ export default function RecipeForm() {
return () => clearInterval(id) return () => clearInterval(id)
}, [pageState]) }, [pageState])
// Convertit l'enregistrement en File // Convertit l'enregistrement en File sans repasser par une blob URL,
// pour éviter toute altération ou perte lors du fetch() local.
useEffect(() => { useEffect(() => {
if (!currentRecording) return if (!currentRecording) return
fetch(currentRecording) const mime = currentRecording.type || "audio/webm"
.then((res) => res.blob()) const ext = mime.includes("mp4") ? "m4a" : mime.includes("ogg") ? "ogg" : "webm"
.then((blob) => { const file = new File([currentRecording], `recording.${ext}`, { type: mime })
// Détecte le format réel du blob (webm, mp4, ogg…) setAudioFile(file)
const mime = blob.type || "audio/webm" setPageState("review")
const ext = mime.includes("mp4") ? "m4a" : mime.includes("ogg") ? "ogg" : "webm" setError("")
const file = new File([blob], `recording.${ext}`, { type: mime })
setAudioFile(file)
setPageState("review")
setError("")
})
}, [currentRecording]) }, [currentRecording])
// Timer // Timer