This commit is contained in:
legop3
2025-12-03 00:48:25 -05:00
parent e94a52d7fd
commit e0312c3fbb
8 changed files with 22 additions and 25 deletions
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-D8Lb2mUY.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DQ1jpCiS.css">
<script type="module" crossorigin src="/assets/index-ba8YMKfH.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-7IHa35aL.css">
</head>
<body>
<div id="root"></div>
@@ -4,20 +4,14 @@ const { getRoomCameras, roomCameraEvents } = require('./roomCameraService');
const POLL_INTERVAL_MS = 800;
const FETCH_TIMEOUT_MS = 2000;
const STALE_AFTER_MS = 8000;
const cameraState = new Map(); // id -> {frame, ts, stale, error, failures, fetching}
const cameraState = new Map(); // id -> {frame, ts, error, failures, fetching}
const events = new EventEmitter(); // frame, status
let pollTimer = null;
function markState(id, updates = {}) {
const prev = cameraState.get(id) || {};
const next = { ...prev, ...updates };
if (next.ts != null) {
next.stale = Date.now() - next.ts > STALE_AFTER_MS || !!next.stale;
} else {
next.stale = true;
}
cameraState.set(id, next);
return next;
}
@@ -37,8 +31,8 @@ async function fetchSnapshot(camera) {
const arrayBuffer = await res.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const ts = Date.now();
const next = markState(id, { frame: buffer, ts, error: null, failures: 0 });
events.emit('frame', { id, buffer, ts, stale: !!next.stale });
markState(id, { frame: buffer, ts, error: null, failures: 0 });
events.emit('frame', { id, buffer, ts });
} catch (err) {
const failures = (state?.failures || 0) + 1;
markState(id, { error: err.message, failures });
@@ -70,11 +64,9 @@ function startAll() {
function getState(id) {
const state = cameraState.get(id);
if (!state) return null;
const stale = state.ts == null || Date.now() - state.ts > STALE_AFTER_MS;
return {
frame: state.frame || null,
ts: state.ts || null,
stale,
error: state.error || null,
};
}
@@ -82,13 +82,13 @@ function sendStatus(socket, cameraId, status) {
socket.emit('roomCamera:status', { id: cameraId, ...status });
}
roomCameraStreamEvents.on('frame', ({ id, buffer, ts, stale }) => {
roomCameraStreamEvents.on('frame', ({ id, buffer, ts }) => {
const bucket = cameraSubscribers.get(id);
if (!bucket || !buffer) return;
bucket.forEach((socketId) => {
const socket = io.sockets.sockets.get(socketId);
if (!socket) return;
sendFrame(socket, id, { ts, stale: !!stale }, buffer);
sendFrame(socket, id, { ts }, buffer);
});
});
@@ -123,11 +123,10 @@ io.on('connection', (socket) => {
validIds.forEach((cameraId) => {
const state = getRoomCameraState(cameraId);
if (state?.frame) {
sendFrame(socket, cameraId, { ts: state.ts, stale: !!state.stale }, state.frame);
sendFrame(socket, cameraId, { ts: state.ts }, state.frame);
}
sendStatus(socket, cameraId, {
ts: state?.ts || null,
stale: state?.stale ?? true,
error: state?.error || null,
});
});
+12 -4
View File
@@ -1,10 +1,17 @@
import { useMemo } from 'react';
import { useEffect, useMemo, useState } from 'react';
export default function RoomCameraFeed({ feed, label }) {
const [blink, setBlink] = useState(false);
useEffect(() => {
if (!feed) return;
if (!feed.ts && !feed.objectUrl) return;
setBlink((prev) => !prev);
}, [feed?.ts, feed?.objectUrl]);
const statusText = useMemo(() => {
if (!feed) return 'Connecting…';
if (feed.error) return `Error: ${feed.error}`;
if (feed.status === 'playing' && feed.stale) return 'Stale frame';
return feed.status || 'Connecting…';
}, [feed]);
@@ -18,8 +25,9 @@ export default function RoomCameraFeed({ feed, label }) {
<div className="pointer-events-none absolute left-0 top-0 bg-black/70 px-0.5 py-0.5 text-xs font-semibold text-white">
{label}
</div>
<div className="pointer-events-none absolute bottom-0 left-0 m-0.5 rounded bg-black/70 px-0.5 py-0.25 text-[0.7rem] text-slate-100">
{statusText}
<div className="pointer-events-none absolute bottom-0 left-0 m-0.5 flex items-center gap-1 rounded bg-black/70 px-0.5 py-0.25 text-[0.7rem] text-slate-100">
<span className={`h-2 w-2 rounded-full ${blink ? 'bg-emerald-400' : 'bg-slate-500'}`} />
<span>{statusText}</span>
</div>
</div>
);
@@ -44,7 +44,6 @@ export function useRoomCameraSnapshots(sourceList = []) {
...prev,
[meta.id]: {
status: 'playing',
stale: !!meta.stale,
ts: meta.ts || Date.now(),
error: null,
objectUrl: url,
@@ -60,7 +59,6 @@ export function useRoomCameraSnapshots(sourceList = []) {
...(prev[meta.id] || {}),
status: meta.error ? 'error' : prev[meta.id]?.status || 'connecting',
error: meta.error || null,
stale: meta.stale ?? prev[meta.id]?.stale ?? true,
ts: meta.ts || prev[meta.id]?.ts || null,
objectUrl: prev[meta.id]?.objectUrl || null,
},