diff --git a/internal/media/encode.go b/internal/media/encode.go index 886f5c9..8cc447b 100644 --- a/internal/media/encode.go +++ b/internal/media/encode.go @@ -66,9 +66,10 @@ func Ladder(i Info) []Rung { } vr := v.Spherical steps := flatSteps - src := min(v.Width, v.Height) // a flat picture is named by its short side + w, h := v.Shown() + src := min(w, h) // a flat picture is named by its short side if vr { - steps, src = vrSteps, v.Width + steps, src = vrSteps, w } // A picture between two steps (a 2292×960 capture) gets its own size on top, at the bit // rate of the step above, so the best on offer is never less than the file. @@ -94,14 +95,15 @@ func Ladder(i Info) []Rung { func rung(i Info, s step) Rung { v := i.Video + sw, sh := v.Shown() var w, h int switch { case v.Spherical: - w, h = s.size, even(float64(s.size)*float64(v.Height)/float64(v.Width)) - case v.Width >= v.Height: - h, w = s.size, even(float64(s.size)*float64(v.Width)/float64(v.Height)) + w, h = s.size, even(float64(s.size)*float64(sh)/float64(sw)) + case sw >= sh: + h, w = s.size, even(float64(s.size)*float64(sw)/float64(sh)) default: // standing up - w, h = s.size, even(float64(s.size)*float64(v.Height)/float64(v.Width)) + w, h = s.size, even(float64(s.size)*float64(sh)/float64(sw)) } fps := v.FPS if fps <= 0 { @@ -193,7 +195,7 @@ func Devices(c Caps) []Device { switch { case g.API != "vaapi": d.Reason = "only VAAPI devices encode here" - case !has(g.Encodes, "h264"): + case g.Encodes != nil && !has(g.Encodes, "h264"): d.Reason = "doesn't encode H.264" default: d.OK = true @@ -232,7 +234,9 @@ func recipes(c Caps, device string, src Info, r Rung) []Recipe { if device != CPU { for i := range gpus { g := &gpus[i] - if g.API != "vaapi" || !has(g.Encodes, "h264") || (device != "" && g.Device != device) { + // Encodes is vainfo's answer; without vainfo there is none, and the card is tried + // anyway, with the next recipe behind it. + if g.API != "vaapi" || (g.Encodes != nil && !has(g.Encodes, "h264")) || (device != "" && g.Device != device) { continue } video := append(append([]string{"-c:v", "h264_vaapi"}, rate...), "-bf", "0") @@ -240,28 +244,56 @@ func recipes(c Caps, device string, src Info, r Rung) []Recipe { if name == "" { name = g.Device } - scale := fmt.Sprintf("scale_vaapi=w=%d:h=%d:format=nv12", r.Width, r.Height) + // The card doesn't turn a phone's picture by itself (ffmpeg's own turning is for + // pictures in memory), so it is told to, and ffmpeg is told not to. + scale := turn(src) + fmt.Sprintf("scale_vaapi=w=%d:h=%d:format=nv12", r.Width, r.Height) + // HDR10 (PQ) is tone-mapped on the card, which needs the file's mastering data; + // HLG is made to look right on an ordinary screen as it is, so it is only scaled + // (tonemap_vaapi refuses it). Either way the next recipe tone-maps in software. if src.Video != nil && has(g.Codecs, src.Video.Codec) { vf := fps + scale - if src.HDR() { + if src.PQ() { vf = fps + "tonemap_vaapi=format=nv12:t=bt709:p=bt709:m=bt709," + scale } - out = append(out, Recipe{Engine: name, Device: g.Device, Pre: []string{"-hwaccel", "vaapi", "-hwaccel_device", g.Device, "-hwaccel_output_format", "vaapi"}, VF: vf, Video: video}) + out = append(out, Recipe{Engine: name, Device: g.Device, Pre: []string{"-noautorotate", "-hwaccel", "vaapi", "-hwaccel_device", g.Device, "-hwaccel_output_format", "vaapi"}, VF: vf, Video: video}) + } + cpuVF := fmt.Sprintf("%sformat=nv12,scale=%d:%d,hwupload", fps, r.Width, r.Height) + if src.PQ() { + cpuVF = fps + toneMap + fmt.Sprintf(",scale=%d:%d,format=nv12,hwupload", r.Width, r.Height) } out = append(out, Recipe{Engine: name + ", decoded on the CPU", Device: g.Device, Pre: []string{"-init_hw_device", "vaapi=va:" + g.Device, "-filter_hw_device", "va"}, - VF: fmt.Sprintf("%sformat=nv12,scale=%d:%d,hwupload", fps, r.Width, r.Height), Video: video}) + VF: cpuVF, Video: video}) } } if has(c.Encoders, "libx264") { vf := fmt.Sprintf("%sscale=%d:%d,format=yuv420p", fps, r.Width, r.Height) - if src.HDR() { - vf = fmt.Sprintf("%szscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable,zscale=t=bt709:m=bt709,scale=%d:%d,format=yuv420p", fps, r.Width, r.Height) + if src.PQ() { + vf = fps + toneMap + fmt.Sprintf(",scale=%d:%d,format=yuv420p", r.Width, r.Height) } out = append(out, Recipe{Engine: "CPU (libx264)", Device: CPU, VF: vf, Video: append([]string{"-c:v", "libx264", "-preset", "veryfast", "-sc_threshold", "0"}, rate...)}) } return out } +// turn is the card's filter that turns a picture the way it is meant to be shown. +func turn(src Info) string { + if src.Video == nil { + return "" + } + switch src.Video.Rotation { + case 90: + return "transpose_vaapi=dir=clock," + case 180: + return "transpose_vaapi=dir=reversal," + case 270: + return "transpose_vaapi=dir=cclock," + } + return "" +} + +// toneMap turns an HDR10 picture into an ordinary one, in software. +const toneMap = "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable,zscale=t=bt709:m=bt709:r=tv" + // Seg is the length of a piece, in seconds. const Seg = 3.0 diff --git a/internal/media/encode_test.go b/internal/media/encode_test.go index 901e4c7..8ce3761 100644 --- a/internal/media/encode_test.go +++ b/internal/media/encode_test.go @@ -92,8 +92,28 @@ func TestRecipes(t *testing.T) { 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[len(rs)-1].VF, "tonemap=hable") { - t.Fatalf("hdr: %q … %q", rs[0].VF, rs[len(rs)-1].VF) + 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) diff --git a/internal/media/inspect.go b/internal/media/inspect.go index ab722f5..aedfc2d 100644 --- a/internal/media/inspect.go +++ b/internal/media/inspect.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "errors" + "math" "os/exec" "strconv" "strings" @@ -26,6 +27,15 @@ type Track struct { Channels int `json:"channels,omitempty"` AttachedPic bool `json:"attached_pic,omitempty"` Spherical bool `json:"spherical,omitempty"` // an equirectangular 360 picture + Rotation int `json:"rotation,omitempty"` // degrees clockwise to turn it to be shown: 0, 90, 180, 270 (a phone held upright) +} + +// Shown is the picture's size as it is shown, turned by its rotation. +func (t *Track) Shown() (w, h int) { + if t.Rotation == 90 || t.Rotation == 270 { + return t.Height, t.Width + } + return t.Width, t.Height } // Info is what a file holds: the picture to take, the sound to go with it, and the whole. @@ -38,7 +48,12 @@ type Info struct { Audio *Track `json:"audio,omitempty"` // the first sound stream } -// HDR reports a picture that needs tone mapping to look right on an SDR screen. +// PQ reports HDR10 (and Dolby Vision's base layer): a picture that can't be shown on an SDR +// screen without tone mapping. HLG, the other kind, can, if a little flat. +func (i Info) PQ() bool { return i.Video != nil && i.Video.ColorTransfer == "smpte2084" } + +// HDR reports a picture that needs tone mapping to look right on an SDR screen. (The player +// tone-maps only PQ: HLG is made to be shown on an SDR screen as it is.) func (i Info) HDR() bool { return i.Video != nil && (i.Video.ColorTransfer == "smpte2084" || i.Video.ColorTransfer == "arib-std-b67") } @@ -60,8 +75,9 @@ type probeJSON struct { AttachedPic int `json:"attached_pic"` } `json:"disposition"` SideData []struct { - Type string `json:"side_data_type"` - Projection string `json:"projection"` + Type string `json:"side_data_type"` + Projection string `json:"projection"` + Rotation float64 `json:"rotation"` } `json:"side_data_list"` } `json:"streams"` Format struct { @@ -79,7 +95,7 @@ func Inspect(ctx context.Context, c Caps, path string, root bool) (Info, error) return Info{}, errors.New("ffprobe is not installed") } argv := []string{c.FFprobe, "-v", "error", "-show_entries", - "stream=index,codec_type,codec_name,profile,pix_fmt,width,height,r_frame_rate,avg_frame_rate,color_transfer,channels,disposition:stream_side_data=side_data_type,projection:format=format_name,duration,bit_rate,size", + "stream=index,codec_type,codec_name,profile,pix_fmt,width,height,r_frame_rate,avg_frame_rate,color_transfer,channels,disposition:stream_side_data=side_data_type,projection,rotation:format=format_name,duration,bit_rate,size", "-of", "json", path} if root { argv = priv.Wrap(argv...) @@ -117,6 +133,10 @@ func Inspect(ctx context.Context, c Caps, path string, root bool) (Info, error) if sd.Type == "Spherical Mapping" && strings.HasPrefix(sd.Projection, "equirectangular") { st.Spherical = true } + if sd.Type == "Display Matrix" { + // ffprobe's rotation is counter-clockwise; ffmpeg turns it back the other way + st.Rotation = ((-int(math.Round(sd.Rotation/90))*90)%360 + 360) % 360 + } } st.Spherical = st.Spherical || (s.CodecType == "video" && s.Width == 2*s.Height && s.Width >= 3840) st.FPS = fps(s.AvgFrameRate)