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): ...@@ -826,18 +826,21 @@ class RenderDialog(Loggable):
second = encoder_combo.props.model.iter_nth_child(first, 0) second = encoder_combo.props.model.iter_nth_child(first, 0)
encoder_combo.set_active_iter(second) 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. """Opens a dialog to edit the properties for the specified factory.
Args: Args:
factory (Gst.ElementFactory): The factory for editing. 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, self.dialog = GstElementSettingsDialog(factory, properties=properties,
caps=getattr(self.project, media_type + '_profile').get_format(),
parent_window=self.window) parent_window=self.window)
self.dialog.ok_btn.connect( self.dialog.ok_btn.connect(
"clicked", self._okButtonClickedCb, settings_attr) "clicked", self._okButtonClickedCb, media_type)
def __additional_debug_info(self, error): def __additional_debug_info(self, error):
if self.project.vencoder == 'x264enc': if self.project.vencoder == 'x264enc':
...@@ -985,8 +988,13 @@ class RenderDialog(Loggable): ...@@ -985,8 +988,13 @@ class RenderDialog(Loggable):
# ------------------- Callbacks ------------------------------------------ # # ------------------- Callbacks ------------------------------------------ #
# -- UI callbacks # -- UI callbacks
def _okButtonClickedCb(self, unused_button, settings_attr): def _okButtonClickedCb(self, unused_button, media_type):
setattr(self.project, settings_attr, self.dialog.getSettings()) 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() self.dialog.window.destroy()
def _renderButtonClickedCb(self, unused_button): def _renderButtonClickedCb(self, unused_button):
...@@ -1202,7 +1210,7 @@ class RenderDialog(Loggable): ...@@ -1202,7 +1210,7 @@ class RenderDialog(Loggable):
if self._setting_encoding_profile: if self._setting_encoding_profile:
return return
factory = get_combo_value(self.video_encoder_combo) factory = get_combo_value(self.video_encoder_combo)
self._elementSettingsDialog(factory, 'vcodecsettings') self._elementSettingsDialog(factory, 'video')
def _channelsComboChangedCb(self, combo): def _channelsComboChangedCb(self, combo):
if self._setting_encoding_profile: if self._setting_encoding_profile:
...@@ -1228,7 +1236,7 @@ class RenderDialog(Loggable): ...@@ -1228,7 +1236,7 @@ class RenderDialog(Loggable):
def _audioSettingsButtonClickedCb(self, unused_button): def _audioSettingsButtonClickedCb(self, unused_button):
factory = get_combo_value(self.audio_encoder_combo) factory = get_combo_value(self.audio_encoder_combo)
self._elementSettingsDialog(factory, 'acodecsettings') self._elementSettingsDialog(factory, 'audio')
def _muxerComboChangedCb(self, combo): def _muxerComboChangedCb(self, combo):
"""Handles the changing of the container format combobox.""" """Handles the changing of the container format combobox."""
......
...@@ -693,12 +693,20 @@ class GstElementSettingsWidget(Gtk.Box, Loggable): ...@@ -693,12 +693,20 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
("x264enc", "multipass-cache-file"): is_valid_file ("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): def __init__(self, controllable=True):
Gtk.Box.__init__(self) Gtk.Box.__init__(self)
Loggable.__init__(self) Loggable.__init__(self)
self.element = None self.element = None
self.ignore = [] self.ignore = []
self.properties = {} self.properties = {}
# Maps caps fields to the corresponding widgets.
self.caps_widgets = {}
self.__controllable = controllable self.__controllable = controllable
self.set_orientation(Gtk.Orientation.VERTICAL) self.set_orientation(Gtk.Orientation.VERTICAL)
self.__bindings_by_keyframe_button = {} self.__bindings_by_keyframe_button = {}
...@@ -819,7 +827,18 @@ class GstElementSettingsWidget(Gtk.Box, Loggable): ...@@ -819,7 +827,18 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
props = GObject.list_properties(self.element) props = GObject.list_properties(self.element)
return [prop for prop in props if prop.name not in self.ignore] 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. """Prepares a Gtk.Grid containing the property widgets of an element.
Each property is on a separate row. Each property is on a separate row.
...@@ -856,11 +875,35 @@ class GstElementSettingsWidget(Gtk.Box, Loggable): ...@@ -856,11 +875,35 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
grid.props.column_spacing = SPACING grid.props.column_spacing = SPACING
grid.props.border_width = 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 # We do not know how to work with GObjects, so blacklist
# them to avoid noise in the UI # them to avoid noise in the UI
if (not prop.flags & GObject.PARAM_WRITABLE or if (not prop.flags & GObject.ParamFlags.WRITABLE or
not prop.flags & GObject.PARAM_READABLE or not prop.flags & GObject.ParamFlags.READABLE or
GObject.type_is_a(prop.value_type, GObject.Object)): GObject.type_is_a(prop.value_type, GObject.Object)):
continue continue
...@@ -896,7 +939,6 @@ class GstElementSettingsWidget(Gtk.Box, Loggable): ...@@ -896,7 +939,6 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
label.set_alignment(0.0, 0.5) label.set_alignment(0.0, 0.5)
grid.attach(label, 0, y, 1, 1) grid.attach(label, 0, y, 1, 1)
grid.attach(widget, 1, y, 1, 1) grid.attach(widget, 1, y, 1, 1)
if hasattr(prop, 'blurb'): if hasattr(prop, 'blurb'):
widget.set_tooltip_text(prop.blurb) widget.set_tooltip_text(prop.blurb)
...@@ -927,6 +969,20 @@ class GstElementSettingsWidget(Gtk.Box, Loggable): ...@@ -927,6 +969,20 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
self.pack_start(grid, expand=False, fill=False, padding=0) self.pack_start(grid, expand=False, fill=False, padding=0)
self.show_all() 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): def _propertyChangedCb(self, effect, gst_element, pspec):
if gst_element.get_control_binding(pspec.name): if gst_element.get_control_binding(pspec.name):
self.log("%s controlled, not displaying value", pspec.name) self.log("%s controlled, not displaying value", pspec.name)
...@@ -1045,6 +1101,15 @@ class GstElementSettingsWidget(Gtk.Box, Loggable): ...@@ -1045,6 +1101,15 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
values[prop.name] = value values[prop.name] = value
return values 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): def make_property_widget(self, prop, value=None):
"""Creates a widget for the specified element property.""" """Creates a widget for the specified element property."""
type_name = GObject.type_name(prop.value_type.fundamental) type_name = GObject.type_name(prop.value_type.fundamental)
...@@ -1089,7 +1154,8 @@ class GstElementSettingsWidget(Gtk.Box, Loggable): ...@@ -1089,7 +1154,8 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
class GstElementSettingsDialog(Loggable): class GstElementSettingsDialog(Loggable):
"""Dialog window for viewing/modifying properties of a Gst.Element.""" """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) Loggable.__init__(self)
self.debug("factory: %s, properties: %s", elementfactory, properties) self.debug("factory: %s, properties: %s", elementfactory, properties)
...@@ -1099,6 +1165,7 @@ class GstElementSettingsDialog(Loggable): ...@@ -1099,6 +1165,7 @@ class GstElementSettingsDialog(Loggable):
self.warning( self.warning(
"Couldn't create element from factory %s", self.factory) "Couldn't create element from factory %s", self.factory)
self.properties = properties self.properties = properties
self.__caps = caps
self.builder = Gtk.Builder() self.builder = Gtk.Builder()
self.builder.add_from_file( self.builder.add_from_file(
...@@ -1113,9 +1180,22 @@ class GstElementSettingsDialog(Loggable): ...@@ -1113,9 +1180,22 @@ class GstElementSettingsDialog(Loggable):
# set title and frame label # set title and frame label
self.window.set_title( self.window.set_title(
_("Properties for %s") % self.factory.get_longname()) _("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.setElement(self.element, self.properties)
self.elementsettings.add_widgets( self.elementsettings.add_widgets(GstElementSettingsWidget.make_property_widget,
GstElementSettingsWidget.make_property_widget, with_reset_button=True) with_reset_button=True,
caps_values=caps_values)
# Try to avoid scrolling, whenever possible. # Try to avoid scrolling, whenever possible.
screen_height = self.window.get_screen().get_height() screen_height = self.window.get_screen().get_height()
...@@ -1145,6 +1225,17 @@ class GstElementSettingsDialog(Loggable): ...@@ -1145,6 +1225,17 @@ class GstElementSettingsDialog(Loggable):
dict: A property name to value map.""" dict: A property name to value map."""
return self.elementsettings.getSettings() 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): def _resetValuesClickedCb(self, unused_button):
self.resetAll() self.resetAll()
......
...@@ -237,6 +237,15 @@ class TestCase(unittest.TestCase, Loggable): ...@@ -237,6 +237,15 @@ class TestCase(unittest.TestCase, Loggable):
expect_selected) expect_selected)
self.assertEqual(ges_clip.selected.selected, 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 @contextlib.contextmanager
def created_project_file(asset_uri): def created_project_file(asset_uri):
......
...@@ -176,9 +176,9 @@ class TestRender(BaseTestMediaLibrary): ...@@ -176,9 +176,9 @@ class TestRender(BaseTestMediaLibrary):
preset_combo.connect("changed", preset_changed_cb, changed) preset_combo.connect("changed", preset_changed_cb, changed)
test_data = [ test_data = [
("test", {'aencoder': "vorbisenc", ("test", {"aencoder": "vorbisenc",
'vencoder': "theoraenc", "vencoder": "theoraenc",
'muxer': "oggmux"}), "muxer": "oggmux"}),
("test_ogg-vp8-opus", { ("test_ogg-vp8-opus", {
"aencoder": "opusenc", "aencoder": "opusenc",
"vencoder": ["vp8enc", "vaapivp8enc"], "vencoder": ["vp8enc", "vaapivp8enc"],
...@@ -236,13 +236,13 @@ class TestRender(BaseTestMediaLibrary): ...@@ -236,13 +236,13 @@ class TestRender(BaseTestMediaLibrary):
project = self.create_simple_project() project = self.create_simple_project()
dialog = self.create_rendering_dialog(project) dialog = self.create_rendering_dialog(project)
preset_combo = dialog.render_presets.combo 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) self.assertIsNotNone(i)
preset_combo.set_active(i) preset_combo.set_active(i)
# Check the 'test' profile is selected # Check the 'test' profile is selected
active_iter = preset_combo.get_active_iter() 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 # Remove current profile and verify it has been removed
dialog.render_presets.action_remove.activate() dialog.render_presets.action_remove.activate()
...@@ -256,12 +256,12 @@ class TestRender(BaseTestMediaLibrary): ...@@ -256,12 +256,12 @@ class TestRender(BaseTestMediaLibrary):
self.assertTrue(dialog.render_presets.action_save.get_enabled()) self.assertTrue(dialog.render_presets.action_save.get_enabled())
dialog.render_presets.action_save.activate(None) dialog.render_presets.action_save.activate(None)
self.assertEqual([i[0] for i in preset_combo.props.model], 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() 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): def setup_project_with_profile(self, profile_name):
"""Checks that rendering with the specified profile works.""" """Creates a simple project, open the render dialog and select @profile_name."""
project = self.create_simple_project() project = self.create_simple_project()
dialog = self.create_rendering_dialog(project) dialog = self.create_rendering_dialog(project)
...@@ -272,7 +272,11 @@ class TestRender(BaseTestMediaLibrary): ...@@ -272,7 +272,11 @@ class TestRender(BaseTestMediaLibrary):
self.assertIsNotNone(i) self.assertIsNotNone(i)
preset_combo.set_active(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): def render(self, dialog):
"""Renders pipeline from @dialog.""" """Renders pipeline from @dialog."""
...@@ -352,3 +356,26 @@ class TestRender(BaseTestMediaLibrary): ...@@ -352,3 +356,26 @@ class TestRender(BaseTestMediaLibrary):
def test_rendering_with_default_profile(self): def test_rendering_with_default_profile(self):
"""Tests rendering a simple timeline with the default profile.""" """Tests rendering a simple timeline with the default profile."""
self.check_simple_rendering_profile(None) 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