Skip to content

Commit ab740f8

Browse files
committed
Refactor part of the black bar filter to use HW accel - kept optional
Unfortunately, this did not actually make the process faster. In fact, it made it slower most of the time. So I have decided to leave it disabled for now.
1 parent 385377d commit ab740f8

11 files changed

Lines changed: 149 additions & 61 deletions

File tree

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- Fixed an issue with default tune options on libx264 and libx265
55
- Removed the tune option from QSV encoders (not supported).
66
- Changed the VAAPI hardware decoding setting to now be a dropdown menu instead of a checkbox (like all the other encoders).
7+
- Speed up crop-detect on smaller files.
78

89
**<span style="color:#56adda">0.1.11</span>**
910
- Fix CQP quality selector for VAAPI encoding

lib/encoders/base.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
#!/usr/bin/env python3
22
# -*- coding:utf-8 -*-
3+
import logging
4+
5+
logger = logging.getLogger("Unmanic.Plugin.video_transcoder")
6+
37

48
class Encoder:
5-
def __init__(self, settings, probe=None):
9+
def __init__(self, settings=None, probe=None):
610
self.settings = settings
711
self.probe = probe
812

13+
def set_probe(self, probe=None, probe_info=None):
14+
if isinstance(probe_info, dict):
15+
from video_transcoder.lib.ffmpeg import Probe
16+
probe = Probe(logger, allowed_mimetypes=['video'])
17+
probe.set_probe(probe_info)
18+
self.probe = probe
19+
920
def _target_pix_fmt_for_encoder(self, encoder_name: str) -> str:
1021
"""
1122
Determines the target pixel format for a given encoder based on the source pixel format.
@@ -96,7 +107,6 @@ def _target_color_config_for_encoder(self, encoder_name: str):
96107
"color_tags": color_tags,
97108
"stream_color_params": stream_color_params,
98109
}
99-
print(result)
100110
# TODO: Check if we need this
101111
# result.update(encoder_config)
102112
return result

lib/encoders/libsvtav1.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525

2626

2727
class LibsvtAv1Encoder(Encoder):
28-
def __init__(self, settings, probe=None):
29-
super().__init__(settings, probe=probe)
28+
def __init__(self, settings=None, probe=None):
29+
super().__init__(settings=settings, probe=probe)
3030

3131
def provides(self):
3232
return {

lib/encoders/libx.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525

2626

2727
class LibxEncoder(Encoder):
28-
def __init__(self, settings, probe=None):
29-
super().__init__(settings, probe=probe)
28+
def __init__(self, settings=None, probe=None):
29+
super().__init__(settings=settings, probe=probe)
3030

3131
def provides(self):
3232
return {

lib/encoders/nvenc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ def get_configured_device(settings):
8989

9090

9191
class NvencEncoder(Encoder):
92-
def __init__(self, settings, probe=None):
93-
super().__init__(settings, probe=probe)
92+
def __init__(self, settings=None, probe=None):
93+
super().__init__(settings=settings, probe=probe)
9494

9595
def _map_pix_fmt(self, is_h264: bool, is_10bit: bool) -> str:
9696
if is_10bit and not is_h264:

lib/encoders/qsv.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@
3535

3636

3737
class QsvEncoder(Encoder):
38-
def __init__(self, settings, probe=None):
39-
super().__init__(settings, probe=probe)
38+
def __init__(self, settings=None, probe=None):
39+
super().__init__(settings=settings, probe=probe)
4040

4141
def _map_pix_fmt(self, is_h264: bool, is_10bit: bool) -> str:
4242
if is_10bit and not is_h264:
@@ -79,17 +79,15 @@ def generate_default_args(self):
7979
# Encode only (no decoding)
8080
# REF: https://trac.ffmpeg.org/wiki/Hardware/QuickSync#Transcode
8181
generic_kwargs = {
82-
"-init_hw_device": "qsv=hw",
83-
"-filter_hw_device": "hw",
82+
"-init_hw_device": "qsv=qsv0",
83+
"-filter_hw_device": "qsv0",
8484
}
8585
advanced_kwargs = {}
8686
# Check if we are using a HW accelerated decoder> Modify args as required
8787
if self.settings.get_setting('qsv_decoding_method') in ['qsv']:
8888
generic_kwargs.update({
8989
"-hwaccel": "qsv",
9090
"-hwaccel_output_format": "qsv",
91-
"-init_hw_device": "qsv=hw",
92-
"-filter_hw_device": "hw",
9391
})
9492
return generic_kwargs, advanced_kwargs
9593

lib/encoders/vaapi.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ def list_available_vaapi_devices():
4949

5050

5151
class VaapiEncoder(Encoder):
52-
def __init__(self, settings, probe=None):
53-
super().__init__(settings, probe=probe)
52+
def __init__(self, settings=None, probe=None):
53+
super().__init__(settings=settings, probe=probe)
5454

5555
def _map_pix_fmt(self, is_h264: bool, is_10bit: bool) -> str:
5656
if is_10bit and not is_h264:
@@ -130,10 +130,9 @@ def generate_default_args(self):
130130
"-hwaccel": "vaapi",
131131
"-hwaccel_output_format": "vaapi",
132132
"-hwaccel_device": dev_id,
133+
"-filter_hw_device": dev_id,
133134
}
134-
advanced_kwargs = {
135-
"-filter_hw_device": dev_id,
136-
}
135+
advanced_kwargs = {}
137136
else:
138137
# Encode only (no decoding)
139138
# REF: https://trac.ffmpeg.org/wiki/Hardware/VAAPI#Encode-only (sorta)

lib/global_settings.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,8 @@ def get_video_encoder_form_settings(self):
174174
"input_type": "select",
175175
"select_options": [],
176176
}
177-
for encoder_name in self.settings.encoders:
178-
encoder_lib = self.settings.encoders.get(encoder_name)
177+
encoder_libs = tools.available_encoders(settings=self.settings)
178+
for encoder_name, encoder_lib in encoder_libs.items():
179179
encoder_details = encoder_lib.encoder_details(encoder_name)
180180
if encoder_details.get('codec') != self.settings.get_setting('video_codec'):
181181
continue
@@ -257,6 +257,7 @@ def get_autocrop_black_bars_form_settings(self):
257257
"description": "Runs FFmpeg 'cropdetect' on the file to auto-detect the crop size.\n"
258258
"This detected crop size is then applied during video transcode as a 'crop' filter.",
259259
"sub_setting": True,
260+
"req_lev": 2,
260261
}
261262
if not self.settings.get_setting('apply_smart_filters'):
262263
values["display"] = 'hidden'
@@ -335,6 +336,7 @@ def get_strip_data_streams_form_settings(self):
335336
"Certain subtitle formats are stored as data streams in some containers.\n"
336337
"Data streams are not supported by all containers.",
337338
"sub_setting": True,
339+
"req_lev": 2,
338340
}
339341
if not self.settings.get_setting('apply_smart_filters'):
340342
values["display"] = 'hidden'
@@ -349,6 +351,7 @@ def get_strip_attachment_streams_form_settings(self):
349351
"These streams could contain fonts used in rendering subtitles.\n"
350352
"Attachment streams are not supported by all containers.",
351353
"sub_setting": True,
354+
"req_lev": 2,
352355
}
353356
if not self.settings.get_setting('apply_smart_filters'):
354357
values["display"] = 'hidden'

lib/plugin_stream_mapper.py

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,16 @@ def set_default_values(self, settings, abspath, probe):
8989
if self.settings.get_setting('apply_smart_filters'):
9090
if self.settings.get_setting('autocrop_black_bars'):
9191
# Test if the file has black bars
92-
self.crop_value = tools.detect_black_bars(abspath, probe.get_probe())
92+
self.crop_value = tools.detect_black_bars(abspath, probe.get_probe(), self.settings)
9393

9494
# Build hardware acceleration args based on encoder
9595
# Note: these are not applied to advanced mode - advanced mode was returned above
96-
for encoder_name in self.settings.encoders:
97-
encoder_lib = self.settings.encoders.get(encoder_name)
98-
if self.settings.get_setting('video_encoder') in encoder_lib.provides():
99-
generic_kwargs, advanced_kwargs = encoder_lib.generate_default_args()
100-
self.set_ffmpeg_generic_options(**generic_kwargs)
101-
self.set_ffmpeg_advanced_options(**advanced_kwargs)
96+
encoder_name = self.settings.get_setting('video_encoder')
97+
encoder_lib = tools.available_encoders(settings=self.settings).get(encoder_name)
98+
if encoder_lib:
99+
generic_kwargs, advanced_kwargs = encoder_lib.generate_default_args()
100+
self.set_ffmpeg_generic_options(**generic_kwargs)
101+
self.set_ffmpeg_advanced_options(**advanced_kwargs)
102102

103103
def scale_resolution(self, stream_info: dict):
104104
def get_test_resolution(settings):
@@ -142,7 +142,6 @@ def build_filter_chain(self, stream_info, stream_id):
142142
:param stream_id:
143143
:return:
144144
"""
145-
filter_id = '0:v:{}'.format(stream_id)
146145
software_filters = []
147146
hardware_filters = []
148147
filter_args = []
@@ -221,21 +220,8 @@ def build_filter_chain(self, stream_info, stream_id):
221220
return None, None
222221

223222
# Join filtergraph
224-
filtergraph = ''
225-
count = 1
226-
for filter_string in filter_args:
227-
# If we are appending to existing filters, separate by a semicolon to start a new chain
228-
if filtergraph:
229-
filtergraph += ';'
230-
# Add the input for this filter
231-
filtergraph += '[{}]'.format(filter_id)
232-
# Add filtergraph
233-
filtergraph += '{}'.format(filter_string)
234-
# Update filter ID and add it to the end
235-
filter_id = '0:vf:{}-{}'.format(stream_id, count)
236-
filtergraph += '[{}]'.format(filter_id)
237-
# Increment filter ID counter
238-
count += 1
223+
filter_id = '0:v:{}'.format(stream_id)
224+
filter_id, filtergraph = tools.join_filtergraph(filter_id, filter_args, stream_id)
239225

240226
return filter_id, filtergraph
241227

lib/tools.py

Lines changed: 107 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828
from collections import Counter
2929
from 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
3136
from video_transcoder.lib.ffmpeg import StreamMapper
3237

3338
image_video_codecs = [
@@ -114,6 +119,22 @@
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+
117138
def 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-
133153
def 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

Comments
 (0)