"use client"; import React, { useState, useRef, useEffect } from "react"; import LoadingSpinner from "./LoadingSpinner"; import { getBestRecordingFormat, SUPPORTED_AUDIO_FORMATS, } from "@/lib/config/audio-config"; interface AudioRecorderProps { onRecordingComplete?: () => void; } export default function AudioRecorder({ onRecordingComplete, }: AudioRecorderProps) { const [isRecording, setIsRecording] = useState(false); const [isPaused, setIsPaused] = useState(false); const [recordingTime, setRecordingTime] = useState(0); const [audioBlob, setAudioBlob] = useState(null); const [isUploading, setIsUploading] = useState(false); const [audioLevel, setAudioLevel] = useState(0); const [recordingTitle, setRecordingTitle] = useState(""); // 录音标题 const [isRenaming, setIsRenaming] = useState(false); // 是否正在重命名 const mediaRecorderRef = useRef(null); const streamRef = useRef(null); const timerRef = useRef(null); const animationFrameRef = useRef(null); const audioContextRef = useRef(null); const analyserRef = useRef(null); const sourceRef = useRef(null); // 音频分析器设置 const setupAudioAnalyzer = (stream: MediaStream) => { try { const audioContext = new AudioContext(); const analyser = audioContext.createAnalyser(); const source = audioContext.createMediaStreamSource(stream); analyser.fftSize = 256; analyser.smoothingTimeConstant = 0.8; source.connect(analyser); audioContextRef.current = audioContext; analyserRef.current = analyser; return true; // 表示设置成功 } catch (error) { console.error("音频分析器设置失败:", error); return false; // 表示设置失败 } }; // 更新音频电平 const updateAudioLevel = () => { if (!analyserRef.current || !isRecording || isPaused) { setAudioLevel(0); return; } const dataArray = new Uint8Array(analyserRef.current.frequencyBinCount); analyserRef.current.getByteFrequencyData(dataArray); // 计算平均音量 const average = dataArray.reduce((sum, value) => sum + value, 0) / dataArray.length; const normalizedLevel = Math.min(average / 128, 1); setAudioLevel(normalizedLevel); if (isRecording && !isPaused) { animationFrameRef.current = requestAnimationFrame(updateAudioLevel); } }; // 清理音频分析器 const cleanupAudioAnalyzer = () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); animationFrameRef.current = null; } if (audioContextRef.current) { audioContextRef.current.close(); audioContextRef.current = null; } analyserRef.current = null; setAudioLevel(0); }; const startRecording = async () => { try { // 录音参数直接写死 const constraints = { audio: { sampleRate: 48000, channelCount: 2, echoCancellation: true, noiseSuppression: true, autoGainControl: true, }, }; const stream = await navigator.mediaDevices.getUserMedia(constraints); streamRef.current = stream; // 录音格式直接写死为 webm const mimeType = "audio/webm;codecs=opus"; // 检查浏览器是否支持该格式 if (!MediaRecorder.isTypeSupported(mimeType)) { console.warn(`不支持的格式: ${mimeType},使用默认格式`); // 使用默认格式 const defaultFormat = getBestRecordingFormat(); const mediaRecorder = new MediaRecorder(stream, { mimeType: defaultFormat.mimeType, }); mediaRecorderRef.current = mediaRecorder; } else { const mediaRecorder = new MediaRecorder(stream, { mimeType: mimeType, }); mediaRecorderRef.current = mediaRecorder; } const audioChunks: Blob[] = []; mediaRecorderRef.current.ondataavailable = (event) => { if (event.data.size > 0) { audioChunks.push(event.data); } }; mediaRecorderRef.current.onstop = () => { // 使用实际使用的 MIME 类型 const actualMimeType = mediaRecorderRef.current?.mimeType || "audio/webm"; const audioBlob = new Blob(audioChunks, { type: actualMimeType, }); setAudioBlob(audioBlob); stream.getTracks().forEach((track) => track.stop()); cleanupAudioAnalyzer(); }; // 设置音频分析器 let analyzerSetup = false; try { analyzerSetup = setupAudioAnalyzer(stream); if (analyzerSetup) { // 音频分析器设置成功 } } catch (error) { // 音频分析器设置失败,但不影响录音功能 return false; // 表示设置失败 } // 开始录音 mediaRecorderRef.current.start(); setIsRecording(true); setIsPaused(false); setRecordingTime(0); // 开始计时 timerRef.current = setInterval(() => { setRecordingTime((prev) => prev + 1); }, 1000); // 开始音频分析 if (analyzerSetup) { updateAudioLevel(); } } catch (error) { // 无法访问麦克风 alert("无法访问麦克风,请检查权限设置。"); } }; const pauseRecording = () => { if (mediaRecorderRef.current && isRecording) { if (isPaused) { // 继续录音 mediaRecorderRef.current.resume(); setIsPaused(false); // 重新启动计时器 timerRef.current = setInterval(() => { setRecordingTime((prev) => prev + 1); }, 1000); // 重新开始音频分析 updateAudioLevel(); } else { // 暂停录音 mediaRecorderRef.current.pause(); setIsPaused(true); // 停止计时器 if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } // 停止音频分析 if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); animationFrameRef.current = null; } setAudioLevel(0); } } }; const stopRecording = () => { if (mediaRecorderRef.current) { mediaRecorderRef.current.stop(); setIsRecording(false); setIsPaused(false); // setIsAnalyzing(false); // This line is removed // 停止计时器 if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } // 清理音频分析器 cleanupAudioAnalyzer(); // 停止音频流 if (streamRef.current) { streamRef.current.getTracks().forEach((track) => track.stop()); streamRef.current = null; } // 保持录音时长,不清零 } }; const cancelRecording = () => { if (mediaRecorderRef.current) { mediaRecorderRef.current.stop(); setIsRecording(false); setIsPaused(false); // 停止计时器 if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } // 清理音频分析器 cleanupAudioAnalyzer(); // 停止音频流 if (streamRef.current) { streamRef.current.getTracks().forEach((track) => track.stop()); streamRef.current = null; } setAudioBlob(null); } }; const uploadRecording = async () => { if (!audioBlob) return; setIsUploading(true); try { // 直接上传文件到后端,让后端处理 S3 上传 const formData = new FormData(); formData.append("audio", audioBlob, `recording-${Date.now()}.webm`); formData.append("duration", recordingTime.toString()); // 添加录音标题 const title = recordingTitle.trim() || `录音 ${new Date().toLocaleString("zh-CN")}`; formData.append("title", title); const response = await fetch("/api/recordings/upload", { method: "POST", body: formData, }); if (response.ok) { alert("录音上传成功!"); setAudioBlob(null); setRecordingTitle(""); // 清空录音标题 // 保持录音时长用于显示,不重置为0 // 触发父组件刷新 console.log("录音上传成功,触发刷新事件"); window.dispatchEvent(new CustomEvent("recording-uploaded")); } else { const errorData = await response.json(); throw new Error(errorData.error || "保存录音记录失败"); } } catch (error) { console.error("上传失败:", error); alert(`上传失败:${error instanceof Error ? error.message : "请重试"}`); } finally { setIsUploading(false); } }; const formatTime = (seconds: number) => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins.toString().padStart(2, "0")}:${secs .toString() .padStart(2, "0")}`; }; // 生成波形条 const generateWaveformBars = () => { const bars = []; const barCount = 20; const baseHeight = 4; for (let i = 0; i < barCount; i++) { const height = isRecording && !isPaused ? baseHeight + audioLevel * 20 * Math.random() : baseHeight; bars.push(
); } return bars; }; // 组件卸载时清理 useEffect(() => { return () => { cleanupAudioAnalyzer(); if (streamRef.current) { streamRef.current.getTracks().forEach((track) => track.stop()); } }; }, []); return ( <>

录音机

{/* 录音状态指示器 */}
{!isRecording ? "准备就绪" : isPaused ? "已暂停" : "录音中..."}
{/* 波形可视化 */}
{generateWaveformBars()}
{isRecording && (
{isPaused ? "已暂停" : "录音中..."}
)}
{isRecording && (
{formatTime(recordingTime)}
{isPaused ? "已暂停" : "录音中..."}
{/* 录音进度条 */}
进度: {Math.round((recordingTime / 300) * 100)}% (最大5分钟)
)}
{!isRecording ? ( ) : ( <> )}
{audioBlob && (
{/* 录音标题 */}
{isRenaming ? (
setRecordingTitle(e.target.value)} placeholder="输入录音标题" className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white text-center min-w-0 flex-1 max-w-xs" maxLength={100} autoFocus />
) : (

{recordingTitle || "录音"}

)}
)}
); }