Files
2026-09-09 21:44:05 -05:00

102 lines
2.7 KiB
Go

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
}
}