mini viewer!!

This commit is contained in:
legop3
2025-12-22 00:37:47 -05:00
parent e0a64e828e
commit e3874b6237
8 changed files with 191 additions and 16 deletions
+1 -1
View File
@@ -2,5 +2,5 @@
- new page
- designed for a very small screen
- not interactive at all
- a small summarized status of what's going on in the basement
- a small summarized status of what's going on in the basement (where the rovers are)
- this small screen does not have space to show all info and all video for all rovers
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -11,8 +11,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-CgezuFMn.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DkBejE8J.css">
<script type="module" crossorigin src="/assets/index-BpyM0lPm.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B88Ix9u6.css">
</head>
<body>
<div id="root"></div>
+2
View File
@@ -8,6 +8,7 @@ import { SessionProvider } from './context/SessionContext.jsx'
import { TelemetryProvider } from './context/TelemetryContext.jsx'
import { ChatProvider } from './context/ChatContext.jsx'
import SpectatorApp from './spectate/SpectatorApp.jsx'
import MiniSummaryApp from './mini/MiniSummaryApp.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
@@ -19,6 +20,7 @@ createRoot(document.getElementById('root')).render(
<Routes>
<Route path="/" element={<App />} />
<Route path="/spectate" element={<SpectatorApp />} />
<Route path="/mini" element={<MiniSummaryApp />} />
</Routes>
</BrowserRouter>
</ChatProvider>
+173
View File
@@ -0,0 +1,173 @@
import { useEffect, useMemo, useState } from 'react';
import { SettingsProvider } from '../settings/index.js';
import { useSession } from '../context/SessionContext.jsx';
import { useTelemetryFrames } from '../context/TelemetryContext.jsx';
import { useVideoRequests } from '../hooks/useVideoRequests.js';
import { useSpectatorMode } from '../hooks/useSpectatorMode.js';
import { useRoomCameraSnapshots } from '../hooks/useRoomCameraSnapshots.js';
import VideoTile from '../components/VideoTile.jsx';
import ChatPanel from '../components/ChatPanel.jsx';
const ROTATE_MS = 9000;
function formatDriverLabel({ roverId, session }) {
const activeDriverId = session?.activeDrivers?.[roverId] || null;
const user = (session?.users || []).find((entry) => entry.socketId === activeDriverId);
const label = user?.nickname || (activeDriverId ? activeDriverId.slice(0, 6) : 'No driver');
const mode = session?.mode;
const turnInfo = session?.turnQueues?.[roverId];
return mode === 'turns' && turnInfo?.current ? `${label} (turns)` : label;
}
function MiniSummaryContent() {
const { session } = useSession();
const spectatorReady = useSpectatorMode();
const frames = useTelemetryFrames();
const roster = session?.roster ?? [];
const roomCameras = session?.roomCameras || [];
const feeds = useRoomCameraSnapshots(roomCameras.map((camera) => ({ id: camera.id })));
const [index, setIndex] = useState(0);
const entries = useMemo(
() =>
roster.flatMap((rover) => {
if (!rover?.id) return [];
const id = String(rover.id);
const base = [{ type: 'rover', id, key: id }];
if (rover.media?.audioPublishUrl) {
base.push({ type: 'rover', id: `${id}-audio`, key: `${id}-audio` });
}
return base;
}),
[roster],
);
const videoSources = useVideoRequests(entries);
const roverPool = useMemo(() => {
if (!roster.length) return [];
const withVideo = roster.filter((rover) => videoSources[rover.id]?.url);
return withVideo.length ? withVideo : roster;
}, [roster, videoSources]);
const rotationPool = useMemo(() => {
const items = [];
roverPool.forEach((rover) => items.push({ type: 'rover', rover }));
roomCameras.forEach((camera) => items.push({ type: 'room', camera }));
return items;
}, [roverPool, roomCameras]);
const rotationKey = useMemo(
() =>
rotationPool
.map((entry) =>
entry.type === 'rover' ? `r:${entry.rover.id}` : `room:${entry.camera.id}`,
)
.join('|'),
[rotationPool],
);
useEffect(() => {
setIndex(0);
}, [rotationKey]);
useEffect(() => {
if (!rotationPool.length) return undefined;
const timer = setInterval(() => {
setIndex((prev) => (prev + 1) % rotationPool.length);
}, ROTATE_MS);
return () => clearInterval(timer);
}, [rotationPool.length, rotationKey]);
const activeEntry = rotationPool.length ? rotationPool[index % rotationPool.length] : null;
const activeRover = activeEntry?.type === 'rover' ? activeEntry.rover : null;
const activeCamera = activeEntry?.type === 'room' ? activeEntry.camera : null;
const activeVideo = activeRover ? videoSources[activeRover.id] || null : null;
const activeAudio = activeRover ? videoSources[`${activeRover.id}-audio`] || null : null;
const activeFrame = activeRover ? frames[activeRover.id] || null : null;
const driverLabel = activeRover ? formatDriverLabel({ roverId: activeRover.id, session }) : null;
const activeFeed = activeCamera ? feeds[activeCamera.id] || null : null;
return (
<div className="relative h-screen w-screen overflow-hidden bg-black p-0.5 text-slate-100 flex flex-col gap-0.5">
<ChatOverlay />
<section className="panel relative flex min-h-0 flex-1 overflow-hidden">
{!spectatorReady ? (
<div className="flex h-full w-full items-center justify-center text-sm text-slate-500">
Switching to spectator
</div>
) : activeRover ? (
<div className="relative h-full w-full">
<VideoTile
sessionInfo={activeVideo}
audioSessionInfo={activeAudio}
label={activeRover.name || activeRover.id}
telemetryFrame={activeFrame}
batteryConfig={activeRover.battery}
layoutFormat="mobile"
hudVariant="spectator"
driverLabel={driverLabel}
hudForceMap
hudMapPosition="bottom-left"
/>
</div>
) : activeCamera ? (
<RoomCameraFrame camera={activeCamera} feed={activeFeed} />
) : (
<div className="flex h-full w-full items-center justify-center text-sm text-slate-500">
No sources available.
</div>
)}
</section>
</div>
);
}
export default function MiniSummaryApp() {
return (
<SettingsProvider>
<MiniSummaryContent />
</SettingsProvider>
);
}
function RoomCameraFrame({ camera, feed }) {
const hasImage = feed?.objectUrl;
const connecting = feed && feed.status === 'connecting';
return (
<div className="relative h-full w-full bg-zinc-950">
{hasImage ? (
<img
src={feed.objectUrl}
alt={camera.name || camera.id}
className="h-full w-full object-cover"
draggable={false}
/>
) : (
<div className="flex h-full w-full items-center justify-center text-sm text-slate-500">
{connecting ? `Connecting to ${camera.name || camera.id}` : 'No frame yet'}
</div>
)}
<div className="absolute left-1 top-1 rounded bg-black/60 px-1 py-0.25 text-xs text-slate-200">
{camera.name || camera.id}
</div>
</div>
);
}
function ChatOverlay() {
return (
<div
className="pointer-events-none absolute left-1/2 top-1 z-30"
style={{ transform: 'translate(-50%, 0) scale(0.7)', transformOrigin: 'top center' }}
>
<div
className="pointer-events-none overflow-hidden rounded-md"
style={{ width: '50vw', minWidth: '16rem', maxWidth: '24rem', opacity: 0.55, maxHeight: '12rem' }}
>
<ChatPanel hideInput hideSpectatorNotice />
</div>
</div>
);
}