Files
oko_public/frame/cameraFunctions.go
2026-09-09 21:44:05 -05:00

123 lines
3.5 KiB
Go

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
}