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

ComponentPurpose
facecam-probeDetect, fingerprint, and enumerate the camera
facecam-daemonCapture from device, output to v4l2loopback virtual camera
facecam-ctlControl the daemon: profiles, controls, diagnostics
facecam-harnessAutomated compatibility and stability testing
facecam-visualLive 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

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:0078 on an even-numbered bus (002, 004, 006...) = USB 3.0, correct
  • 0fd9:0077 on 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

ModeFormatBandwidth
1080p60UYVY~249 MB/s
1080p30UYVY~124 MB/s
720p60UYVY~111 MB/s
720p30UYVY~55 MB/s
1080p60MJPEG~5-15 MB/s (variable)
720p30MJPEG~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:

  1. Write 0 to /sys/bus/usb/devices/<dev>/authorized (deauthorize)
  2. Wait 500ms for kernel driver unbind
  3. Write 1 to reauthorize (triggers re-enumeration)
  4. Wait 1500ms for device stabilization
  5. 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
StateDescription
IdleNo device detected, waiting
ProbingDevice found, reading capabilities and controls
StartingFormat set, MMAP buffers allocated, about to stream
StreamingActive frame forwarding
RecoveringError detected, performing USB reset
FailedMax recovery attempts exceeded
ShuttingDownGraceful exit in progress

Recovery Logic

On any capture error:

  1. Increment recovery counter
  2. If counter > max_recovery_attempts (default 5): enter Failed state
  3. Perform USB sysfs reset
  4. Wait for device re-enumeration (2s)
  5. Re-open device, re-apply format and controls
  6. 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:

  1. Load the active profile's preferred format/resolution
  2. Filter to formats marked reliable in the quirk registry (UYVY, MJPEG)
  3. Match the closest available mode
  4. 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 device
  • frames_written — total frames written to v4l2loopback
  • frames_dropped — frames lost due to sink write failures
  • recovery_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

IDSeveritySummary
BOGUS_NV12ErrorNV12 format advertised but produces garbage frames
BOGUS_YU12ErrorYU12 format advertised but produces garbage frames
OPEN_CLOSE_LOCKUPCriticalDevice locks up after consumer close/reopen
STARTUP_UNRELIABILITYError~50% failure rate on initial stream start
NO_MJPEG_OLD_FWWarningNo MJPEG on firmware < 4.00
CHROMIUM_FORMAT_REJECTErrorChrome rejects devices with both CAPTURE+OUTPUT caps
USB2_FALLBACK_MODECriticalPID changes to 0x0077 on USB 2.0, no video
USB3_REQUIREDCriticalDevice requires USB 3.0 SuperSpeed
BANDWIDTH_STARVATIONWarningHub sharing causes drops at ~249 MB/s
YUYV_UYVY_AMBIGUITYInfoWire 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:

  1. Document the exact symptoms (error codes, frame data, timing)
  2. Identify the trigger conditions (firmware version, format, USB topology)
  3. Determine a mitigation
  4. Add to quirks::quirk_registry() in crates/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

FlagDefaultDescription
--sourceauto-detectPhysical camera device path
--sink/dev/video10v4l2loopback output device
--profiledefaultProfile to apply on startup
--foregroundoffDon't daemonize
--log-formatjsonLog format (json for production, text for debug)
--config~/.config/facecam/daemon.tomlConfig 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:

TestWhat it checks
device_detectionUSB enumeration finds Facecam with correct PID
format_enumerationV4L2 formats are listed, bogus ones flagged
control_enumerationAll expected controls are present
format_negotiationEach format can be set via VIDIOC_S_FMT
open_close_cyclesDevice survives N open/close cycles
control_roundtripSet/get control values match
usb_topologyDevice is on USB 3.0+
kernel_modulesuvcvideo loaded, v4l2loopback status
stream_stabilityDevice remains responsive over time
usb_recoveryUSB 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

KeyAction
+ / -Brightness up/down (step 10)
[ / ]Contrast up/down (step 1)
Z / XZoom in/out (step 1)

Diagnostic Overlays

KeyFeatureDescription
WZebra stripesRed diagonal hatching on pixels with luma > 235 (overexposure)
EFocus peakingMagenta dots on sharp edges (Sobel edge detection)
AA/B captureFreezes current frame as reference (left side)
DA/B clearRemoves the reference frame
< / >A/B splitMoves the comparison split line

