Skip to content

Commit 385377d

Browse files
committed
Add support for automatic HDR10 mapping
1 parent 9860914 commit 385377d

File tree

11 files changed

+820
-406
lines changed

11 files changed

+820
-406
lines changed

changelog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
**<span style="color:#56adda">0.1.12</span>**
2+
- Improved handling for HDR content with new helper tools for detecting and parsing metadata.
3+
- Removed the look-ahead feature from QSV's HEVC and AV1 encoders (not supported).
4+
- Fixed an issue with default tune options on libx264 and libx265
5+
- Removed the tune option from QSV encoders (not supported).
6+
- Changed the VAAPI hardware decoding setting to now be a dropdown menu instead of a checkbox (like all the other encoders).
7+
18
**<span style="color:#56adda">0.1.11</span>**
29
- Fix CQP quality selector for VAAPI encoding
310

lib/encoders/base.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#!/usr/bin/env python3
2+
# -*- coding:utf-8 -*-
3+
4+
class Encoder:
5+
def __init__(self, settings, probe=None):
6+
self.settings = settings
7+
self.probe = probe
8+
9+
def _target_pix_fmt_for_encoder(self, encoder_name: str) -> str:
10+
"""
11+
Determines the target pixel format for a given encoder based on the source pixel format.
12+
This generic method handles software encoders and calls a specific hardware mapper.
13+
14+
Args:
15+
encoder_name: The name of the FFmpeg encoder (e.g., "hevc_vaapi").
16+
17+
Returns:
18+
The target pixel format string supported by the encoder.
19+
"""
20+
if not self.probe:
21+
raise ValueError("Probe not yet specified on Encoder class")
22+
src_pix_fmt = self.probe.get_video_stream_pix_fmt()
23+
enc = (encoder_name or "").lower()
24+
src = (src_pix_fmt or "").lower()
25+
26+
is_h264 = "h264" in enc
27+
28+
# Determine bit depth of the source pixel format
29+
is_10bit_or_more = any(tag in src for tag in ("10", "12", "p010", "p016"))
30+
31+
return self._map_pix_fmt(is_h264, is_10bit_or_more)
32+
33+
def _map_pix_fmt(self, is_h264: bool, is_10bit: bool) -> str:
34+
"""
35+
This method is implemented by child classes to provide the specific
36+
pixel format mapping logic. This default implementation handles
37+
software formats.
38+
"""
39+
if is_10bit and not is_h264:
40+
return "yuv420p10le"
41+
else:
42+
return "yuv420p"
43+
44+
def _target_color_config_for_encoder(self, encoder_name: str):
45+
"""
46+
Returns a dict describing how to preserve HDR for the given encoder.
47+
This generic method handles common HDR checks and calls a specific
48+
mapping method for encoder-specific logic.
49+
"""
50+
if not self.probe:
51+
raise ValueError("Probe not yet specified on Encoder class")
52+
53+
# If the source is not HDR, return early with no changes
54+
if not self.probe.is_hdr_source():
55+
return {
56+
"apply_color_params": False,
57+
"setparams_filter": None,
58+
"color_tags": {},
59+
"stream_color_params": {},
60+
}
61+
62+
# Common pieces for HDR10
63+
src_color_tags = self.probe.get_color_tags()
64+
color_tags = {
65+
"color_primaries": src_color_tags.get("color_primaries", "bt2020"),
66+
"color_trc": src_color_tags.get("color_trc", "smpte2084"),
67+
"colorspace": src_color_tags.get("colorspace", "bt2020nc"),
68+
"color_range": src_color_tags.get("color_range", "tv"),
69+
}
70+
71+
# Build the setparams filter string
72+
setparams_filter = (
73+
f"setparams="
74+
f"range={color_tags['color_range']}:"
75+
f"color_primaries={color_tags['color_primaries']}:"
76+
f"color_trc={color_tags['color_trc']}:"
77+
f"colorspace={color_tags['colorspace']}"
78+
)
79+
80+
# Build the color tags dictionary for output stream
81+
stream_color_params = {
82+
"-color_primaries": color_tags['color_primaries'],
83+
"-color_trc": color_tags['color_trc'],
84+
"-colorspace": color_tags['colorspace'],
85+
"-color_range": color_tags['color_range'],
86+
}
87+
88+
# Get encoder-specific configuration
89+
# TODO: Check if we need this
90+
# encoder_config = self._map_color_config_for_encoder(encoder_name, color_tags)
91+
92+
# Merge the generic and specific configurations
93+
result = {
94+
"apply_color_params": True,
95+
"setparams_filter": setparams_filter,
96+
"color_tags": color_tags,
97+
"stream_color_params": stream_color_params,
98+
}
99+
print(result)
100+
# TODO: Check if we need this
101+
# result.update(encoder_config)
102+
return result
103+
104+
def _map_color_config_for_encoder(self, encoder_name: str, color_tags: dict):
105+
"""
106+
This method must be implemented by a child class to provide the
107+
specific HDR configuration for a given encoder.
108+
109+
Returns a dict containing:
110+
"requires_p010": bool,
111+
"encoder_args": dict,
112+
"bitstream_filter": str|None,
113+
"notes": str
114+
"""
115+
raise NotImplementedError("This method must be implemented by a child class.")
116+
117+
def provides(self):
118+
"""
119+
Returns a dictionary of supported encoder types for a given plugin.
120+
Each dictionary entry should contain the codec name and a human-readable label.
121+
"""
122+
raise NotImplementedError("This method must be implemented by a child class.")
123+
124+
def options(self):
125+
"""
126+
Returns a dictionary of configurable options for the encoder plugin.
127+
"""
128+
raise NotImplementedError("This method must be implemented by a child class.")
129+
130+
def generate_default_args(self):
131+
"""
132+
Generate a dictionary of default FFmpeg args for the encoder.
133+
This method is primarily for hardware encoders to set up the device and context.
134+
"""
135+
raise NotImplementedError("This method must be implemented by a child class.")
136+
137+
def generate_filtergraphs(self, current_filter_args, smart_filters, encoder_name):
138+
"""
139+
Generate the required filtergraph for the encoder based on the workflow.
140+
"""
141+
raise NotImplementedError("This method must be implemented by a child class.")
142+
143+
def stream_args(self, stream_info, stream_id, encoder_name):
144+
"""
145+
Generate a list of arguments for the encoder.
146+
"""
147+
raise NotImplementedError("This method must be implemented by a child class.")

