@@ -37,6 +37,7 @@ type Options struct {
3737 ReapInterval time.Duration
3838 RestartGraceTimeout time.Duration
3939 Runner Runner
40+ HardwareProbe HardwareProbe
4041}
4142
4243type Request struct {
@@ -128,12 +129,13 @@ func NewManager(options Options) *Manager {
128129 options .HardwareDecode = strings .ToLower (strings .TrimSpace (options .HardwareDecode ))
129130 options .HardwareDevice = strings .TrimSpace (options .HardwareDevice )
130131 if options .Runner == nil && options .FFmpegPath != "" {
132+ ffmpegOptions := resolveHardwareDecodeOptions (options .FFmpegPath , FFmpegOptions {
133+ HardwareDecode : options .HardwareDecode ,
134+ HardwareDevice : options .HardwareDevice ,
135+ }, options .HardwareProbe )
131136 options .Runner = FFmpegRunner {
132- Path : options .FFmpegPath ,
133- Options : FFmpegOptions {
134- HardwareDecode : options .HardwareDecode ,
135- HardwareDevice : options .HardwareDevice ,
136- },
137+ Path : options .FFmpegPath ,
138+ Options : ffmpegOptions ,
137139 }
138140 }
139141 return & Manager {options : options , sessions : map [string ]* Session {}, media : map [string ]MediaInfo {}}
@@ -681,6 +683,79 @@ type FFmpegOptions struct {
681683 HardwareDevice string
682684}
683685
686+ type HardwareProbe func (ffmpegPath string , options FFmpegOptions ) error
687+
688+ func resolveHardwareDecodeOptions (ffmpegPath string , options FFmpegOptions , probe HardwareProbe ) FFmpegOptions {
689+ options .HardwareDecode = strings .ToLower (strings .TrimSpace (options .HardwareDecode ))
690+ options .HardwareDevice = strings .TrimSpace (options .HardwareDevice )
691+ switch options .HardwareDecode {
692+ case "" , "none" , "off" , "false" :
693+ return FFmpegOptions {}
694+ case "vaapi" :
695+ if options .HardwareDevice == "" {
696+ options .HardwareDevice = "/dev/dri/renderD128"
697+ }
698+ default :
699+ logging .Infof ("hardware decode unavailable mode=%s reason=unsupported fallback=software" , options .HardwareDecode )
700+ return FFmpegOptions {}
701+ }
702+
703+ if probe == nil {
704+ probe = defaultHardwareProbe
705+ }
706+ if err := probe (ffmpegPath , options ); err != nil {
707+ logging .Infof ("hardware decode unavailable mode=%s device=%s reason=%v fallback=software" , options .HardwareDecode , options .HardwareDevice , err )
708+ return FFmpegOptions {}
709+ }
710+ logging .Infof ("hardware decode enabled mode=%s device=%s" , options .HardwareDecode , options .HardwareDevice )
711+ return options
712+ }
713+
714+ func defaultHardwareProbe (ffmpegPath string , options FFmpegOptions ) error {
715+ if strings .TrimSpace (ffmpegPath ) == "" {
716+ return errors .New ("ffmpeg path is required" )
717+ }
718+ switch options .HardwareDecode {
719+ case "vaapi" :
720+ if err := probeFFmpegHWAccel (ffmpegPath , "vaapi" ); err != nil {
721+ return err
722+ }
723+ return probeDevice (options .HardwareDevice )
724+ default :
725+ return fmt .Errorf ("unsupported hardware decode mode %q" , options .HardwareDecode )
726+ }
727+ }
728+
729+ func probeFFmpegHWAccel (ffmpegPath , name string ) error {
730+ ctx , cancel := context .WithTimeout (context .Background (), 3 * time .Second )
731+ defer cancel ()
732+
733+ output , err := exec .CommandContext (ctx , ffmpegPath , "-hide_banner" , "-hwaccels" ).CombinedOutput ()
734+ if ctx .Err () != nil {
735+ return fmt .Errorf ("ffmpeg hwaccel probe timed out" )
736+ }
737+ if err != nil {
738+ return fmt .Errorf ("ffmpeg hwaccel probe failed: %w" , err )
739+ }
740+ for _ , line := range strings .Split (string (output ), "\n " ) {
741+ if strings .EqualFold (strings .TrimSpace (line ), name ) {
742+ return nil
743+ }
744+ }
745+ return fmt .Errorf ("ffmpeg does not list %s hwaccel" , name )
746+ }
747+
748+ func probeDevice (device string ) error {
749+ if strings .TrimSpace (device ) == "" {
750+ return errors .New ("hardware device is required" )
751+ }
752+ file , err := os .OpenFile (device , os .O_RDWR , 0 )
753+ if err != nil {
754+ return fmt .Errorf ("open hardware device: %w" , err )
755+ }
756+ return file .Close ()
757+ }
758+
684759func (r FFmpegRunner ) Start (ctx context.Context , session * Session , request Request ) (Process , error ) {
685760 if r .Path == "" {
686761 return nil , errors .New ("ffmpeg path is required" )
0 commit comments