import { useEffect, useRef, useState } from "react"; import { copy, type Language } from "../copy"; import { OnboardingFrame } from "./OnboardingFrame"; export type DeviceCheckResult = { deviceId?: string; voiceReady: boolean }; function MicIcon() { return ( ); } export function DeviceCheck({ language = "vi", onComplete }: { language?: Language; onComplete: (result: DeviceCheckResult) => void; }) { const text = copy[language].deviceCheck; const kicker = copy[language].productName; const [devices, setDevices] = useState([]); const [deviceId, setDeviceId] = useState(""); const [level, setLevel] = useState(0); const [state, setState] = useState<"idle" | "ready" | "unavailable" | "denied">("idle"); const streamRef = useRef(null); const audioContextRef = useRef(null); const frameRef = useRef(null); const testRef = useRef(0); function stopTest() { testRef.current += 1; if (frameRef.current !== null) cancelAnimationFrame(frameRef.current); frameRef.current = null; streamRef.current?.getTracks().forEach((track) => track.stop()); streamRef.current = null; void audioContextRef.current?.close(); audioContextRef.current = null; } useEffect(() => { void navigator.mediaDevices?.enumerateDevices?.().then((all) => { const microphones = all.filter((device) => device.kind === "audioinput"); setDevices(microphones); setDeviceId((current) => current || microphones[0]?.deviceId || ""); if (!microphones.length) setState("unavailable"); }).catch(() => setState("unavailable")); return stopTest; }, []); async function testMicrophone() { stopTest(); const attempt = ++testRef.current; if (!navigator.mediaDevices?.getUserMedia) { setState("unavailable"); return; } try { const stream = await navigator.mediaDevices.getUserMedia({ audio: deviceId ? { deviceId: { exact: deviceId } } : true, }); if (testRef.current !== attempt) { stream.getTracks().forEach((track) => track.stop()); return; } streamRef.current = stream; const context = new AudioContext(); audioContextRef.current = context; const analyser = context.createAnalyser(); const source = context.createMediaStreamSource(stream); source.connect(analyser); const values = new Uint8Array(analyser.fftSize); const updateLevel = () => { analyser.getByteTimeDomainData(values); setLevel(values.reduce((sum, value) => sum + Math.abs(value - 128), 0) / values.length / 128); frameRef.current = requestAnimationFrame(updateLevel); }; updateLevel(); setState("ready"); } catch { stopTest(); setState(devices.length ? "denied" : "unavailable"); } } const blocked = state === "unavailable" || state === "denied"; return ( {text.body} {devices.length > 1 ? ( {text.microphone} setDeviceId(event.target.value)}> {devices.map((device, index) => {device.label || `${text.microphone} ${index + 1}`})} ) : null} {text.level} {text.scaleLow} {text.scaleHigh} {state === "ready" ? {text.ready} : null} {state === "idle" ? {text.idle} : null} {blocked ? {state === "unavailable" ? text.unavailable : text.denied} : null} void testMicrophone()}>{state === "idle" ? text.test : text.retry} {state === "ready" ? ( { stopTest(); onComplete({ deviceId, voiceReady: true }); }}>{text.continueVoice} ) : null} { stopTest(); onComplete({ voiceReady: false }); }}>{text.continueTyped} ); }
{text.body}
{text.ready}
{text.idle}
{state === "unavailable" ? text.unavailable : text.denied}