In this post, we’ll build a small “CCTV” system using:
- Raspberry Pi + Camera
- Python
- Flask (for HTTP streaming)
- Picamera2 + OpenCV
- systemd (to run on boot)
- Basic Auth (for simple password protection)
- MP4 recording to disk
You’ll end up with:
- A live IP camera stream you can open in any browser
- Password-protected access
- Automatic video recording to
.mp4files - The service starting automatically on boot
1. Install required packages
On your Raspberry Pi:
sudo apt update
sudo apt install -y \
python3-pip \
python3-flask \
python3-picamera2 \
python3-opencv
Make sure the camera works at OS level first:
rpicam-still -o test.jpg
If that saves an image, your camera stack is fine.
2. Create the streaming + recording server (Python)
Create a file, e.g. stream_server.py:
from flask import Flask, Response, request
from functools import wraps
from picamera2 import Picamera2
import cv2
import os
import time
import threading
from datetime import datetime
app = Flask(__name__)
# ========= PASSWORD PROTECTION =========
USERNAME = "piuser" # change this
PASSWORD = "mypassword" # change this
def check_auth(username, password):
return username == USERNAME and password == PASSWORD
def authenticate():
return Response(
"Login required\n",
401,
{"WWW-Authenticate": 'Basic realm="Login Required"'}
)
def requires_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
return authenticate()
return f(*args, **kwargs)
return decorated
# ========= CAMERA + RECORDING SETUP =========
picam2 = Picamera2()
video_config = picam2.create_video_configuration(
main={"size": (640, 480), "format": "BGR888"} # BGR is natural for OpenCV
)
picam2.configure(video_config)
picam2.start()
os.makedirs("recordings", exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
video_path = f"recordings/record_{timestamp}.mp4"
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
fps = 10.0
frame_size = (640, 480)
video_writer = cv2.VideoWriter(video_path, fourcc, fps, frame_size)
last_jpeg = None
frame_lock = threading.Lock()
running = True
def capture_loop():
"""Single loop that talks to the camera and writes recording + JPEG."""
global last_jpeg, running
while running:
frame = picam2.capture_array() # BGR888
# Write to recording
video_writer.write(frame)
# Encode JPEG for streaming
ret, buffer = cv2.imencode(".jpg", frame)
if ret:
jpg_bytes = buffer.tobytes()
with frame_lock:
last_jpeg = jpg_bytes
# small sleep to avoid hammering CPU
time.sleep(0.03) # ~30 fps max
# Start the background capture thread
capture_thread = threading.Thread(target=capture_loop, daemon=True)
capture_thread.start()
def generate_frames():
"""Yield the latest JPEG over MJPEG."""
global last_jpeg
while True:
with frame_lock:
frame = last_jpeg
if frame is not None:
yield (
b"--frame\r\n"
b"Content-Type: image/jpeg\r\n\r\n" + frame + b"\r\n"
)
time.sleep(0.03)
# ========= ROUTES =========
@app.route("/video")
@requires_auth
def video():
return Response(
generate_frames(),
mimetype="multipart/x-mixed-replace; boundary=frame"
)
@app.route("/")
@requires_auth
def index():
return (
"<h1>Raspberry Pi Camera Stream</h1>"
"<p>Go to <a href=\"/video\">/video</a> to see the live stream.</p>"
)
def cleanup():
"""Release camera and writer cleanly."""
global running
running = False
time.sleep(0.1)
video_writer.release()
picam2.stop()
if __name__ == "__main__":
try:
# threaded=False so Flask doesn’t spawn multiple request threads
app.run(host="0.0.0.0", port=5000, debug=False, threaded=False)
finally:
cleanup()
Run it manually to test:
python3 stream_server.py
From another device on the same network:
- Find Pi IP:
hostname -I - Open in browser:
http://<pi-ip>:5000/→ info page (will ask for username/password)http://<pi-ip>:5000/video→ live MJPEG stream (also password-protected)
Recordings will be stored in:
~/recordings/record_YYYYMMDD_HHMMSS.mp4
3. Run it automatically on boot (systemd)
Put your script somewhere stable, e.g.:
/home/pi/stream_server.py
Create a systemd service:
sudo nano /etc/systemd/system/pi_cam_stream.service
Add:
[Unit]
Description=Raspberry Pi Camera Stream
After=network.target
[Service]
ExecStart=/usr/bin/python3 /home/pi/stream_server.py
WorkingDirectory=/home/pi
User=pi
Restart=always
[Install]
WantedBy=multi-user.target
Enable & start:
sudo systemctl daemon-reload
sudo systemctl enable pi_cam_stream.service
sudo systemctl start pi_cam_stream.service
Check status:
sudo systemctl status pi_cam_stream.service
Now your stream + recording:
- starts automatically at boot
- restarts if it crashes
- is available at
http://<pi-ip>:5000/on your LAN
4. Notes & possible improvements
- Security
- Use strong username/password.
- For internet access, prefer VPN (WireGuard, Tailscale) or SSH tunneling instead of directly exposing port 5000.
- Storage
- Add rotation (new file every X minutes/hours).
- Add cleanup logic to delete oldest files when disk is nearly full.
- Performance
- Lower resolution (e.g. 480×360) for older boards like Raspberry Pi 3.
- Tweak
fps(e.g. 5–10 fps is usually enough for CCTV-style use).