Skip to content

Repository files navigation

Camera Feed Lab

Camera Feed Lab

Camera Feed Lab gives you a roomful of virtual security cameras without buying or wiring physical hardware. It serves configurable RTSP camera feeds from generated test patterns or approved recordings, then uses its companion Chain Monitor to check whether that video is actually arriving, advancing, and rendering in the system under test.

Use it to develop or test an NVR, VMS, video wall, player, analytics service, recorder, or other RTSP consumer. The same configuration can run on a laptop, in CI, or as a shared lab feeder. Synthetic feeds start without media files; recorded-media tests use configurable local or shared read-only mounts that remain outside the repository and image.

Camera Feed Lab is not an NVR. It does not discover cameras, record footage, manage retention, run analytics, or emulate vendor management APIs. It provides repeatable camera inputs and end-to-end verification for products that do those things.

What it includes

Camera Feed Lab has two cooperating parts:

  1. Feed server — validates a JSON manifest, supervises MediaMTX and FFmpeg, and publishes the requested RTSP/TCP camera paths.
  2. Chain Monitor — independently probes delivered streams, consumes generic application telemetry, and can verify that individual regions on a physical display are neither dark nor frozen.

You can run the feed server alone. Chain Monitor is useful when a healthy publisher is not enough and the test must prove that video survived the full path through another application or display.

Quick start

The default Compose stack is self-contained and generates a site-neutral synthetic compatibility profile. It needs no footage or credentials.

docker compose up --build -d
curl --fail http://127.0.0.1:8080/health

Open a stream with an RTSP/TCP client, for example:

rtsp://127.0.0.1:8554/unicast/c1/s1/live

Stop the stack with:

docker compose down

The default host bindings are:

Port Protocol Purpose
8554 RTSP over TCP Camera-stream readers
8080 HTTP over TCP Health and readiness, loopback-bound by default

Deployment overrides may change host bindings. The hardened portable container supports RTSP/TCP; UDP and multicast require a broader platform-specific network deployment and are outside the default contract.

Features

Camera feeds

  • Define 1 to 64 source streams in a strict versioned JSON manifest.
  • Give each stream a stable ID, unique RTSP path, activation mode, source, and explicit video/audio behavior.
  • Add up to 128 unique reader aliases per stream. Each alias publishes directly from the declared source plan and cannot be used to replace a feed.
  • Generate deterministic testsrc2 or SMPTE-bar video at selected dimensions and rational frame rates.
  • Add an optional deterministic audio tone.
  • Replay direct MPEG-PS, MPEG-TS, MP4/MOV, or MXF files once, continuously, or in wall-clock-aligned full-day mode.
  • Packet-copy video or transcode it to H.264 or HEVC with selected dimensions, frame rate, bitrate, and GOP length.
  • Drop, copy, or transcode audio to AAC.
  • Start publishers with the service or on demand when a reader connects.
  • Publish every FFmpeg feed to MediaMTX through independent flow-controlled RTSP/TCP connections.

Verification

  • Wait for machine-readable readiness before starting consumer tests.
  • Probe RTSP delivery using monotonic-time pacing measurements.
  • Consume strict generic JSON telemetry from the system under test.
  • Compare memory-only PNG/JPEG frames to detect dark or static output.
  • Divide a display into independent normalized regions so moving UI chrome cannot conceal a frozen video tile.
  • Require authoritative physical scanout evidence while treating browser or application screenshots as supplementary render evidence.
  • Expose current health, metrics, bounded history, and an escaped HTML dashboard without exposing configured endpoints or captured frames.

Safety and repeatability

  • Reject unknown, duplicate, and case-variant JSON keys.
  • Validate the complete manifest, media set, and FFmpeg plan before starting a child process.
  • Confine media access beneath a rooted read-only media directory and pass validated files to FFmpeg by descriptor.
  • Reject absolute paths, traversal, symlink escapes, special files, playlists, network sources, and unsupported containers.
  • Build every child command as an executable plus argument array; manifest text is never evaluated as shell syntax.
  • Restrict MediaMTX publishing and API access to loopback processes while allowing intended external readers.
  • Supervise complete MediaMTX/FFmpeg process groups, drain both output pipes, forward termination signals, and preserve meaningful failure status.
  • Reconstruct ephemeral runtime state from the immutable manifest after every restart.
  • Emit first-party and child events as structured JSON Lines.

Configuration

The canonical inputs are:

The container reads its manifest from /config/camera-feed-lab.json, reads media below /media, and writes generated runtime state only below /tmp. Host paths are deployment concerns rather than image contents.

Custom read-only media

File paths in a manifest are relative to /media. A local override can mount a custom manifest and media root read-only:

services:
  camera-feed-lab:
    volumes:
      - ./config/custom.json:/config/camera-feed-lab.json:ro
      - ./media:/media:ro

