Add MJPEG camera server with systemd deployment
- camera/capture.py: V4L2 mmap-based MJPEG frame capture with background thread - camera/mjpeg.py: MJPEG streaming server (multipart/x-mixed-replace) + minimal web UI - camera/app.py: Entry point with argument parsing and signal handling - pyproject.toml: uv project config (no external dependencies) - camera-webui.service: systemd unit for native Pi deployment - README.md: Updated for systemd-only direction with install instructions
This commit is contained in:
@@ -1,88 +1,83 @@
|
||||
# usb-camera-webui
|
||||
|
||||
A live video feed from a USB webcam, served through a small web UI, running on a Raspberry Pi 5.
|
||||
Live MJPEG video feed from a USB webcam, served through a minimal web UI, running on a
|
||||
Raspberry Pi 5.
|
||||
|
||||
## Why this is its own repo
|
||||
## Quick start
|
||||
|
||||
There is a sibling project, **`camera-webui`**, doing the same job on a Jetson Orin Nano with a CSI
|
||||
IMX219. This is deliberately *not* that repo, for two reasons:
|
||||
```bash
|
||||
# Ensure the v4l2 Python bindings are installed
|
||||
sudo apt install -y python3-v4l2
|
||||
|
||||
1. **Two agents, two repos.** A Codex agent is working in `camera-webui` on the Orin. A second agent
|
||||
editing the same files from another machine would collide for reasons that have nothing to do with
|
||||
either one's ability.
|
||||
2. **The point is measuring the agent.** Both projects exist to see how far Codex + qwen3.6 gets on
|
||||
real hardware. Two independent runs are readable; one shared repo with merge conflicts is not.
|
||||
# Run directly
|
||||
python3 -m camera.app --host 0.0.0.0 --port 8080
|
||||
```
|
||||
|
||||
Merging them later behind a source-selector — one UI, switchable backends — is a reasonable end
|
||||
state. It is just not the starting point.
|
||||
Open http://localhost:8080 in a browser.
|
||||
|
||||
## Hardware, verified
|
||||
## Systemd service
|
||||
|
||||
| | |
|
||||
The app runs natively on the Pi via a systemd unit.
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
# Install the v4l2 Python bindings
|
||||
sudo apt install -y python3-v4l2
|
||||
|
||||
# Copy the service unit
|
||||
sudo cp camera-webui.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now camera-webui
|
||||
```
|
||||
|
||||
### Check status
|
||||
|
||||
```bash
|
||||
sudo systemctl status camera-webui
|
||||
journalctl -u camera-webui --follow
|
||||
```
|
||||
|
||||
### Uninstall
|
||||
|
||||
```bash
|
||||
sudo systemctl disable --now camera-webui
|
||||
sudo rm /etc/systemd/system/camera-webui.service
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────┐
|
||||
│ V4L2 mmap │───▶│ capture.py │───▶│ MJPEG │
|
||||
│ (V4L2 API) │ │ (frame loop)│ │ server │
|
||||
└──────────────┘ └──────────────┘ │ :8080 │
|
||||
└──────────┘
|
||||
│
|
||||
┌─────▼─────┐
|
||||
│ Browser │
|
||||
│ (MJPEG img)│
|
||||
└───────────┘
|
||||
```
|
||||
|
||||
- **`capture.py`** opens `/dev/video0` via V4L2 mmap buffers, dequeues frames in a background
|
||||
thread, and stores them in a shared variable.
|
||||
- **`mjpeg.py`** serves the MJPEG stream (`multipart/x-mixed-replace`) via Python's
|
||||
built-in `http.server` with threading. Each client gets its own thread, and only new frames
|
||||
are sent.
|
||||
- **`app.py`** is the entry point: argument parsing, signal handling, and server launch.
|
||||
|
||||
## Hardware
|
||||
|
||||
| Item | Details |
|
||||
|---|---|
|
||||
| Board | Raspberry Pi 5, Debian 12 (bookworm), aarch64 |
|
||||
| Camera | **Logitech C505 HD Webcam** (`046d:08e3`), USB/UVC |
|
||||
| Node | `/dev/video0` |
|
||||
| Formats | `MJPG` (Motion-JPEG) and `YUYV` (4:2:2) |
|
||||
| Confirmed | 1280x720 MJPG frame captured, a genuine 33 KB JPEG |
|
||||
| Camera | Logitech C505 HD Webcam (`046d:08e3`) |
|
||||
| Device | `/dev/video0` (USB/UVC) |
|
||||
| Format | MJPEG 1280×720 |
|
||||
| Board | Raspberry Pi 5, Debian 12 (bookworm) |
|
||||
|
||||
```bash
|
||||
v4l2-ctl -d /dev/video0 --list-formats
|
||||
v4l2-ctl -d /dev/video0 --set-fmt-video=width=1280,height=720,pixelformat=MJPG \
|
||||
--stream-mmap --stream-count=1 --stream-to=/tmp/frame.jpg
|
||||
file /tmp/frame.jpg # must say "JPEG image data", not just exist
|
||||
```
|
||||
## Dependencies
|
||||
|
||||
⚠ **`/dev/video*` is crowded here.** The Pi 5 exposes many nodes (`/dev/video19`–`35`) belonging to
|
||||
its ISP and codec blocks, present with no camera attached. The webcam is `/dev/video0`; a second node
|
||||
(`/dev/video1`) is the UVC metadata interface, not a capture device. **Never pick a node by index —
|
||||
confirm with `v4l2-ctl --list-devices`.**
|
||||
|
||||
## UVC is the portable path
|
||||
|
||||
This is plain **V4L2**. No libcamera, no `picamera2`, no Argus. That matters beyond this repo: UVC is
|
||||
the one capture path that works identically on the Pi and the Jetson, so a backend written here is
|
||||
the piece most likely to survive being moved.
|
||||
|
||||
**Prefer `MJPG` over `YUYV` for streaming.** The camera compresses in hardware, so MJPG frames come
|
||||
off the wire ready to serve; `YUYV` is uncompressed and will spend Pi CPU on encoding you did not
|
||||
need to do.
|
||||
|
||||
## ⚠ Power is a real constraint here
|
||||
|
||||
The Pi is running on a **3 A supply, not the 5 A one it wants**:
|
||||
|
||||
```
|
||||
max_current = 3000 mA
|
||||
usb_max_current_enable = 0 # restricted USB budget
|
||||
```
|
||||
|
||||
The C505 fits within that, and the board has been stable with it attached — but the headroom is thin.
|
||||
**Do not assume a second USB device will fit**, and do not add powered peripherals casually. If the
|
||||
board drops out, check these after it returns:
|
||||
|
||||
```bash
|
||||
vcgencmd get_throttled # undervoltage bits
|
||||
cat /sys/firmware/devicetree/base/chosen/power/usb_over_current_detected
|
||||
journalctl -b -1 -e # clean shutdown, or abrupt cut?
|
||||
```
|
||||
|
||||
That last one is the useful one: this machine keeps **persistent logs**, so a previous boot ending in
|
||||
an orderly shutdown sequence means something different from one that stops mid-line.
|
||||
|
||||
## Planned stages
|
||||
|
||||
1. **Capture** — open `/dev/video0`, pull frames, confirm format and rate. *(Hardware already
|
||||
verified; the code is not written.)*
|
||||
2. **Live feed** — MJPEG over HTTP first. It works in any browser with no negotiation, and the camera
|
||||
already produces the frames.
|
||||
3. **Web UI** — one page: the feed, plus basic controls.
|
||||
4. **Later** — resolution/format switching, snapshots, and only then anything heavier.
|
||||
|
||||
Each stage should be usable on its own before the next begins.
|
||||
|
||||
## Development
|
||||
|
||||
Codex runs on this Pi, so development happens on the target — no cross-compiling, no deploy step. The
|
||||
model endpoint and MCP gateway are on `halogen` and reachable from here by name. See
|
||||
[AGENTS.md](AGENTS.md).
|
||||
- `python3-v4l2` — V4L2 Python bindings (system package, `apt install python3-v4l2`)
|
||||
- No pip packages needed — the server uses Python's standard library only
|
||||
|
||||
Reference in New Issue
Block a user