Camera node

Slipstream

A Raspberry Pi 5 with a Logitech C270 webcam that turns a USB camera into a network camera. Anything on the network can pull the feed: OBS, a browser, or a script.

The whole thing in one picture

Video flows left to right. The camera is opened exactly once, by ffmpeg; everything else reads from MediaMTX.

Raspberry Pi 5 C270 webcam USB MJPEG 720p30 ffmpeg encodes with libx264 H.264 MediaMTX stream server OBS RTSP :8554/cam Browser WebRTC :8889/cam health.py status and stills is the stream ready? grab one frame Anything HTTP :8080 systemd starts both services at boot and restarts them if they exit

The hardware

One self-contained node. It runs headless: no monitor, keyboard or mouse in normal use, just power and Wi-Fi.

PartWhat
ComputerRaspberry Pi 5 (CanaKit kit)
CameraLogitech C270 USB webcam, 720p
StorageSanDisk Ultra Plus 256 GB microSDXC
OSRaspberry Pi OS, 64-bit
NetworkWi-Fi, with Tailscale for reaching it from anywhere
PowerUSB-C. The power cable is the only cable that leaves the node.
EnclosureGrey PVC electrical junction box with the Pi inside, a screw-down lid and vent holes drilled in the back. The camera clips to the top, wrapped in clear film, with a short USB cable down to the Pi.
The prototype: a webcam wrapped in clear film, clipped to the lid of a grey PVC junction box.
The prototype
Isometric line drawing of the same node with numbered parts: webcam, clip mount, lid, gasket, junction box, mounting tab, power cable, USB cable and rear vent holes.
The same node as a drawing

Nothing is exposed to the public internet. The node is reached over Tailscale or the local network only, and it asks for a password there too.

Use it

Once yours is built, these are the addresses. Replace YOUR-PI with your Pi's hostname and PASSWORD with the password install.sh prints.

WhatWhere
OBS Media Sourcertsp://slipstream:PASSWORD@YOUR-PI:8554/cam (uncheck "Local File")
Browser viewhttp://YOUR-PI:8889/cam
Control pagehttp://YOUR-PI:8080/
Healthhttp://YOUR-PI:8080/health
Still imagehttp://YOUR-PI:8080/still.jpg

Everything asks for a login: user slipstream, and a password that exists only on the Pi. install.sh creates it on first run and prints it; sudo cat /etc/slipstream/password on the Pi shows it again. To change it, delete that file and run install.sh again.

How each piece works

1. The camera hands over MJPEG

The C270 can't send uncompressed 720p at 30 fps over USB 2, so it compresses each frame as a JPEG inside the camera. That stream of JPEGs is MJPEG. It is easy to produce but large, and neither RTSP players nor browsers want it, so it has to be converted.

The camera is addressed by its /dev/v4l/by-id/… path rather than /dev/video0, so it is still found if the Pi numbers its devices differently after a reboot.

2. ffmpeg re-encodes to H.264

ffmpeg decodes the JPEGs and encodes H.264, the format OBS and browsers both play. The Pi 5 has no hardware H.264 encoder, so this runs on the CPU with libx264. The settings all trade picture efficiency for low delay and low CPU use:

SettingWhat it does
-video_size 1280x720 -framerate 30Asks the camera for its top mode.
-preset ultrafastThe cheapest encoding effort. Keeps the Pi's CPU comfortable at the cost of needing more bitrate for the same quality.
-tune zerolatencyThe encoder emits each frame as soon as it arrives instead of holding frames back to look ahead.
-g 30A full keyframe every 30 frames, which is once a second. A viewer who joins waits at most a second for a picture.
-b:v 3000k -maxrate 3000k -bufsize 1500kHolds the stream near 3 Mbit/s, with only half a second of slack, so the bitrate stays steady on the network.
-pix_fmt yuv420pThe colour format every player supports.

3. MediaMTX serves it

MediaMTX is a small stream server. It launches ffmpeg itself (runOnInit), receives the H.264 on the path cam, and re-serves the same stream in several protocols without re-encoding:

PortProtocolUsed by
8554RTSPOBS, VLC, ffmpeg, the health service's stills
8889WebRTCBrowsers, with low delay
8888HLSBrowsers, more delay but more forgiving (a MediaMTX default)
9997Control APIThe health service only; bound to localhost

If ffmpeg exits, for example because the camera is unplugged, MediaMTX starts it again (runOnInitRestart). Plug the camera back in and the stream returns on its own.

4. health.py answers "is it working?"

A small Python HTTP server on port 8080. It also serves the control page and runs the current mode, described below. Its two status endpoints:

EndpointReturns
/healthJSON: whether the stream is ready, whether the camera is plugged in, CPU temperature, uptime, current mode. Status 200 when the stream is up, 503 when it is not, so a monitor only needs the status code.
/still.jpgOne frame as a JPEG, or 503 if the stream is down.

The still is taken from the running stream, not from the camera. A V4L2 camera can only be opened by one program at a time, and ffmpeg already has it.

5. systemd keeps it alive

Two units, both enabled at boot and both set to restart two seconds after any exit: slipstream-mediamtx.service and slipstream-health.service.

Deploy

The Pi runs whatever is in this repository. From the Mac, in the repo folder:

rsync -a --exclude .git ./ slipstream:slipstream/ && ssh slipstream 'bash ~/slipstream/pi/install.sh'

install.sh installs ffmpeg and MediaMTX if they are missing, creates the password on first run, writes the MediaMTX config with that password to /etc/slipstream/, then installs and restarts the two services.

Modes

The camera stream always runs, so OBS and the browser view work in every mode. A mode decides what else runs on top of the stream. One mode is active at a time, and the choice survives a reboot.

ModeWhat it addsWhere the output goes
LiveNothing. Just the stream.OBS and browsers
WatchSaves a snapshot when something moves, at most one every 20 seconds.The SD card, shown on the control page
TimelapseSaves a still every 60 seconds.The SD card, shown on the control page

Switching

The control page, served by the Pi on port 8080, has a button for each mode, a live view, and the most recent snapshots. The same switch is available as a command on the Pi:

slipstream-mode watch

The stream and the control page ask for a password, which exists only on the Pi. Commands run on the Pi itself need no password.

How Watch decides something moved

Twice a second it takes a tiny 160×90 greyscale copy of the picture and compares it with the previous one. If more than 1% of the pixels changed noticeably, it saves the current full-size frame. The thresholds and intervals are constants at the top of pi/modes.py.

Adding a mode

A mode is one Python function in pi/modes.py that reads from the stream, plus one entry in the MODES table. The control page picks up its name and description automatically.