Webcams
Recore can run one or more USB webcams for monitoring prints. This page covers how to choose a camera, how many will work at once, and how to diagnose the common failures.
All figures on this page were measured on a Recore A7 running rebuild-fluidd-1adfb1e (kernel 6.18.33) in August 2026, with three USB cameras attached.
The short version
- Buy a camera that supports MJPEG in hardware. This matters more than resolution, brand or price. A camera without it can cost 20x the USB bandwidth and forces the CPU to compress every frame.
- Roughly three cameras at 640x480, or two at 1280x720. The limit is USB bandwidth, not the processor.
- If a camera will not start, check
dmesgforFailed to submit URB 0 (-28)before suspecting anything else.
One camera, or several
One camera works out of the box. Rebuild ships ustreamer and a udev rule that creates /dev/webcam for the first USB camera it finds. Plug a camera in and it is served on port 8080 as /webcam/, with nothing to configure. This is handled by ustreamer.service, which starts automatically when a camera appears.
Several cameras need crowsnest, which is not included in the image. The shipped ustreamer service is hard-wired to one device and one port:
ExecStart=/home/printer/ustreamer/ustreamer --device /dev/webcam --host=0.0.0.0 --port=8080 -m MJPEG
Crowsnest is a supervisor that launches one streamer process per camera, each on its own port, from a single configuration file. Moonraker already permits controlling it - it is listed in moonraker.asvc - so it can be installed without further changes to the image.
When installing crowsnest, disable the shipped ustreamer service first. Otherwise the two compete for the same camera and for port 8080:
sudo systemctl disable --now ustreamer
| One camera | Several cameras | |
|---|---|---|
| Software | ustreamer (shipped) | crowsnest (installed separately) |
| Configuration | none needed | ~/printer_data/config/crowsnest.conf
|
| Ports | 8080 only | 8080, 8081, 8082, ... |
| URLs | /webcam/ |
/webcam/, /webcam2/, /webcam3/
|
The rest of this page applies to both, except where crowsnest configuration is shown explicitly.
Why MJPEG matters
USB cameras deliver frames in one of two ways:
- MJPEG (
MJPG) - the camera compresses each frame itself. The Recore copies compressed frames straight to the browser without touching them. - Raw (
YUYV) - the camera sends uncompressed pixels. Every frame must then be JPEG-compressed in software on the Recore's CPU.
A single 640x480 raw frame is 614 kB. The same frame as MJPEG is typically 20-40 kB. That difference applies to both the USB bus and the processor.
Measured on the same board, same bus:
| Camera | MJPEG? | Result |
|---|---|---|
| Logitech BRIO | yes | 1280x720 @ 54 fps, hardware passthrough |
| AUKEY PC-W1 | yes | 1280x720 @ 30 fps, hardware passthrough |
| Vimicro "Venus" | no | never obtained USB bandwidth alongside the others, even at 320x240 |
The raw-only camera could not be made to work as a third camera at any resolution. It was not scaling its USB bandwidth request with frame size.
Checking a camera before buying
If you can plug it into any Linux machine:
v4l2-ctl --list-formats-ext -d /dev/video0
Look for this line:
[1]: 'MJPG' (Motion-JPEG, compressed)
If only 'YUYV' appears, the camera has no hardware compression. It will work as a single camera at low resolution, but it is a poor choice for a Recore.
Some cameras also list H264 or HEVC. These are not currently used - the streaming software (ustreamer) handles MJPEG only - but a camera offering H.264 will almost always offer MJPEG as well.
How many cameras can run at once
The A64 has a single USB host controller, and every USB device on a Recore sits behind one hub connected to it. USB 2.0 reserves bandwidth for cameras up front, when the stream starts, and the total is fixed.
Measured with three MJPEG cameras:
| Configuration | Result |
|---|---|
| 3 cameras @ 640x480 | all three work - 27, 20 and 30 fps |
| 3 cameras @ 1280x720 | only the first starts; other two fail immediately |
| 2 cameras @ 1280x720 | both work - 53 and 30 fps |
| 2 cameras @ 1920x1080 | first works, second fails |
Bandwidth is claimed on a first-come basis. Whichever camera starts first gets what it asks for, and later cameras get whatever is left - which may be nothing.
The signature of running out of bandwidth
$ dmesg | grep URB uvcvideo 1-1.3:1.1: Failed to submit URB 0 (-28). uvcvideo 1-1.1:1.1: Failed to submit URB 0 (-28).
-28 is ENOSPC. In this context it does not mean disk space - it means the USB controller could not reserve enough bandwidth for the stream.
This failure is immediate and unrelated to system load. If a camera fails this way it will fail on an idle machine just as reliably. The fix is to lower resolution or frame rate, or to use fewer cameras.
CPU cost
With hardware MJPEG the CPU cost is close to nothing. Three cameras at 640x480, all three being watched in Fluidd:
cpu0 9% cpu1 5% cpu2 8% cpu3 6% (softirq 0% on all cores) ustreamer processes: ~3.6% each, about 11% of one core in total klipper (python): 7.7% <- more than all three cameras combined
Every frame captured reached the browser (queued_fps equalled captured_fps on all three streams), so nothing was being dropped.
A raw YUYV camera changes this picture completely, because ustreamer must then run a software JPEG encoder - it starts four worker threads for a single 640x480 stream.
Conclusion: on a Recore, multiple cameras are limited by USB bandwidth, not by the processor. Three cameras left the SoC around 90% idle.
Low frame rate from a single camera
If one camera runs far below its rated frame rate, the cause is usually exposure time, not bandwidth.
In dim light a camera on automatic exposure will lengthen its exposure, and exposure time puts a hard ceiling on frame rate: 140 ms per frame is at most 7 fps, no matter what else is configured.
Measured on a Vimicro camera at 640x480 (Recore A7):
| Setting | Frame rate |
|---|---|
| auto exposure (default) | 7.2 fps |
exposure_time_absolute=100 (~10 ms) |
19.9 fps |
exposure_time_absolute=30 (~3 ms) |
30.0 fps |
Note the trade-off: shorter exposure means a darker image. More light on the subject is the better fix where possible.
The diagnostic that distinguishes the two causes: lower the resolution and re-measure.
- Frame rate improves roughly in proportion - the limit was bandwidth.
- Frame rate does not change - the limit is exposure (or something else per-frame).
In the case above, 320x240 gave exactly the same 7 fps as 640x480, which ruled out bandwidth before anything was changed.
Setting exposure manually in crowsnest:
[cam 1] v4l2ctl: auto_exposure=1,exposure_time_absolute=100
Configuration
Use stable device paths
Do not use /dev/video0, /dev/video2 and so on in configuration. These numbers change whenever a camera is added, removed or replugged - on the test board /dev/webcam moved from video2 to video5 to video7 in a single afternoon as cameras were swapped, silently changing which camera was being streamed.
Use the by-id path instead:
$ ls /dev/v4l/by-id/ usb-046d_Logitech_BRIO_E76A9283-video-index0 usb-Sonix_Technology_Co.__Ltd._AUKEY_PC-W1_SN0001-video-index0
index0 matters: a UVC camera exposes several device nodes and only the first is the capture device. The BRIO exposes four; the rest carry metadata.
Example crowsnest configuration
Three cameras, all MJPEG, at a resolution where all three fit:
[crowsnest] log_level: verbose rollover_on_start: false no_proxy: false [cam 1] mode: ustreamer port: 8080 device: /dev/v4l/by-id/usb-046d_Logitech_BRIO_E76A9283-video-index0 resolution: 640x480 max_fps: 30 custom_flags: --format=MJPEG --drop-same-frames=30 [cam 2] mode: ustreamer port: 8081 device: /dev/v4l/by-id/usb-Sonix_Technology_Co.__Ltd._AUKEY_PC-W1_SN0001-video-index0 resolution: 640x480 max_fps: 30 custom_flags: --format=MJPEG --drop-same-frames=30 [cam 3] mode: ustreamer port: 8082 device: /dev/v4l/by-id/usb-Linux_Foundation_Webcam_gadget-video-index0 resolution: 640x480 max_fps: 30 custom_flags: --format=MJPEG --drop-same-frames=30
Ports map to URLs: 8080 is /webcam/, 8081 is /webcam2/, 8082 is /webcam3/.
Adding cameras to Fluidd
Each camera has to be registered in Moonraker before it appears in the interface. In Fluidd: Settings -> Camera -> Add Camera, then set the stream URL to /webcam2/?action=stream and the snapshot URL to /webcam2/?action=snapshot.
Choose service "MJPEG Stream" rather than "MJPEG Stream (Adaptive)". In adaptive mode the browser fetches individual snapshots at its own pace instead of consuming the stream, and it throttles to target_fps_idle - 5 fps by default - whenever the printer is not printing. A camera capturing at 20 fps can appear to run at 2 fps for this reason alone, with nothing wrong on the Recore at all.
Troubleshooting
| Symptom | Likely cause | Check |
|---|---|---|
| Second or third camera never starts | USB bandwidth | dmesg | grep URB for -28
|
| Low fps, unchanged by lowering resolution | exposure time | v4l2-ctl -d /dev/webcam --list-ctrls
|
| Low fps, improves when resolution is lowered | USB bandwidth | reduce resolution or camera count |
| Fluidd shows ~2-5 fps, board reports more | Fluidd adaptive mode | set service to plain "MJPEG Stream" |
| High CPU from ustreamer | camera has no MJPEG | v4l2-ctl --list-formats-ext
|
| Camera works, then stops after replug | device path changed | use /dev/v4l/by-id/
|
| All cameras stop after adding one | crowsnest crash - see below | journalctl -u crowsnest
|
Useful commands
# What formats and frame rates does this camera support?
v4l2-ctl --list-formats-ext -d /dev/video2
# What controls does it have (exposure, brightness, ...)?
v4l2-ctl --list-ctrls -d /dev/video2
# What is the stream actually doing right now?
curl -s http://localhost:8080/state
# Which USB devices are attached, and at what speed?
for d in /sys/bus/usb/devices/[0-9]*-[0-9]*; do
echo "$(basename $d) $(cat $d/speed)M $(cat $d/product 2>/dev/null)"
done
The /state endpoint is the quickest way to see what is happening:
"encoder": {"type": "HW"} <- hardware MJPEG, no CPU cost
"encoder": {"type": "CPU"} <- software encoding, camera has no MJPEG
"online": false <- camera not delivering frames at all
"captured_fps": 27 <- what the camera produces
"queued_fps": 27 <- what reaches viewers; lower means drops
Known issue: one camera can prevent all others from starting
Crowsnest v5.0.11 crashes on startup if any attached camera exposes a V4L2 control without a default value, which is legal and occurs on some cameras:
File "crowsnest/camera/types/uvc.py", line 72, in get_controls_string
line += f" default={data['default']}"
KeyError: 'default'
The crash happens before any camera starts, so a single unusual camera takes down every stream on the machine. Changing the log level does not avoid it, because the controls are enumerated regardless.
Workaround, until fixed upstream - in crowsnest/camera/types/uvc.py:
line += f" default={data.get('default', 'n/a')}"
Background: why the USB bus is the limit
The Allwinner A64 has one EHCI host controller. Every USB device on a Recore - cameras, WiFi dongles, USB storage, USB serial - shares it.
USB 2.0 gives cameras isochronous transfers: guaranteed, reserved bandwidth, claimed when the stream opens. The specification caps this at 80% of the bus, and the reservation is made up front based on the requested resolution and frame rate. This is why a camera either starts successfully or fails immediately, rather than degrading gradually.
It was previously suspected that USB interrupt handling on a single CPU core was the constraint. Measurement does not support this for cameras:
$ grep ehci /proc/interrupts
57: 429 992573 0 0 GICv2 106 Level ehci_hcd:usb1
cpu0 cpu1
The interrupt is handled on one core, but with three cameras streaming to three viewers that core was only 5% busy, and softirq time was zero across all cores.
However, high single-core load has been measured on Recore when networking runs over a USB WiFi dongle - 73% of one core to receive 1.2 MB/s. In that case the USB completion handling and the entire network stack land on the same core. Cameras over Ethernet do not have this problem, because Ethernet on the A64 is a separate peripheral that does not use USB at all.
So: a Recore with cameras and a USB WiFi dongle is a meaningfully heavier configuration than one using Ethernet. If cameras behave badly over WiFi but well over Ethernet, this is why.
H.264 and HEVC
Some cameras offer H.264 or HEVC in addition to MJPEG. These are far more efficient than MJPEG, but they cannot currently be used on Recore. This section records why, so the question does not need re-investigating.
The efficiency is dramatic
Measured on a 4K microscope camera, same board, same bus, 45 frames per run:
| Format | Resolution | Frame rate | Per frame |
|---|---|---|---|
| MJPEG | 1920x1080 | 29.9 fps | 395 kB |
| H.264 | 1920x1080 | 29.8 fps | 18 kB (22x smaller) |
| H.264 | 3840x2160 | 26.3 fps | 89 kB |
| HEVC | 3840x2160 | 30.0 fps | 73 kB (25x smaller) |
| MJPEG | 3840x2160 | 12.7 fps | 1871 kB |
4K HEVC at a full 30 fps uses about five times less bandwidth than 1080p MJPEG. Note also that MJPEG at 4K cannot reach 30 fps at all - it manages 12.7 fps because it saturates the USB bus, while HEVC at the same resolution runs at the full rate.
If these codecs worked, the bandwidth limits described above would largely disappear.
Why it does not work
The blocker is camera-streamer, not crowsnest and not ustreamer.
- ustreamer handles MJPEG only, by design. It is not the limitation, because crowsnest can launch camera-streamer instead.
- camera-streamer does support H.264 and captures it correctly - it negotiates
1920x1080/H264without trouble - but it then fails to start:
device/buffer_list.c: CAMERA:capture: Using: 1920x1080/H264, buffers=3 <- capture works device/camera/camera_decoder.c: CAMERA: Cannot find 'H264' decoder device/camera/camera_output.c: CAMERA: Cannot find source for 'SNAPSHOT' for one of the formats 'JPEG, MJPG'.
Two things combine to make this a hard stop:
1. The snapshot output cannot be disabled. camera-streamer 0.4.2.4 provides --camera-stream.disabled and --camera-video.disabled, but there is no equivalent for snapshot. Snapshot must be JPEG or MJPG, so H.264 input has to be decoded to satisfy it. Disabling the other two outputs does not help - it still exits.
2. The A64's video decoder is the wrong kind. The SoC does have a hardware decoder (cedrus), and it does handle H.264 and HEVC, but note the formats it accepts:
$ v4l2-ctl -d /dev/video0 --list-formats-out [1]: 'S264' (H.264 Parsed Slice Data, compressed) [2]: 'S265' (HEVC Parsed Slice Data, compressed)
"Parsed Slice Data" means this is a stateless decoder. Userspace has to parse the bitstream itself, extract the sequence and picture parameter sets, and drive the hardware slice by slice through the Media Request API. camera-streamer expects a stateful decoder - the kind found on the Raspberry Pi, where a stream goes in and frames come out. These are fundamentally different programming models, so camera-streamer cannot use cedrus even though the hardware is capable.
This is not a missing driver, a missing package or a configuration mistake.
What would be needed
- Patch camera-streamer to allow the snapshot output to be disabled, then pass H.264 through to WebRTC without decoding at all. This is the smallest change and would deliver the bandwidth saving. The drawback is that WebRTC is the only viable output - Fluidd displays cameras with an ordinary image element, which cannot show H.264.
- Use a decoder that speaks stateless V4L2 - ffmpeg with the Request API, or libva - to convert H.264 to MJPEG on the board. This keeps compatibility with existing interfaces, but re-introduces a per-frame cost, which is much of what the codec was meant to avoid.
Until one of those exists, use MJPEG, and treat a camera's H.264 or HEVC support as a bonus that is not currently usable. A camera offering H.264 will almost always offer MJPEG too, so such cameras are still fine to buy.
See also
- Rebuild issue #76 - USB interrupt handling and multi-camera investigation, with measurements
- Crowsnest documentation