use new audio recorder
This commit is contained in:
parent
20c345f05a
commit
de70929a62
@ -34,7 +34,8 @@
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss": "^4.0.12",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vite-plugin-pwa": "^0.21.1"
|
||||
"vite-plugin-pwa": "^0.21.1",
|
||||
"vmsg": "^0.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
|
||||
8
frontend/pnpm-lock.yaml
generated
8
frontend/pnpm-lock.yaml
generated
@ -80,6 +80,9 @@ importers:
|
||||
vite-plugin-pwa:
|
||||
specifier: ^0.21.1
|
||||
version: 0.21.1(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0)
|
||||
vmsg:
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
devDependencies:
|
||||
'@eslint/js':
|
||||
specifier: ^9.21.0
|
||||
@ -2976,6 +2979,9 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
vmsg@0.4.0:
|
||||
resolution: {integrity: sha512-46BBqRSfqdFGUpO2j+Hpz8T9YE5uWG0/PWal1PT+R1o8NEthtjG/XWl4HzbB8hIHpg/UtmKvsxL2OKQBrIYcHQ==}
|
||||
|
||||
webidl-conversions@4.0.2:
|
||||
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
|
||||
|
||||
@ -6036,6 +6042,8 @@ snapshots:
|
||||
lightningcss: 1.29.2
|
||||
terser: 5.39.0
|
||||
|
||||
vmsg@0.4.0: {}
|
||||
|
||||
webidl-conversions@4.0.2: {}
|
||||
|
||||
whatwg-url@7.1.0:
|
||||
|
||||
81
frontend/src/hooks/useAudioRecorder.ts
Normal file
81
frontend/src/hooks/useAudioRecorder.ts
Normal file
@ -0,0 +1,81 @@
|
||||
// useAudioRecorder.ts
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import vmsg from 'vmsg'
|
||||
|
||||
// Initialize the recorder once
|
||||
const recorder = new vmsg.Recorder({
|
||||
wasmURL: "https://unpkg.com/vmsg@0.3.0/vmsg.wasm"
|
||||
})
|
||||
|
||||
export function useAudioRecorder() {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isRecording, setIsRecording] = useState(false)
|
||||
const [recordings, setRecordings] = useState<string[]>([])
|
||||
const [currentRecording, setCurrentRecording] = useState<string | null>(null)
|
||||
|
||||
const startRecording = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
await recorder.initAudio()
|
||||
await recorder.initWorker()
|
||||
recorder.startRecording()
|
||||
setIsRecording(true)
|
||||
} catch (e) {
|
||||
console.error('Failed to start recording:', e)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const stopRecording = useCallback(async () => {
|
||||
if (!isRecording) return
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const blob = await recorder.stopRecording()
|
||||
const url = URL.createObjectURL(blob)
|
||||
setRecordings(prev => [...prev, url])
|
||||
setCurrentRecording(url)
|
||||
return { blob, url }
|
||||
} catch (e) {
|
||||
console.error('Failed to stop recording:', e)
|
||||
} finally {
|
||||
setIsRecording(false)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [isRecording])
|
||||
|
||||
const toggleRecording = useCallback(async () => {
|
||||
if (isRecording) {
|
||||
return await stopRecording()
|
||||
} else {
|
||||
await startRecording()
|
||||
return null
|
||||
}
|
||||
}, [isRecording, startRecording, stopRecording])
|
||||
|
||||
const clearRecordings = useCallback(() => {
|
||||
// Revoke object URLs to prevent memory leaks
|
||||
recordings.forEach(url => URL.revokeObjectURL(url))
|
||||
setRecordings([])
|
||||
setCurrentRecording(null)
|
||||
}, [recordings])
|
||||
|
||||
// Clean up object URLs when component unmounts
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
recordings.forEach(url => URL.revokeObjectURL(url))
|
||||
}
|
||||
}, [recordings])
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
isRecording,
|
||||
recordings,
|
||||
currentRecording,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
toggleRecording,
|
||||
clearRecordings
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { recipeService } from "@/api/recipe"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@ -11,40 +11,42 @@ import { Mic, Upload, ArrowLeft, Loader2 } from "lucide-react"
|
||||
import { motion } from "framer-motion"
|
||||
import { KitchenIllustration } from "@/components/illustrations/KitchenIllustration"
|
||||
import { CookingLoader } from "@/components/illustrations/CookingLoader"
|
||||
import { useAudioRecorder } from "@/hooks/useAudioRecorder"
|
||||
|
||||
export default function RecipeForm() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const {
|
||||
isLoading: isRecorderLoading,
|
||||
isRecording,
|
||||
currentRecording,
|
||||
startRecording,
|
||||
stopRecording
|
||||
} = useAudioRecorder()
|
||||
|
||||
const [audioFile, setAudioFile] = useState<File | null>(null)
|
||||
const [isRecording, setIsRecording] = useState(false)
|
||||
const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder | null>(null)
|
||||
const [recordingStatus, setRecordingStatus] = useState<"idle" | "recording" | "processing">("idle")
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
|
||||
|
||||
// Démarrer l'enregistrement audio
|
||||
const startRecording = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
||||
const recorder = new MediaRecorder(stream)
|
||||
setMediaRecorder(recorder)
|
||||
|
||||
const chunks: BlobPart[] = []
|
||||
recorder.ondataavailable = (e) => {
|
||||
chunks.push(e.data)
|
||||
}
|
||||
|
||||
recorder.onstop = () => {
|
||||
const blob = new Blob(chunks, { type: "audio/webm" })
|
||||
const file = new File([blob], "recording.webm", { type: "audio/webm" })
|
||||
// Update audioFile when recording is available
|
||||
useEffect(() => {
|
||||
if (currentRecording) {
|
||||
fetch(currentRecording)
|
||||
.then(res => res.blob())
|
||||
.then(blob => {
|
||||
const file = new File([blob], "recording.mp3", { type: "audio/mp3" })
|
||||
setAudioFile(file)
|
||||
setRecordingStatus("idle")
|
||||
setError("")
|
||||
})
|
||||
}
|
||||
}, [currentRecording])
|
||||
|
||||
recorder.start()
|
||||
setIsRecording(true)
|
||||
// Handle recording start
|
||||
const handleStartRecording = async () => {
|
||||
try {
|
||||
await startRecording()
|
||||
setRecordingStatus("recording")
|
||||
} catch (err) {
|
||||
console.error("Erreur lors de l'accès au microphone:", err)
|
||||
@ -52,15 +54,12 @@ export default function RecipeForm() {
|
||||
}
|
||||
}
|
||||
|
||||
// Arrêter l'enregistrement audio
|
||||
const stopRecording = () => {
|
||||
if (mediaRecorder && isRecording) {
|
||||
mediaRecorder.stop()
|
||||
setIsRecording(false)
|
||||
// Handle recording stop
|
||||
const handleStopRecording = async () => {
|
||||
if (isRecording) {
|
||||
await stopRecording()
|
||||
setRecordingStatus("processing")
|
||||
|
||||
// Arrêter toutes les pistes audio
|
||||
mediaRecorder.stream.getTracks().forEach((track) => track.stop())
|
||||
// Processing will be updated to "idle" when the audioFile is set
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,7 +178,7 @@ export default function RecipeForm() {
|
||||
<Button
|
||||
variant="default"
|
||||
className="cursor-pointer"
|
||||
onClick={startRecording}
|
||||
onClick={handleStartRecording}
|
||||
>
|
||||
<Mic className="mr-2 h-4 w-4" />
|
||||
Commencer l'enregistrement
|
||||
@ -190,7 +189,7 @@ export default function RecipeForm() {
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="cursor-pointer"
|
||||
onClick={stopRecording}
|
||||
onClick={handleStopRecording}
|
||||
>
|
||||
<motion.div
|
||||
className="flex items-center"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user