import { useRef, useState, useCallback } from 'react'; export function usePushToTalk() { const [isRecording, setIsRecording] = useState(false); const streamRef = useRef(null); // mic stream, acquired on demand const recorderRef = useRef(null); const recognitionRef = useRef(null); const chunksRef = useRef([]); const transcriptRef = useRef(''); // Resolves once the mic stream is acquired and MediaRecorder is running. // Throws if mic permission is denied. const start = useCallback(async () => { if (isRecording) return; chunksRef.current = []; transcriptRef.current = ''; // ── Acquire mic stream (on-demand, not pre-warmed) ─────────── // If the previous stream is still active (same session, held open) reuse it; // otherwise request a new one — this is where the brief "preparing" delay happens. if (!streamRef.current?.active) { streamRef.current = await navigator.mediaDevices.getUserMedia({ audio: true }); } // ── MediaRecorder ──────────────────────────────────────────── const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm'; const recorder = new MediaRecorder(streamRef.current, { mimeType }); recorder.ondataavailable = (e) => { if (e.data.size > 0) chunksRef.current.push(e.data); }; recorder.start(100); recorderRef.current = recorder; // ── SpeechRecognition: real-time transcript ────────────────── const SR = window.SpeechRecognition || window.webkitSpeechRecognition; if (SR) { const sr = new SR(); sr.lang = 'zh-CN'; sr.interimResults = true; sr.continuous = true; sr.onresult = (e) => { let final = ''; for (const res of e.results) { if (res.isFinal) final += res[0].transcript; } if (final) transcriptRef.current = final; }; sr.onerror = (e) => { console.warn('SpeechRecognition error:', e.error); }; sr.start(); recognitionRef.current = sr; } setIsRecording(true); // start() resolves here — caller knows mic is live and recording has begun }, [isRecording]); // Returns { transcript, audioBase64, mimeType } const stop = useCallback(() => new Promise((resolve) => { setIsRecording(false); const transcript = transcriptRef.current; try { recognitionRef.current?.stop(); } catch {} recognitionRef.current = null; // Release mic stream so the OS indicator light turns off streamRef.current?.getTracks().forEach(t => t.stop()); streamRef.current = null; const recorder = recorderRef.current; if (!recorder) { return resolve({ transcript, audioBase64: null, mimeType: null }); } recorder.onstop = async () => { const mimeType = recorder.mimeType; if (chunksRef.current.length === 0) { return resolve({ transcript, audioBase64: null, mimeType }); } const blob = new Blob(chunksRef.current, { type: mimeType }); const reader = new FileReader(); reader.onloadend = () => { resolve({ transcript, audioBase64: reader.result.split(',')[1], mimeType }); }; reader.readAsDataURL(blob); }; recorder.stop(); recorderRef.current = null; }), []); return { isRecording, start, stop }; }