123 lines
3.5 KiB
Go
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
|
|
}
|