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 TestLadder(t *testing.T) { vr := src("hevc", 7680, 3840, 50) vr.Video.Spherical = true r := Ladder(vr) if len(r) != 4 || r[0].Name != "3840" || r[0].Height != 1920 || r[0].level != "5.2" || r[3].FPS != 25 || r[0].Label != "2160s" { t.Fatalf("8K 360: %+v", r) } if r[0].codecs != "avc1.640034,mp4a.40.2" { t.Fatalf("codecs %q", r[0].codecs) } r = Ladder(src("h264", 2292, 960, 60)) // a capture: its own size on top, then the steps under it if len(r) != 3 || r[0].Label != "960p" || r[0].Width != 2292 || r[0].FPS != 60 || r[0].Bitrate != 5_000_000 || r[1].Label != "720p" || r[1].Width != 1720 || r[1].FPS != 30 { t.Fatalf("2292×960: %+v", r) } r = Ladder(src("h264", 1920, 1080, 59.94)) if len(r) != 3 || r[0].Label != "1080p" || r[0].FPS != 59.94 || r[1].FPS != 29.97 || r[2].Width != 854 { t.Fatalf("1080p: %+v", r) } if r = Ladder(src("h264", 1080, 1920, 30)); r[0].Width != 1080 || r[0].Height != 1920 { // standing up t.Fatalf("portrait: %+v", r) } if r = Ladder(src("h264", 320, 240, 30)); len(r) != 1 || r[0].Width != 320 || r[0].Label != "240p" { t.Fatalf("tiny: %+v", r) } silent := src("h264", 1280, 720, 30) silent.Audio = nil if r = Ladder(silent); strings.Contains(r[0].codecs, "mp4a") { t.Fatalf("no sound, no AAC: %q", r[0].codecs) } if Ladder(Info{Duration: 10}) != nil { t.Fatal("no picture, no ladder") } if fpsArg(29.97) != "30000/1001" || fpsArg(25) != "25" { t.Fatal(fpsArg(29.97), fpsArg(25)) } } 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 TestRecipes(t *testing.T) { c := gridCaps() in := src("hevc", 3840, 2160, 60) r1080 := Ladder(in)[2] // any device: the card first, decoding on it; then the iGPU the same way; then the CPU got := strings.Join(engineNames(recipes(c, "", in, r1080)), "|") if got != "Intel Arc A380|Intel Arc A380, decoded on the CPU|Intel UHD Graphics 750|Intel UHD Graphics 750, decoded on the CPU|CPU (libx264)" { t.Fatalf("any: %s", got) } // the iGPU asked for: only it, then the CPU as the fallback if got = strings.Join(engineNames(recipes(c, "/dev/dri/renderD128", in, r1080)), "|"); got != "Intel UHD Graphics 750|Intel UHD Graphics 750, decoded on the CPU|CPU (libx264)" { t.Fatalf("iGPU: %s", got) } if got = strings.Join(engineNames(recipes(c, CPU, in, r1080)), "|"); got != "CPU (libx264)" { t.Fatalf("cpu: %s", got) } // an AV1 source: neither card decodes it, so no same-card recipe if got = strings.Join(engineNames(recipes(c, "/dev/dri/renderD129", src("av1", 1920, 1080, 30), r1080)), "|"); got != "Intel Arc A380, decoded on the CPU|CPU (libx264)" { t.Fatalf("av1: %s", got) } // a machine with no GPU and no x264: nothing bare := Caps{FFmpeg: "ffmpeg", FFprobe: "ffprobe", Encoders: []string{"aac"}} if len(recipes(bare, "", in, r1080)) != 0 { t.Fatal("no x264 means nothing") } // 30 fps from 60 for the lower rungs; HDR tone-mapped on both paths r720 := Ladder(in)[3] hdr := src("hevc", 3840, 2160, 60) hdr.Video.ColorTransfer = "smpte2084" rs := recipes(c, "", hdr, r720) if !strings.HasPrefix(rs[0].VF, "fps=30,tonemap_vaapi") || !strings.Contains(rs[1].VF, "tonemap=hable") || !strings.HasSuffix(rs[1].VF, "hwupload") || !strings.Contains(rs[len(rs)-1].VF, "tonemap=hable") { t.Fatalf("hdr: %q … %q … %q", rs[0].VF, rs[1].VF, rs[len(rs)-1].VF) } hdr.Video.ColorTransfer = "arib-std-b67" // HLG: shown as it is, on every path if rs = recipes(c, "", hdr, r720); strings.Contains(rs[0].VF, "tonemap") || strings.Contains(rs[1].VF, "tonemap") { t.Fatalf("hlg: %q … %q", rs[0].VF, rs[1].VF) } // a phone's picture, upright: the ladder by the shown size, turned on the card phone := src("hevc", 1920, 1080, 30) phone.Video.Rotation = 90 lr := Ladder(phone) if lr[0].Width != 1080 || lr[0].Height != 1920 { t.Fatalf("upright: %+v", lr[0]) } if rs = recipes(c, "", phone, lr[0]); !strings.HasPrefix(rs[0].VF, "transpose_vaapi=dir=clock,scale_vaapi=w=1080:h=1920") || rs[0].Pre[0] != "-noautorotate" || strings.Contains(rs[1].VF, "transpose") { t.Fatalf("turned: %q %v / %q", rs[0].VF, rs[0].Pre, rs[1].VF) } // no vainfo: the card is tried all the same nov := gridCaps() nov.GPUs[1].Encodes = nil if rs = recipes(nov, "/dev/dri/renderD129", in, r1080); rs[0].Engine != "Intel Arc A380" || !Devices(nov)[1].OK { t.Fatalf("no vainfo: %v", engineNames(rs)) } if rs := recipes(c, "", in, r1080); strings.Contains(rs[0].VF, "fps=") { t.Fatalf("same rate needs no fps filter: %q", rs[0].VF) } } func TestDevices(t *testing.T) { c := gridCaps() c.Unusable = []GPU{{Device: "/dev/dri/renderD130", Name: "Something", Reason: "permission denied"}} d := Devices(c) if len(d) != 4 || !d[0].OK || !d[1].OK || d[2].OK || d[2].Reason == "" || d[3].ID != CPU || !d[3].OK { t.Fatalf("%+v", d) } if d := Devices(Caps{Error: "ffmpeg is not installed"}); len(d) != 1 || d[0].OK { t.Fatalf("no ffmpeg: %+v", d) } } func TestPieceArgs(t *testing.T) { in := src("h264", 1920, 1080, 60) r := Ladder(in)[0] rc := recipes(gridCaps(), "/dev/dri/renderD129", in, r)[0] a := strings.Join(pieceArgs("/d", "/v.mkv", in, rc, r, 12), " ") for _, want := range []string{"-hwaccel vaapi -hwaccel_device /dev/dri/renderD129", "-ss 36.000 -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 5000k -maxrate 7500k", "-level 4.2", "-c:a aac -b:a 160k -ac 2", "-force_key_frames expr:gte(t,n_forced*3)", "-output_ts_offset 46.000", "-hls_time 2.50", "-start_number 12", "-hls_segment_filename /d/%d.ts"} { if !strings.Contains(a, want) { t.Errorf("missing %q in\n%s", want, a) } } if strings.Contains(a, "-reconnect") { t.Error("a path needs no reconnect") } if a := strings.Join(pieceArgs("/d", "http://10.0.0.234:8100/api/play-src/x", in, rc, r, 0), " "); !strings.Contains(a, "-reconnect 1") { t.Error("a URL reconnects") } in.Audio = nil if a := strings.Join(pieceArgs("/d", "/v", in, rc, r, 0), " "); !strings.Contains(a, "-an") { t.Error("no sound means -an") } } func TestCapsOnly(t *testing.T) { c := gridCaps() if got := c.Only("/dev/dri/renderD129"); len(got.GPUs) != 1 || got.GPUs[0].Name != "Intel Arc A380" || len(c.GPUs) != 2 { t.Fatalf("one card: %+v", got.GPUs) } if len(c.Only(CPU).GPUs) != 0 || len(c.Only("/dev/dri/renderD200").GPUs) != 0 || len(c.Only("").GPUs) != 2 { t.Fatal("cpu, a device not here, any") } // previews then go to that card, or the CPU if g := c.Only("/dev/dri/renderD129").pick("hevc"); g == nil || g.Device != "/dev/dri/renderD129" { t.Fatalf("pick: %+v", g) } }