Salut.
J’ai retrouvé au fond d’un carton un vieux Steam Link modèle 1003. Après une petite recherche sur internet il se trouve que c’est très simple d’y installer Debian pour l’utiliser comme un petit serveur. Ça fait un moment que je voulais enregistrer le flux vidéo de la caméra IP chez moi dans un lieu physiquement éloigné. Couplé à un VPN et un disque dur externe il fera parfaitement l’affaire.
Voici comment j’ai fait, et surtout les deux ou trois pièges qui m’ont coûté une soirée.
Le principe : copier les paquets, pas réencoder
La tentation, quand on parle d’enregistrement vidéo, c’est de sortir ffmpeg et de lui demander du H.264 bien propre. Mauvaise idée ici : le processeur du Steam Link est un ARMv7 simple cœur à 1 GHz. Réencoder du 1080p en temps réel le ferait tousser.
Avec -c copy, ffmpeg recopie les paquets du flux sans y toucher : on obtient exactement la qualité de la caméra pour un coût processeur négligeable. Le seul vrai choix, c’est le format, et j’ai pris du MKV plutôt que du MP4. Un MP4 écrit par une machine qui s’arrête brutalement reste souvent illisible, parce que son index est écrit à la fin du fichier ; un MKV encaisse la coupure et reste lisible jusqu’à la dernière image enregistrée.
L’idée directrice est donc simple : un fichier par tranche de dix minutes, lisible dans VLC depuis n’importe quelle machine du réseau, et une rotation qui supprime les plus anciens. Pas de cloud, pas d’abonnement, pas de boîtier à 300 € et une consommation électrique faible.
Ce qu’il faut sur la machine
Un Debian à jour, et ffmpeg : sudo apt-get update && sudo apt-get -y install ffmpeg. Sur ce matériel, comptez un bon quart d’heure : le disque système est une clé USB.
Côté stockage, un disque monté sur /srv/storage. Le mien est en NTFS, ce qui n’est pas le choix que j’aurais fait pour un serveur Linux, mais sur un disque externe pour des vidéos de caméra de surveillance cela permettra de le débrancher et de le lire facilement n’importe où.
Le point à retenir : le dossier d’enregistrement doit être accessible en écriture par l’utilisateur qui lance ffmpeg — sur NTFS, ça se règle au montage (uid, gid, dmask, fmask).
# /etc/fstab — UUID factice UUID=XXXX-XXXX /srv/storage ntfs-3g defaults,nofail,noatime,uid=1000,gid=1000,dmask=0022,fmask=0133 0 0
L’URL RTSP de la caméra
Toutes les caméras IP un peu sérieuses exposent un flux RTSP. Chez Reolink, le flux principal est rtsp://utilisateur:motdepasse@IP:554/h264Preview_01_main, et le sous-flux (basse résolution, pratique pour une vignette ou une détection de mouvement) s’appelle h264Preview_01_sub.
Avant d’écrire une ligne de script, on vérifie que le flux est là et on relève ses caractéristiques :
ffprobe -v error \ -show_entries stream=codec_name,width,height,avg_frame_rate,bit_rate \ -of default=noprint_wrappers=1 \ 'rtsp://camera:motdepasse@192.168.1.50:554/h264Preview_01_main'
Chez moi, ça donne du H.264 en 1920×1072, une piste audio AAC, et 1,2 Mbit/s au total. Ce dernier chiffre n’est pas décoratif : c’est lui qui décide de l’autonomie du disque. À 1,2 Mbit/s, une journée pèse environ 13 Go.
Le script d’enregistrement
Deux fichiers : les paramètres dans /etc/cctv.env (en 600, root), et le script dans /usr/local/sbin/cctv-record.
CAM_HOST='192.168.1.50' # à changer par votre IP CAM_USER='camera' CAM_PASS='…' # mot de passe de la caméra CAM_PATH='h264Preview_01_main' CCTV_DIR='/srv/storage/cctv' CCTV_SEGMENT='600' # durée d'un fichier, en secondes
Le script lit cette conf, construit l’URL RTSP et lance ffmpeg en exec — de cette façon systemd suit directement le processus ffmpeg, sans shell intermédiaire :
#!/usr/bin/env bash
set -euo pipefail
CONF="${CONF:-/etc/cctv.env}"
. "$CONF"
: "${CAM_HOST:?}" "${CAM_USER:?}" "${CAM_PASS:?}"
DIR="${CCTV_DIR:-/srv/storage/cctv}"
SEGMENT="${CCTV_SEGMENT:-600}"
PATH_RTSP="${CAM_PATH:-h264Preview_01_main}"
command -v ffmpeg >/dev/null 2>&1 || { echo "ffmpeg absent" >&2; exit 1; }
mkdir -p "$DIR"
URL="rtsp://${CAM_USER}:${CAM_PASS}@${CAM_HOST}:554/${PATH_RTSP}"
# ffmpeg recopie l'URL (donc les identifiants) dans certaines erreurs :
# on masque le mot de passe avant que quoi que ce soit n'atteigne le journal.
exec /usr/bin/ffmpeg -nostdin -hide_banner -loglevel error \
-rtsp_transport tcp \
-i "$URL" \
-map 0:v:0 -map 0:a? -c copy \
-f segment -segment_format matroska -segment_time "$SEGMENT" \
-segment_atclocktime 1 -reset_timestamps 1 -strftime 1 \
"$DIR/%Y-%m-%d_%H-%M-%S.mkv" \
2> >(sed -u -E 's#rtsp://[^@/[:space:]]*@#rtsp://***@#g' >&2)
Trois détails méritent qu’on s’y arrête.
-rtsp_transport tcp : par défaut, ffmpeg lit le RTSP en UDP. Sur un réseau domestique un peu chargé, les paquets perdus se voient tout de suite — image figée, blocs, artefacts — alors qu’en TCP on préfère un peu de latence à une image abîmée.
-segment_time 600 -segment_atclocktime 1 : les fichiers font dix minutes et commencent sur un multiple de dix minutes de l’horloge. C’est ce qui rend la recherche confortable : pour un événement de 14 h 32, on ouvre le fichier de 14 h 30, pas celui de 14 h 27. -reset_timestamps 1 fait repartir chaque fichier de zéro, ce qui évite les sauts de lecture dans VLC.
Le filtre sed : ffmpeg recopie parfois l’URL complète — donc le mot de passe — dans ses messages d’erreur. La sortie d’erreur passe donc par un sed qui remplace rtsp://…@ par rtsp://***@ avant d’atteindre le journal.
Les fichiers arrivent dans /srv/storage/cctv sous la forme 2026-10-22_18-30-00.mkv.
Le service systemd
Un enregistreur qui s’arrête au premier incident ne sert à rien, donc c’est systemd qui le tient :
[Unit] Description=Enregistrement RTSP de la caméra (segments MKV, sans réencodage) After=network-online.target time-sync.target Wants=network-online.target time-sync.target [Service] Type=simple ExecStart=/usr/local/sbin/cctv-record Restart=always RestartSec=30 Nice=5 IOSchedulingClass=best-effort IOSchedulingPriority=6 [Install] WantedBy=multi-user.target
Restart=always couvre le crash, mais RestartSec=30 n’est pas là par hasard. Quand ffmpeg meurt brutalement, la caméra garde un moment la session RTSP ouverte ; se reconnecter immédiatement produit des bad cseq et des horodatages incohérents. J’ai eu un fichier annoncé à 35 792 secondes pour 90 secondes réelles, supprimé aussitôt. Trente secondes de marge suffisent à ce que la caméra libère la session. Et Nice=5 avec l’ordonnancement best-effort : l’enregistrement ne doit jamais passer devant le reste du système, qui est quand même un simple cœur ARM.
La rotation
Le disque fait 1,8 To, autant dire qu’il se remplit lentement — mais il se remplit. Plutôt que de surveiller l’espace à la main, un second script supprime les fichiers les plus anciens dès que le dossier dépasse un seuil :
#!/usr/bin/env bash
set -euo pipefail
CONF="${CONF:-/etc/cctv.env}"
[ -r "$CONF" ] && . "$CONF"
DIR="${CCTV_DIR:-/srv/storage/cctv}"
LIMIT="${CCTV_LIMIT_BYTES:-900000000000}" # 900 Go décimaux
[ -d "$DIR" ] || exit 0
total="$(du -sb "$DIR" | awk '{print $1}')"
if [ "$total" -le "$LIMIT" ]; then
echo "occupation ${total} octets <= limite ${LIMIT} — rien à faire"
exit 0
fi
# le fichier en cours d'écriture est le plus récent : on l'exclut
newest="$(find "$DIR" -maxdepth 1 -type f -name '*.mkv' -printf '%T@ %p\n' \
| sort -n | tail -1 | cut -d' ' -f2-)"
deleted=0
while [ "$total" -gt "$LIMIT" ]; do
line="$(find "$DIR" -maxdepth 1 -type f -name '*.mkv' ! -path "$newest" \
-printf '%T@ %s %p\n' | sort -n | head -1)"
[ -n "$line" ] || break
size="$(printf '%s' "$line" | awk '{print $2}')"
path="$(printf '%s' "$line" | cut -d' ' -f3-)"
rm -f -- "$path"
total=$((total - size))
deleted=$((deleted + 1))
done
echo "rotation : ${deleted} fichier(s) supprimé(s), ${total} octets (limite ${LIMIT})"
Deux points importants. Le seuil est en octets décimaux (900000000000, soit 900 Go), parce que du -sb renvoie des octets et qu’on veut comparer des choses comparables. Et surtout, le fichier le plus récent est exclu de la suppression : c’est celui que ffmpeg est en train d’écrire, le supprimer ne libère rien et casse l’enregistrement en cours.
Un timer systemd l’appelle toutes les cinq minutes, et deux minutes après le démarrage :
[Unit] Description=Vérifie l'occupation des enregistrements caméra [Timer] OnBootSec=2min OnUnitActiveSec=5min AccuracySec=30s Persistent=false Unit=cctv-rotate.service [Install] WantedBy=timers.target
Ce que ça donne en vrai
Le flux est à 1,2 Mbit/s, soit 150 Ko/s. Le disque, lui, encaisse 8,9 Mo/s en écriture — mesuré à 256 Mio écrits en 30 secondes. Autrement dit, le flux utilise moins de 2 % de la capacité du disque : la limite n’est pas le stockage, c’est la caméra.
À ce débit, 900 Go représentent environ 70 jours d’enregistrement continu. La rotation commence donc à mordre au bout de deux mois et demi, et on garde en permanence les deux derniers mois.
Les pièges rencontrés
La session RTSP orpheline. C’est le piège du SIGKILL : ffmpeg est tué net, la caméra garde la session ouverte, et la reconnexion immédiate produit un fichier de durée aberrante. D’où le RestartSec=30, et l’arrêt propre par SIGTERM qui laisse ffmpeg finaliser le MKV. Si le problème revient sans kill, la piste est un watchdog qui ffprobe le dernier segment et redémarre le service quand la durée annoncée est incohérente.
Le mot de passe visible. ffmpeg reçoit l’URL complète, donc le mot de passe, sur sa ligne de commande : n’importe quel utilisateur local peut le lire avec ps. Sur une machine à un seul compte et sans exposition Internet, c’est un compromis acceptable ; ailleurs, il faut passer par un fichier de conf lu par ffmpeg ou par un proxy RTSP local. Dans tous les cas, les journaux doivent être filtrés — c’est le rôle du sed.
L’horloge. Pas d’horloge matérielle sur cette machine : au démarrage, l’heure est fausse jusqu’à la synchronisation NTP, et comme les noms de fichiers sont horodatés, les premiers segments peuvent porter une date de 1970 ou l’heure du dernier arrêt. D’où le time-sync.target dans les dépendances du service.
L’arrêt brutal. Suspend, halt et reboot ne sont pas fiables ici : on fait sync, puis on coupe l’alimentation. Le système de fichiers de la racine est en ext3, un fsck au boot n’est donc pas exclu.
NTFS. C’est le choix de la compatibilité, pas de la performance ni des droits POSIX. Un ext4 serait techniquement plus rapide et plus propre.
Vérifier que ça tourne
systemctl status cctv-record cctv-rotate.timer journalctl -u cctv-record -f ls -lt /srv/storage/cctv | head
Un fichier doit apparaître dans le dossier à chaque tranche de dix minutes, et grossir pendant ces dix minutes. Si un fichier est créé mais reste à quelques kilo-octets, c’est que le flux n’arrive pas : reprendre l’URL et les identifiants avec ffprobe.
La suite
Il reste une chose à faire : une sonde qui prévienne si l’enregistrement s’arrête. Parce qu’un enregistreur silencieux qui ne tourne plus, c’est exactement le genre de panne qu’on découvre le jour où on a besoin de la vidéo.