A lightweight, self-hosted Network Video Recorder (NVR) built in Go that uses FFmpeg for RTSP recording and MP4 output. SimpleNVR integrates with go2rtc for camera stream management and provides a web interface for monitoring, recording, and playback.
Built with go2rtc and Home Assistant in mind, this NVR assumes you already have adequate live viewing arrangements from your go2rtc headend to endpoints such as Home Assistant or TVs. It provides basic live stream preview for troubleshooting but focuses on recording and playback.
It ingests RTSP streams via FFmpeg, usually from a go2rtc server, and saves them to disk in 10-minute segments with configurable retention periods per camera (1-365 days).
It provides an intuitive timeline-based interface to replay recordings with multi-camera synchronized playback and download merged clips as needed. It runs from a Docker container and tries to be as unobtrusive as possible. It pairs well with TrueNAS Scale or similar storage solutions.
⚠️ Security Notice
This app makes no attempt at access control or authentication. You are expected to use reverse proxies, VLANs, and firewall rules to achieve access control.
I really didn't want to build another app, but proprietary NVRs are maddening to use, and existing alternatives are often bloated and quirky. They try to do everything: intercoms, entry systems, alerting, AI object recognition, stream viewing, event orchestration—much of which duplicates tools you likely already run elsewhere.
SimpleNVR was built with a simple philosophy: an NVR should strictly ingest video, catalog and store it, and make it painless to review and export.
/path/to/SimpleNVR/
├── certs/
│ ├── cert.pem
│ └── key.pem
└── recordings/
├── cameras.json
└── events.json
services:
simplenvr:
container_name: simplenvr
image: ghcr.io/mouldybread/simplenvr:latest
restart: unless-stopped
ports:
- "8443:443"
volumes:
- ./recordings:/recordings
- ./certs:/certsdocker-compose up -dThe application will be available at https://<YOUR_SERVER_IP>:8443.
- MP4 Output - Browser-compatible video files with H.264 codec
- 10-Minute Segments - Automatic file splitting for reliability and management
- Configurable Retention - Per-camera retention periods (1-365 days)
- Timeline Playback - Interactive timeline with multi-camera synchronized playback
- Event Timeline Integration - Visual event markers with click-to-seek
- Recording Downloads - Export merged recordings with customizable time ranges
- Live Stream Preview - Real-time camera feeds through web interface
- go2rtc Integration - Auto-discover and import cameras (prevents duplicate imports by URL)
- Drag-and-Drop Reordering - Organize camera order with visual feedback
- Toast Notifications - Non-intrusive feedback for all operations
- Per-Camera Storage Tracking - View disk space consumed by each camera
- Native HTTPS/TLS Support - Auto-generates self-signed certificates in
/certson first boot, or loads custom certificates
SimpleNVR includes a comprehensive event tracking and visualization system:
- Motion Detected (👁️)
- Person Detected (👤)
- Vehicle Detected (🚗)
- Intrusion (
⚠️ ) - Loitering (⏱️)
- Object Removed (📦)
- Object Left Behind (🎒)
- Sound Alert (🔔)
- Line Crossing (➡️)
- Tamper Alert (🚨)
- Timeline Visualization - Events appear as interactive markers on playback timeline
- Event Markers - Distinct emoji icons for each event type with color-coded indicators
- Interactive Markers - Click any event marker to jump directly to recording at that moment
- Event Tooltips - Hover over markers to see event details (type, camera, time, duration)
- Event Log Page - Dedicated page with filterable, paginated event history
- Event Filtering - Filter events by camera, event type, and date range
- Pagination Control - Adjustable display (10-100 events per page)
- Per-Camera Event Management - Clear events for individual cameras
- Bulk Event Operations - Delete all events with confirmation code
- Event Storage - JSON-based storage in
/recordings/events.json
- Add events manually or import from external systems
- Events tied to specific cameras and timestamps
- Duration tracking for each event
- Automatic synchronization with timeline playback
- Event count display in camera management
- Unified Control Header - All playback controls accessible from single header bar
- Timeline View - Visual representation of recording availability with 24-hour timeline
- Timeline Zoom - Mouse wheel zoom with adaptive time markers (hourly to 1-minute granularity)
- Event Markers - Visual indicators for camera events on timeline
- Multi-Camera Playback - View up to 16 cameras simultaneously in grid layout
- Dropdown Camera Selector - Multi-select dropdown with color-coded badges
- Fullscreen Mode - Immersive viewing with timeline controls
- Download Manager - Select cameras and time ranges for merged video downloads
- Collapsible Sections - Organized layout with state persistence
- Responsive Design - Desktop and mobile compatible with consistent spacing
- Dark Mode Support - System preference detection
- Color-coded Identification - 16 distinct colors for camera differentiation
- Interactive Timeline - Click timeline to jump directly to specific times or event markers
- Timeline Zoom - Scroll to zoom in/out centered on cursor position
- Full day view: Hourly markers
- 4-12 hours: 30-minute intervals
- 2-4 hours: 10-minute intervals with HH:MM labels
- <2 hours: 1-minute precision with detailed markers
- Event-based Navigation - Click event markers to seek directly to events
- Event Tooltips - Rich event information on hover
- Time Range Selection - Easily navigate through recordings
- Playback Controls - Header controls for Speed (0.25x–4x), Mute All, Fullscreen, and Download
- Keyboard Shortcuts - Space for play/pause, Arrow keys (Left/Right) for 10-minute segment navigation
- Multi-Camera Sync - Synchronized playback across selected cameras
- Fullscreen with Timeline - Access timeline and controls in fullscreen mode
- Persistent Camera Order - Order set in configuration maintains in playback view
- Clean Video Display - Individual video controls hidden for streamlined interface
- Continuous Recording - Start/stop recording per camera
- Automatic Cleanup - Old recordings deleted based on retention settings
- Organized Storage - Each camera gets unique directory with ID
- Auto-reconnect - Automatic retry on stream errors (5-second delay)
- Recording Downloads - Export concatenated recordings as single MP4 files
- Storage Metrics - Per-camera disk usage displayed in camera management
- Drag-and-Drop Ordering - Reorder cameras with visual feedback and hover highlighting
- Real-time Updates - Camera list updates immediately across all views
- Persistent Order - Camera arrangement saved and synchronized
- Color Assignment - Automatic color assignment from 16-color palette
- Bulk Operations - Set retention, remove cameras, cleanup recordings, clear events
- Duplicate Prevention - go2rtc scan prevents importing duplicate RTSP URLs
- Storage Display - View disk space used by each camera's recordings
The main interface features a unified header bar containing:
- Simple NVR title (left)
- Camera Selector dropdown with color badges
- Date Picker for selecting playback date
- Speed Control for playback speed adjustment (0.25x to 4x)
- Inline Action Buttons - Mute All, Fullscreen, and Download
- Events Icon (📋) for viewing the event log modal
- Settings Icon (⚙️) for camera management
- Click the ⚙️ Settings icon in the top right.
- Enter your go2rtc URL (default:
http://go2rtc:1984). - Click Scan & Import Streams.
- All streams will be automatically added with assigned colors (duplicate URLs are automatically skipped).
- Open Camera Management via ⚙️ Settings.
- Under Add Camera Manually, enter the camera name and RTSP URL.
- Click Add Camera.
- Open Camera Management modal.
- Hover over a camera card to locate the drag handle (6 dots icon on the left).
- Click and drag the card into the desired order.
- The arrangement automatically saves and applies across playback and grid views.
- Each camera automatically receives one of 16 distinct colors.
- Colors appear in dropdown selector badges, timeline recording segments, video labels, and event markers.
- Start/Stop Recording: Toggle recording directly on individual camera cards in Camera Management.
- Set Retention Period: Set retention days per camera, or use Set Retention under Bulk Operations.
- View Storage Usage: Real-time disk space usage is calculated and displayed on each camera card.
- Click the 📋 Events icon in the header.
- Filter events by Camera, Event Type, or Date Range.
- Use page navigation controls or change pagination density (10–100 events per page).
- Events appear as circular emoji markers on the 24-hour timeline.
- Hover over any marker for an instant detail tooltip.
- Click any event marker to seek the video grid directly to that timestamp.
- Open the Cameras dropdown in the header and check the cameras you wish to view.
- Select a date using the Date picker.
- Click anywhere on the timeline canvas to seek all synchronized camera streams to that exact moment.
- Hover over the timeline canvas and scroll with your mouse wheel to zoom in/out centered on your cursor.
- Zoom adjusts dynamically from full day (24h) down to 1-minute precision intervals.
- Space: Toggle Play/Pause across all active videos.
- Left Arrow (←): Jump 10 minutes backward.
- Right Arrow (→): Jump 10 minutes forward.
Grid layout automatically adjusts for the number of selected cameras:
- 1 camera: Full screen
- 2 cameras: 2x1 grid
- 3-4 cameras: 2x2 grid
- 5-6 cameras: 3x2 grid
- 7-9 cameras: 3x3 grid
- 10+ cameras: 4x? grid
All cameras sync to the same timeline position. Video stream labels show the camera name with its assigned color. Individual video controls are hidden for a clean interface, and arrangement matches the configured order.
- Click on the timeline near your desired recording section.
- Click Download in the header.
- Adjust the download window using -10 min or +10 min adjustments, or click Reset.
- Select the included cameras and click Download Merged Videos.
- Open Camera Management (⚙️ icon).
- Click 👁️ View Live on any camera.
- Stream opens in a modal window.
- Click ✖ Close or press Esc to exit.
- Click Remove once to arm the button.
- Click Remove again within 3 seconds to confirm deletion.
- Use Remove All Cameras for bulk deletion (requires confirmation code).
- Set All Retention: Update retention for all cameras at once.
- Delete All Recordings: Remove all recordings from all cameras (requires confirmation code).
- Delete All Cameras: Remove all cameras (requires confirmation code).
- Clear All Events: Remove all events from all cameras (requires confirmation code).
- Cleanup Orphaned Recordings: Remove leftover recordings from deleted cameras.
SimpleNVR ingests external events (AI detections, line crossings, motion sensors) via Home Assistant's native rest_command.
Add the following to your Home Assistant configuration.yaml file (adjust IP address and port to match your SimpleNVR instance):
rest_command:
simplenvr_trigger_event:
url: "https://<YOUR_SERVER_IP>:8443/api/events/trigger"
method: POST
verify_ssl: false # Required if using auto-generated self-signed certificates
content_type: "application/json"
payload: >
{
"camera_name": "{{ camera_name }}",
"label": "{{ label }}",
"start_time": "{{ start_time }}",
"duration_seconds": {{ duration_seconds }}
}- Go to Settings > Devices & Services > Helpers.
- Click + Create Helper and select Toggle.
- Name it
Cam1 Event Toggle(input_boolean.cam1_event_toggle).
This automation calculates event duration when the toggle turns off and posts the event details to SimpleNVR:
alias: "NVR: Log Cam1 Event to SimpleNVR"
description: "Calculates event duration from the toggle and sends it to the NVR"
mode: single
trigger:
- platform: state
entity_id: input_boolean.cam1_event_toggle
from: "on"
to: "off"
action:
- service: rest_command.simplenvr_trigger_event
data:
# MUST match the exact camera name configured inside SimpleNVR
camera_name: "Cam1"
# Event label (e.g., Line Crossing, Motion Detected, Person Detected)
label: "Line Crossing"
# ISO8601 UTC start time from state change
start_time: "{{ trigger.from_state.last_updated.isoformat() }}"
# Duration calculation in seconds
duration_seconds: >
{{ (as_timestamp(trigger.to_state.last_updated) - as_timestamp(trigger.from_state.last_updated)) | int(default=5) }}SimpleNVR runs over HTTPS on port 443 inside the container (mapped to 8443 in the example Docker Compose file).
- Auto-Generated Self-Signed Certs: If no certificates exist in
/certs, SimpleNVR automatically generates self-signedcert.pemandkey.pemfiles on first boot. - Custom Certificates: To use custom or Let's Encrypt certificates, mount your own
cert.pemandkey.pemfiles inside/certs/.
- Location:
/recordings/{camera_id}/ - Format:
YYYYMMDD_HHMMSS_CameraName.mp4 - Segment Length: 10 minutes per file
- Files per day: 144 per camera (24 hours × 6 segments/hour)
cameras.json: Camera definitions, retention, colors, and order (/recordings/cameras.json)events.json: Logged event markers (/recordings/events.json)cert.pem/key.pem: TLS certificates (/certs/)
main.go: HTTP/HTTPS server, API handlers, camera managementrecorder.go: RTSP recording, MP4 muxing, cleanup routineshandlers.go: HTTP request handlers, camera operationsplayback.go: Timeline data, recording queriesevents.go: Event management, storage, and API endpointscleanup.go: Retention and orphan cleanuptemplates/: HTML templates (index, camera cards)static/js/: Modular JavaScript (playback, cameras, modals, core, recordings, events, toast)static/style.css: Application styling with responsive design
- RTSP Connection:
gortsplibclient connects to camera RTSP stream. - H.264 Decoding: RTP packets depacketized to NAL units.
- SPS/PPS Extraction: Codec parameters parsed from stream.
- MP4 Muxing:
deepch/vdkwrites MP4 container to disk. - Segmentation: Rotates file every 10 minutes.
- Cleanup: Old files deleted based on per-camera retention settings.
- Storage: Local JSON file with event array (
/recordings/events.json). - Structure: Timestamp, camera name, label, duration in seconds.
- Timeline Integration: Events loaded via date-based API queries.
- Canvas Rendering: Drawn as circular markers with emoji icons.
- Click Detection: Euclidean distance calculation from cursor to event marker.
- Tooltip System: Dynamic DOM elements created/destroyed on hover.
- Seek Integration: Event click triggers video seek to exact event timestamp.
- Date-based Query: API returns all recordings and events for selected date.
- Visual Timeline: Canvas-based 24-hour timeline showing recording availability.
- Event Overlay: Events rendered on top of recording segments.
- Zoom Implementation:
- Mouse wheel event with
preventDefault. - Dynamic zoom level (1.0 = full day, up to 24.0 = 1-hour view).
- Cursor-centered zooming preserves context.
- Adaptive time markers based on visible range.
- Efficient rendering of only visible segments and events.
- Mouse wheel event with
- Color-coded Cameras: 16 distinct colors automatically assigned.
- Real-time Sync: Multiple cameras play synchronized to selected time.
- Segment Matching: Finds appropriate recording segment based on timestamp.
- Persistent Selection: Camera selection stored in
localStorage.
- Drag-and-Drop: HTML5 drag and drop with visual feedback.
- Hover Highlighting: Drag-over class shows valid drop zones.
- Drag Leave Detection: Highlight clears when dragging away from target.
- Configuration Sync: Order set in modal applies to playback grid.
- Stable Colors: Color assignments tied to camera ID, not position.
- Label Synchronization: Video labels update when order changes.
- Persistent Storage: Order saved to
cameras.json. - Real-time Updates: Changes immediately reflected in all views.
- FFmpeg Concatenation: Uses concat demuxer to merge 10-minute segments.
- Streaming Output: Progressive download as FFmpeg processes.
- Time Range Filtering: Selects only recordings within specified time range.
- Per-Camera Output: Creates separate merged file for each selected camera.
- Browser-friendly: MP4 output with fragmented headers for streaming.
GET /api/cameras- List all cameras (HTML partial with drag-drop markup)POST /api/cameras/add- Add new camera with color assignmentPOST /api/cameras/delete- Remove cameraPOST /api/cameras/delete-all- Remove all camerasPOST /api/cameras/update-retention- Update single camera retentionPOST /api/cameras/update-retention-all- Update all cameras retentionPOST /api/cameras/reorder- Update camera display order
POST /api/record/start- Start recording cameraPOST /api/record/stop- Stop recording camera
GET /api/recordings/list?camera_id={id}- List recordings (JSON)GET /api/timeline/recordings?date=YYYY-MM-DD- Get timeline data with color infoGET /recordings/{camera_id}/{filename}- Serve recording file
GET /api/events/list- List all events (JSON)POST /api/events/add- Add new eventPOST /api/events/clear-camera- Clear events for specific cameraPOST /api/events/clear-all- Clear all events (requires confirmation)
GET /api/download?camera_id={id}&start_time={iso8601}&end_time={iso8601}- Download merged recording
POST /api/go2rtc/scan- Scan and import streams (skips duplicate URLs)
GET /api/stream/{camera_name}- Proxy live stream from go2rtc
POST /api/recordings/delete-all- Delete all recordings from all camerasPOST /api/recordings/cleanup-orphaned- Remove orphaned recordings
github.com/bluenviron/gortsplib/v4: RTSP clientgithub.com/bluenviron/mediacommon: H.264 handlinggithub.com/deepch/vdk: MP4 container muxinggithub.com/pion/rtp: RTP processing
- FFmpeg: Video concatenation for downloads
- Vanilla JavaScript: No framework dependencies
- HTML5 Canvas: Timeline rendering with zoom and events
- HTML5 Drag and Drop: Camera reordering
- CSS Variables: Theming and customization
- localStorage: Persistent camera selection
- Chrome/Edge: Full support with hardware acceleration
- Firefox: Full support with compatibility optimizations
- Safari: Full support (H.264 native)
- Mobile: Responsive design, touch-friendly
Edit recorder.go:
segmentDuration := 10 * time.Minute // Change to desired durationEdit main.go in addCameraHandler:
RetentionDays: 7, // Change default daysEdit main.go (and docker-compose.yml if mapping ports):
http.ListenAndServeTLS(":443", certPath, keyPath, nil)Edit playback.js:
const CHUNK_SIZE_MS = 10 * 60 * 1000; // 10 minutes in millisecondsEdit playback.js:
const COLORS = [
'#3b82f6', // Blue
'#10b981', // Green
// ... add or modify colors
];Edit playback.js:
const EVENT_ICONS = {
'Motion Detected': '👁️',
'Person Detected': '👤',
// ... add or modify event types and icons
};Edit playback.js in setupTimelineZoom:
const newZoomLevel = Math.max(1.0, Math.min(24.0, timelineZoomLevel * zoomDelta));
// 1.0 = full day minimum, 24.0 = 1-hour maximumEdit playback.js in updateVideoGrid:
video.controls = false; // Set to true to show individual controlsdocker logs -f simplenvrStarting recording for {camera}- Recording initiatedCompleted 10-minute segment for {camera}- Segment savedRecording error for {camera}- Connection issue, retryingCleaned up old recording- Retention cleanup executedDownload request for camera {name}: {count} files- Download initiatedDownload completed successfully for camera {name}- Download finishedCamera order updated- Drag-drop reorder savedEvent added: {type} for {camera}- New event loggedSkipping camera {name} - URL already exists- go2rtc scan duplicate preventionFound existing TLS certificates in /certs.- HTTPS certs loadedNo TLS certificates found. Auto-generating self-signed TLS certificate...- Cert generation trigger
- Check RTSP URL is correct.
- Verify camera is H.264 encoded.
- Ensure network connectivity to camera.
- Check logs for connection errors (
docker logs simplenvr).
- Verify recordings exist in
/recordings/{camera_id}/. - Check browser console for errors.
- Ensure MP4 files are not corrupted.
- Disable browser extensions that might interfere with video playback.
- Verify correct date is selected in the Date picker.
- Check that cameras are checked in the Cameras dropdown.
- Confirm recordings exist for that date in the filesystem.
- Check browser console for API errors.
- Try zooming out to full day view.
- Verify events exist in
/recordings/events.json. - Check browser console for JavaScript errors.
- Confirm date matches event timestamps.
- Check that events are properly formatted in JSON.
- Ensure mouse cursor is over the timeline canvas element.
- Check browser console for JavaScript errors.
- Verify browser supports wheel events.
- CPU: ~2-5% per active recording
- RAM: ~50-100MB per camera
- Disk I/O: ~1-2 MB/s per camera (depends on stream bitrate)
- Network: Matches camera RTSP bitrate
- 10 cameras: Standard hardware (4 cores, 8GB RAM)
- 25 cameras: Mid-range server (8 cores, 16GB RAM)
- 50+ cameras: Dedicated server (16+ cores, 32GB+ RAM)
Example: 1080p camera @ 4 Mbps
- Per hour: ~1.8 GB
- Per day: ~43 GB
- 7-day retention: ~301 GB
- 30-day retention: ~1.29 TB
- Keeping cameras and the NVR on a separate isolated VLAN is highly recommended.
- go2rtc acts as the main RTSP gateway.
- Native HTTPS: Serves web interface and API natively over HTTPS on port 443.
- VLAN/Firewalls: Network isolation is strongly recommended since there is no built-in auth.
- Recordings stored on local host filesystem.
- No cloud uploads (100% self-hosted).
- Container has limited access to recordings and certs directories.
- Events stored locally in JSON format.
IMPORTANT NOTICE: SimpleNVR is provided on an "AS IS" and "AS AVAILABLE" basis, without warranties or conditions of any kind, either express or implied.
This software is strictly intended for personal, non-critical, or self-hosted experimental use and is not certified for high-risk, critical surveillance, or life-safety applications. The author(s) and contributor(s) make no guarantees, representations, or warranties that:
- The software will operate uninterrupted, continuously, or error-free.
- Video footage, motion events, audio, or metadata will be successfully captured, processed, stored, or retrieved.
- The software will survive hardware failures, network disruptions, power outages, software bugs, or unexpected system crashes.
By using this software, you acknowledge and agree that you are solely responsible for:
- Independently testing, verifying, and monitoring the recording integrity and system health in your deployment environment.
- Maintaining appropriate backup, failover, and hardware redundancy protocols for critical footage.
- Ensuring compliance with local surveillance, privacy, and video retention laws.
To the maximum extent permitted by applicable law, in no event shall the author(s), developer(s), or copyright holder(s) be liable for any claim, loss, damage, or liability whatsoever—whether direct, indirect, incidental, special, or consequential—arising out of or in connection with the software, including without limitation:
- Property damage, theft, burglary, robbery, or unrecorded security incidents.
- Data corruption, loss of video logs, or storage drive wear.
- Financial loss, business interruption, or legal claims resulting from software malfunction or failure to record.
BY INSTALLING, DEPLOYING, OR USING THIS SOFTWARE, YOU ACKNOWLEDGE THAT YOU HAVE READ THIS DISCLAIMER, UNDERSTAND IT, AND AGREE TO ASSUME ALL ASSOCIATED RISKS.
This project is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (CC BY-NC-SA 4.0).
- Attribution (BY): Reusers must give appropriate credit, provide a link to the license, and indicate if changes were made.
- NonCommercial (NC): Reusers may not use the material for commercial purposes.
- ShareAlike (SA): Reusers who remix, transform, or build upon the material must distribute their contributions under the exact same license.
See the full LICENSE file for complete details.
For issues, feature requests, or questions, please open an issue in the repository at https://github.com/mouldybread/SimpleNVR.
- Review logs:
docker logs simplenvr - Check recording directory:
/recordings/ - Check certs directory:
/certs/ - Check event log:
/recordings/events.json - Verify go2rtc:
http://go2rtc:1984 - Check configuration:
/recordings/cameras.json