Files
galahad2lcd/internal/h264/h264_test.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

204 lines
5.1 KiB
Go

package h264
import (
"bytes"
"os"
"path/filepath"
"testing"
)
// streamNALs builds an Annex-B byte stream from a list of NAL unit types.
// Each NAL gets a minimal one-byte payload so the type is easy to assert.
func streamNALs(types ...byte) []byte {
var out []byte
for _, t := range types {
out = append(out, 0x00, 0x00, 0x01) // 3-byte start code
out = append(out, 0x60|t) // nal_ref_idc=3, type in low bits
out = append(out, 0x00)
}
return out
}
// firstNALType extracts the type of the first NAL in a byte slice.
func firstNALType(au []byte) byte {
if len(au) < 4 {
return 0
}
// Skip the start code: 3 or 4 bytes.
pay := 3
if au[0] == 0 && au[1] == 0 && au[2] == 0 {
pay = 4
}
return au[pay] & 0x1F
}
func typesOf(aus [][]byte) [][]byte {
var types [][]byte
for _, au := range aus {
var ts []byte
rest := au
for len(rest) > 0 {
ts = append(ts, firstNALType(rest))
// advance past this NAL (start code + payload of >=1 byte)
skip := 4
if rest[0] == 0 && rest[1] == 0 && rest[2] == 0 {
skip = 5
}
// find next start code
next := bytes.Index(rest[skip:], []byte{0x00, 0x00, 0x01})
if next < 0 {
break
}
rest = rest[skip+next:]
}
types = append(types, ts)
}
return types
}
func TestSplitAUsEmpty(t *testing.T) {
if got := SplitAUs(nil); got != nil {
t.Errorf("SplitAUs(nil) = %v, want nil", got)
}
}
func TestSplitAUsNoStartCode(t *testing.T) {
data := []byte{0x67, 0x42, 0x00}
got := SplitAUs(data)
if len(got) != 1 || !bytes.Equal(got[0], data) {
t.Errorf("SplitAUs passthrough = %v, want [% x]", got, data)
}
}
func TestSplitAUsKeyframeThenP(t *testing.T) {
// SPS(7), PPS(8), IDR(5), P(1), P(1)
stream := streamNALs(7, 8, 5, 1, 1)
aus := SplitAUs(stream)
if len(aus) != 3 {
t.Fatalf("got %d AUs, want 3", len(aus))
}
types := typesOf(aus)
want := [][]byte{{7, 8, 5}, {1}, {1}}
for i := range want {
if !bytes.Equal(types[i], want[i]) {
t.Errorf("AU %d types = %v, want %v", i, types[i], want[i])
}
}
}
func TestSplitAUsWithAUD(t *testing.T) {
// AUD(9), IDR(5), P(1)
stream := streamNALs(9, 5, 1)
aus := SplitAUs(stream)
if len(aus) != 2 {
t.Fatalf("got %d AUs, want 2", len(aus))
}
types := typesOf(aus)
want := [][]byte{{9, 5}, {1}}
for i := range want {
if !bytes.Equal(types[i], want[i]) {
t.Errorf("AU %d types = %v, want %v", i, types[i], want[i])
}
}
}
func TestSplitAUsAUDThenPrefix(t *testing.T) {
// AUD(9), SPS(7), PPS(8), IDR(5), P(1)
stream := streamNALs(9, 7, 8, 5, 1)
aus := SplitAUs(stream)
if len(aus) != 2 {
t.Fatalf("got %d AUs, want 2", len(aus))
}
types := typesOf(aus)
want := [][]byte{{9, 7, 8, 5}, {1}}
for i := range want {
if !bytes.Equal(types[i], want[i]) {
t.Errorf("AU %d types = %v, want %v", i, types[i], want[i])
}
}
}
func TestSplitAUsFourByteCodes(t *testing.T) {
var stream []byte
for _, t := range []byte{7, 8, 5, 1} {
stream = append(stream, 0x00, 0x00, 0x00, 0x01) // 4-byte
stream = append(stream, 0x60|t, 0x00)
}
aus := SplitAUs(stream)
if len(aus) != 2 {
t.Fatalf("got %d AUs, want 2", len(aus))
}
}
func TestSplitAUsMixedCodes(t *testing.T) {
var stream []byte
stream = append(stream, 0x00, 0x00, 0x01, 0x67, 0x00) // 3-byte SPS
stream = append(stream, 0x00, 0x00, 0x00, 0x01, 0x68, 0x00) // 4-byte PPS
stream = append(stream, 0x00, 0x00, 0x00, 0x01, 0x65, 0x00) // 4-byte IDR
stream = append(stream, 0x00, 0x00, 0x01, 0x41, 0x00) // 3-byte P
aus := SplitAUs(stream)
if len(aus) != 2 {
t.Fatalf("got %d AUs, want 2", len(aus))
}
types := typesOf(aus)
want := [][]byte{{7, 8, 5}, {1}}
for i := range want {
if !bytes.Equal(types[i], want[i]) {
t.Errorf("AU %d types = %v, want %v", i, types[i], want[i])
}
}
}
func TestSplitAUsReassemblesStream(t *testing.T) {
stream := streamNALs(7, 8, 5, 1, 1, 7, 8, 5, 1)
aus := SplitAUs(stream)
var rebuilt []byte
for _, au := range aus {
rebuilt = append(rebuilt, au...)
}
if !bytes.Equal(rebuilt, stream) {
t.Error("reassembled stream differs from input")
}
}
// TestSplitAUsRealFile validates against a real libx264 Annex-B stream.
func TestSplitAUsRealFile(t *testing.T) {
path := filepath.Join("..", "..", "testdata", "sample.h264")
data, err := os.ReadFile(path)
if err != nil {
t.Skipf("testdata not available: %v", err)
}
aus := SplitAUs(data)
if len(aus) < 2 {
t.Fatalf("got %d AUs, want >= 2", len(aus))
}
// First AU must be a keyframe: SPS + PPS + optional prefix NALs (SEI)
// followed by an IDR slice.
first := typesOf(aus[:1])[0]
if len(first) < 3 || first[0] != 7 || first[1] != 8 || first[len(first)-1] != 5 {
t.Errorf("first AU types = %v, want prefix [7 8 ... 5]", first)
}
// All AUs must be non-empty and start with a valid start code.
for i, au := range aus {
if len(au) < 4 {
t.Fatalf("AU %d too short: %d bytes", i, len(au))
}
if !(au[0] == 0 && au[1] == 0 && (au[2] == 0 || au[2] == 1)) {
t.Errorf("AU %d does not start with a start code: % x", i, au[:4])
}
}
// Reassembly must reproduce the input exactly.
var rebuilt []byte
for _, au := range aus {
rebuilt = append(rebuilt, au...)
}
if !bytes.Equal(rebuilt, data) {
t.Error("reassembled stream differs from input")
}
}