package ffmpeg import ( "bytes" "context" "fmt" "math" "regexp" "strconv" "strings" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" ) var ( // Hardware codec's VideoCodecN264 = makeVideoCodec("H264 NVENC", "h264_nvenc") VideoCodecN264H = makeVideoCodec("H264 NVENC HQ profile", "h264_nvenc") VideoCodecI264 = makeVideoCodec("H264 Intel Quick Sync Video (QSV)", "h264_qsv") VideoCodecI264C = makeVideoCodec("H264 Intel Quick Sync Video (QSV) Compatibility profile", "h264_qsv") VideoCodecA264 = makeVideoCodec("H264 Advanced Media Framework (AMF)", "h264_amf") VideoCodecM264 = makeVideoCodec("H264 VideoToolbox", "h264_videotoolbox") VideoCodecV264 = makeVideoCodec("H264 VAAPI", "h264_vaapi") VideoCodecR264 = makeVideoCodec("H264 V4L2M2M", "h264_v4l2m2m") VideoCodecO264 = makeVideoCodec("H264 OMX", "h264_omx") VideoCodecIVP9 = makeVideoCodec("VP9 Intel Quick Sync Video (QSV)", "vp9_qsv") VideoCodecVVP9 = makeVideoCodec("VP9 VAAPI", "vp9_vaapi") VideoCodecVVPX = makeVideoCodec("VP8 VAAPI", "vp8_vaapi") ) const minHeight int = 480 // Tests all (given) hardware codec's func (f *FFMpeg) InitHWSupport(ctx context.Context) { var hwCodecSupport []VideoCodec // Note that the first compatible codec is returned, so order is important for _, codec := range []VideoCodec{ VideoCodecN264H, VideoCodecN264, VideoCodecI264, VideoCodecI264C, VideoCodecV264, VideoCodecR264, VideoCodecIVP9, VideoCodecVVP9, VideoCodecM264, } { var args Args args = append(args, "-hide_banner") args = args.LogLevel(LogLevelWarning) args = f.hwDeviceInit(args, codec, false) args = args.Format("lavfi") vFile := &models.VideoFile{Width: 1280, Height: 720} args = args.Input(fmt.Sprintf("color=c=red:s=%dx%d", vFile.Width, vFile.Height)) args = args.Duration(0.1) // Test scaling videoFilter := f.hwMaxResFilter(codec, vFile, minHeight, false) 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] Codec %s not supported. Error output:\n%s", codec, errOutput) } else { hwCodecSupport = append(hwCodecSupport, codec) } } outstr := fmt.Sprintf("[InitHWSupport] Supported HW codecs [%d]:\n", len(hwCodecSupport)) for _, codec := range hwCodecSupport { outstr += fmt.Sprintf("\t%s - %s\n", codec.Name, codec.CodeName) } logger.Info(outstr) f.hwCodecSupport = hwCodecSupport } func (f *FFMpeg) hwCanFullHWTranscode(ctx context.Context, codec VideoCodec, vf *models.VideoFile, reqHeight int) bool { if codec == VideoCodecCopy { return false } 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(1) videoFilter := f.hwMaxResFilter(codec, vf, reqHeight, 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, toCodec VideoCodec, fullhw bool) Args { switch toCodec { case VideoCodecN264, VideoCodecN264H: args = append(args, "-hwaccel_device") args = append(args, "0") if fullhw { args = append(args, "-threads") args = append(args, "1") args = append(args, "-hwaccel") args = append(args, "cuda") args = append(args, "-hwaccel_output_format") args = append(args, "cuda") } 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, VideoCodecI264C, VideoCodecIVP9: 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") } case VideoCodecM264: if fullhw { args = append(args, "-hwaccel") args = append(args, "videotoolbox") args = append(args, "-hwaccel_output_format") args = append(args, "videotoolbox_vld") } else { args = append(args, "-init_hw_device") args = append(args, "videotoolbox=vt") } } return args } // Initialise a video filter for HW encoding func (f *FFMpeg) hwFilterInit(toCodec VideoCodec, fullhw bool) VideoFilter { var videoFilter VideoFilter switch toCodec { case VideoCodecV264, VideoCodecVVP9: if !fullhw { videoFilter = videoFilter.Append("format=nv12") videoFilter = videoFilter.Append("hwupload") } case VideoCodecN264, VideoCodecN264H: if !fullhw { videoFilter = videoFilter.Append("format=nv12") videoFilter = videoFilter.Append("hwupload_cuda") } case VideoCodecI264, VideoCodecI264C, VideoCodecIVP9: if !fullhw { videoFilter = videoFilter.Append("hwupload=extra_hw_frames=64") videoFilter = videoFilter.Append("format=qsv") } case VideoCodecM264: if !fullhw { videoFilter = videoFilter.Append("format=nv12") videoFilter = videoFilter.Append("hwupload") } } return videoFilter } var scaler_re = regexp.MustCompile(`scale=(?P([-\d]+):([-\d]+))`) func templateReplaceScale(input string, template string, match []int, vf *models.VideoFile, minusonehack bool) string { result := []byte{} if minusonehack { // Parse width and height w, err := strconv.Atoi(input[match[4]:match[5]]) if err != nil { logger.Error("failed to parse width") return input } h, err := strconv.Atoi(input[match[6]:match[7]]) if err != nil { logger.Error("failed to parse height") return input } // Calculate ratio ratio := float64(vf.Width) / float64(vf.Height) if w < 0 { w = int(math.Round(float64(h) * ratio)) } else if h < 0 { h = int(math.Round(float64(w) / ratio)) } // Fix not divisible by 2 errors if w%2 != 0 { w++ } if h%2 != 0 { h++ } template = strings.ReplaceAll(template, "$value", fmt.Sprintf("%d:%d", w, h)) } res := string(scaler_re.ExpandString(result, template, input, match)) matchStart := match[0] matchEnd := match[1] return input[0:matchStart] + res + input[matchEnd:] } // Replace video filter scaling with hardware scaling for full hardware transcoding (also fixes the format) func (f *FFMpeg) hwCodecFilter(args VideoFilter, codec VideoCodec, vf *models.VideoFile, fullhw bool) VideoFilter { sargs := string(args) match := scaler_re.FindStringSubmatchIndex(sargs) if match == nil { return f.hwApplyFullHWFilter(args, codec, fullhw) } return f.hwApplyScaleTemplate(sargs, codec, match, vf, fullhw) } // Apply format switching if applicable func (f *FFMpeg) hwApplyFullHWFilter(args VideoFilter, codec VideoCodec, fullhw bool) VideoFilter { switch codec { case VideoCodecN264, VideoCodecN264H: if fullhw && f.version.Gteq(Version{major: 5}) { // Added in FFMpeg 5 args = args.Append("scale_cuda=format=yuv420p") } case VideoCodecV264, VideoCodecVVP9: if fullhw && f.version.Gteq(Version{major: 3, minor: 1}) { // Added in FFMpeg 3.1 args = args.Append("scale_vaapi=format=nv12") } case VideoCodecI264, VideoCodecI264C, VideoCodecIVP9: if fullhw && f.version.Gteq(Version{major: 3, minor: 3}) { // Added in FFMpeg 3.3 args = args.Append("scale_qsv=format=nv12") } } return args } // Switch scaler func (f *FFMpeg) hwApplyScaleTemplate(sargs string, codec VideoCodec, match []int, vf *models.VideoFile, fullhw bool) VideoFilter { var template string switch codec { case VideoCodecN264, VideoCodecN264H: template = "scale_cuda=$value" if fullhw && f.version.Gteq(Version{major: 5}) { // Added in FFMpeg 5 template += ":format=yuv420p" } case VideoCodecV264, VideoCodecVVP9: template = "scale_vaapi=$value" if fullhw && f.version.Gteq(Version{major: 3, minor: 1}) { // Added in FFMpeg 3.1 template += ":format=nv12" } case VideoCodecI264, VideoCodecI264C, VideoCodecIVP9: template = "scale_qsv=$value" if fullhw && f.version.Gteq(Version{major: 3, minor: 3}) { // Added in FFMpeg 3.3 template += ":format=nv12" } case VideoCodecM264: template = "scale_vt=$value" default: return VideoFilter(sargs) } // BUG: [scale_qsv]: Size values less than -1 are not acceptable. isIntel := codec == VideoCodecI264 || codec == VideoCodecI264C || codec == VideoCodecIVP9 // BUG: scale_vt doesn't call ff_scale_adjust_dimensions, thus cant accept negative size values isApple := codec == VideoCodecM264 return VideoFilter(templateReplaceScale(sargs, template, match, vf, isIntel || isApple)) } // Returns the max resolution for a given codec, or a default func (f *FFMpeg) hwCodecMaxRes(codec VideoCodec) (int, int) { switch codec { case VideoCodecN264, VideoCodecN264H, VideoCodecI264, VideoCodecI264C: return 4096, 4096 } return 0, 0 } // Return a maxres filter func (f *FFMpeg) hwMaxResFilter(toCodec VideoCodec, vf *models.VideoFile, reqHeight int, fullhw bool) VideoFilter { if vf.Width == 0 || vf.Height == 0 { return "" } videoFilter := f.hwFilterInit(toCodec, fullhw) maxWidth, maxHeight := f.hwCodecMaxRes(toCodec) videoFilter = videoFilter.ScaleMaxLM(vf.Width, vf.Height, reqHeight, maxWidth, maxHeight) return f.hwCodecFilter(videoFilter, toCodec, vf, fullhw) } // Return if a hardware accelerated for HLS is available func (f *FFMpeg) hwCodecHLSCompatible() *VideoCodec { for _, element := range f.hwCodecSupport { switch element { case VideoCodecN264, VideoCodecN264H, VideoCodecI264, VideoCodecI264C, VideoCodecV264, VideoCodecR264, VideoCodecM264: // Note that the Apple encoder sucks at startup, thus HLS quality is crap return &element } } return nil } // Return if a hardware accelerated codec for MP4 is available func (f *FFMpeg) hwCodecMP4Compatible() *VideoCodec { for _, element := range f.hwCodecSupport { switch element { case VideoCodecN264, VideoCodecN264H, VideoCodecI264, VideoCodecI264C, VideoCodecM264: return &element } } return nil } // Return if a hardware accelerated codec for WebM is available func (f *FFMpeg) hwCodecWEBMCompatible() *VideoCodec { for _, element := range f.hwCodecSupport { switch element { case VideoCodecIVP9, VideoCodecVVP9: return &element } } return nil }