From e2e8af54552a4d45c2d274b5f45774e69835685e Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 30 Jul 2026 07:48:46 +0800 Subject: [PATCH] =?UTF-8?q?refactor(CloneModal):=20=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E5=BD=95=E9=9F=B3Hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CloneModal/hooks/useAudioRecorder.ts | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 apps/web/src/components/voice/CloneModal/hooks/useAudioRecorder.ts diff --git a/apps/web/src/components/voice/CloneModal/hooks/useAudioRecorder.ts b/apps/web/src/components/voice/CloneModal/hooks/useAudioRecorder.ts new file mode 100644 index 000000000..54a32b4ea --- /dev/null +++ b/apps/web/src/components/voice/CloneModal/hooks/useAudioRecorder.ts @@ -0,0 +1,120 @@ +import { useState, useRef, useCallback, useEffect } from "react" +import { MAX_RECORD_SECONDS } from "../constants" +import { formatRecordTime } from "../utils" + +/** + * 录音 Hook + * 封装浏览器 MediaRecorder 录音逻辑,含计时、自动停止、时长上限 + */ +export const useAudioRecorder = (onError?: (msg: string) => void) => { + const [isRecording, setIsRecording] = useState(false) + const [recordTime, setRecordTime] = useState(0) + const [recordedBlob, setRecordedBlob] = useState(null) + + const recordTimerRef = useRef | null>(null) + const mediaRecorderRef = useRef(null) + const audioChunksRef = useRef([]) + + /** 重置录音状态 */ + const reset = useCallback(() => { + setIsRecording(false) + setRecordTime(0) + setRecordedBlob(null) + audioChunksRef.current = [] + if (recordTimerRef.current) { + clearInterval(recordTimerRef.current) + recordTimerRef.current = null + } + if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") { + mediaRecorderRef.current.stop() + } + mediaRecorderRef.current = null + }, []) + + /** 开始/停止录制 */ + const toggleRecord = useCallback(async () => { + if (isRecording) { + // 停止录制 + setIsRecording(false) + if (recordTimerRef.current) { + clearInterval(recordTimerRef.current) + recordTimerRef.current = null + } + if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") { + mediaRecorderRef.current.stop() + } + return + } + + // 开始录制 + try { + const stream = await navigator.mediaDevices.getUserMedia({ + audio: true, + }) + const mediaRecorder = new MediaRecorder(stream) + mediaRecorderRef.current = mediaRecorder + audioChunksRef.current = [] + + mediaRecorder.ondataavailable = (event) => { + if (event.data.size > 0) { + audioChunksRef.current.push(event.data) + } + } + + mediaRecorder.onstop = () => { + const blob = new Blob(audioChunksRef.current, { type: "audio/webm" }) + setRecordedBlob(blob) + // 停止音轨 + stream.getTracks().forEach((track) => track.stop()) + } + + mediaRecorder.start() + setIsRecording(true) + setRecordTime(0) + setRecordedBlob(null) + + recordTimerRef.current = setInterval(() => { + setRecordTime((prev) => { + const next = prev + 1 + if (next >= MAX_RECORD_SECONDS) { + // 达到时长上限,自动停止 + setTimeout(() => { + setIsRecording(false) + if (recordTimerRef.current) { + clearInterval(recordTimerRef.current) + recordTimerRef.current = null + } + if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") { + mediaRecorderRef.current.stop() + } + onError?.("已达最长录制时长(5分钟),已自动停止") + }, 0) + return MAX_RECORD_SECONDS + } + return next + }) + }, 1000) + } catch { + onError?.("无法访问麦克风,请检查浏览器权限设置") + } + }, [isRecording, onError]) + + // 组件卸载时清理 + useEffect(() => { + return () => { + if (recordTimerRef.current) clearInterval(recordTimerRef.current) + if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") { + mediaRecorderRef.current.stop() + } + } + }, []) + + return { + isRecording, + recordTime, + recordedBlob, + formattedTime: formatRecordTime(recordTime), + toggleRecord, + reset, + } +}