Exemplo n.º 1
0
class RenderDialog(Loggable):
    """Render dialog box.

    @ivar preferred_aencoder: The last audio encoder selected by the user.
    @type preferred_aencoder: str
    @ivar preferred_vencoder: The last video encoder selected by the user.
    @type preferred_vencoder: str
    """
    INHIBIT_REASON = _("Currently rendering")

    def __init__(self, app, project, pipeline=None):

        from pitivi.preset import RenderPresetManager

        Loggable.__init__(self)

        self.app = app
        self.project = project
        self.system = app.system

        if pipeline is not None:
            self._pipeline = pipeline
        else:
            self._pipeline = self.project.pipeline

        self.outfile = None
        self.notification = None

        # Variables to keep track of progress indication timers:
        self._filesizeEstimateTimer = self._timeEstimateTimer = None
        self._is_rendering = False
        self._rendering_is_paused = False
        self.current_position = None
        self._time_started = 0
        self._time_spent_paused = 0  # Avoids the ETA being wrong on resume

        # Various gstreamer signal connection ID's
        # {object: sigId}
        self._gstSigId = {}

        self._createUi()

        # FIXME: re-enable this widget when bug #637078 is implemented
        self.selected_only_button.destroy()

        # Directory and Filename
        self.filebutton.set_current_folder(self.app.settings.lastExportFolder)
        if not self.project.name:
            self.updateFilename(_("Untitled"))
        else:
            self.updateFilename(self.project.name)

        # We store these so that when the user tries various container formats,
        # (AKA muxers) we select these a/v encoders, if they are compatible with
        # the current container format.
        self.preferred_vencoder = self.project.vencoder
        self.preferred_aencoder = self.project.aencoder

        self._initializeComboboxModels()
        self._displaySettings()
        self._displayRenderSettings()

        self.window.connect("delete-event", self._deleteEventCb)
        self.project.connect("rendering-settings-changed", self._settingsChanged)

        # Monitor changes

        self.wg = RippleUpdateGroup()
        self.wg.addVertex(self.frame_rate_combo, signal="changed")
        self.wg.addVertex(self.save_render_preset_button, update_func=self._updateRenderSaveButton)
        self.wg.addVertex(self.channels_combo, signal="changed")
        self.wg.addVertex(self.sample_rate_combo, signal="changed")
        self.wg.addVertex(self.muxercombobox, signal="changed")
        self.wg.addVertex(self.audio_encoder_combo, signal="changed")
        self.wg.addVertex(self.video_encoder_combo, signal="changed")
        self.render_presets = RenderPresetManager()
        self.render_presets.loadAll()

        self._fillPresetsTreeview(
            self.render_preset_treeview,
            self.render_presets,
            self._updateRenderPresetButtons)

        self.wg.addEdge(self.frame_rate_combo, self.save_render_preset_button)
        self.wg.addEdge(self.audio_encoder_combo, self.save_render_preset_button)
        self.wg.addEdge(self.video_encoder_combo, self.save_render_preset_button)
        self.wg.addEdge(self.muxercombobox, self.save_render_preset_button)
        self.wg.addEdge(self.channels_combo, self.save_render_preset_button)
        self.wg.addEdge(self.sample_rate_combo, self.save_render_preset_button)

        self._infobarForPresetManager = {self.render_presets: self.render_preset_infobar}

        # Bind widgets to RenderPresetsManager
        self.bindCombo(self.render_presets, "channels", self.channels_combo)
        self.bindCombo(self.render_presets, "sample-rate", self.sample_rate_combo)
        self.bindCombo(self.render_presets, "acodec", self.audio_encoder_combo)
        self.bindCombo(self.render_presets, "vcodec", self.video_encoder_combo)
        self.bindCombo(self.render_presets, "container", self.muxercombobox)
        self.bindCombo(self.render_presets, "frame-rate", self.frame_rate_combo)
        self.bindHeight(self.render_presets)
        self.bindWidth(self.render_presets)

        self.createNoPreset(self.render_presets)

    def createNoPreset(self, mgr):
        mgr.prependPreset(_("No preset"), {
            "channels": int(get_combo_value(self.channels_combo)),
            "sample-rate": int(get_combo_value(self.sample_rate_combo)),
            "acodec": get_combo_value(self.audio_encoder_combo).get_name(),
            "vcodec": get_combo_value(self.video_encoder_combo).get_name(),
            "container": get_combo_value(self.muxercombobox).get_name(),
            "frame-rate": Gst.Fraction(
                int(get_combo_value(self.frame_rate_combo).num),
                int(get_combo_value(self.frame_rate_combo).denom)),
            "height": self.project.videoheight,
            "width": self.project.videowidth})

    def bindCombo(self, mgr, name, widget):
        if name == "container":
            mgr.bindWidget(name,
                lambda x: self.muxer_setter(widget, x),
                lambda: get_combo_value(widget).get_name())

        elif name == "acodec":
            mgr.bindWidget(name,
                lambda x: self.acodec_setter(widget, x),
                lambda: get_combo_value(widget).get_name())

        elif name == "vcodec":
            mgr.bindWidget(name,
                lambda x: self.vcodec_setter(widget, x),
                lambda: get_combo_value(widget).get_name())

        elif name == "sample-rate":
            mgr.bindWidget(name,
                lambda x: self.sample_rate_setter(widget, x),
                lambda: get_combo_value(widget))

        elif name == "channels":
            mgr.bindWidget(name,
                lambda x: self.channels_setter(widget, x),
                lambda: get_combo_value(widget))

        elif name == "frame-rate":
            mgr.bindWidget(name,
                lambda x: self.framerate_setter(widget, x),
                lambda: get_combo_value(widget))

    def muxer_setter(self, widget, value):
        set_combo_value(widget, Gst.ElementFactory.find(value))
        self.project.setEncoders(muxer=value)

        # Update the extension of the filename.
        basename = os.path.splitext(self.fileentry.get_text())[0]
        self.updateFilename(basename)

        # Update muxer-dependent widgets.
        self.muxer_combo_changing = True
        try:
            self.updateAvailableEncoders()
        finally:
            self.muxer_combo_changing = False

    def acodec_setter(self, widget, value):
        set_combo_value(widget, Gst.ElementFactory.find(value))
        self.project.aencoder = value
        if not self.muxer_combo_changing:
            # The user directly changed the audio encoder combo.
            self.preferred_aencoder = value

    def vcodec_setter(self, widget, value):
        set_combo_value(widget, Gst.ElementFactory.find(value))
        self.project.setEncoders(vencoder=value)
        if not self.muxer_combo_changing:
            # The user directly changed the video encoder combo.
            self.preferred_vencoder = value

    def sample_rate_setter(self, widget, value):
        self.project.audiorate = set_combo_value(widget, value)

    def channels_setter(self, widget, value):
        self.project.audiochannels = set_combo_value(widget, value)

    def framerate_setter(self, widget, value):
        self.project.videorate = set_combo_value(widget, value)

    def bindHeight(self, mgr):
        mgr.bindWidget("height",
                    lambda x: setattr(self.project, "videoheight", x),
                    lambda: 0)

    def bindWidth(self, mgr):
        mgr.bindWidget("width",
                    lambda x: setattr(self.project, "videowidth", x),
                    lambda: 0)

    def _fillPresetsTreeview(self, treeview, mgr, update_buttons_func):
        """Set up the specified treeview to display the specified presets.

        @param treeview: The treeview for displaying the presets.
        @type treeview: TreeView
        @param mgr: The preset manager.
        @type mgr: PresetManager
        @param update_buttons_func: A function which updates the buttons for
        removing and saving a preset, enabling or disabling them accordingly.
        @type update_buttons_func: function
        """
        renderer = Gtk.CellRendererText()
        renderer.props.editable = True
        column = Gtk.TreeViewColumn("Preset", renderer, text=0)
        treeview.append_column(column)
        treeview.props.headers_visible = False
        model = mgr.getModel()
        treeview.set_model(model)
        model.connect("row-inserted", self._newPresetCb, column, renderer, treeview)
        renderer.connect("edited", self._presetNameEditedCb, mgr)
        renderer.connect("editing-started", self._presetNameEditingStartedCb, mgr)
        treeview.get_selection().connect("changed", self._presetChangedCb,
                                        mgr, update_buttons_func)
        treeview.connect("focus-out-event", self._treeviewDefocusedCb, mgr)

    def _newPresetCb(self, model, path, iter_, column, renderer, treeview):
        """Handle the addition of a preset to the model of the preset manager.
        """
        treeview.set_cursor_on_cell(path, column, renderer, start_editing=True)
        treeview.grab_focus()

    def _presetNameEditedCb(self, renderer, path, new_text, mgr):
        """Handle the renaming of a preset."""
        from pitivi.preset import DuplicatePresetNameException

        try:
            mgr.renamePreset(path, new_text)
            self._updateRenderPresetButtons()
        except DuplicatePresetNameException:
            error_markup = _('"%s" already exists.') % new_text
            self._showPresetManagerError(mgr, error_markup)

    def _presetNameEditingStartedCb(self, renderer, editable, path, mgr):
        """Handle the start of a preset renaming."""
        self._hidePresetManagerError(mgr)

    def _treeviewDefocusedCb(self, widget, event, mgr):
        """Handle the treeview loosing the focus."""
        self._hidePresetManagerError(mgr)

    def _showPresetManagerError(self, mgr, error_markup):
        """Show the specified error on the infobar associated with the manager.

        @param mgr: The preset manager for which to show the error.
        @type mgr: PresetManager
        """
        infobar = self._infobarForPresetManager[mgr]
        # The infobar must contain exactly one object in the content area:
        # a label for displaying the error.
        label = infobar.get_content_area().children()[0]
        label.set_markup(error_markup)
        infobar.show()

    def _hidePresetManagerError(self, mgr):
        """Hide the error infobar associated with the manager.

        @param mgr: The preset manager for which to hide the error infobar.
        @type mgr: PresetManager
        """
        infobar = self._infobarForPresetManager[mgr]
        infobar.hide()

    def _updateRenderSaveButton(self, unused_in, button):
        button.set_sensitive(self.render_presets.isSaveButtonSensitive())

    @staticmethod
    def _getUniquePresetName(mgr):
        """Get a unique name for a new preset for the specified PresetManager.
        """
        existing_preset_names = list(mgr.getPresetNames())
        preset_name = _("New preset")
        i = 1
        while preset_name in existing_preset_names:
            preset_name = _("New preset %d") % i
            i += 1
        return preset_name

    def _addRenderPresetButtonClickedCb(self, button):
        preset_name = self._getUniquePresetName(self.render_presets)
        self.render_presets.addPreset(preset_name, {
            "channels": int(get_combo_value(self.channels_combo)),
            "sample-rate": int(get_combo_value(self.sample_rate_combo)),
            "acodec": get_combo_value(self.audio_encoder_combo).get_name(),
            "vcodec": get_combo_value(self.video_encoder_combo).get_name(),
            "container": get_combo_value(self.muxercombobox).get_name(),
            "frame-rate": Gst.Fraction(int(get_combo_value(self.frame_rate_combo).num),
                            int(get_combo_value(self.frame_rate_combo).denom)),
            "height": 0,
            "width": 0})

        self.render_presets.restorePreset(preset_name)
        self._updateRenderPresetButtons()

    def _saveRenderPresetButtonClickedCb(self, button):
        self.render_presets.saveCurrentPreset()
        self.save_render_preset_button.set_sensitive(False)
        self.remove_render_preset_button.set_sensitive(True)

    def _updateRenderPresetButtons(self):
        can_save = self.render_presets.isSaveButtonSensitive()
        self.save_render_preset_button.set_sensitive(can_save)
        can_remove = self.render_presets.isRemoveButtonSensitive()
        self.remove_render_preset_button.set_sensitive(can_remove)

    def _removeRenderPresetButtonClickedCb(self, button):
        selection = self.render_preset_treeview.get_selection()
        model, iter_ = selection.get_selected()
        if iter_:
            self.render_presets.removePreset(model[iter_][0])

    def _presetChangedCb(self, selection, mgr, update_preset_buttons_func):
        """Handle the selection of a preset."""
        model, iter_ = selection.get_selected()
        if iter_:
            self.selected_preset = model[iter_][0]
        else:
            self.selected_preset = None

        mgr.restorePreset(self.selected_preset)
        self._displaySettings()
        update_preset_buttons_func()
        self._hidePresetManagerError(mgr)

    def _createUi(self):
        builder = Gtk.Builder()
        builder.add_from_file(os.path.join(configure.get_ui_dir(), "renderingdialog.ui"))
        builder.connect_signals(self)

        self.window = builder.get_object("render-dialog")
        self.selected_only_button = builder.get_object("selected_only_button")
        self.video_output_checkbutton = builder.get_object("video_output_checkbutton")
        self.audio_output_checkbutton = builder.get_object("audio_output_checkbutton")
        self.render_button = builder.get_object("render_button")
        self.video_settings_button = builder.get_object("video_settings_button")
        self.audio_settings_button = builder.get_object("audio_settings_button")
        self.frame_rate_combo = builder.get_object("frame_rate_combo")
        self.scale_spinbutton = builder.get_object("scale_spinbutton")
        self.channels_combo = builder.get_object("channels_combo")
        self.sample_rate_combo = builder.get_object("sample_rate_combo")
        self.muxercombobox = builder.get_object("muxercombobox")
        self.audio_encoder_combo = builder.get_object("audio_encoder_combo")
        self.video_encoder_combo = builder.get_object("video_encoder_combo")
        self.filebutton = builder.get_object("filebutton")
        self.fileentry = builder.get_object("fileentry")
        self.resolution_label = builder.get_object("resolution_label")
        self.render_preset_treeview = builder.get_object("render_preset_treeview")
        self.save_render_preset_button = builder.get_object("save_render_preset_button")
        self.remove_render_preset_button = builder.get_object("remove_render_preset_button")
        self.render_preset_infobar = builder.get_object("render-preset-infobar")

        icon = os.path.join(configure.get_pixmap_dir(), "pitivi-render-16.png")
        self.window.set_icon_from_file(icon)
        self.window.set_transient_for(self.app.gui)

        # Set the shading style in the toolbar below presets
        presets_toolbar = builder.get_object("render_presets_toolbar")
        presets_toolbar.get_style_context().add_class("inline-toolbar")

    def _settingsChanged(self, project, key, value):
        self.updateResolution()

    def _initializeComboboxModels(self):
        # Avoid loop import
        self.frame_rate_combo.set_model(frame_rates)
        self.channels_combo.set_model(audio_channels)
        self.sample_rate_combo.set_model(audio_rates)
        self.muxercombobox.set_model(factorylist(CachedEncoderList().muxers))

    def _displaySettings(self):
        """Display the settings that also change in the ProjectSettingsDialog.
        """
        # Video settings
        set_combo_value(self.frame_rate_combo, self.project.videorate)
        # Audio settings
        set_combo_value(self.channels_combo, self.project.audiochannels)
        set_combo_value(self.sample_rate_combo, self.project.audiorate)

    def _displayRenderSettings(self):
        """Display the settings which can be changed only in the RenderDialog.
        """
        # Video settings
        # note: this will trigger an update of the video resolution label
        self.scale_spinbutton.set_value(self.project.render_scale)
        # Muxer settings
        # note: this will trigger an update of the codec comboboxes
        set_combo_value(self.muxercombobox,
            Gst.ElementFactory.find(self.project.muxer))

    def _checkForExistingFile(self, *args):
        """
        Display a warning icon and tooltip if the file path already exists.
        """
        path = self.filebutton.get_current_folder()
        if not path:
            # This happens when the window is initialized.
            return
        warning_icon = Gtk.STOCK_DIALOG_WARNING
        filename = self.fileentry.get_text()
        if not filename:
            tooltip_text = _("A file name is required.")
        elif filename and os.path.exists(os.path.join(path, filename)):
            tooltip_text = _("This file already exists.\n"
                             "If you don't want to overwrite it, choose a "
                             "different file name or folder.")
        else:
            warning_icon = None
            tooltip_text = None
        self.fileentry.set_icon_from_stock(1, warning_icon)
        self.fileentry.set_icon_tooltip_text(1, tooltip_text)

    def _getFilesizeEstimate(self):
        """
        Using the current render output's filesize and position in the timeline,
        return a human-readable (ex: "14 MB") estimate of the final filesize.

        Estimates in megabytes (over 30 MB) are rounded to the nearest 10 MB
        to smooth out small variations. You'd be surprised how imprecision can
        improve perceived accuracy.
        """
        if not self.current_position or self.current_position == 0:
            return None

        current_filesize = os.stat(path_from_uri(self.outfile)).st_size
        length = self.app.current_project.timeline.props.duration
        estimated_size = float(current_filesize * float(length) / self.current_position)
        # Now let's make it human-readable (instead of octets).
        # If it's in the giga range (10⁹) instead of mega (10⁶), use 2 decimals
        if estimated_size > 10e8:
            gigabytes = estimated_size / (10 ** 9)
            return _("%.2f GB" % gigabytes)
        else:
            megabytes = int(estimated_size / (10 ** 6))
            if megabytes > 30:
                megabytes = int(round(megabytes, -1))  # -1 means round to 10
            return _("%d MB" % megabytes)

    def updateFilename(self, basename):
        """Updates the filename UI element to show the specified file name."""
        extension = extension_for_muxer(self.project.muxer)
        if extension:
            name = "%s%s%s" % (basename, os.path.extsep, extension)
        else:
            name = basename
        self.fileentry.set_text(name)

    def updateAvailableEncoders(self):
        """Update the encoder comboboxes to show the available encoders."""
        encoders = CachedEncoderList()
        vencoder_model = factorylist(encoders.video_combination[self.project.muxer])
        self.video_encoder_combo.set_model(vencoder_model)

        aencoder_model = factorylist(encoders.audio_combination[self.project.muxer])
        self.audio_encoder_combo.set_model(aencoder_model)

        self._updateEncoderCombo(self.video_encoder_combo, self.preferred_vencoder)
        self._updateEncoderCombo(self.audio_encoder_combo, self.preferred_aencoder)

    def _updateEncoderCombo(self, encoder_combo, preferred_encoder):
        """Select the specified encoder for the specified encoder combo."""
        if preferred_encoder:
            # A preference exists, pick it if it can be found in
            # the current model of the combobox.
            vencoder = Gst.ElementFactory.find(preferred_encoder)
            set_combo_value(encoder_combo, vencoder, default_index=0)
        else:
            # No preference exists, pick the first encoder from
            # the current model of the combobox.
            encoder_combo.set_active(0)

    def _elementSettingsDialog(self, factory, settings_attr):
        """Open a dialog to edit the properties for the specified factory.

        @param factory: An element factory whose properties the user will edit.
        @type factory: Gst.ElementFactory
        @param settings_attr: The MultimediaSettings attribute holding
        the properties.
        @type settings_attr: str
        """
        properties = getattr(self.project, settings_attr)
        self.dialog = GstElementSettingsDialog(factory, properties=properties,
                                            parent_window=self.window, isControllable=False)
        self.dialog.ok_btn.connect("clicked", self._okButtonClickedCb, settings_attr)

    def _showRenderErrorDialog(self, error, details):
        primary_message = _("Sorry, something didn’t work right.")
        secondary_message = _("An error occured while trying to render your "
            "project. You might want to check our troubleshooting guide or "
            "file a bug report. See the details below for some basic "
            "information that may help identify the problem.")

        dialog = Gtk.MessageDialog(self.window, Gtk.DialogFlags.MODAL,
            Gtk.MessageType.ERROR, Gtk.ButtonsType.OK,
            primary_message)
        dialog.set_property("secondary-text", secondary_message)

        expander = Gtk.Expander()
        expander.set_label(_("Details"))
        details_label = Gtk.Label(str(error) + "\n\n" + str(details))
        details_label.set_line_wrap(True)
        details_label.set_selectable(True)
        expander.add(details_label)
        dialog.get_message_area().add(expander)
        dialog.show_all()  # Ensure the expander and its children show up
        dialog.run()
        dialog.destroy()

    def startAction(self):
        """ Start the render process """
        self._pipeline.set_state(Gst.State.NULL)
        # FIXME: https://github.com/pitivi/gst-editing-services/issues/23
        self._pipeline.set_mode(GES.PipelineFlags.RENDER)
        encodebin = self._pipeline.get_by_name("internal-encodebin")
        self._gstSigId[encodebin] = encodebin.connect("element-added", self._elementAddedCb)
        self._pipeline.set_state(Gst.State.PLAYING)
        self._is_rendering = True
        self._time_started = time.time()

    def _cancelRender(self, *unused_args):
        self.debug("Aborting render")
        self._shutDown()
        self._destroyProgressWindow()

    def _shutDown(self):
        """ The render process has been aborted, shutdown the gstreamer pipeline
        and disconnect from its signals """
        self._is_rendering = False
        self._rendering_is_paused = False
        self._time_spent_paused = 0
        self._pipeline.set_state(Gst.State.NULL)
        self._disconnectFromGst()
        self._pipeline.set_mode(GES.PipelineFlags.FULL_PREVIEW)

    def _pauseRender(self, progress):
        self._rendering_is_paused = self.progress.play_pause_button.get_active()
        if self._rendering_is_paused:
            self._last_timestamp_when_pausing = time.time()
        else:
            self._time_spent_paused += time.time() - self._last_timestamp_when_pausing
            self.debug("Resuming render after %d seconds in pause" % self._time_spent_paused)
        self.app.current_project.pipeline.togglePlayback()

    def _destroyProgressWindow(self):
        """ Handle the completion or the cancellation of the render process. """
        self.progress.window.destroy()
        self.progress = None
        self.window.show()  # Show the rendering dialog again

    def _disconnectFromGst(self):
        for obj, id in self._gstSigId.iteritems():
            obj.disconnect(id)
        self._gstSigId = {}
        try:
            self.app.current_project.pipeline.disconnect_by_func(self._updatePositionCb)
        except TypeError:
            # The render was successful, so this was already disconnected
            pass

    def destroy(self):
        self.window.destroy()

    #------------------- Callbacks ------------------------------------------#

    #-- UI callbacks
    def _okButtonClickedCb(self, unused_button, settings_attr):
        setattr(self, settings_attr, self.dialog.getSettings())
        self.dialog.window.destroy()

    def _renderButtonClickedCb(self, unused_button):
        """
        The render button inside the render dialog has been clicked,
        start the rendering process.
        """
        self.outfile = os.path.join(self.filebutton.get_uri(),
                                    self.fileentry.get_text())
        self.progress = RenderingProgressDialog(self.app, self)
        self.window.hide()  # Hide the rendering settings dialog while rendering

        # Now find a format to set on the restriction caps.
        # The reason is we can't send different formats on the encoders.
        factory = Gst.ElementFactory.find(self.project.vencoder)
        for struct in factory.get_static_pad_templates():
            if struct.direction == Gst.PadDirection.SINK:
                caps = struct.get_caps()
                fixed = caps.fixate()
                fmt = fixed.get_structure(0).get_value("format")
                self.project.video_profile.get_restriction()[0]["format"] = fmt
                break

        self._pipeline.set_render_settings(self.outfile, self.project.container_profile)
        self.startAction()
        self.progress.window.show()
        self.progress.connect("cancel", self._cancelRender)
        self.progress.connect("pause", self._pauseRender)
        bus = self._pipeline.get_bus()
        bus.add_signal_watch()
        self._gstSigId[bus] = bus.connect('message', self._busMessageCb)
        self.app.current_project.pipeline.connect("position", self._updatePositionCb)
        # Force writing the config now, or the path will be reset
        # if the user opens the rendering dialog again
        self.app.settings.lastExportFolder = self.filebutton.get_current_folder()
        self.app.settings.storeSettings()

    def _closeButtonClickedCb(self, unused_button):
        self.debug("Render dialog's Close button clicked")
        self.destroy()

    def _deleteEventCb(self, window, event):
        self.debug("Render dialog is being deleted")
        self.destroy()

    def _containerContextHelpClickedCb(self, unused_button):
        show_user_manual("codecscontainers")

    #-- Periodic (timer) callbacks
    def _updateTimeEstimateCb(self):
        if self._rendering_is_paused:
            return True  # Do nothing until we resume rendering
        elif self._is_rendering:
            timediff = time.time() - self._time_started - self._time_spent_paused
            length = self.app.current_project.timeline.props.duration
            totaltime = (timediff * float(length) / float(self.current_position)) - timediff
            time_estimate = beautify_ETA(int(totaltime * Gst.SECOND))
            if time_estimate:
                self.progress.updateProgressbarETA(time_estimate)
            return True
        else:
            self._timeEstimateTimer = None
            self.debug("Stopping the ETA timer")
            return False  # Stop the timer

    def _updateFilesizeEstimateCb(self):
        if self._rendering_is_paused:
            return True  # Do nothing until we resume rendering
        elif self._is_rendering:
            est_filesize = self._getFilesizeEstimate()
            if est_filesize:
                self.progress.setFilesizeEstimate(est_filesize)
            return True
        else:
            self.debug("Stopping the filesize estimation timer")
            self._filesizeEstimateTimer = None
            return False  # Stop the timer

    #-- GStreamer callbacks
    def _busMessageCb(self, unused_bus, message):
        if message.type == Gst.MessageType.EOS:  # Render complete
            self.debug("got EOS message, render complete")
            self._shutDown()
            self.progress.progressbar.set_text(_("Render complete"))
            self.progress.window.set_title(_("Render complete"))
            self.progress.setFilesizeEstimate(None)
            if has_libnotify:
                Notify.init("pitivi")
                if not self.progress.window.is_active():
                    self.notification = Notify.Notification.new(_("Render complete"), _('"%s" has finished rendering.' % self.fileentry.get_text()), "pitivi")
                    self.notification.show()
            if has_canberra:
                canberra = pycanberra.Canberra()
                canberra.play(1, pycanberra.CA_PROP_EVENT_ID, "complete-media", None)
            self.progress.play_rendered_file_button.show()
            self.progress.close_button.show()
            self.progress.cancel_button.hide()
            self.progress.play_pause_button.hide()

        elif message.type == Gst.MessageType.ERROR:
            # Errors in a GStreamer pipeline are fatal. If we encounter one,
            # we should abort and show the error instead of sitting around.
            error, details = message.parse_error()
            self._cancelRender()
            self._showRenderErrorDialog(error, details)

        elif message.type == Gst.MessageType.STATE_CHANGED and self.progress:
            prev, state, pending = message.parse_state_changed()
            if message.src == self._pipeline:
                state_really_changed = pending == Gst.State.VOID_PENDING
                if state_really_changed:
                    if state == Gst.State.PLAYING:
                        self.debug("Rendering started/resumed, inhibiting sleep")
                        self.system.inhibitSleep(RenderDialog.INHIBIT_REASON)
                    else:
                        self.system.uninhibitSleep(RenderDialog.INHIBIT_REASON)

    def _updatePositionCb(self, pipeline, position):
        """
        Unlike other progression indicator callbacks, this one occurs every time
        the pipeline emits a position changed signal, which is *very* often.
        This should only be used for a smooth progressbar/percentage, not text.
        """
        self.current_position = position
        if not self.progress or not position:
            return

        length = self.app.current_project.timeline.props.duration
        fraction = float(min(position, length)) / float(length)
        self.progress.updatePosition(fraction)

        # In order to have enough averaging, only display the ETA after 5s
        timediff = time.time() - self._time_started
        if not self._timeEstimateTimer:
            if timediff < 6:
                self.progress.progressbar.set_text(_("Estimating..."))
            else:
                self._timeEstimateTimer = GLib.timeout_add_seconds(3, self._updateTimeEstimateCb)

        # Filesize is trickier and needs more time to be meaningful:
        if not self._filesizeEstimateTimer and (fraction > 0.33 or timediff > 180):
            self._filesizeEstimateTimer = GLib.timeout_add_seconds(5, self._updateFilesizeEstimateCb)

    def _elementAddedCb(self, bin, element):
        """
        Setting properties on Gst.Element-s has they are added to the
        Gst.Encodebin
        """
        factory = element.get_factory()
        settings = {}
        if factory == get_combo_value(self.video_encoder_combo):
            settings = self.project.vcodecsettings
        elif factory == get_combo_value(self.audio_encoder_combo):
            settings = self.project.acodecsettings

        for propname, value in settings.iteritems():
            element.set_property(propname, value)
            self.debug("Setting %s to %s", propname, value)

    #-- Settings changed callbacks
    def _scaleSpinbuttonChangedCb(self, button):
        render_scale = self.scale_spinbutton.get_value()
        self.project.render_scale = render_scale
        self.updateResolution()

    def updateResolution(self):
        width, height = self.project.getVideoWidthAndHeight(render=True)
        self.resolution_label.set_text(u"%d×%d" % (width, height))

    def _projectSettingsButtonClickedCb(self, button):
        from pitivi.project import ProjectSettingsDialog
        dialog = ProjectSettingsDialog(self.window, self.project)
        dialog.window.run()

    def _audioOutputCheckbuttonToggledCb(self, audio):
        active = self.audio_output_checkbutton.get_active()
        if active:
            self.channels_combo.set_sensitive(True)
            self.sample_rate_combo.set_sensitive(True)
            self.audio_encoder_combo.set_sensitive(True)
            self.audio_settings_button.set_sensitive(True)
            self.render_button.set_sensitive(True)
        else:
            self.channels_combo.set_sensitive(False)
            self.sample_rate_combo.set_sensitive(False)
            self.audio_encoder_combo.set_sensitive(False)
            self.audio_settings_button.set_sensitive(False)
            if not self.video_output_checkbutton.get_active():
                self.render_button.set_sensitive(False)

    def _videoOutputCheckbuttonToggledCb(self, video):
        active = self.video_output_checkbutton.get_active()
        if active:
            self.scale_spinbutton.set_sensitive(True)
            self.frame_rate_combo.set_sensitive(True)
            self.video_encoder_combo.set_sensitive(True)
            self.video_settings_button.set_sensitive(True)
            self.render_button.set_sensitive(True)
        else:
            self.scale_spinbutton.set_sensitive(False)
            self.frame_rate_combo.set_sensitive(False)
            self.video_encoder_combo.set_sensitive(False)
            self.video_settings_button.set_sensitive(False)
            if not self.audio_output_checkbutton.get_active():
                self.render_button.set_sensitive(False)

    def _frameRateComboChangedCb(self, combo):
        framerate = get_combo_value(combo)
        self.project.framerate = framerate

    def _videoEncoderComboChangedCb(self, combo):
        vencoder = get_combo_value(combo).get_name()
        self.project.vencoder = vencoder

        if not self.muxer_combo_changing:
            # The user directly changed the video encoder combo.
            self.preferred_vencoder = vencoder

    def _videoSettingsButtonClickedCb(self, button):
        factory = get_combo_value(self.video_encoder_combo)
        self._elementSettingsDialog(factory, 'vcodecsettings')

    def _channelsComboChangedCb(self, combo):
        self.project.audiochannels = get_combo_value(combo)

    def _sampleRateComboChangedCb(self, combo):
        self.project.audiorate = get_combo_value(combo)

    def _audioEncoderChangedComboCb(self, combo):
        aencoder = get_combo_value(combo).get_name()
        self.project.aencoder = aencoder
        if not self.muxer_combo_changing:
            # The user directly changed the audio encoder combo.
            self.preferred_aencoder = aencoder

    def _audioSettingsButtonClickedCb(self, button):
        factory = get_combo_value(self.audio_encoder_combo)
        self._elementSettingsDialog(factory, 'acodecsettings')

    def _muxerComboChangedCb(self, muxer_combo):
        """Handle the changing of the container format combobox."""
        self.project.muxer = get_combo_value(muxer_combo).get_name()

        # Update the extension of the filename.
        basename = os.path.splitext(self.fileentry.get_text())[0]
        self.updateFilename(basename)

        # Update muxer-dependent widgets.
        self.muxer_combo_changing = True
        try:
            self.updateAvailableEncoders()
        finally:
            self.muxer_combo_changing = False
