package frame import ( "fmt" "sync" "time" "gocv.io/x/gocv" ) // isCaptureDevice returns true if idx is a real video capture device // (not a video output, loopback, tuner, etc.) by checking for a non-zero // frame size — only capture devices report this before streaming. func isCaptureDevice(idx int) bool { if idx < 0 { return false } cam, err := gocv.VideoCaptureDevice(idx) if err != nil { return false } defer cam.Close() return cam.Get(gocv.VideoCaptureFrameWidth) > 0 } // Scan discovers working V4L camera indices in [min, max]. // Only real video capture devices are reported — non-capture V4L2 devices // (output, loopback, tuners) are filtered out. Each device is opened and // immediately closed; handles are never leaked. func Scan(min, max int) ([]int, error) { return scanRange(min, max, 1, nil) } // ScanSkip is Scan restricted to every skip-th index in [min, max], so a // rebalance pass can re-probe the bus without hammering every device. func ScanSkip(min, max, skip int) ([]int, error) { return scanRange(min, max, skip, nil) } // ScanExcluding is Scan that never opens or reports the given indices, so a // hotplug loop can re-probe the bus without touching devices it is already // streaming from — a busy V4L handle can transiently report a zero size and // flap the rebalance into a teardown/restart loop. func ScanExcluding(min, max int, exclude map[int]struct{}) ([]int, error) { return scanRange(min, max, 1, exclude) } // ScanSkipExcluding is ScanSkip with an exclusion set, see ScanExcluding. func ScanSkipExcluding(min, max, skip int, exclude map[int]struct{}) ([]int, error) { return scanRange(min, max, skip, exclude) } func scanRange(min, max, skip int, exclude map[int]struct{}) ([]int, error) { if min > max { return nil, fmt.Errorf("empty range [%d, %d]", min, max) } if skip < 1 { skip = 1 } var indices []int var mu sync.Mutex var wg sync.WaitGroup for i := min; i <= max; i += skip { if _, excluded := exclude[i]; excluded { continue } wg.Add(1) go func(idx int) { defer wg.Done() if isCaptureDevice(idx) { mu.Lock() indices = append(indices, idx) mu.Unlock() } }(i) } wg.Wait() if len(indices) == 0 { return nil, fmt.Errorf("no cameras found in range [%d, %d]", min, max) } return indices, nil } // WaitGroupTimeout waits up to timeout for wg to reach zero, returning false // if it times out so shutdown paths can bound how long they stall on workers // that are blocked in a downstream retry. func WaitGroupTimeout(wg *sync.WaitGroup, timeout time.Duration) bool { done := make(chan struct{}) go func() { wg.Wait() close(done) }() select { case <-done: return true case <-time.After(timeout): return false } }