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