diff --git a/pkg/ffmpeg/codec_hardware.go b/pkg/ffmpeg/codec_hardware.go index e8f70033a..ec71627d3 100644 --- a/pkg/ffmpeg/codec_hardware.go +++ b/pkg/ffmpeg/codec_hardware.go @@ -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[-\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 diff --git a/pkg/ffmpeg/ffmpeg.go b/pkg/ffmpeg/ffmpeg.go index 4303644a3..da1b7faf6 100644 --- a/pkg/ffmpeg/ffmpeg.go +++ b/pkg/ffmpeg/ffmpeg.go @@ -2,10 +2,13 @@ package ffmpeg import ( + "bytes" "context" "errors" "fmt" "os/exec" + "regexp" + "strconv" "strings" stashExec "github.com/stashapp/stash/pkg/exec" @@ -93,9 +96,54 @@ func ResolveFFMpeg(path string, fallbackPath string) string { return ret } +func (f *FFMpeg) getVersion() error { + var args Args + args = append(args, "-version") + cmd := f.Command(context.Background(), args) + + var stdout bytes.Buffer + cmd.Stdout = &stdout + + var err error + if err = cmd.Run(); err != nil { + return err + } + + version_re := regexp.MustCompile(`ffmpeg version ((\d+)\.(\d+)\.(\d+))`) + stdoutStr := stdout.String() + match := version_re.FindStringSubmatchIndex(stdoutStr) + if match == nil { + return errors.New("version string malformed") + } + + majorS := stdoutStr[match[4]:match[5]] + minorS := stdoutStr[match[6]:match[7]] + patchS := stdoutStr[match[8]:match[9]] + if i, err := strconv.Atoi(majorS); err == nil { + f.version.major = i + } + if i, err := strconv.Atoi(minorS); err == nil { + f.version.minor = i + } + if i, err := strconv.Atoi(patchS); err == nil { + f.version.patch = i + } + logger.Debugf("FFMpeg version %d.%d.%d detected", f.version.major, f.version.minor, f.version.patch) + + return nil +} + +// FFMpeg version params +type FFMpegVersion struct { + major int + minor int + patch int +} + // FFMpeg provides an interface to ffmpeg. type FFMpeg struct { ffmpeg string + version FFMpegVersion hwCodecSupport []VideoCodec } @@ -104,6 +152,9 @@ func NewEncoder(ffmpegPath string) *FFMpeg { ret := &FFMpeg{ ffmpeg: ffmpegPath, } + if err := ret.getVersion(); err != nil { + logger.Warnf("FFMpeg version not detected %v", err) + } return ret } diff --git a/pkg/ffmpeg/stream_segmented.go b/pkg/ffmpeg/stream_segmented.go index 68e6f4282..42f534e78 100644 --- a/pkg/ffmpeg/stream_segmented.go +++ b/pkg/ffmpeg/stream_segmented.go @@ -81,6 +81,7 @@ var ( "-f", "hls", "-start_number", fmt.Sprint(segment), "-hls_time", fmt.Sprint(segmentLength), + "-hls_flags", "split_by_time", "-hls_segment_type", "mpegts", "-hls_playlist_type", "vod", "-hls_segment_filename", filepath.Join(outputDir, ".%d.ts"), @@ -110,6 +111,7 @@ var ( "-f", "hls", "-start_number", fmt.Sprint(segment), "-hls_time", fmt.Sprint(segmentLength), + "-hls_flags", "split_by_time", "-hls_segment_type", "mpegts", "-hls_playlist_type", "vod", "-hls_segment_filename", filepath.Join(outputDir, ".%d.ts"), @@ -328,7 +330,8 @@ func (s *runningStream) makeStreamArgs(sm *StreamManager, segment int) Args { codec := HLSGetCodec(sm, s.streamType.Name) - args = sm.encoder.hwDeviceInit(args, codec) + fullhw := sm.config.GetTranscodeHardwareAcceleration() && sm.encoder.hwCanFullHWTranscode(sm.context, s.vf, codec) + args = sm.encoder.hwDeviceInit(args, codec, fullhw) args = append(args, extraInputArgs...) if segment > 0 { @@ -339,7 +342,7 @@ func (s *runningStream) makeStreamArgs(sm *StreamManager, segment int) Args { videoOnly := ProbeAudioCodec(s.vf.AudioCodec) == MissingUnsupported - videoFilter := sm.encoder.hwMaxResFilter(codec, s.vf.Width, s.vf.Height, s.maxTranscodeSize) + videoFilter := sm.encoder.hwMaxResFilter(codec, s.vf.Width, s.vf.Height, s.maxTranscodeSize, fullhw) args = append(args, s.streamType.Args(codec, segment, videoFilter, videoOnly, s.outputDir)...) diff --git a/pkg/ffmpeg/stream_transcode.go b/pkg/ffmpeg/stream_transcode.go index 503def595..27475a5f2 100644 --- a/pkg/ffmpeg/stream_transcode.go +++ b/pkg/ffmpeg/stream_transcode.go @@ -186,7 +186,8 @@ func (o TranscodeOptions) makeStreamArgs(sm *StreamManager) Args { codec := o.FileGetCodec(sm, maxTranscodeSize) - args = sm.encoder.hwDeviceInit(args, codec) + fullhw := sm.config.GetTranscodeHardwareAcceleration() && sm.encoder.hwCanFullHWTranscode(sm.context, o.VideoFile, codec) + args = sm.encoder.hwDeviceInit(args, codec, fullhw) args = append(args, extraInputArgs...) if o.StartTime != 0 { @@ -197,7 +198,7 @@ func (o TranscodeOptions) makeStreamArgs(sm *StreamManager) Args { videoOnly := ProbeAudioCodec(o.VideoFile.AudioCodec) == MissingUnsupported - videoFilter := sm.encoder.hwMaxResFilter(codec, o.VideoFile.Width, o.VideoFile.Height, maxTranscodeSize) + videoFilter := sm.encoder.hwMaxResFilter(codec, o.VideoFile.Width, o.VideoFile.Height, maxTranscodeSize, fullhw) args = append(args, o.StreamType.Args(codec, videoFilter, videoOnly)...)