mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
132 lines
4.3 KiB
JavaScript
132 lines
4.3 KiB
JavaScript
// Hook: useRoverSnapshots
|
|
// Purpose: Subscribes to rover snapshot streams and stores latest per-rover image states. Scope: Manages socket request/subscription lifecycle and data normalization.
|
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
import { useSocket } from '../context/SocketContext.jsx';
|
|
|
|
export function useRoverSnapshots(sourceList = [], options = {}) {
|
|
const socket = useSocket();
|
|
const { enabled = true, version = null } = options;
|
|
const [feeds, setFeeds] = useState({});
|
|
const objectUrls = useRef(new Map());
|
|
const ids = useMemo(
|
|
() => sourceList.map((e) => (typeof e === 'string' ? e : e.id)).filter(Boolean),
|
|
[sourceList],
|
|
);
|
|
const idsKey = useMemo(() => {
|
|
const base = ids.join('|');
|
|
return version ? `${base}|v:${version}` : base;
|
|
}, [ids, version]);
|
|
const idsRef = useRef([]);
|
|
const [connectionNonce, setConnectionNonce] = useState(0);
|
|
const statsRef = useRef(new Map());
|
|
const debugSnapshots =
|
|
typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('debugSnapshots');
|
|
|
|
useEffect(() => {
|
|
if (!socket) return undefined;
|
|
const handleConnect = () => setConnectionNonce((prev) => prev + 1);
|
|
socket.on('connect', handleConnect);
|
|
return () => socket.off('connect', handleConnect);
|
|
}, [socket]);
|
|
|
|
useEffect(() => {
|
|
idsRef.current = ids;
|
|
}, [idsKey, ids]);
|
|
|
|
useEffect(() => {
|
|
objectUrls.current.forEach((url) => URL.revokeObjectURL(url));
|
|
objectUrls.current.clear();
|
|
setFeeds({});
|
|
}, [idsKey]);
|
|
|
|
useEffect(() => {
|
|
if (!enabled) {
|
|
objectUrls.current.forEach((url) => URL.revokeObjectURL(url));
|
|
objectUrls.current.clear();
|
|
setFeeds({});
|
|
return undefined;
|
|
}
|
|
if (!idsRef.current.length || !socket) {
|
|
return undefined;
|
|
}
|
|
let cancelled = false;
|
|
const currentIds = idsRef.current;
|
|
|
|
const handleFrame = (meta = {}, buffer) => {
|
|
if (cancelled || !meta.id || !buffer) return;
|
|
const sizeBytes = buffer.byteLength ?? buffer.length ?? 0;
|
|
const now = Date.now();
|
|
const prevStats = statsRef.current.get(meta.id) || {
|
|
count: 0,
|
|
totalBytes: 0,
|
|
lastLogAt: 0,
|
|
};
|
|
const nextStats = {
|
|
count: prevStats.count + 1,
|
|
totalBytes: prevStats.totalBytes + sizeBytes,
|
|
lastLogAt: prevStats.lastLogAt,
|
|
};
|
|
if (debugSnapshots && (!nextStats.lastLogAt || now - nextStats.lastLogAt >= 10000)) {
|
|
const avgBytes = nextStats.count ? nextStats.totalBytes / nextStats.count : 0;
|
|
console.log(
|
|
'[roverSnapshot]',
|
|
meta.id,
|
|
`frame=${sizeBytes}B`,
|
|
`avg=${Math.round(avgBytes)}B`,
|
|
`count=${nextStats.count}`,
|
|
);
|
|
nextStats.lastLogAt = now;
|
|
}
|
|
statsRef.current.set(meta.id, nextStats);
|
|
const blob = new Blob([buffer], { type: 'image/jpeg' });
|
|
const url = URL.createObjectURL(blob);
|
|
const prevUrl = objectUrls.current.get(meta.id);
|
|
if (prevUrl) {
|
|
URL.revokeObjectURL(prevUrl);
|
|
}
|
|
objectUrls.current.set(meta.id, url);
|
|
setFeeds((prev) => ({
|
|
...prev,
|
|
[meta.id]: {
|
|
status: 'playing',
|
|
ts: meta.ts || Date.now(),
|
|
error: null,
|
|
objectUrl: url,
|
|
},
|
|
}));
|
|
};
|
|
|
|
const handleStatus = (meta = {}) => {
|
|
if (cancelled || !meta.id) return;
|
|
setFeeds((prev) => ({
|
|
...prev,
|
|
[meta.id]: {
|
|
...(prev[meta.id] || {}),
|
|
status: meta.error ? 'error' : prev[meta.id]?.status || 'connecting',
|
|
error: meta.error || null,
|
|
ts: meta.ts || prev[meta.id]?.ts || null,
|
|
objectUrl: prev[meta.id]?.objectUrl || null,
|
|
},
|
|
}));
|
|
};
|
|
|
|
socket.on('roverSnapshot:frame', handleFrame);
|
|
socket.on('roverSnapshot:status', handleStatus);
|
|
|
|
socket.emit('roverSnapshot:subscribe', { ids: currentIds }, (resp = {}) => {
|
|
if (resp.error) return;
|
|
});
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
socket.emit('roverSnapshot:unsubscribe', { ids: currentIds });
|
|
socket.off('roverSnapshot:frame', handleFrame);
|
|
socket.off('roverSnapshot:status', handleStatus);
|
|
objectUrls.current.forEach((url) => URL.revokeObjectURL(url));
|
|
objectUrls.current.clear();
|
|
};
|
|
}, [socket, idsKey, enabled, connectionNonce, debugSnapshots]);
|
|
|
|
return feeds;
|
|
}
|