A Windows viewer for DVR/NVR security cameras over RTSP that tells you why a camera won't connect instead of showing a black square.
I wrote this because the off-the-shelf DVR apps simply refused to connect to my recorder. DVR Viewer scans the recorder, fingerprints the vendor and tries every known RTSP URL layout. Then it shows the cameras in a live grid. When something fails, the diagnostics pane tells you exactly what went wrong: wrong password, RTSP switched off, stream limit reached and so on.
- Automatic camera discovery: port scan, vendor fingerprinting (Hikvision, Dahua, XiongMai, Uniview, Reolink …), and a sweep of ~35 known RTSP URL layouts over TCP and UDP.
- Human-readable diagnostics: every probe is logged, and failures are explained (see Reading the diagnostics).
- Live grid: 1×1, 2×2, 3×3 or 4×4 layouts, per-tile quality setting, double-click to expand,
F11for fullscreen. - Snapshots and recording: PNG snapshots, plus MP4 recording that copies the stream without re-encoding, so it uses almost no CPU.
- Audio from any one camera.
- Auto-reconnect with back-off (up to 30 s) when a stream drops.
- Passwords stay safe: stored with Windows DPAPI, so the password only decrypts under your Windows account on that PC.
- MJPEG-over-HTTP fallback for budget recorders without working RTSP.
- Download
DVR-Viewer-win64.zipfrom the latest release. - Extract it anywhere and run
DVR Viewer.exe. - Install ffmpeg (see below). The app needs it even when you use the
.exe.
- Install Python 3.10+ from python.org.
- Download this repository (Code → Download ZIP, or
git clone https://github.com/deniskostadinov1995/dvr-viewer.git). - Install the dependency:
pip install -r requirements.txt
- Double-click
DVR Viewer.bat. If the window closes immediately, useDVR Viewer (debug).bat. It's the same app, but the console stays open so you can read the error.
The ffmpeg binaries are ~200 MB each, so they are not bundled. Install them with:
winget install Gyan.FFmpegThe app looks for ffmpeg in this order: your PATH, the app's own folder, an ffmpeg\ or ffmpeg\bin\ subfolder next to the app, then the winget install location.
On a PC without ffmpeg, you can make it portable: put ffmpeg.exe and ffprobe.exe into an ffmpeg\ folder next to the .exe. Add ffplay.exe too if you want the Preview and Audio buttons.
- The app opens on the Setup page.
- Enter the recorder's IP address, username, password, and how many channels it has.
- Press Find my cameras.
- When streams are found they are added to the list. Press Save and view cameras.
- Port scan: checks the 16 ports recorders normally use (554, 8000, 34567, 37777, 8899 …).
- Fingerprint: reads the RTSP
Server:header and HTTP banner to name the vendor. - Path sweep: tries ~35 known RTSP URL layouts, over TCP and then UDP, and reports what each one answered.
- HTTP fallback: if no RTSP path works, it looks for MJPEG over HTTP.
- Channel walk: once one channel works, it walks the same layout across every channel to find the ones with a camera.
The full transcript appears in the Diagnostics pane. Save log writes it to a text file.
| What the log says | What it means |
|---|---|
401 Unauthorized on many paths |
The path is right, the login is wrong. Some recorders need a separate "media"/"ONVIF" user, and some reject passwords with symbols. |
404 Not Found on everything |
The device speaks RTSP, but none of the known URL layouts fit. Open an issue with the log. |
Nothing is listening on any RTSP port |
RTSP is switched off on the recorder. Enable it, usually under Network > Advanced > RTSP or Network > Platform Access > ONVIF. |
503 Service Unavailable |
The DVR hit its stream limit. Close its web page or phone app and retry. |
no reply on this port |
Wrong IP, the DVR is on another subnet, or a firewall is in the way. |
| No ports open at all | Wrong IP, or the recorder is off. Check its own screen for its address. |
| Control | What it does |
|---|---|
| Layout | 1×1, 2×2, 3×3 or 4×4 |
| Quality | Decode height per tile. Lower means less CPU. Start at Medium, and drop to Low on a weak PC with many channels. |
| Expand / double-click | One camera fills the window. Esc goes back. |
F11 |
Fullscreen |
| Snapshot | Saves a PNG into snapshots\ |
| Record | Copies the stream to MP4 in recordings\ (no re-encoding) |
| Audio | Plays that camera's audio (opens a second connection to the DVR) |
| Reconnect | Forces a reconnect on that tile |
In Setup, select it and press:
- Test: re-probes it and prints the result, retrying over UDP if TCP fails.
- Preview: opens the stream in
ffplay. If ffplay plays it but the app doesn't, it's a bug in the app. If ffplay fails too, the problem is the stream or the network. - Copy URL: copies the full URL, including the password, to paste into VLC.
Add manually accepts any URL ffmpeg can read:
rtsp://admin:password@192.168.1.108:554/Streaming/Channels/101
http://admin:password@192.168.1.108/cgi-bin/mjpg/video.cgi?channel=1&subtype=1
If the password contains @ : / ? or #, percent-encode it (@ → %40). The automatic scan does this for you.
| Running as | Settings & captures |
|---|---|
.exe |
%LOCALAPPDATA%\DVR Viewer\ (config.json, snapshots\, recordings\). This is outside the app folder, so updating or rebuilding the app never wipes it. |
| from source | next to the .py files |
config.json stores the DVR password as a Windows DPAPI blob. Only your Windows account on that PC can decrypt it, so the file is useless if copied elsewhere.
Camera entries store the URL without credentials. The username and password are only injected when ffmpeg is launched:
{ "name": "Camera 1", "url": "rtsp://192.168.1.108:554/ch01/0", "cred_enc": "" }An empty cred_enc means the camera uses the main account, so changing the password once updates every camera.
pip install pyinstallerThen run build_exe.bat. The result lands in dist\DVR Viewer\. Your settings are not touched.
make_icon.py regenerates dvr_viewer.ico (needs pip install pillow).
Each camera is one ffmpeg process that decodes, scales and pads to a fixed size, then pipes raw RGB frames to the app. Python never does per-pixel work.
Hardware decoding (hw_accel in config.json) is off by default. In testing, d3d11va decoding of 1080p H.264 was slower than pure software (2.00 s vs 1.79 s), because frames have to be copied back from GPU memory for the CPU scaler anyway. Turn it on for a weak CPU; the app falls back to software automatically if GPU decoding yields no frames.
| File | Purpose |
|---|---|
dvr_viewer.py |
GUI: live grid, setup page, diagnostics |
dvr_core.py |
Port scan, fingerprinting, RTSP path library, prober |
dvr_stream.py |
ffmpeg decode pipeline, recorder, audio |
dvr_config.py |
Settings + DPAPI password encryption |
DVR Viewer.spec / build_exe.bat |
PyInstaller build |
make_icon.py |
Generates the app icon |
MIT © Denis Kostadinov

