This commit is contained in:
legop3
2026-07-11 14:07:20 -04:00
parent 6747658106
commit 18649deeae
8 changed files with 95 additions and 13 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
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
@@ -78,8 +78,8 @@
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
<title>Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-BnruAIv2.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-s_YS9lA-.css">
<script type="module" crossorigin src="/assets/index-DF69h8i3.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-MAURNhur.css">
</head>
<body>
<div id="root"></div>
+71 -1
View File
@@ -52,6 +52,18 @@ const state = {
status: null,
light: null,
ir: null,
publisher: {
running: false,
pid: null,
startedAt: null,
restartAt: null,
restartCount: 0,
exitCode: null,
exitSignal: null,
exitedAt: null,
lastStderr: '',
lastEvent: 'idle',
},
};
let onvifCam = null;
@@ -61,6 +73,7 @@ let turnTimer = null;
let blockedTimer = null;
let publisherProcess = null;
let publisherRestartTimer = null;
let publisherStderrSyncTimer = null;
let snapshotTimer = null;
let spotlightVerifyTimer = null;
let vendorStatePromise = Promise.resolve();
@@ -73,6 +86,27 @@ function emitChange(reason = 'change') {
events.emit('change', { reason, state: getPublicState() });
}
function schedulePublisherStateSync(reason = 'publisher') {
/*
ffmpeg can print many warning/progress lines in bursts. Keep the latest text
in state immediately, but debounce session sync so one noisy transcoder does
not force every connected client to resync for each stderr chunk.
*/
if (publisherStderrSyncTimer) return;
publisherStderrSyncTimer = setTimeout(() => {
publisherStderrSyncTimer = null;
emitChange(reason);
}, 500);
}
function updatePublisherState(patch = {}, reason = 'publisher') {
state.publisher = {
...(state.publisher || {}),
...patch,
};
emitChange(reason);
}
function clampUnit(value) {
const number = Number(value) || 0;
return Math.max(-1, Math.min(1, number));
@@ -186,6 +220,7 @@ function getPublicState(socket = null) {
status: state.status,
light: state.light,
ir: state.ir,
publisher: state.publisher,
isOperator: Boolean(socketId && state.operatorSocketId === socketId),
queuedPosition: socketId ? state.queue.indexOf(socketId) + 1 || null : null,
canUse: socket ? canUsePtzFeature(socket) : false,
@@ -249,6 +284,12 @@ function stopPublisher() {
} catch {}
publisherProcess = null;
}
updatePublisherState({
running: false,
pid: null,
restartAt: null,
lastEvent: 'stopped',
}, 'publisher-stop');
}
function startPublisher() {
@@ -337,13 +378,42 @@ function startPublisher() {
output,
], { stdio: ['ignore', 'ignore', 'pipe'] });
publisherProcess = proc;
updatePublisherState({
running: true,
pid: proc.pid || null,
startedAt: Date.now(),
restartAt: null,
exitCode: null,
exitSignal: null,
exitedAt: null,
lastEvent: 'started',
}, 'publisher-start');
proc.stderr.on('data', (chunk) => {
const text = String(chunk || '').trim();
if (text) logger.warn('publisher stderr', { text: text.slice(0, 500) });
if (!text) return;
const lastStderr = text.slice(-1000);
state.publisher = {
...(state.publisher || {}),
lastStderr,
lastEvent: 'stderr',
};
logger.warn('publisher stderr', { text: text.slice(0, 500) });
schedulePublisherStateSync('publisher-stderr');
});
proc.on('exit', (code, signal) => {
if (publisherProcess === proc) publisherProcess = null;
logger.warn('publisher exited', { code, signal });
const restartAt = enabled && state.rtspUri ? Date.now() + 1500 : null;
updatePublisherState({
running: false,
pid: null,
restartAt,
restartCount: Number(state.publisher?.restartCount || 0) + (restartAt ? 1 : 0),
exitCode: code,
exitSignal: signal,
exitedAt: Date.now(),
lastEvent: restartAt ? 'restarting' : 'exited',
}, 'publisher-exit');
if (enabled && state.rtspUri) {
publisherRestartTimer = setTimeout(() => {
publisherRestartTimer = null;
@@ -107,6 +107,12 @@ function PtzQueueList({ queue = [], operatorLabel = '' }) {
function PtzStatePanel({ ptz, onClose, onRelease, releaseDisabled = false }) {
const spotlightOn = isSpotlightOn(ptz?.light);
const irMode = normalizeIrMode(ptz?.ir?.state);
const publisher = ptz?.publisher || {};
const publisherStatus = publisher.running
? `running${publisher.pid ? ` ${publisher.pid}` : ''}`
: publisher.restartAt
? 'restarting'
: publisher.lastEvent || 'stopped';
const statusTone = ptz?.error ? 'text-amber-300' : ptz?.isOperator ? 'text-emerald-300' : 'text-slate-100';
return (
@@ -121,6 +127,12 @@ function PtzStatePanel({ ptz, onClose, onRelease, releaseDisabled = false }) {
<StatusRow label="Spotlight" value={spotlightOn ? 'On' : 'Off'} tone={spotlightOn ? 'text-emerald-300' : 'text-slate-200'} />
<StatusRow label="Infrared mode" value={irMode} />
<StatusRow label="Stream" value={ptz?.status || ptz?.error || 'idle'} tone={ptz?.error ? 'text-amber-300' : ''} />
<StatusRow label="Transcoder" value={publisherStatus} tone={publisher.running ? 'text-emerald-300' : 'text-amber-300'} />
{publisher.lastStderr ? (
<div className="surface max-h-24 overflow-y-auto whitespace-pre-wrap break-words font-mono text-[0.68rem] leading-tight text-slate-200">
{publisher.lastStderr}
</div>
) : null}
{ptz?.blocked?.message ? (
<div className="rounded border border-amber-500/50 bg-amber-950/40 p-1 text-xs text-amber-100">
{ptz.blocked.message}