package frame import ( "context" "crypto/rand" "fmt" "log" "time" "gocv.io/x/gocv" ) // maxConsecutiveReadFailures is how many back-to-back failed V4L reads in a // row are tolerated before Capture concludes the device is gone and signals // the caller by closing the output channel. const maxConsecutiveReadFailures = 30 func genGuid() ([]byte, error) { id := make([]byte, 16) if _, err := rand.Read(id); err != nil { return nil, fmt.Errorf("genGuid: crypto/rand failed: %w", err) } id[6] = (id[6] & 0x0f) | 0x40 id[8] = (id[8] & 0x3f) | 0x80 return id, nil } // Capture opens a V4L camera and streams Frame values into output until ctx is // cancelled. fps caps how many frames per second are processed (copied and // forwarded), and requests the same rate from the driver when the driver // honors it; 0 or negative means process at the camera's native rate. width and // height, when > 0, request a specific capture resolution from the driver // (0 = keep the device default). Extra frames are dropped without copying. // If the device stops delivering frames for an extended period (maxConsecutiveReadFailures // consecutive read failures), Capture logs the failure, closes output, and // returns, so a dead camera surfaces to the caller instead of spinning // forever. The caller is responsible for draining output after cancellation or // failure, and must not close output itself. func Capture(ctx context.Context, v4lIndex int, fps int, width, height int, output chan<- Frame) error { cam, err := gocv.VideoCaptureDevice(v4lIndex) if err != nil { return fmt.Errorf("capture device %d: %w", v4lIndex, err) } if width > 0 { cam.Set(gocv.VideoCaptureFrameWidth, float64(width)) } if height > 0 { cam.Set(gocv.VideoCaptureFrameHeight, float64(height)) } // NOTE: VideoCaptureFPS is intentionally NOT set here. On V4L2 (and this // UVC driver in particular) it is ignored for throttling, and setting it // alongside a resolution change makes the device stream at 2-3x its native // rate. The fps cap below (software) is what actually limits frame rate. /* TODO: This code shouldn't have hardware-specific code for only one specific hardware * While the Raspberry Pi Zero W is the default hardware target for the frontend, we should * keep hardware agnostic flexibility in mind. */ minInterval := time.Duration(0) if fps > 0 { minInterval = time.Second / time.Duration(fps) } go func() { defer cam.Close() mat := gocv.NewMat() defer mat.Close() var lastSent time.Time failures := 0 for { select { case <-ctx.Done(): return default: } if !cam.Read(&mat) { failures++ if failures >= maxConsecutiveReadFailures { log.Printf("capture device %d: %d consecutive read failures, closing stream", v4lIndex, failures) close(output) return } time.Sleep(50 * time.Millisecond) continue } failures = 0 now := time.Now() if minInterval > 0 && !lastSent.IsZero() && now.Sub(lastSent) < minInterval { continue } guid, err := genGuid() if err != nil { log.Printf("capture device %d: %v", v4lIndex, err) continue } f := Frame{ PixelBytes: mat.ToBytes(), Width: uint(mat.Cols()), Height: uint(mat.Rows()), GocvImageType: mat.Type(), Detections: make(map[string][]Detection), Channels: mat.Channels(), Guid: guid, Timestamp: uint64(now.UnixNano()), } lastSent = now select { case output <- f: default: } } }() return nil }