Exemplo n.º 2
0
class RenderDialog(Loggable):
    """Render dialog box.

    Args:
        app (Pitivi): The app.
        project (Project): The project to be rendered.

    Attributes:
        preferred_aencoder (str): The last audio encoder selected by the user.
        preferred_vencoder (str): The last video encoder selected by the user.
    """
    INHIBIT_REASON = _("Currently rendering")

    _factory_formats = {}

    def __init__(self, app, project):
        from pitivi.preset import RenderPresetManager

        Loggable.__init__(self)

        self.app = app
        self.project = project
        self.system = app.system
        self._pipeline = self.project.pipeline

        self.outfile = None
        self.notification = None

        # Variables to keep track of progress indication timers:
        self._filesizeEstimateTimer = self._timeEstimateTimer = None
        self._is_rendering = False
        self._rendering_is_paused = False
        self.current_position = None
        self._time_started = 0
        self._time_spent_paused = 0  # Avoids the ETA being wrong on resume

        # Various gstreamer signal connection ID's
        # {object: sigId}
        self._gstSigId = {}

        self.render_presets = RenderPresetManager(self.app.system)
        self.render_presets.loadAll()

        self._createUi()

        # Directory and Filename
        self.filebutton.set_current_folder(self.app.settings.lastExportFolder)
        if not self.project.name:
            self.updateFilename(_("Untitled"))
        else:
            self.updateFilename(self.project.name)

        # We store these so that when the user tries various container formats,
        # (AKA muxers) we select these a/v encoders, if they are compatible with
        # the current container format.
        self.preferred_vencoder = self.project.vencoder
        self.preferred_aencoder = self.project.aencoder
        self.__unproxiedClips = {}

        self._initializeComboboxModels()
        self._displaySettings()
        self._displayRenderSettings()

        self.window.connect("delete-event", self._deleteEventCb)
        self.project.connect(
            "rendering-settings-changed", self._settingsChanged)

        # Monitor changes

        self.wg = RippleUpdateGroup()
        self.wg.addVertex(self.frame_rate_combo, signal="changed")
        self.wg.addVertex(self.channels_combo, signal="changed")
        self.wg.addVertex(self.sample_rate_combo, signal="changed")
        self.wg.addVertex(self.muxercombobox, signal="changed")
        self.wg.addVertex(self.audio_encoder_combo, signal="changed")
        self.wg.addVertex(self.video_encoder_combo, signal="changed")
        self.wg.addVertex(self.preset_menubutton,
                          update_func=self._updatePresetMenuButton)

        self.wg.addEdge(self.frame_rate_combo, self.preset_menubutton)
        self.wg.addEdge(self.audio_encoder_combo, self.preset_menubutton)
        self.wg.addEdge(self.video_encoder_combo, self.preset_menubutton)
        self.wg.addEdge(self.muxercombobox, self.preset_menubutton)
        self.wg.addEdge(self.channels_combo, self.preset_menubutton)
        self.wg.addEdge(self.sample_rate_combo, self.preset_menubutton)

        # Bind widgets to RenderPresetsManager
        self.render_presets.bindWidget(
            "container",
            lambda x: self.muxer_setter(self.muxercombobox, x),
            lambda: get_combo_value(self.muxercombobox).get_name())
        self.render_presets.bindWidget(
            "acodec",
            lambda x: self.acodec_setter(self.audio_encoder_combo, x),
            lambda: get_combo_value(self.audio_encoder_combo).get_name())
        self.render_presets.bindWidget(
            "vcodec",
            lambda x: self.vcodec_setter(self.video_encoder_combo, x),
            lambda: get_combo_value(self.video_encoder_combo).get_name())
        self.render_presets.bindWidget(
            "sample-rate",
            lambda x: self.sample_rate_setter(self.sample_rate_combo, x),
            lambda: get_combo_value(self.sample_rate_combo))
        self.render_presets.bindWidget(
            "channels",
            lambda x: self.channels_setter(self.channels_combo, x),
            lambda: get_combo_value(self.channels_combo))
        self.render_presets.bindWidget(
            "frame-rate",
            lambda x: self.framerate_setter(self.frame_rate_combo, x),
            lambda: get_combo_value(self.frame_rate_combo))
        self.render_presets.bindWidget(
            "height",
            lambda x: setattr(self.project, "videoheight", x),
            lambda: 0)
        self.render_presets.bindWidget(
            "width",
            lambda x: setattr(self.project, "videowidth", x),
            lambda: 0)

    def _updatePresetMenuButton(self, unused_source, unused_target):
        self.render_presets.updateMenuActions()

    def muxer_setter(self, widget, value):
        set_combo_value(widget, Gst.ElementFactory.find(value))
        self.project.setEncoders(muxer=value)

        # Update the extension of the filename.
        basename = os.path.splitext(self.fileentry.get_text())[0]
        self.updateFilename(basename)

        # Update muxer-dependent widgets.
        self.muxer_combo_changing = True
        try:
            self.updateAvailableEncoders()
        finally:
            self.muxer_combo_changing = False

    def acodec_setter(self, widget, value):
        set_combo_value(widget, Gst.ElementFactory.find(value))
        self.project.aencoder = value
        if not self.muxer_combo_changing:
            # The user directly changed the audio encoder combo.
            self.preferred_aencoder = value

    def vcodec_setter(self, widget, value):
        set_combo_value(widget, Gst.ElementFactory.find(value))
        self.project.setEncoders(vencoder=value)
        if not self.muxer_combo_changing:
            # The user directly changed the video encoder combo.
            self.preferred_vencoder = value

    def sample_rate_setter(self, widget, value):
        set_combo_value(widget, value)
        self.project.audiorate = value

    def channels_setter(self, widget, value):
        set_combo_value(widget, value)
        self.project.audiochannels = value

    def framerate_setter(self, widget, value):
        set_combo_value(widget, value)
        self.project.videorate = value

    def _createUi(self):
        builder = Gtk.Builder()
        builder.add_from_file(
            os.path.join(configure.get_ui_dir(), "renderingdialog.ui"))
        builder.connect_signals(self)

        self.window = builder.get_object("render-dialog")
        self.video_output_checkbutton = builder.get_object(
            "video_output_checkbutton")
        self.audio_output_checkbutton = builder.get_object(
            "audio_output_checkbutton")
        self.render_button = builder.get_object("render_button")
        self.video_settings_button = builder.get_object(
            "video_settings_button")
        self.audio_settings_button = builder.get_object(
            "audio_settings_button")
        self.frame_rate_combo = builder.get_object("frame_rate_combo")
        self.scale_spinbutton = builder.get_object("scale_spinbutton")
        self.channels_combo = builder.get_object("channels_combo")
        self.sample_rate_combo = builder.get_object("sample_rate_combo")
        self.muxercombobox = builder.get_object("muxercombobox")
        self.audio_encoder_combo = builder.get_object("audio_encoder_combo")
        self.video_encoder_combo = builder.get_object("video_encoder_combo")
        self.filebutton = builder.get_object("filebutton")
        self.fileentry = builder.get_object("fileentry")
        self.resolution_label = builder.get_object("resolution_label")
        self.presets_combo = builder.get_object("presets_combo")
        self.preset_menubutton = builder.get_object("preset_menubutton")

        self.video_output_checkbutton.props.active = self.project.video_profile.is_enabled()
        self.audio_output_checkbutton.props.active = self.project.audio_profile.is_enabled()

        self.__automatically_use_proxies = builder.get_object(
            "automatically_use_proxies")

        self.__always_use_proxies = builder.get_object("always_use_proxies")
        self.__always_use_proxies.props.group = self.__automatically_use_proxies

        self.__never_use_proxies = builder.get_object("never_use_proxies")
        self.__never_use_proxies.props.group = self.__automatically_use_proxies

        self.render_presets.setupUi(self.presets_combo, self.preset_menubutton)

        icon = os.path.join(configure.get_pixmap_dir(), "pitivi-render-16.png")
        self.window.set_icon_from_file(icon)
        self.window.set_transient_for(self.app.gui)

    def _settingsChanged(self, unused_project, unused_key, unused_value):
        self.updateResolution()

    def _initializeComboboxModels(self):
        # Avoid loop import
        self.frame_rate_combo.set_model(frame_rates)
        self.channels_combo.set_model(audio_channels)
        self.sample_rate_combo.set_model(audio_rates)
        self.muxercombobox.set_model(factorylist(CachedEncoderList().muxers))

    def _displaySettings(self):
        """Displays the settings also in the ProjectSettingsDialog."""
        # Video settings
        set_combo_value(self.frame_rate_combo, self.project.videorate)
        # Audio settings
        set_combo_value(self.channels_combo, self.project.audiochannels)
        set_combo_value(self.sample_rate_combo, self.project.audiorate)

    def _displayRenderSettings(self):
        """Displays the settings available only in the RenderDialog."""
        # Video settings
        # This will trigger an update of the video resolution label.
        self.scale_spinbutton.set_value(self.project.render_scale)
        # Muxer settings
        # This will trigger an update of the codec comboboxes.
        set_combo_value(self.muxercombobox,
                        Gst.ElementFactory.find(self.project.muxer))

    def _checkForExistingFile(self, *unused_args):
        """Displays a warning if the file path already exists."""
        path = self.filebutton.get_current_folder()
        if not path:
            # This happens when the window is initialized.
            return
        warning_icon = "dialog-warning"
        filename = self.fileentry.get_text()
        if not filename:
            tooltip_text = _("A file name is required.")
        elif filename and os.path.exists(os.path.join(path, filename)):
            tooltip_text = _("This file already exists.\n"
                             "If you don't want to overwrite it, choose a "
                             "different file name or folder.")
        else:
            warning_icon = None
            tooltip_text = None
        self.fileentry.set_icon_from_icon_name(1, warning_icon)
        self.fileentry.set_icon_tooltip_text(1, tooltip_text)

    def _getFilesizeEstimate(self):
        """Estimates the final file size.

        Estimates in megabytes (over 30 MB) are rounded to the nearest 10 MB
        to smooth out small variations. You'd be surprised how imprecision can
        improve perceived accuracy.

        Returns:
            str: A human-readable (ex: "14 MB") estimate for the file size.
        """
        if not self.current_position or self.current_position == 0:
            return None

        current_filesize = os.stat(path_from_uri(self.outfile)).st_size
        length = self.project.ges_timeline.props.duration
        estimated_size = float(
            current_filesize * float(length) / self.current_position)
        # Now let's make it human-readable (instead of octets).
        # If it's in the giga range (10⁹) instead of mega (10⁶), use 2 decimals
        if estimated_size > 10e8:
            gigabytes = estimated_size / (10 ** 9)
            return _("%.2f GB" % gigabytes)
        else:
            megabytes = int(estimated_size / (10 ** 6))
            if megabytes > 30:
                megabytes = int(round(megabytes, -1))  # -1 means round to 10
            return _("%d MB" % megabytes)

    def updateFilename(self, basename):
        """Updates the filename UI element to show the specified file name."""
        extension = extension_for_muxer(self.project.muxer)
        if extension:
            name = "%s%s%s" % (basename, os.path.extsep, extension)
        else:
            name = basename
        self.fileentry.set_text(name)

    def updateAvailableEncoders(self):
        """Updates the encoder comboboxes to show the available encoders."""
        encoders = CachedEncoderList()
        vencoder_model = factorylist(
            encoders.video_combination[self.project.muxer])
        self.video_encoder_combo.set_model(vencoder_model)

        aencoder_model = factorylist(
            encoders.audio_combination[self.project.muxer])
        self.audio_encoder_combo.set_model(aencoder_model)

        self._updateEncoderCombo(
            self.video_encoder_combo, self.preferred_vencoder)
        self._updateEncoderCombo(
            self.audio_encoder_combo, self.preferred_aencoder)

    def _updateEncoderCombo(self, encoder_combo, preferred_encoder):
        """Selects the specified encoder for the specified encoder combo."""
        if preferred_encoder:
            # A preference exists, pick it if it can be found in
            # the current model of the combobox.
            vencoder = Gst.ElementFactory.find(preferred_encoder)
            set_combo_value(encoder_combo, vencoder, default_index=0)
        else:
            # No preference exists, pick the first encoder from
            # the current model of the combobox.
            encoder_combo.set_active(0)

    def _elementSettingsDialog(self, factory, settings_attr):
        """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.
        """
        properties = getattr(self.project, settings_attr)
        self.dialog = GstElementSettingsDialog(factory, properties=properties,
                                               parent_window=self.window)
        self.dialog.ok_btn.connect(
            "clicked", self._okButtonClickedCb, settings_attr)

    def _showRenderErrorDialog(self, error, unused_details):
        primary_message = _("Sorry, something didn’t work right.")
        secondary_message = _("An error occurred while trying to render your "
                              "project. You might want to check our "
                              "troubleshooting guide or file a bug report. "
                              "The GStreamer error was:") + "\n\n<i>" + str(error) + "</i>"

        dialog = Gtk.MessageDialog(transient_for=self.window, modal=True,
                                   message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK,
                                   text=primary_message)
        dialog.set_property("secondary-text", secondary_message)
        dialog.set_property("secondary-use-markup", True)
        dialog.show_all()
        dialog.run()
        dialog.destroy()

    def startAction(self):
        """Starts the render process."""
        self._pipeline.set_state(Gst.State.NULL)
        # FIXME: https://github.com/pitivi/gst-editing-services/issues/23
        self._pipeline.set_mode(GES.PipelineFlags.RENDER)
        encodebin = self._pipeline.get_by_name("internal-encodebin")
        self._gstSigId[encodebin] = encodebin.connect(
            "element-added", self._elementAddedCb)
        for element in encodebin.iterate_recurse():
            self._elementAddedCb(encodebin, element)
        self._pipeline.set_state(Gst.State.PLAYING)
        self._is_rendering = True
        self._time_started = time.time()

    def _cancelRender(self, *unused_args):
        self.debug("Aborting render")
        self._shutDown()
        self._destroyProgressWindow()

    def _shutDown(self):
        """Shuts down the pipeline and disconnects from its signals."""
        self._is_rendering = False
        self._rendering_is_paused = False
        self._time_spent_paused = 0
        self._pipeline.set_state(Gst.State.NULL)
        self.__useProxyAssets()
        self._disconnectFromGst()
        self._pipeline.set_mode(GES.PipelineFlags.FULL_PREVIEW)
        self._pipeline.set_state(Gst.State.PAUSED)
        self.project.set_rendering(False)

    def _pauseRender(self, unused_progress):
        self._rendering_is_paused = self.progress.play_pause_button.get_active(
        )
        if self._rendering_is_paused:
            self._last_timestamp_when_pausing = time.time()
        else:
            self._time_spent_paused += time.time(
            ) - self._last_timestamp_when_pausing
            self.debug(
                "Resuming render after %d seconds in pause", self._time_spent_paused)
        self.project.pipeline.togglePlayback()

    def _destroyProgressWindow(self):
        """Handles the completion or the cancellation of the render process."""
        self.progress.window.destroy()
        self.progress = None
        self.window.show()  # Show the rendering dialog again

    def _disconnectFromGst(self):
        for obj, id in self._gstSigId.items():
            obj.disconnect(id)
        self._gstSigId = {}
        try:
            self.project.pipeline.disconnect_by_func(self._updatePositionCb)
        except TypeError:
            # The render was successful, so this was already disconnected
            pass

    def destroy(self):
        self.window.destroy()

    @staticmethod
    def _maybePlayFinishedSound():
        if "pycanberra" in missing_soft_deps:
            return
        import pycanberra
        canberra = pycanberra.Canberra()
        canberra.play(1, pycanberra.CA_PROP_EVENT_ID, "complete-media", None)

    def __maybeUseSourceAsset(self):
        if self.__always_use_proxies.get_active():
            self.debug("Rendering from proxies, not replacing assets")
            return

        for layer in self.app.gui.timeline_ui.ges_timeline.get_layers():
            for clip in layer.get_clips():
                if not isinstance(clip, GES.UriClip):
                    continue

                asset = clip.get_asset()
                asset_target = asset.get_proxy_target()
                if not asset_target:
                    continue

                if self.__automatically_use_proxies.get_active():
                    if self.app.proxy_manager.isAssetFormatWellSupported(
                            asset_target):
                        self.info("Asset %s format well supported, "
                                  "rendering from real asset.",
                                  asset_target.props.id)
                    else:
                        self.info("Asset %s format not well supported, "
                                  "rendering from proxy.",
                                  asset_target.props.id)
                        continue

                if not asset_target.get_error():
                    clip.set_asset(asset_target)
                    self.error("Using %s as an asset (instead of %s)",
                               asset_target.get_id(),
                               asset.get_id())
                    self.__unproxiedClips[clip] = asset

    def __useProxyAssets(self):
        for clip, asset in self.__unproxiedClips.items():
            clip.set_asset(asset)

        self.__unproxiedClips = {}

    # ------------------- Callbacks ------------------------------------------ #

    # -- UI callbacks
    def _okButtonClickedCb(self, unused_button, settings_attr):
        setattr(self.project, settings_attr, self.dialog.getSettings())
        self.dialog.window.destroy()

    def _renderButtonClickedCb(self, unused_button):
        """Starts the rendering process."""
        self.__maybeUseSourceAsset()
        self.outfile = os.path.join(self.filebutton.get_uri(),
                                    self.fileentry.get_text())
        self.progress = RenderingProgressDialog(self.app, self)
        # Hide the rendering settings dialog while rendering
        self.window.hide()

        encoder_string = self.project.vencoder
        try:
            fmt = self._factory_formats[encoder_string]
            self.project.video_profile.get_restriction()[0]["format"] = fmt
        except KeyError:
            # Now find a format to set on the restriction caps.
            # The reason is we can't send different formats on the encoders.
            factory = Gst.ElementFactory.find(self.project.vencoder)
            for struct in factory.get_static_pad_templates():
                if struct.direction == Gst.PadDirection.SINK:
                    caps = Gst.Caps.from_string(struct.get_caps().to_string())
                    fixed = caps.fixate()
                    fmt = fixed.get_structure(0).get_value("format")
                    self.project.setVideoRestriction("format", fmt)
                    self._factory_formats[encoder_string] = fmt
                    break

        self.app.gui.timeline_ui.zoomFit()
        self.project.set_rendering(True)
        self._pipeline.set_render_settings(
            self.outfile, self.project.container_profile)
        self.startAction()
        self.progress.window.show()
        self.progress.connect("cancel", self._cancelRender)
        self.progress.connect("pause", self._pauseRender)
        bus = self._pipeline.get_bus()
        bus.add_signal_watch()
        self._gstSigId[bus] = bus.connect('message', self._busMessageCb)
        self.project.pipeline.connect("position", self._updatePositionCb)
        # Force writing the config now, or the path will be reset
        # if the user opens the rendering dialog again
        self.app.settings.lastExportFolder = self.filebutton.get_current_folder(
        )
        self.app.settings.storeSettings()

    def _closeButtonClickedCb(self, unused_button):
        self.debug("Render dialog's Close button clicked")
        self.destroy()

    def _deleteEventCb(self, unused_window, unused_event):
        self.debug("Render dialog is being deleted")
        self.destroy()

    def _containerContextHelpClickedCb(self, unused_button):
        show_user_manual("codecscontainers")

    # Periodic (timer) callbacks
    def _updateTimeEstimateCb(self):
        if self._rendering_is_paused:
            return True  # Do nothing until we resume rendering
        elif self._is_rendering:
            timediff = time.time() - \
                self._time_started - self._time_spent_paused
            length = self.project.ges_timeline.props.duration
            totaltime = (timediff * float(length) /
                         float(self.current_position)) - timediff
            time_estimate = beautify_ETA(int(totaltime * Gst.SECOND))
            if time_estimate:
                self.progress.updateProgressbarETA(time_estimate)
            return True
        else:
            self._timeEstimateTimer = None
            self.debug("Stopping the ETA timer")
            return False  # Stop the timer

    def _updateFilesizeEstimateCb(self):
        if self._rendering_is_paused:
            return True  # Do nothing until we resume rendering
        elif self._is_rendering:
            est_filesize = self._getFilesizeEstimate()
            if est_filesize:
                self.progress.setFilesizeEstimate(est_filesize)
            return True
        else:
            self.debug("Stopping the filesize estimation timer")
            self._filesizeEstimateTimer = None
            return False  # Stop the timer

    # GStreamer callbacks
    def _busMessageCb(self, unused_bus, message):
        if message.type == Gst.MessageType.EOS:  # Render complete
            self.debug("got EOS message, render complete")
            self._shutDown()
            self.progress.progressbar.set_fraction(1.0)
            self.progress.progressbar.set_text(_("Render complete"))
            self.progress.window.set_title(_("Render complete"))
            self.progress.setFilesizeEstimate(None)
            if not self.progress.window.is_active():
                notification = _(
                    '"%s" has finished rendering.' % self.fileentry.get_text())
                self.notification = self.app.system.desktopMessage(
                    _("Render complete"), notification, "pitivi")
            self._maybePlayFinishedSound()
            self.progress.play_rendered_file_button.show()
            self.progress.close_button.show()
            self.progress.cancel_button.hide()
            self.progress.play_pause_button.hide()

        elif message.type == Gst.MessageType.ERROR:
            # Errors in a GStreamer pipeline are fatal. If we encounter one,
            # we should abort and show the error instead of sitting around.
            error, details = message.parse_error()
            self._cancelRender()
            self._showRenderErrorDialog(error, details)

        elif message.type == Gst.MessageType.STATE_CHANGED and self.progress:
            prev, state, pending = message.parse_state_changed()
            if message.src == self._pipeline:
                state_really_changed = pending == Gst.State.VOID_PENDING
                if state_really_changed:
                    if state == Gst.State.PLAYING:
                        self.debug(
                            "Rendering started/resumed, inhibiting sleep")
                        self.system.inhibitSleep(RenderDialog.INHIBIT_REASON)
                    else:
                        self.system.uninhibitSleep(RenderDialog.INHIBIT_REASON)

    def _updatePositionCb(self, unused_pipeline, position):
        """Updates the progress bar and triggers the update of the file size.

        This one occurs every time the pipeline emits a position changed signal,
        which is *very* often.
        """
        self.current_position = position
        if not self.progress or not position:
            return

        length = self.project.ges_timeline.props.duration
        fraction = float(min(position, length)) / float(length)
        self.progress.updatePosition(fraction)

        # In order to have enough averaging, only display the ETA after 5s
        timediff = time.time() - self._time_started
        if not self._timeEstimateTimer:
            if timediff < 6:
                self.progress.progressbar.set_text(_("Estimating..."))
            else:
                self._timeEstimateTimer = GLib.timeout_add_seconds(
                    3, self._updateTimeEstimateCb)

        # Filesize is trickier and needs more time to be meaningful.
        if not self._filesizeEstimateTimer and (fraction > 0.33 or timediff > 180):
            self._filesizeEstimateTimer = GLib.timeout_add_seconds(
                5, self._updateFilesizeEstimateCb)

    def _elementAddedCb(self, unused_bin, gst_element):
        """Sets properties on the specified Gst.Element."""
        factory = gst_element.get_factory()
        settings = {}
        if factory == get_combo_value(self.video_encoder_combo):
            settings = self.project.vcodecsettings
        elif factory == get_combo_value(self.audio_encoder_combo):
            settings = self.project.acodecsettings

        for propname, value in settings.items():
            gst_element.set_property(propname, value)
            self.debug("Setting %s to %s", propname, value)

    # Settings changed callbacks
    def _scaleSpinbuttonChangedCb(self, unused_button):
        render_scale = self.scale_spinbutton.get_value()
        self.project.render_scale = render_scale
        self.updateResolution()

    def updateResolution(self):
        width, height = self.project.getVideoWidthAndHeight(True)
        self.resolution_label.set_text("%d×%d" % (width, height))

    def _projectSettingsButtonClickedCb(self, unused_button):
        from pitivi.project import ProjectSettingsDialog
        dialog = ProjectSettingsDialog(self.window, self.project, self.app)
        dialog.window.run()

    def _audioOutputCheckbuttonToggledCb(self, unused_audio):
        active = self.audio_output_checkbutton.get_active()
        self.channels_combo.set_sensitive(active)
        self.sample_rate_combo.set_sensitive(active)
        self.audio_encoder_combo.set_sensitive(active)
        self.audio_settings_button.set_sensitive(active)
        self.project.audio_profile.set_enabled(active)
        self.__updateRenderButtonSensitivity()

    def _videoOutputCheckbuttonToggledCb(self, unused_video):
        active = self.video_output_checkbutton.get_active()
        self.scale_spinbutton.set_sensitive(active)
        self.frame_rate_combo.set_sensitive(active)
        self.video_encoder_combo.set_sensitive(active)
        self.video_settings_button.set_sensitive(active)
        self.project.video_profile.set_enabled(active)
        self.__updateRenderButtonSensitivity()

    def __updateRenderButtonSensitivity(self):
        video_enabled = self.video_output_checkbutton.get_active()
        audio_enabled = self.audio_output_checkbutton.get_active()
        self.render_button.set_sensitive(video_enabled or audio_enabled)

    def _frameRateComboChangedCb(self, combo):
        framerate = get_combo_value(combo)
        self.project.framerate = framerate

    def _videoEncoderComboChangedCb(self, combo):
        vencoder = get_combo_value(combo).get_name()
        self.project.vencoder = vencoder

        if not self.muxer_combo_changing:
            # The user directly changed the video encoder combo.
            self.preferred_vencoder = vencoder

    def _videoSettingsButtonClickedCb(self, unused_button):
        factory = get_combo_value(self.video_encoder_combo)
        self._elementSettingsDialog(factory, 'vcodecsettings')

    def _channelsComboChangedCb(self, combo):
        self.project.audiochannels = get_combo_value(combo)

    def _sampleRateComboChangedCb(self, combo):
        self.project.audiorate = get_combo_value(combo)

    def _audioEncoderChangedComboCb(self, combo):
        aencoder = get_combo_value(combo).get_name()
        self.project.aencoder = aencoder
        if not self.muxer_combo_changing:
            # The user directly changed the audio encoder combo.
            self.preferred_aencoder = aencoder

    def _audioSettingsButtonClickedCb(self, unused_button):
        factory = get_combo_value(self.audio_encoder_combo)
        self._elementSettingsDialog(factory, 'acodecsettings')

    def _muxerComboChangedCb(self, muxer_combo):
        """Handles the changing of the container format combobox."""
        self.project.muxer = get_combo_value(muxer_combo).get_name()

        # Update the extension of the filename.
        basename = os.path.splitext(self.fileentry.get_text())[0]
        self.updateFilename(basename)

        # Update muxer-dependent widgets.
        self.muxer_combo_changing = True
        try:
            self.updateAvailableEncoders()
        finally:
            self.muxer_combo_changing = False
