Dev-only tooling to loop phone-recorded mp4s as local RTSP streams (MediaMTX + ffmpeg) so the real camera pipeline can be exercised without physical cameras. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6.3 KiB
Camera Stream Simulator — Design
Status: Approved Date: 2026-04-05 Scope: Development tooling only. Not shipped to operators.
Purpose
Loop phone-recorded video files as local RTSP streams so Vigilar's real camera
pipeline (OpenCV capture, motion detection, HLS, recording, reconnect logic)
can be exercised end-to-end without physical cameras. The operator records
3–5 minutes of footage from each angle on a phone, drops the files into a
videos/ directory, and runs a single script that serves them as
rtsp://127.0.0.1:8554/<camera_id>.
Primary use case: manual dev testing with 4–5 real clips. Secondary use case: stress testing with up to 32 distinct clips.
Non-Goals
- Not a production feature. Never invoked in deployed installs.
- Not a pytest fixture. Manual workflow only.
- No per-stream jitter, audio, non-mp4 inputs, or CLI flags.
- No Python rewrite or
vigilar simsubcommand.
Architecture
One bash script, one helper binary (MediaMTX), one ffmpeg process per stream.
scripts/sim_cameras.sh
│
├── starts: mediamtx (127.0.0.1:8554) ← single static Go binary
├── reads: config/vigilar.toml ← discovers camera ids
├── for each [[cameras]] block:
│ expects videos/<camera_id>.mp4
│ spawns: ffmpeg -stream_loop -1 -re -i videos/<id>.mp4 \
│ -c copy -f rtsp rtsp://127.0.0.1:8554/<id>
├── writes: config/vigilar.sim.toml ← copy of real config,
│ rtsp_urls rewritten
└── foreground; Ctrl-C kills all children
Operator workflow:
$ ./scripts/sim_cameras.sh
→ serving 5 streams on 127.0.0.1:8554
→ wrote config/vigilar.sim.toml
$ vigilar start --config config/vigilar.sim.toml # in another terminal
Strict 1:1 mapping — each simulated stream corresponds to exactly one video
file, named after the camera id in vigilar.toml. No file reuse, no cycling.
Components
scripts/sim_cameras.sh
Bash, no Python dependencies. Responsibilities:
- Resolve paths:
$REPO/config/vigilar.toml,$REPO/videos/,$REPO/.sim/mediamtx,$REPO/.sim/logs/. - Check prerequisites:
ffmpegon PATH,mediamtxbinary present,videos/directory exists. - Parse camera ids from
vigilar.tomlby grepping^id = "..."lines inside[[cameras]]blocks. No TOML parser — tolerable because the config format is stable and this is dev tooling. - Verify
videos/<id>.mp4exists for every camera id. List all missing files in one error message, then exit 1. - Launch
mediamtxin the background, writing its log to.sim/logs/mediamtx.log. Wait ~500ms, then verify the RTSP port (8554) is listening (ss -lntor/dev/tcp). - Launch one ffmpeg process per camera, backgrounded, with stdout/stderr
redirected to
.sim/logs/<id>.log. Track PIDs in a bash array. - Generate
config/vigilar.sim.toml: copy the real TOML, rewrite eachrtsp_url = "..."line tortsp://127.0.0.1:8554/<id>usingsedscoped per camera block. - Print a summary: each served URL, the sim config path, and "Press Ctrl-C to stop".
- Install a
trapon EXIT/INT/TERM that kills mediamtx and every ffmpeg child PID, then removes.sim/. Idempotent. waiton child PIDs so the script blocks until Ctrl-C.
scripts/download_mediamtx.sh
Parallel to the existing scripts/download_model.sh. Downloads the
MediaMTX static binary for the current architecture from the official
GitHub release into .sim/mediamtx, verifies it is executable, and exits.
Run once per machine.
videos/ (gitignored)
Operator drops <camera_id>.mp4 files matching camera ids in
config/vigilar.toml. Format is mp4 only; other extensions are rejected.
.sim/ (gitignored)
Working directory, recreated each run:
.sim/mediamtx— the static binary (installed bydownload_mediamtx.sh).sim/logs/mediamtx.log.sim/logs/<camera_id>.log
config/vigilar.sim.toml (gitignored)
Generated artifact. Overwritten on every run. Never hand-edited. The real
config/vigilar.toml is read-only from the simulator's perspective.
MediaMTX
Single static Go binary. Default configuration is sufficient — listens on
RTSP port 8554, accepts rtsp://127.0.0.1:8554/<path> publishes from
ffmpeg. No custom yaml required.
Error Handling
Fail loud and early. No silent fallbacks.
| Condition | Behavior |
|---|---|
ffmpeg not on PATH |
Print install hint, exit 1 |
.sim/mediamtx missing |
Print "run scripts/download_mediamtx.sh", exit 1 |
videos/ missing or any videos/<id>.mp4 missing |
List all missing files, exit 1 |
| MediaMTX fails to bind 8554 | Dump .sim/logs/mediamtx.log tail, exit 1 |
| ffmpeg child dies mid-run | Log to stderr, leave others running. Vigilar's reconnect logic handles the gap — this is a feature, not a bug. |
| Ctrl-C / script death | trap kills mediamtx + all ffmpeg PIDs, removes .sim/ |
Acceptance Criteria
Manual verification — this is dev tooling, not production code.
- Drop 5 mp4s matching camera ids into
videos/, run./scripts/sim_cameras.sh. 5 URLs print. ffprobe rtsp://127.0.0.1:8554/<id>succeeds for each stream.- In another terminal:
vigilar start --config config/vigilar.sim.toml. The web UI shows 5 camera tiles with live HLS. Waving at phone footage triggers motion events in the event log. - Kill one ffmpeg process manually. Vigilar marks that camera as disconnected; reconnect loop engages. Restart the same ffmpeg invocation → stream recovers in the UI.
- Ctrl-C the sim script.
pgrep ffmpegandpgrep mediamtxboth empty..sim/removed. - Scale test: populate 32 dummy mp4s matching 32 camera blocks. Script starts cleanly; CPU remains within reason (subjective; no hard target).
Automated Check
One light test: tests/unit/test_sim_script.py runs
bash -n scripts/sim_cameras.sh (syntax check) and, if shellcheck is
available, runs it as well. No integration tests — the simulator exists to
support manual testing.
File Inventory
New files:
scripts/sim_cameras.shscripts/download_mediamtx.shtests/unit/test_sim_script.py
Modified files:
.gitignore— addvideos/,.sim/,config/vigilar.sim.toml
No changes to vigilar/ package code. The simulator is fully external.