[pitivi/clipprops: 11/14] render: Show the matching profile




commit 45d043532ba8769b243ff75ddc23a962785ec39d
Author: Alexandru Băluț <alexandru balut gmail com>
Date:   Mon Sep 14 03:27:24 2020 +0200

    render: Show the matching profile

 pitivi/project.py    |  64 ++++++++++++++++++++-
 pitivi/render.py     | 156 +++++++++++++++++++++++++++++++++++----------------
 tests/test_render.py |  99 ++++++++++++++++++++++++++++++--
 3 files changed, 265 insertions(+), 54 deletions(-)
---
diff --git a/pitivi/project.py b/pitivi/project.py
index afbab4cde..f4f7e255b 100644
--- a/pitivi/project.py
+++ b/pitivi/project.py
@@ -1494,6 +1494,7 @@ class Project(Loggable, GES.Project):
         if profiles:
             # The project just loaded, check the new
             # encoding profile and make use of it now.
+            self.info("Using first encoding profile: %s", [p.get_preset_name() for p in profiles])
             self.set_container_profile(profiles[0])
             self._load_encoder_settings(profiles)
 
@@ -1540,6 +1541,7 @@ class Project(Loggable, GES.Project):
         if not aencoder:
             self.error("Can't use profile, no audio encoder found.")
             return False
+
         if not vencoder:
             self.error("Can't use profile, no video encoder found.")
             return False
@@ -1550,6 +1552,52 @@ class Project(Loggable, GES.Project):
 
         return True
 
+    def is_profile_subset(self, profile, superset):
+        return self._get_element_factory_name(profile) == self._get_element_factory_name(superset)
+
+    def matches_container_profile(self, container_profile):
+        if not self.is_profile_subset(container_profile, self.container_profile):
+            return False
+
+        video_matches = False
+        has_video = False
+        audio_matches = False
+        has_audio = False
+        for profile in container_profile.get_profiles():
+            if isinstance(profile, GstPbutils.EncodingVideoProfile):
+                has_video = True
+                video_matches |= self.is_profile_subset(profile, self.video_profile)
+
+                # For example: "Profile Youtube"
+                preset_name = profile.get_preset()
+                if preset_name:
+                    # We assume container_profile has the same preset
+                    # as the included video profile.
+
+                    # For example: "x264enc"
+                    preset_factory_name = self.video_profile.get_preset_name()
+                    tmp_preset = Gst.ElementFactory.make(preset_factory_name, None)
+                    current_preset_name = self.video_profile.get_preset()
+                    tmp_preset.load_preset(current_preset_name)
+
+                    res, last_applied_preset_name = tmp_preset.get_meta(current_preset_name, 
"pitivi::OriginalPreset")
+                    if res and preset_name != last_applied_preset_name:
+                        return False
+
+            elif isinstance(profile, GstPbutils.EncodingAudioProfile):
+                has_audio = True
+                audio_matches |= self.is_profile_subset(profile, self.audio_profile)
+
+        if has_audio:
+            if not audio_matches:
+                return False
+
+        if has_video:
+            if not video_matches:
+                return False
+
+        return True
+
     def _load_encoder_settings(self, profiles):
         for container_profile in profiles:
             if not isinstance(container_profile, GstPbutils.EncodingContainerProfile):
@@ -1626,6 +1674,10 @@ class Project(Loggable, GES.Project):
                 if cache_key not in cache:
                     continue
 
+                current_preset = profile.get_preset()
+                if current_preset and current_preset.startswith("encoder_settings_"):
+                    current_preset = None
+
                 # The settings for the current GstPbutils.EncodingProfile.
                 settings = cache[cache_key]
                 # The name of the Gst.Preset storing the settings.
@@ -1638,13 +1690,18 @@ class Project(Loggable, GES.Project):
                 # automatically.
                 profile.set_preset(preset_name)
 
+                # The original preset name is also important.
+                preset.set_meta(preset_name, "pitivi::OriginalPreset", current_preset)
+
                 # Store the current GstPbutils.EncodingProfile's settings
                 # in the Gst.Preset.
                 for prop, value in settings.items():
                     preset.set_property(prop, value)
 
                 # Serialize the GstPbutils.EncodingProfile's settings
