package playback import ( "context" "testing" "time" ) func TestMediaStatsStoreObservesVideo(t *testing.T) { store := NewMediaStatsStore() now := time.Date(2026, time.September, 1, 12, 0, 0, 0, time.UTC) store.now = func() time.Time { return now } frame := VideoFrame{ Index: 10, Width: 1920, Height: 1080, Stride: 5120, Size: 5_529_600, Label: "Main video", FrameRateNumerator: 30000, FrameRateDenominator: 1001, } store.ObserveVideo(frame) now = now.Add(40 * time.Millisecond) frame.Index++ frame.Invalid = true store.ObserveVideo(frame) got := store.Snapshot().Video if !got.Available || got.Label != "Main video" || got.Index != 11 { t.Fatalf("video stats = %#v", got) } if got.Width != 1920 || got.Height != 1080 || got.Stride != 5120 { t.Fatalf("video dimensions = %#v", got) } if got.DeclaredFPS < 29.96 || got.DeclaredFPS > 29.98 { t.Fatalf("declared FPS = %v", got.DeclaredFPS) } if got.FrameDT != 40*time.Millisecond || got.Invalid != 1 { t.Fatalf("video timing = %#v", got) } } func TestMediaStatsStoreCalculatesReceivedFPS(t *testing.T) { store := NewMediaStatsStore() now := time.Date(2026, time.September, 1, 12, 0, 0, 0, time.UTC) store.now = func() time.Time { return now } store.ObserveVideo(VideoFrame{}) now = now.Add(500 * time.Millisecond) store.ObserveVideo(VideoFrame{}) now = now.Add(500 * time.Millisecond) store.ObserveVideo(VideoFrame{}) if got := store.Snapshot().Video.ReceivedFPS; got != 3 { t.Fatalf("received FPS = %v, want 3", got) } } func TestMediaStatsStoreObservesAudio(t *testing.T) { store := NewMediaStatsStore() store.ObserveAudio(AudioFrame{ Index: 100, SampleCount: 480, Channels: 2, Label: "Programme audio", SampleRateNumerator: 48_000, SampleRateDenominator: 1, }) got := store.Snapshot().Audio if !got.Available || got.Label != "Programme audio" || got.Index != 100 { t.Fatalf("audio stats = %#v", got) } if got.SampleRateHz != 48_000 || got.Channels != 2 || got.SampleCount != 480 { t.Fatalf("audio format = %#v", got) } if got.BatchDuration != 10*time.Millisecond { t.Fatalf("batch duration = %v, want 10ms", got.BatchDuration) } } type recordingVideoStatsSink struct{ frame VideoFrame } func (s *recordingVideoStatsSink) ConsumeVideo(_ context.Context, frame VideoFrame) error { s.frame = frame return nil } type recordingAudioStatsSink struct{ frame AudioFrame } func (s *recordingAudioStatsSink) ConsumeAudio(_ context.Context, frame AudioFrame) error { s.frame = frame return nil } func TestStatsSinksObserveAndForwardFrames(t *testing.T) { store := NewMediaStatsStore() videoDownstream := &recordingVideoStatsSink{} audioDownstream := &recordingAudioStatsSink{} video := VideoFrame{Index: 7, Label: "video"} audio := AudioFrame{Index: 8, Label: "audio"} if err := (VideoStatsSink{Stats: store, Sink: videoDownstream}).ConsumeVideo(context.Background(), video); err != nil { t.Fatalf("ConsumeVideo() error = %v", err) } if err := (AudioStatsSink{Stats: store, Sink: audioDownstream}).ConsumeAudio(context.Background(), audio); err != nil { t.Fatalf("ConsumeAudio() error = %v", err) } if videoDownstream.frame.Index != video.Index || audioDownstream.frame.Index != audio.Index { t.Fatalf("forwarded frames = %#v, %#v", videoDownstream.frame, audioDownstream.frame) } snapshot := store.Snapshot() if snapshot.Video.Label != "video" || snapshot.Audio.Label != "audio" { t.Fatalf("stats snapshot = %#v", snapshot) } }