A self-hosted, bandwidth-conscious camera monitoring system. Continuously records a rolling 10-minute buffer from any RTSP/ONVIF camera and serves a modern web UI for on-demand snapshots and clip playback — zero data leaves your LAN.
- Rolling buffer — Continuous 10-minute (configurable) recording via ffmpeg
- On-demand only — Snapshots and video served only when you click a button
- Modern dark UI — Dashboard, gallery, clips browser, settings, diagnostics
- Diagnostics — CPU, RAM, temperature, disk, recorder uptime, error log
- Settings page — Camera IP, RTSP path, codec, retention, storage cap, MQTT
- Plugin system — MQTT stub included; easy to add more (webhook, Telegram, etc.)
- Systemd service — Auto-starts on boot, restarts on failure
- Raspberry Pi (any model with enough storage; Pi 4/5 recommended)
- Raspberry Pi OS / Debian Bookworm (aarch64 or armhf)
- Camera with RTSP stream (ONVIF compatible recommended)
- Network access between Pi and camera
git clone https://github.com/yourname/campi # or copy files to ~/campi
cd ~/campi
sudo bash install.shThe installer will:
- Install
ffmpegand Python 3 via apt - Create a Python virtual environment and install dependencies
- Create
media/subdirectories for clips, snapshots, exports - Write a default
config.json - Install and start a
systemdservice (campi.service)
After install, open http://<raspberry-pi-ip>:8081 (default port; configurable in config.json).
Live preview supports WebRTC (via go2rtc, with aiortc fallback) and MJPEG; use the stream selector on the dashboard (Auto / Force WebRTC / Force MJPEG).
- Navigate to Settings in the web UI
- Enter your camera's IP address, username, password
- Set the RTSP stream path (check your camera manual; common values below)
- Click Test Connection — if a snapshot appears, you're good
- Save Settings — the recorder restarts automatically
| Brand | Typical path |
|---|---|
| Hikvision | /Streaming/Channels/101 |
| Dahua | /cam/realmonitor?channel=1&subtype=0 |
| Reolink | /h264Preview_01_main |
| Amcrest | /cam/realmonitor?channel=1&subtype=0 |
| Generic | /stream1 or /video1 |
campi/
├── app.py Main Flask application + API routes
├── recorder.py ffmpeg-based rolling buffer recorder
├── config.py Settings manager (JSON-backed)
├── config.json Your settings (auto-created)
├── requirements.txt
├── install.sh
├── templates/
│ ├── base.html Shared shell, sidebar, topbar
│ ├── index.html Dashboard
│ ├── snapshots.html Snapshot gallery
│ ├── clips.html Clip browser
│ ├── settings.html Settings form
│ └── diagnostics.html System health
├── plugins/
│ └── mqtt_plugin.py MQTT extension stub
└── media/ Created at runtime
├── clips/ Rolling buffer segments (seg_*.mp4)
├── snapshots/ Captured frames (snap_*.jpg)
└── exports/ Merged clip exports (export_*.mp4)
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/snapshot |
Capture fresh snapshot, return path |
| GET | /api/snapshot/latest |
Path of latest saved snapshot |
| GET | /api/snapshots/list |
List snapshots (?limit=N) |
| DELETE | /api/snapshots/clear |
Delete all snapshots |
| GET | /api/clips/list |
List buffer segments |
| GET | /api/clips/last10 |
Export & return merged last-10-min clip |
| GET | /api/settings |
Get current settings (passwords redacted) |
| POST | /api/settings |
Update settings (JSON body) |
| GET | /api/diagnostics |
Full system health JSON |
| POST | /api/recorder/start |
Start recorder |
| POST | /api/recorder/stop |
Stop recorder |
| POST | /api/recorder/restart |
Restart recorder |
| GET | /api/plugins |
List active plugins |
- Create
plugins/myplugin.pywith aregister(app, config, recorder)function - Import and call it from
app.py(afterrecorder = Recorder(config)) - The plugin can publish routes, subscribe to events, or run background threads
- See
plugins/mqtt_plugin.pyfor the pattern
| Key | Default | Description |
|---|---|---|
camera_ip |
192.168.1.100 | Camera LAN IP |
camera_user |
admin | RTSP/ONVIF username |
camera_password |
admin | RTSP/ONVIF password |
rtsp_port |
554 | RTSP port |
rtsp_path |
/stream1 | Stream path on camera |
rtsp_url_override |
"" | Full RTSP URL (overrides all above if set) |
buffer_minutes |
10 | Rolling buffer retention in minutes |
segment_seconds |
60 | Length of each buffer segment file |
video_codec |
copy | copy = no re-encode; h264 / h265 = encode |
snapshot_interval_s |
0 | Auto-snapshot interval (0 = on-demand only) |
max_storage_mb |
2048 | Hard storage cap; oldest clips deleted first |
web_port |
8080 | Flask listen port |
autostart |
true | Start recorder on app launch |
mqtt_enabled |
false | Enable MQTT plugin |
journalctl -u campi -f # Follow live logs
journalctl -u campi --since todaysudo systemctl start campi
sudo systemctl stop campi
sudo systemctl restart campi
sudo systemctl status campi