Full hardware transcoding (#4765)

This commit is contained in:
NodudeWasTaken
2024-05-10 07:55:31 +02:00
committed by GitHub
parent 29859fa4ad
commit c5fef3977e
4 changed files with 194 additions and 51 deletions

View File

@@ -8,6 +8,7 @@ import (
"strings"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
)
var (
@@ -24,6 +25,8 @@ var (
VideoCodecVVPX VideoCodec = "vp8_vaapi"
)
const minHeight int = 256
// Tests all (given) hardware codec's
func (f *FFMpeg) InitHWSupport(ctx context.Context) {
var hwCodecSupport []VideoCodec
@@ -39,15 +42,13 @@ func (f *FFMpeg) InitHWSupport(ctx context.Context) {
var args Args
args = append(args, "-hide_banner")
args = args.LogLevel(LogLevelWarning)
args = f.hwDeviceInit(args, codec)
args = f.hwDeviceInit(args, codec, false)
args = args.Format("lavfi")
args = args.Input("color=c=red")
args = args.Input(fmt.Sprintf("color=c=red:s=%dx%d", 1280, 720))
args = args.Duration(0.1)
videoFilter := f.hwFilterInit(codec)
// Test scaling
videoFilter = videoFilter.ScaleDimensions(-2, 160)
videoFilter = f.hwCodecFilter(videoFilter, codec)
videoFilter := f.hwMaxResFilter(codec, 1280, 720, minHeight, false)
args = append(args, CodecInit(codec)...)
args = args.VideoFilter(videoFilter)
@@ -59,12 +60,7 @@ func (f *FFMpeg) InitHWSupport(ctx context.Context) {
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {
logger.Debugf("[InitHWSupport] error starting command: %v", err)
continue
}
if err := cmd.Wait(); err != nil {
if err := cmd.Run(); err != nil {
errOutput := stderr.String()
if len(errOutput) == 0 {
@@ -77,7 +73,7 @@ func (f *FFMpeg) InitHWSupport(ctx context.Context) {
}
}
outstr := "[InitHWSupport] Supported HW codecs:\n"
outstr := fmt.Sprintf("[InitHWSupport] Supported HW codecs [%d]:\n", len(hwCodecSupport))
for _, codec := range hwCodecSupport {
outstr += fmt.Sprintf("\t%s\n", codec)
}
@@ -86,66 +82,153 @@ func (f *FFMpeg) InitHWSupport(ctx context.Context) {
f.hwCodecSupport = hwCodecSupport
}
func (f *FFMpeg) hwCanFullHWTranscode(ctx context.Context, vf *models.VideoFile, codec VideoCodec) bool {
var args Args
args = append(args, "-hide_banner")
args = args.LogLevel(LogLevelWarning)
args = args.XError()
args = f.hwDeviceInit(args, codec, true)
args = args.Input(vf.Path)
args = args.Duration(0.1)
videoFilter := f.hwMaxResFilter(codec, vf.Width, vf.Height, minHeight, true)
args = append(args, CodecInit(codec)...)
args = args.VideoFilter(videoFilter)
args = args.Format("null")
args = args.Output("-")
cmd := f.Command(ctx, args)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
errOutput := stderr.String()
if len(errOutput) == 0 {
errOutput = err.Error()
}
logger.Debugf("[InitHWSupport] Full hardware transcode for file %s not supported. Error output:\n%s", vf.Basename, errOutput)
return false
}
return true
}
// Prepend input for hardware encoding only
func (f *FFMpeg) hwDeviceInit(args Args, codec VideoCodec) Args {
switch codec {
func (f *FFMpeg) hwDeviceInit(args Args, toCodec VideoCodec, fullhw bool) Args {
switch toCodec {
case VideoCodecN264:
args = append(args, "-hwaccel_device")
args = append(args, "0")
if fullhw {
args = append(args, "-hwaccel")
args = append(args, "cuda")
args = append(args, "-hwaccel_output_format")
args = append(args, "cuda")
args = append(args, "-extra_hw_frames")
args = append(args, "5")
}
case VideoCodecV264,
VideoCodecVVP9:
args = append(args, "-vaapi_device")
args = append(args, "/dev/dri/renderD128")
if fullhw {
args = append(args, "-hwaccel")
args = append(args, "vaapi")
args = append(args, "-hwaccel_output_format")
args = append(args, "vaapi")
}
case VideoCodecI264,
VideoCodecIVP9:
args = append(args, "-init_hw_device")
args = append(args, "qsv=hw")
args = append(args, "-filter_hw_device")
args = append(args, "hw")
if fullhw {
args = append(args, "-hwaccel")
args = append(args, "qsv")
args = append(args, "-hwaccel_output_format")
args = append(args, "qsv")
} else {
args = append(args, "-init_hw_device")
args = append(args, "qsv=hw")
args = append(args, "-filter_hw_device")
args = append(args, "hw")
}
}
return args
}
// Initialise a video filter for HW encoding
func (f *FFMpeg) hwFilterInit(codec VideoCodec) VideoFilter {
func (f *FFMpeg) hwFilterInit(toCodec VideoCodec, fullhw bool) VideoFilter {
var videoFilter VideoFilter
switch codec {
switch toCodec {
case VideoCodecV264,
VideoCodecVVP9:
videoFilter = videoFilter.Append("format=nv12")
videoFilter = videoFilter.Append("hwupload")
if !fullhw {
videoFilter = videoFilter.Append("format=nv12")
videoFilter = videoFilter.Append("hwupload")
}
case VideoCodecN264:
videoFilter = videoFilter.Append("format=nv12")
videoFilter = videoFilter.Append("hwupload_cuda")
if !fullhw {
videoFilter = videoFilter.Append("format=nv12")
videoFilter = videoFilter.Append("hwupload_cuda")
}
case VideoCodecI264,
VideoCodecIVP9:
videoFilter = videoFilter.Append("hwupload=extra_hw_frames=64")
videoFilter = videoFilter.Append("format=qsv")
if !fullhw {
videoFilter = videoFilter.Append("hwupload=extra_hw_frames=64")
videoFilter = videoFilter.Append("format=qsv")
}
}
return videoFilter
}
var scaler_re = regexp.MustCompile(`scale=(?P<value>[-\d]+:[-\d]+)`)
func templateReplaceScale(input string, template string, match []int, minusonehack bool) string {
result := []byte{}
res := string(scaler_re.ExpandString(result, template, input, match))
// BUG: [scale_qsv]: Size values less than -1 are not acceptable.
// Fix: Replace all instances of -2 with -1 in a scale operation
if minusonehack {
res = strings.ReplaceAll(res, "-2", "-1")
}
matchStart := match[0]
matchEnd := match[1]
return input[0:matchStart] + res + input[matchEnd:]
}
// Replace video filter scaling with hardware scaling for full hardware transcoding
func (f *FFMpeg) hwCodecFilter(args VideoFilter, codec VideoCodec) VideoFilter {
func (f *FFMpeg) hwCodecFilter(args VideoFilter, codec VideoCodec, fullhw bool) VideoFilter {
sargs := string(args)
if strings.Contains(sargs, "scale=") {
switch codec {
case VideoCodecN264:
args = VideoFilter(strings.Replace(sargs, "scale=", "scale_cuda=", 1))
case VideoCodecV264,
VideoCodecVVP9:
args = VideoFilter(strings.Replace(sargs, "scale=", "scale_vaapi=", 1))
case VideoCodecI264,
VideoCodecIVP9:
// BUG: [scale_qsv]: Size values less than -1 are not acceptable.
// Fix: Replace all instances of -2 with -1 in a scale operation
re := regexp.MustCompile(`(scale=)([\d:]*)(-2)(.*)`)
sargs = re.ReplaceAllString(sargs, "scale=$2-1$4")
args = VideoFilter(strings.Replace(sargs, "scale=", "scale_qsv=", 1))
match := scaler_re.FindStringSubmatchIndex(sargs)
if match == nil {
return args
}
switch codec {
case VideoCodecN264:
template := "scale_cuda=$value"
// In 10bit inputs you might get an error like "10 bit encode not supported"
if fullhw && f.version.major >= 5 {
template += ":format=nv12"
}
args = VideoFilter(templateReplaceScale(sargs, template, match, false))
case VideoCodecV264,
VideoCodecVVP9:
template := "scale_vaapi=$value"
args = VideoFilter(templateReplaceScale(sargs, template, match, false))
case VideoCodecI264,
VideoCodecIVP9:
template := "scale_qsv=$value"
args = VideoFilter(templateReplaceScale(sargs, template, match, true))
}
return args
@@ -153,7 +236,9 @@ func (f *FFMpeg) hwCodecFilter(args VideoFilter, codec VideoCodec) VideoFilter {
// Returns the max resolution for a given codec, or a default
func (f *FFMpeg) hwCodecMaxRes(codec VideoCodec, dW int, dH int) (int, int) {
if codec == VideoCodecN264 {
switch codec {
case VideoCodecN264,
VideoCodecI264:
return 4096, 4096
}
@@ -161,11 +246,14 @@ func (f *FFMpeg) hwCodecMaxRes(codec VideoCodec, dW int, dH int) (int, int) {
}
// Return a maxres filter
func (f *FFMpeg) hwMaxResFilter(codec VideoCodec, width int, height int, max int) VideoFilter {
videoFilter := f.hwFilterInit(codec)
maxWidth, maxHeight := f.hwCodecMaxRes(codec, width, height)
videoFilter = videoFilter.ScaleMaxLM(width, height, max, maxWidth, maxHeight)
return f.hwCodecFilter(videoFilter, codec)
func (f *FFMpeg) hwMaxResFilter(toCodec VideoCodec, width int, height int, reqHeight int, fullhw bool) VideoFilter {
if width == 0 || height == 0 {
return ""
}
videoFilter := f.hwFilterInit(toCodec, fullhw)
maxWidth, maxHeight := f.hwCodecMaxRes(toCodec, width, height)
videoFilter = videoFilter.ScaleMaxLM(width, height, reqHeight, maxWidth, maxHeight)
return f.hwCodecFilter(videoFilter, toCodec, fullhw)
}
// Return if a hardware accelerated for HLS is available