Camera and NVR Streaming/Management
  • Python 50.4%
  • JavaScript 47.8%
  • HTML 0.6%
  • CSS 0.5%
  • Makefile 0.3%
  • Other 0.4%
Find a file
eli southern c71307a5d6
Merge pull request #9 from elirtf/cursor
back to baseline with cameras working
2026-04-20 15:42:12 -05:00
.cursor/rules changed rule 2026-03-31 15:31:44 -05:00
app Migration 013_camera_transcode_off_default runs inside create_app() and flips all camera.transcode = 1 rows to 0. 2026-04-20 15:21:02 -05:00
docs 1. CRITICAL — Flask / nginx 502 (connection refused) 2026-04-15 08:50:42 -05:00
frontend single literal 6 → named constant MAX_CONCURRENT_LIVE_DECODERS = 24, with a comment correcting the previous "HTTP/1.1 6-connection" justification (WebSockets don't share that budget). 2026-04-20 15:35:01 -05:00
go2rtc /api/cameras/<name>/stats 1 2026-04-01 16:52:01 -05:00
mobile Update README.md 2026-03-30 14:18:51 -05:00
nginx Update nginx.conf 2026-04-14 15:51:07 -05:00
.dockerignore Removed global *.ts ignore (this could silently break Docker builds if you add TypeScript files later). 2026-03-31 16:15:54 -05:00
.gitattributes for windows compatibility 2026-04-08 16:01:17 -05:00
.gitignore Merge branch 'master' into cursor 2026-03-30 13:02:45 -05:00
compose.override.yml.example go2rtc hardening override for local tweaking 2026-04-01 12:54:58 -05:00
docker-compose.yml GO2RTC_TRANSCODE_DEFAULT now defaults to False in app/go2rtc_config.py and app/go2rtc.py, so new configs use plain rtsp://…, which MSE can use. 2026-04-15 11:52:13 -05:00
docker-entrypoint-opus.sh reverting changes to docker-entrypoint-opus.sh and Dockerfile 2026-04-01 11:09:45 -05:00
Dockerfile after copying the entrypoint, sed -i 's/\r$//' so a CRLF copy still builds correctly. 2026-04-08 16:01:33 -05:00
LICENSE Update LICENSE 2026-03-04 16:00:53 -06:00
Makefile PROJECT_DIR ?= $(CURDIR); DOCKER ?= docker with DC = $(DOCKER) compose. 2026-03-25 10:07:44 -05:00
README.md Merge pull request #9 from elirtf/cursor 2026-04-20 15:42:12 -05:00
requirements.txt frontend/src/hooks/useLiveStreamGate.js — Optional maxConcurrentLiveDecoders (default unused). When set, tiles queue for a slot: at most 6 live decoders run at once; when you scroll or a tile leaves the viewport, the next queued tile gets a slot. 2026-04-15 11:39:23 -05:00
reset_opus.sh refactor 2026-03-26 11:22:41 -05:00
run.py 1. CRITICAL — Flask / nginx 502 (connection refused) 2026-04-15 08:50:42 -05:00

Opus Logo

Opus NVR

A lightweight, self-hosted IP camera recorder and viewer. For new installs, cameras send RTSP directly to Opus (live + continuous recording + timeline playback). A vendor NVR is optional—the UI still supports importing channels from an existing recorder for migration only.

Deploy with Docker Compose on Linux (Ubuntu Server is the documented standard).

License: MIT Docker Python Status Repo Size


Features

Live Viewing

Streaming

Devices & Configuration

Sites (legacy NVR import), camera list, and Configuration (system info, Settings / go2rtc + recording options, diagnostics, per-site stream table with RTSP edit).

Authentication & Access Control


Tech Stack

Layer Technology
Backend Flask
Authentication Flask-Login
Database SQLite via Peewee (migrations in app/migrations/)
Stream Server go2rtc
Reverse Proxy nginx
Frontend React + Tailwind CSS
Containerization Docker + Docker Compose

Getting Started

Prerequisites

  • Docker Desktop (Windows/macOS) or Docker Engine + Compose (Linux)
  • Git

Setup

# 1. Clone the repo
git clone https://github.com/elirtf/opus.git
cd opus

# 2. Create your environment file
# Edit .env and set SECRET_KEY, and optionally GO2RTC_URL
      # GO2RTC_URL=http://go2rtc:1984 (Default)
      # GO2RTC_CONFIG_PATH=/config/go2rtc.yaml (Default — Opus writes this from Configuration → Settings)
      # GO2RTC_RTSP_URL=rtsp://go2rtc:8554 (Default)
      # GO2RTC_ALLOW_ARBITRARY_EXEC=false (Optional — overrides UI “allow exec sources” when set)
      # SECRET_KEY=secret-key
      # Optional: comma-separated browser origins for split-host UIs (enables flask-cors)
      # CORS_ORIGINS=https://app.example.com,http://localhost:5173

# 3. Build and start
docker compose up --build

The app will be available at http://localhost.

You do not need Node.js or npm on your PC for this — the Docker build installs frontend dependencies and produces the UI inside the image. Run npm install / npm run dev in frontend only when you are developing or testing the web UI yourself (especially mobile: layout, PWA, live view on a phone). See docs/DEV_WORKFLOW.md for that optional workflow.

Remote access

Off-site / phones: use a mesh VPN (e.g. Tailscale) so devices reach Opus privately — see docs/remote-viewing.md. Before release, run docs/MOBILE_QA_v1.md with VPN on and cellular data.

Operational alerts

Background checks can POST JSON to ALERT_WEBHOOK_URL and/or send email via SMTP (ALERT_SMTP_HOST, ALERT_EMAIL_FROM, ALERT_EMAIL_TO, optional ALERT_SMTP_USER / ALERT_SMTP_PASSWORD). Email uses the same payloads as the webhook, so lock-screen notifications come from the mail app users already have. See commented variables in docker-compose.yml and the module docstring in app/ops_alerts.py.

First-time Setup

On a fresh database (no user accounts), the UI sends you to /setup to create the administrator username and password. Passwords are stored with bcrypt. There are no baked-in default credentials.

If SECRET_KEY is not set in .env, Opus writes a random key to instance/.flask_secret_key (next to the SQLite file) so the API container can start without manual env editing.

If you see the login page again after an update, the session usually expired — sign in with the same credentials you created at setup.


Stream Architecture

IP Camera (RTSP)
      │
      ├──────────────────────────────┐
      ▼                              ▼
 go2rtc (:1984)                 Recorder + FFmpeg
 (live / MSE in browser)         (MP4 segments on disk)
      │                              │
      └────────── nginx (:80) ───────┘
                      │
                 Opus API + React UI

Recommended path: one RTSP URL per camera (main for recording; optional substream for live tiles). Migration path: import channels from an existing NVR under Devices → Sites & migration.

Motion and event-based recording

Opus can record continuously (full timeline retention) or in Events mode (motion-triggered clips). For Events mode:

  • Run the processor service from Docker Compose (app.processing_service). By default it samples the sub stream for motion when one exists (lower CPU); clips are still captured from main (-c:v copy). Set MOTION_RTSP_MODE=main on the processor to force full-res motion sampling. Clips live under RECORDINGS_DIR/clips/. Use the Recordings → Events tab (playback). Live view also prefers sub when configured.
  • Choose the mode per camera under Recordings → Settings → Camera Recording: Off, Continuous, or Events (motion). You can also set recording_policy to events_only or continuous via PATCH /api/cameras/<id>.
  • By default, Events mode does not run 24/7 segment recording (no always-on FFmpeg writer for those cameras), so the Playback timeline stays empty for them — footage lives under Events as motion clips. Opus does not read camera/NVR “motion only” flags; it decides motion in software using the processor. If you want a rolling segment buffer on disk for pre-roll (like a traditional NVR), set EVENTS_ONLY_RECORD_SEGMENTS=1 (or true / yes / on) on the recorder service — any other value leaves Events as clip-only (see docker-compose.yml and docs/hardware-sizing.md).
  • Tune behavior with environment variables on the processor (and shared retention settings): see docs/hardware-sizing.md for PROCESSING_POLL_SECONDS, CLIP_SECONDS, MOTION_COOLDOWN_SECONDS, MOTION_RTSP_MODE, EVENTS_ONLY_BUFFER_HOURS, CLIP_RETENTION_DAYS, and related notes.
  • If you record through the go2rtc RTSP relay (GO2RTC_RTSP_URL), use the same URL on both the recorder and processor services so segments, motion sampling, and clips refer to the same paths (details in docker-compose.yml).
  • Clip timing: Under Recordings → Settings, configure core capture length, optional post-roll (extra seconds after the trigger), pre-roll (up to 15s from the latest completed segment file when segment files exist), poll interval, and cooldown. The same values can be set via environment variables on the processor service (CLIP_SECONDS, CLIP_PRE_SECONDS, CLIP_POST_SECONDS, etc.); DB settings override env when set in the UI. True “seconds before motion” without any buffer is not possible from live RTSP alone — pre-roll uses recorded segments when available.

Documentation

Doc Topic
docs/certified-cameras.md Certified list + regression checklist
docs/hardware-sizing.md Storage, retention, processor/recorder env notes
docs/nvr-replacement-lab.md Migration / lab validation
docs/DEV_WORKFLOW.md Local dev: Compose vs split loop, Makefile
docs/remote-viewing.md Remote viewing (VPN-first) + optional public URL
docs/MOBILE_QA_v1.md Mobile QA before release
docs/PRODUCT_SCOPE_V1.md v1.0 scope summary
mobile/README.md Optional App Store / Play wrapper

Development

Step-by-step commands (PowerShell, env vars, Makefile on WSL): docs/DEV_WORKFLOW.md.

After git pull: run docker compose up --build -d so Opus rebuilds from your repo (docker compose pull alone only updates pre-built images like nginx/go2rtc, not your app code). The opus container syncs the fresh React build into the static_files volume on every start so the UI updates without pruning volumes. Do not rely on docker prune for routine updates.

Typical fast loop (optional — for developing the frontend, including mobile; not required if you only use full Docker): run go2rtc with Docker (docker compose up go2rtc), run Flask (python run.py), and run the Vite dev server (cd frontend && npm install && npm run dev). Set GO2RTC_URL=http://127.0.0.1:1984 for local Flask so it talks to the published go2rtc port. The frontend proxies /api to localhost:5000 and /go2rtc to go2rtc on localhost:1984 (see frontend/vite.config.js). Optional: copy compose.override.yml.example to compose.override.yml for local compose tweaks (file is gitignored).

To run the backend alone:

# Create a virtual environment
python -m venv .venv
.venv\Scripts\activate       # Windows
source .venv/bin/activate    # macOS/Linux

# Install dependencies
pip install -r requirements.txt

# Set env vars
set SECRET_KEY=dev            # Windows
export SECRET_KEY=dev         # macOS/Linux

# Run
python run.py

You'll still need go2rtc running separately (or skip it — the app works without it, streams just won't load).

Quick smoke check: log in, open dashboard live tile, run Discovery (subnet scan uses background job + polling), open Recordings and confirm engine status loads when the recorder service and RECORDER_INTERNAL_STATUS_URL are set.


Contributing

  1. Fork the repo and create a feature branch (git checkout -b feature/my-thing)
  2. Make your changes, test with docker compose up --build
  3. Open a pull request with a clear description of what changed and why

Please keep PRs focused — one feature or fix per PR makes review much easier.


License

See LICENSE file.