-                # from the cache into a Gst.Preset.
+                # from the cache into e.g.
+                # $XDG_DATA_HOME/gstreamer-1.0/presets/GstX264Enc.prs
+                # for x264enc presets.
                 res = preset.save_preset(preset_name)
                 assert res
 
@@ -2091,7 +2148,7 @@ class Project(Loggable, GES.Project):
         if profile.get_preset_name():
             return profile.get_preset_name()
 
-        factories = self.__factories_compatible_with_profile(profile)
+        factories = Project.__factories_compatible_with_profile(profile)
         if not factories:
             return None
 
@@ -2112,7 +2169,8 @@ class Project(Loggable, GES.Project):
         self.error("Could not find any element with preset %s", preset)
         return None
 
-    def __factories_compatible_with_profile(self, profile):
+    @staticmethod
+    def __factories_compatible_with_profile(profile):
         """Finds factories of the same type as the specified profile.
 
         Args:
diff --git a/pitivi/render.py b/pitivi/render.py
index e7b01e606..1a8e1e87d 100644
--- a/pitivi/render.py
+++ b/pitivi/render.py
@@ -96,6 +96,14 @@ def set_icon_and_title(icon, title, preset_item, icon_size=Gtk.IconSize.DND):
 
 
 class PresetItem(GObject.Object):
+    """Info about a render preset.
+
+    Attributes:
+        name (string): Name of the target containing the profile.
+        target (GstPbutils.EncodingTarget): The encoding target containing
+            the profile.
+        profile (GstPbutils.EncodingContainerProfile): The represented preset.
+    """
 
     def __init__(self, name, target, profile):
         GObject.Object.__init__(self)
@@ -168,7 +176,7 @@ class PresetsManager(GObject.Object, Loggable):
     """
 
     __gsignals__ = {
-        "profile-selected": (GObject.SignalFlags.RUN_LAST, None, (PresetItem,))
+        "profile-updated": (GObject.SignalFlags.RUN_LAST, None, (PresetItem,))
     }
 
     def __init__(self, project):
@@ -242,7 +250,7 @@ class PresetsManager(GObject.Object, Loggable):
         self.model.remove(pos)
 
         self.cur_preset_item = None
-        self.emit("profile-selected", None)
+        self.emit("profile-updated", None)
 
     def _save_preset_cb(self, unused_action, unused_param):
         name = self.cur_preset_item.target.get_name()
@@ -254,7 +262,7 @@ class PresetsManager(GObject.Object, Loggable):
 
         # Recreate the preset with the current values.
         self.cur_preset_item = self.create_preset(name)
-        self.emit("profile-selected", self.cur_preset_item)
+        self.emit("profile-updated", self.cur_preset_item)
 
     def _add_target(self, encoding_target):
         """Adds the profiles of the specified encoding_target as render presets.
@@ -311,11 +319,12 @@ class PresetsManager(GObject.Object, Loggable):
             preset_item (PresetItem): The row representing the preset to be applied.
         """
         self.cur_preset_item = preset_item
-        writable = len(preset_item.target.get_profiles()) == 1 and os.access(preset_item.target.get_path(), 
os.W_OK)
+        writable = bool(preset_item) and \
+                   len(preset_item.target.get_profiles()) == 1 and \
+                   os.access(preset_item.target.get_path(), os.W_OK)
 
         self.action_remove.set_enabled(writable)
         self.action_save.set_enabled(writable)
-        self.emit("profile-selected", preset_item)
 
     def select_default_preset(self):
         """Selects the default hardcoded preset."""
@@ -324,6 +333,15 @@ class PresetsManager(GObject.Object, Loggable):
                 self.select_preset(item)
                 break
 