Exemplo n.º 3
0
class RenderDialog(Loggable):
    """Render dialog box.

    @ivar preferred_aencoder: The last audio encoder selected by the user.
    @type preferred_aencoder: str
    @ivar preferred_vencoder: The last video encoder selected by the user.
    @type preferred_vencoder: str
    """
    INHIBIT_REASON = _("Currently rendering")

    def __init__(self, app, project, pipeline=None):

        from pitivi.preset import RenderPresetManager

        Loggable.__init__(self)

        self.app = app
        self.project = project
        self.system = app.system
        if pipeline is not None:
            self._pipeline = pipeline
        else:
            self._pipeline = self.project.pipeline

        self.outfile = None
        self.notification = None
        self.timestarted = 0

        # Various gstreamer signal connection ID's
        # {object: sigId}
        self._gstSigId = {}

        self.builder = Gtk.Builder()
        self.builder.add_from_file(os.path.join(configure.get_ui_dir(),
            "renderingdialog.ui"))
        self._setProperties()
        self.builder.connect_signals(self)

        # UI widgets
        icon = os.path.join(configure.get_pixmap_dir(), "pitivi-render-16.png")
        self.window.set_icon_from_file(icon)

        # Set the shading style in the toolbar below presets
        presets_toolbar = self.builder.get_object("render_presets_toolbar")
        presets_toolbar.get_style_context().add_class("inline-toolbar")

        # FIXME: re-enable this widget when bug #637078 is implemented
        self.selected_only_button.destroy()

        # Directory and Filename
        self.filebutton.set_current_folder(self.app.settings.lastExportFolder)
        if not self.project.name:
            self.updateFilename(_("Untitled"))
        else:
            self.updateFilename(self.project.name)

        # We store these so that when the user tries various container formats,
        # (AKA muxers) we select these a/v encoders, if they are compatible with
        # the current container format.
        self.preferred_vencoder = self.project.vencoder
        self.preferred_aencoder = self.project.aencoder

        self._initializeComboboxModels()
        self._displaySettings()
        self._displayRenderSettings()

        self.window.connect("delete-event", self._deleteEventCb)
        self.project.connect("rendering-settings-changed", self._settingsChanged)

        # Monitor changes

        self.wg = RippleUpdateGroup()
        self.wg.addVertex(self.frame_rate_combo, signal="changed")
        self.wg.addVertex(self.save_render_preset_button, update_func=self._updateRenderSaveButton)
        self.wg.addVertex(self.channels_combo, signal="changed")
        self.wg.addVertex(self.sample_rate_combo, signal="changed")
        self.wg.addVertex(self.sample_depth_combo, signal="changed")
        self.wg.addVertex(self.muxercombobox, signal="changed")
        self.wg.addVertex(self.audio_encoder_combo, signal="changed")
        self.wg.addVertex(self.video_encoder_combo, signal="changed")
        self.render_presets = RenderPresetManager()
        self.render_presets.loadAll()

        self._fillPresetsTreeview(
            self.render_preset_treeview,
            self.render_presets,
            self._updateRenderPresetButtons)

        self.wg.addEdge(self.frame_rate_combo, self.save_render_preset_button)
        self.wg.addEdge(self.audio_encoder_combo, self.save_render_preset_button)
        self.wg.addEdge(self.video_encoder_combo, self.save_render_preset_button)
        self.wg.addEdge(self.muxercombobox, self.save_render_preset_button)
        self.wg.addEdge(self.channels_combo, self.save_render_preset_button)
        self.wg.addEdge(self.sample_rate_combo, self.save_render_preset_button)
        self.wg.addEdge(self.sample_depth_combo, self.save_render_preset_button)

        self._infobarForPresetManager = {self.render_presets: self.render_preset_infobar}

        # Bind widgets to RenderPresetsManager
        self.bindCombo(self.render_presets, "channels", self.channels_combo)
        self.bindCombo(self.render_presets, "sample-rate", self.sample_rate_combo)
        self.bindCombo(self.render_presets, "depth", self.sample_depth_combo)
        self.bindCombo(self.render_presets, "acodec", self.audio_encoder_combo)
        self.bindCombo(self.render_presets, "vcodec", self.video_encoder_combo)
        self.bindCombo(self.render_presets, "container", self.muxercombobox)
        self.bindCombo(self.render_presets, "frame-rate", self.frame_rate_combo)
        self.bindHeight(self.render_presets)
        self.bindWidth(self.render_presets)

        self.createNoPreset(self.render_presets)

    def createNoPreset(self, mgr):
        mgr.prependPreset(_("No preset"), {
            "depth": int(get_combo_value(self.sample_depth_combo)),
            "channels": int(get_combo_value(self.channels_combo)),
            "sample-rate": int(get_combo_value(self.sample_rate_combo)),
            "acodec": get_combo_value(self.audio_encoder_combo).get_name(),
            "vcodec": get_combo_value(self.video_encoder_combo).get_name(),
            "container": get_combo_value(self.muxercombobox).get_name(),
            "frame-rate": Gst.Fraction(
                int(get_combo_value(self.frame_rate_combo).num),
                int(get_combo_value(self.frame_rate_combo).denom)),
            "height": self.project.videoheight,
            "width": self.project.videowidth})

    def bindCombo(self, mgr, name, widget):
        if name == "container":
            mgr.bindWidget(name,
                lambda x: self.muxer_setter(widget, x),
                lambda: get_combo_value(widget).get_name())

        elif name == "acodec":
            mgr.bindWidget(name,
                lambda x: self.acodec_setter(widget, x),
                lambda: get_combo_value(widget).get_name())

        elif name == "vcodec":
            mgr.bindWidget(name,
                lambda x: self.vcodec_setter(widget, x),
                lambda: get_combo_value(widget).get_name())

        elif name == "depth":
            mgr.bindWidget(name,
                lambda x: self.sample_depth_setter(widget, x),
                lambda: get_combo_value(widget))

        elif name == "sample-rate":
            mgr.bindWidget(name,
                lambda x: self.sample_rate_setter(widget, x),
                lambda: get_combo_value(widget))

        elif name == "channels":
            mgr.bindWidget(name,
                lambda x: self.channels_setter(widget, x),
                lambda: get_combo_value(widget))

        elif name == "frame-rate":
            mgr.bindWidget(name,
                lambda x: self.framerate_setter(widget, x),
                lambda: get_combo_value(widget))

    def muxer_setter(self, widget, value):
        set_combo_value(widget, Gst.ElementFactory.find(value))
        self.project.setEncoders(muxer=value)

        # Update the extension of the filename.
        basename = os.path.splitext(self.fileentry.get_text())[0]
        self.updateFilename(basename)

        # Update muxer-dependent widgets.
        self.muxer_combo_changing = True
        try:
            self.updateAvailableEncoders()
        finally:
            self.muxer_combo_changing = False

    def acodec_setter(self, widget, value):
        set_combo_value(widget, Gst.ElementFactory.find(value))
        self.project.aencoder = value
        if not self.muxer_combo_changing:
            # The user directly changed the audio encoder combo.
            self.preferred_aencoder = value

    def vcodec_setter(self, widget, value):
        set_combo_value(widget, Gst.ElementFactory.find(value))
        self.project.setEncoders(vencoder=value)
        if not self.muxer_combo_changing:
            # The user directly changed the video encoder combo.
            self.preferred_vencoder = value

    def sample_depth_setter(self, widget, value):
        self.project.audiodepth = set_combo_value(widget, value)

    def sample_rate_setter(self, widget, value):
        self.project.audiorate = set_combo_value(widget, value)

    def channels_setter(self, widget, value):
        self.project.audiochannels = set_combo_value(widget, value)

    def framerate_setter(self, widget, value):
        self.project.videorate = set_combo_value(widget, value)

    def bindHeight(self, mgr):
        mgr.bindWidget("height",
                    lambda x: setattr(self.project, "videoheight", x),
                    lambda: 0)

    def bindWidth(self, mgr):
        mgr.bindWidget("width",
                    lambda x: setattr(self.project, "videowidth", x),
                    lambda: 0)

    def _fillPresetsTreeview(self, treeview, mgr, update_buttons_func):
        """Set up the specified treeview to display the specified presets.

        @param treeview: The treeview for displaying the presets.
        @type treeview: TreeView
        @param mgr: The preset manager.
        @type mgr: PresetManager
        @param update_buttons_func: A function which updates the buttons for
        removing and saving a preset, enabling or disabling them accordingly.
        @type update_buttons_func: function
        """
        renderer = Gtk.CellRendererText()
        renderer.props.editable = True
        column = Gtk.TreeViewColumn("Preset", renderer, text=0)
        treeview.append_column(column)
        treeview.props.headers_visible = False
        model = mgr.getModel()
        treeview.set_model(model)
        model.connect("row-inserted", self._newPresetCb, column, renderer, treeview)
        renderer.connect("edited", self._presetNameEditedCb, mgr)
        renderer.connect("editing-started", self._presetNameEditingStartedCb, mgr)
        treeview.get_selection().connect("changed", self._presetChangedCb,
                                        mgr, update_buttons_func)
        treeview.connect("focus-out-event", self._treeviewDefocusedCb, mgr)

    def _newPresetCb(self, model, path, iter_, column, renderer, treeview):
        """Handle the addition of a preset to the model of the preset manager.
        """
        treeview.set_cursor_on_cell(path, column, renderer, start_editing=True)
        treeview.grab_focus()

    def _presetNameEditedCb(self, renderer, path, new_text, mgr):
        """Handle the renaming of a preset."""
        from pitivi.preset import DuplicatePresetNameException

        try:
            mgr.renamePreset(path, new_text)
            self._updateRenderPresetButtons()
        except DuplicatePresetNameException:
            error_markup = _('"%s" already exists.') % new_text
            self._showPresetManagerError(mgr, error_markup)

    def _presetNameEditingStartedCb(self, renderer, editable, path, mgr):
        """Handle the start of a preset renaming."""
        self._hidePresetManagerError(mgr)

    def _treeviewDefocusedCb(self, widget, event, mgr):
        """Handle the treeview loosing the focus."""
        self._hidePresetManagerError(mgr)

    def _showPresetManagerError(self, mgr, error_markup):
        """Show the specified error on the infobar associated with the manager.

        @param mgr: The preset manager for which to show the error.
        @type mgr: PresetManager
        """
        infobar = self._infobarForPresetManager[mgr]
        # The infobar must contain exactly one object in the content area:
        # a label for displaying the error.
        label = infobar.get_content_area().children()[0]
        label.set_markup(error_markup)
        infobar.show()

    def _hidePresetManagerError(self, mgr):
        """Hide the error infobar associated with the manager.

        @param mgr: The preset manager for which to hide the error infobar.
        @type mgr: PresetManager
        """
        infobar = self._infobarForPresetManager[mgr]
        infobar.hide()

    def _updateRenderSaveButton(self, unused_in, button):
        button.set_sensitive(self.render_presets.isSaveButtonSensitive())

    @staticmethod
    def _getUniquePresetName(mgr):
        """Get a unique name for a new preset for the specified PresetManager.
        """
        existing_preset_names = list(mgr.getPresetNames())
        preset_name = _("New preset")
        i = 1
        while preset_name in existing_preset_names:
            preset_name = _("New preset %d") % i
            i += 1
        return preset_name

    def _addRenderPresetButtonClickedCb(self, button):
        preset_name = self._getUniquePresetName(self.render_presets)
        self.render_presets.addPreset(preset_name, {
            "depth": int(get_combo_value(self.sample_depth_combo)),
            "channels": int(get_combo_value(self.channels_combo)),
            "sample-rate": int(get_combo_value(self.sample_rate_combo)),
            "acodec": get_combo_value(self.audio_encoder_combo).get_name(),
            "vcodec": get_combo_value(self.video_encoder_combo).get_name(),
            "container": get_combo_value(self.muxercombobox).get_name(),
            "frame-rate": Gst.Fraction(int(get_combo_value(self.frame_rate_combo).num),
                            int(get_combo_value(self.frame_rate_combo).denom)),
            "height": 0,
            "width": 0})

        self.render_presets.restorePreset(preset_name)
        self._updateRenderPresetButtons()

    def _saveRenderPresetButtonClickedCb(self, button):
        self.render_presets.saveCurrentPreset()
        self.save_render_preset_button.set_sensitive(False)
        self.remove_render_preset_button.set_sensitive(True)

    def _updateRenderPresetButtons(self):
        can_save = self.render_presets.isSaveButtonSensitive()
        self.save_render_preset_button.set_sensitive(can_save)
        can_remove = self.render_presets.isRemoveButtonSensitive()
        self.remove_render_preset_button.set_sensitive(can_remove)

    def _removeRenderPresetButtonClickedCb(self, button):
        selection = self.render_preset_treeview.get_selection()
        model, iter_ = selection.get_selected()
        if iter_:
            self.render_presets.removePreset(model[iter_][0])

    def _presetChangedCb(self, selection, mgr, update_preset_buttons_func):
        """Handle the selection of a preset."""
        model, iter_ = selection.get_selected()
        if iter_:
            self.selected_preset = model[iter_][0]
        else:
            self.selected_preset = None

        mgr.restorePreset(self.selected_preset)
        self._displaySettings()
        update_preset_buttons_func()
        self._hidePresetManagerError(mgr)

    def _setProperties(self):
        self.window = self.builder.get_object("render-dialog")
        self.selected_only_button = self.builder.get_object("selected_only_button")
        self.video_output_checkbutton = self.builder.get_object("video_output_checkbutton")
        self.audio_output_checkbutton = self.builder.get_object("audio_output_checkbutton")
        self.render_button = self.builder.get_object("render_button")
        self.video_settings_button = self.builder.get_object("video_settings_button")
        self.audio_settings_button = self.builder.get_object("audio_settings_button")
        self.frame_rate_combo = self.builder.get_object("frame_rate_combo")
        self.scale_spinbutton = self.builder.get_object("scale_spinbutton")
        self.channels_combo = self.builder.get_object("channels_combo")
        self.sample_rate_combo = self.builder.get_object("sample_rate_combo")
        self.sample_depth_combo = self.builder.get_object("sample_depth_combo")
        self.muxercombobox = self.builder.get_object("muxercombobox")
        self.audio_encoder_combo = self.builder.get_object("audio_encoder_combo")
        self.video_encoder_combo = self.builder.get_object("video_encoder_combo")
        self.filebutton = self.builder.get_object("filebutton")
        self.fileentry = self.builder.get_object("fileentry")
        self.resolution_label = self.builder.get_object("resolution_label")
        self.render_preset_treeview = self.builder.get_object("render_preset_treeview")
        self.save_render_preset_button = self.builder.get_object("save_render_preset_button")
        self.remove_render_preset_button = self.builder.get_object("remove_render_preset_button")
        self.render_preset_infobar = self.builder.get_object("render-preset-infobar")

    def _settingsChanged(self, project, key, value):
        self.updateResolution()

    def _initializeComboboxModels(self):
        # Avoid loop import
        self.frame_rate_combo.set_model(frame_rates)
        self.channels_combo.set_model(audio_channels)
        self.sample_rate_combo.set_model(audio_rates)
        self.sample_depth_combo.set_model(audio_depths)
        self.muxercombobox.set_model(factorylist(CachedEncoderList().muxers))

    def _displaySettings(self):
        """Display the settings that also change in the ProjectSettingsDialog.
        """
        # Video settings
        set_combo_value(self.frame_rate_combo, self.project.videorate)
        # Audio settings
        set_combo_value(self.channels_combo, self.project.audiochannels)
        set_combo_value(self.sample_rate_combo, self.project.audiorate)
        set_combo_value(self.sample_depth_combo, self.project.audiodepth)

    def _displayRenderSettings(self):
        """Display the settings which can be changed only in the RenderDialog.
        """
        # Video settings
        # note: this will trigger an update of the video resolution label
        self.scale_spinbutton.set_value(self.project.render_scale)
        # Muxer settings
        # note: this will trigger an update of the codec comboboxes
        set_combo_value(self.muxercombobox,
            Gst.ElementFactory.find(self.project.muxer))

    def _checkForExistingFile(self, *args):
        """
        Display a warning icon and tooltip if the file path already exists.
        """
        path = self.filebutton.get_current_folder()
        if not path:
            # This happens when the window is initialized.
            return
        warning_icon = Gtk.STOCK_DIALOG_WARNING
        filename = self.fileentry.get_text()
        if not filename:
            tooltip_text = _("A file name is required.")
        elif filename and os.path.exists(os.path.join(path, filename)):
            tooltip_text = _("This file already exists.\n"
                             "If you don't want to overwrite it, choose a "
                             "different file name or folder.")
        else:
            warning_icon = None
            tooltip_text = None
        self.fileentry.set_icon_from_stock(1, warning_icon)
        self.fileentry.set_icon_tooltip_text(1, tooltip_text)

    def updateFilename(self, basename):
        """Updates the filename UI element to show the specified file name."""
        extension = extension_for_muxer(self.project.muxer)
        if extension:
            name = "%s%s%s" % (basename, os.path.extsep, extension)
        else:
            name = basename
        self.fileentry.set_text(name)

    def updateAvailableEncoders(self):
        """Update the encoder comboboxes to show the available encoders."""
        encoders = CachedEncoderList()
        vencoder_model = factorylist(encoders.video_combination[self.project.muxer])
        self.video_encoder_combo.set_model(vencoder_model)

        aencoder_model = factorylist(encoders.audio_combination[self.project.muxer])
        self.audio_encoder_combo.set_model(aencoder_model)

        self._updateEncoderCombo(self.video_encoder_combo, self.preferred_vencoder)
        self._updateEncoderCombo(self.audio_encoder_combo, self.preferred_aencoder)

    def _updateEncoderCombo(self, encoder_combo, preferred_encoder):
        """Select the specified encoder for the specified encoder combo."""
        if preferred_encoder:
            # A preference exists, pick it if it can be found in
            # the current model of the combobox.
            vencoder = Gst.ElementFactory.find(preferred_encoder)
            set_combo_value(encoder_combo, vencoder, default_index=0)
        else:
            # No preference exists, pick the first encoder from
            # the current model of the combobox.
            encoder_combo.set_active(0)

    def _elementSettingsDialog(self, factory, settings_attr):
        """Open a dialog to edit the properties for the specified factory.

        @param factory: An element factory whose properties the user will edit.
        @type factory: Gst.ElementFactory
        @param settings_attr: The MultimediaSettings attribute holding
        the properties.
        @type settings_attr: str
        """
        properties = getattr(self.project, settings_attr)
        self.dialog = GstElementSettingsDialog(factory, properties=properties,
                                            parent_window=self.window)
        self.dialog.ok_btn.connect("clicked", self._okButtonClickedCb, settings_attr)

    def _showRenderErrorDialog(self, error, details):
        primary_message = _("Sorry, something didn’t work right.")
        secondary_message = _("An error occured while trying to render your "
            "project. You might want to check our troubleshooting guide or "
            "file a bug report. See the details below for some basic "
            "information that may help identify the problem.")

        dialog = Gtk.MessageDialog(self.window, Gtk.DialogFlags.MODAL,
            Gtk.MessageType.ERROR, Gtk.ButtonsType.OK,
            primary_message)
        dialog.set_property("secondary-text", secondary_message)

        expander = Gtk.Expander()
        expander.set_label(_("Details"))
        details_label = Gtk.Label(str(error) + "\n\n" + str(details))
        details_label.set_line_wrap(True)
        details_label.set_selectable(True)
        expander.add(details_label)
        dialog.get_message_area().add(expander)
        dialog.show_all()  # Ensure the expander and its children show up
        dialog.run()
        dialog.destroy()

    def startAction(self):
        """ Start the render process """
        self._pipeline.set_state(Gst.State.NULL)
        self._pipeline.set_mode(GES.PipelineFlags.SMART_RENDER)
        encodebin = self._pipeline.get_by_name("internal-encodebin")
        self._gstSigId[encodebin] = encodebin.connect("element-added", self._elementAddedCb)
        self._pipeline.set_state(Gst.State.PLAYING)

    def _cancelRender(self, *unused_args):
        self.debug("Aborting render")
        self._shutDown()
        self._destroyProgressWindow()

    def _shutDown(self):
        """ The render process has been aborted, shutdown the gstreamer pipeline
        and disconnect from its signals """
        self._pipeline.set_state(Gst.State.NULL)
        self._disconnectFromGst()
        self._pipeline.set_mode(GES.PipelineFlags.FULL_PREVIEW)

    def _pauseRender(self, progress):
        self.app.current.pipeline.togglePlayback()

    def _destroyProgressWindow(self):
        """ Handle the completion or the cancellation of the render process. """
        self.progress.window.destroy()
        self.progress = None
        self.window.show()  # Show the rendering dialog again

    def _disconnectFromGst(self):
        for obj, id in self._gstSigId.iteritems():
            obj.disconnect(id)
        self._gstSigId = {}
        self.app.current.pipeline.disconnect_by_func(self._updatePositionCb)

    def destroy(self):
        self.window.destroy()

    #------------------- Callbacks ------------------------------------------#

    #-- UI callbacks
    def _okButtonClickedCb(self, unused_button, settings_attr):
        setattr(self, settings_attr, self.dialog.getSettings())
        self.dialog.window.destroy()

    def _renderButtonClickedCb(self, unused_button):
        """
        The render button inside the render dialog has been clicked,
        start the rendering process.
        """
        self.outfile = os.path.join(self.filebutton.get_uri(),
                                    self.fileentry.get_text())
        self.progress = RenderingProgressDialog(self.app, self)
        self.window.hide()  # Hide the rendering settings dialog while rendering

        self._pipeline.set_render_settings(self.outfile, self.project.container_profile)
        self.startAction()
        self.progress.window.show()
        self.progress.connect("cancel", self._cancelRender)
        self.progress.connect("pause", self._pauseRender)
        bus = self._pipeline.get_bus()
        bus.add_signal_watch()
        self._gstSigId[bus] = bus.connect('message', self._busMessageCb)
        self.app.current.pipeline.connect("position", self._updatePositionCb)
        # Force writing the config now, or the path will be reset
        # if the user opens the rendering dialog again
        self.app.settings.lastExportFolder = self.filebutton.get_current_folder()
        self.app.settings.storeSettings()

    def _closeButtonClickedCb(self, unused_button):
        self.debug("Render dialog's Close button clicked")
        self.destroy()

    def _deleteEventCb(self, window, event):
        self.debug("Render dialog is being deleted")
        self.destroy()

    def _containerContextHelpClickedCb(self, unused_button):
        show_user_manual("codecscontainers")

    #-- GStreamer callbacks
    def _busMessageCb(self, unused_bus, message):
        if message.type == Gst.MessageType.EOS:  # Render complete
            self.debug("got EOS message, render complete")
            self._shutDown()
            self.progress.progressbar.set_text(_("Render complete"))
            self.progress.window.set_title(_("Render complete"))
            if has_libnotify:
                Notify.init("pitivi")
                if not self.progress.window.is_active():
                    self.notification = Notify.Notification.new(_("Render complete"), _('"%s" has finished rendering.' % self.fileentry.get_text()), "pitivi")
                    self.notification.show()
            if has_canberra:
                canberra = pycanberra.Canberra()
                canberra.play(1, pycanberra.CA_PROP_EVENT_ID, "complete-media", None)
            self.progress.play_rendered_file_button.show()
            self.progress.close_button.show()
            self.progress.cancel_button.hide()
            self.progress.play_pause_button.hide()

        elif message.type == Gst.MessageType.ERROR:
            # Errors in a GStreamer pipeline are fatal. If we encounter one,
            # we should abort and show the error instead of sitting around.
            error, details = message.parse_error()
            self._cancelRender()
            self._showRenderErrorDialog(error, details)

        elif message.type == Gst.MessageType.STATE_CHANGED and self.progress:
            prev, state, pending = message.parse_state_changed()
            if message.src == self._pipeline:
                state_really_changed = pending == Gst.State.VOID_PENDING
                if state_really_changed:
                    if state == Gst.State.PLAYING:
                        self.debug("Rendering started/resumed, resetting ETA calculation and inhibiting sleep")
                        self.timestarted = time.time()
                        self.system.inhibitSleep(RenderDialog.INHIBIT_REASON)
                    else:
                        self.system.uninhibitSleep(RenderDialog.INHIBIT_REASON)

    def _updatePositionCb(self, pipeline, position):
        if self.progress:
            text = None
            timediff = time.time() - self.timestarted
            length = self.app.current.timeline.props.duration
            fraction = float(min(position, length)) / float(length)
            if timediff > 5.0 and position:
                # only display ETA after 5s in order to have enough averaging and
                # if the position is non-null
                totaltime = (timediff * float(length) / float(position)) - timediff
                text = beautify_ETA(int(totaltime * Gst.SECOND))
            self.progress.updatePosition(fraction, text)

    def _elementAddedCb(self, bin, element):
        """
        Setting properties on Gst.Element-s has they are added to the
        Gst.Encodebin
        """
        factory = element.get_factory()
        settings = {}
        if factory == get_combo_value(self.video_encoder_combo):
            settings = self.project.vcodecsettings
        elif factory == get_combo_value(self.audio_encoder_combo):
            settings = self.project.acodecsettings

        for propname, value in settings.iteritems():
            element.set_property(propname, value)
            self.debug("Setting %s to %s", propname, value)

    #-- Settings changed callbacks
    def _scaleSpinbuttonChangedCb(self, button):
        render_scale = self.scale_spinbutton.get_value()
        self.project.render_scale = render_scale
        self.updateResolution()

    def updateResolution(self):
        width, height = self.project.getVideoWidthAndHeight(render=True)
        self.resolution_label.set_text(u"%d×%d" % (width, height))

    def _projectSettingsButtonClickedCb(self, button):
        from pitivi.project import ProjectSettingsDialog
        dialog = ProjectSettingsDialog(self.window, self.project)
        dialog.window.run()

    def _audioOutputCheckbuttonToggledCb(self, audio):
        active = self.audio_output_checkbutton.get_active()
        if active:
            self.channels_combo.set_sensitive(True)
            self.sample_rate_combo.set_sensitive(True)
            self.sample_depth_combo.set_sensitive(True)
            self.audio_encoder_combo.set_sensitive(True)
            self.audio_settings_button.set_sensitive(True)
            self.render_button.set_sensitive(True)
        else:
            self.channels_combo.set_sensitive(False)
            self.sample_rate_combo.set_sensitive(False)
            self.sample_depth_combo.set_sensitive(False)
            self.audio_encoder_combo.set_sensitive(False)
            self.audio_settings_button.set_sensitive(False)
            if not self.video_output_checkbutton.get_active():
                self.render_button.set_sensitive(False)

    def _videoOutputCheckbuttonToggledCb(self, video):
        active = self.video_output_checkbutton.get_active()
        if active:
            self.scale_spinbutton.set_sensitive(True)
            self.frame_rate_combo.set_sensitive(True)
            self.video_encoder_combo.set_sensitive(True)
            self.video_settings_button.set_sensitive(True)
            self.render_button.set_sensitive(True)
        else:
            self.scale_spinbutton.set_sensitive(False)
            self.frame_rate_combo.set_sensitive(False)
            self.video_encoder_combo.set_sensitive(False)
            self.video_settings_button.set_sensitive(False)
            if not self.audio_output_checkbutton.get_active():
                self.render_button.set_sensitive(False)

    def _frameRateComboChangedCb(self, combo):
        framerate = get_combo_value(combo)
        self.project.framerate = framerate

    def _videoEncoderComboChangedCb(self, combo):
        vencoder = get_combo_value(combo).get_name()
        self.project.vencoder = vencoder

        if not self.muxer_combo_changing:
            # The user directly changed the video encoder combo.
            self.preferred_vencoder = vencoder

    def _videoSettingsButtonClickedCb(self, button):
        factory = get_combo_value(self.video_encoder_combo)
        self._elementSettingsDialog(factory, 'vcodecsettings')

    def _channelsComboChangedCb(self, combo):
        self.project.audiochannels = get_combo_value(combo)

    def _sampleDepthComboChangedCb(self, combo):
        self.project.audiodepth = get_combo_value(combo)

    def _sampleRateComboChangedCb(self, combo):
        self.project.audiorate = get_combo_value(combo)

    def _audioEncoderChangedComboCb(self, combo):
        aencoder = get_combo_value(combo).get_name()
        self.project.aencoder = aencoder
        if not self.muxer_combo_changing:
            # The user directly changed the audio encoder combo.
            self.preferred_aencoder = aencoder

    def _audioSettingsButtonClickedCb(self, button):
        factory = get_combo_value(self.audio_encoder_combo)
        self._elementSettingsDialog(factory, 'acodecsettings')

    def _muxerComboChangedCb(self, muxer_combo):
        """Handle the changing of the container format combobox."""
        self.project.muxer = get_combo_value(muxer_combo).get_name()

        # Update the extension of the filename.
        basename = os.path.splitext(self.fileentry.get_text())[0]
        self.updateFilename(basename)

        # Update muxer-dependent widgets.
        self.muxer_combo_changing = True
        try:
            self.updateAvailableEncoders()
        finally:
            self.muxer_combo_changing = False
