UNPKG

@p0llen/speaker-detector-client

Version:

React components and hooks for live speaker detection and mic analysis.

226 lines â€ĸ 7.64 kB
import _asyncToGenerator from "@babel/runtime/helpers/esm/asyncToGenerator"; import React, { useState, useRef, useEffect, useCallback } from "react"; import useMicCheck from "../hooks/useMicCheck"; var MicStatus = _ref => { var { backendOnline, status, listeningMode, isSpeaking, title = "🎤 Microphone & Detection Status", enableMicTest = true, micTestButtonLabel = "🎤 Mic Test" } = _ref; var { devices, selectedDeviceId, setSelectedDeviceId, refreshDeviceList } = useMicCheck(); var [showMicTest, setShowMicTest] = useState(false); var combinedStatusLabel = () => { if (backendOnline === false) return "🔌 Backend unreachable"; if (listeningMode === "off" || status === "disabled") return "🔇 Detection disabled"; if (status === "pending") return "âŗ Engine awaiting data..."; if (status === "listening") return isSpeaking ? "🎙 Voice detected" : "đŸ•ĩī¸ Listening..."; if (backendOnline === null) return "🔎 Checking backend..."; return "â„šī¸ Ready"; }; var onSelectDevice = e => { setSelectedDeviceId(e.target.value); }; return /*#__PURE__*/React.createElement("div", { className: "mic-status-panel" }, /*#__PURE__*/React.createElement("h4", { className: "speaker-heading" }, title), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("strong", null, "Status:"), " ", combinedStatusLabel()), /*#__PURE__*/React.createElement("div", { style: { marginTop: 8, marginBottom: 16 } }, /*#__PURE__*/React.createElement("label", null, "Input device:\xA0", /*#__PURE__*/React.createElement("select", { value: selectedDeviceId || "", onChange: onSelectDevice, style: { padding: 4 } }, devices.length === 0 ? /*#__PURE__*/React.createElement("option", { value: "" }, "(no audio inputs)") : devices.map((d, i) => /*#__PURE__*/React.createElement("option", { key: d.deviceId, value: d.deviceId }, d.label || "Microphone ".concat(i + 1))))), /*#__PURE__*/React.createElement("button", { onClick: refreshDeviceList, style: { marginLeft: 8 } }, "\uD83D\uDD04 Refresh Inputs"), enableMicTest && /*#__PURE__*/React.createElement("button", { onClick: () => setShowMicTest(v => !v), style: { marginLeft: 8 } }, showMicTest ? "âŦ†ī¸ Close Mic Test" : micTestButtonLabel + " âŦ‡ī¸")), enableMicTest && showMicTest && /*#__PURE__*/React.createElement(MicTestSection, { deviceId: selectedDeviceId })); }; export default MicStatus; function MicTestSection(_ref2) { var { deviceId } = _ref2; var canvasRef = useRef(null); var audioRef = useRef(null); var streamRef = useRef(null); var audioCtxRef = useRef(null); var analyserRef = useRef(null); var rafRef = useRef(null); var [recording, setRecording] = useState(false); var [audioURL, setAudioURL] = useState(null); var [recorder, setRecorder] = useState(null); var chunks = useRef([]); var cleanup = useCallback(() => { var _audioCtxRef$current; if (rafRef.current) cancelAnimationFrame(rafRef.current); if ((_audioCtxRef$current = audioCtxRef.current) !== null && _audioCtxRef$current !== void 0 && _audioCtxRef$current.state && audioCtxRef.current.state !== "closed") { audioCtxRef.current.close().catch(() => {}); } if (streamRef.current) { streamRef.current.getTracks().forEach(t => t.stop()); streamRef.current = null; } audioCtxRef.current = null; }, []); var draw = () => { var canvas = canvasRef.current; var analyser = analyserRef.current; if (!canvas || !analyser) return; var ctx = canvas.getContext("2d"); var bufferLength = analyser.fftSize; var dataArray = new Uint8Array(bufferLength); analyser.getByteTimeDomainData(dataArray); // Clear previous frame and draw without forcing a dark background ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.lineWidth = 2; try { var c = getComputedStyle(canvas).color || '#666'; ctx.strokeStyle = c; } catch (_unused) { ctx.strokeStyle = '#666'; } ctx.beginPath(); var slice = canvas.width / bufferLength; var x = 0; for (var i = 0; i < bufferLength; i++) { var v = dataArray[i] / 128.0; var y = v * canvas.height / 2; i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); x += slice; } ctx.lineTo(canvas.width, canvas.height / 2); ctx.stroke(); rafRef.current = requestAnimationFrame(draw); }; var setupMic = useCallback(/*#__PURE__*/_asyncToGenerator(function* () { cleanup(); try { var constraints = !deviceId || deviceId === 'default' || deviceId === 'communications' ? { audio: true } : { audio: { deviceId: { exact: deviceId } } }; var stream = yield navigator.mediaDevices.getUserMedia(constraints); streamRef.current = stream; var AudioCtx = window.AudioContext || window.webkitAudioContext; var audioCtx = new AudioCtx(); var source = audioCtx.createMediaStreamSource(stream); var analyser = audioCtx.createAnalyser(); analyser.fftSize = 2048; source.connect(analyser); audioCtxRef.current = audioCtx; analyserRef.current = analyser; draw(); } catch (err) { alert("âš ī¸ Microphone access failed: " + err.message); } }), [deviceId, cleanup]); useEffect(() => { setupMic(); return cleanup; }, [setupMic]); var startRecording = () => { var stream = streamRef.current; if (!stream || !stream.active) { alert("🎤 Mic stream not ready."); return; } chunks.current = []; var mediaRecorder = new MediaRecorder(stream); mediaRecorder.ondataavailable = e => chunks.current.push(e.data); mediaRecorder.onstop = () => { if (chunks.current.length > 0) { var blob = new Blob(chunks.current, { type: "audio/webm" }); var url = URL.createObjectURL(blob); setAudioURL(url); setTimeout(() => { if (audioRef.current) { audioRef.current.src = url; audioRef.current.play().catch(() => {}); } }, 50); } }; mediaRecorder.start(); setRecorder(mediaRecorder); setRecording(true); }; var stopRecording = () => { if (recorder) recorder.stop(); setRecording(false); }; var restart = () => { setAudioURL(null); setRecording(false); chunks.current = []; }; return /*#__PURE__*/React.createElement("div", { style: { marginTop: 12, padding: 12 } }, /*#__PURE__*/React.createElement("h5", { style: { marginBottom: 8 } }, "\uD83C\uDFA7 Mic Signal Visualizer"), /*#__PURE__*/React.createElement("canvas", { ref: canvasRef, width: 560, height: 100, style: { width: '100%', height: 120 } }), /*#__PURE__*/React.createElement("div", { style: { marginTop: 12 } }, !recording && !audioURL && /*#__PURE__*/React.createElement("button", { onClick: startRecording }, "\u23FA Start Recording"), recording && /*#__PURE__*/React.createElement("button", { onClick: stopRecording }, "\u23F9 Stop Recording"), audioURL && !recording && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("audio", { ref: audioRef, controls: true, style: { display: "block", margin: "8px 0" } }), /*#__PURE__*/React.createElement("button", { onClick: restart }, "\uD83D\uDD04 Restart")))); }