102 lines
2.7 KiB
Go
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
|
||
|
|
}
|
||
|
|
}
|