fix: preserve recorder blob without object-url refetch
All checks were successful
Build & Deploy to K3s / build-and-deploy (push) Successful in 35s
All checks were successful
Build & Deploy to K3s / build-and-deploy (push) Successful in 35s
This commit is contained in:
parent
6dd7d2c4e5
commit
800214976f
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user