mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
rover snapshots in server
This commit is contained in:
@@ -6,7 +6,6 @@ MEDIAMTX_BASE_URL="https://github.com/bluenviron/mediamtx/releases/download/v${M
|
|||||||
MEDIAMTX_BIN="/usr/local/bin/mediamtx"
|
MEDIAMTX_BIN="/usr/local/bin/mediamtx"
|
||||||
MEDIAMTX_CONF_DIR="/etc/mediamtx"
|
MEDIAMTX_CONF_DIR="/etc/mediamtx"
|
||||||
MEDIAMTX_CONFIG="$MEDIAMTX_CONF_DIR/mediamtx.yml"
|
MEDIAMTX_CONFIG="$MEDIAMTX_CONF_DIR/mediamtx.yml"
|
||||||
MEDIAMTX_SNAPSHOT_SCRIPT="$MEDIAMTX_CONF_DIR/rover-snapshot.sh"
|
|
||||||
MEDIAMTX_SERVICE="/etc/systemd/system/mediamtx.service"
|
MEDIAMTX_SERVICE="/etc/systemd/system/mediamtx.service"
|
||||||
MULTIROVER_SERVICE="/etc/systemd/system/multirover.service"
|
MULTIROVER_SERVICE="/etc/systemd/system/multirover.service"
|
||||||
SNAPSHOT_DIR="/var/lib/rover-snapshots"
|
SNAPSHOT_DIR="/var/lib/rover-snapshots"
|
||||||
@@ -27,7 +26,6 @@ SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
|||||||
SERVER_DIR="$SCRIPT_DIR"
|
SERVER_DIR="$SCRIPT_DIR"
|
||||||
CONFIG_PATH="$SERVER_DIR/config.yaml"
|
CONFIG_PATH="$SERVER_DIR/config.yaml"
|
||||||
MEDIAMTX_TEMPLATE="$SERVER_DIR/mediamtx/mediamtx.yml"
|
MEDIAMTX_TEMPLATE="$SERVER_DIR/mediamtx/mediamtx.yml"
|
||||||
MEDIAMTX_SNAPSHOT_TEMPLATE="$SERVER_DIR/mediamtx/rover-snapshot.sh"
|
|
||||||
|
|
||||||
echo "[1/6] Installing dependencies..."
|
echo "[1/6] Installing dependencies..."
|
||||||
dnf install -y nodejs npm curl tar >/dev/null
|
dnf install -y nodejs npm curl tar >/dev/null
|
||||||
@@ -75,13 +73,6 @@ fi
|
|||||||
echo " Installing mediaMTX config -> $MEDIAMTX_CONFIG"
|
echo " Installing mediaMTX config -> $MEDIAMTX_CONFIG"
|
||||||
rm -f "$MEDIAMTX_CONFIG"
|
rm -f "$MEDIAMTX_CONFIG"
|
||||||
install -m 0644 "$MEDIAMTX_TEMPLATE" "$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"
|
chown -R "$TARGET_USER":"$TARGET_USER" "$MEDIAMTX_CONF_DIR"
|
||||||
|
|
||||||
echo "[4/6] Writing systemd units..."
|
echo "[4/6] Writing systemd units..."
|
||||||
|
|||||||
@@ -42,4 +42,3 @@ paths:
|
|||||||
all:
|
all:
|
||||||
source: publisher
|
source: publisher
|
||||||
sourceOnDemand: no
|
sourceOnDemand: no
|
||||||
runOnReady: /etc/mediamtx/rover-snapshot.sh
|
|
||||||
|
|||||||
@@ -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}"
|
|
||||||
@@ -62,10 +62,17 @@ async function listLatestSegments(sourceKey, neededCount) {
|
|||||||
const dir = path.join(replaySegmentsDir, sourceKey);
|
const dir = path.join(replaySegmentsDir, sourceKey);
|
||||||
const entries = await fsp.readdir(dir, { withFileTypes: true });
|
const entries = await fsp.readdir(dir, { withFileTypes: true });
|
||||||
const files = [];
|
const files = [];
|
||||||
|
const now = Date.now();
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (!entry.isFile() || !entry.name.endsWith('.mp4')) continue;
|
if (!entry.isFile() || !entry.name.endsWith('.mp4')) continue;
|
||||||
const filePath = path.join(dir, entry.name);
|
const filePath = path.join(dir, entry.name);
|
||||||
const stat = await fsp.stat(filePath);
|
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.push({ filePath, mtimeMs: stat.mtimeMs });
|
||||||
}
|
}
|
||||||
files.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
files.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ const FPS = 15;
|
|||||||
const SCALE_WIDTH = 640;
|
const SCALE_WIDTH = 640;
|
||||||
const MAX_BYTES = Number.parseInt(process.env.REPLAY_SEGMENT_MAX_BYTES || '0', 10);
|
const MAX_BYTES = Number.parseInt(process.env.REPLAY_SEGMENT_MAX_BYTES || '0', 10);
|
||||||
const FFMPEG_BIN = process.env.FFMPEG_BIN || 'ffmpeg';
|
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 }
|
const recorders = new Map(); // key -> { proc, source }
|
||||||
let cleanupTimer = null;
|
let cleanupTimer = null;
|
||||||
@@ -52,6 +55,60 @@ function buildInputUrl(source) {
|
|||||||
return `srt://127.0.0.1:9000?streamid=read:${encodeURIComponent(source.id)}`;
|
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) {
|
async function ensureDir(dir) {
|
||||||
await fsp.mkdir(dir, { recursive: true });
|
await fsp.mkdir(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
@@ -154,12 +211,20 @@ function syncRecorders() {
|
|||||||
if (!recorders.has(key)) {
|
if (!recorders.has(key)) {
|
||||||
spawnRecorder(source);
|
spawnRecorder(source);
|
||||||
}
|
}
|
||||||
|
if (source.type === 'rover' && !roverPreviews.has(key)) {
|
||||||
|
spawnRoverPreview(source);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
Array.from(recorders.keys()).forEach((key) => {
|
Array.from(recorders.keys()).forEach((key) => {
|
||||||
if (!desiredKeys.has(key)) {
|
if (!desiredKeys.has(key)) {
|
||||||
stopRecorder(key);
|
stopRecorder(key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Array.from(roverPreviews.keys()).forEach((key) => {
|
||||||
|
if (!desiredKeys.has(key)) {
|
||||||
|
stopRoverPreview(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cleanupSegments() {
|
async function cleanupSegments() {
|
||||||
|
|||||||
Reference in New Issue
Block a user