+    def select_matching_preset(self):
+        """Selects the first preset matching the project's encoders settings."""
+        for item in self.model:
+            if self.project.matches_container_profile(item.profile):
+                self.select_preset(item)
+                return
+
+        self.select_preset(None)
+
 
 class Encoders(Loggable):
     """Registry of available Muxers, Audio encoders and Video encoders.
@@ -587,7 +605,7 @@ class QualityAdapter(Loggable):
             prop_name = list(props_values.keys())[0]
         self.prop_name = prop_name
 
-    def update_adjustment(self, adjustment, vcodecsettings, callback_handler_id):
+    def calculate_quality(self, vcodecsettings):
         if self.prop_name in vcodecsettings:
             encoder_property_value = vcodecsettings[self.prop_name]
             values = self.props_values[self.prop_name]
@@ -601,11 +619,7 @@ class QualityAdapter(Loggable):
             quality = Quality.LOW
             self.debug("Cannot calculate quality from missing prop %s", self.prop_name)
 
-        adjustment.handler_block(callback_handler_id)
-        try:
-            adjustment.props.value = quality
-        finally:
-            adjustment.handler_unblock(callback_handler_id)
+        return quality
 
     def update_project_vcodecsettings(self, project, quality):
         for prop_name, values in self.props_values.items():
@@ -779,7 +793,6 @@ class RenderDialog(Loggable):
         self._gst_signal_handlers_ids = {}
 
         self.presets_manager = PresetsManager(project)
-        self.presets_manager.connect("profile-selected", self._presets_manager_profile_selected_cb)
 
         # Whether encoders changing are a result of changing the muxer.
         self.muxer_combo_changing = False
@@ -819,41 +832,44 @@ class RenderDialog(Loggable):
         self.project.connect("rendering-settings-changed",
                              self._rendering_settings_changed_cb)
 
-        # Monitor changes
+        self.presets_manager.connect("profile-updated", self._presets_manager_profile_updated_cb)
 
+        has_vcodecsettings = bool(self.project.vcodecsettings)
+        if has_vcodecsettings:
+            self.presets_manager.select_matching_preset()
+        else:
+            self.presets_manager.select_default_preset()
+            cur_preset_item = self.presets_manager.cur_preset_item
+            if cur_preset_item and self.apply_preset(cur_preset_item):
+                self.apply_vcodecsettings_quality(Quality.MEDIUM)
+            else:
+                self.presets_manager.select_preset(None)
+
+        set_icon_and_title(self.preset_icon, self.preset_label, self.presets_manager.cur_preset_item)
+        self._update_quality_scale()
+
+        # Monitor changes to keep the preset_selection_menubutton updated.
         self.widgets_group = RippleUpdateGroup()
+        self.widgets_group.add_vertex(self.preset_selection_menubutton,
+                                      update_func=self._update_preset_selection_menubutton_func)
+
+        self.widgets_group.add_vertex(self.muxer_combo, signal="changed")
+        self.widgets_group.add_vertex(self.video_encoder_combo, signal="changed")
         self.widgets_group.add_vertex(self.frame_rate_combo, signal="changed")
+        self.widgets_group.add_vertex(self.audio_encoder_combo, signal="changed")
         self.widgets_group.add_vertex(self.channels_combo, signal="changed")
         self.widgets_group.add_vertex(self.sample_rate_combo, signal="changed")
-        self.widgets_group.add_vertex(self.muxer_combo, signal="changed")
-        self.widgets_group.add_vertex(self.audio_encoder_combo, signal="changed")
-        self.widgets_group.add_vertex(self.video_encoder_combo, signal="changed")
-        self.widgets_group.add_vertex(self.preset_menubutton, signal="clicked")
-        self.widgets_group.add_vertex(self.preset_selection_menubutton, signal="clicked")
-        self.widgets_group.add_vertex(self.quality_adjustment, signal="value-changed", 
update_func=self._update_quality_adjustment_func)
-
-        self.widgets_group.add_edge(self.frame_rate_combo, self.preset_menubutton)
-        self.widgets_group.add_edge(self.audio_encoder_combo, self.preset_menubutton)
-        self.widgets_group.add_edge(self.video_encoder_combo, self.preset_menubutton)
-        self.widgets_group.add_edge(self.muxer_combo, self.preset_menubutton)
-        self.widgets_group.add_edge(self.channels_combo, self.preset_menubutton)
-        self.widgets_group.add_edge(self.sample_rate_combo, self.preset_menubutton)
-        self.widgets_group.add_edge(self.preset_selection_menubutton, self.audio_encoder_combo)
-        self.widgets_group.add_edge(self.preset_selection_menubutton, self.video_encoder_combo)
-        self.widgets_group.add_edge(self.video_encoder_combo, self.quality_adjustment)
-
-        if not self.project.vcodecsettings:
-            self.presets_manager.select_default_preset()
-            self.quality_adjustment.props.value = Quality.MEDIUM
 
-    def _presets_manager_profile_selected_cb(self, unused_target, preset_item):
-        """Handles the selection of a render preset."""
-        set_icon_and_title(self.preset_icon, self.preset_label, preset_item)
+        self.widgets_group.add_edge(self.muxer_combo, self.preset_selection_menubutton)
+        self.widgets_group.add_edge(self.video_encoder_combo, self.preset_selection_menubutton)
+        self.widgets_group.add_edge(self.frame_rate_combo, self.preset_selection_menubutton)
+        self.widgets_group.add_edge(self.audio_encoder_combo, self.preset_selection_menubutton)
+        self.widgets_group.add_edge(self.channels_combo, self.preset_selection_menubutton)
+        self.widgets_group.add_edge(self.sample_rate_combo, self.preset_selection_menubutton)
 
-        if preset_item:
-            old_profile = self.project.container_profile
-            if not self._set_encoding_profile(preset_item.profile):
-                self._set_encoding_profile(old_profile)
+    def _presets_manager_profile_updated_cb(self, presets_manager, preset_item):
+        """Handles the saving or removing of a render preset."""
+        set_icon_and_title(self.preset_icon, self.preset_label, preset_item)
 
     def _set_encoding_profile(self, encoding_profile):
         """Sets the encoding profile of the project.
