package trickplay import ( "encoding/binary" "errors" "testing" ) // buildBIF assembles a file in the shape Emby writes one, so the tests exercise the same // arithmetic the real parser will meet rather than a convenient fiction. func buildBIF(count int, multiplier uint32, frameSizes []int) []byte { length := IndexLength(count) file := make([]byte, length) copy(file, magic) binary.LittleEndian.PutUint32(file[12:16], uint32(count)) binary.LittleEndian.PutUint32(file[16:20], multiplier) offset := length for entry := 0; entry < count; entry++ { at := HeaderSize + entry*entrySize binary.LittleEndian.PutUint32(file[at:at+4], uint32(entry)) binary.LittleEndian.PutUint32(file[at+4:at+8], uint32(offset)) size := 100 if entry < len(frameSizes) { size = frameSizes[entry] } offset += size file = append(file, make([]byte, size)...) } at := HeaderSize + count*entrySize binary.LittleEndian.PutUint32(file[at:at+4], 0xFFFFFFFF) binary.LittleEndian.PutUint32(file[at+4:at+8], uint32(offset)) return file } func TestParseIndexReadsEmbysLayout(t *testing.T) { // Emby 4.10 writes a multiplier of 10000 with timestamps counting 0, 1, 2 — so the // interval is ten seconds, and reading the multiplier as the interval would be right // only by accident. The multiplication is the part worth pinning. file := buildBIF(3, 10_000, []int{500, 600, 700}) index, err := ParseIndex(file) if err != nil { t.Fatalf("ParseIndex: %v", err) } if index.Count != 3 { t.Fatalf("Count = %d, want 3", index.Count) } if index.IntervalMs != 10_000 { t.Fatalf("IntervalMs = %d, want 10000", index.IntervalMs) } start, end, ok := index.Frame(1) if !ok { t.Fatal("Frame(1) not ok") } if want := int64(IndexLength(3) + 500); start != want { t.Fatalf("frame 1 starts at %d, want %d", start, want) } if end-start != 600 { t.Fatalf("frame 1 is %d bytes, want 600", end-start) } } func TestParseIndexAcceptsATitleWithNoThumbnails(t *testing.T) { // This is what Emby serves for a title it has not generated previews for: a // well-formed 72-byte header with a count of zero. It must read as "this title has // none", never as a broken file, or every such title logs an error. file := buildBIF(0, 10_000, nil) index, err := ParseIndex(file) if err != nil { t.Fatalf("ParseIndex: %v", err) } if index.Available() { t.Fatal("a zero-frame BIF reported itself as available") } } func TestParseIndexRejectsWhatIsNotABIF(t *testing.T) { if _, err := ParseIndex([]byte("not found")); !errors.Is(err, ErrNotBIF) && !errors.Is(err, ErrShort) { t.Fatalf("err = %v, want ErrNotBIF or ErrShort", err) } file := buildBIF(2, 10_000, nil) file[3] = 'X' if _, err := ParseIndex(file); !errors.Is(err, ErrNotBIF) { t.Fatalf("err = %v, want ErrNotBIF", err) } } func TestParseIndexRejectsFramesInsideTheIndex(t *testing.T) { // An offset pointing back into the index would have the gateway serve a slice of the // index itself as a JPEG. Refuse the file rather than hand a television nonsense. file := buildBIF(2, 10_000, nil) binary.LittleEndian.PutUint32(file[HeaderSize+4:HeaderSize+8], 8) if _, err := ParseIndex(file); !errors.Is(err, ErrNotBIF) { t.Fatalf("err = %v, want ErrNotBIF", err) } } func TestParseIndexAsksForMoreRatherThanFailing(t *testing.T) { // A caller reads a fixed window off the front of the file, so a long title legitimately // arrives with the index cut short. That must be answerable — fetch more — rather than // looking like a bad file. file := buildBIF(40, 10_000, nil) if _, err := ParseIndex(file[:HeaderSize+16]); !errors.Is(err, ErrShort) { t.Fatalf("err = %v, want ErrShort", err) } if _, _, err := ParseHeader(file[:20]); !errors.Is(err, ErrShort) { t.Fatalf("ParseHeader err = %v, want ErrShort", err) } } func TestFrameAtClampsRatherThanRefusing(t *testing.T) { // The preview is drawn while somebody is still moving a seek target about, so a // position past the last frame must show the last frame. Nothing is worse here than // the thumbnail blanking at exactly the end of a film. index, err := ParseIndex(buildBIF(3, 10_000, nil)) if err != nil { t.Fatalf("ParseIndex: %v", err) } for _, tc := range []struct { positionMs int64 want int }{ {-5_000, 0}, {0, 0}, {9_999, 0}, {10_000, 1}, {25_000, 2}, {9_000_000, 2}, } { if got := index.FrameAt(tc.positionMs); got != tc.want { t.Fatalf("FrameAt(%d) = %d, want %d", tc.positionMs, got, tc.want) } } } func TestFrameRefusesAnIndexOutOfRange(t *testing.T) { index, err := ParseIndex(buildBIF(2, 10_000, nil)) if err != nil { t.Fatalf("ParseIndex: %v", err) } for _, n := range []int{-1, 2, 99} { if _, _, ok := index.Frame(n); ok { t.Fatalf("Frame(%d) was served", n) } } }