[pitivi/1.0] render: Add a way to specify h264 `profile` when rendering
- From: Thibault Saunier <tsaunier src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi/1.0] render: Add a way to specify h264 `profile` when rendering
- Date: Fri, 18 May 2018 18:00:06 +0000 (UTC)
commit 120cb16aa373f62708506afda555a60d9878a07a
Author: Thibault Saunier <tsaunier igalia com>
Date: Wed Apr 4 18:41:03 2018 -0300
render: Add a way to specify h264 `profile` when rendering
And make "high" the default.
Closes #2012
pitivi/render.py | 24 ++++++----
pitivi/utils/widgets.py | 125 ++++++++++++++++++++++++++++++++++++++++--------
tests/common.py | 9 ++++
tests/test_render.py | 47 ++++++++++++++----
4 files changed, 166 insertions(+), 39 deletions(-)
---
diff --git a/pitivi/render.py b/pitivi/render.py
index 3ed81737..3ebe6fae 100644
--- a/pitivi/render.py
+++ b/pitivi/render.py
@@ -826,18 +826,21 @@ class RenderDialog(Loggable):
second = encoder_combo.props.model.iter_nth_child(first, 0)
encoder_combo.set_active_iter(second)
- def _elementSettingsDialog(self, factory, settings_attr):
+ def _elementSettingsDialog(self, factory, media_type):
"""Opens a dialog to edit the properties for the specified factory.
Args:
factory (Gst.ElementFactory): The factory for editing.
- settings_attr (str): The Project attribute holding the properties.
+ media_type (str): String describing the media type ('audio' or 'video')
"""
- properties = getattr(self.project, settings_attr)
+ # Reconsitute the property name from the media type (vcodecsettings or acodecsettings)
+ properties = getattr(self.project, media_type[0] + 'codecsettings')
+
self.dialog = GstElementSettingsDialog(factory, properties=properties,
+ caps=getattr(self.project, media_type +
'_profile').get_format(),
parent_window=self.window)
self.dialog.ok_btn.connect(
- "clicked", self._okButtonClickedCb, settings_attr)
+ "clicked", self._okButtonClickedCb, media_type)
def __additional_debug_info(self, error):
if self.project.vencoder == 'x264enc':
@@ -985,8 +988,13 @@ class RenderDialog(Loggable):
# ------------------- Callbacks ------------------------------------------ #
# -- UI callbacks
- def _okButtonClickedCb(self, unused_button, settings_attr):
- setattr(self.project, settings_attr, self.dialog.getSettings())
+ def _okButtonClickedCb(self, unused_button, media_type):
+ assert(media_type in ("audio", "video"))
+ setattr(self.project, media_type[0] + 'codecsettings', self.dialog.getSettings())
+
+ caps = self.dialog.get_caps()
+ if caps:
+ getattr(self.project, media_type + '_profile').set_format(caps)
self.dialog.window.destroy()
def _renderButtonClickedCb(self, unused_button):
@@ -1202,7 +1210,7 @@ class RenderDialog(Loggable):
if self._setting_encoding_profile:
return
factory = get_combo_value(self.video_encoder_combo)
- self._elementSettingsDialog(factory, 'vcodecsettings')
+ self._elementSettingsDialog(factory, 'video')
def _channelsComboChangedCb(self, combo):
if self._setting_encoding_profile:
@@ -1228,7 +1236,7 @@ class RenderDialog(Loggable):
def _audioSettingsButtonClickedCb(self, unused_button):
factory = get_combo_value(self.audio_encoder_combo)
- self._elementSettingsDialog(factory, 'acodecsettings')
+ self._elementSettingsDialog(factory, 'audio')
def _muxerComboChangedCb(self, combo):
"""Handles the changing of the container format combobox."""
diff --git a/pitivi/utils/widgets.py b/pitivi/utils/widgets.py
index d3988b62..46e83655 100644
--- a/pitivi/utils/widgets.py
+++ b/pitivi/utils/widgets.py
@@ -647,12 +647,20 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
("x264enc", "multipass-cache-file"): is_valid_file
}
+ # Dictionary that references the GstCaps field to expose in the UI
+ # for a well known set of elements.
+ CAP_FIELDS_TO_EXPOSE = {
+ "x264enc": {"profile": Gst.ValueList(["high", "main", "baseline"])}
+ }
+
def __init__(self, controllable=True):
Gtk.Box.__init__(self)
Loggable.__init__(self)
self.element = None
self.ignore = []
self.properties = {}
+ # Maps caps fields to the corresponding widgets.
+ self.caps_widgets = {}
self.__controllable = controllable
self.set_orientation(Gtk.Orientation.VERTICAL)
@@ -668,7 +676,7 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
break
def setElement(self, element, values={}, ignore=['name'],
- with_reset_button=False):
+ with_reset_button=False, caps=None):
"""Sets the element to be edited.
Args:
@@ -676,13 +684,40 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
If empty, the default values will be used.
with_reset_button (bool): Whether to show a reset button for each
property.
+ caps (Gst.Caps): The caps used as "restrictions"
+ on @element source pad. Only values defined with
+ CAPS_FIELDS_TO_EXPOSE will be taken into account.
"""
self.info("element: %s, use values: %s", element, values)
self.element = element
self.ignore = ignore
- self.__add_widgets(values, with_reset_button)
- def __add_widgets(self, values, with_reset_button):
+ caps_values = {}
+ if caps:
+ element_name = None
+ if isinstance(self.element, Gst.Element):
+ element_name = self.element.get_factory().get_name()
+ src_caps_fields = self.CAP_FIELDS_TO_EXPOSE.get(element_name)
+ if src_caps_fields:
+ for field in src_caps_fields.keys():
+ val = caps[0][field]
+ if val is not None and Gst.value_is_fixed(val):
+ caps_values[field] = val
+
+ self.__add_widgets(values, with_reset_button, caps_values)
+
+ def __add_widget_to_grid(self, grid, nick, widget, y):
+ if isinstance(widget, ToggleWidget):
+ widget.set_label(nick)
+ grid.attach(widget, 0, y, 2, 1)
+ else:
+ text = _("%(preference_label)s:") % {"preference_label": nick}
+ label = Gtk.Label(label=text)
+ label.props.yalign = 0.5
+ grid.attach(label, 0, y, 1, 1)
+ grid.attach(widget, 1, y, 1, 1)
+
+ def __add_widgets(self, values, with_reset_button, caps_values):
"""Prepares a Gtk.Grid containing the property widgets of an element.
Each property is on a separate row.
@@ -711,11 +746,35 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
grid.props.column_spacing = SPACING
grid.props.border_width = SPACING
- for y, prop in enumerate(props):
+ element_name = None
+ if isinstance(self.element, Gst.Element):
+ element_name = self.element.get_factory().get_name()
+
+ src_caps_fields = self.CAP_FIELDS_TO_EXPOSE.get(element_name)
+ y = 0
+ if src_caps_fields:
+ srccaps = self.element.get_static_pad('src').get_pad_template().caps
+
+ vals = {}
+ for field, prefered_value in src_caps_fields.items():
+ gvalue = srccaps[0][field]
+ if isinstance(gvalue, Gst.ValueList) and isinstance(prefered_value, Gst.ValueList):
+ prefered_value = Gst.ValueList([v for v in prefered_value if v in gvalue])
+ gvalue = Gst.ValueList.merge(prefered_value, gvalue)
+
+ widget = self._make_widget_from_gvalue(gvalue, prefered_value)
+ if caps_values.get(field):
+ widget.setWidgetValue(caps_values[field])
+ self.__add_widget_to_grid(grid, field.capitalize(), widget, y)
+ y += 1
+
+ self.caps_widgets[field] = widget
+
+ for y, prop in enumerate(props, start=y):
# We do not know how to work with GObjects, so blacklist
# them to avoid noise in the UI
- if (not prop.flags & GObject.PARAM_WRITABLE or
- not prop.flags & GObject.PARAM_READABLE or
+ if (not prop.flags & GObject.ParamFlags.WRITABLE or
+ not prop.flags & GObject.ParamFlags.READABLE or
GObject.type_is_a(prop.value_type, GObject.Object)):
continue
@@ -732,9 +791,6 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
prop_value = values[prop.name]
prop_widget = self._makePropertyWidget(prop, prop_value)
- element_name = None
- if isinstance(self.element, Gst.Element):
- element_name = self.element.get_factory().get_name()
try:
validation_func = self.INPUT_VALIDATION_FUNCTIONS[(element_name, prop.name)]
widget = InputValidationWidget(prop_widget, validation_func)
@@ -742,16 +798,7 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
except KeyError:
widget = prop_widget
- if isinstance(prop_widget, ToggleWidget):
- prop_widget.set_label(prop.nick)
- grid.attach(widget, 0, y, 2, 1)
- else:
- text = _("%(preference_label)s:") % {"preference_label": prop.nick}
- label = Gtk.Label(label=text)
- label.set_alignment(0.0, 0.5)
- grid.attach(label, 0, y, 1, 1)
- grid.attach(widget, 1, y, 1, 1)
-
+ self.__add_widget_to_grid(grid, prop.nick, widget, y)
if hasattr(prop, 'blurb'):
widget.set_tooltip_text(prop.blurb)
@@ -782,6 +829,20 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
self.pack_start(grid, expand=False, fill=False, padding=0)
self.show_all()
+ def _make_widget_from_gvalue(self, gvalue, default):
+ if type(gvalue) == Gst.ValueList:
+ choices = []
+ for val in gvalue:
+ choices.append([val, val])
+ widget = ChoiceWidget(choices, default=default[0])
+ widget.setWidgetValue(default[0])
+ else:
+ # TODO: implement widgets for other types.
+ self.fixme("Unsupported value type: %s", type(gvalue))
+ widget = DefaultWidget()
+
+ return widget
+
def _propertyChangedCb(self, effect, gst_element, pspec):
if gst_element.get_control_binding(pspec.name):
self.log("%s controlled, not displaying value", pspec.name)
@@ -899,6 +960,15 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
values[prop.name] = value
return values
+ def get_caps_values(self):
+ values = {}
+ for field, widget in self.caps_widgets.items():
+ value = widget.getWidgetValue()
+ if value is not None:
+ values[field] = value
+
+ return values
+
def _makePropertyWidget(self, prop, value=None):
"""Creates a widget for the specified element property."""
type_name = GObject.type_name(prop.value_type.fundamental)
@@ -938,7 +1008,8 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
class GstElementSettingsDialog(Loggable):
"""Dialog window for viewing/modifying properties of a Gst.Element."""
- def __init__(self, elementfactory, properties, parent_window=None):
+ def __init__(self, elementfactory, properties, parent_window=None,
+ caps=None):
Loggable.__init__(self)
self.debug("factory: %s, properties: %s", elementfactory, properties)
@@ -948,6 +1019,7 @@ class GstElementSettingsDialog(Loggable):
self.warning(
"Couldn't create element from factory %s", self.factory)
self.properties = properties
+ self.__caps = caps
self.builder = Gtk.Builder()
self.builder.add_from_file(
@@ -962,7 +1034,7 @@ class GstElementSettingsDialog(Loggable):
# set title and frame label
self.window.set_title(
_("Properties for %s") % self.factory.get_longname())
- self.elementsettings.setElement(self.element, self.properties)
+ self.elementsettings.setElement(self.element, self.properties, caps=self.__caps)
# Try to avoid scrolling, whenever possible.
screen_height = self.window.get_screen().get_height()
@@ -992,6 +1064,17 @@ class GstElementSettingsDialog(Loggable):
dict: A property name to value map."""
return self.elementsettings.getSettings()
+ def get_caps(self):
+ values = self.elementsettings.get_caps_values()
+ if self.__caps and values:
+ caps = Gst.Caps(self.__caps.to_string())
+
+ for field, value in values.items():
+ caps.set_value(field, value)
+
+ return caps
+ return None
+
def _resetValuesClickedCb(self, unused_button):
self.resetAll()
diff --git a/tests/common.py b/tests/common.py
index 67138c00..4cb0a91e 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -237,6 +237,15 @@ class TestCase(unittest.TestCase, Loggable):
expect_selected)
self.assertEqual(ges_clip.selected.selected, expect_selected)
+ def assert_caps_equal(self, caps1, caps2):
+ if isinstance(caps1, str):
+ caps1 = Gst.Caps(caps1)
+ if isinstance(caps2, str):
+ caps2 = Gst.Caps(caps2)
+
+ self.assertTrue(caps1.is_equal(caps2),
+ "%s != %s" % (caps1.to_string(), caps2.to_string()))
+
@contextlib.contextmanager
def created_project_file(asset_uri):
diff --git a/tests/test_render.py b/tests/test_render.py
index 687e955f..c495be89 100644
--- a/tests/test_render.py
+++ b/tests/test_render.py
@@ -176,9 +176,9 @@ class TestRender(BaseTestMediaLibrary):
preset_combo.connect("changed", preset_changed_cb, changed)
test_data = [
- ("test", {'aencoder': "vorbisenc",
- 'vencoder': "theoraenc",
- 'muxer': "oggmux"}),
+ ("test", {"aencoder": "vorbisenc",
+ "vencoder": "theoraenc",
+ "muxer": "oggmux"}),
("test_ogg-vp8-opus", {
"aencoder": "opusenc",
"vencoder": ["vp8enc", "vaapivp8enc"],
@@ -236,13 +236,13 @@ class TestRender(BaseTestMediaLibrary):
project = self.create_simple_project()
dialog = self.create_rendering_dialog(project)
preset_combo = dialog.render_presets.combo
- i = find_preset_row_index(preset_combo, 'test')
+ i = find_preset_row_index(preset_combo, "test")
self.assertIsNotNone(i)
preset_combo.set_active(i)
# Check the 'test' profile is selected
active_iter = preset_combo.get_active_iter()
- self.assertEqual(preset_combo.props.model.get_value(active_iter, 0), 'test')
+ self.assertEqual(preset_combo.props.model.get_value(active_iter, 0), "test")
# Remove current profile and verify it has been removed
dialog.render_presets.action_remove.activate()
@@ -256,12 +256,12 @@ class TestRender(BaseTestMediaLibrary):
self.assertTrue(dialog.render_presets.action_save.get_enabled())
dialog.render_presets.action_save.activate(None)
self.assertEqual([i[0] for i in preset_combo.props.model],
- sorted(profile_names + ['test']))
+ sorted(profile_names + ["test"]))
active_iter = preset_combo.get_active_iter()
- self.assertEqual(preset_combo.props.model.get_value(active_iter, 0), 'test')
+ self.assertEqual(preset_combo.props.model.get_value(active_iter, 0), "test")
- def check_simple_rendering_profile(self, profile_name):
- """Checks that rendering with the specified profile works."""
+ def setup_project_with_profile(self, profile_name):
+ """Creates a simple project, open the render dialog and select @profile_name."""
project = self.create_simple_project()
dialog = self.create_rendering_dialog(project)
@@ -272,7 +272,11 @@ class TestRender(BaseTestMediaLibrary):
self.assertIsNotNone(i)
preset_combo.set_active(i)
- self.render(dialog)
+ return project, dialog
+
+ def check_simple_rendering_profile(self, profile_name):
+ """Checks that rendering with the specified profile works."""
+ self.render(self.setup_project_with_profile(profile_name)[1])
def render(self, dialog):
"""Renders pipeline from @dialog."""
@@ -352,3 +356,26 @@ class TestRender(BaseTestMediaLibrary):
def test_rendering_with_default_profile(self):
"""Tests rendering a simple timeline with the default profile."""
self.check_simple_rendering_profile(None)
+
+ @skipUnless(*encoding_target_exists("youtube"))
+ def test_setting_caps_fields_in_advanced_dialog(self):
+ """Tests setting special advanced setting (which are actually set on caps)."""
+ project, dialog = self.setup_project_with_profile("youtube")
+
+ dialog.window = None # Make sure the dialog window is never set to Mock.
+ dialog._videoSettingsButtonClickedCb(None)
+ self.assertEqual(dialog.dialog.elementsettings.get_caps_values(), {"profile": "high"})
+
+ dialog.dialog.elementsettings.caps_widgets["profile"].setWidgetValue("baseline")
+ self.assertEqual(dialog.dialog.elementsettings.get_caps_values(), {"profile": "baseline"})
+
+ caps = dialog.dialog.get_caps()
+ self.assert_caps_equal(caps, "video/x-h264,profile=baseline")
+
+ dialog.dialog.ok_btn.emit("clicked")
+ self.assert_caps_equal(project.video_profile.get_format(), "video/x-h264,profile=baseline")
+
+ dialog._videoSettingsButtonClickedCb(None)
+
+ caps = dialog.dialog.get_caps()
+ self.assert_caps_equal(caps, "video/x-h264,profile=baseline")
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]