@@ -869,6 +885,7 @@ class RenderDialog(Loggable):
         try:
             muxer = Encoders().factories_by_name.get(self.project.muxer)
             if not set_combo_value(self.muxer_combo, muxer):
+                self.error("Failed to set muxer_combo to %s", muxer)
                 return False
 
             self.update_available_encoders()
@@ -988,8 +1005,28 @@ class RenderDialog(Loggable):
         return PresetBoxRow(preset_item)
 
     def _preset_listbox_row_activated_cb(self, listbox, row):
-        self.presets_manager.select_preset(row.preset_item)
-        self.preset_popover.hide()
+        if self.apply_preset(row.preset_item):
+            quality = Quality.MEDIUM
+            if self.quality_scale.get_sensitive():
+                quality = self.quality_adjustment.props.value
+
+            self.apply_vcodecsettings_quality(quality)
+            self._update_quality_scale()
+
+            self.preset_popover.hide()
+
+    def apply_preset(self, preset_item):
+        old_profile = self.project.container_profile
+        profile = preset_item.profile.copy()
+        if not self._set_encoding_profile(profile):
+            self.error("failed to apply the encoding profile, reverting to previous one")
+            self._set_encoding_profile(old_profile)
+            return False
+
+        self.presets_manager.select_preset(preset_item)
+        set_icon_and_title(self.preset_icon, self.preset_label, preset_item)
+
+        return True
 
     def _preset_selection_menubutton_clicked_cb(self, button):
         self.preset_popover.show_all()
@@ -1638,12 +1675,14 @@ class RenderDialog(Loggable):
     def _frame_rate_combo_changed_cb(self, combo):
         if self._setting_encoding_profile:
             return
+
         framerate = get_combo_value(combo)
         self.project.videorate = framerate
 
     def _video_encoder_combo_changed_cb(self, combo):
         if self._setting_encoding_profile:
             return
+
         factory = get_combo_value(combo)
         name = factory.get_name()
         self.project.vencoder = name
@@ -1652,26 +1691,31 @@ class RenderDialog(Loggable):
             self.debug("User chose a video encoder: %s", name)
             self.preferred_vencoder = name
         self._update_valid_video_restrictions(factory)
+        self._update_quality_scale()
 
     def _video_settings_button_clicked_cb(self, unused_button):
         if self._setting_encoding_profile:
             return
+
         factory = get_combo_value(self.video_encoder_combo)
         self._element_settings_dialog(factory, "video")
 
     def _channels_combo_changed_cb(self, combo):
         if self._setting_encoding_profile:
             return
