'use client' import { useState, useCallback, useRef, useEffect } from 'react' export interface AudioRecorderState { isRecording: boolean audioBlob: Blob | null recordingTime: number isProcessing: boolean error: string | null } interface AudioRecorderProps { onRecordingComplete: (audioBlob: Blob) => void onRecordingStart?: () => void onRecordingStop?: () => void onError?: (error: string) => void maxDuration?: number // 最大錄音時長(秒) disabled?: boolean className?: string } export function AudioRecorder({ onRecordingComplete, onRecordingStart, onRecordingStop, onError, maxDuration = 30, disabled = false, className = "" }: AudioRecorderProps) { const [state, setState] = useState({ isRecording: false, audioBlob: null, recordingTime: 0, isProcessing: false, error: null }) const mediaRecorderRef = useRef(null) const streamRef = useRef(null) const audioChunksRef = useRef([]) const timerRef = useRef(null) // 清理函數 const cleanup = useCallback(() => { if (timerRef.current) { clearInterval(timerRef.current) timerRef.current = null } if (streamRef.current) { streamRef.current.getTracks().forEach(track => track.stop()) streamRef.current = null } if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') { mediaRecorderRef.current.stop() } audioChunksRef.current = [] }, []) // 組件卸載時清理 useEffect(() => { return cleanup }, [cleanup]) // 開始錄音 const startRecording = useCallback(async () => { if (disabled) return try { setState(prev => ({ ...prev, error: null, isProcessing: true })) // 請求麥克風權限 const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 16000 // Azure Speech Services 推薦採樣率 } }) streamRef.current = stream audioChunksRef.current = [] // 創建 MediaRecorder const mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' // 現代瀏覽器支援的格式 }) mediaRecorderRef.current = mediaRecorder // 處理錄音資料 mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { audioChunksRef.current.push(event.data) } } // 錄音完成處理 mediaRecorder.onstop = () => { setState(prev => ({ ...prev, isProcessing: true })) const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' }) setState(prev => ({ ...prev, audioBlob, isRecording: false, isProcessing: false })) onRecordingComplete(audioBlob) cleanup() } // 開始錄音 mediaRecorder.start(1000) // 每秒收集一次資料 // 啟動計時器 let seconds = 0 timerRef.current = setInterval(() => { seconds++ setState(prev => ({ ...prev, recordingTime: seconds })) // 達到最大時長自動停止 if (seconds >= maxDuration) { stopRecording() } }, 1000) setState(prev => ({ ...prev, isRecording: true, recordingTime: 0, isProcessing: false })) onRecordingStart?.() } catch (error) { const errorMessage = error instanceof Error ? error.message : '麥克風存取失敗' setState(prev => ({ ...prev, error: errorMessage, isProcessing: false })) onError?.(errorMessage) cleanup() } }, [disabled, maxDuration, onRecordingComplete, onRecordingStart, onError, cleanup]) // 停止錄音 const stopRecording = useCallback(() => { if (mediaRecorderRef.current && mediaRecorderRef.current.state === 'recording') { mediaRecorderRef.current.stop() onRecordingStop?.() } if (timerRef.current) { clearInterval(timerRef.current) timerRef.current = null } }, [onRecordingStop]) // 格式化時間顯示 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')}` } return (
{/* 錯誤顯示 */} {state.error && (

⚠️ {state.error}

)} {/* 錄音按鈕 */}
{!state.isRecording ? ( ) : ( )}
{/* 錄音狀態顯示 */} {state.isRecording && (
錄音中
{formatTime(state.recordingTime)}
最長 {formatTime(maxDuration)}
{/* 進度條 */}
)} {/* 錄音完成提示 */} {state.audioBlob && !state.isRecording && (

✅ 錄音完成!時長: {formatTime(state.recordingTime)}

)} {/* 瀏覽器兼容性提示 */} {!navigator.mediaDevices?.getUserMedia && (

⚠️ 您的瀏覽器不支援錄音功能,建議使用 Chrome 或 Firefox

)}
) } // Hook 版本,提供更靈活的使用方式 export function useAudioRecorder(maxDuration: number = 30) { const [state, setState] = useState({ isRecording: false, audioBlob: null, recordingTime: 0, isProcessing: false, error: null }) const mediaRecorderRef = useRef(null) const streamRef = useRef(null) const audioChunksRef = useRef([]) const timerRef = useRef(null) const cleanup = useCallback(() => { if (timerRef.current) { clearInterval(timerRef.current) timerRef.current = null } if (streamRef.current) { streamRef.current.getTracks().forEach(track => track.stop()) streamRef.current = null } if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') { mediaRecorderRef.current.stop() } audioChunksRef.current = [] }, []) const startRecording = useCallback(async () => { try { setState(prev => ({ ...prev, error: null, isProcessing: true })) const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 16000 } }) streamRef.current = stream audioChunksRef.current = [] const mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' }) mediaRecorderRef.current = mediaRecorder mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { audioChunksRef.current.push(event.data) } } mediaRecorder.onstop = () => { const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' }) setState(prev => ({ ...prev, audioBlob, isRecording: false, isProcessing: false })) cleanup() } mediaRecorder.start(1000) let seconds = 0 timerRef.current = setInterval(() => { seconds++ setState(prev => ({ ...prev, recordingTime: seconds })) if (seconds >= maxDuration) { stopRecording() } }, 1000) setState(prev => ({ ...prev, isRecording: true, recordingTime: 0, isProcessing: false })) } catch (error) { const errorMessage = error instanceof Error ? error.message : '錄音失敗' setState(prev => ({ ...prev, error: errorMessage, isProcessing: false })) cleanup() } }, [maxDuration, cleanup]) const stopRecording = useCallback(() => { if (mediaRecorderRef.current && mediaRecorderRef.current.state === 'recording') { mediaRecorderRef.current.stop() } if (timerRef.current) { clearInterval(timerRef.current) timerRef.current = null } }, []) const resetRecording = useCallback(() => { cleanup() setState({ isRecording: false, audioBlob: null, recordingTime: 0, isProcessing: false, error: null }) }, [cleanup]) // 清理函數 useEffect(() => { return cleanup }, [cleanup]) return { ...state, startRecording, stopRecording, resetRecording } }