A self-hosted, Linux-first network video recorder (NVR) built as an open alternative to commercial NVR software. Designed to run on a home server or NAS and be accessible from any browser on your local network or over Tailscale.
- Tight auto-optimizing camera grid — columns adjust automatically as cameras are added or removed
- Inline live sub-stream video in every tile (low-latency HLS)
- Click any tile to open the full HD stream in a modal
- Single-port architecture: all traffic (UI, API, HLS streams) flows through port 8099 — no firewall exceptions needed beyond one port
- Sub-stream recording — always-on, continuous low-bitrate recording for every enabled camera, stored separately from HD clips
- HD recording — two modes per camera:
motion— HD recording starts only when CodeProject.AI detects a configured label (person, car, etc.)continuous— HD recording runs 24/7
- Pre-roll — ring buffer keeps the last N seconds before motion so clips include context
- Segmented clips — ffmpeg writes rolling
.mp4segments; configurable segment length per camera - Retention pruning — clips older than
retention_daysare automatically deleted per camera - Trigger labels — every recording event is tagged with what triggered it (
person,car,continuous, etc.) - Audio transcoded to AAC (cameras that output unsupported codecs like
pcm_mulaware handled automatically)
- Integrates with CodeProject.AI Server for on-device object detection
- Polls camera snapshots on a configurable interval
- Supports any label CodeProject.AI can recognize (person, car, dog, etc.)
- Configurable confidence threshold and cooldown per camera
- Webhook notifications on detection
- Filter by camera, date, and status
- Every clip card shows the trigger label, timestamps, status badge, and download links
- Inline video player
- Delete individual recording events
- Full CRUD via the web UI — no YAML editing required after initial setup
- Supports dual streams (main + sub), snapshot URL, detection, PTZ, and recording config per camera
- Scan any IPv4 CIDR for likely camera devices
- Checks RTSP ports, ONVIF paths, and HTTP banners to identify cameras and guess RTSP/snapshot URLs
- One-click "Add Camera" pre-fills the form from scan results
- Directional pad + zoom buttons in the live grid (hover to reveal)
- Supports Hikvision and Dahua CGI protocol
- PTZ base URL auto-inferred from the RTSP URL if not set
IP Cameras (H.265 / H.264 RTSP)
│
▼
ffmpeg transcode ──────────────────────────────────────────┐
(H.264 @ 720p sub / 1080p HD) │
│ │
▼ ffmpeg record
MediaMTX RTSP receiver (:19554) (segmented MP4)
│
▼
MediaMTX HLS server (:18991)
│
▼
FastAPI /hls/{path} proxy (:8099) ◄── all browser traffic
│
▼
Browser (HLS.js)
- FastAPI (port 8099) serves the UI, REST API, and proxies HLS — one port for everything
- MediaMTX handles RTSP ingest and HLS output (internal, not exposed to clients)
- CodeProject.AI runs object detection on camera snapshots (port 32168, optional)
- ffmpeg does codec transcoding and clip recording
- Linux host with Docker Engine and the Compose plugin
- IP cameras that expose RTSP streams
git clone https://github.com/t69broken/Open-Iris.git
cd Open-IrisCopy the example files:
cp .env.example .env
cp app/cameras.example.yaml data/cameras.yamlEdit .env to set a username and password (or leave both blank to disable auth):
BLUE_NVR_USERNAME=admin
BLUE_NVR_PASSWORD=yourpasswordEdit data/cameras.yaml with your camera details (see Camera Configuration below).
docker compose up -d --buildhttp://<host-ip>:8099
- Live tab — live camera grid
- Clips tab — recorded events browser
- Cameras tab — add, edit, delete cameras
- Discover tab — scan your LAN for cameras
Cameras are defined in data/cameras.yaml. The web UI writes to this file, but you can also edit it directly and reload (curl -X POST http://localhost:8099/api/reload).
cameras:
- id: front-door # URL-safe slug, must be unique
name: Front Door # Display name
rtsp_url: rtsp://user:pass@192.168.1.50:554/h264Preview_01_main
rtsp_url_sub: rtsp://user:pass@192.168.1.50:554/h264Preview_01_sub # optional low-res stream
snapshot_url: http://192.168.1.50/cgi-bin/snapshot.cgi # optional, needed for AI detection
enabled: true
record:
enabled: true
mode: motion # "motion" (AI-triggered) or "continuous" (always-on HD)
segment_seconds: 60 # clip segment length in seconds (min 10)
pre_roll_seconds: 5 # seconds of pre-roll to prepend (0 to disable)
retention_days: 30 # auto-delete HD clips older than this (1–365)
detect:
enabled: true
labels: # labels that trigger HD recording
- person
- car
min_confidence: 0.50 # detection threshold (0.0–1.0)
cooldown_seconds: 20 # keep recording this long after last match
poll_interval_seconds: 3 # snapshot poll rate in seconds
webhook_url: # optional POST URL for motion events
ptz:
enabled: false
type: hikvision # "hikvision" or "dahua"
base_url: # inferred from rtsp_url if omitted
username:
password:- If
rtsp_url_subis set, the sub-stream is used for the live grid tiles and continuous sub-stream recording. The main stream is used for HD recording only. - If
snapshot_urlis not set, Open-Iris uses ffmpeg to grab a frame from the RTSP stream for detection — slower but works with any camera. - Sub-stream recording runs continuously regardless of
record.mode. Themodesetting controls HD recording only.
By default recordings are stored inside ./data/recordings/ (inside the project directory). To store clips on a separate drive:
- Update
docker-compose.ymlto add a volume mount:
volumes:
- ./app:/app/app:ro
- ./data:/data
- /mnt/your-drive:/mnt/your-drive # add this- Update the environment variables:
environment:
RECORDINGS_DIR: /mnt/your-drive/nvr/hd
SUB_RECORDINGS_DIR: /mnt/your-drive/nvr/subAll variables are optional — defaults work for a standard install.
| Variable | Default | Description |
|---|---|---|
CAMERA_CONFIG_PATH |
/data/cameras.yaml |
Camera inventory file |
MEDIAMTX_CONFIG_PATH |
/data/mediamtx.generated.yml |
Generated MediaMTX config path |
MEDIAMTX_RTSP_ADDRESS |
:19554 |
MediaMTX RTSP receiver port |
MEDIAMTX_HLS_ADDRESS |
:18991 |
MediaMTX HLS server port |
MEDIAMTX_API_ADDRESS |
:19997 |
MediaMTX management API port |
MEDIAMTX_RTSP_PUSH |
rtsp://127.0.0.1:19554 |
RTSP push target for ffmpeg transcoders |
HLS_UPSTREAM |
http://127.0.0.1:18991 |
MediaMTX HLS address (used by the proxy) |
CODEPROJECTAI_URL |
http://localhost:32168 |
CodeProject.AI server URL |
RECORDINGS_DIR |
/data/recordings/hd |
HD clip storage directory |
SUB_RECORDINGS_DIR |
/data/recordings/sub |
Sub-stream clip storage directory |
EVENTS_INDEX_PATH |
/data/recordings-index.json |
HD recording event index |
SUB_EVENTS_INDEX_PATH |
/data/sub-recordings-index.json |
Sub-stream event index |
PREROLL_DIR |
/data/preroll |
Pre-roll ring buffer directory |
DEFAULT_SCAN_CIDR |
192.168.1.0/24 |
Default CIDR for LAN discovery |
FFMPEG_BIN |
/usr/bin/ffmpeg |
ffmpeg binary path |
RETENTION_CHECK_INTERVAL |
3600 |
Seconds between retention prune runs |
HEALTH_CHECK_INTERVAL |
30 |
Seconds between camera health probes |
BLUE_NVR_USERNAME |
(none) | HTTP Basic Auth username |
BLUE_NVR_PASSWORD |
(none) | HTTP Basic Auth password |
If both BLUE_NVR_USERNAME and BLUE_NVR_PASSWORD are set, the dashboard and API require HTTP Basic Auth. The /health, /static/, /hls/, and /recordings/ paths are always exempt so streams and clips load correctly in the browser.
The recommended remote setup is Tailscale:
- Install Tailscale on the Linux host and join it to your tailnet
- Access the dashboard using the host's Tailscale IP or MagicDNS name:
http://<tailscale-host>:8099 - Keep all ports bound to the host — rely on Tailscale ACLs for access control
Only port 8099 needs to be reachable. All HLS streams are proxied through it.
Open-Iris transcodes camera streams to H.264 before serving them via HLS (most cameras output H.265 which browsers can't decode natively). The ffmpeg transcoder:
- Outputs 720p for live grid tiles (
{id}-web) - Outputs 1080p for HD modal streams (
{id}-web-hd) - Copies the main stream codec directly for recording (no re-encode — fast and lossless)
- Transcodes audio to AAC for MP4 compatibility (cameras using
pcm_mulawor other unsupported codecs are handled automatically)
- Single-credential Basic Auth only (no multi-user or role-based permissions)
- ONVIF not supported for PTZ — Hikvision and Dahua CGI only
- LAN discovery is heuristic; ONVIF discovery is not implemented
- No mobile push notifications (webhook integration works as a bridge)
- No two-way audio
- Timeline / scrubber playback not yet implemented
Open-Iris/
├── app/
│ ├── main.py # FastAPI backend — all server logic
│ ├── requirements.txt
│ ├── cameras.example.yaml # camera config template
│ └── static/
│ ├── index.html
│ ├── app.js
│ ├── styles.css
│ └── hls.min.js # HLS.js bundled locally
├── data/
│ ├── cameras.yaml # your camera config (gitignored)
│ ├── mediamtx.generated.yml # auto-generated at startup (gitignored)
│ ├── codeprojectai-appsettings.json
│ └── recordings/ # clip storage (gitignored)
├── docker-compose.yml
├── Dockerfile
├── .env # your credentials (gitignored)
└── .env.example # credentials template
MIT