+
         self.project.audiochannels = get_combo_value(combo)
 
     def _sample_rate_combo_changed_cb(self, combo):
         if self._setting_encoding_profile:
             return
+
         self.project.audiorate = get_combo_value(combo)
 
     def _audio_encoder_changed_combo_cb(self, combo):
         if self._setting_encoding_profile:
             return
+
         factory = get_combo_value(combo)
         name = factory.get_name()
         self.project.aencoder = name
@@ -1703,18 +1747,36 @@ class RenderDialog(Loggable):
         # Update muxer-dependent widgets.
         self.update_available_encoders()
 
-    def _update_quality_adjustment_func(self, unused_source, adjustment):
+    def _update_preset_selection_menubutton_func(self, source_widget, target_widget):
+        if self._setting_encoding_profile:
+            return
+
+        self.presets_manager.select_matching_preset()
+
+    def _update_quality_scale(self):
         encoder = get_combo_value(self.video_encoder_combo)
         adapter = quality_adapters.get(encoder.get_name())
+
         self.quality_scale.set_sensitive(bool(adapter))
 
         if adapter:
-            adapter.update_adjustment(self.quality_adjustment, self.project.vcodecsettings, 
self.quality_adjustment_handler_id)
+            quality = adapter.calculate_quality(self.project.vcodecsettings)
         else:
-            self.quality_adjustment.props.value = self.quality_adjustment.props.lower
+            quality = self.quality_adjustment.props.lower
+        self.quality_adjustment.handler_block(self.quality_adjustment_handler_id)
+        try:
+            self.quality_adjustment.props.value = quality
+        finally:
+            self.quality_adjustment.handler_unblock(self.quality_adjustment_handler_id)
 
     def _quality_adjustment_value_changed_cb(self, adjustment):
+        self.apply_vcodecsettings_quality(self.quality_adjustment.props.value)
+
+    def apply_vcodecsettings_quality(self, quality):
         encoder = get_combo_value(self.video_encoder_combo)
         adapter = quality_adapters.get(encoder.get_name())
-        quality = round(self.quality_adjustment.props.value)
-        adapter.update_project_vcodecsettings(self.project, quality)
+        if not adapter:
+            # The current video encoder is not yet supported.
+            return
+
+        adapter.update_project_vcodecsettings(self.project, round(quality))
diff --git a/tests/test_render.py b/tests/test_render.py
index b518f13da..2905d8b6c 100644
--- a/tests/test_render.py
+++ b/tests/test_render.py
@@ -95,13 +95,12 @@ class TestQualityAdapter(common.TestCase):
     def check_adapter(self, adapter, expected_qualities):
         qualities = []
         for prop_value in range(len(expected_qualities)):
-            adjustment = mock.Mock()
             vcodecsettings = {adapter.prop_name: prop_value}
-            adapter.update_adjustment(adjustment, vcodecsettings, 0)
-            qualities.append(adjustment.props.value)
+            quality = adapter.calculate_quality(vcodecsettings)
+            qualities.append(quality)
         self.assertListEqual(qualities, expected_qualities)
 
