diff --git a/vidnumerator.go b/vidnumerator.go index 469eb55..7e4290a 100644 --- a/vidnumerator.go +++ b/vidnumerator.go @@ -1,6 +1,7 @@ package vidnumerator import ( + "errors" "fmt" "os" "path/filepath" @@ -90,16 +91,16 @@ func IsVideoCapture(path string) (bool, error) { return ic.isVideoCapture(), nil } -// 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") - if err != nil { - return []string{}, err - } - // iterate over the files in the directory +func shouldSkipDeviceError(err error) bool { + return errors.Is(err, unix.ENOTTY) || + errors.Is(err, unix.EINVAL) || + errors.Is(err, unix.ENODEV) || + errors.Is(err, unix.ENOENT) +} + +func enumeratedVideoDevicesFromEntries(dirPath string, entries []os.DirEntry, isVideoCapture func(string) (bool, error)) ([]string, error) { devNames := []string{} - for _, file := range d { + for _, file := range entries { if file.IsDir() { continue } @@ -107,9 +108,12 @@ func EnumeratedVideoDevices() ([]string, error) { if !strings.HasPrefix(fname, "video") { continue } - fname = filepath.Join("/dev/", fname) - isVidCap, err := IsVideoCapture(fname) + fname = filepath.Join(dirPath, fname) + isVidCap, err := isVideoCapture(fname) if err != nil { + if shouldSkipDeviceError(err) { + continue + } return []string{}, err } if isVidCap { @@ -118,3 +122,13 @@ func EnumeratedVideoDevices() ([]string, error) { } return devNames, nil } + +// EnumeratedVideoDevices lists all /dev/video* nodes that support video capture. +func EnumeratedVideoDevices() ([]string, error) { + entries, err := os.ReadDir("/dev") + if err != nil { + return []string{}, err + } + + return enumeratedVideoDevicesFromEntries("/dev", entries, IsVideoCapture) +} diff --git a/vidnumerator_test.go b/vidnumerator_test.go index 1362da4..d482b6d 100644 --- a/vidnumerator_test.go +++ b/vidnumerator_test.go @@ -1,8 +1,12 @@ package vidnumerator import ( + "errors" + "fmt" "os" "testing" + + "golang.org/x/sys/unix" ) func TestCapQueryFdNilReceiver(t *testing.T) { @@ -130,3 +134,77 @@ func TestCapIsVideoCaptureRequiresCaptureAndStreaming(t *testing.T) { }) } } + +func TestShouldSkipDeviceError(t *testing.T) { + tests := []struct { + name string + err error + want bool + }{ + {name: "enotty", err: unix.ENOTTY, want: true}, + {name: "einval", err: unix.EINVAL, want: true}, + {name: "enodev", err: unix.ENODEV, want: true}, + {name: "enoent", err: unix.ENOENT, want: true}, + {name: "wrapped enotty", err: fmt.Errorf("wrapped: %w", unix.ENOTTY), want: true}, + {name: "permission", err: os.ErrPermission, want: false}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := shouldSkipDeviceError(test.err); got != test.want { + t.Fatalf("shouldSkipDeviceError(%v) = %v, want %v", test.err, got, test.want) + } + }) + } +} + +func TestEnumeratedVideoDevicesFromEntriesSkipsExpectedDeviceErrors(t *testing.T) { + entries := []os.DirEntry{ + fakeDirEntry{name: "video0"}, + fakeDirEntry{name: "video1"}, + fakeDirEntry{name: "video2"}, + fakeDirEntry{name: "not-video"}, + fakeDirEntry{name: "video-dir", dir: true}, + } + + devices, err := enumeratedVideoDevicesFromEntries("/dev", entries, func(path string) (bool, error) { + switch path { + case "/dev/video0": + return true, nil + case "/dev/video1": + return false, unix.ENOTTY + case "/dev/video2": + return false, nil + default: + return false, errors.New("unexpected path") + } + }) + if err != nil { + t.Fatalf("enumeratedVideoDevicesFromEntries() error = %v", err) + } + if len(devices) != 1 || devices[0] != "/dev/video0" { + t.Fatalf("enumeratedVideoDevicesFromEntries() = %v, want [/dev/video0]", devices) + } +} + +func TestEnumeratedVideoDevicesFromEntriesReturnsUnexpectedErrors(t *testing.T) { + entries := []os.DirEntry{fakeDirEntry{name: "video0"}} + expectedErr := errors.New("boom") + + _, err := enumeratedVideoDevicesFromEntries("/dev", entries, func(path string) (bool, error) { + return false, expectedErr + }) + if !errors.Is(err, expectedErr) { + t.Fatalf("enumeratedVideoDevicesFromEntries() error = %v, want %v", err, expectedErr) + } +} + +type fakeDirEntry struct { + name string + dir bool +} + +func (entry fakeDirEntry) Name() string { return entry.name } +func (entry fakeDirEntry) IsDir() bool { return entry.dir } +func (entry fakeDirEntry) Type() os.FileMode { return 0 } +func (entry fakeDirEntry) Info() (os.FileInfo, error) { return nil, nil }