Exemplo n.º 4
0
class RenderDialog(Loggable):
    """Render dialog box.

    @ivar preferred_aencoder: The last audio encoder selected by the user.
    @type preferred_aencoder: str
    @ivar preferred_vencoder: The last video encoder selected by the user.
    @type preferred_vencoder: str
    """
    INHIBIT_REASON = _("Currently rendering")

    _factory_formats = {}

    def __init__(self, app, project, pipeline=None):

        from pitivi.preset import RenderPresetManager

        Loggable.__init__(self)

        self.app = app
        self.project = project
        self.system = app.system

        if pipeline is not None:
            self._pipeline = pipeline
        else:
            self._pipeline = self.project.pipeline

        self.outfile = None
        self.notification = None

        # Variables to keep track of progress indication timers:
        self._filesizeEstimateTimer = self._timeEstimateTimer = None
        self._is_rendering = False
        self._rendering_is_paused = False
        self.current_position = None
        self._time_started = 0
        self._time_spent_paused = 0  # Avoids the ETA being wrong on resume

        # Various gstreamer signal connection ID's
        # {object: sigId}
        self._gstSigId = {}

        self._createUi()

        # FIXME: re-enable this widget when bug #637078 is implemented
        self.selected_only_button.destroy()

        # Directory and Filename
        self.filebutton.set_current_folder(self.app.settings.lastExportFolder)
        if not self.project.name:
            self.updateFilename(_("Untitled"))
        else:
            self.updateFilename(self.project.name)

        # We store these so that when the user tries various container formats,
        # (AKA muxers) we select these a/v encoders, if they are compatible with
        # the current container format.
        self.preferred_vencoder = self.project.vencoder
        self.preferred_aencoder = self.project.aencoder

        self._initializeComboboxModels()
        self._displaySettings()
        self._displayRenderSettings()

        self.window.connect("delete-event", self._deleteEventCb)
        self.project.connect("rendering-settings-changed",
                             self._settingsChanged)

        # Monitor changes

        self.wg = RippleUpdateGroup()
        self.wg.addVertex(self.frame_rate_combo, signal="changed")
        self.wg.addVertex(self.save_render_preset_button,
                          update_func=self._updateRenderSaveButton)
        self.wg.addVertex(self.channels_combo, signal="changed")
        self.wg.addVertex(self.sample_rate_combo, signal="changed")
        self.wg.addVertex(self.muxercombobox, signal="changed")
        self.wg.addVertex(self.audio_encoder_combo, signal="changed")
        self.wg.addVertex(self.video_encoder_combo, signal="changed")
        self.render_presets = RenderPresetManager()
        self.render_presets.loadAll()

        self._fillPresetsTreeview(self.render_preset_treeview,
                                  self.render_presets,
                                  self._updateRenderPresetButtons)

        self.wg.addEdge(self.frame_rate_combo, self.save_render_preset_button)
        self.wg.addEdge(self.audio_encoder_combo,
                        self.save_render_preset_button)
        self.wg.addEdge(self.video_encoder_combo,
                        self.save_render_preset_button)
        self.wg.addEdge(self.muxercombobox, self.save_render_preset_button)
        self.wg.addEdge(self.channels_combo, self.save_render_preset_button)
        self.wg.addEdge(self.sample_rate_combo, self.save_render_preset_button)

        self._infobarForPresetManager = {
            self.render_presets: self.render_preset_infobar
        }

        # Bind widgets to RenderPresetsManager
        self.bindCombo(self.render_presets, "channels", self.channels_combo)
        self.bindCombo(self.render_presets, "sample-rate",
                       self.sample_rate_combo)
        self.bindCombo(self.render_presets, "acodec", self.audio_encoder_combo)
        self.bindCombo(self.render_presets, "vcodec", self.video_encoder_combo)
        self.bindCombo(self.render_presets, "container", self.muxercombobox)
        self.bindCombo(self.render_presets, "frame-rate",
                       self.frame_rate_combo)
        self.bindHeight(self.render_presets)
        self.bindWidth(self.render_presets)

        self.createNoPreset(self.render_presets)

    def createNoPreset(self, mgr):
        mgr.prependPreset(
            _("No preset"), {
                "channels":
                int(get_combo_value(self.channels_combo)),
                "sample-rate":
                int(get_combo_value(self.sample_rate_combo)),
                "acodec":
                get_combo_value(self.audio_encoder_combo).get_name(),
                "vcodec":
                get_combo_value(self.video_encoder_combo).get_name(),
                "container":
                get_combo_value(self.muxercombobox).get_name(),
                "frame-rate":
                Gst.Fraction(int(get_combo_value(self.frame_rate_combo).num),
                             int(get_combo_value(
                                 self.frame_rate_combo).denom)),
                "height":
                self.project.videoheight,
                "width":
                self.project.videowidth
            })

    def bindCombo(self, mgr, name, widget):
        if name == "container":
            mgr.bindWidget(name, lambda x: self.muxer_setter(widget, x),
                           lambda: get_combo_value(widget).get_name())

        elif name == "acodec":
            mgr.bindWidget(name, lambda x: self.acodec_setter(widget, x),
                           lambda: get_combo_value(widget).get_name())

        elif name == "vcodec":
            mgr.bindWidget(name, lambda x: self.vcodec_setter(widget, x),
                           lambda: get_combo_value(widget).get_name())

        elif name == "sample-rate":
            mgr.bindWidget(name, lambda x: self.sample_rate_setter(widget, x),
                           lambda: get_combo_value(widget))

        elif name == "channels":
            mgr.bindWidget(name, lambda x: self.channels_setter(widget, x),
                           lambda: get_combo_value(widget))

        elif name == "frame-rate":
            mgr.bindWidget(name, lambda x: self.framerate_setter(widget, x),
                           lambda: get_combo_value(widget))

    def muxer_setter(self, widget, value):
        set_combo_value(widget, Gst.ElementFactory.find(value))
        self.project.setEncoders(muxer=value)

        # Update the extension of the filename.
        basename = os.path.splitext(self.fileentry.get_text())[0]
        self.updateFilename(basename)

        # Update muxer-dependent widgets.
        self.muxer_combo_changing = True
        try:
            self.updateAvailableEncoders()
        finally:
            self.muxer_combo_changing = False

    def acodec_setter(self, widget, value):
        set_combo_value(widget, Gst.ElementFactory.find(value))
        self.project.aencoder = value
        if not self.muxer_combo_changing:
            # The user directly changed the audio encoder combo.
            self.preferred_aencoder = value

    def vcodec_setter(self, widget, value):
        set_combo_value(widget, Gst.ElementFactory.find(value))
        self.project.setEncoders(vencoder=value)
        if not self.muxer_combo_changing:
            # The user directly changed the video encoder combo.
            self.preferred_vencoder = value

    def sample_rate_setter(self, widget, value):
        self.project.audiorate = set_combo_value(widget, value)

    def channels_setter(self, widget, value):
        self.project.audiochannels = set_combo_value(widget, value)

    def framerate_setter(self, widget, value):
        self.project.videorate = set_combo_value(widget, value)

    def bindHeight(self, mgr):
        mgr.bindWidget("height",
                       lambda x: setattr(self.project, "videoheight", x),
                       lambda: 0)

    def bindWidth(self, mgr):
        mgr.bindWidget("width",
                       lambda x: setattr(self.project, "videowidth", x),
                       lambda: 0)

    def _fillPresetsTreeview(self, treeview, mgr, update_buttons_func):
        """Set up the specified treeview to display the specified presets.

        @param treeview: The treeview for displaying the presets.
        @type treeview: TreeView
        @param mgr: The preset manager.
        @type mgr: PresetManager
        @param update_buttons_func: A function which updates the buttons for
        removing and saving a preset, enabling or disabling them accordingly.
        @type update_buttons_func: function
        """
        renderer = Gtk.CellRendererText()
        renderer.props.editable = True
        column = Gtk.TreeViewColumn("Preset", renderer, text=0)
        treeview.append_column(column)
        treeview.props.headers_visible = False
        model = mgr.getModel()
        treeview.set_model(model)
        model.connect("row-inserted", self._newPresetCb, column, renderer,
                      treeview)
        renderer.connect("edited", self._presetNameEditedCb, mgr)
        renderer.connect("editing-started", self._presetNameEditingStartedCb,
                         mgr)
        treeview.get_selection().connect("changed", self._presetChangedCb, mgr,
                                         update_buttons_func)
        treeview.connect("focus-out-event", self._treeviewDefocusedCb, mgr)

    def _newPresetCb(self, unused_model, path, unused_iter_, column, renderer,
                     treeview):
        """Handle the addition of a preset to the model of the preset manager.
        """
        treeview.set_cursor_on_cell(path, column, renderer, start_editing=True)
        treeview.grab_focus()

    def _presetNameEditedCb(self, unused_renderer, path, new_text, mgr):
        """Handle the renaming of a preset."""
        from pitivi.preset import DuplicatePresetNameException

        try:
            mgr.renamePreset(path, new_text)
            self._updateRenderPresetButtons()
        except DuplicatePresetNameException:
            error_markup = _('"%s" already exists.') % new_text
            self._showPresetManagerError(mgr, error_markup)

    def _presetNameEditingStartedCb(self, unused_renderer, unused_editable,
                                    unused_path, mgr):
        """Handle the start of a preset renaming."""
        self._hidePresetManagerError(mgr)

    def _treeviewDefocusedCb(self, unused_widget, unused_event, mgr):
        """Handle the treeview loosing the focus."""
        self._hidePresetManagerError(mgr)

    def _showPresetManagerError(self, mgr, error_markup):
        """Show the specified error on the infobar associated with the manager.

        @param mgr: The preset manager for which to show the error.
        @type mgr: PresetManager
        """
        infobar = self._infobarForPresetManager[mgr]
        # The infobar must contain exactly one object in the content area:
        # a label for displaying the error.
        label = infobar.get_content_area().children()[0]
        label.set_markup(error_markup)
        infobar.show()

    def _hidePresetManagerError(self, mgr):
        """Hide the error infobar associated with the manager.

        @param mgr: The preset manager for which to hide the error infobar.
        @type mgr: PresetManager
        """
        infobar = self._infobarForPresetManager[mgr]
        infobar.hide()

    def _updateRenderSaveButton(self, unused_in, button):
        button.set_sensitive(self.render_presets.isSaveButtonSensitive())

    @staticmethod
    def _getUniquePresetName(mgr):
        """Get a unique name for a new preset for the specified PresetManager.
        """
        existing_preset_names = list(mgr.getPresetNames())
        preset_name = _("New preset")
        i = 1
        while preset_name in existing_preset_names:
            preset_name = _("New preset %d") % i
            i += 1
        return preset_name

    def _addRenderPresetButtonClickedCb(self, unused_button):
        preset_name = self._getUniquePresetName(self.render_presets)
        self.render_presets.addPreset(
            preset_name, {
                "channels":
                int(get_combo_value(self.channels_combo)),
                "sample-rate":
                int(get_combo_value(self.sample_rate_combo)),
                "acodec":
                get_combo_value(self.audio_encoder_combo).get_name(),
                "vcodec":
                get_combo_value(self.video_encoder_combo).get_name(),
                "container":
                get_combo_value(self.muxercombobox).get_name(),
                "frame-rate":
                Gst.Fraction(int(get_combo_value(self.frame_rate_combo).num),
                             int(get_combo_value(
                                 self.frame_rate_combo).denom)),
                "height":
                0,
                "width":
                0
            })

        self.render_presets.restorePreset(preset_name)
        self._updateRenderPresetButtons()

    def _saveRenderPresetButtonClickedCb(self, unused_button):
        self.render_presets.saveCurrentPreset()
        self.save_render_preset_button.set_sensitive(False)
        self.remove_render_preset_button.set_sensitive(True)

    def _updateRenderPresetButtons(self):
        can_save = self.render_presets.isSaveButtonSensitive()
        self.save_render_preset_button.set_sensitive(can_save)
        can_remove = self.render_presets.isRemoveButtonSensitive()
        self.remove_render_preset_button.set_sensitive(can_remove)

    def _removeRenderPresetButtonClickedCb(self, unused_button):
        selection = self.render_preset_treeview.get_selection()
        model, iter_ = selection.get_selected()
        if iter_:
            self.render_presets.removePreset(model[iter_][0])

    def _presetChangedCb(self, selection, mgr, update_preset_buttons_func):
        """Handle the selection of a preset."""
        model, iter_ = selection.get_selected()
        if iter_:
            self.selected_preset = model[iter_][0]
        else:
            self.selected_preset = None

        mgr.restorePreset(self.selected_preset)
        self._displaySettings()
        update_preset_buttons_func()
        self._hidePresetManagerError(mgr)

    def _createUi(self):
        builder = Gtk.Builder()
        builder.add_from_file(
            os.path.join(configure.get_ui_dir(), "renderingdialog.ui"))
        builder.connect_signals(self)

        self.window = builder.get_object("render-dialog")
        self.selected_only_button = builder.get_object("selected_only_button")
        self.video_output_checkbutton = builder.get_object(
            "video_output_checkbutton")
        self.audio_output_checkbutton = builder.get_object(
            "audio_output_checkbutton")
        self.render_button = builder.get_object("render_button")
        self.video_settings_button = builder.get_object(
            "video_settings_button")
        self.audio_settings_button = builder.get_object(
            "audio_settings_button")
        self.frame_rate_combo = builder.get_object("frame_rate_combo")
        self.scale_spinbutton = builder.get_object("scale_spinbutton")
        self.channels_combo = builder.get_object("channels_combo")
        self.sample_rate_combo = builder.get_object("sample_rate_combo")
        self.muxercombobox = builder.get_object("muxercombobox")
        self.audio_encoder_combo = builder.get_object("audio_encoder_combo")
        self.video_encoder_combo = builder.get_object("video_encoder_combo")
        self.filebutton = builder.get_object("filebutton")
        self.fileentry = builder.get_object("fileentry")
        self.resolution_label = builder.get_object("resolution_label")
        self.render_preset_treeview = builder.get_object(
            "render_preset_treeview")
        self.save_render_preset_button = builder.get_object(
            "save_render_preset_button")
        self.remove_render_preset_button = builder.get_object(
            "remove_render_preset_button")
        self.render_preset_infobar = builder.get_object(
            "render-preset-infobar")

        icon = os.path.join(configure.get_pixmap_dir(), "pitivi-render-16.png")
        self.window.set_icon_from_file(icon)
        self.window.set_transient_for(self.app.gui)

        # Set the shading style in the toolbar below presets
        presets_toolbar = builder.get_object("render_presets_toolbar")
        presets_toolbar.get_style_context().add_class(
            Gtk.STYLE_CLASS_INLINE_TOOLBAR)

    def _settingsChanged(self, unused_project, unused_key, unused_value):
        self.updateResolution()

    def _initializeComboboxModels(self):
        # Avoid loop import
        self.frame_rate_combo.set_model(frame_rates)
        self.channels_combo.set_model(audio_channels)
        self.sample_rate_combo.set_model(audio_rates)
        self.muxercombobox.set_model(factorylist(CachedEncoderList().muxers))

    def _displaySettings(self):
        """Display the settings that also change in the ProjectSettingsDialog.
        """
        # Video settings
        set_combo_value(self.frame_rate_combo, self.project.videorate)
        # Audio settings
        set_combo_value(self.channels_combo, self.project.audiochannels)
        set_combo_value(self.sample_rate_combo, self.project.audiorate)

    def _displayRenderSettings(self):
        """Display the settings which can be changed only in the RenderDialog.
        """
        # Video settings
        # note: this will trigger an update of the video resolution label
        self.scale_spinbutton.set_value(self.project.render_scale)
        # Muxer settings
        # note: this will trigger an update of the codec comboboxes
        set_combo_value(self.muxercombobox,
                        Gst.ElementFactory.find(self.project.muxer))

    def _checkForExistingFile(self, *unused_args):
        """
        Display a warning icon and tooltip if the file path already exists.
        """
        path = self.filebutton.get_current_folder()
        if not path:
            # This happens when the window is initialized.
            return
        warning_icon = "dialog-warning"
        filename = self.fileentry.get_text()
        if not filename:
            tooltip_text = _("A file name is required.")
        elif filename and os.path.exists(os.path.join(path, filename)):
            tooltip_text = _("This file already exists.\n"
                             "If you don't want to overwrite it, choose a "
                             "different file name or folder.")
        else:
            warning_icon = None
            tooltip_text = None
        self.fileentry.set_icon_from_icon_name(1, warning_icon)
        self.fileentry.set_icon_tooltip_text(1, tooltip_text)

    def _getFilesizeEstimate(self):
        """
        Using the current render output's filesize and position in the timeline,
        return a human-readable (ex: "14 MB") estimate of the final filesize.

        Estimates in megabytes (over 30 MB) are rounded to the nearest 10 MB
        to smooth out small variations. You'd be surprised how imprecision can
        improve perceived accuracy.
        """
        if not self.current_position or self.current_position == 0:
            return None

        current_filesize = os.stat(path_from_uri(self.outfile)).st_size
        length = self.app.project_manager.current_project.timeline.props.duration
        estimated_size = float(current_filesize * float(length) /
                               self.current_position)
        # Now let's make it human-readable (instead of octets).
        # If it's in the giga range (10⁹) instead of mega (10⁶), use 2 decimals
        if estimated_size > 10e8:
            gigabytes = estimated_size / (10**9)
            return _("%.2f GB" % gigabytes)
        else:
            megabytes = int(estimated_size / (10**6))
            if megabytes > 30:
                megabytes = int(round(megabytes, -1))  # -1 means round to 10
            return _("%d MB" % megabytes)

    def updateFilename(self, basename):
        """Updates the filename UI element to show the specified file name."""
        extension = extension_for_muxer(self.project.muxer)
        if extension:
            name = "%s%s%s" % (basename, os.path.extsep, extension)
        else:
            name = basename
        self.fileentry.set_text(name)

    def updateAvailableEncoders(self):
        """Update the encoder comboboxes to show the available encoders."""
        encoders = CachedEncoderList()
        vencoder_model = factorylist(
            encoders.video_combination[self.project.muxer])
        self.video_encoder_combo.set_model(vencoder_model)

        aencoder_model = factorylist(
            encoders.audio_combination[self.project.muxer])
        self.audio_encoder_combo.set_model(aencoder_model)

        self._updateEncoderCombo(self.video_encoder_combo,
                                 self.preferred_vencoder)
        self._updateEncoderCombo(self.audio_encoder_combo,
                                 self.preferred_aencoder)

    def _updateEncoderCombo(self, encoder_combo, preferred_encoder):
        """Select the specified encoder for the specified encoder combo."""
        if preferred_encoder:
            # A preference exists, pick it if it can be found in
            # the current model of the combobox.
            vencoder = Gst.ElementFactory.find(preferred_encoder)
            set_combo_value(encoder_combo, vencoder, default_index=0)
        else:
            # No preference exists, pick the first encoder from
            # the current model of the combobox.
            encoder_combo.set_active(0)

    def _elementSettingsDialog(self, factory, settings_attr):
        """Open a dialog to edit the properties for the specified factory.

        @param factory: An element factory whose properties the user will edit.
        @type factory: Gst.ElementFactory
        @param settings_attr: The MultimediaSettings attribute holding
        the properties.
        @type settings_attr: str
        """
        properties = getattr(self.project, settings_attr)
        self.dialog = GstElementSettingsDialog(factory,
                                               properties=properties,
                                               parent_window=self.window,
                                               isControllable=False)
        self.dialog.ok_btn.connect("clicked", self._okButtonClickedCb,
                                   settings_attr)

    def _showRenderErrorDialog(self, error, details):
        primary_message = _("Sorry, something didn’t work right.")
        secondary_message = _(
            "An error occured while trying to render your "
            "project. You might want to check our troubleshooting guide or "
            "file a bug report. See the details below for some basic "
            "information that may help identify the problem.")

        dialog = Gtk.MessageDialog(transient_for=self.window,
                                   modal=True,
                                   message_type=Gtk.MessageType.ERROR,
                                   buttons=Gtk.ButtonsType.OK,
                                   text=primary_message)
        dialog.set_property("secondary-text", secondary_message)

        expander = Gtk.Expander()
        expander.set_label(_("Details"))
        details_label = Gtk.Label(label=str(error) + "\n\n" + str(details))
        details_label.set_line_wrap(True)
        details_label.set_selectable(True)
        expander.add(details_label)
        dialog.get_message_area().add(expander)
        dialog.show_all()  # Ensure the expander and its children show up
        dialog.run()
        dialog.destroy()

    def startAction(self):
        """ Start the render process """
        self._pipeline.set_state(Gst.State.NULL)
        # FIXME: https://github.com/pitivi/gst-editing-services/issues/23
        self._pipeline.set_mode(GES.PipelineFlags.RENDER)
        encodebin = self._pipeline.get_by_name("internal-encodebin")
        self._gstSigId[encodebin] = encodebin.connect("element-added",
                                                      self._elementAddedCb)
        self._pipeline.set_state(Gst.State.PLAYING)
        self._is_rendering = True
        self._time_started = time.time()

    def _cancelRender(self, *unused_args):
        self.debug("Aborting render")
        self._shutDown()
        self._destroyProgressWindow()

    def _shutDown(self):
        """Shutdown the gstreamer pipeline and disconnect from its signals."""
        self.project.set_rendering(False)
        self._is_rendering = False
        self._rendering_is_paused = False
        self._time_spent_paused = 0
        self._pipeline.set_state(Gst.State.NULL)
        self._disconnectFromGst()
        self._pipeline.set_mode(GES.PipelineFlags.FULL_PREVIEW)

    def _pauseRender(self, unused_progress):
        self._rendering_is_paused = self.progress.play_pause_button.get_active(
        )
        if self._rendering_is_paused:
            self._last_timestamp_when_pausing = time.time()
        else:
            self._time_spent_paused += time.time(
            ) - self._last_timestamp_when_pausing
            self.debug("Resuming render after %d seconds in pause",
                       self._time_spent_paused)
        self.app.project_manager.current_project.pipeline.togglePlayback()

    def _destroyProgressWindow(self):
        """ Handle the completion or the cancellation of the render process. """
        self.progress.window.destroy()
        self.progress = None
        self.window.show()  # Show the rendering dialog again

    def _disconnectFromGst(self):
        for obj, id in self._gstSigId.items():
            obj.disconnect(id)
        self._gstSigId = {}
        try:
            self.app.project_manager.current_project.pipeline.disconnect_by_func(
                self._updatePositionCb)
        except TypeError:
            # The render was successful, so this was already disconnected
            pass

    def destroy(self):
        self.window.destroy()

    @staticmethod
    def _maybePlayFinishedSound():
        if not PYCANBERRA_SOFT_DEPENDENCY:
            return
        import pycanberra
        canberra = pycanberra.Canberra()
        canberra.play(1, pycanberra.CA_PROP_EVENT_ID, "complete-media", None)

    #------------------- Callbacks ------------------------------------------#

    #-- UI callbacks
    def _okButtonClickedCb(self, unused_button, settings_attr):
        setattr(self.project, settings_attr, self.dialog.getSettings())
        self.dialog.window.destroy()

    def _renderButtonClickedCb(self, unused_button):
        """
        The render button inside the render dialog has been clicked,
        start the rendering process.
        """
        self.outfile = os.path.join(self.filebutton.get_uri(),
                                    self.fileentry.get_text())
        self.progress = RenderingProgressDialog(self.app, self)
        self.window.hide(
        )  # Hide the rendering settings dialog while rendering

        encoder_string = self.project.vencoder
        try:
            fmt = self._factory_formats[encoder_string]
            self.project.video_profile.get_restriction()[0]["format"] = fmt
        except KeyError:
            # Now find a format to set on the restriction caps.
            # The reason is we can't send different formats on the encoders.
            factory = Gst.ElementFactory.find(self.project.vencoder)
            for struct in factory.get_static_pad_templates():
                if struct.direction == Gst.PadDirection.SINK:
                    caps = Gst.Caps.from_string(struct.get_caps().to_string())
                    fixed = caps.fixate()
                    fmt = fixed.get_structure(0).get_value("format")
                    self.project.video_profile.get_restriction(
                    )[0]["format"] = fmt
                    self._factory_formats[encoder_string] = fmt
                    break

        self.project.set_rendering(True)
        self._pipeline.set_render_settings(self.outfile,
                                           self.project.container_profile)
        self.startAction()
        self.progress.window.show()
        self.progress.connect("cancel", self._cancelRender)
        self.progress.connect("pause", self._pauseRender)
        bus = self._pipeline.get_bus()
        bus.add_signal_watch()
        self._gstSigId[bus] = bus.connect('message', self._busMessageCb)
        self.app.project_manager.current_project.pipeline.connect(
            "position", self._updatePositionCb)
        # Force writing the config now, or the path will be reset
        # if the user opens the rendering dialog again
        self.app.settings.lastExportFolder = self.filebutton.get_current_folder(
        )
        self.app.settings.storeSettings()

    def _closeButtonClickedCb(self, unused_button):
        self.debug("Render dialog's Close button clicked")
        self.destroy()

    def _deleteEventCb(self, unused_window, unused_event):
        self.debug("Render dialog is being deleted")
        self.destroy()

    def _containerContextHelpClickedCb(self, unused_button):
        show_user_manual("codecscontainers")

    #-- Periodic (timer) callbacks
    def _updateTimeEstimateCb(self):
        if self._rendering_is_paused:
            return True  # Do nothing until we resume rendering
        elif self._is_rendering:
            timediff = time.time(
            ) - self._time_started - self._time_spent_paused
            length = self.app.project_manager.current_project.timeline.props.duration
            totaltime = (timediff * float(length) /
                         float(self.current_position)) - timediff
            time_estimate = beautify_ETA(int(totaltime * Gst.SECOND))
            if time_estimate:
                self.progress.updateProgressbarETA(time_estimate)
            return True
        else:
            self._timeEstimateTimer = None
            self.debug("Stopping the ETA timer")
            return False  # Stop the timer

    def _updateFilesizeEstimateCb(self):
        if self._rendering_is_paused:
            return True  # Do nothing until we resume rendering
        elif self._is_rendering:
            est_filesize = self._getFilesizeEstimate()
            if est_filesize:
                self.progress.setFilesizeEstimate(est_filesize)
            return True
        else:
            self.debug("Stopping the filesize estimation timer")
            self._filesizeEstimateTimer = None
            return False  # Stop the timer

    #-- GStreamer callbacks
    def _busMessageCb(self, unused_bus, message):
        if message.type == Gst.MessageType.EOS:  # Render complete
            self.debug("got EOS message, render complete")
            self._shutDown()
            self.progress.progressbar.set_text(_("Render complete"))
            self.progress.window.set_title(_("Render complete"))
            self.progress.setFilesizeEstimate(None)
            if not self.progress.window.is_active():
                notification = _('"%s" has finished rendering.' %
                                 self.fileentry.get_text())
                self.notification = self.app.system.desktopMessage(
                    _("Render complete"), notification, "pitivi")
            self._maybePlayFinishedSound()
            self.progress.play_rendered_file_button.show()
            self.progress.close_button.show()
            self.progress.cancel_button.hide()
            self.progress.play_pause_button.hide()

        elif message.type == Gst.MessageType.ERROR:
            # Errors in a GStreamer pipeline are fatal. If we encounter one,
            # we should abort and show the error instead of sitting around.
            error, details = message.parse_error()
            self._cancelRender()
            self._showRenderErrorDialog(error, details)

        elif message.type == Gst.MessageType.STATE_CHANGED and self.progress:
            prev, state, pending = message.parse_state_changed()
            if message.src == self._pipeline:
                state_really_changed = pending == Gst.State.VOID_PENDING
                if state_really_changed:
                    if state == Gst.State.PLAYING:
                        self.debug(
                            "Rendering started/resumed, inhibiting sleep")
                        self.system.inhibitSleep(RenderDialog.INHIBIT_REASON)
                    else:
                        self.system.uninhibitSleep(RenderDialog.INHIBIT_REASON)

    def _updatePositionCb(self, unused_pipeline, position):
        """
        Unlike other progression indicator callbacks, this one occurs every time
        the pipeline emits a position changed signal, which is *very* often.
        This should only be used for a smooth progressbar/percentage, not text.
        """
        self.current_position = position
        if not self.progress or not position:
            return

        length = self.app.project_manager.current_project.timeline.props.duration
        fraction = float(min(position, length)) / float(length)
        self.progress.updatePosition(fraction)

        # In order to have enough averaging, only display the ETA after 5s
        timediff = time.time() - self._time_started
        if not self._timeEstimateTimer:
            if timediff < 6:
                self.progress.progressbar.set_text(_("Estimating..."))
            else:
                self._timeEstimateTimer = GLib.timeout_add_seconds(
                    3, self._updateTimeEstimateCb)

        # Filesize is trickier and needs more time to be meaningful:
        if not self._filesizeEstimateTimer and (fraction > 0.33
                                                or timediff > 180):
            self._filesizeEstimateTimer = GLib.timeout_add_seconds(
                5, self._updateFilesizeEstimateCb)

    def _elementAddedCb(self, unused_bin, element):
        """
        Setting properties on Gst.Element-s has they are added to the
        Gst.Encodebin
        """
        factory = element.get_factory()
        settings = {}
        if factory == get_combo_value(self.video_encoder_combo):
            settings = self.project.vcodecsettings
        elif factory == get_combo_value(self.audio_encoder_combo):
            settings = self.project.acodecsettings

        for propname, value in settings.items():
            element.set_property(propname, value)
            self.debug("Setting %s to %s", propname, value)

    #-- Settings changed callbacks
    def _scaleSpinbuttonChangedCb(self, unused_button):
        render_scale = self.scale_spinbutton.get_value()
        self.project.render_scale = render_scale
        self.updateResolution()

    def updateResolution(self):
        width, height = self.project.getVideoWidthAndHeight(True)
        self.resolution_label.set_text("%d×%d" % (width, height))

    def _projectSettingsButtonClickedCb(self, unused_button):
        from pitivi.project import ProjectSettingsDialog
        dialog = ProjectSettingsDialog(self.window, self.project)
        dialog.window.run()

    def _audioOutputCheckbuttonToggledCb(self, unused_audio):
        active = self.audio_output_checkbutton.get_active()
        if active:
            self.channels_combo.set_sensitive(True)
            self.sample_rate_combo.set_sensitive(True)
            self.audio_encoder_combo.set_sensitive(True)
            self.audio_settings_button.set_sensitive(True)
            self.render_button.set_sensitive(True)
        else:
            self.channels_combo.set_sensitive(False)
            self.sample_rate_combo.set_sensitive(False)
            self.audio_encoder_combo.set_sensitive(False)
            self.audio_settings_button.set_sensitive(False)
            if not self.video_output_checkbutton.get_active():
                self.render_button.set_sensitive(False)

    def _videoOutputCheckbuttonToggledCb(self, unused_video):
        active = self.video_output_checkbutton.get_active()
        if active:
            self.scale_spinbutton.set_sensitive(True)
            self.frame_rate_combo.set_sensitive(True)
            self.video_encoder_combo.set_sensitive(True)
            self.video_settings_button.set_sensitive(True)
            self.render_button.set_sensitive(True)
        else:
            self.scale_spinbutton.set_sensitive(False)
            self.frame_rate_combo.set_sensitive(False)
            self.video_encoder_combo.set_sensitive(False)
            self.video_settings_button.set_sensitive(False)
            if not self.audio_output_checkbutton.get_active():
                self.render_button.set_sensitive(False)

    def _frameRateComboChangedCb(self, combo):
        framerate = get_combo_value(combo)
        self.project.framerate = framerate

    def _videoEncoderComboChangedCb(self, combo):
        vencoder = get_combo_value(combo).get_name()
        self.project.vencoder = vencoder

        if not self.muxer_combo_changing:
            # The user directly changed the video encoder combo.
            self.preferred_vencoder = vencoder

    def _videoSettingsButtonClickedCb(self, unused_button):
        factory = get_combo_value(self.video_encoder_combo)
        self._elementSettingsDialog(factory, 'vcodecsettings')

    def _channelsComboChangedCb(self, combo):
        self.project.audiochannels = get_combo_value(combo)

    def _sampleRateComboChangedCb(self, combo):
        self.project.audiorate = get_combo_value(combo)

    def _audioEncoderChangedComboCb(self, combo):
        aencoder = get_combo_value(combo).get_name()
        self.project.aencoder = aencoder
        if not self.muxer_combo_changing:
            # The user directly changed the audio encoder combo.
            self.preferred_aencoder = aencoder

    def _audioSettingsButtonClickedCb(self, unused_button):
        factory = get_combo_value(self.audio_encoder_combo)
        self._elementSettingsDialog(factory, 'acodecsettings')

    def _muxerComboChangedCb(self, muxer_combo):
        """Handle the changing of the container format combobox."""
        self.project.muxer = get_combo_value(muxer_combo).get_name()

        # Update the extension of the filename.
        basename = os.path.splitext(self.fileentry.get_text())[0]
        self.updateFilename(basename)

        # Update muxer-dependent widgets.
        self.muxer_combo_changing = True
        try:
            self.updateAvailableEncoders()
        finally:
            self.muxer_combo_changing = False
