Add PiCam network streamer with auto-setup for Pi 5

Initial commit of the MJPEG streaming server for Raspberry Pi.
Supports Pi Camera Module (via picamera2), USB webcams (via OpenCV),
and auto-detection of camera type.

Includes:
- `stream_picamera2.py` — optimized Pi CSI camera streamer
- `stream_usb.py` — generic USB camera streamer
- `stream.py` — auto-detects camera and launches correct streamer
- `image-setup/` — SD card imaging scripts and Pi 5 headless setup
  guide for first-boot automation with systemd service
- `requirements.txt` — Python dependencies
This commit is contained in:
2026-05-23 10:39:31 +02:00
commit 6faf505f4b
10 changed files with 778 additions and 0 deletions
+161
View File
@@ -0,0 +1,161 @@
import logging
import threading
import time
import cv2
from flask import Flask, Response, render_template_string
app = Flask(__name__)
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
class UsbCamera:
def __init__(self, device_index=0, resolution=(640, 480), framerate=30):
self.device_index = device_index
self.resolution = resolution
self.framerate = framerate
self.cap = None
self.frame = None
self.lock = threading.Lock()
self.running = False
self.thread = None
def start(self):
self.cap = cv2.VideoCapture(self.device_index)
if not self.cap.isOpened():
raise RuntimeError(f"Cannot open camera device {self.device_index}")
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.resolution[0])
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.resolution[1])
self.cap.set(cv2.CAP_PROP_FPS, self.framerate)
# Give the driver a moment to apply settings
time.sleep(0.5)
self.running = True
self.thread = threading.Thread(target=self._capture, daemon=True)
self.thread.start()
logging.info("USB camera started.")
def _capture(self):
while self.running:
ret, img = self.cap.read()
if not ret:
logging.warning("Failed to read frame from camera.")
time.sleep(0.05)
continue
ret2, buf = cv2.imencode(".jpg", img)
if ret2:
with self.lock:
self.frame = buf.tobytes()
# Throttle slightly to avoid maxing CPU
time.sleep(1.0 / max(self.framerate, 1))
def get_frame(self):
with self.lock:
return self.frame
def stop(self):
self.running = False
if self.thread:
self.thread.join(timeout=2.0)
if self.cap:
self.cap.release()
logging.info("USB camera stopped.")
camera = None
HTML_PAGE = """
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PiCam Stream</title>
<style>
body {
background: #111;
color: #eee;
font-family: system-ui, -apple-system, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
}
h1 { margin-bottom: 0.5rem; }
p { color: #888; margin-bottom: 1rem; }
img {
max-width: 95vw;
max-height: 80vh;
border: 2px solid #333;
border-radius: 8px;
background: #000;
}
</style>
</head>
<body>
<h1>PiCam Live Stream</h1>
<p>{{ resolution }} @ {{ framerate }} fps</p>
<img src="{{ url_for('video_feed') }}" alt="Live stream">
</body>
</html>
"""
@app.route("/")
def index():
return render_template_string(
HTML_PAGE, resolution=camera.resolution, framerate=camera.framerate
)
@app.route("/video_feed")
def video_feed():
def generate():
while True:
frame = camera.get_frame()
if frame is None:
time.sleep(0.01)
continue
yield (b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + frame + b"\r\n")
return Response(generate(), mimetype="multipart/x-mixed-replace; boundary=frame")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description="PiCam MJPEG Streamer using OpenCV (USB camera)"
)
parser.add_argument(
"--host", default="0.0.0.0", help="Interface to bind (default: 0.0.0.0)"
)
parser.add_argument(
"--port", type=int, default=5000, help="Port to bind (default: 5000)"
)
parser.add_argument(
"--device", type=int, default=0, help="Video device index (default: 0)"
)
parser.add_argument("--width", type=int, default=640, help="Capture width")
parser.add_argument("--height", type=int, default=480, help="Capture height")
parser.add_argument("--fps", type=int, default=30, help="Target framerate")
args = parser.parse_args()
camera = UsbCamera(
device_index=args.device,
resolution=(args.width, args.height),
framerate=args.fps,
)
camera.start()
try:
logging.info(f"Starting server on http://{args.host}:{args.port}")
app.run(host=args.host, port=args.port, threaded=True, debug=False)
except KeyboardInterrupt:
pass
finally:
camera.stop()