@p0llen/speaker-detector-client
Version:
React components and hooks for live speaker detection and mic analysis.
226 lines âĸ 7.64 kB
JavaScript
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"))));
}