Facecam Ubuntu Runtime
A Linux-native runtime for the Elgato Facecam on Ubuntu. No official Linux driver exists — this project provides the complete control plane: device probing, a normalization daemon, control CLI, compatibility harness, and a broadcast-grade visual diagnostic tool.
Why This Exists
The Elgato Facecam is a premium USB webcam that works seamlessly on Windows and macOS via Camera Hub. On Linux, it enumerates as a standard UVC device but exhibits several quirks that break common workflows:
- Bogus format advertisement — the device claims to support NV12/YU12 but only UYVY and MJPEG produce valid frames
- Open/close cycle lockup — after the first application closes the device, subsequent opens fail until a USB reset
- ~50% startup failure rate — the camera randomly fails to initialize on first open
- Chromium incompatibility — Chrome/Electron apps reject the device without v4l2loopback normalization
- No Linux control persistence — settings reset on every plug cycle
This project solves all of these with a Rust-based daemon that owns the physical device, normalizes output through v4l2loopback, and provides deterministic recovery.
What's Included
| Component | Purpose |
|---|---|
facecam-probe | Detect, fingerprint, and enumerate the camera |
facecam-daemon | Capture from device, output to v4l2loopback virtual camera |
facecam-ctl | Control the daemon: profiles, controls, diagnostics |
facecam-harness | Automated compatibility and stability testing |
facecam-visual | Live viewer with waveform, histogram, zebras, focus peaking |
Quick Install
sudo dpkg -i facecam-ubuntu_0.1.0_amd64.deb
sudo apt-get install -f # pulls v4l2loopback-dkms, v4l-utils
facecam-probe detect
Design Principles
- Deterministic over optimistic — every mitigation traces to an observed device behavior
- Machine-readable everything — JSON logs, structured diagnostics, typed IPC
- Ubuntu-first — tested on 24.04/25.10, kernel 6.8+
- No fragile shell scripts — core runtime in Rust, proper error handling
- Recoverable by design — USB reset, retry logic, and watchdog built in
Installation
From .deb Package (Recommended)
Download the latest .deb from the Releases page and install:
sudo dpkg -i facecam-ubuntu_0.1.0_amd64.deb
sudo apt-get install -f
This installs:
- 5 binaries to
/usr/bin/ - udev rules to
/etc/udev/rules.d/99-facecam.rules - v4l2loopback config to
/etc/modprobe.d/v4l2loopback.conf - Module autoload to
/etc/modules-load.d/v4l2loopback-load.conf - systemd service to
/lib/systemd/system/facecam-daemon.service
Dependencies (v4l2loopback-dkms, v4l-utils, libusb-1.0-0) are pulled automatically.
From Source
Prerequisites
# System packages
sudo apt-get install -y \
v4l2loopback-dkms v4l2loopback-utils v4l-utils \
libusb-1.0-0-dev pkg-config build-essential
# Rust toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Build & Install
git clone https://github.com/facecam-ubuntu/facecam-ubuntu
cd facecam-ubuntu
sudo ./install.sh
Or build manually:
cargo build --workspace --release
sudo cp target/release/facecam-{probe,daemon,ctl,harness,visual} /usr/local/bin/
sudo cp config/99-facecam.rules /etc/udev/rules.d/
sudo cp config/v4l2loopback.conf /etc/modprobe.d/
sudo cp config/facecam-daemon.service /etc/systemd/system/
sudo udevadm control --reload-rules
sudo systemctl daemon-reload
Load v4l2loopback
sudo modprobe v4l2loopback video_nr=10 card_label="Facecam Normalized" exclusive_caps=1
Verify:
ls /dev/video10 # Should exist
Uninstall
sudo dpkg -r facecam-ubuntu
Or manually remove binaries from /usr/local/bin/ and config files.
Quick Start
1. Plug In the Camera
Connect the Elgato Facecam to a USB 3.0 port (blue port, USB-C, or Thunderbolt). USB 2.0 will not work — the camera enters a fallback mode with PID 0x0077 and refuses to expose any video interface.
2. Verify Detection
facecam-probe detect
Expected output:
Device: Elgato Facecam (PID 0x0078)
Firmware: 4.09
USB: bus 10 addr 2 (SuperSpeed (5 Gbps))
V4L2: /dev/video0
Card: Elgato Facecam: Elgato Facecam
If you see USB2 FALLBACK, move the cable to a different port or try a different cable.
3. Check What the Camera Offers
facecam-probe formats # List pixel formats and resolutions
facecam-probe controls # List all V4L2 controls with ranges
facecam-probe quirks # Show known device quirks
4. See It Live
facecam-visual --resolution 720
A window opens with your live camera feed plus diagnostic overlays. Press W for zebra stripes, E for focus peaking.
5. Start the Daemon
The daemon captures from the physical camera and outputs to a v4l2loopback virtual camera that all apps can use:
# Load v4l2loopback if not already loaded
sudo modprobe v4l2loopback video_nr=10 card_label="Facecam Normalized" exclusive_caps=1
# Start the daemon
sudo systemctl start facecam-daemon
# Check status
facecam-ctl status
6. Use in Applications
Open OBS, Chrome, Zoom, or any video app and select "Facecam Normalized" (/dev/video10) as your camera. This virtual camera is stable across open/close cycles and works with all applications including Chromium-based browsers.
7. Adjust Settings
facecam-ctl profile list # See available profiles
facecam-ctl profile apply streaming # Apply streaming preset
facecam-ctl control set brightness 150 # Tweak individual controls
facecam-ctl control list # See all current values
Hardware Requirements
USB 3.0 is Mandatory
The Elgato Facecam requires USB 3.0 SuperSpeed (5 Gbps). On USB 2.0, the camera presents a different product ID (0x0077 instead of 0x0078) with the string USB3-REQUIRED-FOR-FACECAM and exposes no video interface.
UYVY 1080p60 requires ~249 MB/s — over 60% of USB 2.0's theoretical maximum and well beyond its practical throughput.
How to Identify USB 3.0 Ports
- Blue USB-A ports are USB 3.0+
- USB-C ports are usually USB 3.0+ (but verify with
lsusb -t) - Thunderbolt ports support USB 3.0+
Verify After Connecting
lsusb | grep 0fd9
0fd9:0078on an even-numbered bus (002, 004, 006...) = USB 3.0, correct0fd9:0077on an odd-numbered bus = USB 2.0 fallback, move the cable
Or use the probe:
facecam-probe detect
Cable Quality Matters
A USB-C to USB-C cable must be USB 3.0 rated. Common failure mode: a USB 2.0 cable causes the camera to fall back to PID 0x0077 even when plugged into a 3.0 port. If you see the fallback, try a different cable first.
USB Topology
Avoid sharing USB controllers with other high-bandwidth devices. UYVY 1080p60 at ~249 MB/s leaves little headroom on a shared USB 3.0 controller.
facecam-probe topology # Shows sysfs path, speed, bus info
lsusb -t # Full USB tree
Thunderbolt docks are a known source of instability due to bandwidth sharing.
Bandwidth by Mode
| Mode | Format | Bandwidth |
|---|---|---|
| 1080p60 | UYVY | ~249 MB/s |
| 1080p30 | UYVY | ~124 MB/s |
| 720p60 | UYVY | ~111 MB/s |
| 720p30 | UYVY | ~55 MB/s |
| 1080p60 | MJPEG | ~5-15 MB/s (variable) |
| 720p30 | MJPEG | ~2-5 MB/s (variable) |
MJPEG mode drastically reduces bandwidth and is recommended when sharing USB controllers.
System Overview
Architecture Diagram
USB 3.0
|
+-------+-------+
| Elgato Facecam | PID 0x0078, UVC 1.10
| /dev/video0 | UYVY + MJPEG
+-------+-------+
|
| MMAP streaming (VIDIOC_QBUF/DQBUF)
|
+-------+-------+
| facecam-daemon | Single long-lived process
| | Owns the physical device
| - Capture | Prevents open/close lockup
| - Recovery | USB reset on failure
| - Controls | Profile application
+-------+-------+
|
| write() frames
|
+-------+-------+
| v4l2loopback | /dev/video10
| "Facecam | exclusive_caps=1
| Normalized" | Multiple consumers OK
+-------+-------+
|
+------------+------------+
| | |
+--+--+ +--+--+ +--+--+
| OBS | |Chrome| | Zoom | Any V4L2 consumer
+------+ +------+ +------+
Key Design Decisions
Single-Producer Daemon
The daemon is the only process that opens the physical Facecam device. This solves the open/close lockup bug — consumer applications open and close the v4l2loopback device freely without touching the physical hardware.
v4l2loopback with exclusive_caps
The exclusive_caps=1 parameter is mandatory. Without it, Chromium-based browsers refuse to use the virtual camera because they reject devices that report both V4L2_CAP_VIDEO_CAPTURE and V4L2_CAP_VIDEO_OUTPUT.
USB Reset Recovery
The daemon implements automatic recovery via the sysfs authorized flag cycle:
- Write
0to/sys/bus/usb/devices/<dev>/authorized(deauthorize) - Wait 500ms for kernel driver unbind
- Write
1to reauthorize (triggers re-enumeration) - Wait 1500ms for device stabilization
- Retry the failed operation
Profile-Based Control Persistence
V4L2 controls reset on device disconnect. The daemon re-applies the active profile's control values after every recovery cycle, ensuring consistent camera settings.
Crate Structure
facecam-ubuntu/
crates/
facecam-common/ Shared library — types, quirks, v4l2, USB, IPC
facecam-probe/ Device detection and enumeration CLI
facecam-daemon/ Normalization daemon
facecam-ctl/ Control CLI (talks to daemon via Unix socket)
facecam-harness/ Automated compatibility test suite
facecam-visual/ Live visual diagnostic tool
All crates share facecam-common for device identification, V4L2 ioctl wrappers, USB enumeration, the quirk registry, profile management, and IPC types.
Normalization Pipeline
Frame Flow
Physical Device (/dev/video0)
|
| VIDIOC_DQBUF (MMAP buffer)
v
[Frame in UYVY or MJPEG]
|
| Copy to v4l2loopback
v
Virtual Device (/dev/video10)
|
| Consumer opens and reads
v
Application (OBS, Chrome, etc.)
Pipeline States
The daemon operates as a state machine:
Idle ──> Probing ──> Starting ──> Streaming
^ |
| v
+──── Failed <──── Recovering <─────+
|
ShuttingDown
| State | Description |
|---|---|
Idle | No device detected, waiting |
Probing | Device found, reading capabilities and controls |
Starting | Format set, MMAP buffers allocated, about to stream |
Streaming | Active frame forwarding |
Recovering | Error detected, performing USB reset |
Failed | Max recovery attempts exceeded |
ShuttingDown | Graceful exit in progress |
Recovery Logic
On any capture error:
- Increment recovery counter
- If counter >
max_recovery_attempts(default 5): enterFailedstate - Perform USB sysfs reset
- Wait for device re-enumeration (2s)
- Re-open device, re-apply format and controls
- Resume streaming
The retry_with_reset helper in facecam-common::recovery encapsulates this pattern for any fallible operation.
Format Selection
The daemon selects the best available format:
- Load the active profile's preferred format/resolution
- Filter to formats marked reliable in the quirk registry (UYVY, MJPEG)
- Match the closest available mode
- Fall back to the first reliable mode if no match
Frame Statistics
The daemon tracks per-5-second windows:
frames_captured— total frames dequeued from the deviceframes_written— total frames written to v4l2loopbackframes_dropped— frames lost due to sink write failuresrecovery_count— number of USB reset cycles performed
These are exposed via the IPC status command and included in diagnostics bundles.
Quirk Registry
Every workaround in this project traces to a specific observed device behavior. The quirk registry in facecam-common::quirks catalogs these with severity levels and mitigations.
Active Quirks
| ID | Severity | Summary |
|---|---|---|
BOGUS_NV12 | Error | NV12 format advertised but produces garbage frames |
BOGUS_YU12 | Error | YU12 format advertised but produces garbage frames |
OPEN_CLOSE_LOCKUP | Critical | Device locks up after consumer close/reopen |
STARTUP_UNRELIABILITY | Error | ~50% failure rate on initial stream start |
NO_MJPEG_OLD_FW | Warning | No MJPEG on firmware < 4.00 |
CHROMIUM_FORMAT_REJECT | Error | Chrome rejects devices with both CAPTURE+OUTPUT caps |
USB2_FALLBACK_MODE | Critical | PID changes to 0x0077 on USB 2.0, no video |
USB3_REQUIRED | Critical | Device requires USB 3.0 SuperSpeed |
BANDWIDTH_STARVATION | Warning | Hub sharing causes drops at ~249 MB/s |
YUYV_UYVY_AMBIGUITY | Info | Wire format is UYVY despite some reports of YUYV |
Quirk Structure
Each quirk contains:
#![allow(unused)] fn main() { pub struct Quirk { pub id: &'static str, // Machine-readable identifier pub summary: &'static str, // One-line description pub description: &'static str, // Full explanation with observed behavior pub affected: QuirkScope, // What triggers this quirk pub severity: QuirkSeverity, // Info, Warning, Error, Critical pub mitigation: QuirkMitigation, // How the system handles it } }
Querying Quirks
# Show all quirks applicable to connected device
facecam-probe quirks
# JSON output for automation
facecam-probe --format json quirks
Adding New Quirks
When you observe a new device behavior:
- Document the exact symptoms (error codes, frame data, timing)
- Identify the trigger conditions (firmware version, format, USB topology)
- Determine a mitigation
- Add to
quirks::quirk_registry()incrates/facecam-common/src/quirks.rs
Every quirk must be traceable to a real observation — no speculative entries.
IPC Protocol
The daemon and CLI communicate via a Unix domain socket at $XDG_RUNTIME_DIR/facecam-daemon.sock (typically /run/user/1000/facecam-daemon.sock).
Wire Format
One JSON object per line, newline-delimited. Client sends a DaemonCommand, daemon responds with a DaemonResponse.
Commands
"Status"
{"ApplyProfile": {"name": "streaming"}}
{"SetControl": {"name": "brightness", "value": 150}}
{"GetControl": {"name": "contrast"}}
"GetAllControls"
"ExportDiagnostics"
"ForceReset"
"RestartPipeline"
"Shutdown"
Responses
{"Status": {"state": "streaming", "health": "healthy", "fps": 30.0, ...}}
{"Ok": "Profile 'streaming' applied (7 controls set)"}
{"ControlValue": {"name": "brightness", "value": 150}}
{"Controls": [{"name": "brightness", "id": 9963776, "value": 150, ...}, ...]}
{"DiagnosticsExported": "/home/user/.local/share/facecam/diagnostics/facecam-diag-20260404-185406.json"}
{"Error": "No source device connected"}
Example: Manual Socket Interaction
# Connect and send a status query
echo '"Status"' | socat - UNIX-CONNECT:$XDG_RUNTIME_DIR/facecam-daemon.sock
Security
The socket is created with mode 0660. Users in the video group can connect. The daemon requires write access to /sys/bus/usb/devices/ for USB reset operations.
facecam-probe
Detect, fingerprint, and enumerate the Elgato Facecam. This is the first tool to run when setting up or debugging.
Usage
facecam-probe [--format text|json] [-v] <command>
Commands
detect
Scans USB bus for Elgato devices, enriches with V4L2 info.
$ facecam-probe detect
Device: Elgato Facecam (PID 0x0078)
Firmware: 4.09
Serial: FW06M1A07449
USB: bus 10 addr 2 (SuperSpeed (5 Gbps))
V4L2: /dev/video0
Card: Elgato Facecam: Elgato Facecam
formats
Lists all pixel formats, resolutions, and frame rates. Flags broken formats.
$ facecam-probe formats
Pixel Formats:
[0] UYVY - UYVY 4:2:2 [RELIABLE]
[1] MJPG - Motion-JPEG [RELIABLE]
Video Modes:
1920x1080 @ 60.0 fps (UYVY) (249 MB/s)
1280x720 @ 30.0 fps (MJPG)
...
controls
Enumerates all V4L2 controls with current values, ranges, and menu items.
$ facecam-probe controls
Brightness (0x00980900):
type=Integer value=128 range=[0, 255] step=1 default=128
Contrast (0x00980901):
type=Integer value=5 range=[0, 10] step=1 default=3
...
topology
Shows USB sysfs details: bus, speed, manufacturer, firmware BCD, configuration.
quirks
Lists all quirks applicable to the detected device and firmware version.
diagnostics
Collects system info, kernel module status, V4L2 devices, and exports a JSON bundle.
validate
Attempts to stream each advertised format and verifies actual frame delivery.
JSON Output
All commands support --format json for machine-readable output:
facecam-probe --format json detect | jq .
facecam-daemon
The normalization daemon — captures from the physical Facecam and outputs to a v4l2loopback virtual camera.
Usage
facecam-daemon [--source /dev/video0] [--sink /dev/video10] \
[--profile default] [--foreground] [--log-format json|text]
systemd Service
The recommended way to run the daemon:
sudo systemctl start facecam-daemon
sudo systemctl enable facecam-daemon # Auto-start on plug
sudo systemctl status facecam-daemon
journalctl -u facecam-daemon -f # Follow logs
The udev rule (99-facecam.rules) triggers the service automatically when the Facecam is plugged in.
Configuration
Config file: ~/.config/facecam/daemon.toml
max_recovery_attempts = 5
frame_timeout_ms = 5000
[loopback]
video_nr = 10
card_label = "Facecam Normalized"
max_openers = 10
[logging]
max_events = 1000
Options
| Flag | Default | Description |
|---|---|---|
--source | auto-detect | Physical camera device path |
--sink | /dev/video10 | v4l2loopback output device |
--profile | default | Profile to apply on startup |
--foreground | off | Don't daemonize |
--log-format | json | Log format (json for production, text for debug) |
--config | ~/.config/facecam/daemon.toml | Config file path |
Recovery Behavior
The daemon automatically recovers from:
- Device read errors (USB glitch)
- Stream initialization failures (~50% startup bug)
- Device disconnection (unplug/replug)
After max_recovery_attempts consecutive failures (default 5), it enters Failed state and waits for manual intervention via facecam-ctl restart or facecam-ctl reset.
facecam-ctl
Control CLI for the daemon. Communicates via Unix domain socket.
Commands
status
$ facecam-ctl status
State: streaming
Health: healthy
Uptime: 3600s
Connected: true
Mode: 1280x720 @ 30.0 fps (UYVY)
Frames: captured=108000 written=107998 dropped=2
Recoveries: 0
Profile: streaming
control
facecam-ctl control list # All controls with values
facecam-ctl control get brightness # Single control
facecam-ctl control set brightness 150 # Set a control
facecam-ctl control set contrast 5
facecam-ctl control set zoom 10
profile
facecam-ctl profile list # Available profiles
facecam-ctl profile show streaming # Profile details
facecam-ctl profile apply streaming # Apply a profile
facecam-ctl profile init # Create default profiles
diagnostics
Exports a full diagnostics bundle (JSON) for remote debugging:
$ facecam-ctl diagnostics
Diagnostics exported to: ~/.local/share/facecam/diagnostics/facecam-diag-20260404-185406.json
Operational Commands
facecam-ctl reset # Force USB reset
facecam-ctl restart # Restart the capture pipeline
facecam-ctl shutdown # Stop the daemon
JSON Output
All commands support --json:
facecam-ctl --json status | jq .state
facecam-ctl --json control list | jq '.[].name'
facecam-harness
Automated compatibility and stability testing. Produces machine-readable reports.
Usage
facecam-harness [--device /dev/video0] [--json] [-v] <command>
Commands
full
Runs the complete test suite:
$ facecam-harness full
Running: device_detection ...
[PASS] device_detection (45ms)
Running: format_enumeration ...
[PASS] format_enumeration (12ms)
Running: control_enumeration ...
[PASS] control_enumeration (8ms)
Running: format_negotiation ...
[PASS] format_negotiation (23ms)
Running: open_close_cycles ...
[PASS] open_close_cycles (1204ms)
Running: control_roundtrip ...
[PASS] control_roundtrip (15ms)
Running: usb_topology ...
[PASS] usb_topology (3ms)
Running: kernel_modules ...
[PASS] kernel_modules (1ms)
8/8 passed, 0 failed
Individual Tests
facecam-harness formats # Format enumeration
facecam-harness open-close --cycles 50 # Open/close stability
facecam-harness controls # Control roundtrip
facecam-harness stream-stability --duration 300 # 5-minute soak test
facecam-harness recovery # USB reset test
report
List and view previous test reports:
facecam-harness report
Reports are saved to ~/.local/share/facecam/harness/harness-<timestamp>.json.
Test Matrix
The harness validates:
| Test | What it checks |
|---|---|
device_detection | USB enumeration finds Facecam with correct PID |
format_enumeration | V4L2 formats are listed, bogus ones flagged |
control_enumeration | All expected controls are present |
format_negotiation | Each format can be set via VIDIOC_S_FMT |
open_close_cycles | Device survives N open/close cycles |
control_roundtrip | Set/get control values match |
usb_topology | Device is on USB 3.0+ |
kernel_modules | uvcvideo loaded, v4l2loopback status |
stream_stability | Device remains responsive over time |
usb_recovery | USB reset mechanism works |
CI Integration
facecam-harness --json full > report.json
# Check exit code: 0 = all passed, 1 = failures
facecam-visual
Live visual diagnostic tool with broadcast-grade analysis overlays.
Usage
facecam-visual [--device /dev/video0] [--resolution 720] [--fps 30] [--mjpeg]
Window Layout
+---------------------------------------+
| LIVE CAMERA FEED |
| [30 FPS] [ZEBRA] |
| [FOCUS] |
| [ A/B ] |
+=======================================+
| WAVEFORM MONITOR | RGB HISTOGRAM |
+---------------------------------------+
| 30.0fps 33.3ms Brt:128 Con:5 ... |
+---------------------------------------+
| Frame Timing Waterfall |
+---------------------------------------+
Keyboard Controls
Camera Controls
| Key | Action |
|---|---|
| + / - | Brightness up/down (step 10) |
| [ / ] | Contrast up/down (step 1) |
| Z / X | Zoom in/out (step 1) |
Diagnostic Overlays
| Key | Feature | Description |
|---|---|---|
| W | Zebra stripes | Red diagonal hatching on pixels with luma > 235 (overexposure) |
| E | Focus peaking | Magenta dots on sharp edges (Sobel edge detection) |
| A | A/B capture | Freezes current frame as reference (left side) |
| D | A/B clear | Removes the reference frame |
| < / > | A/B split | Moves the comparison split line |
General
| Key | Action |
|---|---|
| S | Save snapshot (PPM format with overlays) |
| R | Force USB reset |
| Space | Pause/unpause |
| F | Toggle help text |
| Q / Esc | Quit |
Analysis Features
Waveform Monitor (bottom-left)
Plots luma distribution per video column. Standard broadcast tool for checking exposure:
- Green = safe range (16-235 IRE)
- Yellow = hot highlights (>200)
- Red = clipping (>235)
- Blue = crushed blacks (<16)
- Dashed reference lines at 0 IRE and 100 IRE
RGB Histogram (bottom-right)
Per-channel pixel value distribution with additive blending:
- Red, green, blue curves overlaid
- Left-edge blue bar = crushed blacks (>2% of pixels at 0)
- Right-edge red bar = blown highlights (>2% of pixels at 255)
Frame Timing Waterfall (bottom strip)
Scrolling timeline where each column = one frame:
- Green = delivered on time (<110% of target)
- Yellow = late (110-150% of target)
- Orange = very late (150-200%)
- Red = severely delayed or dropped (>200%)
- Dashed reference line at target frame time
Zebra Stripes
Industry-standard overexposure indicator. Red diagonal hatching overlaid on any pixel where luma exceeds 235. The pattern animates to distinguish it from image content.
Focus Peaking
Sobel edge detection with configurable sensitivity. Magenta dots mark sharp edges in the image. Since the Facecam has fixed focus, this helps verify subject distance is in the lens sweet spot.
A/B Comparison
Capture a reference frame with A, then adjust controls. The window splits: left = reference, right = live. Move the split with </>. Clear with D.
Session Summary
On exit, prints aggregate statistics:
Session summary:
Total frames: 1847
Dropped: 0
Avg FPS: 30.0
Avg frame: 33.3ms
Jitter: 0.42ms
Uptime: 61.6s
Elgato Facecam Specs
Hardware
| Field | Value |
|---|---|
| Vendor ID | 0x0fd9 (Elgato Systems GmbH) |
| Product ID | 0x0078 (normal) / 0x0077 (USB2 fallback) |
| UVC Version | 1.10 |
| Sensor | Sony STARVIS CMOS |
| Lens | Elgato Prime Lens, all-glass, f/2.4, 24mm equivalent |
| Focus | Fixed (no autofocus) |
| Max Resolution | 1920x1080 |
| FOV | 82 degrees diagonal |
| USB | 3.0 SuperSpeed (mandatory) |
| Power | Bus-powered, 304mA max |
| Interfaces | UVC (video) + HID (proprietary controls) |
USB Descriptor Summary
From lsusb -v:
- Configuration:
USB-3.0 - Camera Terminal: Auto-Exposure Mode, Exposure Time Absolute, Zoom Absolute
- Processing Unit: Brightness, Contrast, Saturation, Sharpness, White Balance Temperature, Power Line Frequency, WB Auto
- Extension Unit: GUID
{a8e5782b-36e6-4fa1-87f8-83e32b323124}, 9 proprietary controls (noise reduction, metering mode, save-to-flash, etc.) - HID Interface: Endpoint 0x89, for Camera Hub protocol
Video Formats (Firmware 4.09)
| Format | Resolutions | Frame Rates |
|---|---|---|
| UYVY 4:2:2 | 1920x1080, 1280x720, 960x540 | 60, 30 |
| MJPEG | 1920x1080, 1280x720, 960x540 | 60, 30 |
Still Image Capture
The USB descriptor advertises still image capture:
- UYVY: 3840x2160 (4K)
- MJPEG: 4128x3096, 4128x2322
These are available via UVC still image capture but not through standard V4L2 streaming.
Color Space
- Color Primaries: BT.709 / sRGB
- Transfer Function: BT.709
- Matrix Coefficients: SMPTE 170M (BT.601)
Firmware Versions
Version History
| Firmware | Key Changes | Linux Impact |
|---|---|---|
| 2.00 | Initial release | Only uncompressed formats; format bugs |
| 2.52 | Fixed settings save bugs | Minor |
| 3.00 | Bulk/iso transfer mode; improved ISP | Transfer mode selectable |
| 4.00+ | MJPEG format added | Chromium compatibility fixed |
| 4.09 | Latest; minor refinements | Confirmed working, recommended |
Detecting Firmware Version
facecam-probe detect # Shows firmware field
facecam-probe topology # Shows bcdDevice raw value
The firmware version comes from the USB bcdDevice descriptor. 0x0409 = firmware 4.09.
Firmware Update
Firmware updates can only be performed via Camera Hub on Windows or macOS. There is no Linux-native update mechanism.
If you're on firmware < 4.00:
- Borrow a Windows or Mac machine
- Install Camera Hub
- Connect the Facecam and follow the update prompt
- Verify on Linux:
facecam-probe detectshould show 4.09
Firmware-Gated Features
| Feature | Minimum Firmware |
|---|---|
| MJPEG format | 4.00 |
| Bulk/iso transfer mode selection | 3.00 |
| Reliable settings save | 2.52 |
V4L2 Controls
Available Controls (Firmware 4.09)
Verified from live device testing:
| Control | CID | Type | Range | Default | Notes |
|---|---|---|---|---|---|
| Brightness | 0x00980900 | int | 0-255 | 128 | Processing Unit |
| Contrast | 0x00980901 | int | 0-10 | 3 | Processing Unit |
| Saturation | 0x00980902 | int | 0-63 | 35 | Processing Unit |
| Sharpness | 0x0098091b | int | 0-4 | 2 | Processing Unit |
| WB Auto | 0x0098090c | bool | 0-1 | 1 | Disables WB Temp when on |
| WB Temperature | 0x0098091a | int | 2800-12500 | 5000 | Step 100, Kelvin |
| Power Line Freq | 0x00980918 | menu | 0-2 | 2 | 0=Off, 1=50Hz, 2=60Hz |
| Auto Exposure | 0x009a0901 | menu | 0-3 | 0 | 0=Auto, 2=Shutter Priority |
| Exposure Time | 0x009a0902 | int | 1-2500 | 156 | Units of 100us |
| Zoom Absolute | 0x009a090d | int | 1-31 | 1 | Digital zoom/crop |
Exposure Time Values
The exposure time is in units of 100 microseconds:
| Value | Shutter Speed | Use Case |
|---|---|---|
| 1 | 1/10000s | Bright daylight |
| 10 | 1/1000s | Well-lit room |
| 78 | 1/128s | Indoor |
| 156 | 1/64s | Default |
| 500 | 1/20s | Low light |
| 2500 | 1/4s | Very dark (motion blur) |
White Balance Temperature
| Kelvin | Light Source |
|---|---|
| 2800 | Candlelight |
| 3200 | Tungsten / warm white |
| 4000 | Fluorescent |
| 5000 | Default / neutral daylight |
| 6500 | Overcast / cool daylight |
| 10000 | Blue sky |
| 12500 | Maximum (very cool) |
Controls NOT Available via V4L2
These require the proprietary HID protocol (Camera Hub on Windows/Mac):
- Noise reduction (on/off)
- Metering mode (average / center-weighted)
- Save settings to device flash
- USB transfer mode (bulk / isochronous)
- Firmware update
The Extension Unit GUID is {a8e5782b-36e6-4fa1-87f8-83e32b323124} with 9 vendor-specific controls.
Command Line Usage
# Read controls
facecam-probe controls
facecam-ctl control list
facecam-ctl control get brightness
# Set controls
facecam-ctl control set brightness 150
facecam-ctl control set contrast 5
facecam-ctl control set zoom 10
# Or directly with v4l2-ctl
v4l2-ctl -d /dev/video0 --set-ctrl brightness=150
USB Behavior
Dual-PID Enumeration
The Facecam uses different USB Product IDs depending on the port speed:
| Speed | PID | Product String | Video |
|---|---|---|---|
| USB 3.0 SuperSpeed | 0x0078 | Elgato Facecam | Full UVC |
| USB 2.0 High-Speed | 0x0077 | USB3-REQUIRED-FOR-FACECAM | None |
This is a deliberate firmware behavior — the camera refuses to operate on USB 2.0 rather than producing a degraded experience.
Open/Close Lockup
The most impactful Linux-specific bug. After the first application closes the V4L2 device file descriptor, subsequent opens fail with EBUSY or produce no frames.
Root cause: Unknown — likely a firmware-side USB endpoint state management issue. The UVC driver's uvc_video_stop_streaming may leave the device in a state it can't recover from.
Workaround: The normalization daemon keeps the device open continuously. Consumer apps use the v4l2loopback virtual camera.
Recovery: USB sysfs authorized flag cycle forces kernel re-enumeration:
# Automated by facecam-ctl reset and the daemon's recovery logic
echo 0 > /sys/bus/usb/devices/10-2/authorized
sleep 0.5
echo 1 > /sys/bus/usb/devices/10-2/authorized
Startup Unreliability
The camera fails to start streaming approximately 50% of the time on first open. The daemon's retry_with_reset logic handles this by:
- Attempting the operation
- On failure, performing USB reset
- Waiting for re-enumeration
- Retrying (up to
max_recovery_attemptstimes)
USB Transfer Modes
Firmware 3.00+ supports two transfer modes:
| Mode | Description | Default |
|---|---|---|
| Bulk | Lower overhead, less error recovery | Linux default |
| Isochronous | Guaranteed bandwidth, better for real-time | Mac default |
The mode is stored in device flash and can only be changed via Camera Hub. Switching to isochronous has been reported to resolve freezing on some systems.
Sysfs Path
The Facecam's sysfs path follows the pattern:
/sys/bus/usb/devices/<bus>-<port>
Example: /sys/bus/usb/devices/10-2 for bus 10, port 2.
Key sysfs files:
authorized — 0/1, controls device binding
idVendor — 0fd9
idProduct — 0078
bcdDevice — 0409 (firmware version)
speed — 5000 (Mbps)
manufacturer — Elgato
product — Elgato Facecam
App Compatibility
Via Normalized Virtual Camera (Recommended)
When using the daemon with v4l2loopback (/dev/video10), all applications work:
| Application | Status | Notes |
|---|---|---|
| OBS Studio | Works | Select "Facecam Normalized" |
| Chrome/Chromium | Works | exclusive_caps=1 required |
| Firefox | Works | |
| Zoom | Works | |
| Google Meet | Works | Via Chrome |
| Microsoft Teams | Works | Via Chrome |
| Slack | Works | Electron/Chromium |
| Discord | Works | |
| Cheese | Works | |
| mpv | Works | mpv av://v4l2:/dev/video10 |
Direct Device Access (No Daemon)
Without the normalization pipeline, compatibility depends on firmware version:
| Application | Firmware < 4.00 | Firmware 4.00+ |
|---|---|---|
| OBS Studio | Works (UYVY) | Works |
| Firefox | Works | Works |
| Chrome/Chromium | FAILS | Works (MJPEG) |
| Electron apps | FAILS | Works |
| Cheese | Works | Works |
Why Chromium Fails
Chromium's camera enumeration rejects V4L2 devices that report both V4L2_CAP_VIDEO_CAPTURE and V4L2_CAP_VIDEO_OUTPUT capabilities. Additionally, pre-4.00 firmware only offers uncompressed formats which Chromium cannot negotiate.
The v4l2loopback exclusive_caps=1 parameter solves this by making the virtual device report only CAPTURE capability to consumers.
PipeWire / Wayland
On modern Wayland desktops, some applications access cameras through PipeWire's xdg-desktop-portal Camera interface rather than V4L2 directly. The v4l2loopback device is visible through PipeWire's V4L2 backend, so the normalization pipeline remains compatible.
# Verify PipeWire sees the virtual camera
pw-cli list-objects | grep -i facecam
Profiles
Profiles store a named set of camera controls and preferred video mode. They persist as TOML files in ~/.config/facecam/profiles/.
Default Profiles
facecam-ctl profile init # Creates defaults if they don't exist
facecam-ctl profile list
| Profile | Resolution | Format | Description |
|---|---|---|---|
default | 1080p30 | UYVY | Factory defaults, auto exposure/WB |
streaming | 1080p60 | MJPEG | Optimized for live streaming |
lowlight | 720p30 | UYVY | Higher brightness, manual exposure |
meeting | 720p30 | MJPEG | Bandwidth-friendly for video calls |
Profile Format
name = "streaming"
description = "Optimized for live streaming"
[video_mode]
width = 1920
height = 1080
fps = 60
format = "MJPG"
[controls]
brightness = 140
contrast = 4
saturation = 40
sharpness = 2
white_balance_temperature_auto = 1
auto_exposure = 0
power_line_frequency = 2
Creating Custom Profiles
- Create a TOML file in
~/.config/facecam/profiles/:
cat > ~/.config/facecam/profiles/podcast.toml << 'EOF'
name = "podcast"
description = "Podcast recording — warm tones, shallow zoom"
[video_mode]
width = 1920
height = 1080
fps = 30
format = "UYVY"
[controls]
brightness = 135
contrast = 4
saturation = 45
sharpness = 2
white_balance_temperature_auto = 0
white_balance_temperature = 4200
auto_exposure = 0
zoom = 5
EOF
- Apply it:
facecam-ctl profile apply podcast
Profile Application
When a profile is applied:
- Each control in
[controls]is set viaVIDIOC_S_CTRL - Unknown control names are silently skipped
- The daemon updates its active profile name in status
- On recovery (USB reset), the active profile is re-applied automatically
Diagnostics & Support Bundles
Exporting a Bundle
# Via the daemon
facecam-ctl diagnostics
# Standalone (no daemon needed)
facecam-probe diagnostics
Bundles are saved to ~/.local/share/facecam/diagnostics/facecam-diag-<timestamp>.json.
Bundle Contents
{
"generated_at": "2026-04-04T18:54:06Z",
"system": {
"hostname": "workstation",
"kernel_version": "Linux 6.17.0-20-generic ...",
"os_release": "Ubuntu 25.10",
"ubuntu_version": "25.10",
"uptime_secs": 97518
},
"device": {
"product": "Facecam",
"firmware": {"major": 4, "minor": 9},
"serial": "FW06M1A07449",
"usb_bus": 10,
"usb_speed": "Super"
},
"daemon_status": {
"state": "streaming",
"health": "healthy",
"frames_captured": 54000,
"recovery_count": 0
},
"controls": [...],
"kernel_modules": {
"uvcvideo_loaded": true,
"uvcvideo_version": "1.1.1",
"v4l2loopback_loaded": true
},
"v4l2_devices": ["/dev/video0", "/dev/video1", "/dev/video10"],
"config_files": [...]
}
What to Include in Bug Reports
- The diagnostics JSON bundle
- Output of
facecam-probe detect --format json - Output of
facecam-probe quirks --format json dmesg | grep -i "usb\|uvc\|video"(last 50 lines)facecam-harness --json fullreport if the camera is connected
Log Files
The daemon logs to stdout/stderr in JSON format (when running via systemd):
journalctl -u facecam-daemon --since "1 hour ago" --no-pager
Log fields: timestamp, level, message, and structured key-value pairs for every event.
Troubleshooting
Camera Not Detected
Symptom: facecam-probe detect shows no devices.
- Check
lsusb | grep 0fd9— if nothing, the camera isn't powered - Try a different USB port and cable
- Check
dmesg | tail -20for USB errors - Verify the port is USB 3.0:
lsusb -t
USB 2.0 Fallback (PID 0x0077)
Symptom: facecam-probe detect shows USB2 FALLBACK — NOT FUNCTIONAL.
The camera is on a USB 2.0 port or using a USB 2.0 cable.
- Try a different cable — this is the #1 cause
- Move to a blue USB-A port or USB-C/Thunderbolt port
- Avoid USB hubs — connect directly to the motherboard
Black Screen in Applications
Symptom: Camera detected but apps show black video.
- Check format:
v4l2-ctl -d /dev/video0 --get-fmt-video - If using the daemon, verify v4l2loopback is loaded:
ls /dev/video10 - Check daemon status:
facecam-ctl status - Try
facecam-visualto verify the camera produces frames
EBUSY or Device Locked
Symptom: "Device or resource busy" when opening the camera.
Another process has the device open. The open/close lockup quirk may also be triggered.
- Check:
fuser /dev/video0 - Kill the holding process or use the daemon (which owns the device exclusively)
- Force reset:
facecam-ctl resetorfacecam-probewith--devicepointing to the correct node
No v4l2loopback Device
Symptom: /dev/video10 doesn't exist.
sudo modprobe v4l2loopback video_nr=10 card_label="Facecam Normalized" exclusive_caps=1
ls /dev/video10
If modprobe fails, install the DKMS package:
sudo apt-get install v4l2loopback-dkms
Chrome Can't See the Camera
Symptom: Chrome/Chromium doesn't list the camera.
- Use the v4l2loopback virtual camera, not the direct device
- Verify
exclusive_caps=1is set:cat /sys/module/v4l2loopback/parameters/exclusive_caps - The daemon must be actively streaming before Chrome will detect the device
- Try
chrome://settings/content/camerato check permissions
Daemon Enters Failed State
Symptom: facecam-ctl status shows state: failed.
The daemon exhausted its recovery attempts.
facecam-ctl reset # Try a USB reset
facecam-ctl restart # Restart the pipeline
# Or restart the whole service
sudo systemctl restart facecam-daemon
Check logs for the root cause:
journalctl -u facecam-daemon --since "5 min ago"
Poor Frame Rate
Symptom: FPS below expected, stuttering.
- Check USB bandwidth:
facecam-probe topology— verify SuperSpeed - Use MJPEG mode to reduce bandwidth:
facecam-visual --mjpeg - Avoid sharing the USB controller with other devices
- Check CPU usage — UYVY-to-RGB conversion at 1080p60 needs moderate CPU
Building from Source
Prerequisites
# Ubuntu 24.04+
sudo apt-get install -y \
build-essential pkg-config \
libusb-1.0-0-dev \
libxkbcommon-dev libwayland-dev libx11-dev \
v4l2loopback-dkms v4l-utils
# Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Build
git clone https://github.com/facecam-ubuntu/facecam-ubuntu
cd facecam-ubuntu
cargo build --workspace --release
Binaries land in target/release/:
facecam-probefacecam-daemonfacecam-ctlfacecam-harnessfacecam-visual
Workspace Structure
Cargo.toml # Workspace root
crates/
facecam-common/ # Shared library (types, v4l2, USB, quirks, IPC)
facecam-probe/ # Device probe CLI
facecam-daemon/ # Normalization daemon
facecam-ctl/ # Control CLI
facecam-harness/ # Compatibility harness
facecam-visual/ # Visual diagnostic tool
fuzz/
targets/ # AFL++ fuzz harnesses (excluded from workspace)
corpus/ # Seed inputs
config/ # System config files (udev, systemd, modprobe)
docs/ # This mdbook
research/ # Technical research memo
Building the .deb
cargo build --workspace --release
PKG_DIR="facecam-ubuntu_0.1.0_amd64"
mkdir -p "$PKG_DIR"/{DEBIAN,usr/bin,etc/udev/rules.d,etc/modprobe.d,etc/modules-load.d,lib/systemd/system}
cp target/release/facecam-{probe,daemon,ctl,harness,visual} "$PKG_DIR/usr/bin/"
strip "$PKG_DIR/usr/bin/"facecam-*
cp config/99-facecam.rules "$PKG_DIR/etc/udev/rules.d/"
cp config/v4l2loopback.conf "$PKG_DIR/etc/modprobe.d/"
cp config/v4l2loopback-load.conf "$PKG_DIR/etc/modules-load.d/"
cp config/facecam-daemon.service "$PKG_DIR/lib/systemd/system/"
# Create DEBIAN/control, postinst, prerm (see CI workflow for full details)
dpkg-deb --build --root-owner-group "$PKG_DIR"
Building the Docs
cd docs
mdbook build # Output in docs/book/
mdbook serve # Local preview at http://localhost:3000
Fuzzing
The project includes AFL++ fuzz harnesses for all input parsing surfaces.
Targets
| Target | Attack Surface | Input |
|---|---|---|
fuzz_v4l2_querycap | VIDIOC_QUERYCAP response (104 bytes) | Kernel ioctl struct |
fuzz_v4l2_controls | VIDIOC_QUERYCTRL response (68 bytes) | Kernel ioctl struct |
fuzz_ipc_command | DaemonCommand JSON | Unix socket input |
fuzz_profile_parse | Profile TOML | User-writable files |
fuzz_format_fourcc | Pixel format + video mode | USB descriptor data |
fuzz_firmware_bcd | Product ID + BCD version | USB descriptor data |
Running
# Install AFL++ and cargo-afl
sudo apt-get install afl++
cargo install cargo-afl
# Build instrumented targets
cd fuzz/targets
cargo afl build --release
# Generate seed corpus
cd ../..
python3 fuzz/gen_corpus.py
# Run a single fuzzer
cargo afl fuzz -i fuzz/corpus/ipc_parse -o fuzz/findings/ipc_command \
-- fuzz/targets/target/release/fuzz_ipc_command
# Run all fuzzers in parallel
for target in v4l2_querycap ipc_command profile_parse format_fourcc firmware_bcd; do
corpus="fuzz/corpus/v4l2_parse"
[ "$target" = "ipc_command" ] && corpus="fuzz/corpus/ipc_parse"
[ "$target" = "profile_parse" ] && corpus="fuzz/corpus/profile_parse"
[ "$target" = "format_fourcc" ] && corpus="fuzz/corpus/usb_descriptor"
[ "$target" = "firmware_bcd" ] && corpus="fuzz/corpus/usb_descriptor"
timeout 120 cargo afl fuzz -i "$corpus" -o "fuzz/findings/$target" \
-- "fuzz/targets/target/release/fuzz_$target" &
done
wait
Results
Initial fuzzing run (633,000+ executions across all targets):
| Target | Executions | Exec/sec | Crashes | Stability |
|---|---|---|---|---|
| v4l2_querycap | 87,018 | 725/s | 0 | 100% |
| ipc_command | 309,552 | 2,581/s | 0 | 100% |
| profile_parse | 63,621 | 530/s | 0 | 97.4% |
| format_fourcc | 86,538 | 721/s | 0 | 100% |
| firmware_bcd | 86,529 | 721/s | 0 | 100% |
Zero crashes across all targets. Rust's type safety and bounds checking prevent the buffer overflow and integer overflow classes that AFL++ typically finds in C parsers.
Contributing
Getting Started
- Fork the repository
- Clone and build:
cargo build --workspace - Run tests:
cargo test --workspace - Make changes on a feature branch
Code Style
cargo fmtbefore committingcargo clippy -- -D warningsmust pass- No new warnings in any crate
Adding a New Quirk
Observed a new device behavior? Add it to the registry:
- Document the symptoms, trigger, and firmware version in a GitHub issue
- Add the quirk to
crates/facecam-common/src/quirks.rs - Add a test to the harness if the behavior is testable
- Update
docs/src/architecture/quirks.md
Adding a New Control
If a firmware update exposes new V4L2 controls:
- Verify with
v4l2-ctl --list-ctrlsand document the CID, range, default - Add to
v4l2::control_name_to_id()mapping - Update
docs/src/device/controls.md - Update default profiles if the control has useful presets
Pull Request Checklist
-
cargo fmt --all -- --checkpasses -
cargo clippy --workspace -- -D warningspasses -
cargo build --workspace --releasesucceeds - New quirks have traceable observations
- Docs updated for user-facing changes
- Commit messages describe why, not just what
Architecture Notes
- facecam-common is the shared library — put types, parsing, and protocol logic here
- Binary crates should be thin wrappers around common functionality
- V4L2 ioctls use raw byte arrays with verified struct offsets (see
v4l2.rscomments) - No C dependencies in the core library except libc and libusb