General

KeyAction
SSave snapshot (PPM format with overlays)
RForce USB reset
SpacePause/unpause
FToggle help text
Q / EscQuit

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

FieldValue
Vendor ID0x0fd9 (Elgato Systems GmbH)
Product ID0x0078 (normal) / 0x0077 (USB2 fallback)
UVC Version1.10
SensorSony STARVIS CMOS
LensElgato Prime Lens, all-glass, f/2.4, 24mm equivalent
FocusFixed (no autofocus)
Max Resolution1920x1080
FOV82 degrees diagonal
USB3.0 SuperSpeed (mandatory)
PowerBus-powered, 304mA max
InterfacesUVC (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)

FormatResolutionsFrame Rates
UYVY 4:2:21920x1080, 1280x720, 960x54060, 30
MJPEG1920x1080, 1280x720, 960x54060, 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

FirmwareKey ChangesLinux Impact
2.00Initial releaseOnly uncompressed formats; format bugs
2.52Fixed settings save bugsMinor
3.00Bulk/iso transfer mode; improved ISPTransfer mode selectable
4.00+MJPEG format addedChromium compatibility fixed
4.09Latest; minor refinementsConfirmed 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:

  1. Borrow a Windows or Mac machine
  2. Install Camera Hub
  3. Connect the Facecam and follow the update prompt
  4. Verify on Linux: facecam-probe detect should show 4.09

Firmware-Gated Features

FeatureMinimum Firmware
MJPEG format4.00
Bulk/iso transfer mode selection3.00
Reliable settings save2.52

V4L2 Controls

Available Controls (Firmware 4.09)

Verified from live device testing:

ControlCIDTypeRangeDefaultNotes
Brightness0x00980900int0-255128Processing Unit
Contrast0x00980901int0-103Processing Unit
Saturation0x00980902int0-6335Processing Unit
Sharpness0x0098091bint0-42Processing Unit
WB Auto0x0098090cbool0-11Disables WB Temp when on
WB Temperature0x0098091aint2800-125005000Step 100, Kelvin
Power Line Freq0x00980918menu0-220=Off, 1=50Hz, 2=60Hz
Auto Exposure0x009a0901menu0-300=Auto, 2=Shutter Priority
Exposure Time0x009a0902int1-2500156Units of 100us
Zoom Absolute0x009a090dint1-311Digital zoom/crop

Exposure Time Values

The exposure time is in units of 100 microseconds:

ValueShutter SpeedUse Case
11/10000sBright daylight
101/1000sWell-lit room
781/128sIndoor
1561/64sDefault
5001/20sLow light
25001/4sVery dark (motion blur)

White Balance Temperature