-    def test_update_adjustment(self):
+    def test_calculate_quality(self):
         self.check_adapter(QualityAdapter({"prop1": (0, 3, 5)}),
                            [Quality.LOW, Quality.LOW, Quality.LOW, Quality.MEDIUM, Quality.MEDIUM, 
Quality.HIGH])
         self.check_adapter(QualityAdapter({"prop1": (100, 3, 2)}),
@@ -529,3 +528,95 @@ class TestRender(BaseTestMediaLibrary):
 
         caps = dialog.dialog.get_caps()
         self.assert_caps_equal(caps, "video/x-h264,profile=baseline")
+
+    def check_quality_widget(self, dialog, vencoder, vcodecsettings, preset, sensitive, value):
+        if vencoder:
+            self.assertEqual(dialog.project.vencoder, vencoder)
+        if vcodecsettings is not None:
+            self.assertDictEqual(dialog.project.vcodecsettings, vcodecsettings)
+
+        if preset:
+            self.assertEqual(dialog.presets_manager.cur_preset_item.name, preset)
+        else:
+            self.assertIsNone(dialog.presets_manager.cur_preset_item)
+
+        self.assertEqual(dialog.quality_scale.props.sensitive, sensitive)
+        self.assertEqual(dialog.quality_adjustment.props.value, value)
+
+    @skipUnless(*encoding_target_exists("dvd"))
+    @skipUnless(*encoding_target_exists("youtube"))
+    @skipUnless(*factory_exists("pngenc"))
+    def test_quality_widget(self):
+        project = self.create_simple_project()
+        dialog = self.create_rendering_dialog(project)
+        self.check_quality_widget(dialog,
+                                  vencoder="x264enc", vcodecsettings={"quantizer": 21, "pass": 5},
+                                  preset="youtube",
+                                  sensitive=True, value=Quality.MEDIUM)
+
+        self.assertEqual(project.video_profile.get_preset_name(), "x264enc")
+        dialog.quality_adjustment.props.value = Quality.HIGH
+        self.check_quality_widget(dialog,
+                                  vencoder="x264enc", vcodecsettings={"quantizer": 18, "pass": 5},
+                                  preset="youtube",
+                                  sensitive=True, value=Quality.HIGH)
+
+        self.select_render_preset(dialog, "dvd")
+        self.check_quality_widget(dialog,
+                                  vencoder=None, vcodecsettings={},
+                                  preset="dvd",
+                                  sensitive=False, value=Quality.LOW)
+
+        self.select_render_preset(dialog, "youtube")
+        self.assertEqual(project.video_profile.get_preset_name(), "x264enc")
+        self.check_quality_widget(dialog,
+                                  vencoder="x264enc", vcodecsettings={"quantizer": 21, "pass": 5},
+                                  preset="youtube",
+                                  sensitive=True, value=Quality.MEDIUM)
+
+        self.assertTrue(set_combo_value(dialog.video_encoder_combo,
+                                        Gst.ElementFactory.find("pngenc")))
+        self.check_quality_widget(dialog,
+                                  vencoder="pngenc", vcodecsettings={},
+                                  preset=None,
+                                  sensitive=False, value=Quality.LOW)
+
+        self.select_render_preset(dialog, "youtube")
+        self.check_quality_widget(dialog,
+                                  vencoder="x264enc", vcodecsettings={"quantizer": 21, "pass": 5},
+                                  preset="youtube",
+                                  sensitive=True, value=Quality.MEDIUM)
+
+    def test_preset_persistent(self):
+        """Checks the render preset is remembered when loading a project."""
+        project = self.create_simple_project()
+        self.assertEqual(project.muxer, "webmmux")
+        self.assertEqual(project.vencoder, "vp8enc")
+        self.assertDictEqual(project.vcodecsettings, {})
+
+        dialog = self.create_rendering_dialog(project)
+        self.check_quality_widget(dialog,
+                                  vencoder="x264enc", vcodecsettings={"quantizer": 21, "pass": 5},
+                                  preset="youtube",
+                                  sensitive=True, value=Quality.MEDIUM)
+
+        project_manager = project.app.project_manager
+        with tempfile.NamedTemporaryFile() as temp_file:
+            uri = Gst.filename_to_uri(temp_file.name)
+            project_manager.save_project(uri=uri, backup=False)
+
+            app2 = common.create_pitivi()
+            project2 = app2.project_manager.load_project(uri)
+            timeline_container = TimelineContainer(app2, editor_state=app2.gui.editor.editor_state)
+            timeline_container.set_project(project2)
+            common.create_main_loop().run(until_empty=True)
+            self.assertEqual(project2.muxer, "qtmux")
+            self.assertEqual(project2.vencoder, "x264enc")
+            self.assertTrue(set({"quantizer": 21, "pass": 
5}.items()).issubset(set(project2.vcodecsettings.items())))
+
+        dialog2 = self.create_rendering_dialog(project2)
+        self.assertTrue(set({"quantizer": 21, "pass": 
5}.items()).issubset(set(project2.vcodecsettings.items())))
+        self.check_quality_widget(dialog2,
+                                  vencoder="x264enc", vcodecsettings=None,
+                                  preset="youtube",
+                                  sensitive=True, value=Quality.MEDIUM)


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]