[pitivi] Make setting encoding profiles more robust



commit 272f5a6045483ca45be9922c3a6fca0edfd55c08
Author: Thibault Saunier <tsaunier gnome org>
Date:   Wed Jul 26 11:40:34 2017 -0400

    Make setting encoding profiles more robust
    
    When setting an encoding profile from a file we need to make sure
    several restrictions are handled:
    
     - We need to make sure the resulting restriction caps are
       compatible with the encoder that is going to be used by encodebin
    
     - We should ensure that the profile restriction caps are fully taken
       into account (if those restriction are not compatible with the encoder
       we can't do much)
    
     - We need to try as much as possible to use user previously set formats
    
     - We need to ensure fields that are mandatory for us are set in a way
       that is compatible with the encoder
    
    This introduces a utility function (for better testability) that allows
    this kind of caps fixation and some unit tests for this function.
    
    Reviewed-by: Alex Băluț <<alexandru balut gmail com>>
    Differential Revision: https://phabricator.freedesktop.org/D1807

 pitivi/project.py    |  116 +++++++++++++++++++++++++++++---------------------
 pitivi/render.py     |   18 ++++----
 pitivi/utils/misc.py |   87 +++++++++++++++++++++++++++++++++++++
 tests/test_render.py |    6 ++-
 tests/test_utils.py  |   35 +++++++++++++++
 5 files changed, 203 insertions(+), 59 deletions(-)
---
diff --git a/pitivi/project.py b/pitivi/project.py
index ce5cccc..8f81aea 100644
--- a/pitivi/project.py
+++ b/pitivi/project.py
@@ -39,6 +39,7 @@ from pitivi.render import Encoders
 from pitivi.undo.project import AssetAddedIntention
 from pitivi.undo.project import AssetProxiedIntention
 from pitivi.utils.loggable import Loggable
+from pitivi.utils.misc import fixate_caps_with_default_values
 from pitivi.utils.misc import isWritable
 from pitivi.utils.misc import path_from_uri
 from pitivi.utils.misc import quote_uri
@@ -1171,14 +1172,6 @@ class Project(Loggable, GES.Project):
         if container_profile == self.container_profile:
             return False
 
-        previous_audio_rest = None
-        previous_video_rest = None
-        if not reset_all and self.container_profile:
-            if self.audio_profile:
-                previous_audio_rest = self.audio_profile.get_restriction()
-            if self.video_profile:
-                previous_video_rest = self.video_profile.get_restriction()
-
         muxer = self._getElementFactoryName(
             Encoders().muxers, container_profile)
         if muxer is None:
@@ -1192,7 +1185,7 @@ class Project(Loggable, GES.Project):
                 if profile.get_restriction() is None:
                     profile.set_restriction(Gst.Caps("video/x-raw"))
 
-                self._ensureVideoRestrictions(profile, previous_video_rest)
+                self._ensureVideoRestrictions(profile)
                 vencoder = self._getElementFactoryName(Encoders().vencoders, profile)
                 if vencoder:
                     profile.set_preset_name(vencoder)
@@ -1201,7 +1194,7 @@ class Project(Loggable, GES.Project):
                 if profile.get_restriction() is None:
                     profile.set_restriction(Gst.Caps("audio/x-raw"))
 
-                self._ensureAudioRestrictions(profile, previous_audio_rest)
+                self._ensureAudioRestrictions(profile)
                 aencoder = self._getElementFactoryName(Encoders().aencoders, profile)
                 if aencoder:
                     profile.set_preset_name(aencoder)
@@ -1491,57 +1484,82 @@ class Project(Loggable, GES.Project):
         if not self.ges_timeline.get_layers():
             self.ges_timeline.append_layer()
 
-    def _ensureRestrictions(self, profile, values, ref_restrictions=None):
-        """Make sure restriction values defined in @values are set on @profile.
+    def _ensureRestrictions(self, profile, defaults, ref_restrictions=None,
+                            prev_vals=None):
+        """Make sure restriction values defined in @defaults are set on @profile.
 
         Attributes:
             profile (Gst.EncodingProfile): The Gst.EncodingProfile to use
-            values (dict): A key value dict to use to set restriction values
+            defaults (dict): A key value dict to use to set restriction defaults
             ref_restrictions (Gst.Caps): Reuse values from those caps instead
                                          of @values if available.
 
         """
-        self.debug("Ensuring %s", profile.get_restriction().to_string())
-        for fieldname, value in values:
-            # Only consider the first GstStructure
-            # FIXME Figure out everywhere how to be smarter.
-            cvalue = profile.get_restriction()[0][fieldname]
-            if cvalue is None:
-                if ref_restrictions and ref_restrictions[0][fieldname]:
-                    value = ref_restrictions[0][fieldname]
-                res = Project._set_restriction(profile, fieldname, value)
-
-        encoder = profile.get_preset_name()
-        if encoder:
-            self._enforce_video_encoder_restrictions(encoder, profile)
-
-    def _ensureVideoRestrictions(self, profile=None, ref_restrictions=None):
-        values = [
-            ("width", 720),
-            ("height", 576),
-            ("framerate", Gst.Fraction(25, 1)),
-            ("pixel-aspect-ratio", Gst.Fraction(1, 1))
-        ]
+        encoder = None
+        if isinstance(profile, GstPbutils.EncodingAudioProfile):
+            facttype = Gst.ELEMENT_FACTORY_TYPE_AUDIO_ENCODER
+        else:
+            facttype = Gst.ELEMENT_FACTORY_TYPE_VIDEO_ENCODER
+
+        ebin = Gst.ElementFactory.make('encodebin', None)
+        ebin.props.profile = profile
+        for element in ebin.iterate_recurse():
+            if element.get_factory().list_is_type(facttype):
+                encoder = element
+                break
+
+        encoder_sinkcaps = encoder.sinkpads[0].get_pad_template().get_caps().copy()
+        self.debug("%s - Ensuring %s\n  defaults: %s\n  ref_restrictions: %s\n  prev_vals: %s)",
+                   encoder, encoder_sinkcaps, defaults, ref_restrictions,
+                   prev_vals)
+        restriction = fixate_caps_with_default_values(encoder_sinkcaps,
+                                                      ref_restrictions,
+                                                      defaults,
+                                                      prev_vals)
+        assert(restriction)
+        preset_name = encoder.get_factory().get_name()
+        profile.set_restriction(restriction)
+        profile.set_preset_name(preset_name)
+
+        self._enforce_video_encoder_restrictions(preset_name, profile)
+        self.info("Fully set restriction: %s", profile.get_restriction().to_string())
+
+    def _ensureVideoRestrictions(self, profile=None):
+        defaults = {
+            "width": 720,
+            "height": 576,
+            "framerate": Gst.Fraction(25, 1),
+            "pixel-aspect-ratio": Gst.Fraction(1, 1)
+        }
+
+        prev_vals = None
+        if self.video_profile:
+            prev_vals = self.video_profile.get_restriction().copy()
+
+        ref_restrictions = None
         if not profile:
             profile = self.video_profile
-        self._ensureRestrictions(profile, values, ref_restrictions)
+        else:
+            ref_restrictions = profile.get_restriction()
 
-    def _ensureAudioRestrictions(self, profile=None, ref_restrictions=None):
+        self._ensureRestrictions(profile, defaults, ref_restrictions,
+                                 prev_vals)
+
+    def _ensureAudioRestrictions(self, profile=None):
+        ref_restrictions = None
         if not profile:
             profile = self.audio_profile
-        defaults = [["channels", 2], ["rate", 44100]]
-        for fv in defaults:
-            field, value = fv
-            fvalue = profile.get_format()[0][field]
-            if isinstance(fvalue, Gst.ValueList) and value not in fvalue.array:
-                fv[1] = fvalue.array[0]
-            elif isinstance(fvalue, range) and value not in fvalue:
-                fv[1] = fvalue[0]
-            else:
-                self.warning("How should we handle ensuring restriction caps"
-                             " compatibility for field %s with format value: %s",
-                             field, fvalue)
-        return self._ensureRestrictions(profile, defaults, ref_restrictions)
+        else:
+            ref_restrictions = profile.get_restriction()
+
+        defaults = {"channels": Gst.IntRange(range(1, 2147483647)),
+                    "rate": Gst.IntRange(range(8000, GLib.MAXINT))}
+        prev_vals = None
+        if self.audio_profile:
+            prev_vals = self.audio_profile.get_restriction().copy()
+
+        return self._ensureRestrictions(profile, defaults, ref_restrictions,
+                                        prev_vals)
 
     def _maybeInitSettingsFromAsset(self, asset):
         """Updates the project settings to match the specified asset.
diff --git a/pitivi/render.py b/pitivi/render.py
index 65d99e3..72af178 100644
--- a/pitivi/render.py
+++ b/pitivi/render.py
@@ -479,7 +479,7 @@ class RenderDialog(Loggable):
         def factory(x):
             return Encoders().factories_by_name.get(getattr(self.project, x))
 
-        self.project.set_container_profile(encoding_profile, reset_all=True)
+        self.project.set_container_profile(encoding_profile)
         self._setting_encoding_profile = True
 
         if not set_combo_value(self.muxer_combo, factory('muxer')):
@@ -487,15 +487,15 @@ class RenderDialog(Loggable):
             return
 
         self.updateAvailableEncoders()
-        for i, (combo, value) in enumerate([
-                (self.audio_encoder_combo, factory('aencoder')),
-                (self.video_encoder_combo, factory('vencoder')),
-                (self.sample_rate_combo, self.project.audiorate),
-                (self.channels_combo, self.project.audiochannels),
-                (self.frame_rate_combo, self.project.videorate)]):
+        for i, (combo, name, value) in enumerate([
+                (self.audio_encoder_combo, "aencoder", factory("aencoder")),
+                (self.video_encoder_combo, "vencoder", factory("vencoder")),
+                (self.sample_rate_combo, "audiorate", self.project.audiorate),
+                (self.channels_combo, "audiochannels", self.project.audiochannels),
+                (self.frame_rate_combo, "videorate", self.project.videorate)]):
             if value is None:
-                self.error("%d - Got no value for combo %s... rolling back",
-                           i, combo)
+                self.error("%d - Got no value for %s (%s)... rolling back",
+                           i, name, combo)
                 rollback()
                 return
 
diff --git a/pitivi/utils/misc.py b/pitivi/utils/misc.py
index b6612e3..3b229e3 100644
--- a/pitivi/utils/misc.py
+++ b/pitivi/utils/misc.py
@@ -306,3 +306,90 @@ def unicode_error_dialog():
     dialog.set_title(_("Error while decoding a string"))
     dialog.run()
     dialog.destroy()
+
+
+def intersect(v1, v2):
+    s = Gst.Structure('t', t=v1).intersect(Gst.Structure('t', t=v2))
+    if s:
+        return s['t']
+
+    return None
+
+
+def fixate_caps_with_default_values(template, restrictions, default_values,
+                                    prev_vals=None):
+    """Fixates @template taking into account other restriction values.
+
+    The resulting caps will only contain the fields from @default_values,
+    @restrictions and @prev_vals
+
+    Args:
+        template (Gst.Caps) : The pad template to fixate.
+        restrictions (Gst.Caps): Restriction caps to be used to fixate
+            @template. This is the minimum requested
+            restriction. Can be None
+        default_values (dict) : Dictionary containing the minimal fields
+            to be fixated and some default values (can be ranges).
+        prev_vals (Optional[Gst.Caps]) : Some values that were previously
+            used, and should be kept instead of the default values if possible.
+
+    Returns:
+        Gst.Caps: The caps resulting from the previously defined operations.
+    """
+    res = Gst.Caps.new_empty()
+    fields = set(default_values.keys())
+    if restrictions:
+        for struct in restrictions:
+            fields.update(struct.keys())
+
+        log.debug("utils", "Intersect template %s with the restriction %s",
+                  template, restrictions)
+        tmp = template.intersect(restrictions)
+
+        if not tmp:
+            log.warning("utils",
+                        "No common format between template %s and restrictions %s",
+                        template, restrictions)
+        else:
+            template = tmp
+
+    for struct in template:
+        struct = struct.copy()
+        for field in fields:
+            prev_val = None
+            default_val = default_values.get(field)
+            if prev_vals and prev_vals[0].has_field(field):
+                prev_val = prev_vals[0][field]
+
+            if not struct.has_field(field):
+                if prev_val:
+                    struct[field] = prev_val
+                elif default_val:
+                    struct[field] = default_val
+            else:
+                v = None
+                struct_val = struct[field]
+                if prev_val:
+                    v = intersect(struct_val, prev_val)
+                    if v is not None:
+                        struct[field] = v
+                if v is None and default_val:
+                    v = intersect(struct_val, default_val)
+                    if v is not None:
+                        struct[field] = v
+                else:
+                    log.info("utils", "Field %s from %s is plainly fixated",
+                             field, struct)
+
+        struct = struct.copy()
+        for key in struct.keys():
+            if key not in fields:
+                struct.remove_field(key)
+
+        log.debug("utils", "Adding %s to resulting caps", struct)
+        res.append_structure(struct)
+
+    res.mini_object.refcount += 1
+    res = res.fixate()
+    log.debug("utils", "Fixated %s", res)
+    return res
diff --git a/tests/test_render.py b/tests/test_render.py
index c3bcf48..875a036 100644
--- a/tests/test_render.py
+++ b/tests/test_render.py
@@ -272,8 +272,12 @@ class TestRender(common.TestCase):
             i = find_preset_row_index(preset_combo, profile_name)
             self.assertIsNotNone(i)
             preset_combo.set_active(i)
-        from pitivi.render import RenderingProgressDialog
 
+        self.render(dialog)
+
+    def render(self, dialog):
+        """Renders pipeline from @dialog."""
+        from pitivi.render import RenderingProgressDialog
         with tempfile.TemporaryDirectory() as temp_dir:
             # Start rendering
             with mock.patch.object(dialog.filebutton, "get_uri",
diff --git a/tests/test_utils.py b/tests/test_utils.py
index bf966ab..86d91b8 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -19,12 +19,14 @@
 # Boston, MA 02110-1301, USA.
 from unittest import TestCase
 
+from gi.repository import GLib
 from gi.repository import Gst
 
 from pitivi.check import CairoDependency
 from pitivi.check import ClassicDependency
 from pitivi.check import GstDependency
 from pitivi.check import GtkDependency
+from pitivi.utils.misc import fixate_caps_with_default_values
 from pitivi.utils.ui import beautify_length
 
 second = Gst.SECOND
@@ -92,3 +94,36 @@ class TestDependencyChecks(TestCase):
         classic_dep = ClassicDependency("numpy", None)
         classic_dep.check()
         self.assertTrue(classic_dep.satisfied)
+
+
+class TestMiscUtils(TestCase):
+
+    def test_fixate_caps_with_defalt_values(self):
+        voaacenc_caps = Gst.Caps.from_string(
+            "audio/x-raw, format=(string)S16LE, layout=(string)interleaved, rate=(int){ 8000, 11025, 12000, 
16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000 }, channels=(int)1;"
+            "audio/x-raw, format=(string)S16LE, layout=(string)interleaved, rate=(int){ 8000, 11025, 12000, 
16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000 }, channels=(int)2, 
channel-mask=(bitmask)0x0000000000000003")
+        yt_audiorest = Gst.Caps("audio/x-raw,channels=6,channel-mask=0x3f,rate={48000,96000};"
+            "audio/x-raw,channels=2,rate={48000,96000}")
+
+        vorbis_caps = Gst.Caps("audio/x-raw, format=(string)F32LE, layout=(string)interleaved, rate=(int)[ 
1, 200000 ], channels=(int)1;"
+                               "audio/x-raw, format=(string)F32LE, layout=(string)interleaved, rate=(int)[ 
1, 200000 ], channels=(int)2, channel-mask=(bitmask)0x0000000000000003;"
+                               "audio/x-raw, format=(string)F32LE, layout=(string)interleaved, rate=(int)[ 
1, 200000 ], channels=(int)3, channel-mask=(bitmask)0x0000000000000007;"
+                               "audio/x-raw, format=(string)F32LE, layout=(string)interleaved, rate=(int)[ 
1, 200000 ], channels=(int)4, channel-mask=(bitmask)0x0000000000000033;"
+                               "audio/x-raw, format=(string)F32LE, layout=(string)interleaved, rate=(int)[ 
1, 200000 ], channels=(int)5, channel-mask=(bitmask)0x0000000000000037;"
+                               "audio/x-raw, format=(string)F32LE, layout=(string)interleaved, rate=(int)[ 
1, 200000 ], channels=(int)6, channel-mask=(bitmask)0x000000000000003f;"
+                               "audio/x-raw, format=(string)F32LE, layout=(string)interleaved, rate=(int)[ 
1, 200000 ], channels=(int)7, channel-mask=(bitmask)0x0000000000000d0f;"
+                               "audio/x-raw, format=(string)F32LE, layout=(string)interleaved, rate=(int)[ 
1, 200000 ], channels=(int)8, channel-mask=(bitmask)0x0000000000000c3f;"
+                               "audio/x-raw, format=(string)F32LE, layout=(string)interleaved, rate=(int)[ 
1, 200000 ], channels=(int)[ 9, 255 ], channel-mask=(bitmask)0x0000000000000000")
+
+        audio_defaults = {'channels': Gst.IntRange(range(1, 2147483647)),
+                          "rate": Gst.IntRange(range(8000, GLib.MAXINT))}
+
+        dataset = [
+            (voaacenc_caps, yt_audiorest, audio_defaults, None, Gst.Caps("audio/x-raw, 
channels=2,rate=48000,channel-mask=(bitmask)0x03")),
+            (vorbis_caps, None, audio_defaults, Gst.Caps('audio/x-raw,channels=1,rate=8000'))
+        ]
+
+        for data in dataset:
+            res = fixate_caps_with_default_values(*data[:-1])
+            print(res)
+            self.assertTrue(res.is_equal_fixed(data[-1]), "%s != %s" % (res, data[-1]))


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