KelvinLight Source
2800Candlelight
3200Tungsten / warm white
4000Fluorescent
5000Default / neutral daylight
6500Overcast / cool daylight
10000Blue sky
12500Maximum (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:

SpeedPIDProduct StringVideo
USB 3.0 SuperSpeed0x0078Elgato FacecamFull UVC
USB 2.0 High-Speed0x0077USB3-REQUIRED-FOR-FACECAMNone

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:

  1. Attempting the operation
  2. On failure, performing USB reset
  3. Waiting for re-enumeration
  4. Retrying (up to max_recovery_attempts times)

USB Transfer Modes

Firmware 3.00+ supports two transfer modes:

ModeDescriptionDefault
BulkLower overhead, less error recoveryLinux default
IsochronousGuaranteed bandwidth, better for real-timeMac 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

When using the daemon with v4l2loopback (/dev/video10), all applications work:

ApplicationStatusNotes
OBS StudioWorksSelect "Facecam Normalized"
Chrome/ChromiumWorksexclusive_caps=1 required
FirefoxWorks
ZoomWorks
Google MeetWorksVia Chrome
Microsoft TeamsWorksVia Chrome
SlackWorksElectron/Chromium
DiscordWorks
CheeseWorks
mpvWorksmpv av://v4l2:/dev/video10

Direct Device Access (No Daemon)

Without the normalization pipeline, compatibility depends on firmware version:

ApplicationFirmware < 4.00Firmware 4.00+
OBS StudioWorks (UYVY)Works
FirefoxWorksWorks
Chrome/ChromiumFAILSWorks (MJPEG)
Electron appsFAILSWorks
CheeseWorksWorks

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
ProfileResolutionFormatDescription
default1080p30UYVYFactory defaults, auto exposure/WB
streaming1080p60MJPEGOptimized for live streaming
lowlight720p30UYVYHigher brightness, manual exposure
meeting720p30MJPEGBandwidth-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

  1. 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
  1. Apply it:
facecam-ctl profile apply podcast

Profile Application

When a profile is applied:

  1. Each control in [controls] is set via VIDIOC_S_CTRL
  2. Unknown control names are silently skipped
  3. The daemon updates its active profile name in status
  4. 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

  1. The diagnostics JSON bundle
  2. Output of facecam-probe detect --format json
  3. Output of facecam-probe quirks --format json
  4. dmesg | grep -i "usb\|uvc\|video" (last 50 lines)
  5. facecam-harness --json full report 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.

  1. Check lsusb | grep 0fd9 — if nothing, the camera isn't powered
  2. Try a different USB port and cable
  3. Check dmesg | tail -20 for USB errors
  4. 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.

  1. Try a different cable — this is the #1 cause
  2. Move to a blue USB-A port or USB-C/Thunderbolt port
  3. Avoid USB hubs — connect directly to the motherboard

Black Screen in Applications

Symptom: Camera detected but apps show black video.

  1. Check format: v4l2-ctl -d /dev/video0 --get-fmt-video
  2. If using the daemon, verify v4l2loopback is loaded: ls /dev/video10
  3. Check daemon status: facecam-ctl status
  4. Try facecam-visual to 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.

  1. Check: fuser /dev/video0
  2. Kill the holding process or use the daemon (which owns the device exclusively)
  3. Force reset: facecam-ctl reset or facecam-probe with --device pointing 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.

  1. Use the v4l2loopback virtual camera, not the direct device
  2. Verify exclusive_caps=1 is set: cat /sys/module/v4l2loopback/parameters/exclusive_caps
  3. The daemon must be actively streaming before Chrome will detect the device
  4. Try chrome://settings/content/camera to 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.

  1. Check USB bandwidth: facecam-probe topology — verify SuperSpeed
  2. Use MJPEG mode to reduce bandwidth: facecam-visual --mjpeg
  3. Avoid sharing the USB controller with other devices
  4. 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-probe
  • facecam-daemon
  • facecam-ctl
  • facecam-harness
  • facecam-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

TargetAttack SurfaceInput
fuzz_v4l2_querycapVIDIOC_QUERYCAP response (104 bytes)Kernel ioctl struct
fuzz_v4l2_controlsVIDIOC_QUERYCTRL response (68 bytes)Kernel ioctl struct
fuzz_ipc_commandDaemonCommand JSONUnix socket input
fuzz_profile_parseProfile TOMLUser-writable files
fuzz_format_fourccPixel format + video modeUSB descriptor data
fuzz_firmware_bcdProduct ID + BCD versionUSB 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):

TargetExecutionsExec/secCrashesStability
v4l2_querycap87,018725/s0100%
ipc_command309,5522,581/s0100%
profile_parse63,621530/s097.4%
format_fourcc86,538721/s0100%
firmware_bcd86,529721/s0100%

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

  1. Fork the repository
  2. Clone and build: cargo build --workspace
  3. Run tests: cargo test --workspace
  4. Make changes on a feature branch

Code Style

  • cargo fmt before committing
  • cargo clippy -- -D warnings must pass
  • No new warnings in any crate

Adding a New Quirk

Observed a new device behavior? Add it to the registry:

  1. Document the symptoms, trigger, and firmware version in a GitHub issue
  2. Add the quirk to crates/facecam-common/src/quirks.rs
  3. Add a test to the harness if the behavior is testable
  4. Update docs/src/architecture/quirks.md

Adding a New Control

If a firmware update exposes new V4L2 controls:

  1. Verify with v4l2-ctl --list-ctrls and document the CID, range, default
  2. Add to v4l2::control_name_to_id() mapping
  3. Update docs/src/device/controls.md
  4. Update default profiles if the control has useful presets

Pull Request Checklist

  • cargo fmt --all -- --check passes
  • cargo clippy --workspace -- -D warnings passes
  • cargo build --workspace --release succeeds
  • 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.rs comments)
  • No C dependencies in the core library except libc and libusb