tonemap_vaapi refuses HLG, and the hardware path never honoured a rotation (ffmpeg's autorotate is for frames in memory). HDR10 is still tone-mapped, on the card and, failing that, in software before the upload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
158 lines
6.7 KiB
Go
158 lines
6.7 KiB
Go
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")
|
||
}
|
||
}
|