From 1226e78755bb5d45d5085965b5096fc833d42656 Mon Sep 17 00:00:00 2001 From: Tai Groot Date: Sun, 19 Apr 2026 08:02:19 +0000 Subject: [PATCH] fix(v4l2): detect capture devices by capability bits --- vidnumerator.go | 33 +++++++++++++++------ vidnumerator_test.go | 68 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 85 insertions(+), 16 deletions(-) diff --git a/vidnumerator.go b/vidnumerator.go index 4828726..469eb55 100644 --- a/vidnumerator.go +++ b/vidnumerator.go @@ -29,9 +29,9 @@ const ( (0 << IOCNrShift) | (unsafe.Sizeof(cap{}) << IOCSizeShift) - // V4L2CapVideoCapture is the device capability flag indicating - // the device supports video capture (V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING | V4L2_CAP_DEVICE_CAPS). - V4L2CapVideoCapture uint32 = 69206017 + V4L2CapVideoCapture uint32 = 0x00000001 + V4L2CapStreaming uint32 = 0x04000000 + V4L2CapDeviceCaps uint32 = 0x80000000 ) type cap struct { @@ -60,22 +60,37 @@ func (r *cap) QueryFd(fileDescriptor int) error { return nil } -// this function checks the ioctl for VIDIOC_QUERYCAP to see if the device is a video capture device +func (r cap) videoCaptureCaps() uint32 { + if r.capabilities&V4L2CapDeviceCaps != 0 { + return r.deviceCaps + } + return r.capabilities +} + +func (r cap) isVideoCapture() bool { + caps := r.videoCaptureCaps() + return caps&V4L2CapVideoCapture != 0 && caps&V4L2CapStreaming != 0 +} + +// IsVideoCapture checks the ioctl for VIDIOC_QUERYCAP to see if the device is a video capture device. func IsVideoCapture(path string) (bool, error) { - f, err := os.OpenFile(path, os.O_RDONLY, 0o755) + f, err := os.OpenFile(path, os.O_RDONLY, 0) if err != nil { return false, err } - fd := f.Fd() + defer func() { + _ = f.Close() + }() + ic := cap{} - err = ic.QueryFd(int(fd)) + err = ic.QueryFd(int(f.Fd())) if err != nil { return false, err } - return ic.deviceCaps == V4L2CapVideoCapture, nil + return ic.isVideoCapture(), nil } -// this function checks the ioctl for VIDIOC_QUERYCAP to see if the device is a video capture device +// EnumeratedVideoDevices lists all /dev/video* nodes that support video capture. func EnumeratedVideoDevices() ([]string, error) { // list all files in the /dev directory d, err := os.ReadDir("/dev") diff --git a/vidnumerator_test.go b/vidnumerator_test.go index 871ddb5..1362da4 100644 --- a/vidnumerator_test.go +++ b/vidnumerator_test.go @@ -67,12 +67,66 @@ func TestEnumeratedVideoDevices(t *testing.T) { } } -func TestV4L2CapVideoCaptureConstant(t *testing.T) { - // Verify the constant matches the expected V4L2 capability flags. - // 69206017 = 0x04200001 = V4L2_CAP_VIDEO_CAPTURE (0x1) | V4L2_CAP_STREAMING (0x04000000) | V4L2_CAP_DEVICE_CAPS (0x80000000) - // Note: 69206017 = 0x41F8001 — let's verify the actual hex. - expected := uint32(69206017) - if V4L2CapVideoCapture != expected { - t.Fatalf("V4L2CapVideoCapture = %d, expected %d", V4L2CapVideoCapture, expected) +func TestCapVideoCaptureCapsUsesDeviceCapsWhenPresent(t *testing.T) { + ic := cap{ + capabilities: V4L2CapDeviceCaps, + deviceCaps: V4L2CapVideoCapture | V4L2CapStreaming, + } + + if got := ic.videoCaptureCaps(); got != ic.deviceCaps { + t.Fatalf("videoCaptureCaps() = %#x, want %#x", got, ic.deviceCaps) + } +} + +func TestCapVideoCaptureCapsFallsBackToCapabilities(t *testing.T) { + ic := cap{ + capabilities: V4L2CapVideoCapture | V4L2CapStreaming, + } + + if got := ic.videoCaptureCaps(); got != ic.capabilities { + t.Fatalf("videoCaptureCaps() = %#x, want %#x", got, ic.capabilities) + } +} + +func TestCapIsVideoCaptureAcceptsAdditionalFlags(t *testing.T) { + ic := cap{ + capabilities: V4L2CapDeviceCaps, + deviceCaps: V4L2CapVideoCapture | + V4L2CapStreaming | + 0x00000002, + } + + if !ic.isVideoCapture() { + t.Fatal("expected device with capture and streaming flags to be detected") + } +} + +func TestCapIsVideoCaptureRequiresCaptureAndStreaming(t *testing.T) { + tests := []struct { + name string + ic cap + }{ + { + name: "missing capture", + ic: cap{ + capabilities: V4L2CapDeviceCaps, + deviceCaps: V4L2CapStreaming, + }, + }, + { + name: "missing streaming", + ic: cap{ + capabilities: V4L2CapDeviceCaps, + deviceCaps: V4L2CapVideoCapture, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.ic.isVideoCapture() { + t.Fatal("expected non-capture device") + } + }) } }