lib/encoders/libsvtav1.py

Lines changed: 53 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@
2121
If not, see <https://www.gnu.org/licenses/>.
2222
2323
"""
24+
from video_transcoder.lib.encoders.base import Encoder
2425

2526

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

3131
def provides(self):
3232
return {
@@ -48,7 +48,7 @@ def options(self):
4848
"enable_qm": False,
4949
"qm_min": "8",
5050
"encoder_additional_params": "no_additional_params",
51-
"additional_params": "",
51+
"additional_params": "",
5252
}
5353

5454
def generate_default_args(self):
@@ -62,20 +62,44 @@ def generate_default_args(self):
6262
advanced_kwargs = {}
6363
return generic_kwargs, advanced_kwargs
6464

65-
def generate_filtergraphs(self):
65+
def generate_filtergraphs(self, current_filter_args, smart_filters, encoder_name):
6666
"""
6767
Generate the required filter for this encoder
68-
No filters are required for libx encoders
6968
7069
:return:
7170
"""
72-
return []
71+
generic_kwargs = {}
72+
advanced_kwargs = {}
73+
start_filter_args = []
74+
end_filter_args = []
75+
76+
# Check software format to use
77+
target_fmt = self._target_pix_fmt_for_encoder(encoder_name)
78+
79+
# Handle HDR (only for HEVC)
80+
target_color_config = self._target_color_config_for_encoder(encoder_name)
81+
82+
# If we have existing filters:
83+
if smart_filters or current_filter_args:
84+
start_filter_args.append(f'format={target_fmt}')
85+
if target_color_config.get('apply_color_params'):
86+
# Apply setparams filter if software filters exist (apply at the start of the filters list) to preserve HDR tags
87+
end_filter_args.append(target_color_config['setparams_filter'])
88+
89+
# Return built args
90+
return {
91+
"generic_kwargs": generic_kwargs,
92+
"advanced_kwargs": advanced_kwargs,
93+
"smart_filters": smart_filters,
94+
"start_filter_args": start_filter_args,
95+
"end_filter_args": end_filter_args,
96+
}
7397

7498
def encoder_details(self, encoder):
7599
provides = self.provides()
76100
return provides.get(encoder, {})
77101

78-
def args(self, stream_id):
102+
def stream_args(self, stream_id):
79103
stream_encoding = []
80104

81105
# Use defaults for basic mode
@@ -90,29 +114,30 @@ def args(self, stream_id):
90114
default_crf = 23
91115
stream_encoding += ['-crf', str(default_crf)]
92116
return stream_encoding
93-
117+
94118
stav1_params = ["enable-stat-report=1"]
95119
stav1_params += ['tune=' + str(self.settings.get_setting('tune_stvav1'))]
96-
120+
97121
if self.settings.get_setting('overlays'):
98122
# Enable overlays
99123
stav1_params += ['enable-overlays=1']
100-
124+
101125
if self.settings.get_setting('variance_boost'):
102126
# Enable variance boost
103127
stav1_params += ['enable-variance-boost=1']
104-
128+
105129
if self.settings.get_setting('enable_qm'):
106130
# Enable quantization matrix
107131
stav1_params += ['enable-qm=1']
108132
stav1_params += ['qm-min=' + str(self.settings.get_setting('qm_min'))]
109133

110-
if self.settings.get_setting('encoder_additional_params') in ['additional_params'] and len(self.settings.get_setting('encoder_svtav1_additional_params')):
134+
if self.settings.get_setting('encoder_additional_params') in ['additional_params'] and len(
135+
self.settings.get_setting('encoder_svtav1_additional_params')):
111136
# Add additional parameters for SVT-AV1
112137
stav1_params += self.settings.get_setting('encoder_svtav1_additional_params')
113-
138+
114139
stream_encoding += ['-svtav1-params', ":".join(stav1_params)]
115-
140+
116141
# Add the preset
117142
if self.settings.get_setting('preset'):
118143
stream_encoding += ['-preset', str(self.settings.get_setting('preset'))]
@@ -221,7 +246,7 @@ def get_constant_quality_scale_form_settings(self):
221246
if self.settings.get_setting('video_encoder') in ['libsvtav1']:
222247
values["description"] = "Default value for libsvtav1 = 23"
223248
return values
224-
249+
225250
def get_video_pix_fmt_form_settings(self):
226251
values = {
227252
"label": "Pixel Format",
@@ -246,7 +271,7 @@ def get_video_pix_fmt_form_settings(self):
246271
if self.settings.get_setting('mode') not in ['standard']:
247272
values["display"] = "hidden"
248273
return values
249-
274+
250275
def get_tune_stvav1_form_settings(self):
251276
values = {
252277
"label": "SVT-AV1: Tune",
@@ -271,7 +296,7 @@ def get_tune_stvav1_form_settings(self):
271296
if self.settings.get_setting('mode') not in ['standard']:
272297
values["display"] = "hidden"
273298
return values
274-
299+
275300
def get_overlays_form_settings(self):
276301
values = {
277302
"label": "SVT-AV1: Enable Overlays",
@@ -282,7 +307,8 @@ def get_overlays_form_settings(self):
282307
"value": 0,
283308
"label": "No (Default)",
284309
},
285-
{ "value": 1,
310+
{
311+
"value": 1,
286312
"label": "Yes"
287313
},
288314
]
@@ -310,7 +336,7 @@ def get_variance_boost_form_settings(self):
310336
if self.settings.get_setting('mode') not in ['standard']:
311337
values["display"] = "hidden"
312338
return values
313-
339+
314340
def get_enable_qm_form_settings(self):
315341
values = {
316342
"label": "SVT-AV1: Enable Quantization Matrix (enable-qm)",
@@ -348,9 +374,9 @@ def get_qm_min_form_settings(self):
348374
values["display"] = "hidden"
349375
if (not self.settings.get_setting('enable_qm')):
350376
values["display"] = "hidden"
351-
377+
352378
return values
353-
379+
354380
def get_encoder_additional_params_form_settings(self):
355381
values = {
356382
"label": "SVT-AV1: Additional Parameters",
@@ -371,10 +397,10 @@ def get_encoder_additional_params_form_settings(self):
371397
if self.settings.get_setting('mode') not in ['standard']:
372398
values["display"] = "hidden"
373399
return values
374-
400+
375401
def get_additional_params_form_settings(self):
376402
values = {
377-
"label": "SVT-AV1: Additional Parameters field",
403+
"label": "SVT-AV1: Additional Parameters field",
378404
"description": "Additional SVT-AV1 parameters as a colon-separated string (e.g., enable-cdef=1:enable-restoration=1).",
379405
"sub_setting": True,
380406
"input_type": "textarea",
@@ -383,5 +409,5 @@ def get_additional_params_form_settings(self):
383409
values["display"] = "hidden"
384410
if self.settings.get_setting('encoder_additional_params') not in ['additional_params']:
385411
values["display"] = "hidden"
386-
387-
return values
412+
413+
return values

0 commit comments

Comments
 (0)