package media import ( "strings" "testing" ) func src(codec string, w, h int, fps float64) Info { return Info{Duration: 100, Video: &Track{Codec: codec, Width: w, Height: h, FPS: fps}, Audio: &Track{Codec: "opus", Channels: 2}} } func TestFit(t *testing.T) { q := func(id string) Quality { x, _ := qualityByID(id); return x } for _, c := range []struct { q string in Info codec string w, h, kbs int }{ {"1080p", src("hevc", 3840, 2160, 60), "h264", 1920, 1080, 10000}, {"original", src("hevc", 3840, 2160, 30), "h264", 3840, 2160, 20000}, {"720p", src("h264", 2292, 960, 60), "h264", 1718, 720, 5000}, // odd width made even {"1080p", src("h264", 1280, 720, 30), "h264", 1280, 720, 4000}, // never upscaled {"480p", src("h264", 1920, 1080, 30), "hevc", 852, 480, 1200}, // 853.3 rounds down, then even {"original", src("av1", 640, 360, 24), "av1", 640, 360, 1200}, } { w, h, k := fit(q(c.q), c.in, c.codec) if w != c.w || h != c.h || k != c.kbs { t.Errorf("%s %s %dx%d@%.0f → %dx%d %dk, want %dx%d %dk", c.q, c.codec, c.in.Video.Width, c.in.Video.Height, c.in.Video.FPS, w, h, k, c.w, c.h, c.kbs) } } } func gridCaps() Caps { return Caps{FFmpeg: "ffmpeg", FFprobe: "ffprobe", Encoders: []string{"aac", "libsvtav1", "libx264", "libx265"}, GPUs: []GPU{ {API: "vaapi", Device: "/dev/dri/renderD128", Name: "Intel UHD Graphics 750", Integrated: true, Codecs: []string{"h264", "hevc", "vp9"}, Encodes: []string{"h264", "hevc"}}, {API: "vaapi", Device: "/dev/dri/renderD129", Name: "Intel Arc A380", Codecs: []string{"h264", "hevc", "vp9"}, Encodes: []string{"av1", "h264", "hevc"}}, }} } func engineNames(r []Recipe) []string { out := make([]string, len(r)) for i := range r { out[i] = r[i].Engine } return out } func TestRecipesOrder(t *testing.T) { c := gridCaps() // h264 from hevc: the card first, decoding on it; then the iGPU the same way; then the CPU got := engineNames(recipes(c, "h264", src("hevc", 3840, 2160, 60), 1920, 1080, 10000)) want := []string{"Intel Arc A380 (vaapi)", "Intel Arc A380 (vaapi, decoded on the CPU)", "Intel UHD Graphics 750 (vaapi)", "Intel UHD Graphics 750 (vaapi, decoded on the CPU)", "CPU (libx264)"} if strings.Join(got, "|") != strings.Join(want, "|") { t.Fatalf("got %v", got) } // av1 source: neither card decodes it, so no same-card recipe, and only the Arc encodes av1 got = engineNames(recipes(c, "av1", src("av1", 1920, 1080, 30), 1920, 1080, 4800)) want = []string{"Intel Arc A380 (vaapi, decoded on the CPU)", "CPU (SVT-AV1)"} if strings.Join(got, "|") != strings.Join(want, "|") { t.Fatalf("av1: got %v", got) } // copy is one recipe, no filter, hvc1 for hevc r := recipes(c, "copy", src("hevc", 1920, 1080, 30), 1920, 1080, 0) if len(r) != 1 || r[0].VF != "" || !has(r[0].Video, "hvc1") || !has(r[0].Video, "copy") { t.Fatalf("copy: %+v", r) } // a machine with no GPU and no x264: nothing for h264, something for av1 bare := Caps{FFmpeg: "ffmpeg", FFprobe: "ffprobe", Encoders: []string{"aac", "libsvtav1"}} if len(recipes(bare, "h264", src("h264", 1280, 720, 30), 1280, 720, 4000)) != 0 { t.Fatal("no x264 means no h264") } if EngineFor(bare, "av1") != "CPU (SVT-AV1)" || EngineFor(bare, "h264") != "" || EngineFor(bare, "copy") != "Original codec" { t.Fatal("engineFor") } if EngineFor(c, "hevc") != "Intel Arc A380 (vaapi)" { t.Fatalf("engineFor on grid: %q", EngineFor(c, "hevc")) } // HDR gets tone-mapped on both paths hdr := src("hevc", 3840, 2160, 60) hdr.Video.ColorTransfer = "smpte2084" r = recipes(c, "h264", hdr, 1920, 1080, 10000) if !strings.HasPrefix(r[0].VF, "tonemap_vaapi") || !strings.Contains(r[len(r)-1].VF, "tonemap=hable") { t.Fatalf("hdr: %q … %q", r[0].VF, r[len(r)-1].VF) } } func TestHLSArgs(t *testing.T) { in := src("h264", 1920, 1080, 60) r := recipes(gridCaps(), "h264", in, 1920, 1080, 10000)[0] a := strings.Join(hlsArgs("/d", "/v.mkv", in, r, false), " ") for _, want := range []string{"-hwaccel vaapi -hwaccel_device /dev/dri/renderD129", "-i /v.mkv", "-map 0:v:0 -map 0:a:0?", "-vf scale_vaapi=w=1920:h=1080:format=nv12", "-c:v h264_vaapi -b:v 10000k -maxrate 12500k", "-c:a aac -b:a 160k -ac 2", "-g 240 -force_key_frames expr:gte(t,n_forced*4)", "-hls_segment_type fmp4", "-hls_segment_filename /d/seg%05d.m4s", "/d/index.m3u8"} { if !strings.Contains(a, want) { t.Errorf("missing %q in\n%s", want, a) } } // aac stays as it is; no audio means none; copy sets no keyframes in.Audio.Codec = "aac" if a := strings.Join(hlsArgs("/d", "/v", in, r, false), " "); !strings.Contains(a, "-c:a copy") { t.Error("aac should be copied") } in.Audio = nil if a := strings.Join(hlsArgs("/d", "/v", in, r, false), " "); !strings.Contains(a, " -an ") { t.Error("no audio stream → -an") } cp := recipes(gridCaps(), "copy", in, 1920, 1080, 0)[0] if a := strings.Join(hlsArgs("/d", "/v", in, cp, true), " "); strings.Contains(a, "force_key_frames") || strings.Contains(a, "-vf") { t.Error("copy must not ask for keyframes or filter") } } func TestSegArgs(t *testing.T) { in := src("h264", 1920, 1080, 25) r := recipes(gridCaps(), "h264", in, 1920, 1080, 10000)[0] first := strings.Join(segArgs("/d", "/v.mkv", in, r, 0, 900, 900, "init.mp4"), " ") for _, want := range []string{"-ss 0 -i /v.mkv", "-output_ts_offset 10", "-start_number 0", "movflags=+frag_discont", "-hls_fmp4_init_filename init.mp4", "-hls_segment_filename /d/seg%05d.m4s", "/d/run.m3u8", "-force_key_frames expr:gte(t,n_forced*4)"} { if !strings.Contains(first, want) { t.Errorf("first run: missing %q in\n%s", want, first) } } if strings.Contains(first, " -t ") { t.Error("a run to the end has no -t") } // a seek: segments 30..44, stopping half a frame short of 45, which is on disk seek := strings.Join(segArgs("/d", "/v.mkv", in, r, 30, 45, 900, "init-run.mp4"), " ") for _, want := range []string{"-ss 120 -i /v.mkv", "-t 59.980", "-output_ts_offset 130", "-start_number 30", "-hls_fmp4_init_filename init-run.mp4"} { if !strings.Contains(seek, want) { t.Errorf("seek run: missing %q in\n%s", want, seek) } } if strings.Index(seek, "-ss 120") > strings.Index(seek, "-i /v.mkv") || strings.Index(seek, "-hwaccel") > strings.Index(seek, "-i /v.mkv") { t.Error("-ss and the decoder's options go before -i") } for d, n := range map[float64]int{9: 3, 8.2: 2, 8.31: 3, 64: 16, 0.1: 1, 3600: 900} { if got := segCount(d); got != n { t.Errorf("segCount(%v) = %d, want %d", d, got, n) } } } func TestParseProgress(t *testing.T) { b := []byte("frame=10\nout_time_us=1000000\nspeed=2.5x\nprogress=continue\nframe=20\nout_time_us=2500000\nspeed=3.1x\nprogress=continue\n") s, sp := parseProgress(b) if s != 2.5 || sp != 3.1 { t.Fatalf("got %v %v", s, sp) } } func TestValidChoice(t *testing.T) { if err := ValidChoice("h264", "720p"); err != nil { t.Fatal(err) } if ValidChoice("vp8", "720p") == nil || ValidChoice("h264", "9p") == nil || ValidChoice("", "") == nil { t.Fatal("bad choices must be refused") } }