files: the player turns a phone's picture upright on the GPU, and shows HLG as it is
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fc4126a0ed
commit
8c69826547
+46
-14
@@ -66,9 +66,10 @@ func Ladder(i Info) []Rung {
|
|||||||
}
|
}
|
||||||
vr := v.Spherical
|
vr := v.Spherical
|
||||||
steps := flatSteps
|
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 {
|
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
|
// 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.
|
// 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 {
|
func rung(i Info, s step) Rung {
|
||||||
v := i.Video
|
v := i.Video
|
||||||
|
sw, sh := v.Shown()
|
||||||
var w, h int
|
var w, h int
|
||||||
switch {
|
switch {
|
||||||
case v.Spherical:
|
case v.Spherical:
|
||||||
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))
|
||||||
case v.Width >= v.Height:
|
case sw >= sh:
|
||||||
h, w = s.size, even(float64(s.size)*float64(v.Width)/float64(v.Height))
|
h, w = s.size, even(float64(s.size)*float64(sw)/float64(sh))
|
||||||
default: // standing up
|
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
|
fps := v.FPS
|
||||||
if fps <= 0 {
|
if fps <= 0 {
|
||||||
@@ -193,7 +195,7 @@ func Devices(c Caps) []Device {
|
|||||||
switch {
|
switch {
|
||||||
case g.API != "vaapi":
|
case g.API != "vaapi":
|
||||||
d.Reason = "only VAAPI devices encode here"
|
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"
|
d.Reason = "doesn't encode H.264"
|
||||||
default:
|
default:
|
||||||
d.OK = true
|
d.OK = true
|
||||||
@@ -232,7 +234,9 @@ func recipes(c Caps, device string, src Info, r Rung) []Recipe {
|
|||||||
if device != CPU {
|
if device != CPU {
|
||||||
for i := range gpus {
|
for i := range gpus {
|
||||||
g := &gpus[i]
|
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
|
continue
|
||||||
}
|
}
|
||||||
video := append(append([]string{"-c:v", "h264_vaapi"}, rate...), "-bf", "0")
|
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 == "" {
|
if name == "" {
|
||||||
name = g.Device
|
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) {
|
if src.Video != nil && has(g.Codecs, src.Video.Codec) {
|
||||||
vf := fps + scale
|
vf := fps + scale
|
||||||
if src.HDR() {
|
if src.PQ() {
|
||||||
vf = fps + "tonemap_vaapi=format=nv12:t=bt709:p=bt709:m=bt709," + scale
|
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"},
|
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") {
|
if has(c.Encoders, "libx264") {
|
||||||
vf := fmt.Sprintf("%sscale=%d:%d,format=yuv420p", fps, r.Width, r.Height)
|
vf := fmt.Sprintf("%sscale=%d:%d,format=yuv420p", fps, r.Width, r.Height)
|
||||||
if src.HDR() {
|
if src.PQ() {
|
||||||
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)
|
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...)})
|
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
|
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.
|
// Seg is the length of a piece, in seconds.
|
||||||
const Seg = 3.0
|
const Seg = 3.0
|
||||||
|
|
||||||
|
|||||||
@@ -92,8 +92,28 @@ func TestRecipes(t *testing.T) {
|
|||||||
hdr := src("hevc", 3840, 2160, 60)
|
hdr := src("hevc", 3840, 2160, 60)
|
||||||
hdr.Video.ColorTransfer = "smpte2084"
|
hdr.Video.ColorTransfer = "smpte2084"
|
||||||
rs := recipes(c, "", hdr, r720)
|
rs := recipes(c, "", hdr, r720)
|
||||||
if !strings.HasPrefix(rs[0].VF, "fps=30,tonemap_vaapi") || !strings.Contains(rs[len(rs)-1].VF, "tonemap=hable") {
|
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", rs[0].VF, rs[len(rs)-1].VF)
|
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=") {
|
if rs := recipes(c, "", in, r1080); strings.Contains(rs[0].VF, "fps=") {
|
||||||
t.Fatalf("same rate needs no fps filter: %q", rs[0].VF)
|
t.Fatalf("same rate needs no fps filter: %q", rs[0].VF)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"math"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -26,6 +27,15 @@ type Track struct {
|
|||||||
Channels int `json:"channels,omitempty"`
|
Channels int `json:"channels,omitempty"`
|
||||||
AttachedPic bool `json:"attached_pic,omitempty"`
|
AttachedPic bool `json:"attached_pic,omitempty"`
|
||||||
Spherical bool `json:"spherical,omitempty"` // an equirectangular 360 picture
|
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.
|
// 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
|
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 {
|
func (i Info) HDR() bool {
|
||||||
return i.Video != nil && (i.Video.ColorTransfer == "smpte2084" || i.Video.ColorTransfer == "arib-std-b67")
|
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"`
|
AttachedPic int `json:"attached_pic"`
|
||||||
} `json:"disposition"`
|
} `json:"disposition"`
|
||||||
SideData []struct {
|
SideData []struct {
|
||||||
Type string `json:"side_data_type"`
|
Type string `json:"side_data_type"`
|
||||||
Projection string `json:"projection"`
|
Projection string `json:"projection"`
|
||||||
|
Rotation float64 `json:"rotation"`
|
||||||
} `json:"side_data_list"`
|
} `json:"side_data_list"`
|
||||||
} `json:"streams"`
|
} `json:"streams"`
|
||||||
Format struct {
|
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")
|
return Info{}, errors.New("ffprobe is not installed")
|
||||||
}
|
}
|
||||||
argv := []string{c.FFprobe, "-v", "error", "-show_entries",
|
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}
|
"-of", "json", path}
|
||||||
if root {
|
if root {
|
||||||
argv = priv.Wrap(argv...)
|
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") {
|
if sd.Type == "Spherical Mapping" && strings.HasPrefix(sd.Projection, "equirectangular") {
|
||||||
st.Spherical = true
|
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.Spherical = st.Spherical || (s.CodecType == "video" && s.Width == 2*s.Height && s.Width >= 3840)
|
||||||
st.FPS = fps(s.AvgFrameRate)
|
st.FPS = fps(s.AvgFrameRate)
|
||||||
|
|||||||
Reference in New Issue
Block a user