mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
wewa
This commit is contained in:
@@ -24,6 +24,7 @@ const KEY_ACTIONS = [
|
||||
{ id: 'cameraDown', label: 'Camera Down', group: 'Camera' },
|
||||
{ id: 'nightVisionToggle', label: 'Toggle Night Vision', group: 'Camera' },
|
||||
{ id: 'hornHonk', label: 'Horn (Hold)', group: 'Audio' },
|
||||
{ id: 'micPtt', label: 'Mic Push To Talk', group: 'Audio' },
|
||||
{ id: 'driveMacro', label: 'Drive Macro', group: 'Macros' },
|
||||
{ id: 'dockMacro', label: 'Dock Macro', group: 'Macros' },
|
||||
{ id: 'chatFocus', label: 'Toggle Chat', group: 'Chat' },
|
||||
|
||||
@@ -7,7 +7,16 @@ import VipVerificationCard from './vip/VipVerificationCard.jsx';
|
||||
import VipIdentityCard from './vip/VipIdentityCard.jsx';
|
||||
|
||||
export default function VipPanel() {
|
||||
const { session, identifySession, requestVerification, playUploadedAudio, stopUploadedAudio } = useSession();
|
||||
const {
|
||||
session,
|
||||
identifySession,
|
||||
requestVerification,
|
||||
playUploadedAudio,
|
||||
stopUploadedAudio,
|
||||
startMicForward,
|
||||
stopMicForward,
|
||||
sendMicChunk,
|
||||
} = useSession();
|
||||
const { value: identity, save: saveIdentity } = useSettingsNamespace('identity', { cookieUserId: '' });
|
||||
const { value: profile } = useSettingsNamespace('profile', { nickname: '' });
|
||||
|
||||
@@ -45,6 +54,9 @@ export default function VipPanel() {
|
||||
audioForwardByRover={session?.audioForward || {}}
|
||||
playUploadedAudio={playUploadedAudio}
|
||||
stopUploadedAudio={stopUploadedAudio}
|
||||
startMicForward={startMicForward}
|
||||
stopMicForward={stopMicForward}
|
||||
sendMicChunk={sendMicChunk}
|
||||
/>
|
||||
) : (
|
||||
<VipVerificationCard
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
MAX_UPLOAD_BYTES,
|
||||
bytesToBase64,
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
flowWrapClass,
|
||||
innerFlowClass,
|
||||
} from './constants.js';
|
||||
import { useControlSystem } from '../../controls/index.js';
|
||||
|
||||
export default function VipAudioForwardingCard({
|
||||
roster = [],
|
||||
@@ -13,12 +14,23 @@ export default function VipAudioForwardingCard({
|
||||
audioForwardByRover = {},
|
||||
playUploadedAudio,
|
||||
stopUploadedAudio,
|
||||
startMicForward,
|
||||
stopMicForward,
|
||||
sendMicChunk,
|
||||
}) {
|
||||
const { state: controlState } = useControlSystem();
|
||||
const [selectedUpload, setSelectedUpload] = useState(null);
|
||||
const [working, setWorking] = useState(false);
|
||||
const [openMicEnabled, setOpenMicEnabled] = useState(false);
|
||||
const [micState, setMicState] = useState('idle');
|
||||
const [message, setMessage] = useState('');
|
||||
const recorderRef = useRef(null);
|
||||
const streamRef = useRef(null);
|
||||
const micActiveRef = useRef(false);
|
||||
const activeRoverRef = useRef('');
|
||||
const singleRoverId = roster.length === 1 ? roster[0].id : '';
|
||||
const targetRoverId = String(singleRoverId || ownRoverId || '').trim();
|
||||
const pttActive = Boolean(controlState?.mic?.pttActive);
|
||||
const selectedForwardState = useMemo(
|
||||
() => (targetRoverId ? audioForwardByRover?.[targetRoverId] || null : null),
|
||||
[audioForwardByRover, targetRoverId],
|
||||
@@ -75,6 +87,137 @@ export default function VipAudioForwardingCard({
|
||||
}
|
||||
};
|
||||
|
||||
const stopMicCapture = useCallback(
|
||||
async (roverId) => {
|
||||
const target = String(roverId || activeRoverRef.current || '').trim();
|
||||
micActiveRef.current = false;
|
||||
setMicState('idle');
|
||||
try {
|
||||
const recorder = recorderRef.current;
|
||||
if (recorder && recorder.state !== 'inactive') {
|
||||
recorder.stop();
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
recorderRef.current = null;
|
||||
if (streamRef.current) {
|
||||
try {
|
||||
streamRef.current.getTracks().forEach((track) => track.stop());
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
streamRef.current = null;
|
||||
if (target) {
|
||||
try {
|
||||
await stopMicForward?.(target);
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
activeRoverRef.current = '';
|
||||
},
|
||||
[stopMicForward],
|
||||
);
|
||||
|
||||
const startMicCapture = useCallback(
|
||||
async (roverId) => {
|
||||
const target = String(roverId || '').trim();
|
||||
if (!target) {
|
||||
throw new Error('Take control of a rover first.');
|
||||
}
|
||||
if (!navigator.mediaDevices?.getUserMedia) {
|
||||
throw new Error('Microphone capture is not supported in this browser.');
|
||||
}
|
||||
if (typeof MediaRecorder === 'undefined') {
|
||||
throw new Error('MediaRecorder is not supported in this browser.');
|
||||
}
|
||||
await stopMicCapture(target);
|
||||
setMicState('starting');
|
||||
let stream = null;
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
channelCount: 1,
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true,
|
||||
},
|
||||
});
|
||||
streamRef.current = stream;
|
||||
await startMicForward?.(target);
|
||||
const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
|
||||
? 'audio/webm;codecs=opus'
|
||||
: 'audio/webm';
|
||||
const recorder = new MediaRecorder(stream, { mimeType, audioBitsPerSecond: 64000 });
|
||||
recorderRef.current = recorder;
|
||||
micActiveRef.current = true;
|
||||
activeRoverRef.current = target;
|
||||
recorder.ondataavailable = async (event) => {
|
||||
try {
|
||||
if (!micActiveRef.current || !event.data || event.data.size <= 0) return;
|
||||
const buffer = await event.data.arrayBuffer();
|
||||
const base64 = bytesToBase64(new Uint8Array(buffer));
|
||||
sendMicChunk?.({ roverId: target, dataBase64: base64 });
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
};
|
||||
recorder.onstop = () => {
|
||||
if (!micActiveRef.current) return;
|
||||
micActiveRef.current = false;
|
||||
setMicState('idle');
|
||||
};
|
||||
recorder.start(120);
|
||||
setMicState('live');
|
||||
} catch (err) {
|
||||
if (stream) {
|
||||
try {
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
streamRef.current = null;
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
[sendMicChunk, startMicForward, stopMicCapture],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const desiredActive = Boolean(openMicEnabled || pttActive);
|
||||
const roverId = targetRoverId;
|
||||
let cancelled = false;
|
||||
async function syncMicState() {
|
||||
if (!roverId || !desiredActive) {
|
||||
await stopMicCapture(activeRoverRef.current || roverId);
|
||||
return;
|
||||
}
|
||||
if (micActiveRef.current && activeRoverRef.current === roverId) return;
|
||||
try {
|
||||
await startMicCapture(roverId);
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
setMicState('error');
|
||||
setMessage(err?.message || 'Failed to start microphone forwarding.');
|
||||
}
|
||||
}
|
||||
}
|
||||
syncMicState();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [openMicEnabled, pttActive, startMicCapture, stopMicCapture, targetRoverId]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
stopMicCapture(activeRoverRef.current);
|
||||
},
|
||||
[stopMicCapture],
|
||||
);
|
||||
|
||||
return (
|
||||
<section className={`surface ${flowWrapClass}`}>
|
||||
<div className={innerFlowClass}>
|
||||
@@ -102,6 +245,20 @@ export default function VipAudioForwardingCard({
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
<div className="surface-muted mx-auto flex w-full max-w-sm flex-col gap-0.5 p-0.5 text-xs text-slate-300 text-center">
|
||||
<p className="text-slate-200">Microphone Forwarding</p>
|
||||
<label className="flex items-center justify-center gap-0.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={openMicEnabled}
|
||||
disabled={!targetRoverId}
|
||||
onChange={(event) => setOpenMicEnabled(Boolean(event.target.checked))}
|
||||
/>
|
||||
<span>Open mic</span>
|
||||
</label>
|
||||
<p className="text-slate-400">PTT key: {controlState?.keymap?.micPtt?.[0] || 'v'} (hold)</p>
|
||||
<p className="text-slate-400">mic: {micState}</p>
|
||||
</div>
|
||||
{selectedForwardState ? (
|
||||
<div className="surface-muted mx-auto w-full max-w-sm text-xs text-slate-300 text-center">
|
||||
state: {selectedForwardState.state || 'idle'}
|
||||
|
||||
Reference in New Issue
Block a user