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
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
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
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