A shared drive uses the same container boundary with a different private bind source. Do not commit recordings, credentials, host paths, private addresses, or site mappings.

Supported direct containers are:

  • MPEG-PS: .mpg, .mpeg, .vob, .ps
  • MPEG-TS: .ts, .m2ts, .mts
  • MP4/MOV: .mp4, .m4v, .mov
  • MXF: .mxf

Playlist and manifest formats are intentionally rejected because their nested references could escape the validated media root.

Wall-clock day replay

A looped full-day recording can follow current time of day:

{
  "kind": "file",
  "path": "sample-day.mp4",
  "loop": true,
  "replay": {
    "mode": "wall_clock_day",
    "utc_offset_seconds": 0
  }
}

When the publisher starts, it seeks to the corresponding seconds-of-day and then plays in real time. The historical recording retains its original content; the UTC offset and media stay in private deployment configuration.

Health contract

GET /health binds before MediaMTX starts and reports the manifest's explicit default, development, or production environment. It returns:

  • 503 while the MediaMTX API is unavailable;
  • 503 while any always-on path is missing or offline;
  • 200 when MediaMTX and every required always-on path are ready.

An unused on-demand path is reported as idle and does not make the service unhealthy. Successful health probes do not create log noise.

Native CLI

Build the current command:

go build -trimpath -o camera-feed-lab ./cmd/camera-feed-lab

Validate without launching children:

./camera-feed-lab validate --config ./config/camera-feed-lab.compatibility.json

Run locally:

./camera-feed-lab run \
  --config ./config/camera-feed-lab.compatibility.json \
  --media-root /media

MediaMTX hooks use the internal publish command to start a single planned stream. Container healthchecks use the quiet healthcheck command. Run ./camera-feed-lab without arguments for the command contract.

All deployment-variable values are startup flags:

Concern Flags
Inputs --config, --media-root
MediaMTX --rtsp-address, --api-address, --api-url, --mediamtx
Health --health-address, --health-read-header-timeout, --health-idle-timeout
Local publishing --publish-base
Runtime state --runtime-root
Demand lifecycle --demand-start-timeout, --demand-close-after
Shutdown --shutdown-timeout, --child-stop-timeout

Architecture

manifest ──► strict decode and validation ──► FFmpeg plans
   │                                                │
   │                                                ▼
   ├──► ephemeral MediaMTX configuration ──► supervised publishers
   │                                                │
   │                                                ▼
   ├──► health/readiness                      MediaMTX RTSP/TCP
   │                                                │
   └────────────────────────────────────────────────┴──► consumers

consumers ──► Chain Monitor probes/collectors ──► stream + display verdicts

The Go supervisor owns operating-system process state directly: child PIDs, file descriptors, pipes, sockets, signals, and exit events. Desired state is the validated manifest, so no database is needed to rebuild runtime state. This focused native boundary is recorded in the project's scoped architecture exception; all other engineering, testing, security, and review requirements remain applicable.

The implementation uses only the Go standard library. Media work is performed by pinned MediaMTX and FFmpeg runtimes rather than in the Go control process.

Chain Monitor

Chain Monitor lives under tools/chain-monitor. It runs independently from the feed server and has no committed topology or useful estate defaults. A deployment supplies stream endpoints, generic telemetry collectors, display capture sources, credentials, regions, masks, and thresholds through private read-only configuration.

Captured frame bytes and image-derived identifiers are never persisted or returned by its HTTP surface. Optional bearer tokens are read from private configuration and are not logged. Chain Monitor is development and test instrumentation, not a field-monitoring or incident-response product.

See the Chain Monitor documentation for its configuration and endpoint contracts.

Development and verification

Tool versions and the JSON Schema checker's dependencies are pinned under tools/.

Run the complete local quality gate:

make check

It runs formatting, go vet, lint, schema validation, race-enabled tests, coverage, decomposition checks, and the rendered Compose security contract.

On a safe Docker builder, run the real-container suite:

make container-integration

The suite exercises the actual Compose deployment, healthcheck, RTSP decode, pacing bounds, publisher rejection, container hardening, bounded logs, reader fan-out, and clean termination. Evidence is retained under .artifacts/container-integration/<run-id>/; transient containers, images, and networks are removed on both success and failure.

Security and privacy boundary

The public source and images contain no footage, credentials, private host addresses, site mappings, captures, or test evidence. Configuration examples use synthetic sources and generic paths. Deployments provide approved media, addresses, credentials, and mounts separately.

The runtime container uses pinned base images, a dedicated non-root identity, a read-only root filesystem, dropped capabilities, no-new-privileges, bounded PID and log budgets, and a small ephemeral tmpfs. The default network contract exposes only RTSP/TCP and loopback-bound health.

License

Camera Feed Lab is available under the MIT License.

About

Generate and verify repeatable RTSP camera feeds for development, CI, and video-system testing

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages