mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
ugh
This commit is contained in:
@@ -1,8 +1,52 @@
|
||||
export default function RoomCameraPanel() {
|
||||
import { useSession } from '../context/SessionContext.jsx';
|
||||
import { useVideoRequests } from '../hooks/useVideoRequests.js';
|
||||
import VideoTile from './VideoTile.jsx';
|
||||
|
||||
function EmptyState() {
|
||||
return (
|
||||
<div className="panel-section min-h-[8rem] space-y-0.5 text-base">
|
||||
<p className="text-center text-sm text-slate-400">Room camera</p>
|
||||
<p className="text-center text-slate-200">Feed placeholder. Wire upcoming room cam here.</p>
|
||||
<div className="panel-section space-y-0.5 text-sm">
|
||||
<p className="text-center text-slate-400">No room cameras configured.</p>
|
||||
<p className="text-center text-slate-500">Add entries to server config to populate this list.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RoomCameraPanel() {
|
||||
const { session } = useSession();
|
||||
const cameras = session?.roomCameras || [];
|
||||
const sourceDescriptors = cameras.map((camera) => ({ type: 'room', id: camera.id, key: `room:${camera.id}` }));
|
||||
const videoSources = useVideoRequests(sourceDescriptors);
|
||||
|
||||
if (cameras.length === 0) {
|
||||
return <EmptyState />;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="panel-section space-y-0.5 text-base">
|
||||
<header className="flex items-center justify-between text-sm text-slate-400">
|
||||
<p>Room cameras</p>
|
||||
<span>{cameras.length}</span>
|
||||
</header>
|
||||
<div className="grid gap-0.5 sm:grid-cols-2">
|
||||
{cameras.map((camera) => {
|
||||
const key = `room:${camera.id}`;
|
||||
const sessionInfo = videoSources[key];
|
||||
return (
|
||||
<article key={camera.id} className="space-y-0.5 rounded border border-slate-800 bg-zinc-950 p-0.5">
|
||||
<header className="space-y-0.5">
|
||||
<p className="text-lg font-semibold text-white">{camera.name || camera.id}</p>
|
||||
{camera.description && <p className="text-xs text-slate-500">{camera.description}</p>}
|
||||
</header>
|
||||
<VideoTile
|
||||
sessionInfo={sessionInfo}
|
||||
label={camera.name || camera.id}
|
||||
forceMute
|
||||
showBatteryBar={false}
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,14 @@ 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 }) {
|
||||
export default function VideoTile({
|
||||
sessionInfo,
|
||||
label,
|
||||
forceMute = false,
|
||||
telemetryFrame,
|
||||
batteryConfig,
|
||||
showBatteryBar = true,
|
||||
}) {
|
||||
const videoRef = useRef(null);
|
||||
const restartTimer = useRef(null);
|
||||
const unmuteTimer = useRef(null);
|
||||
@@ -27,7 +34,6 @@ export default function VideoTile({ sessionInfo, label, forceMute = false, telem
|
||||
: Object.entries(wheelOvercurrents)
|
||||
.filter(([, active]) => Boolean(active))
|
||||
.map(([key]) => key);
|
||||
const overcurrentActive = overcurrentMotors.length > 0;
|
||||
|
||||
const scheduleRestart = useCallback(() => {
|
||||
clearTimeout(restartTimer.current);
|
||||
@@ -147,6 +153,14 @@ export default function VideoTile({ sessionInfo, label, forceMute = false, telem
|
||||
? `${status} (${detail})`
|
||||
: status;
|
||||
|
||||
const renderStatusSection = () => (
|
||||
<div className="panel-section space-y-0.5 text-sm">
|
||||
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||
<span>{renderedStatus}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="relative w-full overflow-hidden bg-black aspect-video">
|
||||
@@ -161,12 +175,16 @@ export default function VideoTile({ sessionInfo, label, forceMute = false, telem
|
||||
<HudOverlay frame={telemetryFrame} label={label}/>
|
||||
<OvercurrentOverlay motors={overcurrentMotors} />
|
||||
</div>
|
||||
<BatteryBar charge={batteryCharge} config={batteryConfig} label={label} status={renderedStatus} />
|
||||
{showBatteryBar ? (
|
||||
<BatteryBar charge={batteryCharge} config={batteryConfig} label={label} status={renderedStatus} />
|
||||
) : (
|
||||
renderStatusSection()
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BatteryBar({ charge, config, label, status }) {
|
||||
function BatteryBar({ charge, config, status }) {
|
||||
const full = config?.Full;
|
||||
const warn = config?.Warn;
|
||||
const urgent = config?.Urgent ?? null;
|
||||
|
||||
@@ -1,23 +1,75 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSocket } from '../context/SocketContext.jsx';
|
||||
|
||||
export function useVideoRequests(roverIds = []) {
|
||||
function normalizeEntry(entry) {
|
||||
if (!entry) return null;
|
||||
if (typeof entry === 'string') {
|
||||
const id = String(entry).trim();
|
||||
if (!id) return null;
|
||||
return { type: 'rover', id, key: id };
|
||||
}
|
||||
if (typeof entry === 'object') {
|
||||
if (entry.type && entry.id) {
|
||||
const id = String(entry.id);
|
||||
return { type: entry.type, id, key: entry.key || `${entry.type}:${id}` };
|
||||
}
|
||||
if (entry.roverId) {
|
||||
const id = String(entry.roverId);
|
||||
return { type: 'rover', id, key: entry.key || id };
|
||||
}
|
||||
if (entry.roomCameraId) {
|
||||
const id = String(entry.roomCameraId);
|
||||
return { type: 'room', id, key: entry.key || `room:${id}` };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function dedupeEntries(entries = []) {
|
||||
const seen = new Set();
|
||||
const unique = [];
|
||||
entries.forEach((entry) => {
|
||||
if (!entry?.key || seen.has(entry.key)) {
|
||||
return;
|
||||
}
|
||||
seen.add(entry.key);
|
||||
unique.push(entry);
|
||||
});
|
||||
return unique.sort((a, b) => a.key.localeCompare(b.key));
|
||||
}
|
||||
|
||||
export function useVideoRequests(sourceList = []) {
|
||||
const socket = useSocket();
|
||||
const [sources, setSources] = useState({});
|
||||
const normalizedKey = Array.isArray(roverIds) ? roverIds.filter(Boolean).join('|') : '';
|
||||
const normalizedEntries = useMemo(() => {
|
||||
if (!Array.isArray(sourceList)) {
|
||||
return [];
|
||||
}
|
||||
return dedupeEntries(sourceList.map(normalizeEntry).filter(Boolean));
|
||||
}, [sourceList]);
|
||||
const normalizedKey = useMemo(
|
||||
() => normalizedEntries.map((entry) => `${entry.type}:${entry.id}:${entry.key}`).join('|'),
|
||||
[normalizedEntries],
|
||||
);
|
||||
const entriesRef = useRef([]);
|
||||
|
||||
useEffect(() => {
|
||||
entriesRef.current = normalizedEntries;
|
||||
}, [normalizedEntries]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!normalizedKey) {
|
||||
return undefined;
|
||||
}
|
||||
const ids = normalizedKey.split('|');
|
||||
const entries = entriesRef.current;
|
||||
let cancelled = false;
|
||||
ids.forEach((roverId) => {
|
||||
socket.emit('video:request', { roverId }, (resp = {}) => {
|
||||
entries.forEach((entry) => {
|
||||
const payload = entry.type === 'room' ? { roomCameraId: entry.id } : { roverId: entry.id };
|
||||
socket.emit('video:request', payload, (resp = {}) => {
|
||||
if (cancelled) return;
|
||||
setSources((prev) => ({
|
||||
...prev,
|
||||
[roverId]: resp,
|
||||
[entry.key]: resp,
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -29,15 +81,14 @@ export function useVideoRequests(roverIds = []) {
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!normalizedKey) return {};
|
||||
const ids = normalizedKey.split('|');
|
||||
const next = {};
|
||||
ids.forEach((id) => {
|
||||
if (sources[id]) {
|
||||
next[id] = sources[id];
|
||||
normalizedEntries.forEach((entry) => {
|
||||
if (sources[entry.key]) {
|
||||
next[entry.key] = sources[entry.key];
|
||||
}
|
||||
});
|
||||
return next;
|
||||
}, [normalizedKey, sources]);
|
||||
}, [normalizedKey, sources, normalizedEntries]);
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user