Files
galahad2lcd/internal/stream/stream.go
T
Maksim Totmin 4c34f0bedc Initial commit: Go driver for the Lian Li Galahad II LCD
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
2026-08-19 12:03:54 +07:00

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