Skip to content

feat(auth): fall back to direct V4L2 capture for devices libcamera does not list - #237

Merged
phucvinh57 merged 2 commits into
mainfrom
feat/v4l2-fallback-142
Nov 2, 2026
Merged

phucvinh57 merged 2 commits into
mainfrom
feat/v4l2-fallback-142

Conversation

@phucvinh57

@phucvinh57 phucvinh57 commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

Summary

Since 1.4.0 capture goes through libcamera, which does not list V4L2-only devices: custom DKMS drivers such as gc2607, and v4l2loopback (#142). openpnp-capture used to open any node that reported V4L2_CAP_VIDEO_CAPTURE. This PR adds a direct V4L2 capture session, used only when the configured camera path is not a libcamera device.

Closes #142

Changes

  • auth/face (look at openDevice first)
    • New V4l2CaptureSession (auth/face/common/v4l2_capture.{h,cc}), built on kernel headers only.
    • Setup: QUERYCAP (single-planar streaming capture only), ENUM_FMT/ENUM_FRAMESIZES, S_FMT, 4 mmap buffers, STREAMON, poll() against the capture deadline.
    • Formats: YUYV, MJPG, GREY, RGB3, BGR3, decoded with the existing converters.
    • Behaviour matches the libcamera session: drain stale frames, warmup, brightest-of-4 for IR, skip V4L2_BUF_FLAG_ERROR, treat ENODEV as a disconnect.
    • Every error closes the session and returns null or an empty frame.
    • openDevice (the helper runs as root): open with O_PATH (no driver call), fstat, require a char device with major 81 (VIDEO_MAJOR), then reopen through /proc/self/fd/N with O_RDWR|O_NONBLOCK|O_CLOEXEC|O_NOCTTY and re-check st_rdev. A config path such as /dev/watchdog is never opened.
    • Stepwise and continuous size ranges are clamped to the capped preferred size on the step grid (pickCaptureSizeInRange). v4l2loopback's 8192x8192 range gives 640x480 (1280x720 for MJPEG).
    • openCameraSession: when an explicit path is not found by libcamera, log at info level and try V4L2. Auto-select stays libcamera-only. listCameraFormats falls back the same way.
    • meanLuma moved to capture_policy, kMaxDroppedFrames to frame_sampling.h.
  • docs: docs/IR camera.md mentions the fallback.
  • tests:
    • New v4l2_capture: fourcc mapping, format choice, bytes-per-line fallback, video char device check (watchdog, block device, /dev/null rejected).
    • capture_policy: size range cases, meanLuma.

Verification

$ make build-auth CMAKE_ARGS=-DBUILD_TESTS=ON
build exit=0   (no warnings)
$ ctest --test-dir auth/build
100% tests passed, 0 tests failed out of 31
The following tests did not run:  9 - fs_user (Skipped)
$ clang-format --dry-run --Werror <changed auth files>
clang-format exit=0

$ camera_capture_test /dev/video0          # libcamera device: unchanged path
configured as 1280x720-MJPEG (available: MJPEG, YUYV), Captured 1280x720, no "direct V4L2" line
$ camera_capture_test /dev/video1          # metadata node, not listed by libcamera
is not a libcamera device; trying direct V4L2
is not a V4L2 streaming capture device (caps 0x04a00000)   -> exit 1
$ camera_capture_test /dev/video99
Failed to open camera '/dev/video99': No such file or directory  -> exit 1

A temporary harness (not committed) drove V4l2CaptureSession directly:

Path Result
/dev/video0 colour 1280x720 MJPG, 2 captures
/dev/video2 grey 640x360 GREY, 2 captures, brightest-of-4
/dev/null, a symlink to it, /tmp "is not a V4L2 video device", no session
symlink to /dev/video0 session opened, captured

Manual testing:

Distro / version Package (.deb/.rpm/AUR/source) DE / display manager Hardware (camera, IR, fingerprint) Flow tested (login, sudo, polkit, lock screen, app) Result

Not tested:

Review report

  • Round 1 (fresh-context review against REVIEW.md): 1 important, 2 nits, 0 pre-existing.
    • Important: the root helper opened any user-config path O_RDWR, e.g. /dev/watchdog, which lets a user reboot the machine. Fixed with O_PATH + major 81 + /proc/self/fd reopen.
    • Nit: a continuous range was reduced to its max (8192x8192 on v4l2loopback). Fixed.
    • Nit: a stale errno was logged when REQBUFS granted 0 buffers. Fixed.
  • Round 2 (review of the fix commit): 0 important, 1 nit. The reviewer confirmed there is no race between the check and the open.
    • Nit: --list-formats shows a range as min and max sizes. Dismissed: display only, in the debug tool.
  • Security review: not needed by path (auth/face only). The root open was covered in the code review.

Risk

  • Low: docs, UI copy, styling, tests
  • Medium: app logic, Rust/Tauri commands, face/fingerprint pipeline
  • High: PAM module or helper, install/remove scripts, PAM config, packaging, CI/release workflows

What could break, and for whom: only users whose configured camera is not a libcamera device. They used to fail every time; now they capture through V4L2. The helper runs as root, so it opens V4L2 nodes on their behalf, but only nodes with the video major.

How to revert

git revert is enough. No config or packaging change.

Checklist

  • I read the whole diff and can explain every change.
  • Verification above shows real output, not a claim.
  • All Important review findings are fixed or answered.
  • Docs are updated if behavior, install, packaging, or distro support changed.

🤖 Generated with Claude Code

phucvinh57 and others added 2 commits November 2, 2026 14:22
…es not list

libcamera does not list V4L2-only devices (custom DKMS drivers such as
gc2607, v4l2loopback), so an explicit /dev/videoN for them failed since
1.4.0. When an explicit path is not a libcamera camera, open it with a new
V4l2CaptureSession (kernel headers only: QUERYCAP, ENUM_FMT/FRAMESIZES,
S_FMT, 4 mmap buffers, poll). It reuses the libcamera path's format
preference, size cap, warmup and IR brightest-of-N rules, and fails closed.
Auto-select stays libcamera-only. listCameraFormats gets the same fallback.

meanLuma moves to capture_policy and kMaxDroppedFrames to frame_sampling.h
so both sessions share them.

Refs #142

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The helper runs as root and opens the configured camera path, so the V4L2
fallback could open any node, e.g. /dev/watchdog, whose open has side
effects. openDevice now pins the path with O_PATH, requires a char device
with the V4L2 major (81), reopens that same inode via /proc/self/fd with
O_NOCTTY and checks st_rdev again, so a swapped symlink cannot redirect it.

Stepwise/continuous frame sizes are no longer taken at their max
(v4l2loopback without a producer offers 8192x8192): the format's cap is
clamped into the range on its step grid. REQBUFS granting no buffers is
logged on its own instead of with a stale errno.

Refs #142

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@phucvinh57
phucvinh57 merged commit bf3b1ec into main Nov 2, 2026
3 checks passed
@phucvinh57
phucvinh57 deleted the feat/v4l2-fallback-142 branch November 2, 2026 08:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG or FEATURE]: Camera device selection fails for V4L2 devices exposed through /dev/video*

1 participant