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
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
package transcoder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"galahad2lcd/internal/config"
|
||||
)
|
||||
|
||||
func TestBuildFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
rotate int
|
||||
fps float64
|
||||
want string
|
||||
}{
|
||||
{0, 30, "scale=480:480:flags=lanczos,format=yuv420p"},
|
||||
{90, 30, "scale=480:480:flags=lanczos,transpose=1,format=yuv420p"},
|
||||
{180, 30, "scale=480:480:flags=lanczos,transpose=1,transpose=1,format=yuv420p"},
|
||||
{270, 30, "scale=480:480:flags=lanczos,transpose=2,format=yuv420p"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := buildFilter(tt.rotate, tt.fps); got != tt.want {
|
||||
t.Errorf("buildFilter(%d, %v) = %q, want %q", tt.rotate, tt.fps, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFPSRational(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want float64
|
||||
err bool
|
||||
}{
|
||||
{"30000/1001", 30000.0 / 1001, false},
|
||||
{"25", 25, false},
|
||||
{"0/0", 0, true},
|
||||
{"", 0, true},
|
||||
{"bogus", 0, true},
|
||||
{"10/2", 5, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got, err := parseFPSRational(tt.in)
|
||||
if (err != nil) != tt.err {
|
||||
t.Errorf("parseFPSRational(%q) error = %v, wantErr = %v", tt.in, err, tt.err)
|
||||
continue
|
||||
}
|
||||
if !tt.err && got != tt.want {
|
||||
t.Errorf("parseFPSRational(%q) = %v, want %v", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if got := clampFPS(24); got != 24 {
|
||||
t.Errorf("clampFPS(24) = %v, want 24", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTranscodeIntegration exercises the real ffmpeg path when available.
|
||||
func TestTranscodeIntegration(t *testing.T) {
|
||||
if _, err := exec.LookPath("ffmpeg"); err != nil {
|
||||
t.Skip("ffmpeg not installed")
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
input := filepath.Join(dir, "src.gif")
|
||||
cache := filepath.Join(dir, "out.h264")
|
||||
|
||||
// Build a tiny valid GIF with ffmpeg's testsrc2 source.
|
||||
if err := run(context.Background(), "ffmpeg",
|
||||
"-y", "-v", "error",
|
||||
"-f", "lavfi", "-i", "testsrc2=size=480x480:rate=10:duration=1",
|
||||
input); err != nil {
|
||||
t.Fatalf("generate source gif: %v", err)
|
||||
}
|
||||
|
||||
tc := NewWithBinaries("ffmpeg", "ffprobe")
|
||||
cfg := config.Default()
|
||||
cfg.Display.Input = input
|
||||
cfg.Display.Rotate = 90
|
||||
cfg.Stream.CachePath = cache
|
||||
|
||||
fps, err := tc.Transcode(context.Background(), cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("Transcode error = %v", err)
|
||||
}
|
||||
if fps != 10 {
|
||||
t.Errorf("fps = %v, want 10", fps)
|
||||
}
|
||||
|
||||
st, err := os.Stat(cache)
|
||||
if err != nil {
|
||||
t.Fatalf("cache file missing: %v", err)
|
||||
}
|
||||
if st.Size() == 0 {
|
||||
t.Error("cache file empty")
|
||||
}
|
||||
if !strings.HasPrefix(readFirstBytes(t, cache), "\x00\x00\x00\x01") {
|
||||
t.Error("cache file does not start with an Annex-B start code")
|
||||
}
|
||||
}
|
||||
|
||||
func readFirstBytes(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read cache: %v", err)
|
||||
}
|
||||
if len(data) < 4 {
|
||||
return string(data)
|
||||
}
|
||||
return string(data[:4])
|
||||
}
|
||||
Reference in New Issue
Block a user