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:
@@ -0,0 +1,81 @@
|
|||||||
|
# PiCam Network Streamer
|
||||||
|
|
||||||
|
A lightweight MJPEG streaming server for Raspberry Pi. It serves a webpage you can open on any device on your local network to view the live camera feed.
|
||||||
|
|
||||||
|
## Pi 5 OS Image Setup
|
||||||
|
|
||||||
|
For a fully automated first-boot setup on a **Raspberry Pi 5** with an **IMX219** camera, see the [`image-setup/`](image-setup/) directory. It includes:
|
||||||
|
- `SETUP_GUIDE.md` — Step-by-step SD card imaging instructions.
|
||||||
|
- `autosetup.sh` — One-command setup script (installs drivers, dependencies, and auto-starts the streamer as a systemd service).
|
||||||
|
- `config_camera.txt` — `config.txt` snippet to force-enable the IMX219 overlay.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. **Install dependencies**
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Run the streamer**
|
||||||
|
|
||||||
|
*Auto-detect (recommended):*
|
||||||
|
```bash
|
||||||
|
python stream.py
|
||||||
|
```
|
||||||
|
|
||||||
|
*For the official Raspberry Pi Camera Module (CSI):*
|
||||||
|
```bash
|
||||||
|
python stream_picamera2.py
|
||||||
|
```
|
||||||
|
|
||||||
|
*For a USB webcam:*
|
||||||
|
```bash
|
||||||
|
python stream_usb.py
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **View the stream**
|
||||||
|
Open a browser on any device on the same network and go to:
|
||||||
|
```
|
||||||
|
http://<raspberry-pi-ip>:5000
|
||||||
|
```
|
||||||
|
Replace `<raspberry-pi-ip>` with your Pi's IP address.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
Both scripts accept the same command-line arguments:
|
||||||
|
|
||||||
|
| Argument | Description | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `--host` | Network interface to bind to | `0.0.0.0` |
|
||||||
|
| `--port` | Port to serve on | `5000` |
|
||||||
|
| `--width` | Video width in pixels | `640` |
|
||||||
|
| `--height` | Video height in pixels | `480` |
|
||||||
|
| `--fps` | Target framerate | `30` |
|
||||||
|
|
||||||
|
*USB streamer only:*
|
||||||
|
| `--device` | Video device index (e.g., `0` for `/dev/video0`) | `0` |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
Run at 720p on port 8080:
|
||||||
|
```bash
|
||||||
|
python stream_picamera2.py --width 1280 --height 720 --port 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
Use a second USB camera:
|
||||||
|
```bash
|
||||||
|
python stream_usb.py --device 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `stream_picamera2.py` — Optimized streamer for Raspberry Pi Camera Module v2/v3 using `picamera2`.
|
||||||
|
- `stream_usb.py` — Generic streamer for USB cameras using OpenCV.
|
||||||
|
- `stream.py` — Auto-detects camera type and launches the correct streamer.
|
||||||
|
- `requirements.txt` — Python dependencies.
|
||||||
|
- `image-setup/` — Scripts and guides for preparing a headless Pi 5 OS image with IMX219 support.
|
||||||
|
- `SETUP_GUIDE.md` — Full imaging and setup instructions.
|
||||||
|
- `setup-pi.sh` — Run this on a live Pi to install everything and auto-start the streamer.
|
||||||
|
- `autosetup.sh` — For embedding into a custom SD card image (runs on first boot).
|
||||||
|
- `firstrun.sh` — Raspberry Pi first-boot trigger script.
|
||||||
|
- `config_camera.txt` — Snippet to enable the IMX219 overlay.
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# Pi 5 + IMX219 OS Image Setup Guide
|
||||||
|
|
||||||
|
This guide walks you through setting up a **Raspberry Pi 5** with an **IMX219** camera module (Camera Module v2) to run the PiCam streamer.
|
||||||
|
|
||||||
|
## Option A: Easy Method — Run Setup on a Live Pi (Recommended)
|
||||||
|
|
||||||
|
If you already have Raspberry Pi OS running on your Pi 5:
|
||||||
|
|
||||||
|
1. **Copy the project** to your Pi (via USB drive, `scp`, or `git clone`).
|
||||||
|
2. **Run the setup script:**
|
||||||
|
```bash
|
||||||
|
cd piCam/image-setup
|
||||||
|
chmod +x setup-pi.sh
|
||||||
|
./setup-pi.sh
|
||||||
|
```
|
||||||
|
3. **Done.** The streamer will start automatically and will auto-start on every boot.
|
||||||
|
|
||||||
|
Open `http://<pi-ip>:5000` in your browser.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Option B: Prepare an SD Card Image from Scratch
|
||||||
|
|
||||||
|
### 1. Download Raspberry Pi OS
|
||||||
|
|
||||||
|
Download the latest **Raspberry Pi OS (64-bit)** from the official site:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://www.raspberrypi.com/software/operating-systems/
|
||||||
|
```
|
||||||
|
|
||||||
|
Recommended: **Raspberry Pi OS with Desktop** or **Lite** if you want a headless server.
|
||||||
|
|
||||||
|
### 2. Flash the OS to SD Card
|
||||||
|
|
||||||
|
Use the official **Raspberry Pi Imager**:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://www.raspberrypi.com/software/
|
||||||
|
```
|
||||||
|
|
||||||
|
Steps in Imager:
|
||||||
|
1. Choose OS → Raspberry Pi OS (64-bit)
|
||||||
|
2. Choose Storage → Your SD card
|
||||||
|
3. Click the **gear icon (⚙️)** to open Advanced Options and configure:
|
||||||
|
- **Hostname**: `picam` (or your preference)
|
||||||
|
- **Enable SSH**: ☑️ (Use password authentication or public key)
|
||||||
|
- **Set username/password**: `pi` / your-password
|
||||||
|
- **Configure Wi-Fi**: Enter your SSID and password
|
||||||
|
- **Set locale**: Your timezone and keyboard layout
|
||||||
|
4. Click **Write**
|
||||||
|
|
||||||
|
### 3. Enable Camera Hardware
|
||||||
|
|
||||||
|
After flashing, the SD card will show a `bootfs` partition. You need to ensure camera support is enabled.
|
||||||
|
|
||||||
|
#### For Raspberry Pi OS Bookworm and later:
|
||||||
|
|
||||||
|
The IMX219 is usually auto-detected. However, if you need to force it, open `bootfs/config.txt` and add to the end:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# IMX219 Camera Module v2
|
||||||
|
dtoverlay=imx219
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** On Pi 5, the camera uses the CSI/DSI connector. Make sure the flat cable is oriented correctly (blue tape facing away from the USB ports on Pi 5).
|
||||||
|
|
||||||
|
### 4. Copy the Project Files
|
||||||
|
|
||||||
|
Copy the contents of this `image-setup` folder to the SD card:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux/macOS example (adjust paths as needed)
|
||||||
|
cp -r /path/to/piCam/image-setup/* /media/YOUR_USERNAME/bootfs/
|
||||||
|
```
|
||||||
|
|
||||||
|
Specifically, ensure these files are placed on the `bootfs` partition:
|
||||||
|
- `autosetup.sh` → root of `bootfs/`
|
||||||
|
- `firstrun.sh` → root of `bootfs/`
|
||||||
|
|
||||||
|
### 5. Boot the Pi
|
||||||
|
|
||||||
|
Insert the SD card into the Pi 5, connect power, and wait 2-3 minutes for the first-boot setup to complete.
|
||||||
|
|
||||||
|
### 6. Access the Stream
|
||||||
|
|
||||||
|
Once booted, find the Pi on your network:
|
||||||
|
```bash
|
||||||
|
ping picam.local
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open in a browser:
|
||||||
|
```
|
||||||
|
http://picam.local:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Issue | Solution |
|
||||||
|
|-------|----------|
|
||||||
|
| Camera not detected | Run `libcamera-hello --list-cameras` to verify hardware detection |
|
||||||
|
| Green tint on image | Normal under some lighting; adjust AWB in picamera2 config |
|
||||||
|
| Low framerate | Reduce resolution or ensure GPU memory isn't too low |
|
||||||
|
| `picamera2` not found | Run `sudo apt install -y python3-picamera2` |
|
||||||
|
|
||||||
|
## Automated First-Boot Setup
|
||||||
|
|
||||||
|
If you copied the provided files to `bootfs`, the system will:
|
||||||
|
1. Expand the filesystem
|
||||||
|
2. Update all packages
|
||||||
|
3. Install camera drivers, Python dependencies, and Flask
|
||||||
|
4. Enable the camera interface
|
||||||
|
5. Auto-start the streaming server on boot
|
||||||
|
|
||||||
|
Monitor progress by SSHing in:
|
||||||
|
```bash
|
||||||
|
ssh pi@picam.local
|
||||||
|
tail -f /var/log/picam-setup.log
|
||||||
|
```
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# PiCam First-Boot Setup Script
|
||||||
|
# Run this on the Raspberry Pi after first boot to configure the IMX219 camera
|
||||||
|
# and install the streaming server.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
LOGFILE="/var/log/picam-setup.log"
|
||||||
|
PROJECT_DIR="/home/pi/piCam"
|
||||||
|
REPO_URL="" # Leave empty if copying files manually to the SD card
|
||||||
|
|
||||||
|
exec > >(tee -a "$LOGFILE")
|
||||||
|
exec 2>&1
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo "PiCam First-Boot Setup Starting"
|
||||||
|
echo "Date: $(date)"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# Update system
|
||||||
|
echo "[1/6] Updating package lists..."
|
||||||
|
sudo apt-get update
|
||||||
|
|
||||||
|
echo "[2/6] Installing system dependencies..."
|
||||||
|
sudo apt-get install -y \
|
||||||
|
python3-pip \
|
||||||
|
python3-venv \
|
||||||
|
python3-picamera2 \
|
||||||
|
libcamera-dev \
|
||||||
|
libcap-dev \
|
||||||
|
git \
|
||||||
|
i2c-tools \
|
||||||
|
v4l-utils
|
||||||
|
|
||||||
|
# Ensure camera interface is enabled (legacy method, though Bookworm uses libcamera)
|
||||||
|
echo "[3/6] Ensuring camera interface is enabled..."
|
||||||
|
if command -v raspi-config &> /dev/null; then
|
||||||
|
sudo raspi-config nonint do_camera 0 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create project directory
|
||||||
|
echo "[4/6] Setting up project directory..."
|
||||||
|
mkdir -p "$PROJECT_DIR"
|
||||||
|
|
||||||
|
# If files are already present (e.g., copied to SD card), use them.
|
||||||
|
# Otherwise, clone or create a placeholder.
|
||||||
|
if [ ! -f "$PROJECT_DIR/stream_picamera2.py" ]; then
|
||||||
|
echo "Stream scripts not found in $PROJECT_DIR."
|
||||||
|
echo "Please copy the project files manually:"
|
||||||
|
echo " scp -r /path/to/piCam/* pi@picam.local:/home/pi/piCam/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Setup Python virtual environment
|
||||||
|
echo "[5/6] Creating Python virtual environment..."
|
||||||
|
python3 -m venv "$PROJECT_DIR/venv"
|
||||||
|
source "$PROJECT_DIR/venv/bin/activate"
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
echo "[6/6] Installing Python packages..."
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install Flask opencv-python-headless
|
||||||
|
|
||||||
|
# Install systemd service for auto-start
|
||||||
|
echo "Installing systemd service..."
|
||||||
|
cat <<EOF | sudo tee /etc/systemd/system/picam-stream.service
|
||||||
|
[Unit]
|
||||||
|
Description=PiCam MJPEG Streamer
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=pi
|
||||||
|
WorkingDirectory=$PROJECT_DIR
|
||||||
|
Environment="PATH=$PROJECT_DIR/venv/bin"
|
||||||
|
ExecStart=$PROJECT_DIR/venv/bin/python $PROJECT_DIR/stream_picamera2.py --host 0.0.0.0 --port 5000
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable picam-stream.service
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo "Setup Complete!"
|
||||||
|
echo "========================================"
|
||||||
|
echo "The streaming server will start automatically on boot."
|
||||||
|
echo "To start it now, run: sudo systemctl start picam-stream"
|
||||||
|
echo "To check status: sudo systemctl status picam-stream"
|
||||||
|
echo "To view logs: sudo journalctl -u picam-stream -f"
|
||||||
|
echo ""
|
||||||
|
echo "Open in your browser: http://$(hostname -I | awk '{print $1}'):5000"
|
||||||
|
|
||||||
|
# Mark setup as complete so it doesn't run again
|
||||||
|
touch /home/pi/.picam-setup-done
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Add these lines to the end of your /boot/firmware/config.txt (or /boot/config.txt)
|
||||||
|
# to explicitly enable the IMX219 camera module on Raspberry Pi 5.
|
||||||
|
|
||||||
|
# IMX219 Camera Module v2
|
||||||
|
dtoverlay=imx219
|
||||||
|
|
||||||
|
# Ensure camera is enabled (legacy, usually not needed on Bookworm but safe)
|
||||||
|
camera_auto_detect=1
|
||||||
|
|
||||||
|
# Optional: Increase GPU memory if needed for higher resolutions
|
||||||
|
# gpu_mem=128
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Raspberry Pi First-Boot Script
|
||||||
|
# Place this file on the bootfs partition as 'firstrun.sh' to execute on first boot.
|
||||||
|
# It will trigger the autosetup.sh script located in /boot/autosetup.sh
|
||||||
|
|
||||||
|
BOOT_SCRIPT="/boot/firmware/autosetup.sh"
|
||||||
|
|
||||||
|
# For older Raspberry Pi OS versions, the boot partition is mounted at /boot
|
||||||
|
if [ ! -f "$BOOT_SCRIPT" ]; then
|
||||||
|
BOOT_SCRIPT="/boot/autosetup.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$BOOT_SCRIPT" ]; then
|
||||||
|
echo "Found autosetup script at $BOOT_SCRIPT"
|
||||||
|
chmod +x "$BOOT_SCRIPT"
|
||||||
|
# Run as the default user (usually pi)
|
||||||
|
sudo -u pi bash "$BOOT_SCRIPT"
|
||||||
|
else
|
||||||
|
echo "No autosetup.sh found on boot partition."
|
||||||
|
fi
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# PiCam Live Setup — Run this directly on your Raspberry Pi 5 after first boot.
|
||||||
|
# This automates installation of dependencies and enables auto-start streaming.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_DIR="$HOME/piCam"
|
||||||
|
LOGFILE="/var/log/picam-setup.log"
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$LOGFILE")"
|
||||||
|
exec > >(tee -a "$LOGFILE")
|
||||||
|
exec 2>&1
|
||||||
|
|
||||||
|
echo "============================================"
|
||||||
|
echo " PiCam Setup Script"
|
||||||
|
echo " Running on: $(hostname)"
|
||||||
|
echo " Date: $(date)"
|
||||||
|
echo "============================================"
|
||||||
|
|
||||||
|
# 1. System update
|
||||||
|
echo "[1/7] Updating package lists..."
|
||||||
|
sudo apt-get update
|
||||||
|
|
||||||
|
# 2. Install system packages
|
||||||
|
echo "[2/7] Installing system packages..."
|
||||||
|
sudo apt-get install -y \
|
||||||
|
python3-pip \
|
||||||
|
python3-venv \
|
||||||
|
python3-picamera2 \
|
||||||
|
libcamera-dev \
|
||||||
|
libcap-dev \
|
||||||
|
git \
|
||||||
|
i2c-tools \
|
||||||
|
v4l-utils
|
||||||
|
|
||||||
|
# 3. Detect camera
|
||||||
|
echo "[3/7] Detecting camera..."
|
||||||
|
if command -v libcamera-hello >/dev/null 2>&1; then
|
||||||
|
echo "libcamera devices:"
|
||||||
|
libcamera-hello --list-cameras || true
|
||||||
|
else
|
||||||
|
echo "libcamera-hello not found; relying on picamera2 detection."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Copy project files
|
||||||
|
echo "[4/7] Installing project files..."
|
||||||
|
mkdir -p "$PROJECT_DIR"
|
||||||
|
|
||||||
|
if [ -d "$SCRIPT_DIR/../" ] && [ -f "$SCRIPT_DIR/../stream_picamera2.py" ]; then
|
||||||
|
# Running from cloned repo
|
||||||
|
cp "$SCRIPT_DIR/../stream_picamera2.py" "$PROJECT_DIR/"
|
||||||
|
cp "$SCRIPT_DIR/../stream_usb.py" "$PROJECT_DIR/"
|
||||||
|
cp "$SCRIPT_DIR/../stream.py" "$PROJECT_DIR/"
|
||||||
|
cp "$SCRIPT_DIR/../requirements.txt" "$PROJECT_DIR/"
|
||||||
|
else
|
||||||
|
echo "WARNING: Could not find project Python files."
|
||||||
|
echo "Please copy them manually to $PROJECT_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. Python environment
|
||||||
|
echo "[5/7] Creating Python virtual environment..."
|
||||||
|
python3 -m venv "$PROJECT_DIR/venv"
|
||||||
|
source "$PROJECT_DIR/venv/bin/activate"
|
||||||
|
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install Flask opencv-python-headless
|
||||||
|
|
||||||
|
# 6. Create systemd service
|
||||||
|
echo "[6/7] Creating systemd service..."
|
||||||
|
cat <<EOF | sudo tee /etc/systemd/system/picam-stream.service
|
||||||
|
[Unit]
|
||||||
|
Description=PiCam MJPEG Streamer
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=$USER
|
||||||
|
WorkingDirectory=$PROJECT_DIR
|
||||||
|
Environment="PATH=$PROJECT_DIR/venv/bin"
|
||||||
|
ExecStart=$PROJECT_DIR/venv/bin/python $PROJECT_DIR/stream_picamera2.py --host 0.0.0.0 --port 5000
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable picam-stream.service
|
||||||
|
|
||||||
|
# 7. Start service
|
||||||
|
echo "[7/7] Starting streamer..."
|
||||||
|
sudo systemctl start picam-stream.service
|
||||||
|
|
||||||
|
echo "============================================"
|
||||||
|
echo " Setup Complete!"
|
||||||
|
echo "============================================"
|
||||||
|
echo ""
|
||||||
|
echo "Streamer is running as a systemd service."
|
||||||
|
echo ""
|
||||||
|
echo "Commands:"
|
||||||
|
echo " Status: sudo systemctl status picam-stream"
|
||||||
|
echo " Logs: sudo journalctl -u picam-stream -f"
|
||||||
|
echo " Stop: sudo systemctl stop picam-stream"
|
||||||
|
echo " Start: sudo systemctl start picam-stream"
|
||||||
|
echo ""
|
||||||
|
IP=$(hostname -I | awk '{print $1}')
|
||||||
|
echo "Open in browser: http://$IP:5000"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Flask>=2.0.0
|
||||||
|
opencv-python-headless>=4.5.0
|
||||||
|
picamera2>=0.3.9; platform_machine=='aarch64' or platform_machine=='armv7l'
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Auto-detect and start the best available streamer."""
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Check if picamera2 is available and a Pi camera is connected
|
||||||
|
if importlib.util.find_spec("picamera2") is not None:
|
||||||
|
try:
|
||||||
|
from picamera2 import Picamera2
|
||||||
|
|
||||||
|
# Attempt to create a camera to see if hardware is present
|
||||||
|
cam = Picamera2()
|
||||||
|
cam.close()
|
||||||
|
print("Detected Raspberry Pi camera. Starting picamera2 streamer...")
|
||||||
|
subprocess.run([sys.executable, "stream_picamera2.py"] + sys.argv[1:])
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
print(f"picamera2 available but camera not detected ({e}).")
|
||||||
|
else:
|
||||||
|
print("picamera2 not installed.")
|
||||||
|
|
||||||
|
# Fall back to USB / OpenCV
|
||||||
|
print("Falling back to USB camera streamer...")
|
||||||
|
subprocess.run([sys.executable, "stream_usb.py"] + sys.argv[1:])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
from flask import Flask, Response, render_template_string
|
||||||
|
from picamera2 import Picamera2
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
|
||||||
|
class Camera:
|
||||||
|
def __init__(self, resolution=(640, 480), framerate=30):
|
||||||
|
self.resolution = resolution
|
||||||
|
self.framerate = framerate
|
||||||
|
self.picam2 = None
|
||||||
|
self.frame = None
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self.running = False
|
||||||
|
self.thread = None
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.picam2 = Picamera2()
|
||||||
|
config = self.picam2.create_video_configuration(
|
||||||
|
main={"size": self.resolution, "format": "RGB888"},
|
||||||
|
controls={"FrameRate": self.framerate}
|
||||||
|
)
|
||||||
|
self.picam2.configure(config)
|
||||||
|
self.picam2.start()
|
||||||
|
# Allow camera to warm up
|
||||||
|
time.sleep(2)
|
||||||
|
self.running = True
|
||||||
|
self.thread = threading.Thread(target=self._capture, daemon=True)
|
||||||
|
self.thread.start()
|
||||||
|
logging.info("Camera started.")
|
||||||
|
|
||||||
|
def _capture(self):
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
arr = self.picam2.capture_array()
|
||||||
|
# Encode as JPEG
|
||||||
|
import cv2
|
||||||
|
ret, buf = cv2.imencode('.jpg', arr)
|
||||||
|
if ret:
|
||||||
|
with self.lock:
|
||||||
|
self.frame = buf.tobytes()
|
||||||
|
time.sleep(0.001) # Brief sleep to yield
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Capture error: {e}")
|
||||||
|
time.sleep(0.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.picam2:
|
||||||
|
self.picam2.stop()
|
||||||
|
self.picam2.close()
|
||||||
|
logging.info("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\n'
|
||||||
|
b'Content-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 picamera2')
|
||||||
|
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('--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 = Camera(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()
|
||||||
+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