I’ve been thinking about getting some CCTV cameras for a while, and last summer I purchased two Reolink PoE cameras. The footage is stored on a SD card, and uploaded to a local FTP server.
But I also wanted to record continuously, without getting a dedicated Reolink NVR. I’ve seen some YouTube videos by Tall Paul Tech where he uses FFmpeg to record CCTV footage — so let’s do that! 🙂
Table of contents
Introduction
My plan was to set up a new container in Proxmox, and mount a dedicated 8 TB Seagate Skyhawk disk. Then run FFmpeg and record continuously to the disk — voila: NVR!
Proxmox container
After installing the Skyhawk disk, I initialized it with GPT and created a directory:
Then I logged into Proxmox with SSH and root — and mounted the disk to the container and changed the owner and group so the container had write access:
pct set 105 -mp0 /mnt/pve/skyhawk8,mp=/home/thomas/nvr
chown 101000:101000 /mnt/pve/skyhawk8/
When container is not privileged, UID and GID are not mapped. Container IDs gets 10000 added; so UID 1000 in container is 101000 on the host.
On the hypervisor:
drwxr-xr-x 4 101000 101000 4.0K Apr 15 09:58 ssd500
Inside container:
drwxr-xr-x 4 thomas thomas 4.0K Apr 15 09:58 nvr
FTP server
With the disk now mounted, I installed vsftpd
FTP server:
sudo apt install vsftpd
And used the following configuration in /etc/vsftpd.conf
:
listen=NO
listen_ipv6=YES
anonymous_enable=NO
local_enable=YES
write_enable=YES
dirmessage_enable=YES
use_localtime=YES
xferlog_enable=YES
connect_from_port_20=YES
secure_chroot_dir=/var/run/vsftpd/empty
pam_service_name=vsftpd
rsa_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
rsa_private_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
ssl_enable=NO
Now the CCTV cameras could be configured to store their video clips on the FTP server. I used the container user and password as log in credentials.
Streaming proxy
On to the NVR; instead of FFmpeg connecting directly to the cameras — I chose to set up a streaming proxy, called MediaMTX. I found that FFmpeg didn’t handle loosing the connection to the camera very well, and that seems to happen occasionally.
Instead; MediaMTX connects, and keeps an open connection, to the cameras — reconnecting if necessary. Then FFmpeg connects to MediaMTX.
Most IP cameras expose their video stream by using a RTSP server that is embedded into the camera itself. In particular, cameras that are compliant to ONVIF profile S or T meet this requirement. You can use MediaMTX to connect to one or multiple existing RTSP servers and read their video streams. — MediaMTX readme
Another advantage is that all other clients, like VNC, can connect to MediaMTX as well — without increasing the bandwidth usage on the cameras themselves. Reolink cameras only support two simultaneous streams in the highest quality.
I’m using the following configuration — connecting to both the main and sub RTSP streams:
readBufferCount: 4096
paths:
driveway:
source: rtsp://user:password@camera.i:a554/h265Preview_01_main
sourceProtocol: tcp
driveway_sub:
source: rtsp://user:password@camera.ip:554/h264Preview_01_sub
sourceProtocol: tcp
#sourceOnDemand: yes
garage:
source: rtsp://user:password@camera.ip:554/h265Preview_01_main
sourceProtocol: tcp
garage_sub:
source: rtsp://user:password@camera.ip:554/h264Preview_01_sub
sourceProtocol: tcp
#sourceOnDemand: yes
sourceOnDemand
option — MediaMTX only connects to the source stream if needed.
FFmpeg
And finally; the actual NVR magic. Below is the FFmpeg script that connects to a camera video stream — and store it. One parameter is required; TARGET
, this needs to match a path in MediaMTX.
#!/bin/bash
TARGET=$1
# Create output folder if missing, otherwise FFmpeg fails to start
[[ -d /home/thomas/nvr/$TARGET ]] || mkdir /home/thomas/nvr/$TARGET
ffmpeg -hide_banner -y \
-loglevel error \
-rtsp_transport tcp \
-i rtsp://localhost:8554/$TARGET \
-vcodec copy \
-acodec copy \
-map 0 \
-f segment \
-reset_timestamps 1 \
-segment_time 300 \
-segment_format mp4 \
-segment_atclocktime 1 \
-strftime 1 \
/home/thomas/nvr/$TARGET/%Y%m%d_%H%M%S.mp4
Options explained:
-hide_banner
: Don’t print copyright notice, build options, versions, etc.-y
: Overwrite output files without asking.-rtsp_transport tcp
: Use TCP as lower transport protocol.-i rtsp://
: Input stream, in our case the local streaming proxy-vcodec/acodec copy
: Copy video and audio, without re-encoding.-map 0
: Select all streams from input index #0 (the 1st input).-f segment
: Output should be split into segments.-reset_timestamps 1
: Reset timestamps at the beginning of each segment.-segment_time 300
: Set segment duration to 300 seconds.-segment_format mp4
: Set segment format to MP4.-segment_atclocktime 1
: Split at regular clock time intervals.-strftime 1
: Use thestrftime
function to define the name of the new segments to write.- Include year, month, day, hour, minute and second in output MP4 file.
We can use the same script for all cameras, simply by appending a different target/path parameter.
Supervisor
To make sure the streaming proxy, MediaMTX, and NVR scripts keep running — I’m using Supervisor. With the following applications and groups defined in /etc/supervisor/conf.d/nvr.conf
:
[group:server]
programs=streaming
priority=1
[program:streaming]
command=/home/thomas/mediamtx cctv-stream.yml
directory=/home/thomas
autostart=true
autorestart=true
user=thomas
stderr_syslog=true
stdout_syslog=true
[group:nvr]
programs=driveway,garage
priority=10
[program:driveway]
command=/bin/bash nvr.sh driveway
directory=/home/thomas
autostart=true
autorestart=true
user=thomas
stderr_syslog=true
stdout_syslog=true
stopasgroup=true
stopsignal=QUIT
startretries=30
startsecs=5
[program:garage]
command=/bin/bash nvr.sh garage
directory=/home/thomas
autostart=true
autorestart=true
user=thomas
stderr_syslog=true
stdout_syslog=true
stopasgroup=true
stopsignal=QUIT
startretries=30
startsecs=5
The configuration has two groups: streaming
and nvr
— streaming
has a lower priority value, and will be started first. This is important because the NVR scripts will fail if the streaming proxy is not running.
The programs in the nvr
group use the same NVR bash script, but with different arguments. All programs are auto started, and restarted if they die.
Cleanup
To clean up old CCTV recordings, and organize NVR files, another bash script is used:
#!/bin/bash
for cam in `find ~/cctv/ -mindepth 1 -maxdepth 1 -type d -not -path '*/lost+found'`; do
## Clean up old FTP recordings
if [ -d $cam ]; then
find $cam/ -type f -mtime +7 -delete
find $cam/ -mindepth 1 -type d -empty -delete
fi
done
for cam in `find ~/nvr/ -mindepth 1 -maxdepth 1 -type d -not -path '*/lost+found'`; do
## Clean up old NVR recordings
if [ -d $cam ]; then
find $cam/ -type f -mtime +7 -delete
find $cam/ -mindepth 1 -type d -empty -delete
fi
## Organize NVR recordings
if [ -d $cam ]; then
cd $cam/
for fname in *.mp4; do
[ -f "$fname" ] || continue
date=${fname:0:8}
[[ $date == $(date +%Y%m%d) ]] && continue
y=${date:0:4}
m=${date:4:2}
d=${date:6:2}
target="$y/$m/$d"
mkdir -p "$target"
mv -- "$fname" "$target"
done
fi
done
It deletes recordings older than 7 days, and organizes NVR video files into year/month/day
folders. NVR recordings from the same day are not touched, as this could interfere with open files.
The cleanup scripts is scheduled to run every day — one minute over midnight:
$ crontab -l
# m h dom mon dow command
1 0 * * * /home/thomas/clean.sh
Home Assistant
I’ve also added the CCTV cameras to Home Assistant, as Generic Camera devices.
Home Assistant didn’t like the main stream, maybe because it is H.265, or maybe because it is 4K at 25 FPS. I’m instead using using the sub stream — which is H.264, 360p at 15 FPS.
Below is the device configuration for one of the cameras:
"options": {
"use_wallclock_as_timestamps": false,
"stream_source": "rtsp://cctv-nvr.hostname.lan:8554/garage_sub",
"authentication": "basic",
"framerate": 1.0,
"verify_ssl": false,
"limit_refetch_to_url_change": false,
"rtsp_transport": "tcp",
"content_type": "image/jpeg"
}
Wrapping up
So there you have it — a simple, cheap, and effective NVR. Compatible with pretty much anything that has a video stream.
I’m quite happy with my Reolink cameras so far, but I don’t want to be tied into their ecosystem.
🖖
Last commit 2024-12-11, with message: Upgrade Hugo and CCTV cleanup script.