I wanted a home surveillance camera I could actually trust — no cloud subscriptions, no third-party servers, nothing phoning home. Just a Raspberry Pi, a camera module, and a web interface I built myself with Claude. The result is a live MJPEG stream, H.264 video recording, a privacy mode, and a scheduling system, all behind a login page. Zero external dependencies beyond the camera library itself.
// what it is
A password-protected web interface for a Raspberry Pi Camera. You open a browser, log in, and you get a live stream. From there you can start and stop recordings, toggle privacy mode to cut the feed, and configure a schedule so the camera only runs during the hours you want — say, overnight while you're asleep, or while you're at work.
The whole thing is a single Python file (camera_web_secure.py) running
on Python's built-in http.server. No Flask, no Django, no web framework
at all. It runs as a systemd service under a dedicated picam
user, so it starts automatically on boot and restarts itself if it crashes.
// how it works
The camera side uses picamera2 — the modern Python library for Raspberry Pi cameras. It runs two encoders simultaneously: a JPEG encoder for the live stream and an H.264 encoder for recordings. The live stream is served as MJPEG over a plain HTTP connection, which any browser can display natively without plugins.
Authentication is handled with a simple session system. Passwords are hashed with
SHA-256 and stored in a local config file. When you log in, the server creates a
session token using secrets.token_urlsafe(), drops it in a
HttpOnly cookie, and remembers it for one hour. Every protected endpoint
checks for a valid, non-expired session before responding.
The schedule system is the part I'm most happy with. You configure active days and a time window (which can span midnight — e.g. 20:00 to 08:00), and a background thread checks every minute whether the camera should be running. If you're outside the active window, the camera pauses itself. When the window opens again, it resumes. A manual privacy toggle overrides the schedule entirely until you turn it off.
Finally, there's an emergency cleanup script (cleanup_camera.sh) for
when the camera gets stuck — which happens more often than you'd think when you're
killing processes during development. It stops the systemd service, force-kills any
stray Python processes, and cleans up PID files.
// what I learned
Building an HTTP server from scratch without a framework is surprisingly educational. You realize how much Flask and friends are doing for you when you have to implement cookie parsing, session management, multipart streaming, and JSON APIs by hand. It's not hard — Python's stdlib handles most of it — but it's humbling.
The trickiest part was the MJPEG stream. The camera pushes frames to a shared output
object, and each streaming client reads from it in a loop. Getting the threading right
— multiple clients, frame synchronization, clean disconnects — took a few iterations.
The final version uses a threading.Condition to wake up all clients
as soon as a new frame arrives.
I also learned that running a camera as a systemd service requires a
bit of care. The picam user needs to be in the video group,
the working directory has to exist before the service starts, and you really want
Restart=on-failure with a short delay so a crash during startup doesn't
spin forever.
// result
It's running. The Pi sits in a corner, the service starts on boot, and I can pull
up the stream from anywhere on my local network. Recordings land in ~/Videos
and are downloadable directly from the web interface. The schedule keeps it quiet
during the day and active at night — exactly what I wanted.
The obvious next step is making it accessible from outside the local network. A Cloudflare Tunnel would work — I already use one for Foundry VTT. For now, the local-only setup is exactly what I need.
Je voulais une caméra de surveillance à la maison en laquelle je pouvais vraiment avoir confiance — pas d'abonnement infonuagique, pas de serveurs tiers, rien qui envoie des données quelque part. Juste un Raspberry Pi, un module caméra, et une interface web que j'ai construite moi-même avec Claude. Le résultat : un flux MJPEG en direct, l'enregistrement vidéo en H.264, un mode confidentialité, et un système de planification, le tout derrière une page de connexion. Zéro dépendance externe en dehors de la bibliothèque caméra elle-même.
// ce que c'est
Une interface web protégée par mot de passe pour une caméra Raspberry Pi. On ouvre un navigateur, on se connecte, et on obtient un flux en direct. De là, on peut démarrer et arrêter des enregistrements, activer le mode confidentialité pour couper le flux, et configurer une planification pour que la caméra ne tourne que pendant les heures souhaitées — la nuit pendant qu'on dort, ou pendant qu'on est au travail.
Le tout est un seul fichier Python (camera_web_secure.py) tournant sur le module
http.server intégré à Python. Pas de Flask, pas de Django, pas de framework web du tout.
Il tourne comme un service systemd sous un utilisateur dédié picam,
donc il démarre automatiquement au démarrage et se relance tout seul s'il plante.
// comment ça fonctionne
La partie caméra utilise picamera2 — la bibliothèque Python moderne pour les caméras Raspberry Pi. Elle fait tourner deux encodeurs simultanément : un encodeur JPEG pour le flux en direct et un encodeur H.264 pour les enregistrements. Le flux en direct est servi en MJPEG sur une connexion HTTP simple, que n'importe quel navigateur peut afficher nativement sans plugins.
L'authentification est gérée par un système de sessions simple. Les mots de passe sont hachés
avec SHA-256 et stockés dans un fichier de configuration local. À la connexion, le serveur crée
un jeton de session avec secrets.token_urlsafe(), le dépose dans un cookie
HttpOnly, et le garde en mémoire pendant une heure. Chaque point d'accès protégé
vérifie la validité de la session avant de répondre.
Le système de planification est la partie dont je suis le plus satisfait. On configure les jours actifs et une plage horaire (qui peut chevaucher minuit — ex. 20h00 à 08h00), et un fil d'exécution en arrière-plan vérifie chaque minute si la caméra devrait tourner. Si on est en dehors de la plage active, la caméra se met en pause toute seule. Quand la plage s'ouvre à nouveau, elle reprend. Le bouton de confidentialité manuel prend le dessus sur la planification jusqu'à ce qu'on le désactive.
Il y a aussi un script d'urgence (cleanup_camera.sh) pour quand la caméra se bloque —
ce qui arrive plus souvent qu'on ne le croit quand on tue des processus pendant le développement.
Il arrête le service systemd, force-kill les processus Python restants, et nettoie les fichiers PID.
// ce que j'ai appris
Construire un serveur HTTP de zéro sans framework, c'est étonnamment formateur. On réalise tout ce que Flask et ses semblables font pour nous quand on doit implémenter à la main l'analyse des cookies, la gestion des sessions, le streaming multipart et les API JSON. C'est pas difficile — la bibliothèque standard de Python gère l'essentiel — mais c'est humiliant.
La partie la plus délicate était le flux MJPEG. La caméra pousse des images vers un objet de sortie
partagé, et chaque client en streaming le lit en boucle. Bien gérer les fils d'exécution —
plusieurs clients, synchronisation des images, déconnexions propres — a demandé quelques itérations.
La version finale utilise un threading.Condition pour réveiller tous les clients
dès qu'une nouvelle image arrive.
J'ai aussi appris que faire tourner une caméra comme service systemd demande
un peu d'attention. L'utilisateur picam doit être dans le groupe video,
le répertoire de travail doit exister avant le démarrage du service, et on veut vraiment
Restart=on-failure avec un court délai pour qu'un plantage au démarrage
ne tourne pas en boucle indéfiniment.
// résultat
Ça tourne. Le Pi est dans un coin, le service démarre au démarrage, et je peux consulter
le flux depuis n'importe où sur mon réseau local. Les enregistrements atterrissent dans
~/Videos et sont téléchargeables directement depuis l'interface web.
La planification le garde silencieux le jour et actif la nuit — exactement ce que je voulais.
La prochaine étape évidente est de le rendre accessible depuis l'extérieur du réseau local. Un Cloudflare Tunnel ferait l'affaire — j'en utilise déjà un pour Foundry VTT. Pour l'instant, la configuration locale uniquement est exactement ce dont j'ai besoin.