# 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/`. 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 sim` subcommand. ## 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/.mp4 │ spawns: ffmpeg -stream_loop -1 -re -i videos/.mp4 \ │ -c copy -f rtsp rtsp://127.0.0.1:8554/ ├── 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: 1. Resolve paths: `$REPO/config/vigilar.toml`, `$REPO/videos/`, `$REPO/.sim/mediamtx`, `$REPO/.sim/logs/`. 2. Check prerequisites: `ffmpeg` on PATH, `mediamtx` binary present, `videos/` directory exists. 3. Parse camera ids from `vigilar.toml` by grepping `^id = "..."` lines inside `[[cameras]]` blocks. No TOML parser — tolerable because the config format is stable and this is dev tooling. 4. Verify `videos/.mp4` exists for every camera id. List **all** missing files in one error message, then exit 1. 5. Launch `mediamtx` in the background, writing its log to `.sim/logs/mediamtx.log`. Wait ~500ms, then verify the RTSP port (8554) is listening (`ss -lnt` or `/dev/tcp`). 6. Launch one ffmpeg process per camera, backgrounded, with stdout/stderr redirected to `.sim/logs/.log`. Track PIDs in a bash array. 7. Generate `config/vigilar.sim.toml`: copy the real TOML, rewrite each `rtsp_url = "..."` line to `rtsp://127.0.0.1:8554/` using `sed` scoped per camera block. 8. Print a summary: each served URL, the sim config path, and "Press Ctrl-C to stop". 9. Install a `trap` on EXIT/INT/TERM that kills mediamtx and every ffmpeg child PID, then removes `.sim/`. Idempotent. 10. `wait` on 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 `.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 by `download_mediamtx.sh`) - `.sim/logs/mediamtx.log` - `.sim/logs/.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/` 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/.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. 1. Drop 5 mp4s matching camera ids into `videos/`, run `./scripts/sim_cameras.sh`. 5 URLs print. 2. `ffprobe rtsp://127.0.0.1:8554/` succeeds for each stream. 3. 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. 4. Kill one ffmpeg process manually. Vigilar marks that camera as disconnected; reconnect loop engages. Restart the same ffmpeg invocation → stream recovers in the UI. 5. Ctrl-C the sim script. `pgrep ffmpeg` and `pgrep mediamtx` both empty. `.sim/` removed. 6. 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.sh` - `scripts/download_mediamtx.sh` - `tests/unit/test_sim_script.py` Modified files: - `.gitignore` — add `videos/`, `.sim/`, `config/vigilar.sim.toml` No changes to `vigilar/` package code. The simulator is fully external.