Commit 447ecc36 authored by Thibault Saunier's avatar Thibault Saunier

render: Add a way to specify h264 `profile` when rendering

And make "high" the default.

Closes #2012
parent 092d39a2
......@@ -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."""
......
......@@ -693,12 +693,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)
self.__bindings_by_keyframe_button = {}
......@@ -819,7 +827,18 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
props = GObject.list_properties(self.element)
return [prop for prop in props if prop.name not in self.ignore]
def add_widgets(self, create_property_widget, values={}, with_reset_button=False):
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, create_property_widget, values={}, with_reset_button=False, caps_values=None):
"""Prepares a Gtk.Grid containing the property widgets of an element.
Each property is on a separate row.
......@@ -856,11 +875,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
......@@ -896,7 +939,6 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
label.set_alignment(0.0, 0.5)
grid.attach(label, 0, y, 1, 1)
grid.attach(widget, 1, y, 1, 1)
if hasattr(prop, 'blurb'):
widget.set_tooltip_text(prop.blurb)
......@@ -927,6 +969,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)
......@@ -1045,6 +1101,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 make_property_widget(self, prop, value=None):
"""Creates a widget for the specified element property."""
type_name = GObject.type_name(prop.value_type.fundamental)
......@@ -1089,7 +1154,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)
......@@ -1099,6 +1165,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(
......@@ -1113,9 +1180,22 @@ class GstElementSettingsDialog(Loggable):
# set title and frame label
self.window.set_title(
_("Properties for %s") % self.factory.get_longname())
caps_values = {}
if self.__caps:
element_name = None
if isinstance(self.element, Gst.Element):
element_name = self.element.get_factory().get_name()
src_caps_fields = GstElementSettingsWidget.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.elementsettings.setElement(self.element, self.properties)
self.elementsettings.add_widgets(
GstElementSettingsWidget.make_property_widget, with_reset_button=True)
self.elementsettings.add_widgets(GstElementSettingsWidget.make_property_widget,
with_reset_button=True,
caps_values=caps_values)
# Try to avoid scrolling, whenever possible.
screen_height = self.window.get_screen().get_height()
......@@ -1145,6 +1225,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()
......
......@@ -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):
......
......@@ -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")
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