Exemplo n.º 5
0
class ProjectSettingsDialog():

    def __init__(self, parent, project):
        self.project = project
        self.settings = project.getSettings()

        self.builder = gtk.Builder()
        self.builder.add_from_file(os.path.join(get_ui_dir(), "projectsettings.ui"))
        self._setProperties()
        self.builder.connect_signals(self)

        # add custom display aspect ratio widget
        self.dar_fraction_widget = FractionWidget()
        self.video_properties_table.attach(self.dar_fraction_widget,
            0, 1, 6, 7, xoptions=gtk.EXPAND | gtk.FILL, yoptions=0)
        self.dar_fraction_widget.show()

        # add custom pixel aspect ratio widget
        self.par_fraction_widget = FractionWidget()
        self.video_properties_table.attach(self.par_fraction_widget,
            1, 2, 6, 7, xoptions=gtk.EXPAND | gtk.FILL, yoptions=0)
        self.par_fraction_widget.show()

        # add custom framerate widget
        self.frame_rate_fraction_widget = FractionWidget()
        self.video_properties_table.attach(self.frame_rate_fraction_widget,
            1, 2, 2, 3, xoptions=gtk.EXPAND | gtk.FILL, yoptions=0)
        self.frame_rate_fraction_widget.show()

        # populate coboboxes with appropriate data
        self.frame_rate_combo.set_model(frame_rates)
        self.dar_combo.set_model(display_aspect_ratios)
        self.par_combo.set_model(pixel_aspect_ratios)

        self.channels_combo.set_model(audio_channels)
        self.sample_rate_combo.set_model(audio_rates)
        self.sample_depth_combo.set_model(audio_depths)

        # behavior
        self.wg = RippleUpdateGroup()
        self.wg.addVertex(self.frame_rate_combo,
                signal="changed",
                update_func=self._updateCombo,
                update_func_args=(self.frame_rate_fraction_widget,))
        self.wg.addVertex(self.frame_rate_fraction_widget,
                signal="value-changed",
                update_func=self._updateFraction,
                update_func_args=(self.frame_rate_combo,))
        self.wg.addVertex(self.dar_combo, signal="changed")
        self.wg.addVertex(self.dar_fraction_widget, signal="value-changed")
        self.wg.addVertex(self.par_combo, signal="changed")
        self.wg.addVertex(self.par_fraction_widget, signal="value-changed")
        self.wg.addVertex(self.width_spinbutton, signal="value-changed")
        self.wg.addVertex(self.height_spinbutton, signal="value-changed")
        self.wg.addVertex(self.save_audio_preset_button,
                 update_func=self._updateAudioSaveButton)
        self.wg.addVertex(self.save_video_preset_button,
                 update_func=self._updateVideoSaveButton)
        self.wg.addVertex(self.channels_combo, signal="changed")
        self.wg.addVertex(self.sample_rate_combo, signal="changed")
        self.wg.addVertex(self.sample_depth_combo, signal="changed")

        # constrain width and height IFF constrain_sar_button is active
        self.wg.addEdge(self.width_spinbutton, self.height_spinbutton,
            predicate=self.constrained, edge_func=self.updateHeight)
        self.wg.addEdge(self.height_spinbutton, self.width_spinbutton,
            predicate=self.constrained, edge_func=self.updateWidth)

        # keep framereate text field and combo in sync
        self.wg.addBiEdge(self.frame_rate_combo, self.frame_rate_fraction_widget)

        # keep dar text field and combo in sync
        self.wg.addEdge(self.dar_combo, self.dar_fraction_widget,
            edge_func=self.updateDarFromCombo)
        self.wg.addEdge(self.dar_fraction_widget, self.dar_combo,
            edge_func=self.updateDarFromFractionWidget)

        # keep par text field and combo in sync
        self.wg.addEdge(self.par_combo, self.par_fraction_widget,
            edge_func=self.updateParFromCombo)
        self.wg.addEdge(self.par_fraction_widget, self.par_combo,
            edge_func=self.updateParFromFractionWidget)

        # constrain DAR and PAR values. because the combo boxes are already
        # linked, we only have to link the fraction widgets together.
        self.wg.addEdge(self.par_fraction_widget, self.dar_fraction_widget,
            edge_func=self.updateDarFromPar)
        self.wg.addEdge(self.dar_fraction_widget, self.par_fraction_widget,
            edge_func=self.updateParFromDar)

        # update PAR when width/height change and the DAR checkbutton is
        # selected
        self.wg.addEdge(self.width_spinbutton, self.par_fraction_widget,
            predicate=self.darSelected, edge_func=self.updateParFromDar)
        self.wg.addEdge(self.height_spinbutton, self.par_fraction_widget,
            predicate=self.darSelected, edge_func=self.updateParFromDar)

        # update DAR when width/height change and the PAR checkbutton is
        # selected
        self.wg.addEdge(self.width_spinbutton, self.dar_fraction_widget,
            predicate=self.parSelected, edge_func=self.updateDarFromPar)
        self.wg.addEdge(self.height_spinbutton, self.dar_fraction_widget,
            predicate=self.parSelected, edge_func=self.updateDarFromPar)

        # presets
        self.audio_presets = AudioPresetManager()
        self.audio_presets.loadAll()
        self.video_presets = VideoPresetManager()
        self.video_presets.loadAll()

        self._fillPresetsTreeview(
                self.audio_preset_treeview, self.audio_presets,
                self._updateAudioPresetButtons)
        self._fillPresetsTreeview(
                self.video_preset_treeview, self.video_presets,
                self._updateVideoPresetButtons)

        # A map which tells which infobar should be used when displaying
        # an error for a preset manager.
        self._infobarForPresetManager = {
                self.audio_presets: self.audio_preset_infobar,
                self.video_presets: self.video_preset_infobar}

        # Bind the widgets in the Video tab to the Video Presets Manager.
        self.bindSpinbutton(self.video_presets, "width", self.width_spinbutton)
        self.bindSpinbutton(self.video_presets, "height", self.height_spinbutton)
        self.bindFractionWidget(self.video_presets, "frame-rate", self.frame_rate_fraction_widget)
        self.bindPar(self.video_presets)

        # Bind the widgets in the Audio tab to the Audio Presets Manager.
        self.bindCombo(self.audio_presets, "channels", self.channels_combo)
        self.bindCombo(self.audio_presets, "sample-rate", self.sample_rate_combo)
        self.bindCombo(self.audio_presets, "depth", self.sample_depth_combo)

        self.wg.addEdge(self.par_fraction_widget, self.save_video_preset_button)
        self.wg.addEdge(self.frame_rate_fraction_widget, self.save_video_preset_button)
        self.wg.addEdge(self.width_spinbutton, self.save_video_preset_button)
        self.wg.addEdge(self.height_spinbutton, self.save_video_preset_button)

        self.wg.addEdge(self.channels_combo, self.save_audio_preset_button)
        self.wg.addEdge(self.sample_rate_combo, self.save_audio_preset_button)
        self.wg.addEdge(self.sample_depth_combo, self.save_audio_preset_button)

        self.updateUI()

        self.createAudioNoPreset(self.audio_presets)
        self.createVideoNoPreset(self.video_presets)

    def bindPar(self, mgr):

        def updatePar(value):
            # activate par so we can set the value
            self.select_par_radiobutton.props.active = True
            self.par_fraction_widget.setWidgetValue(value)

        mgr.bindWidget("par", updatePar,
            self.par_fraction_widget.getWidgetValue)

    def bindFractionWidget(self, mgr, name, widget):
        mgr.bindWidget(name, widget.setWidgetValue,
            widget.getWidgetValue)

    def bindCombo(self, mgr, name, widget):
        mgr.bindWidget(name,
            lambda x: set_combo_value(widget, x),
            lambda: get_combo_value(widget))

    def bindSpinbutton(self, mgr, name, widget):
        mgr.bindWidget(name,
            lambda x: widget.set_value(float(x)),
            lambda: int(widget.get_value()))

    def _fillPresetsTreeview(self, treeview, mgr, update_buttons_func):
        """Set up the specified treeview to display the specified presets.

        @param treeview: The treeview for displaying the presets.
        @type treeview: TreeView
        @param mgr: The preset manager.
        @type mgr: PresetManager
        @param update_buttons_func: A function which updates the buttons for
        removing and saving a preset, enabling or disabling them accordingly.
        @type update_buttons_func: function
        """
        renderer = gtk.CellRendererText()
        renderer.props.editable = True
        column = gtk.TreeViewColumn("Preset", renderer, text=0)
        treeview.append_column(column)
        treeview.props.headers_visible = False
        model = mgr.getModel()
        treeview.set_model(model)
        model.connect("row-inserted", self._newPresetCb, column, renderer, treeview)
        renderer.connect("edited", self._presetNameEditedCb, mgr)
        renderer.connect("editing-started", self._presetNameEditingStartedCb, mgr)
        treeview.get_selection().connect("changed", self._presetChangedCb, mgr,
                                                    update_buttons_func)
        treeview.connect("focus-out-event", self._treeviewDefocusedCb, mgr)

    def createAudioNoPreset(self, mgr):
        mgr.prependPreset(_("No preset"), {
            "depth": int(get_combo_value(self.sample_depth_combo)),
            "channels": int(get_combo_value(self.channels_combo)),
            "sample-rate": int(get_combo_value(self.sample_rate_combo))})

    def createVideoNoPreset(self, mgr):
        mgr.prependPreset(_("No preset"), {
            "par": gst.Fraction(int(get_combo_value(self.par_combo).num),
                                    int(get_combo_value(self.par_combo).denom)),
            "frame-rate": gst.Fraction(int(get_combo_value(self.frame_rate_combo).num),
                            int(get_combo_value(self.frame_rate_combo).denom)),
            "height": int(self.height_spinbutton.get_value()),
            "width": int(self.width_spinbutton.get_value())})

    def _newPresetCb(self, model, path, iter_, column, renderer, treeview):
        """Handle the addition of a preset to the model of the preset manager.
        """
        treeview.set_cursor_on_cell(path, column, renderer, start_editing=True)
        treeview.grab_focus()

    def _presetNameEditedCb(self, renderer, path, new_text, mgr):
        """Handle the renaming of a preset."""
        try:
            mgr.renamePreset(path, new_text)
        except DuplicatePresetNameException:
            error_markup = _('"%s" already exists.') % new_text
            self._showPresetManagerError(mgr, error_markup)

    def _presetNameEditingStartedCb(self, renderer, editable, path, mgr):
        """Handle the start of a preset renaming."""
        self._hidePresetManagerError(mgr)

    def _presetChangedCb(self, selection, mgr, update_preset_buttons_func):
        """Handle the selection of a preset."""
        model, iter_ = selection.get_selected()
        if iter_:
            preset = model[iter_][0]
        else:
            preset = None
        mgr.restorePreset(preset)
        update_preset_buttons_func()
        self._hidePresetManagerError(mgr)

    def _treeviewDefocusedCb(self, widget, event, mgr):
        self._hidePresetManagerError(mgr)

    def _showPresetManagerError(self, mgr, error_markup):
        """Show the specified error on the infobar associated with the manager.

        @param mgr: The preset manager for which to show the error.
        @type mgr: PresetManager
        """
        infobar = self._infobarForPresetManager[mgr]
        # The infobar must contain exactly one object in the content area:
        # a label for displaying the error.
        label = infobar.get_content_area().children()[0]
        label.set_markup(error_markup)
        infobar.show()

    def _hidePresetManagerError(self, mgr):
        """Hide the error infobar associated with the manager.

        @param mgr: The preset manager for which to hide the error infobar.
        @type mgr: PresetManager
        """
        infobar = self._infobarForPresetManager[mgr]
        infobar.hide()

    def constrained(self):
        return self.constrain_sar_button.props.active

    def _updateFraction(self, unused, fraction, combo):
        fraction.setWidgetValue(get_combo_value(combo))

    def _updateCombo(self, unused, combo, fraction):
        set_combo_value(combo, fraction.getWidgetValue())

    def getSAR(self):
        width = int(self.width_spinbutton.get_value())
        height = int(self.height_spinbutton.get_value())
        return gst.Fraction(width, height)

    def _setProperties(self):
        getObj = self.builder.get_object
        self.window = getObj("project-settings-dialog")
        self.video_properties_table = getObj("video_properties_table")
        self.video_properties_table = getObj("video_properties_table")
        self.frame_rate_combo = getObj("frame_rate_combo")
        self.dar_combo = getObj("dar_combo")
        self.par_combo = getObj("par_combo")
        self.channels_combo = getObj("channels_combo")
        self.sample_rate_combo = getObj("sample_rate_combo")
        self.sample_depth_combo = getObj("sample_depth_combo")
        self.year_spinbutton = getObj("year_spinbutton")
        self.author_entry = getObj("author_entry")
        self.width_spinbutton = getObj("width_spinbutton")
        self.height_spinbutton = getObj("height_spinbutton")
        self.save_audio_preset_button = getObj("save_audio_preset_button")
        self.save_video_preset_button = getObj("save_video_preset_button")
        self.audio_preset_treeview = getObj("audio_preset_treeview")
        self.video_preset_treeview = getObj("video_preset_treeview")
        self.select_par_radiobutton = getObj("select_par_radiobutton")
        self.remove_audio_preset_button = getObj("remove_audio_preset_button")
        self.remove_video_preset_button = getObj("remove_video_preset_button")
        self.constrain_sar_button = getObj("constrain_sar_button")
        self.select_dar_radiobutton = getObj("select_dar_radiobutton")
        self.video_preset_infobar = getObj("video-preset-infobar")
        self.audio_preset_infobar = getObj("audio-preset-infobar")
        self.title_entry = getObj("title_entry")
        self.author_entry = getObj("author_entry")
        self.year_spinbutton = getObj("year_spinbutton")

    def _constrainSarButtonToggledCb(self, button):
        if button.props.active:
            self.sar = self.getSAR()

    def _selectDarRadiobuttonToggledCb(self, button):
        state = button.props.active
        self.dar_fraction_widget.set_sensitive(state)
        self.dar_combo.set_sensitive(state)
        self.par_fraction_widget.set_sensitive(not state)
        self.par_combo.set_sensitive(not state)

    @staticmethod
    def _getUniquePresetName(mgr):
        """Get a unique name for a new preset for the specified PresetManager.
        """
        existing_preset_names = list(mgr.getPresetNames())
        preset_name = _("New preset")
        i = 1
        while preset_name in existing_preset_names:
            preset_name = _("New preset %d") % i
            i += 1
        return preset_name

    def _addAudioPresetButtonClickedCb(self, button):
        preset_name = self._getUniquePresetName(self.audio_presets)
        self.audio_presets.addPreset(preset_name, {
            "channels": get_combo_value(self.channels_combo),
            "sample-rate": get_combo_value(self.sample_rate_combo),
            "depth": get_combo_value(self.sample_depth_combo)
        })
        self.audio_presets.restorePreset(preset_name)
        self._updateAudioPresetButtons()

    def _removeAudioPresetButtonClickedCb(self, button):
        selection = self.audio_preset_treeview.get_selection()
        model, iter_ = selection.get_selected()
        if iter_:
            self.audio_presets.removePreset(model[iter_][0])

    def _saveAudioPresetButtonClickedCb(self, button):
        self.audio_presets.savePreset()
        self.save_audio_preset_button.set_sensitive(False)
        self.remove_audio_preset_button.set_sensitive(True)

    def _addVideoPresetButtonClickedCb(self, button):
        preset_name = self._getUniquePresetName(self.video_presets)
        self.video_presets.addPreset(preset_name, {
            "width": int(self.width_spinbutton.get_value()),
            "height": int(self.height_spinbutton.get_value()),
            "frame-rate": self.frame_rate_fraction_widget.getWidgetValue(),
            "par": self.par_fraction_widget.getWidgetValue(),
        })
        self.video_presets.restorePreset(preset_name)
        self._updateVideoPresetButtons()

    def _removeVideoPresetButtonClickedCb(self, button):
        selection = self.video_preset_treeview.get_selection()
        model, iter_ = selection.get_selected()
        if iter_:
            self.video_presets.removePreset(model[iter_][0])

    def _saveVideoPresetButtonClickedCb(self, button):
        self.video_presets.savePreset()
        self.save_video_preset_button.set_sensitive(False)
        self.remove_video_preset_button.set_sensitive(True)

    def _updateAudioPresetButtons(self):
        can_save = self.audio_presets.isSaveButtonSensitive()
        self.save_audio_preset_button.set_sensitive(can_save)
        can_remove = self.audio_presets.isRemoveButtonSensitive()
        self.remove_audio_preset_button.set_sensitive(can_remove)

    def _updateVideoPresetButtons(self):
        self.save_video_preset_button.set_sensitive(self.video_presets.isSaveButtonSensitive())
        self.remove_video_preset_button.set_sensitive(self.video_presets.isRemoveButtonSensitive())

    def _updateAudioSaveButton(self, unused_in, button):
        button.set_sensitive(self.audio_presets.isSaveButtonSensitive())

    def _updateVideoSaveButton(self, unused_in, button):
        button.set_sensitive(self.video_presets.isSaveButtonSensitive())

    def darSelected(self):
        return self.select_dar_radiobutton.props.active

    def parSelected(self):
        return not self.darSelected()

    def updateWidth(self):
        height = int(self.height_spinbutton.get_value())
        self.width_spinbutton.set_value(height * self.sar)

    def updateHeight(self):
        width = int(self.width_spinbutton.get_value())
        self.height_spinbutton.set_value(width * (1 / self.sar))

    def updateDarFromPar(self):
        par = self.par_fraction_widget.getWidgetValue()
        sar = self.getSAR()
        self.dar_fraction_widget.setWidgetValue(sar * par)

    def updateParFromDar(self):
        dar = self.dar_fraction_widget.getWidgetValue()
        sar = self.getSAR()
        self.par_fraction_widget.setWidgetValue(dar * (1 / sar))

    def updateDarFromCombo(self):
        self.dar_fraction_widget.setWidgetValue(get_combo_value(self.dar_combo))

    def updateDarFromFractionWidget(self):
        set_combo_value(self.dar_combo, self.dar_fraction_widget.getWidgetValue())

    def updateParFromCombo(self):
        self.par_fraction_widget.setWidgetValue(get_combo_value(self.par_combo))

    def updateParFromFractionWidget(self):
        set_combo_value(self.par_combo, self.par_fraction_widget.getWidgetValue())

    def updateUI(self):

        self.width_spinbutton.set_value(self.settings.videowidth)
        self.height_spinbutton.set_value(self.settings.videoheight)

        # video
        self.frame_rate_fraction_widget.setWidgetValue(self.settings.videorate)
        self.par_fraction_widget.setWidgetValue(self.settings.videopar)

        # audio
        set_combo_value(self.channels_combo, self.settings.audiochannels)
        set_combo_value(self.sample_rate_combo, self.settings.audiorate)
        set_combo_value(self.sample_depth_combo, self.settings.audiodepth)

        self._selectDarRadiobuttonToggledCb(self.select_dar_radiobutton)

        # metadata
        self.title_entry.set_text(self.project.name)
        self.author_entry.set_text(self.project.author)
        if self.project.year:
            year = int(self.project.year)
        else:
            year = datetime.now().year
        self.year_spinbutton.get_adjustment().set_value(year)

    def updateMetadata(self):
        self.project.name = self.title_entry.get_text()
        self.project.author = self.author_entry.get_text()
        self.project.year = str(self.year_spinbutton.get_value_as_int())

    def updateSettings(self):
        width = int(self.width_spinbutton.get_value())
        height = int(self.height_spinbutton.get_value())
        par = self.par_fraction_widget.getWidgetValue()
        frame_rate = self.frame_rate_fraction_widget.getWidgetValue()

        channels = get_combo_value(self.channels_combo)
        sample_rate = get_combo_value(self.sample_rate_combo)
        sample_depth = get_combo_value(self.sample_depth_combo)

        self.settings.setVideoProperties(width, height, frame_rate, par)
        self.settings.setAudioProperties(channels, sample_rate, sample_depth)

        self.project.setSettings(self.settings)

    def _responseCb(self, unused_widget, response):
        if response == gtk.RESPONSE_OK:
            self.updateSettings()
            self.updateMetadata()
        self.window.destroy()
