2828from collections import Counter
2929from typing import List , Optional , Iterable
3030
31+ from video_transcoder .lib .encoders .libx import LibxEncoder
32+ from video_transcoder .lib .encoders .libsvtav1 import LibsvtAv1Encoder
33+ from video_transcoder .lib .encoders .qsv import QsvEncoder
34+ from video_transcoder .lib .encoders .vaapi import VaapiEncoder
35+ from video_transcoder .lib .encoders .nvenc import NvencEncoder
3136from video_transcoder .lib .ffmpeg import StreamMapper
3237
3338image_video_codecs = [
114119}
115120
116121
122+ def available_encoders (settings = None , probe = None ):
123+ return_encoders = {}
124+ encoder_libs = [
125+ LibxEncoder ,
126+ LibsvtAv1Encoder ,
127+ QsvEncoder ,
128+ VaapiEncoder ,
129+ NvencEncoder ,
130+ ]
131+ for encoder_class in encoder_libs :
132+ encoder_lib = encoder_class (settings = settings , probe = probe )
133+ for encoder in encoder_lib .provides ():
134+ return_encoders [encoder ] = encoder_lib
135+ return return_encoders
136+
137+
117138def get_video_stream_data (streams ):
118139 width = 0
119140 height = 0
@@ -129,7 +150,6 @@ def get_video_stream_data(streams):
129150 return width , height , video_stream_index
130151
131152
132-
133153def format_command_multiline (mapper , max_width = 120 , indent = " " ):
134154 """
135155 Prints command for debugging...
@@ -180,7 +200,29 @@ def format_command_multiline(mapper, max_width=120, indent=" "):
180200 return " \\ \n " .join (lines )
181201
182202
183- def detect_black_bars (abspath , probe_data ):
203+ def join_filtergraph (filter_id , filter_args , stream_id ):
204+ """
205+ Joins a filtergraph from a collection of args
206+ """
207+ filtergraph = ''
208+ count = 1
209+ for filter_string in filter_args :
210+ # If we are appending to existing filters, separate by a semicolon to start a new chain
211+ if filtergraph :
212+ filtergraph += ';'
213+ # Add the input for this filter
214+ filtergraph += '[{}]' .format (filter_id )
215+ # Add filtergraph
216+ filtergraph += '{}' .format (filter_string )
217+ # Update filter ID and add it to the end
218+ filter_id = '0:vf:{}-{}' .format (stream_id , count )
219+ filtergraph += '[{}]' .format (filter_id )
220+ # Increment filter ID counter
221+ count += 1
222+ return filter_id , filtergraph
223+
224+
225+ def detect_black_bars (abspath , probe_data , settings ):
184226 """
185227 Detect black bars via ffmpeg cropdetect using quorum logic across multiple samples.
186228
@@ -372,17 +414,61 @@ def rdown(v: int, m: int) -> int:
372414
373415 return f"{ w_r } :{ h_r } :{ x_r } :{ y_r } "
374416
375- def _ffmpeg_sample (ss : int , t_seconds : Optional [int ], r_to : Optional [int ]) -> str :
417+ def _ffmpeg_sample (ss : int , t_seconds : Optional [int ], r_to : Optional [int ], enable_hw_accel = False ) -> str :
418+ # NOTE: After adding HW accel, I actually found it to be slower.
419+ # I am leaving the code here with a switch enable_hw_accel incase I come back to test further later on.
376420 mapper = StreamMapper (logger , ['video' , 'audio' , 'subtitle' , 'data' , 'attachment' ])
377421 mapper .set_input_file (abspath )
422+
423+ # Figure out which video stream we're filtering
424+ _ , _ , video_stream_index = get_video_stream_data (probe_data .get ('streams' ))
425+ # Fallback to 0 if probe didn't return a valid index
426+ stream_id = str (video_stream_index if video_stream_index is not None else 0 )
427+
428+ # Configure the cropdetect filter
429+ filter_args = [f"cropdetect=mode=black:round={ r_to } :reset=0" ]
430+
431+ # Build hardware acceleration args based on encoder
432+ # Note: these are not applied to advanced mode - advanced mode was returned above
433+ encoder_name = settings .get_setting ('video_encoder' )
434+ encoder_lib = available_encoders (settings = settings ).get (encoder_name )
435+ if enable_hw_accel and encoder_lib :
436+ encoder_lib .set_probe (probe_info = probe_data )
437+ generic_kwargs , advanced_kwargs = encoder_lib .generate_default_args ()
438+ mapper .set_ffmpeg_generic_options (** generic_kwargs )
439+ mapper .set_ffmpeg_advanced_options (** advanced_kwargs )
440+
441+ filtergraph_config = encoder_lib .generate_filtergraphs (
442+ filter_args ,
443+ [],
444+ encoder_name
445+ )
446+
447+ generic_kwargs = filtergraph_config .get ('generic_kwargs' , {})
448+ mapper .set_ffmpeg_generic_options (** generic_kwargs )
449+
450+ advanced_kwargs = filtergraph_config .get ('advanced_kwargs' , {})
451+ mapper .set_ffmpeg_advanced_options (** advanced_kwargs )
452+
453+ start_filter_args = filtergraph_config .get ('start_filter_args' , [])
454+ end_filter_args = filtergraph_config .get ('end_filter_args' , [])
455+ filter_args = start_filter_args + filter_args + end_filter_args
456+
457+ # Join filtergraph
458+ filter_id = '0:v:{}' .format (stream_id )
459+ filter_id , filtergraph = join_filtergraph (filter_id , filter_args , stream_id )
460+
378461 # Seek to the sample start
379462 mapper .set_ffmpeg_generic_options (** {"-ss" : str (int (ss ))})
380463
381- # Configure time-based cropdetect filter at sample end timestamp
464+ # Ingore non-video streams and insert filter
382465 adv_args = ["-an" , "-sn" , "-dn" ]
383- adv_kwargs = {"-vf" : f"cropdetect=round={ r_to } :reset=0" }
466+ adv_kwargs = {
467+ "-filter_complex" : filtergraph ,
468+ "-map" : f"[{ filter_id } ]" ,
469+ }
384470 if t_seconds and t_seconds > 0 :
385- adv_kwargs [ "-t" ] = str (int (t_seconds ))
471+ mapper . set_ffmpeg_generic_options ( ** { "-t" : str (int (t_seconds ))} )
386472 mapper .set_ffmpeg_advanced_options (* adv_args , ** adv_kwargs )
387473 mapper .set_output_null ()
388474
@@ -447,14 +533,14 @@ def _quorum(last_three: List[str]) -> Optional[str]:
447533 logger .info ("[BB Detection] Sampling video file '%s' (width:%s, height:%s) to detect black bars" ,
448534 abspath , src_w , src_h )
449535
450- # Special case: very short videos (<60s) → single full-file pass
536+ # Special case: very short videos (<60s) → single, capped pass (max 20s)
451537 if total_duration is not None and total_duration < 60 :
452- logger . debug ( "[BB Detection] Duration < 60s. Sampling single full -file pass" )
453- observed = _ffmpeg_sample ( ss = 0 , t_seconds = None , r_to = round_to )
454- observed_raw = _ffmpeg_sample (ss = 0 , t_seconds = None , r_to = round_to )
455-
456- logger .debug ("[BB Detection] Sample #1 @ 0s → %s" , observed )
457- if observed != "NO_CROP" :
538+ # Cap runtime to avoid slow software decode on whole -file scans
539+ t_cap = int ( min ( 20 , max ( 1 , total_duration )) )
540+ logger . debug ( "[BB Detection] Duration < 60s. Sampling capped to %ss from start (ss=0)." , t_cap )
541+ observed_raw = _ffmpeg_sample ( ss = 0 , t_seconds = t_cap , r_to = round_to )
542+ logger .debug ("[BB Detection] Sample #1 @ 0s (t=%ss) → %s" , t_cap , observed_raw )
543+ if observed_raw != "NO_CROP" :
458544 observed = _normalise_crop_or_nocrop (
459545 observed_raw , src_w , src_h ,
460546 min_sum_tb = 12 ,
@@ -463,8 +549,15 @@ def _quorum(last_three: List[str]) -> Optional[str]:
463549 if observed == "NO_CROP" :
464550 logger .debug ("[BB Detection] Decision: NO_CROP (normalised from %s)." , observed_raw )
465551 return None
466- logger .debug ("[BB Detection] Decision: CROP=%s." , observed )
552+
553+ if observed != observed_raw :
554+ logger .debug ("[BB Detection] Decision: CROP=%s (normalised from %s)." , observed , observed_raw )
555+ else :
556+ logger .debug ("[BB Detection] Decision: CROP=%s." , observed )
467557 return observed
558+
559+ # observed_raw == NO_CROP
560+ logger .debug ("[BB Detection] Decision: NO_CROP (short-video capped sample)." )
468561 return None
469562
470563 # Define sampling parameters
0 commit comments