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
144 lines
3.2 KiB
Go
144 lines
3.2 KiB
Go
package stream
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type fakeDevice struct {
|
|
mu sync.Mutex
|
|
wrote [][]byte
|
|
failAt int // fail after this many writes; <0 = never fail
|
|
failErr error
|
|
// onWrite is invoked after each successful write with the running count.
|
|
onWrite func(n int)
|
|
}
|
|
|
|
func (f *fakeDevice) WriteFrame(_ context.Context, frame []byte) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.failAt >= 0 && f.failAt == len(f.wrote) {
|
|
if f.failErr == nil {
|
|
f.failErr = errors.New("boom")
|
|
}
|
|
return f.failErr
|
|
}
|
|
cp := make([]byte, len(frame))
|
|
copy(cp, frame)
|
|
f.wrote = append(f.wrote, cp)
|
|
if f.onWrite != nil {
|
|
f.onWrite(len(f.wrote))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeDevice) Close() error { return nil }
|
|
|
|
func (f *fakeDevice) count() int {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return len(f.wrote)
|
|
}
|
|
|
|
func TestStreamerPreservesOrder(t *testing.T) {
|
|
frames := [][]byte{{1, 2, 3}, {4, 5}, {6}}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
f := &fakeDevice{
|
|
failAt: -1,
|
|
onWrite: func(n int) {
|
|
if n == len(frames) {
|
|
cancel()
|
|
}
|
|
},
|
|
}
|
|
s := New(f, frames, 1000, 1)
|
|
|
|
if err := s.Run(ctx); err != nil {
|
|
t.Fatalf("Run error = %v", err)
|
|
}
|
|
|
|
if f.count() != 3 {
|
|
t.Fatalf("wrote %d frames, want 3", f.count())
|
|
}
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
for i, want := range frames {
|
|
if len(f.wrote[i]) != len(want) {
|
|
t.Errorf("frame %d length = %d, want %d", i, len(f.wrote[i]), len(want))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStreamerStopsOnCancel(t *testing.T) {
|
|
f := &fakeDevice{failAt: -1}
|
|
s := New(f, [][]byte{{1}}, 100, 1)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
if err := s.Run(ctx); err != nil {
|
|
t.Fatalf("Run error = %v, want nil on clean shutdown", err)
|
|
}
|
|
}
|
|
|
|
func TestStreamerPropagatesDeviceError(t *testing.T) {
|
|
f := &fakeDevice{failAt: 1}
|
|
s := New(f, [][]byte{{1}, {2}}, 1000, 1)
|
|
|
|
ctx := context.Background()
|
|
if err := s.Run(ctx); err == nil {
|
|
t.Fatal("Run returned nil, want device error")
|
|
}
|
|
if f.count() != 1 {
|
|
t.Errorf("wrote %d frames before error, want 1", f.count())
|
|
}
|
|
}
|
|
|
|
func TestStreamerNoFrames(t *testing.T) {
|
|
f := &fakeDevice{failAt: -1}
|
|
s := New(f, nil, 30, 1)
|
|
if err := s.Run(context.Background()); err == nil {
|
|
t.Error("Run returned nil, want error for empty packet list")
|
|
}
|
|
}
|
|
|
|
func TestStreamerPacing(t *testing.T) {
|
|
// 10 fps => 100ms per frame. Two frames should take ~200ms.
|
|
f := &fakeDevice{failAt: -1}
|
|
s := New(f, [][]byte{{1}, {2}}, 10, 1)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
start := time.Now()
|
|
go func() {
|
|
time.Sleep(400 * time.Millisecond)
|
|
cancel()
|
|
}()
|
|
_ = s.Run(ctx)
|
|
elapsed := time.Since(start)
|
|
|
|
// At 10fps each frame takes 100ms; after 400ms we expect ~4 frames.
|
|
if f.count() < 2 {
|
|
t.Errorf("too few frames streamed: %d", f.count())
|
|
}
|
|
if elapsed < 150*time.Millisecond {
|
|
t.Errorf("elapsed = %v, pacing too fast", elapsed)
|
|
}
|
|
}
|
|
|
|
func TestLoadPacketsMissingFile(t *testing.T) {
|
|
if _, err := LoadPackets("/nonexistent/cache.h264"); err == nil {
|
|
t.Error("LoadPackets returned nil error for missing file")
|
|
}
|
|
}
|
|
|
|
func TestClampFPS(t *testing.T) {
|
|
if got := clampFPS(0); got != defaultFPS {
|
|
t.Errorf("clampFPS(0) = %v, want %v", got, defaultFPS)
|
|
}
|
|
if got := clampFPS(300); got != 120 {
|
|
t.Errorf("clampFPS(300) = %v, want 120", got)
|
|
}
|
|
}
|