Exemplo n.º 6
0
class RenderDialog(Loggable):
    """Render dialog box.

    @ivar preferred_aencoder: The last audio encoder selected by the user.
    @type preferred_aencoder: str
    @ivar preferred_vencoder: The last video encoder selected by the user.
    @type preferred_vencoder: str
    @ivar settings: The settings used for rendering.
    @type settings: MultimediaSettings
    """
    INHIBIT_REASON = _("Currently rendering")

    def __init__(self, app, project, pipeline=None):

        from pitivi.preset import RenderPresetManager

        Loggable.__init__(self)

        self.app = app
        self.project = project
        self.system = app.system
        if pipeline != None:
            self._pipeline = pipeline
        else:
            self._pipeline = self.project.pipeline

        self.outfile = None
        self.settings = project.getSettings()
        self.timestarted = 0

        # Various gstreamer signal connection ID's
        # {object: sigId}
        self._gstSigId = {}

        self.builder = Gtk.Builder()
        self.builder.add_from_file(os.path.join(configure.get_ui_dir(),
            "renderingdialog.ui"))
        self._setProperties()
        self.builder.connect_signals(self)

        # UI widgets
        icon = os.path.join(configure.get_pixmap_dir(), "pitivi-render-16.png")
        self.window.set_icon_from_file(icon)

        # Set the shading style in the toolbar below presets
        presets_toolbar = self.builder.get_object("render_presets_toolbar")
        presets_toolbar.get_style_context().add_class("inline-toolbar")

        # FIXME: re-enable this widget when bug #637078 is implemented
        self.selected_only_button.destroy()

        # Directory and Filename
        self.filebutton.set_current_folder(self.app.settings.lastExportFolder)
        if not self.project.name:
            self.updateFilename(_("Untitled"))
        else:
            self.updateFilename(self.project.name)

        # We store these so that when the user tries various container formats,
        # (AKA muxers) we select these a/v encoders, if they are compatible with
        # the current container format.
        self.preferred_vencoder = self.settings.vencoder
        self.preferred_aencoder = self.settings.aencoder

        self._initializeComboboxModels()
        self._displaySettings()
        self._displayRenderSettings()

        self.window.connect("delete-event", self._deleteEventCb)
        self.settings.connect("settings-changed", self._settingsChanged)

        # Monitor changes

        self.wg = RippleUpdateGroup()
        self.wg.addVertex(self.frame_rate_combo, signal="changed")
        self.wg.addVertex(self.save_render_preset_button,
                            update_func=self._updateRenderSaveButton)
        self.wg.addVertex(self.channels_combo, signal="changed")
        self.wg.addVertex(self.sample_rate_combo, signal="changed")
        self.wg.addVertex(self.sample_depth_combo, signal="changed")
        self.wg.addVertex(self.muxercombobox, signal="changed")
        self.wg.addVertex(self.audio_encoder_combo, signal="changed")
        self.wg.addVertex(self.video_encoder_combo, signal="changed")
        self.render_presets = RenderPresetManager()
        self.render_presets.loadAll()

        self._fillPresetsTreeview(
                self.render_preset_treeview, self.render_presets,
                self._updateRenderPresetButtons)

        self.wg.addEdge(self.frame_rate_combo, self.save_render_preset_button)
        self.wg.addEdge(self.audio_encoder_combo, self.save_render_preset_button)
        self.wg.addEdge(self.video_encoder_combo, self.save_render_preset_button)
        self.wg.addEdge(self.muxercombobox, self.save_render_preset_button)
        self.wg.addEdge(self.channels_combo, self.save_render_preset_button)
        self.wg.addEdge(self.sample_rate_combo, self.save_render_preset_button)
        self.wg.addEdge(self.sample_depth_combo, self.save_render_preset_button)

        self._infobarForPresetManager = {
                self.render_presets: self.render_preset_infobar}

        # Bind widgets to RenderPresetsManager
        self.bindCombo(self.render_presets, "channels", self.channels_combo)
        self.bindCombo(self.render_presets, "sample-rate", self.sample_rate_combo)
        self.bindCombo(self.render_presets, "depth", self.sample_depth_combo)
        self.bindCombo(self.render_presets, "acodec", self.audio_encoder_combo)
        self.bindCombo(self.render_presets, "vcodec", self.video_encoder_combo)
        self.bindCombo(self.render_presets, "container", self.muxercombobox)
        self.bindCombo(self.render_presets, "frame-rate", self.frame_rate_combo)
        self.bindHeight(self.render_presets)
        self.bindWidth(self.render_presets)

        self.createNoPreset(self.render_presets)

    def createNoPreset(self, mgr):
        mgr.prependPreset(_("No preset"), {
            "depth": int(get_combo_value(self.sample_depth_combo)),
            "channels": int(get_combo_value(self.channels_combo)),
            "sample-rate": int(get_combo_value(self.sample_rate_combo)),
            "acodec": get_combo_value(self.audio_encoder_combo).get_name(),
            "vcodec": get_combo_value(self.video_encoder_combo).get_name(),
            "container": get_combo_value(self.muxercombobox).get_name(),
            "frame-rate": Gst.Fraction(int(get_combo_value(self.frame_rate_combo).num),
                                        int(get_combo_value(self.frame_rate_combo).denom)),
            "height": self.getDimension("height"),
            "width": self.getDimension("width")})

    def bindCombo(self, mgr, name, widget):
        if name == "container":
            mgr.bindWidget(name,
                lambda x: self.muxer_setter(widget, x),
                lambda: get_combo_value(widget).get_name())

        elif name == "acodec":
            mgr.bindWidget(name,
                lambda x: self.acodec_setter(widget, x),
                lambda: get_combo_value(widget).get_name())

        elif name == "vcodec":
            mgr.bindWidget(name,
                lambda x: self.vcodec_setter(widget, x),
                lambda: get_combo_value(widget).get_name())

        elif name == "depth":
            mgr.bindWidget(name,
                lambda x: self.sample_depth_setter(widget, x),
                lambda: get_combo_value(widget))

        elif name == "sample-rate":
            mgr.bindWidget(name,
                lambda x: self.sample_rate_setter(widget, x),
                lambda: get_combo_value(widget))

        elif name == "channels":
            mgr.bindWidget(name,
                lambda x: self.channels_setter(widget, x),
                lambda: get_combo_value(widget))

        elif name == "frame-rate":
            mgr.bindWidget(name,
                lambda x: self.framerate_setter(widget, x),
                lambda: get_combo_value(widget))

    def muxer_setter(self, widget, value):
        set_combo_value(widget, Gst.ElementFactory.find(value))
        self.settings.setEncoders(muxer=value)

        # Update the extension of the filename.
        basename = os.path.splitext(self.fileentry.get_text())[0]
        self.updateFilename(basename)

        # Update muxer-dependent widgets.
        self.muxer_combo_changing = True
        try:
            self.updateAvailableEncoders()
        finally:
            self.muxer_combo_changing = False

    def acodec_setter(self, widget, value):
        set_combo_value(widget, Gst.ElementFactory.find(value))
        self.settings.setEncoders(aencoder=value)
        if not self.muxer_combo_changing:
            # The user directly changed the audio encoder combo.
            self.preferred_aencoder = value

    def vcodec_setter(self, widget, value):
        set_combo_value(widget, Gst.ElementFactory.find(value))
        self.settings.setEncoders(vencoder=value)
        if not self.muxer_combo_changing:
            # The user directly changed the video encoder combo.
            self.preferred_vencoder = value

    def sample_depth_setter(self, widget, value):
        set_combo_value(widget, value)
        self.settings.setAudioProperties(depth=value)

    def sample_rate_setter(self, widget, value):
        set_combo_value(widget, value)
        self.settings.setAudioProperties(rate=value)

    def channels_setter(self, widget, value):
        set_combo_value(widget, value)
        self.settings.setAudioProperties(nbchanns=value)

    def framerate_setter(self, widget, value):
        set_combo_value(widget, value)
        self.settings.setVideoProperties(framerate=value)

    def bindHeight(self, mgr):
        mgr.bindWidget("height",
                       lambda x: self.settings.setVideoProperties(height=x),
                       lambda: 0)

    def bindWidth(self, mgr):
        mgr.bindWidget("width",
                       lambda x: self.settings.setVideoProperties(width=x),
                       lambda: 0)

    def getDimension(self, dimension):
        value = self.settings.getVideoWidthAndHeight()
        if dimension == "height":
            return value[1]
        elif dimension == "width":
            return value[0]

    def _fillPresetsTreeview(self, treeview, mgr, update_buttons_func):
        """Set up the specified treeview to display the specified presets.

        @param treeview: The treeview for displaying the presets.
        @type treeview: TreeView
        @param mgr: The preset manager.
        @type mgr: PresetManager
        @param update_buttons_func: A function which updates the buttons for
        removing and saving a preset, enabling or disabling them accordingly.
        @type update_buttons_func: function
        """
        renderer = Gtk.CellRendererText()
        renderer.props.editable = True
        column = Gtk.TreeViewColumn("Preset", renderer, text=0)
        treeview.append_column(column)
        treeview.props.headers_visible = False
        model = mgr.getModel()
        treeview.set_model(model)
        model.connect("row-inserted", self._newPresetCb, column, renderer, treeview)
        renderer.connect("edited", self._presetNameEditedCb, mgr)
        renderer.connect("editing-started", self._presetNameEditingStartedCb, mgr)
        treeview.get_selection().connect("changed", self._presetChangedCb,
                                        mgr, update_buttons_func)
        treeview.connect("focus-out-event", self._treeviewDefocusedCb, mgr)

    def _newPresetCb(self, model, path, iter_, column, renderer, treeview):
        """Handle the addition of a preset to the model of the preset manager.
        """
        treeview.set_cursor_on_cell(path, column, renderer, start_editing=True)
        treeview.grab_focus()

    def _presetNameEditedCb(self, renderer, path, new_text, mgr):
        """Handle the renaming of a preset."""
        from pitivi.preset import DuplicatePresetNameException

        try:
            mgr.renamePreset(path, new_text)
            self._updateRenderPresetButtons()
        except DuplicatePresetNameException:
            error_markup = _('"%s" already exists.') % new_text
            self._showPresetManagerError(mgr, error_markup)

    def _presetNameEditingStartedCb(self, renderer, editable, path, mgr):
        """Handle the start of a preset renaming."""
        self._hidePresetManagerError(mgr)

    def _treeviewDefocusedCb(self, widget, event, mgr):
        """Handle the treeview loosing the focus."""
        self._hidePresetManagerError(mgr)

    def _showPresetManagerError(self, mgr, error_markup):
        """Show the specified error on the infobar associated with the manager.

        @param mgr: The preset manager for which to show the error.
        @type mgr: PresetManager
        """
        infobar = self._infobarForPresetManager[mgr]
        # The infobar must contain exactly one object in the content area:
        # a label for displaying the error.
        label = infobar.get_content_area().children()[0]
        label.set_markup(error_markup)
        infobar.show()

    def _hidePresetManagerError(self, mgr):
        """Hide the error infobar associated with the manager.

        @param mgr: The preset manager for which to hide the error infobar.
        @type mgr: PresetManager
        """
        infobar = self._infobarForPresetManager[mgr]
        infobar.hide()

    def _updateRenderSaveButton(self, unused_in, button):
        button.set_sensitive(self.render_presets.isSaveButtonSensitive())

    @staticmethod
    def _getUniquePresetName(mgr):
        """Get a unique name for a new preset for the specified PresetManager.
        """
        existing_preset_names = list(mgr.getPresetNames())
        preset_name = _("New preset")
        i = 1
        while preset_name in existing_preset_names:
            preset_name = _("New preset %d") % i
            i += 1
        return preset_name

    def _addRenderPresetButtonClickedCb(self, button):
        preset_name = self._getUniquePresetName(self.render_presets)
        self.render_presets.addPreset(preset_name, {
            "depth": int(get_combo_value(self.sample_depth_combo)),
            "channels": int(get_combo_value(self.channels_combo)),
            "sample-rate": int(get_combo_value(self.sample_rate_combo)),
            "acodec": get_combo_value(self.audio_encoder_combo).get_name(),
            "vcodec": get_combo_value(self.video_encoder_combo).get_name(),
            "container": get_combo_value(self.muxercombobox).get_name(),
            "frame-rate": Gst.Fraction(int(get_combo_value(self.frame_rate_combo).num),
                                        int(get_combo_value(self.frame_rate_combo).denom)),
            "height": 0,
            "width": 0})

        self.render_presets.restorePreset(preset_name)
        self._updateRenderPresetButtons()

    def _saveRenderPresetButtonClickedCb(self, button):
        self.render_presets.savePreset()
        self.save_render_preset_button.set_sensitive(False)
        self.remove_render_preset_button.set_sensitive(True)

    def _updateRenderPresetButtons(self):
        can_save = self.render_presets.isSaveButtonSensitive()
        self.save_render_preset_button.set_sensitive(can_save)
        can_remove = self.render_presets.isRemoveButtonSensitive()
        self.remove_render_preset_button.set_sensitive(can_remove)

    def _removeRenderPresetButtonClickedCb(self, button):
        selection = self.render_preset_treeview.get_selection()
        model, iter_ = selection.get_selected()
        if iter_:
            self.render_presets.removePreset(model[iter_][0])

    def _presetChangedCb(self, selection, mgr, update_preset_buttons_func):
        """Handle the selection of a preset."""
        model, iter_ = selection.get_selected()
        if iter_:
            self.selected_preset = model[iter_][0]
        else:
            self.selected_preset = None

        mgr.restorePreset(self.selected_preset)
        self._displaySettings()
        update_preset_buttons_func()
        self._hidePresetManagerError(mgr)

    def _setProperties(self):
        self.window = self.builder.get_object("render-dialog")
        self.selected_only_button = self.builder.get_object("selected_only_button")
        self.video_output_checkbutton = self.builder.get_object("video_output_checkbutton")
        self.audio_output_checkbutton = self.builder.get_object("audio_output_checkbutton")
        self.render_button = self.builder.get_object("render_button")
        self.video_settings_button = self.builder.get_object("video_settings_button")
        self.audio_settings_button = self.builder.get_object("audio_settings_button")
        self.frame_rate_combo = self.builder.get_object("frame_rate_combo")
        self.scale_spinbutton = self.builder.get_object("scale_spinbutton")
        self.channels_combo = self.builder.get_object("channels_combo")
        self.sample_rate_combo = self.builder.get_object("sample_rate_combo")
        self.sample_depth_combo = self.builder.get_object("sample_depth_combo")
        self.muxercombobox = self.builder.get_object("muxercombobox")
        self.audio_encoder_combo = self.builder.get_object("audio_encoder_combo")
        self.video_encoder_combo = self.builder.get_object("video_encoder_combo")
        self.filebutton = self.builder.get_object("filebutton")
        self.fileentry = self.builder.get_object("fileentry")
        self.resolution_label = self.builder.get_object("resolution_label")
        self.render_preset_treeview = self.builder.get_object("render_preset_treeview")
        self.save_render_preset_button = self.builder.get_object("save_render_preset_button")
        self.remove_render_preset_button = self.builder.get_object("remove_render_preset_button")
        self.render_preset_infobar = self.builder.get_object("render-preset-infobar")

    def _settingsChanged(self, settings):
        self.updateResolution()

    def _initializeComboboxModels(self):
        # Avoid loop import
        from pitivi.settings import MultimediaSettings
        self.frame_rate_combo.set_model(frame_rates)
        self.channels_combo.set_model(audio_channels)
        self.sample_rate_combo.set_model(audio_rates)
        self.sample_depth_combo.set_model(audio_depths)
        self.muxercombobox.set_model(factorylist(MultimediaSettings.muxers))

    def _displaySettings(self):
        """Display the settings that also change in the ProjectSettingsDialog.
        """
        # Video settings
        set_combo_value(self.frame_rate_combo, self.settings.videorate)
        # Audio settings
        set_combo_value(self.channels_combo, self.settings.audiochannels)
        set_combo_value(self.sample_rate_combo, self.settings.audiorate)
        set_combo_value(self.sample_depth_combo, self.settings.audiodepth)

    def _displayRenderSettings(self):
        """Display the settings which can be changed only in the RenderDialog.
        """
        # Video settings
        # note: this will trigger an update of the video resolution label
        self.scale_spinbutton.set_value(self.settings.render_scale)
        # Muxer settings
        # note: this will trigger an update of the codec comboboxes
        set_combo_value(self.muxercombobox,
            Gst.ElementFactory.find(self.settings.muxer))

    def _checkForExistingFile(self, *args):
        """
        Display a warning icon and tooltip if the file path already exists.
        """
        path = self.filebutton.get_current_folder()
        if not path:
            # This happens when the window is initialized.
            return
        warning_icon = Gtk.STOCK_DIALOG_WARNING
        filename = self.fileentry.get_text()
        if not filename:
            tooltip_text = _("A file name is required.")
        elif filename and os.path.exists(os.path.join(path, filename)):
            tooltip_text = _("This file already exists.\n"
                             "If you don't want to overwrite it, choose a "
                             "different file name or folder.")
        else:
            warning_icon = None
            tooltip_text = None
        self.fileentry.set_icon_from_stock(1, warning_icon)
        self.fileentry.set_icon_tooltip_text(1, tooltip_text)

    def updateFilename(self, basename):
        """Updates the filename UI element to show the specified file name."""
        extension = extension_for_muxer(self.settings.muxer)
        if extension:
            name = "%s%s%s" % (basename, os.path.extsep, extension)
        else:
            name = basename
        self.fileentry.set_text(name)

    def updateAvailableEncoders(self):
        """Update the encoder comboboxes to show the available encoders."""
        video_encoders = self.settings.getVideoEncoders()
        video_encoder_model = factorylist(video_encoders)
        self.video_encoder_combo.set_model(video_encoder_model)

        audio_encoders = self.settings.getAudioEncoders()
        audio_encoder_model = factorylist(audio_encoders)
        self.audio_encoder_combo.set_model(audio_encoder_model)

        self._updateEncoderCombo(
                self.video_encoder_combo, self.preferred_vencoder)
        self._updateEncoderCombo(
                self.audio_encoder_combo, self.preferred_aencoder)

    def _updateEncoderCombo(self, encoder_combo, preferred_encoder):
        """Select the specified encoder for the specified encoder combo."""
        if preferred_encoder:
            # A preference exists, pick it if it can be found in
            # the current model of the combobox.
            vencoder = Gst.ElementFactory.find(preferred_encoder)
            set_combo_value(encoder_combo, vencoder, default_index=0)
        else:
            # No preference exists, pick the first encoder from
            # the current model of the combobox.
            encoder_combo.set_active(0)

    def _elementSettingsDialog(self, factory, settings_attr):
        """Open a dialog to edit the properties for the specified factory.

        @param factory: An element factory whose properties the user will edit.
        @type factory: Gst.ElementFactory
        @param settings_attr: The MultimediaSettings attribute holding
        the properties.
        @type settings_attr: str
        """
        properties = getattr(self.settings, settings_attr)
        self.dialog = GstElementSettingsDialog(factory, properties=properties,
                                                parent_window=self.window)
        self.dialog.ok_btn.connect("clicked", self._okButtonClickedCb, settings_attr)

    def startAction(self):
        """ Start the render process """
        self._pipeline.set_state(Gst.State.NULL)
        self._pipeline.set_mode(GES.PipelineFlags.SMART_RENDER)
        encodebin = self._pipeline.get_by_name("internal-encodebin")
        self._gstSigId[encodebin] = encodebin.connect("element-added", self._elementAddedCb)
        self._pipeline.set_state(Gst.State.PLAYING)

    def _cancelRender(self, progress):
        self.debug("aborting render")
        self._shutDown()
        self._destroyProgressWindow()

    def _shutDown(self):
        """ The render process has been aborted, shutdown the gstreamer pipeline
        and disconnect from its signals """
        self._pipeline.set_state(Gst.State.NULL)
        self._disconnectFromGst()
        self._pipeline.set_mode(GES.PipelineFlags.FULL_PREVIEW)

    def _pauseRender(self, progress):
        self.app.current.pipeline.togglePlayback()

    def _destroyProgressWindow(self):
        """ Handle the completion or the cancellation of the render process. """
        self.progress.window.destroy()
        self.progress = None
        self.window.show()  # Show the rendering dialog again

    def _disconnectFromGst(self):
        for obj, id in self._gstSigId.iteritems():
            obj.disconnect(id)
        self._gstSigId = {}
        self.app.current.pipeline.disconnect_by_func(self._updatePositionCb)

    def _updateProjectSettings(self):
        """Updates the settings of the project if the render settings changed.
        """
        settings = self.project.getSettings()
        if (settings.muxer == self.settings.muxer
            and settings.aencoder == self.settings.aencoder
            and settings.vencoder == self.settings.vencoder
            and settings.containersettings == self.settings.containersettings
            and settings.acodecsettings == self.settings.acodecsettings
            and settings.vcodecsettings == self.settings.vcodecsettings
            and settings.render_scale == self.settings.render_scale):
            # No setting which can be changed in the Render dialog
            # and which we want to save have been changed.
            return
        settings.setEncoders(muxer=self.settings.muxer,
                             aencoder=self.settings.aencoder,
                             vencoder=self.settings.vencoder)
        settings.containersettings = self.settings.containersettings
        settings.acodecsettings = self.settings.acodecsettings
        settings.vcodecsettings = self.settings.vcodecsettings
        settings.setVideoProperties(render_scale=self.settings.render_scale)
        # Signal that the project settings have been changed.
        self.project.setSettings(settings)

    def destroy(self):
        self._updateProjectSettings()
        self.window.destroy()

    #------------------- Callbacks ------------------------------------------#

    #-- UI callbacks
    def _okButtonClickedCb(self, unused_button, settings_attr):
        setattr(self.settings, settings_attr, self.dialog.getSettings())
        self.dialog.window.destroy()

    def _renderButtonClickedCb(self, unused_button):
        """
        The render button inside the render dialog has been clicked,
        start the rendering process.
        """
        self.outfile = os.path.join(self.filebutton.get_uri(),
                                    self.fileentry.get_text())
        self.progress = RenderingProgressDialog(self.app, self)
        self.window.hide()  # Hide the rendering settings dialog while rendering

        # FIXME GES: Handle presets here!
        self.containerprofile = EncodingContainerProfile.new(None, None,
                                    Gst.caps_from_string(self.muxertype), None)

        if self.video_output_checkbutton.get_active():
            self.videoprofile = EncodingVideoProfile.new(
                                    Gst.caps_from_string(self.videotype), None,
                                    self.settings.getVideoCaps(True), 0)
            self.containerprofile.add_profile(self.videoprofile)
        if self.audio_output_checkbutton.get_active():
            self.audioprofile = EncodingAudioProfile.new(
                                    Gst.caps_from_string(self.audiotype), None,
                                    self.settings.getAudioCaps(), 0)
            self.containerprofile.add_profile(self.audioprofile)

        self._pipeline.set_render_settings(self.outfile, self.containerprofile)
        self.startAction()
        self.progress.window.show()
        self.progress.connect("cancel", self._cancelRender)
        self.progress.connect("pause", self._pauseRender)
        bus = self._pipeline.get_bus()
        bus.add_signal_watch()
        self._gstSigId[bus] = bus.connect('message', self._busMessageCb)
        self.app.current.pipeline.connect("position", self._updatePositionCb)

    def _closeButtonClickedCb(self, unused_button):
        self.debug("Render dialog's Close button clicked")
        self.destroy()

    def _deleteEventCb(self, window, event):
        self.debug("Render dialog is being deleted")
        self.destroy()

    #-- GStreamer callbacks
    def _busMessageCb(self, unused_bus, message):
        if message.type == Gst.MessageType.EOS:  # Render complete
            self.debug("got EOS message, render complete")
            self._shutDown()
            self._destroyProgressWindow()
        elif message.type == Gst.MessageType.STATE_CHANGED and self.progress:
            prev, state, pending = message.parse_state_changed()
            if message.src == self._pipeline:
                state_really_changed = pending == Gst.State.VOID_PENDING
                if state_really_changed:
                    if state == Gst.State.PLAYING:
                        self.debug("Rendering started/resumed, resetting ETA calculation and inhibiting sleep")
                        self.timestarted = time.time()
                        self.system.inhibitSleep(RenderDialog.INHIBIT_REASON)
                    else:
                        self.system.uninhibitSleep(RenderDialog.INHIBIT_REASON)

    def _updatePositionCb(self, pipeline, position):
        if self.progress:
            text = None
            timediff = time.time() - self.timestarted
            length = self.app.current.timeline.props.duration
            fraction = float(min(position, length)) / float(length)
            if timediff > 5.0 and position:
                # only display ETA after 5s in order to have enough averaging and
                # if the position is non-null
                totaltime = (timediff * float(length) / float(position)) - timediff
                text = beautify_ETA(int(totaltime * Gst.SECOND))
            self.progress.updatePosition(fraction, text)

    def _elementAddedCb(self, bin, element):
        # Setting properties on Gst.Element-s has they are added to the
        # Gst.Encodebin
        if element.get_factory() == get_combo_value(self.video_encoder_combo):
            for setting in self.settings.vcodecsettings:
                element.set_property(setting, self.settings.vcodecsettings[setting])
        elif element.get_factory() == get_combo_value(self.audio_encoder_combo):
            for setting in self.settings.acodecsettings:
                element.set_property(setting, self.settings.vcodecsettings[setting])

    #-- Settings changed callbacks
    def _scaleSpinbuttonChangedCb(self, button):
        render_scale = self.scale_spinbutton.get_value()
        self.settings.setVideoProperties(render_scale=render_scale)
        self.updateResolution()

    def updateResolution(self):
        width, height = self.settings.getVideoWidthAndHeight(render=True)
        self.resolution_label.set_text(u"%d×%d" % (width, height))

    def _projectSettingsButtonClickedCb(self, button):
        from pitivi.project import ProjectSettingsDialog
        dialog = ProjectSettingsDialog(self.window, self.project)
        dialog.window.connect("destroy", self._projectSettingsDestroyCb)
        dialog.window.run()

    def _projectSettingsDestroyCb(self, dialog):
        """Handle the destruction of the ProjectSettingsDialog."""
        settings = self.project.getSettings()
        self.settings.setVideoProperties(width=settings.videowidth,
                                         height=settings.videoheight,
                                         framerate=settings.videorate)
        self.settings.setAudioProperties(nbchanns=settings.audiochannels,
                                         rate=settings.audiorate,
                                         depth=settings.audiodepth)
        self._displaySettings()

    def _audioOutputCheckbuttonToggledCb(self, audio):
        active = self.audio_output_checkbutton.get_active()
        if active:
            self.channels_combo.set_sensitive(True)
            self.sample_rate_combo.set_sensitive(True)
            self.sample_depth_combo.set_sensitive(True)
            self.audio_encoder_combo.set_sensitive(True)
            self.audio_settings_button.set_sensitive(True)
            self.render_button.set_sensitive(True)
        else:
            self.channels_combo.set_sensitive(False)
            self.sample_rate_combo.set_sensitive(False)
            self.sample_depth_combo.set_sensitive(False)
            self.audio_encoder_combo.set_sensitive(False)
            self.audio_settings_button.set_sensitive(False)
            if not self.video_output_checkbutton.get_active():
                self.render_button.set_sensitive(False)

    def _videoOutputCheckbuttonToggledCb(self, video):
        active = self.video_output_checkbutton.get_active()
        if active:
            self.scale_spinbutton.set_sensitive(True)
            self.frame_rate_combo.set_sensitive(True)
            self.video_encoder_combo.set_sensitive(True)
            self.video_settings_button.set_sensitive(True)
            self.render_button.set_sensitive(True)
        else:
            self.scale_spinbutton.set_sensitive(False)
            self.frame_rate_combo.set_sensitive(False)
            self.video_encoder_combo.set_sensitive(False)
            self.video_settings_button.set_sensitive(False)
            if not self.audio_output_checkbutton.get_active():
                self.render_button.set_sensitive(False)

    def _frameRateComboChangedCb(self, combo):
        framerate = get_combo_value(combo)
        self.settings.setVideoProperties(framerate=framerate)

    def _videoEncoderComboChangedCb(self, combo):
        vencoder = get_combo_value(combo).get_name()
        for template in Gst.Registry.get().lookup_feature(vencoder).get_static_pad_templates():
            if template.name_template == "src":
                self.videotype = template.get_caps().to_string()
                for elem in self.videotype.split(","):
                    if "{" in elem or "[" in elem:
                        self.videotype = self.videotype[:self.videotype.index(elem) - 1]
                        break
        self.settings.setEncoders(vencoder=vencoder)
        if not self.muxer_combo_changing:
            # The user directly changed the video encoder combo.
            self.preferred_vencoder = vencoder

    def _videoSettingsButtonClickedCb(self, button):
        factory = get_combo_value(self.video_encoder_combo)
        self._elementSettingsDialog(factory, 'vcodecsettings')

    def _channelsComboChangedCb(self, combo):
        self.settings.setAudioProperties(nbchanns=get_combo_value(combo))

    def _sampleDepthComboChangedCb(self, combo):
        self.settings.setAudioProperties(depth=get_combo_value(combo))

    def _sampleRateComboChangedCb(self, combo):
        self.settings.setAudioProperties(rate=get_combo_value(combo))

    def _audioEncoderChangedComboCb(self, combo):
        aencoder = get_combo_value(combo).get_name()
        self.settings.setEncoders(aencoder=aencoder)
        for template in Gst.Registry.get().lookup_feature(aencoder).get_static_pad_templates():
            if template.name_template == "src":
                self.audiotype = template.get_caps().to_string()
                for elem in self.audiotype.split(","):
                    if "{" in elem or "[" in elem:
                        self.audiotype = self.audiotype[:self.audiotype.index(elem) - 1]
                        break
        if not self.muxer_combo_changing:
            # The user directly changed the audio encoder combo.
            self.preferred_aencoder = aencoder

    def _audioSettingsButtonClickedCb(self, button):
        factory = get_combo_value(self.audio_encoder_combo)
        self._elementSettingsDialog(factory, 'acodecsettings')

    def _muxerComboChangedCb(self, muxer_combo):
        """Handle the changing of the container format combobox."""
        muxer = get_combo_value(muxer_combo).get_name()
        for template in Gst.Registry.get().lookup_feature(muxer).get_static_pad_templates():
            if template.name_template == "src":
                self.muxertype = template.get_caps().to_string()
        self.settings.setEncoders(muxer=muxer)

        # Update the extension of the filename.
        basename = os.path.splitext(self.fileentry.get_text())[0]
        self.updateFilename(basename)

        # Update muxer-dependent widgets.
        self.muxer_combo_changing = True
        try:
            self.updateAvailableEncoders()
        finally:
            self.muxer_combo_changing = False
