mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
stoatus
This commit is contained in:
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
@@ -78,7 +78,7 @@
|
||||
<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-CGjNAimd.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-DPoEKYWc.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-MAURNhur.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -63,6 +63,7 @@ const state = {
|
||||
exitSignal: null,
|
||||
exitedAt: null,
|
||||
lastStderr: '',
|
||||
progress: null,
|
||||
lastEvent: 'idle',
|
||||
},
|
||||
};
|
||||
@@ -108,6 +109,65 @@ function updatePublisherState(patch = {}, reason = 'publisher') {
|
||||
emitChange(reason);
|
||||
}
|
||||
|
||||
function parsePublisherProgressLine(line) {
|
||||
/*
|
||||
ffmpeg's "-progress pipe:2" emits simple key=value telemetry on stderr.
|
||||
Warning lines also arrive on stderr, so keep parsing narrow: only accept the
|
||||
known progress keys and let everything else remain user-visible stderr.
|
||||
This gives the UI enough signal to tell whether the transcoder is actually
|
||||
falling behind without flooding normal server logs.
|
||||
*/
|
||||
const match = String(line || '').match(/^([a-zA-Z_][a-zA-Z0-9_]*)=(.*)$/);
|
||||
if (!match) return false;
|
||||
const [, key, rawValue] = match;
|
||||
const allowed = new Set([
|
||||
'frame',
|
||||
'fps',
|
||||
'stream_0_0_q',
|
||||
'bitrate',
|
||||
'total_size',
|
||||
'out_time_us',
|
||||
'out_time_ms',
|
||||
'out_time',
|
||||
'dup_frames',
|
||||
'drop_frames',
|
||||
'speed',
|
||||
'progress',
|
||||
]);
|
||||
if (!allowed.has(key)) return false;
|
||||
state.publisher = {
|
||||
...(state.publisher || {}),
|
||||
progress: {
|
||||
...(state.publisher?.progress || {}),
|
||||
[key]: rawValue,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
lastEvent: 'progress',
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
function handlePublisherStderr(chunk) {
|
||||
const text = String(chunk || '').trim();
|
||||
if (!text) return;
|
||||
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
||||
const warningLines = [];
|
||||
|
||||
lines.forEach((line) => {
|
||||
if (!parsePublisherProgressLine(line)) warningLines.push(line);
|
||||
});
|
||||
|
||||
if (warningLines.length) {
|
||||
state.publisher = {
|
||||
...(state.publisher || {}),
|
||||
lastStderr: warningLines.join('\n').slice(-1000),
|
||||
lastEvent: 'stderr',
|
||||
};
|
||||
}
|
||||
|
||||
schedulePublisherStateSync(warningLines.length ? 'publisher-stderr' : 'publisher-progress');
|
||||
}
|
||||
|
||||
function clampUnit(value) {
|
||||
const number = Number(value) || 0;
|
||||
return Math.max(-1, Math.min(1, number));
|
||||
@@ -322,12 +382,21 @@ function startPublisher() {
|
||||
flags caused this camera stream to freeze after running for a while, so the
|
||||
safer latency knob is to keep the encoder light and avoid building delay
|
||||
inside x264 itself.
|
||||
|
||||
The mpegts muxer can also hold packets briefly before writing them to SRT.
|
||||
flush_packets/muxdelay/muxpreload are output-side latency knobs; they do not
|
||||
ask the camera or demuxer to discard frames, so they are a safer next step
|
||||
than the stale-frame dropping experiments that made the Reolink feed freeze.
|
||||
*/
|
||||
const proc = spawn('ffmpeg', [
|
||||
'-hide_banner',
|
||||
'-loglevel',
|
||||
'warning',
|
||||
'-nostdin',
|
||||
'-progress',
|
||||
'pipe:2',
|
||||
'-stats_period',
|
||||
'2',
|
||||
'-fflags',
|
||||
'nobuffer',
|
||||
'-flags',
|
||||
@@ -374,6 +443,12 @@ function startPublisher() {
|
||||
'48000',
|
||||
'-strict',
|
||||
'-2',
|
||||
'-flush_packets',
|
||||
'1',
|
||||
'-muxdelay',
|
||||
'0',
|
||||
'-muxpreload',
|
||||
'0',
|
||||
'-f',
|
||||
'mpegts',
|
||||
output,
|
||||
@@ -389,17 +464,7 @@ function startPublisher() {
|
||||
exitedAt: null,
|
||||
lastEvent: 'started',
|
||||
}, 'publisher-start');
|
||||
proc.stderr.on('data', (chunk) => {
|
||||
const text = String(chunk || '').trim();
|
||||
if (!text) return;
|
||||
const lastStderr = text.slice(-1000);
|
||||
state.publisher = {
|
||||
...(state.publisher || {}),
|
||||
lastStderr,
|
||||
lastEvent: 'stderr',
|
||||
};
|
||||
schedulePublisherStateSync('publisher-stderr');
|
||||
});
|
||||
proc.stderr.on('data', handlePublisherStderr);
|
||||
proc.on('exit', (code, signal) => {
|
||||
if (publisherProcess === proc) publisherProcess = null;
|
||||
logger.warn('publisher exited', { code, signal });
|
||||
|
||||
@@ -77,6 +77,22 @@ function StatusRow({ label, value, tone = '' }) {
|
||||
);
|
||||
}
|
||||
|
||||
function formatPublisherProgress(progress = null) {
|
||||
/*
|
||||
ffmpeg progress is intentionally shown as raw operational numbers instead
|
||||
of translated prose. When the PTZ video feels delayed, fps/speed/drop/out
|
||||
time make it obvious whether ffmpeg itself is keeping up or the delay is
|
||||
somewhere before/after the transcoder.
|
||||
*/
|
||||
if (!progress) return null;
|
||||
return [
|
||||
progress.fps ? `fps ${progress.fps}` : null,
|
||||
progress.speed ? `speed ${progress.speed}` : null,
|
||||
progress.drop_frames ? `drop ${progress.drop_frames}` : null,
|
||||
progress.out_time ? `out ${progress.out_time}` : null,
|
||||
].filter(Boolean).join(' | ');
|
||||
}
|
||||
|
||||
function PtzQueueList({ queue = [], operatorLabel = '' }) {
|
||||
const hasQueue = Array.isArray(queue) && queue.length > 0;
|
||||
return (
|
||||
@@ -111,6 +127,7 @@ function PtzStatePanel({ ptz, onClose, onRelease, releaseDisabled = false }) {
|
||||
: publisher.restartAt
|
||||
? 'restarting'
|
||||
: publisher.lastEvent || 'stopped';
|
||||
const publisherProgress = formatPublisherProgress(publisher.progress);
|
||||
const statusTone = ptz?.error ? 'text-amber-300' : ptz?.isOperator ? 'text-emerald-300' : 'text-slate-100';
|
||||
|
||||
return (
|
||||
@@ -126,6 +143,7 @@ function PtzStatePanel({ ptz, onClose, onRelease, releaseDisabled = false }) {
|
||||
<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'} />
|
||||
{publisherProgress ? <StatusRow label="Progress" value={publisherProgress} /> : null}
|
||||
{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}
|
||||
|
||||
@@ -40,6 +40,19 @@ function InfoRow({ label, value, tone = '' }) {
|
||||
);
|
||||
}
|
||||
|
||||
function formatPublisherProgress(progress = null) {
|
||||
/*
|
||||
Keep the spectator card dense, but expose enough ffmpeg progress to tell if
|
||||
the PTZ transcoder is running behind when the live feed looks delayed.
|
||||
*/
|
||||
if (!progress) return null;
|
||||
return [
|
||||
progress.fps ? `fps ${progress.fps}` : null,
|
||||
progress.speed ? `speed ${progress.speed}` : null,
|
||||
progress.drop_frames ? `drop ${progress.drop_frames}` : null,
|
||||
].filter(Boolean).join(' | ');
|
||||
}
|
||||
|
||||
function PtzSnapshotFallback({ label, source }) {
|
||||
const snapshotFeeds = usePtzCameraSnapshots([PTZ_CAMERA_ID], { enabled: true });
|
||||
const snapshot = snapshotFeeds[PTZ_CAMERA_ID] || null;
|
||||
@@ -85,6 +98,7 @@ export default function PtzSpectatorCard() {
|
||||
: publisher.restartAt
|
||||
? 'restarting'
|
||||
: publisher.lastEvent || 'stopped';
|
||||
const publisherProgress = formatPublisherProgress(publisher.progress);
|
||||
const queueCount = Array.isArray(ptz?.queue) ? ptz.queue.length : 0;
|
||||
const label = ptz?.name || 'PTZ Camera';
|
||||
|
||||
@@ -98,6 +112,7 @@ export default function PtzSpectatorCard() {
|
||||
<InfoRow label="Spotlight" value={isSpotlightOn(ptz?.light) ? 'On' : 'Off'} />
|
||||
<InfoRow label="Infrared" value={normalizeInfraredMode(ptz?.ir?.state)} />
|
||||
<InfoRow label="Transcoder" value={publisherStatus} tone={publisher.running ? 'text-emerald-300' : 'text-amber-300'} />
|
||||
{publisherProgress ? <InfoRow label="Progress" value={publisherProgress} /> : null}
|
||||
{publisher.lastStderr ? (
|
||||
<div className="line-clamp-2 break-words font-mono text-[0.65rem] leading-tight text-slate-400">
|
||||
{publisher.lastStderr}
|
||||
|
||||
Reference in New Issue
Block a user