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
+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>
);
}