Exemplo n.º 7
0
class RenderDialog(Loggable):
    """Render dialog box.

    Args:
        app (Pitivi): The app.
        project (Project): The project to be rendered.

    Attributes:
        preferred_aencoder (str): The last audio encoder selected by the user.
        preferred_vencoder (str): The last video encoder selected by the user.
    """
    INHIBIT_REASON = _("Currently rendering")

    _factory_formats = {}

    def __init__(self, app, project):
        Loggable.__init__(self)

        self.app = app
        self.project = project
        self.system = app.system
        self._pipeline = self.project.pipeline

        self.outfile = None
        self.notification = None

        # Variables to keep track of progress indication timers:
        self._filesizeEstimateTimer = self._timeEstimateTimer = None
        self._is_rendering = False
        self._rendering_is_paused = False
        self.current_position = None
        self._time_started = 0
        self._time_spent_paused = 0  # Avoids the ETA being wrong on resume

        # Various gstreamer signal connection ID's
        # {object: sigId}
        self._gstSigId = {}

        self.render_presets = RenderPresetManager(self.app.system, Encoders())
        self.render_presets.loadAll()

        # Whether encoders changing are a result of changing the muxer.
        self.muxer_combo_changing = False
        self._createUi()

        # Directory and Filename
        self.filebutton.set_current_folder(self.app.settings.lastExportFolder)
        if not self.project.name:
            self.updateFilename(_("Untitled"))
        else:
            self.updateFilename(self.project.name)

        # We store these so that when the user tries various container formats,
        # (AKA muxers) we select these a/v encoders, if they are compatible with
        # the current container format.
        self.preferred_vencoder = self.project.vencoder
        self.preferred_aencoder = self.project.aencoder
        self.__unproxiedClips = {}

        self.frame_rate_combo.set_model(frame_rates)
        self.channels_combo.set_model(audio_channels)
        self.sample_rate_combo.set_model(audio_rates)
        self.__initialize_muxers_model()
        self._displaySettings()
        self._displayRenderSettings()

        self.window.connect("delete-event", self._deleteEventCb)
        self.project.connect("rendering-settings-changed",
                             self._settingsChanged)

        # Monitor changes

        self.wg = RippleUpdateGroup()
        self.wg.addVertex(self.frame_rate_combo, signal="changed")
        self.wg.addVertex(self.channels_combo, signal="changed")
        self.wg.addVertex(self.sample_rate_combo, signal="changed")
        self.wg.addVertex(self.muxer_combo, signal="changed")
        self.wg.addVertex(self.audio_encoder_combo, signal="changed")
        self.wg.addVertex(self.video_encoder_combo, signal="changed")
        self.wg.addVertex(self.preset_menubutton,
                          update_func=self._updatePresetMenuButton)

        self.wg.addEdge(self.frame_rate_combo, self.preset_menubutton)
        self.wg.addEdge(self.audio_encoder_combo, self.preset_menubutton)
        self.wg.addEdge(self.video_encoder_combo, self.preset_menubutton)
        self.wg.addEdge(self.muxer_combo, self.preset_menubutton)
        self.wg.addEdge(self.channels_combo, self.preset_menubutton)
        self.wg.addEdge(self.sample_rate_combo, self.preset_menubutton)

        # Bind widgets to RenderPresetsManager
        self.render_presets.bindWidget(
            "container", lambda x: self.muxer_setter(self.muxer_combo, x),
            lambda: get_combo_value(self.muxer_combo).get_name())
        self.render_presets.bindWidget(
            "acodec",
            lambda x: self.acodec_setter(self.audio_encoder_combo, x),
            lambda: get_combo_value(self.audio_encoder_combo).get_name())
        self.render_presets.bindWidget(
            "vcodec",
            lambda x: self.vcodec_setter(self.video_encoder_combo, x),
            lambda: get_combo_value(self.video_encoder_combo).get_name())
        self.render_presets.bindWidget(
            "sample-rate",
            lambda x: self.sample_rate_setter(self.sample_rate_combo, x),
            lambda: get_combo_value(self.sample_rate_combo))
        self.render_presets.bindWidget(
            "channels", lambda x: self.channels_setter(self.channels_combo, x),
            lambda: get_combo_value(self.channels_combo))
        self.render_presets.bindWidget(
            "frame-rate",
            lambda x: self.framerate_setter(self.frame_rate_combo, x),
            lambda: get_combo_value(self.frame_rate_combo))
        self.render_presets.bindWidget(
            "height", lambda x: setattr(self.project, "videoheight", x),
            lambda: 0)
        self.render_presets.bindWidget(
            "width", lambda x: setattr(self.project, "videowidth", x),
            lambda: 0)

    def _updatePresetMenuButton(self, unused_source, unused_target):
        self.render_presets.updateMenuActions()

    def muxer_setter(self, widget, muxer_name):
        set_combo_value(widget, Encoders().factories_by_name.get(muxer_name))
        self.project.setEncoders(muxer=muxer_name)

        # Update the extension of the filename.
        basename = os.path.splitext(self.fileentry.get_text())[0]
        self.updateFilename(basename)

        # Update muxer-dependent widgets.
        self.updateAvailableEncoders()

    def acodec_setter(self, widget, aencoder_name):
        set_combo_value(widget,
                        Encoders().factories_by_name.get(aencoder_name))
        self.project.aencoder = aencoder_name
        if not self.muxer_combo_changing:
            # The user directly changed the audio encoder combo.
            self.preferred_aencoder = aencoder_name

    def vcodec_setter(self, widget, vencoder_name):
        set_combo_value(widget,
                        Encoders().factories_by_name.get(vencoder_name))
        self.project.setEncoders(vencoder=vencoder_name)
        if not self.muxer_combo_changing:
            # The user directly changed the video encoder combo.
            self.preferred_vencoder = vencoder_name

    def sample_rate_setter(self, widget, value):
        set_combo_value(widget, value)
        self.project.audiorate = value

    def channels_setter(self, widget, value):
        set_combo_value(widget, value)
        self.project.audiochannels = value

    def framerate_setter(self, widget, value):
        set_combo_value(widget, value)
        self.project.videorate = value

    def _createUi(self):
        builder = Gtk.Builder()
        builder.add_from_file(
            os.path.join(configure.get_ui_dir(), "renderingdialog.ui"))
        builder.connect_signals(self)

        self.window = builder.get_object("render-dialog")
        self.video_output_checkbutton = builder.get_object(
            "video_output_checkbutton")
        self.audio_output_checkbutton = builder.get_object(
            "audio_output_checkbutton")
        self.render_button = builder.get_object("render_button")
        self.video_settings_button = builder.get_object(
            "video_settings_button")
        self.audio_settings_button = builder.get_object(
            "audio_settings_button")
        self.frame_rate_combo = builder.get_object("frame_rate_combo")
        self.frame_rate_combo.set_model(frame_rates)
        self.scale_spinbutton = builder.get_object("scale_spinbutton")
        self.channels_combo = builder.get_object("channels_combo")
        self.channels_combo.set_model(audio_channels)
        self.sample_rate_combo = builder.get_object("sample_rate_combo")
        self.muxer_combo = builder.get_object("muxercombobox")
        self.audio_encoder_combo = builder.get_object("audio_encoder_combo")
        self.video_encoder_combo = builder.get_object("video_encoder_combo")
        self.filebutton = builder.get_object("filebutton")
        self.fileentry = builder.get_object("fileentry")
        self.resolution_label = builder.get_object("resolution_label")
        self.presets_combo = builder.get_object("presets_combo")
        self.preset_menubutton = builder.get_object("preset_menubutton")

        self.video_output_checkbutton.props.active = self.project.video_profile.is_enabled(
        )
        self.audio_output_checkbutton.props.active = self.project.audio_profile.is_enabled(
        )

        self.__automatically_use_proxies = builder.get_object(
            "automatically_use_proxies")

        self.__always_use_proxies = builder.get_object("always_use_proxies")
        self.__always_use_proxies.props.group = self.__automatically_use_proxies

        self.__never_use_proxies = builder.get_object("never_use_proxies")
        self.__never_use_proxies.props.group = self.__automatically_use_proxies

        self.render_presets.setupUi(self.presets_combo, self.preset_menubutton)

        icon = os.path.join(configure.get_pixmap_dir(), "pitivi-render-16.png")
        self.window.set_icon_from_file(icon)
        self.window.set_transient_for(self.app.gui)

    def _settingsChanged(self, unused_project, unused_key, unused_value):
        self.updateResolution()

    def __initialize_muxers_model(self):
        # By default show only supported muxers and encoders.
        model = self.create_combobox_model(Encoders().muxers)
        self.muxer_combo.set_model(model)

    def create_combobox_model(self, factories):
        """Creates a model for a combobox showing factories.

        Args:
            combobox (Gtk.ComboBox): The combobox to setup.
            factories (List[Gst.ElementFactory]): The factories to display.

        Returns:
            Gtk.ListStore: The model with (display name, factory, unsupported).
        """
        model = Gtk.TreeStore(str, object)
        data_supported = []
        data_unsupported = []
        for factory in factories:
            supported = Encoders().is_supported(factory)
            row = (beautify_factory_name(factory), factory)
            if supported:
                data_supported.append(row)
            else:
                data_unsupported.append(row)

        data_supported.sort()
        for row in data_supported:
            model.append(None, row)

        # Translators: This item appears in a combobox's popup and
        # contains as children the unsupported (but still available)
        # muxers and encoders.
        unsupported_iter = model.append(None, (_("Unsupported"), None))
        data_unsupported.sort()
        for row in data_unsupported:
            model.append(unsupported_iter, row)

        return model

    def _displaySettings(self):
        """Displays the settings also in the ProjectSettingsDialog."""
        # Video settings
        set_combo_value(self.frame_rate_combo, self.project.videorate)
        # Audio settings
        set_combo_value(self.channels_combo, self.project.audiochannels)
        set_combo_value(self.sample_rate_combo, self.project.audiorate)

    def _displayRenderSettings(self):
        """Displays the settings available only in the RenderDialog."""
        # Video settings
        # This will trigger an update of the video resolution label.
        self.scale_spinbutton.set_value(self.project.render_scale)
        # Muxer settings
        # This will trigger an update of the codec comboboxes.
        set_combo_value(self.muxer_combo,
                        Encoders().factories_by_name.get(self.project.muxer))

    def _checkForExistingFile(self, *unused_args):
        """Displays a warning if the file path already exists."""
        path = self.filebutton.get_current_folder()
        if not path:
            # This happens when the window is initialized.
            return
        warning_icon = "dialog-warning"
        filename = self.fileentry.get_text()
        if not filename:
            tooltip_text = _("A file name is required.")
        elif filename and os.path.exists(os.path.join(path, filename)):
            tooltip_text = _("This file already exists.\n"
                             "If you don't want to overwrite it, choose a "
                             "different file name or folder.")
        else:
            warning_icon = None
            tooltip_text = None
        self.fileentry.set_icon_from_icon_name(1, warning_icon)
        self.fileentry.set_icon_tooltip_text(1, tooltip_text)

    def _getFilesizeEstimate(self):
        """Estimates the final file size.

        Estimates in megabytes (over 30 MB) are rounded to the nearest 10 MB
        to smooth out small variations. You'd be surprised how imprecision can
        improve perceived accuracy.

        Returns:
            str: A human-readable (ex: "14 MB") estimate for the file size.
        """
        if not self.current_position or self.current_position == 0:
            return None

        current_filesize = os.stat(path_from_uri(self.outfile)).st_size
        length = self.project.ges_timeline.props.duration
        estimated_size = float(current_filesize * float(length) /
                               self.current_position)
        # Now let's make it human-readable (instead of octets).
        # If it's in the giga range (10⁹) instead of mega (10⁶), use 2 decimals
        if estimated_size > 10e8:
            gigabytes = estimated_size / (10**9)
            return _("%.2f GB" % gigabytes)
        else:
            megabytes = int(estimated_size / (10**6))
            if megabytes > 30:
                megabytes = int(round(megabytes, -1))  # -1 means round to 10
            return _("%d MB" % megabytes)

    def updateFilename(self, basename):
        """Updates the filename UI element to show the specified file name."""
        extension = extension_for_muxer(self.project.muxer)
        if extension:
            name = "%s%s%s" % (basename, os.path.extsep, extension)
        else:
            name = basename
        self.fileentry.set_text(name)

    def updateAvailableEncoders(self):
        """Updates the encoder comboboxes to show the available encoders."""
        self.muxer_combo_changing = True
        try:
            model = self.create_combobox_model(
                Encoders().compatible_video_encoders[self.project.muxer])
            self.video_encoder_combo.set_model(model)
            self._update_encoder_combo(self.video_encoder_combo,
                                       self.preferred_vencoder)

            model = self.create_combobox_model(
                Encoders().compatible_audio_encoders[self.project.muxer])
            self.audio_encoder_combo.set_model(model)
            self._update_encoder_combo(self.audio_encoder_combo,
                                       self.preferred_aencoder)
        finally:
            self.muxer_combo_changing = False

    def _update_encoder_combo(self, encoder_combo, preferred_encoder):
        """Selects the specified encoder for the specified encoder combo."""
        if preferred_encoder:
            # A preference exists, pick it if it can be found in
            # the current model of the combobox.
            encoder = Encoders().factories_by_name.get(preferred_encoder)
            set_combo_value(encoder_combo, encoder)
        if not preferred_encoder or not get_combo_value(encoder_combo):
            # No preference exists or it is not available,
            # pick the first encoder from the combobox's model.
            first = encoder_combo.props.model.get_iter_first()
            if not first:
                # Model is empty. Should not happen.
                self.warning("Model is empty")
                return
            if not encoder_combo.props.model.iter_has_child(first):
                # The first item is a supported factory.
                encoder_combo.set_active_iter(first)
            else:
                # The first element is the Unsupported group.
                second = encoder_combo.props.model.iter_nth_child(first, 0)
                encoder_combo.set_active_iter(second)

    def _elementSettingsDialog(self, factory, settings_attr):
        """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.
        """
        properties = getattr(self.project, settings_attr)
        self.dialog = GstElementSettingsDialog(factory,
                                               properties=properties,
                                               parent_window=self.window)
        self.dialog.ok_btn.connect("clicked", self._okButtonClickedCb,
                                   settings_attr)

    def _showRenderErrorDialog(self, error, unused_details):
        primary_message = _("Sorry, something didn’t work right.")
        secondary_message = _(
            "An error occurred while trying to render your "
            "project. You might want to check our "
            "troubleshooting guide or file a bug report. "
            "The GStreamer error was:") + "\n\n<i>" + str(error) + "</i>"

        dialog = Gtk.MessageDialog(transient_for=self.window,
                                   modal=True,
                                   message_type=Gtk.MessageType.ERROR,
                                   buttons=Gtk.ButtonsType.OK,
                                   text=primary_message)
        dialog.set_property("secondary-text", secondary_message)
        dialog.set_property("secondary-use-markup", True)
        dialog.show_all()
        dialog.run()
        dialog.destroy()

    def startAction(self):
        """Starts the render process."""
        self._pipeline.set_state(Gst.State.NULL)
        # FIXME: https://github.com/pitivi/gst-editing-services/issues/23
        self._pipeline.set_mode(GES.PipelineFlags.RENDER)
        encodebin = self._pipeline.get_by_name("internal-encodebin")
        self._gstSigId[encodebin] = encodebin.connect("element-added",
                                                      self._elementAddedCb)
        for element in encodebin.iterate_recurse():
            self._elementAddedCb(encodebin, element)
        self._pipeline.set_state(Gst.State.PLAYING)
        self._is_rendering = True
        self._time_started = time.time()

    def _cancelRender(self, *unused_args):
        self.debug("Aborting render")
        self._shutDown()
        self._destroyProgressWindow()

    def _shutDown(self):
        """Shuts down the pipeline and disconnects from its signals."""
        self._is_rendering = False
        self._rendering_is_paused = False
        self._time_spent_paused = 0
        self._pipeline.set_state(Gst.State.NULL)
        self.__useProxyAssets()
        self._disconnectFromGst()
        self._pipeline.set_mode(GES.PipelineFlags.FULL_PREVIEW)
        self._pipeline.set_state(Gst.State.PAUSED)
        self.project.set_rendering(False)

    def _pauseRender(self, unused_progress):
        self._rendering_is_paused = self.progress.play_pause_button.get_active(
        )
        if self._rendering_is_paused:
            self._last_timestamp_when_pausing = time.time()
        else:
            self._time_spent_paused += time.time(
            ) - self._last_timestamp_when_pausing
            self.debug("Resuming render after %d seconds in pause",
                       self._time_spent_paused)
        self.project.pipeline.togglePlayback()

    def _destroyProgressWindow(self):
        """Handles the completion or the cancellation of the render process."""
        self.progress.window.destroy()
        self.progress = None
        self.window.show()  # Show the rendering dialog again

    def _disconnectFromGst(self):
        for obj, id in self._gstSigId.items():
            obj.disconnect(id)
        self._gstSigId = {}
        try:
            self.project.pipeline.disconnect_by_func(self._updatePositionCb)
        except TypeError:
            # The render was successful, so this was already disconnected
            pass

    def destroy(self):
        self.window.destroy()

    @staticmethod
    def _maybePlayFinishedSound():
        if "pycanberra" in missing_soft_deps:
            return
        import pycanberra
        canberra = pycanberra.Canberra()
        canberra.play(1, pycanberra.CA_PROP_EVENT_ID, "complete-media", None)

    def __maybeUseSourceAsset(self):
        if self.__always_use_proxies.get_active():
            self.debug("Rendering from proxies, not replacing assets")
            return

        for layer in self.app.gui.timeline_ui.ges_timeline.get_layers():
            for clip in layer.get_clips():
                if not isinstance(clip, GES.UriClip):
                    continue

                asset = clip.get_asset()
                asset_target = asset.get_proxy_target()
                if not asset_target:
                    continue

                if self.__automatically_use_proxies.get_active():
                    if self.app.proxy_manager.isAssetFormatWellSupported(
                            asset_target):
                        self.info(
                            "Asset %s format well supported, "
                            "rendering from real asset.",
                            asset_target.props.id)
                    else:
                        self.info(
                            "Asset %s format not well supported, "
                            "rendering from proxy.", asset_target.props.id)
                        continue

                if not asset_target.get_error():
                    clip.set_asset(asset_target)
                    self.error("Using %s as an asset (instead of %s)",
                               asset_target.get_id(), asset.get_id())
                    self.__unproxiedClips[clip] = asset

    def __useProxyAssets(self):
        for clip, asset in self.__unproxiedClips.items():
            clip.set_asset(asset)

        self.__unproxiedClips = {}

    # ------------------- Callbacks ------------------------------------------ #

    # -- UI callbacks
    def _okButtonClickedCb(self, unused_button, settings_attr):
        setattr(self.project, settings_attr, self.dialog.getSettings())
        self.dialog.window.destroy()

    def _renderButtonClickedCb(self, unused_button):
        """Starts the rendering process."""
        self.__maybeUseSourceAsset()
        self.outfile = os.path.join(self.filebutton.get_uri(),
                                    self.fileentry.get_text())
        self.progress = RenderingProgressDialog(self.app, self)
        # Hide the rendering settings dialog while rendering
        self.window.hide()

        encoder_string = self.project.vencoder
        try:
            fmt = self._factory_formats[encoder_string]
            self.project.video_profile.get_restriction()[0]["format"] = fmt
        except KeyError:
            # Now find a format to set on the restriction caps.
            # The reason is we can't send different formats on the encoders.
            factory = Encoders().factories_by_name.get(self.project.vencoder)
            for struct in factory.get_static_pad_templates():
                if struct.direction == Gst.PadDirection.SINK:
                    caps = Gst.Caps.from_string(struct.get_caps().to_string())
                    fixed = caps.fixate()
                    fmt = fixed.get_structure(0).get_value("format")
                    self.project.setVideoRestriction("format", fmt)
                    self._factory_formats[encoder_string] = fmt
                    break

        self.app.gui.timeline_ui.zoomFit()
        self.project.set_rendering(True)
        self._pipeline.set_render_settings(self.outfile,
                                           self.project.container_profile)
        self.startAction()
        self.progress.window.show()
        self.progress.connect("cancel", self._cancelRender)
        self.progress.connect("pause", self._pauseRender)
        bus = self._pipeline.get_bus()
        bus.add_signal_watch()
        self._gstSigId[bus] = bus.connect('message', self._busMessageCb)
        self.project.pipeline.connect("position", self._updatePositionCb)
        # Force writing the config now, or the path will be reset
        # if the user opens the rendering dialog again
        self.app.settings.lastExportFolder = self.filebutton.get_current_folder(
        )
        self.app.settings.storeSettings()

    def _closeButtonClickedCb(self, unused_button):
        self.debug("Render dialog's Close button clicked")
        self.destroy()

    def _deleteEventCb(self, unused_window, unused_event):
        self.debug("Render dialog is being deleted")
        self.destroy()

    def _containerContextHelpClickedCb(self, unused_button):
        show_user_manual("codecscontainers")

    # Periodic (timer) callbacks
    def _updateTimeEstimateCb(self):
        if self._rendering_is_paused:
            return True  # Do nothing until we resume rendering
        elif self._is_rendering:
            timediff = time.time() - \
                self._time_started - self._time_spent_paused
            length = self.project.ges_timeline.props.duration
            totaltime = (timediff * float(length) /
                         float(self.current_position)) - timediff
            time_estimate = beautify_ETA(int(totaltime * Gst.SECOND))
            if time_estimate:
                self.progress.updateProgressbarETA(time_estimate)
            return True
        else:
            self._timeEstimateTimer = None
            self.debug("Stopping the ETA timer")
            return False  # Stop the timer

    def _updateFilesizeEstimateCb(self):
        if self._rendering_is_paused:
            return True  # Do nothing until we resume rendering
        elif self._is_rendering:
            est_filesize = self._getFilesizeEstimate()
            if est_filesize:
                self.progress.setFilesizeEstimate(est_filesize)
            return True
        else:
            self.debug("Stopping the filesize estimation timer")
            self._filesizeEstimateTimer = None
            return False  # Stop the timer

    # GStreamer callbacks
    def _busMessageCb(self, unused_bus, message):
        if message.type == Gst.MessageType.EOS:  # Render complete
            self.debug("got EOS message, render complete")
            self._shutDown()
            self.progress.progressbar.set_fraction(1.0)
            self.progress.progressbar.set_text(_("Render complete"))
            self.progress.window.set_title(_("Render complete"))
            self.progress.setFilesizeEstimate(None)
            if not self.progress.window.is_active():
                notification = _('"%s" has finished rendering.' %
                                 self.fileentry.get_text())
                self.notification = self.app.system.desktopMessage(
                    _("Render complete"), notification, "pitivi")
            self._maybePlayFinishedSound()
            self.progress.play_rendered_file_button.show()
            self.progress.close_button.show()
            self.progress.cancel_button.hide()
            self.progress.play_pause_button.hide()

        elif message.type == Gst.MessageType.ERROR:
            # Errors in a GStreamer pipeline are fatal. If we encounter one,
            # we should abort and show the error instead of sitting around.
            error, details = message.parse_error()
            self._cancelRender()
            self._showRenderErrorDialog(error, details)

        elif message.type == Gst.MessageType.STATE_CHANGED and self.progress:
            prev, state, pending = message.parse_state_changed()
            if message.src == self._pipeline:
                state_really_changed = pending == Gst.State.VOID_PENDING
                if state_really_changed:
                    if state == Gst.State.PLAYING:
                        self.debug(
                            "Rendering started/resumed, inhibiting sleep")
                        self.system.inhibitSleep(RenderDialog.INHIBIT_REASON)
                    else:
                        self.system.uninhibitSleep(RenderDialog.INHIBIT_REASON)

    def _updatePositionCb(self, unused_pipeline, position):
        """Updates the progress bar and triggers the update of the file size.

        This one occurs every time the pipeline emits a position changed signal,
        which is *very* often.
        """
        self.current_position = position
        if not self.progress or not position:
            return

        length = self.project.ges_timeline.props.duration
        fraction = float(min(position, length)) / float(length)
        self.progress.updatePosition(fraction)

        # In order to have enough averaging, only display the ETA after 5s
        timediff = time.time() - self._time_started
        if not self._timeEstimateTimer:
            if timediff < 6:
                self.progress.progressbar.set_text(_("Estimating..."))
            else:
                self._timeEstimateTimer = GLib.timeout_add_seconds(
                    3, self._updateTimeEstimateCb)

        # Filesize is trickier and needs more time to be meaningful.
        if not self._filesizeEstimateTimer and (fraction > 0.33
                                                or timediff > 180):
            self._filesizeEstimateTimer = GLib.timeout_add_seconds(
                5, self._updateFilesizeEstimateCb)

    def _elementAddedCb(self, unused_bin, gst_element):
        """Sets properties on the specified Gst.Element."""
        factory = gst_element.get_factory()
        settings = {}
        if factory == get_combo_value(self.video_encoder_combo):
            settings = self.project.vcodecsettings
        elif factory == get_combo_value(self.audio_encoder_combo):
            settings = self.project.acodecsettings

        for propname, value in settings.items():
            gst_element.set_property(propname, value)
            self.debug("Setting %s to %s", propname, value)

    # Settings changed callbacks
    def _scaleSpinbuttonChangedCb(self, unused_button):
        render_scale = self.scale_spinbutton.get_value()
        self.project.render_scale = render_scale
        self.updateResolution()

    def updateResolution(self):
        width, height = self.project.getVideoWidthAndHeight(True)
        self.resolution_label.set_text("%d×%d" % (width, height))

    def _projectSettingsButtonClickedCb(self, unused_button):
        from pitivi.project import ProjectSettingsDialog
        dialog = ProjectSettingsDialog(self.window, self.project, self.app)
        dialog.window.run()

    def _audioOutputCheckbuttonToggledCb(self, unused_audio):
        active = self.audio_output_checkbutton.get_active()
        self.channels_combo.set_sensitive(active)
        self.sample_rate_combo.set_sensitive(active)
        self.audio_encoder_combo.set_sensitive(active)
        self.audio_settings_button.set_sensitive(active)
        self.project.audio_profile.set_enabled(active)
        self.__updateRenderButtonSensitivity()

    def _videoOutputCheckbuttonToggledCb(self, unused_video):
        active = self.video_output_checkbutton.get_active()
        self.scale_spinbutton.set_sensitive(active)
        self.frame_rate_combo.set_sensitive(active)
        self.video_encoder_combo.set_sensitive(active)
        self.video_settings_button.set_sensitive(active)
        self.project.video_profile.set_enabled(active)
        self.__updateRenderButtonSensitivity()

    def __updateRenderButtonSensitivity(self):
        video_enabled = self.video_output_checkbutton.get_active()
        audio_enabled = self.audio_output_checkbutton.get_active()
        self.render_button.set_sensitive(video_enabled or audio_enabled)

    def _frameRateComboChangedCb(self, combo):
        framerate = get_combo_value(combo)
        self.project.framerate = framerate

    def _videoEncoderComboChangedCb(self, combo):
        factory = get_combo_value(combo)
        name = factory.get_name()
        self.project.vencoder = name
        if not self.muxer_combo_changing:
            # The user directly changed the video encoder combo.
            self.debug("User chose a video encoder: %s", name)
            self.preferred_vencoder = name

    def _videoSettingsButtonClickedCb(self, unused_button):
        factory = get_combo_value(self.video_encoder_combo)
        self._elementSettingsDialog(factory, 'vcodecsettings')

    def _channelsComboChangedCb(self, combo):
        self.project.audiochannels = get_combo_value(combo)

    def _sampleRateComboChangedCb(self, combo):
        self.project.audiorate = get_combo_value(combo)

    def _audioEncoderChangedComboCb(self, combo):
        factory = get_combo_value(combo)
        name = factory.get_name()
        self.project.aencoder = name
        if not self.muxer_combo_changing:
            # The user directly changed the audio encoder combo.
            self.debug("User chose an audio encoder: %s", name)
            self.preferred_aencoder = name

    def _audioSettingsButtonClickedCb(self, unused_button):
        factory = get_combo_value(self.audio_encoder_combo)
        self._elementSettingsDialog(factory, 'acodecsettings')

    def _muxerComboChangedCb(self, combo):
        """Handles the changing of the container format combobox."""
        factory = get_combo_value(combo)
        self.project.muxer = factory.get_name()

        # Update the extension of the filename.
        basename = os.path.splitext(self.fileentry.get_text())[0]
        self.updateFilename(basename)

        # Update muxer-dependent widgets.
        self.updateAvailableEncoders()