mirror of
https://github.com/taigrr/vidnumerator.git
synced 2026-04-02 05:08:52 -07:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4aa3374967 | |||
| 430e5972d3 | |||
| 7c18d017dc | |||
| 2aebf3fc21 | |||
| 7e00ee78f0 | |||
| 9541c767b4 | |||
| 46bae05250 | |||
| 65d6740d47 | |||
| 665ab96cfa |
19
.github/workflows/test.yml
vendored
Normal file
19
.github/workflows/test.yml
vendored
Normal 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 ./...
|
||||||
2
LICENSE
2
LICENSE
@@ -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
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
purpose with or without fee is hereby granted.
|
purpose with or without fee is hereby granted.
|
||||||
|
|||||||
28
README.md
28
README.md
@@ -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.
|
are webcams and which are the additional metadata control handles.
|
||||||
The list of strings returned are the full filepaths to valid devices.
|
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
4
go.mod
@@ -1,5 +1,5 @@
|
|||||||
module github.com/taigrr/vidnumerator
|
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
4
go.sum
@@ -1,2 +1,2 @@
|
|||||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ const (
|
|||||||
(uintptr('V') << IOCTypeShift) |
|
(uintptr('V') << IOCTypeShift) |
|
||||||
(0 << IOCNrShift) |
|
(0 << IOCNrShift) |
|
||||||
(unsafe.Sizeof(cap{}) << IOCSizeShift)
|
(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 {
|
type cap struct {
|
||||||
@@ -40,13 +44,13 @@ type cap struct {
|
|||||||
reserved [3]uint32
|
reserved [3]uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *cap) QueryFd(fileDesciptor int) error {
|
func (r *cap) QueryFd(fileDescriptor int) error {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return fmt.Errorf("nil receiver")
|
return fmt.Errorf("nil receiver")
|
||||||
}
|
}
|
||||||
_, _, errorNumber := unix.Syscall(
|
_, _, errorNumber := unix.Syscall(
|
||||||
unix.SYS_IOCTL,
|
unix.SYS_IOCTL,
|
||||||
uintptr(fileDesciptor),
|
uintptr(fileDescriptor),
|
||||||
VidIOCQueryCap,
|
VidIOCQueryCap,
|
||||||
uintptr(unsafe.Pointer(r)),
|
uintptr(unsafe.Pointer(r)),
|
||||||
)
|
)
|
||||||
@@ -68,15 +72,15 @@ func IsVideoCapture(path string) (bool, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
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
|
// this function checks the ioctl for VIDIOC_QUERYCAP to see if the device is a video capture device
|
||||||
func EnumeratedVideoDevices() []string {
|
func EnumeratedVideoDevices() ([]string, error) {
|
||||||
// list all files in the /dev directory
|
// list all files in the /dev directory
|
||||||
d, err := os.ReadDir("/dev")
|
d, err := os.ReadDir("/dev")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []string{}
|
return []string{}, err
|
||||||
}
|
}
|
||||||
// iterate over the files in the directory
|
// iterate over the files in the directory
|
||||||
devNames := []string{}
|
devNames := []string{}
|
||||||
@@ -89,9 +93,13 @@ func EnumeratedVideoDevices() []string {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fname = filepath.Join("/dev/", fname)
|
fname = filepath.Join("/dev/", fname)
|
||||||
if isVidCap, _ := IsVideoCapture(fname); isVidCap {
|
isVidCap, err := IsVideoCapture(fname)
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, err
|
||||||
|
}
|
||||||
|
if isVidCap {
|
||||||
devNames = append(devNames, fname)
|
devNames = append(devNames, fname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return devNames
|
return devNames, nil
|
||||||
}
|
}
|
||||||
|
|||||||
78
vidnumerator_test.go
Normal file
78
vidnumerator_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user