// 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 } }