8 Commits

Author SHA1 Message Date
4aa3374967 Merge pull request #2 from taigrr/cd/tests-quality-deps
feat: add tests, fix typo, extract constant, bump deps
2026-03-29 02:03:14 -04:00
430e5972d3 feat: add tests, fix typo, extract magic number, bump deps
- Add 6 tests covering nil receiver, invalid fd, invalid path,
  regular file, device enumeration, and capability constant
- Fix typo: fileDesciptor -> fileDescriptor
- Extract magic number 69206017 into V4L2CapVideoCapture constant
- Bump golang.org/x/sys v0.41.0 -> v0.42.0
- Bump Go 1.26.0 -> 1.26.1
- Add staticcheck to CI workflow
2026-03-28 06:31:45 +00:00
7c18d017dc Merge pull request #1 from taigrr/cd/modernize
chore: modernize Go 1.20→1.26, update deps, add CI
2026-02-23 14:42:41 -05:00
2aebf3fc21 ci: add GitHub Actions test workflow 2026-02-23 18:07:27 +00:00
7e00ee78f0 chore(deps): update Go 1.20->1.26, update golang.org/x/sys 2026-02-23 18:07:27 +00:00
9541c767b4 update readme 2025-05-01 18:37:07 -07:00
46bae05250 Update README.md 2024-06-01 19:05:25 -07:00
65d6740d47 Update README.md 2024-06-01 19:04:48 -07:00
7 changed files with 137 additions and 8 deletions

19
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Test
on:
push:
branches: [master]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- run: go test -race ./...
- run: go vet ./...
- run: go build ./...
- name: Install staticcheck
run: go install honnef.co/go/tools/cmd/staticcheck@latest
- run: staticcheck ./...

View File

@@ -1,4 +1,4 @@
Copyright (C) 2023 by Tai Groot <tai@taigrr.com>
Copyright (C) 2023-2025 by Tai Groot <tai@taigrr.com>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.

View File

@@ -4,3 +4,31 @@ This is a tiny library that uses syscalls to efficiently determine which `/dev/v
are webcams and which are the additional metadata control handles.
The list of strings returned are the full filepaths to valid devices.
>[!IMPORTANT]
>In order for this library to work properly, the executing user must have either root or video group privileges
## Technical Details
The library works by:
1. Scanning `/dev` for files matching the pattern `video*`
2. Using the `VIDIOC_QUERYCAP` ioctl to check if each device is a video capture device
3. Filtering out non-capture devices (like metadata control handles)
The core functionality is implemented through direct syscalls to the Linux kernel's V4L2 (Video4Linux2) API. The library uses the `VIDIOC_QUERYCAP` ioctl command to query device capabilities and determine if a device supports video capture.
## Usage
```go
devices, err := vidnumerator.EnumeratedVideoDevices()
if err != nil {
// handle error
}
// devices will contain paths like "/dev/video0", "/dev/video2", etc.
```
## Implementation Notes
- Uses direct syscalls via `golang.org/x/sys/unix`
- Implements custom ioctl constants for V4L2 device querying
- Checks for specific device capabilities (0x4200001) to identify capture devices

4
go.mod
View File

@@ -1,5 +1,5 @@
module github.com/taigrr/vidnumerator
go 1.20
go 1.26.1
require golang.org/x/sys v0.9.0
require golang.org/x/sys v0.42.0

4
go.sum
View File

@@ -1,2 +1,2 @@
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=

View File

@@ -28,6 +28,10 @@ const (
(uintptr('V') << IOCTypeShift) |
(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
)
type cap struct {
@@ -40,13 +44,13 @@ type cap struct {
reserved [3]uint32
}
func (r *cap) QueryFd(fileDesciptor int) error {
func (r *cap) QueryFd(fileDescriptor int) error {
if r == nil {
return fmt.Errorf("nil receiver")
}
_, _, errorNumber := unix.Syscall(
unix.SYS_IOCTL,
uintptr(fileDesciptor),
uintptr(fileDescriptor),
VidIOCQueryCap,
uintptr(unsafe.Pointer(r)),
)
@@ -68,7 +72,7 @@ func IsVideoCapture(path string) (bool, error) {
if err != nil {
return false, err
}
return ic.deviceCaps == 69206017, nil
return ic.deviceCaps == V4L2CapVideoCapture, nil
}
// this function checks the ioctl for VIDIOC_QUERYCAP to see if the device is a video capture device

78
vidnumerator_test.go Normal file
View File

@@ -0,0 +1,78 @@
package vidnumerator
import (
"os"
"testing"
)
func TestCapQueryFdNilReceiver(t *testing.T) {
var nilCap *cap
err := nilCap.QueryFd(0)
if err == nil {
t.Fatal("expected error for nil receiver, got nil")
}
if err.Error() != "nil receiver" {
t.Fatalf("expected 'nil receiver' error, got: %s", err)
}
}
func TestCapQueryFdInvalidFd(t *testing.T) {
ic := cap{}
err := ic.QueryFd(-1)
if err == nil {
t.Fatal("expected error for invalid file descriptor, got nil")
}
}
func TestIsVideoCaptureInvalidPath(t *testing.T) {
isVid, err := IsVideoCapture("/nonexistent/path")
if err == nil {
t.Fatal("expected error for nonexistent path, got nil")
}
if isVid {
t.Fatal("expected false for nonexistent path")
}
}
func TestIsVideoCaptureRegularFile(t *testing.T) {
tmpFile, err := os.CreateTemp("", "vidnum-test-*")
if err != nil {
t.Fatalf("failed to create temp file: %v", err)
}
defer os.Remove(tmpFile.Name())
defer tmpFile.Close()
isVid, err := IsVideoCapture(tmpFile.Name())
if err == nil {
// Some kernels may return an error, some may not — both are valid
if isVid {
t.Fatal("regular file should not be detected as video capture")
}
}
}
func TestEnumeratedVideoDevices(t *testing.T) {
// This test verifies the function runs without panic.
// On machines without video devices, it should return an empty list.
devices, err := EnumeratedVideoDevices()
if err != nil {
// /dev might not be readable in some CI environments
t.Skipf("EnumeratedVideoDevices returned error (expected in some environments): %v", err)
}
// Just verify all returned paths start with /dev/video
for _, device := range devices {
if len(device) < 10 || device[:10] != "/dev/video" {
t.Errorf("unexpected device path: %s", device)
}
}
}
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)
}
}