[pitivi/1.0] render: Add a way to specify h264 `profile` when rendering



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]