@@ -128,6 +128,9 @@ func NewManagerStrict(options Options) (*Manager, error) {
128128 if err != nil {
129129 return nil , err
130130 }
131+ if ffmpegOptions .HardwareDecode != "" {
132+ logging .Infof ("hardware transcode enabled pipeline=%s" , ffmpegOptionsSummary (ffmpegOptions ))
133+ }
131134 options .Runner = FFmpegRunner {
132135 Path : options .FFmpegPath ,
133136 Options : ffmpegOptions ,
@@ -745,10 +748,10 @@ type HardwareProbe func(ffmpegPath string, options FFmpegOptions) error
745748func resolveHardwareDecodeOptions (ffmpegPath string , options FFmpegOptions , probe HardwareProbe ) FFmpegOptions {
746749 resolved , err := resolveHardwareDecodeOptionsStrict (ffmpegPath , options , probe )
747750 if err != nil {
748- logging .Infof ("hardware decode unavailable mode=%s device=%s reason=%v fallback=software" , options .HardwareDecode , options .HardwareDevice , err )
751+ logging .Infof ("hardware transcode unavailable mode=%s device=%s reason=%v fallback=software" , options .HardwareDecode , options .HardwareDevice , err )
749752 return FFmpegOptions {}
750753 }
751- logging .Infof ("hardware decode enabled mode =%s device=%s " , resolved . HardwareDecode , resolved . HardwareDevice )
754+ logging .Infof ("hardware transcode enabled pipeline =%s" , ffmpegOptionsSummary ( resolved ) )
752755 return resolved
753756}
754757
@@ -787,10 +790,19 @@ func defaultHardwareProbe(ffmpegPath string, options FFmpegOptions) error {
787790 if err := probeFFmpegHWAccel (ffmpegPath , "vaapi" ); err != nil {
788791 return err
789792 }
793+ if err := probeFFmpegEncoder (ffmpegPath , "h264_vaapi" ); err != nil {
794+ return err
795+ }
796+ if err := probeFFmpegFilter (ffmpegPath , "scale_vaapi" ); err != nil {
797+ return err
798+ }
790799 if err := probeDevice (options .HardwareDevice ); err != nil {
791800 return err
792801 }
793- return probeFFmpegVAAPIDeviceInit (ffmpegPath , options .HardwareDevice )
802+ if err := probeFFmpegVAAPIDeviceInit (ffmpegPath , options .HardwareDevice ); err != nil {
803+ return err
804+ }
805+ return probeFFmpegVAAPIFullPipeline (ffmpegPath , options .HardwareDevice )
794806 default :
795807 return fmt .Errorf ("unsupported hardware decode mode %q" , options .HardwareDecode )
796808 }
@@ -815,6 +827,49 @@ func probeFFmpegHWAccel(ffmpegPath, name string) error {
815827 return fmt .Errorf ("ffmpeg does not list %s hwaccel" , name )
816828}
817829
830+ func probeFFmpegEncoder (ffmpegPath , name string ) error {
831+ ctx , cancel := context .WithTimeout (context .Background (), 3 * time .Second )
832+ defer cancel ()
833+
834+ output , err := exec .CommandContext (ctx , ffmpegPath , "-hide_banner" , "-encoders" ).CombinedOutput ()
835+ if ctx .Err () != nil {
836+ return fmt .Errorf ("ffmpeg encoder probe timed out" )
837+ }
838+ if err != nil {
839+ return fmt .Errorf ("ffmpeg encoder probe failed: %w" , err )
840+ }
841+ if ffmpegListContains (output , name ) {
842+ return nil
843+ }
844+ return fmt .Errorf ("ffmpeg does not list %s encoder" , name )
845+ }
846+
847+ func probeFFmpegFilter (ffmpegPath , name string ) error {
848+ ctx , cancel := context .WithTimeout (context .Background (), 3 * time .Second )
849+ defer cancel ()
850+
851+ output , err := exec .CommandContext (ctx , ffmpegPath , "-hide_banner" , "-filters" ).CombinedOutput ()
852+ if ctx .Err () != nil {
853+ return fmt .Errorf ("ffmpeg filter probe timed out" )
854+ }
855+ if err != nil {
856+ return fmt .Errorf ("ffmpeg filter probe failed: %w" , err )
857+ }
858+ if ffmpegListContains (output , name ) {
859+ return nil
860+ }
861+ return fmt .Errorf ("ffmpeg does not list %s filter" , name )
862+ }
863+
864+ func ffmpegListContains (output []byte , name string ) bool {
865+ for _ , line := range strings .Split (string (output ), "\n " ) {
866+ if strings .Contains (strings .ToLower (line ), strings .ToLower (name )) {
867+ return true
868+ }
869+ }
870+ return false
871+ }
872+
818873func probeFFmpegVAAPIDeviceInit (ffmpegPath , device string ) error {
819874 ctx , cancel := context .WithTimeout (context .Background (), 3 * time .Second )
820875 defer cancel ()
@@ -841,6 +896,35 @@ func probeFFmpegVAAPIDeviceInit(ffmpegPath, device string) error {
841896 return nil
842897}
843898
899+ func probeFFmpegVAAPIFullPipeline (ffmpegPath , device string ) error {
900+ ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
901+ defer cancel ()
902+
903+ output , err := exec .CommandContext (ctx , ffmpegPath ,
904+ "-hide_banner" ,
905+ "-loglevel" , "error" ,
906+ "-init_hw_device" , "vaapi=probe:" + device ,
907+ "-filter_hw_device" , "probe" ,
908+ "-f" , "lavfi" ,
909+ "-i" , "nullsrc=s=64x64:d=0.1" ,
910+ "-vf" , "format=nv12,hwupload," + vaapiScaleFilter (64 , 64 ),
911+ "-frames:v" , "1" ,
912+ "-c:v" , "h264_vaapi" ,
913+ "-f" , "null" ,
914+ "-" ,
915+ ).CombinedOutput ()
916+ if ctx .Err () != nil {
917+ return fmt .Errorf ("vaapi full pipeline probe timed out" )
918+ }
919+ if err != nil {
920+ if message := strings .TrimSpace (string (output )); message != "" {
921+ return fmt .Errorf ("vaapi full pipeline probe failed: %w: %s" , err , message )
922+ }
923+ return fmt .Errorf ("vaapi full pipeline probe failed: %w" , err )
924+ }
925+ return nil
926+ }
927+
844928func probeDevice (device string ) error {
845929 if strings .TrimSpace (device ) == "" {
846930 return errors .New ("hardware device is required" )
@@ -927,6 +1011,9 @@ func ffmpegOptionsSummary(options FFmpegOptions) string {
9271011 if mode == "" || mode == "none" || mode == "off" || mode == "false" {
9281012 return "software"
9291013 }
1014+ if mode == "vaapi" {
1015+ mode = "vaapi-full"
1016+ }
9301017 device := strings .TrimSpace (options .HardwareDevice )
9311018 if device == "" {
9321019 return mode
@@ -971,17 +1058,9 @@ func buildFFmpegArgs(session *Session, request Request, options ...FFmpegOptions
9711058 "-i" , request .InputURL ,
9721059 "-map" , "0:v:0" ,
9731060 "-map" , audioMapArg (session , request ),
974- "-vf" , fmt .Sprintf ("scale=w=%d:h=%d:force_original_aspect_ratio=decrease:force_divisible_by=2" , maxTranscodeWidth , maxTranscodeHeight ),
975- "-c:v" , "libx264" ,
976- "-preset" , "veryfast" ,
977- "-tune" , "zerolatency" ,
978- "-profile:v" , "high" ,
979- "-level" , "4.1" ,
980- "-g" , strconv .Itoa (lowLatencyGOP ),
981- "-keyint_min" , strconv .Itoa (lowLatencyGOP ),
982- "-sc_threshold" , "0" ,
983- "-bf" , "0" ,
984- "-pix_fmt" , "yuv420p" ,
1061+ )
1062+ args = appendVideoTranscodeArgs (args , ffmpegOptions )
1063+ args = append (args ,
9851064 "-c:a" , "aac" ,
9861065 "-b:a" , "160k" ,
9871066 "-ac" , "2" ,
@@ -1003,6 +1082,38 @@ func buildFFmpegArgs(session *Session, request Request, options ...FFmpegOptions
10031082 return args
10041083}
10051084
1085+ func appendVideoTranscodeArgs (args []string , options FFmpegOptions ) []string {
1086+ if isVAAPITranscode (options ) {
1087+ return append (args ,
1088+ "-vf" , vaapiScaleFilter (maxTranscodeWidth , maxTranscodeHeight ),
1089+ "-c:v" , "h264_vaapi" ,
1090+ "-profile:v" , "high" ,
1091+ "-level" , "4.1" ,
1092+ "-g" , strconv .Itoa (lowLatencyGOP ),
1093+ "-keyint_min" , strconv .Itoa (lowLatencyGOP ),
1094+ "-bf" , "0" ,
1095+ "-qp" , "23" ,
1096+ )
1097+ }
1098+ return append (args ,
1099+ "-vf" , fmt .Sprintf ("scale=w=%d:h=%d:force_original_aspect_ratio=decrease:force_divisible_by=2" , maxTranscodeWidth , maxTranscodeHeight ),
1100+ "-c:v" , "libx264" ,
1101+ "-preset" , "veryfast" ,
1102+ "-tune" , "zerolatency" ,
1103+ "-profile:v" , "high" ,
1104+ "-level" , "4.1" ,
1105+ "-g" , strconv .Itoa (lowLatencyGOP ),
1106+ "-keyint_min" , strconv .Itoa (lowLatencyGOP ),
1107+ "-sc_threshold" , "0" ,
1108+ "-bf" , "0" ,
1109+ "-pix_fmt" , "yuv420p" ,
1110+ )
1111+ }
1112+
1113+ func vaapiScaleFilter (width , height int ) string {
1114+ return fmt .Sprintf ("scale_vaapi=w=%d:h=%d:force_original_aspect_ratio=decrease:force_divisible_by=2:format=nv12" , width , height )
1115+ }
1116+
10061117func audioMapArg (session * Session , request Request ) string {
10071118 return "0:a:0?"
10081119}
@@ -1016,12 +1127,17 @@ func appendHardwareDecodeArgs(args []string, options FFmpegOptions) []string {
10161127 if device := strings .TrimSpace (options .HardwareDevice ); device != "" {
10171128 args = append (args , "-hwaccel_device" , device )
10181129 }
1130+ args = append (args , "-hwaccel_output_format" , "vaapi" )
10191131 return args
10201132 default :
10211133 return args
10221134 }
10231135}
10241136
1137+ func isVAAPITranscode (options FFmpegOptions ) bool {
1138+ return strings .EqualFold (strings .TrimSpace (options .HardwareDecode ), "vaapi" )
1139+ }
1140+
10251141func ticksSeconds (ticks int64 ) string {
10261142 return strconv .FormatFloat (float64 (ticks )/ 10_000_000 , 'f' , 6 , 64 )
10271143}
0 commit comments