Commit 272f5a60 authored by Thibault Saunier's avatar Thibault Saunier

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: 's avatarAlex Băluț <&lt;alexandru.balut@gmail.com&gt;>
Differential Revision: https://phabricator.freedesktop.org/D1807
parent 4b3bbbda
......@@ -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.
......
......@@ -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
......
......@@ -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
......@@ -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",
......
......@@ -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]))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment