Streams H.264 to the Galahad II LCD via USB (gousb) with: - ffmpeg CLI transcoding (no fragile FFmpeg C-ABI bindings) - pure-Go Annex-B access-unit splitting - framerate pacing with graceful shutdown and USB reconnect - TOML config, cobra CLI, structured slog logging, unit tests
87 lines
2.0 KiB
Go
87 lines
2.0 KiB
Go
// Package stream plays back pre-encoded H.264 access units to a Device at a
|
|
// constant frame rate. Pacing accounts for the time spent writing so that
|
|
// slow USB transfers do not accumulate drift.
|
|
package stream
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"galahad2lcd/internal/device"
|
|
"galahad2lcd/internal/h264"
|
|
)
|
|
|
|
const defaultFPS = 30.0
|
|
|
|
// LoadPackets reads an Annex-B H.264 file from disk and splits it into
|
|
// access units (one per video frame).
|
|
func LoadPackets(path string) ([][]byte, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read cache %q: %w", path, err)
|
|
}
|
|
return h264.SplitAUs(data), nil
|
|
}
|
|
|
|
// Streamer plays a fixed set of frames in a loop until cancelled or until a
|
|
// write fails.
|
|
type Streamer struct {
|
|
device device.Device
|
|
packets [][]byte
|
|
frameTime time.Duration
|
|
}
|
|
|
|
// New builds a Streamer. speed > 1 slows playback, speed < 1 speeds it up.
|
|
func New(dev device.Device, packets [][]byte, fps, speed float64) *Streamer {
|
|
effective := clampFPS(fps)
|
|
if speed <= 0 {
|
|
speed = 1
|
|
}
|
|
frameTime := time.Duration(float64(time.Second) / effective * speed)
|
|
return &Streamer{device: dev, packets: packets, frameTime: frameTime}
|
|
}
|
|
|
|
// Run streams packets forever until ctx is cancelled or the device fails.
|
|
// A nil error means ctx was cancelled and shutdown is clean.
|
|
func (s *Streamer) Run(ctx context.Context) error {
|
|
if len(s.packets) == 0 {
|
|
return errors.New("no frames to stream")
|
|
}
|
|
|
|
for {
|
|
for _, pkt := range s.packets {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil // clean shutdown
|
|
}
|
|
|
|
start := time.Now()
|
|
if err := s.device.WriteFrame(ctx, pkt); err != nil {
|
|
return fmt.Errorf("write frame: %w", err)
|
|
}
|
|
|
|
elapsed := time.Since(start)
|
|
if wait := s.frameTime - elapsed; wait > 0 {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil
|
|
case <-time.After(wait):
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func clampFPS(fps float64) float64 {
|
|
switch {
|
|
case fps <= 0:
|
|
return defaultFPS
|
|
case fps > 120:
|
|
return 120
|
|
default:
|
|
return fps
|
|
}
|
|
}
|