Hermes 628307ff12 test: lock speechbrain lengths and identity camera filter
Filter --cameras 1,5 by cam-NN identity instead of discovery index.
2026-08-28 05:15:49 +00:00

livekit-cameras

Publishes 20 Logitech C920 webcam streams (video + cleaned audio) to a LiveKit room.

Each camera becomes one LiveKit participant (cam-01 .. cam-20) that publishes:

  • a camera video track (1280x720 @ 30 fps by default, MJPG from V4L2)
  • a microphone audio track (16 kHz mono, cleaned with speechbrain)

Layout

run.py              orchestrator: discovery + per-camera worker processes
run_worker.py       one camera: ffmpeg capture -> LiveKit track publish
discovery.py        serial-based camera <-> ALSA card discovery
audio_cleanup.py    speechbrain SpectralMaskEnhancement (16 kHz) + fallback
tokens.py           LiveKit JWT generation
config.py           .env / environment configuration
prefetch_model.py   one-time speechbrain model download
generate_udev_rules.py  (re)generate udev pinning + slots.json from live state
slots.json          stable slot map: cam-NN -> USB serial, /dev/camNN, ALSA card

Setup

uv venv --python 3.12
uv pip install -p .venv/bin/python -r pyproject.toml   # or: uv sync
cp .env.example .env    # then fill in LIVEKIT_URL / API_KEY / API_SECRET
.venv/bin/python prefetch_model.py     # download speechbrain model once

Stable camera identity (cam-01 is always cam-01)

Each C920 has a unique built-in USB serial — that is the camera's UUID. udev rules (/etc/udev/rules.d/99-c920-pin.rules, generated from the live state) bind every slot to a serial, so identity survives reboots, hub reordering, and any connect sequence:

  • video: /dev/cam01 .. /dev/cam20 (primary UVC node; .meta = metadata)
  • audio: fixed ALSA card per slot, hw:2,0 .. hw:21,0 (SOUND_CARD_INDEX)

discovery.py assigns identities from slots.json (serial lookup, with a port-based fallback for cameras not yet in the map). To re-map slots (e.g. after physically swapping cameras), regenerate:

.venv/bin/python generate_udev_rules.py --write
sudo udevadm control --reload-rules
sudo udevadm trigger --subsystem-match=video4linux --action=add
sudo udevadm trigger --subsystem-match=sound --action=add

Run

.venv/bin/python run.py                  # all 20 cameras
.venv/bin/python run.py --cameras 1,5    # subset
.venv/bin/python run.py --once           # status report after timeout

Audio cleanup

The mic audio is cleaned per chunk (default 200 ms) by speechbrain/metricgan-plus-voicebank (16 kHz, CPU). One worker process per camera loads the model once and reuses it; on this 28-core box all 20 stay real-time. ENHANCE_MODE=off (or a failed model load with auto) falls back to a lightweight high-pass + soft-clip DSP path so streams still work.

Notes

  • Video uses MJPG directly from the UVC interface (ffmpeg -f v4l2 -input_format mjpeg) — cheap, and converts to BGRA for LiveKit.
  • Audio: ALSA mics on these C920s deliver 16 kHz stereo only; the worker downmixes to mono for LiveKit.
  • Camera <-> card pairing is resolved by matching USB port paths under /sys, so each mic belongs to its own camera.
  • Workers auto-restart (up to 10 times) if a camera drops.
S
Description
C920 fleet publishers and KMS wall
Readme
42 MiB
Languages
Python 99.6%
Shell 0.4%