From 5fdcb233f839e4fb51c0188520ac30b5836f781b Mon Sep 17 00:00:00 2001 From: legop3 Date: Tue, 13 Jan 2026 18:54:30 -0500 Subject: [PATCH] rover snapshots in server --- server/install_server.sh | 9 --- server/mediamtx/mediamtx.yml | 1 - server/mediamtx/rover-snapshot.sh | 32 ---------- server/src/services/replayBuildService.js | 7 +++ server/src/services/replaySegmentManager.js | 65 +++++++++++++++++++++ 5 files changed, 72 insertions(+), 42 deletions(-) delete mode 100644 server/mediamtx/rover-snapshot.sh diff --git a/server/install_server.sh b/server/install_server.sh index 463f50df..22e547c4 100755 --- a/server/install_server.sh +++ b/server/install_server.sh @@ -6,7 +6,6 @@ MEDIAMTX_BASE_URL="https://github.com/bluenviron/mediamtx/releases/download/v${M MEDIAMTX_BIN="/usr/local/bin/mediamtx" MEDIAMTX_CONF_DIR="/etc/mediamtx" MEDIAMTX_CONFIG="$MEDIAMTX_CONF_DIR/mediamtx.yml" -MEDIAMTX_SNAPSHOT_SCRIPT="$MEDIAMTX_CONF_DIR/rover-snapshot.sh" MEDIAMTX_SERVICE="/etc/systemd/system/mediamtx.service" MULTIROVER_SERVICE="/etc/systemd/system/multirover.service" SNAPSHOT_DIR="/var/lib/rover-snapshots" @@ -27,7 +26,6 @@ SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) SERVER_DIR="$SCRIPT_DIR" CONFIG_PATH="$SERVER_DIR/config.yaml" MEDIAMTX_TEMPLATE="$SERVER_DIR/mediamtx/mediamtx.yml" -MEDIAMTX_SNAPSHOT_TEMPLATE="$SERVER_DIR/mediamtx/rover-snapshot.sh" echo "[1/6] Installing dependencies..." dnf install -y nodejs npm curl tar >/dev/null @@ -75,13 +73,6 @@ fi echo " Installing mediaMTX config -> $MEDIAMTX_CONFIG" rm -f "$MEDIAMTX_CONFIG" install -m 0644 "$MEDIAMTX_TEMPLATE" "$MEDIAMTX_CONFIG" -if [[ ! -f "$MEDIAMTX_SNAPSHOT_TEMPLATE" ]]; then - echo "mediaMTX snapshot script missing at $MEDIAMTX_SNAPSHOT_TEMPLATE" >&2 - exit 1 -fi -echo " Installing mediaMTX snapshot script -> $MEDIAMTX_SNAPSHOT_SCRIPT" -rm -f "$MEDIAMTX_SNAPSHOT_SCRIPT" -install -m 0755 "$MEDIAMTX_SNAPSHOT_TEMPLATE" "$MEDIAMTX_SNAPSHOT_SCRIPT" chown -R "$TARGET_USER":"$TARGET_USER" "$MEDIAMTX_CONF_DIR" echo "[4/6] Writing systemd units..." diff --git a/server/mediamtx/mediamtx.yml b/server/mediamtx/mediamtx.yml index c9b3c542..ac3fff8a 100644 --- a/server/mediamtx/mediamtx.yml +++ b/server/mediamtx/mediamtx.yml @@ -42,4 +42,3 @@ paths: all: source: publisher sourceOnDemand: no - runOnReady: /etc/mediamtx/rover-snapshot.sh diff --git a/server/mediamtx/rover-snapshot.sh b/server/mediamtx/rover-snapshot.sh deleted file mode 100644 index 66e7285e..00000000 --- a/server/mediamtx/rover-snapshot.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -STREAM_ID="${MTX_PATH:-}" -if [[ -z "${STREAM_ID}" ]]; then - echo "MTX_PATH is required." >&2 - exit 1 -fi - -if [[ "${STREAM_ID}" == *-audio ]]; then - exit 0 -fi - -OUTPUT_DIR="${ROVER_SNAPSHOT_DIR:-/var/lib/rover-snapshots}" -FPS="${ROVER_SNAPSHOT_FPS:-3}" -WIDTH="${ROVER_SNAPSHOT_WIDTH:-640}" -QUALITY="${ROVER_SNAPSHOT_QUALITY:-8}" - -mkdir -p "${OUTPUT_DIR}" -OUTPUT_PATH="${OUTPUT_DIR}/${STREAM_ID}.jpg" - -INPUT_URL="${ROVER_SNAPSHOT_INPUT_URL:-srt://127.0.0.1:9000?streamid=read:${STREAM_ID}}" - -exec /usr/bin/ffmpeg -hide_banner -loglevel warning \ - -fflags nobuffer \ - -i "${INPUT_URL}" \ - -vf "fps=${FPS},scale=${WIDTH}:-1" \ - -q:v "${QUALITY}" \ - -an \ - -f image2 \ - -update 1 \ - "${OUTPUT_PATH}" diff --git a/server/src/services/replayBuildService.js b/server/src/services/replayBuildService.js index ec556ae0..6b59b666 100644 --- a/server/src/services/replayBuildService.js +++ b/server/src/services/replayBuildService.js @@ -62,10 +62,17 @@ async function listLatestSegments(sourceKey, neededCount) { const dir = path.join(replaySegmentsDir, sourceKey); const entries = await fsp.readdir(dir, { withFileTypes: true }); const files = []; + const now = Date.now(); for (const entry of entries) { if (!entry.isFile() || !entry.name.endsWith('.mp4')) continue; const filePath = path.join(dir, entry.name); const stat = await fsp.stat(filePath); + if (stat.size < 16 * 1024) { + continue; + } + if (now - stat.mtimeMs < segmentSeconds * 1000) { + continue; + } files.push({ filePath, mtimeMs: stat.mtimeMs }); } files.sort((a, b) => a.mtimeMs - b.mtimeMs); diff --git a/server/src/services/replaySegmentManager.js b/server/src/services/replaySegmentManager.js index cb180829..678b7264 100644 --- a/server/src/services/replaySegmentManager.js +++ b/server/src/services/replaySegmentManager.js @@ -13,6 +13,9 @@ const FPS = 15; const SCALE_WIDTH = 640; const MAX_BYTES = Number.parseInt(process.env.REPLAY_SEGMENT_MAX_BYTES || '0', 10); const FFMPEG_BIN = process.env.FFMPEG_BIN || 'ffmpeg'; +const ROVER_SNAPSHOT_DIR = process.env.ROVER_SNAPSHOT_DIR || '/var/lib/rover-snapshots'; +const ROVER_SNAPSHOT_FPS = 3; +const roverPreviews = new Map(); // key -> proc const recorders = new Map(); // key -> { proc, source } let cleanupTimer = null; @@ -52,6 +55,60 @@ function buildInputUrl(source) { return `srt://127.0.0.1:9000?streamid=read:${encodeURIComponent(source.id)}`; } +function spawnRoverPreview(source) { + const key = sourceKey(source); + const inputUrl = buildInputUrl(source); + if (!inputUrl) return; + const outputPath = path.join(ROVER_SNAPSHOT_DIR, `${source.id}.jpg`); + ensureDir(ROVER_SNAPSHOT_DIR) + .then(() => { + const args = [ + '-hide_banner', + '-loglevel', + 'warning', + '-fflags', + 'nobuffer', + '-i', + inputUrl, + '-vf', + `fps=${ROVER_SNAPSHOT_FPS},scale=${SCALE_WIDTH}:-1`, + '-q:v', + '8', + '-an', + '-f', + 'image2', + '-update', + '1', + outputPath, + ]; + const proc = spawn(FFMPEG_BIN, args, { stdio: 'ignore' }); + roverPreviews.set(key, proc); + proc.on('exit', (code, signal) => { + roverPreviews.delete(key); + if (!shouldRecord(source)) { + return; + } + const delay = 2000; + logger.warn('Rover preview exited; restarting', { key, code, signal }); + setTimeout(() => { + if (!roverPreviews.has(key) && shouldRecord(source)) { + spawnRoverPreview(source); + } + }, delay); + }); + }) + .catch((err) => { + logger.warn('Rover preview setup failed', { key, err: err.message }); + }); +} + +function stopRoverPreview(key) { + const proc = roverPreviews.get(key); + if (!proc) return; + proc.kill('SIGTERM'); + roverPreviews.delete(key); +} + async function ensureDir(dir) { await fsp.mkdir(dir, { recursive: true }); } @@ -154,12 +211,20 @@ function syncRecorders() { if (!recorders.has(key)) { spawnRecorder(source); } + if (source.type === 'rover' && !roverPreviews.has(key)) { + spawnRoverPreview(source); + } }); Array.from(recorders.keys()).forEach((key) => { if (!desiredKeys.has(key)) { stopRecorder(key); } }); + Array.from(roverPreviews.keys()).forEach((key) => { + if (!desiredKeys.has(key)) { + stopRoverPreview(key); + } + }); } async function cleanupSegments() {