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:
+161
@@ -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()
|
||||
Reference in New Issue
Block a user