import { useCallback, useEffect, useRef, useState } from 'react'; import { WhepPlayer } from '../lib/whepPlayer.js'; const RESTART_DELAY_MS = 2000; const UNMUTE_RETRY_MS = 3000; export default function VideoTile({ sessionInfo, label, forceMute = false, telemetryFrame, batteryConfig }) { const videoRef = useRef(null); const restartTimer = useRef(null); const unmuteTimer = useRef(null); const [status, setStatus] = useState('idle'); const [detail, setDetail] = useState(null); const [restartToken, setRestartToken] = useState(0); const [muted, setMuted] = useState(true); const sensors = telemetryFrame?.sensors; const batteryCharge = sensors?.batteryChargeMah ?? null; // console.log('[BatteryBarDebug]', { // frameSensors: sensors, // batteryCharge, // batteryCapacity, // config: batteryConfig, // }); const wheelOvercurrents = sensors?.wheelOvercurrents || null; const overcurrentMotors = wheelOvercurrents == null ? [] : Object.entries(wheelOvercurrents) .filter(([, active]) => Boolean(active)) .map(([key]) => key); const overcurrentActive = overcurrentMotors.length > 0; const scheduleRestart = useCallback(() => { clearTimeout(restartTimer.current); restartTimer.current = setTimeout(() => setRestartToken(Date.now()), RESTART_DELAY_MS); }, []); const attemptUnmute = useCallback( (delay = 0) => { if (forceMute) return; clearTimeout(unmuteTimer.current); const scheduleRetry = () => { clearTimeout(unmuteTimer.current); unmuteTimer.current = setTimeout(() => { if (!forceMute) { tryPlay(); } }, UNMUTE_RETRY_MS); }; const tryPlay = async () => { const video = videoRef.current; if (!video) return; try { video.muted = false; await video.play(); setMuted(false); } catch { video.muted = true; setMuted(true); scheduleRetry(); } }; unmuteTimer.current = setTimeout(tryPlay, delay); }, [forceMute], ); useEffect( () => () => { clearTimeout(restartTimer.current); clearTimeout(unmuteTimer.current); }, [], ); useEffect(() => { if (status === 'playing') { attemptUnmute(0); } }, [status, attemptUnmute]); useEffect(() => { if (!sessionInfo?.url || !videoRef.current) { return undefined; } let active = true; let player; const resetMuteId = setTimeout(() => setMuted(true), 0); const handleStatus = (nextStatus, info) => { if (!active) return; setStatus(nextStatus); setDetail(info || null); if (['error', 'failed', 'disconnected', 'closed'].includes(nextStatus)) { scheduleRestart(); } }; player = new WhepPlayer({ url: sessionInfo.url, token: sessionInfo.token, video: videoRef.current, onStatus: handleStatus, }); player.start().catch((err) => { if (!active) return; setStatus('error'); setDetail(err.message); scheduleRestart(); }); return () => { active = false; clearTimeout(resetMuteId); player?.stop(); }; }, [sessionInfo?.url, sessionInfo?.token, restartToken, scheduleRestart]); useEffect(() => { if (status === 'stopped' && sessionInfo?.url) { scheduleRestart(); } }, [status, sessionInfo?.url, scheduleRestart]); const renderedStatus = !sessionInfo?.url ? 'waiting' : status === 'error' ? `Error: ${detail || 'unknown'}` : detail ? `${status} (${detail})` : status; return (
); } function BatteryBar({ charge, config, label, status }) { const full = config?.Full; const warn = config?.Warn; const urgent = config?.Urgent ?? null; if (charge == null || full == null || warn == null) { return (
{/* {label} */} {status}

Battery telemetry unavailable

); } const span = full - warn; if (span <= 0) return null; const normalized = (charge - warn) / span; const percent = Math.min(1, Math.max(0, normalized)); const percentDisplay = Math.round(percent * 100); const percentText = `${percentDisplay}%`; const depleted = normalized <= 0; const warnTriggered = urgent != null && charge <= urgent; const barClass = depleted ? 'bg-red-500 animate-pulse' : warnTriggered ? 'bg-amber-400' : 'bg-emerald-500'; return (
{label} {status}
{percentText}
); } function HudOverlay({ frame, session }) { const sensors = frame?.sensors; const bumps = sensors?.bumpsAndWheelDrops || {}; const [now, setNow] = useState(() => Date.now()); useEffect(() => { const interval = setInterval(() => setNow(Date.now()), 150); return () => clearInterval(interval); }, []); const pulse = frame?.receivedAt ? now - frame.receivedAt < 200 : false; // const bumperBadges = [ // { label: 'B-L', active: bumps.bumpLeft }, // { label: 'B-R', active: bumps.bumpRight }, // ]; // const wheelBadges = [ // { label: 'Drop L', active: bumps.wheelDropLeft }, // { label: 'Drop R', active: bumps.wheelDropRight }, // ]; console.log('session', session); return (
{/*
sensor
{bumperBadges.map((badge) => ( ))}
{wheelBadges.map((badge) => ( ))}
*/} {/* status that tells you the name of your rover */} {/* bump and wheel drops bar */}
Left Bump
{/* left wheel drop */}
Left Wheel Drop
{/* right wheel drop */}
Right Wheel Drop
{/* right bump */}
Right Bump
); } // function HudBadge({ label, active }) { // return ( // // {label} // // ); // } const OVERCURRENT_LABELS = { leftWheel: 'Left wheel', rightWheel: 'Right wheel', mainBrush: 'Main brush', sideBrush: 'Side brush', }; function OvercurrentOverlay({ motors }) { if (!motors?.length) return null; const labels = motors.map((name) => OVERCURRENT_LABELS[name] || name); return (
Overcurrent
{labels.join(', ')}
); }