[pitivi] Port to the new GESAsset/GESProject/GESMetaContainer APIs
- From: Thibault Saunier <tsaunier src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] Port to the new GESAsset/GESProject/GESMetaContainer APIs
- Date: Mon, 14 Jan 2013 12:37:59 +0000 (UTC)
commit 267dd14179d6f8d5eaec5e4b7e39f119e6ca7ef9
Author: Thibault Saunier <thibault saunier collabora com>
Date: Wed Dec 19 18:00:59 2012 -0300
Port to the new GESAsset/GESProject/GESMetaContainer APIs
+ Quickly fix obselete tests
+ Make some refactoring where needed
+ Bump the GSTreamer dependency to 1.1.0, actually master...
bin/pitivi-git-environment.sh | 4 +-
pitivi/application.py | 7 +-
pitivi/check.py | 2 +-
pitivi/clipproperties.py | 4 +-
pitivi/dialogs/clipmediaprops.py | 26 +-
pitivi/dialogs/startupwizard.py | 17 +-
pitivi/mainwindow.py | 125 +++---
pitivi/mediafilespreviewer.py | 3 +-
pitivi/medialibrary.py | 264 +++---------
pitivi/preset.py | 9 +-
pitivi/project.py | 823 +++++++++++++++++++++++++++++---------
pitivi/render.py | 479 +++++++---------------
pitivi/settings.py | 212 ----------
pitivi/timeline/timeline.py | 185 ++++------
pitivi/timeline/track.py | 3 +-
pitivi/transitions.py | 60 ++--
pitivi/utils/ui.py | 6 +-
pitivi/viewer.py | 1 -
tests/test_settings.py | 60 ++--
19 files changed, 1065 insertions(+), 1225 deletions(-)
---
diff --git a/bin/pitivi-git-environment.sh b/bin/pitivi-git-environment.sh
index 616b8a7..589d6bb 100755
--- a/bin/pitivi-git-environment.sh
+++ b/bin/pitivi-git-environment.sh
@@ -16,7 +16,9 @@
MYPITIVI=${MYPITIVI:-$HOME/pitivi-git}
# Change this variable to 'master' if you prefer to work with the master branch
# When using "master", this script will automatically "pull --rebase" modules.
-GST_RELEASE_TAG="1.0.2"
+# This will fail and check master in the end
+# Using master until we depend on a released version
+GST_RELEASE_TAG="master"
# If you care about building the GStreamer/GES developer API documentation:
BUILD_DOCS=false
# Here are some dependencies for building GStreamer and GES. If they're missing,
diff --git a/pitivi/application.py b/pitivi/application.py
index 06a054a..23d284e 100644
--- a/pitivi/application.py
+++ b/pitivi/application.py
@@ -43,7 +43,6 @@ from pitivi.settings import GlobalSettings
from pitivi.utils.threads import ThreadMaster
from pitivi.mainwindow import PitiviMainWindow
from pitivi.project import ProjectManager, ProjectLogObserver
-from pitivi.undo.medialibrary import MediaLibraryLogObserver
from pitivi.undo.undo import UndoableActionLog, DebugActionLogObserver
from pitivi.dialogs.startupwizard import StartUpWizard
@@ -146,7 +145,6 @@ class Pitivi(Loggable, Signallable):
# TODO reimplement the observing after GES port
#self.timelineLogObserver = TimelineLogObserver(self.action_log)
self.projectLogObserver = ProjectLogObserver(self.action_log)
- self.medialibrary_log_observer = MediaLibraryLogObserver(self.action_log)
self.version_information = {}
self._checkVersion()
@@ -195,12 +193,12 @@ class Pitivi(Loggable, Signallable):
def _newProjectLoaded(self, project):
pass
- def _projectManagerNewProjectLoaded(self, projectManager, project):
+ def _projectManagerNewProjectLoaded(self, projectManager, project,
+ unused_fully_loaded):
self.current = project
self.action_log.clean()
#self.timelineLogObserver.startObserving(project.timeline)
self.projectLogObserver.startObserving(project)
- self.medialibrary_log_observer.startObserving(project.medialibrary)
self._newProjectLoaded(project)
self.emit("new-project-loaded", project)
@@ -326,7 +324,6 @@ class GuiPitivi(InteractivePitivi):
return False
-#FIXME the GES port screwed the import to the timeline
class ProjectCreatorGuiPitivi(GuiPitivi):
"""
Creates an instance of PiTiVi with the UI and loading a list
diff --git a/pitivi/check.py b/pitivi/check.py
index 94db31a..1690084 100644
--- a/pitivi/check.py
+++ b/pitivi/check.py
@@ -41,7 +41,7 @@ HARD_DEPS = {
"GES": "1.0.0",
"gnonlin": "0.11.89.1",
"GooCanvas": "2.0",
- "Gst": "1.0.2",
+ "Gst": "1.1.0",
"Gtk": "3.4.0",
"xdg": None, # "pyxdg", using static python bindings
diff --git a/pitivi/clipproperties.py b/pitivi/clipproperties.py
index b802e96..a383b47 100644
--- a/pitivi/clipproperties.py
+++ b/pitivi/clipproperties.py
@@ -401,7 +401,7 @@ class EffectProperties(Gtk.Expander):
obj = self.timeline_objects[0]
for track_effect in obj.get_top_effects():
if not track_effect.props.bin_description in HIDDEN_EFFECTS:
- material = self.app.effects.getFactoryFromName(
+ asset = self.app.effects.getFactoryFromName(
track_effect.props.bin_description)
to_append = [track_effect.props.active]
track = track_effect.get_track()
@@ -411,7 +411,7 @@ class EffectProperties(Gtk.Expander):
to_append.append("Video")
to_append.append(track_effect.props.bin_description)
- to_append.append(material.description)
+ to_append.append(asset.description)
to_append.append(track_effect)
self.storemodel.append(to_append)
diff --git a/pitivi/dialogs/clipmediaprops.py b/pitivi/dialogs/clipmediaprops.py
index 5846e49..86253bb 100644
--- a/pitivi/dialogs/clipmediaprops.py
+++ b/pitivi/dialogs/clipmediaprops.py
@@ -38,7 +38,6 @@ class clipmediapropsDialog():
def __init__(self, project, audio_streams, video_streams):
self.project = project
- self.settings = self.project.getSettings()
self.audio_streams = audio_streams
self.video_streams = video_streams
self.has_audio = self.has_video = self.is_image = False
@@ -110,36 +109,27 @@ class clipmediapropsDialog():
self.checkbutton6 = builder.get_object("checkbutton6")
def _applyButtonCb(self, unused_button):
- _width = _height = _framerate = _par = -1
- _channels = _rate = _depth = -1
-
+ project = self.projet
if self.has_video:
# This also handles the case where the video is a still image
video = self.video_streams[0]
if self.checkbutton1.get_active():
- _width = video.get_width()
- _height = video.get_height()
+ project.width = video.get_width()
+ project.height = video.get_height()
if (self.checkbutton2.get_active() and not self.is_image):
- _framerate = Gst.Fraction(video.get_framerate_num(),
+ project.framerate = Gst.Fraction(video.get_framerate_num(),
video.get_framerate_denom())
if (self.checkbutton3.get_active() and not self.is_image):
- _par = Gst.Fraction(video.get_par_num(),
+ project.par = Gst.Fraction(video.get_par_num(),
video.get_par_denom())
- self.settings.setVideoProperties(_width, _height, _framerate, _par)
-
if self.has_audio:
audio = self.audio_streams[0]
if self.checkbutton4.get_active():
- _channels = audio.get_channels()
+ project.channels = audio.get_channels()
if self.checkbutton5.get_active():
- _rate = audio.get_sample_rate()
+ project.rate = audio.get_sample_rate()
if self.checkbutton6.get_active():
- _depth = audio.get_depth()
- self.settings.setAudioProperties(_channels, _rate, _depth)
-
- if self.has_video or self.has_audio:
- self.project.setSettings(self.settings)
-
+ project.depth = audio.get_depth()
self.dialog.destroy()
def _cancelButtonCb(self, unused_button):
diff --git a/pitivi/dialogs/startupwizard.py b/pitivi/dialogs/startupwizard.py
index a5dcfaf..a0c2bd4 100644
--- a/pitivi/dialogs/startupwizard.py
+++ b/pitivi/dialogs/startupwizard.py
@@ -23,6 +23,7 @@ import os
from gi.repository import Gtk
from gi.repository import Gdk
+from gi.repository import GES
from gettext import gettext as _
@@ -56,7 +57,10 @@ class StartUpWizard(object):
# simple way to hide it.
filter = Gtk.RecentFilter()
filter.set_name(_("Projects"))
- filter.add_pattern("*.xptv")
+
+ for asset in GES.list_assets(GES.Formatter):
+ filter.add_pattern('*.' + asset.get_meta(GES.META_FORMATTER_EXTENSION))
+
self.recent_chooser.add_filter(filter)
if not missing_soft_deps:
@@ -125,17 +129,16 @@ class StartUpWizard(object):
"""Handle the failure of a project open operation."""
self.show()
- def _projectLoadedCb(self, unused_project_manager, project):
+ def _projectLoadedCb(self, unused_project_manager, project, fully_loaded):
"""Handle the success of a project load operation.
All the create or load project usage scenarios must generate
a new-project-loaded signal from self.app.projectManager!
"""
- if project.disconnect:
- return
- self.app.projectManager.disconnect_by_function(self._projectFailedCb)
- self.app.projectManager.disconnect_by_function(self._projectLoadedCb)
- self.app.projectManager.disconnect_by_function(self._projectLoadingCb)
+ if fully_loaded:
+ self.app.projectManager.disconnect_by_function(self._projectFailedCb)
+ self.app.projectManager.disconnect_by_function(self._projectLoadedCb)
+ self.app.projectManager.disconnect_by_function(self._projectLoadingCb)
def _projectLoadingCb(self, unused_project_manager, unused_project):
"""Handle the start of a project load operation."""
diff --git a/pitivi/mainwindow.py b/pitivi/mainwindow.py
index 727f9b2..cb1a6b8 100644
--- a/pitivi/mainwindow.py
+++ b/pitivi/mainwindow.py
@@ -43,7 +43,7 @@ from pitivi.clipproperties import ClipProperties
from pitivi.configure import pitivi_version, APPNAME, APPURL, get_pixmap_dir, get_ui_dir
from pitivi.effects import EffectListWidget
from pitivi.mediafilespreviewer import PreviewWidget
-from pitivi.medialibrary import MediaLibraryWidget, MediaLibraryError
+from pitivi.medialibrary import MediaLibraryWidget
from pitivi.settings import GlobalSettings
from pitivi.tabsmanager import BaseTabs
from pitivi.timeline.timeline import Timeline
@@ -121,9 +121,6 @@ GlobalSettings.addConfigOption('lastCurrentVersion',
default='')
-#FIXME Hacky, reimplement when avalaible in GES
-formats = [(None, _("PiTiVi native (XML)"), ('xptv',))]
-
# FIXME PyGi to get stock_add working
Gtk.stock_add = lambda items: None
@@ -608,10 +605,13 @@ class PitiviMainWindow(Gtk.Window, Loggable):
def _mediaLibraryPlayCb(self, medialibrary, uri):
self._viewUri(uri)
- def _mediaLibrarySourceRemovedCb(self, medialibrary, uri, unused_info):
+ def _projectChangedCb(self, project):
+ self.main_actions.get_action("SaveProject").set_sensitive(True)
+
+ def _mediaLibrarySourceRemovedCb(self, project, asset):
"""When a clip is removed from the Media Library, tell the timeline
to remove all instances of that clip."""
- self.timeline_ui.purgeObject(uri)
+ self.timeline_ui.purgeObject(asset.get_id())
def _selectedLayerChangedCb(self, widget, layer):
self.main_actions.get_action("RemoveLayer").set_sensitive(layer is not None)
@@ -737,15 +737,16 @@ class PitiviMainWindow(Gtk.Window, Loggable):
chooser.set_select_multiple(False)
# TODO: Remove this set_current_folder call when GTK bug 683999 is fixed
chooser.set_current_folder(self.settings.lastProjectFolder)
- for format in formats:
+ formatter_assets = GES.list_assets(GES.Formatter)
+ formatter_assets.sort(key=lambda x: - x.get_meta(GES.META_FORMATTER_RANK))
+ for format in formatter_assets:
filt = Gtk.FileFilter()
- filt.set_name(format[1])
- for ext in format[2]:
- filt.add_pattern("*%s" % ext)
+ filt.set_name(format.get_meta(GES.META_DESCRIPTION))
+ filt.add_pattern("*%s" % format.get_meta(GES.META_FORMATTER_EXTENSION))
chooser.add_filter(filt)
default = Gtk.FileFilter()
default.set_name(_("All supported formats"))
- default.add_custom(Gtk.FileFilterFlags.URI, GES.Formatter.can_load_uri, None)
+ default.add_custom(Gtk.FileFilterFlags.URI, self._canLoadUri, None)
chooser.add_filter(default)
response = chooser.run()
@@ -754,6 +755,9 @@ class PitiviMainWindow(Gtk.Window, Loggable):
chooser.destroy()
return True
+ def _canLoadUri(self, filterinfo, uri):
+ GES.Formatter.can_load_uri(filterinfo.uri)
+
def _undoCb(self, action):
self.app.action_log.undo()
@@ -767,27 +771,16 @@ class PitiviMainWindow(Gtk.Window, Loggable):
self.prefsdialog.dialog.set_transient_for(self)
self.prefsdialog.run()
- def _projectManagerNewProjectLoadedCb(self, projectManager, project):
+ def _projectManagerNewProjectLoadedCb(self, projectManager, project, unused_fully_loaded):
"""
Once a new project has been loaded, wait for media library's
"ready" signal to populate the timeline.
"""
self.log("A new project is loaded, wait for clips")
- self._connectToProjectSources(self.app.current.medialibrary)
+ self._connectToProject(self.app.current)
self.app.current.timeline.connect("notify::duration",
self._timelineDurationChangedCb)
self.app.current.pipeline.activatePositionListener()
-
- # This should only be done when loading a project, and disconnected
- # as soon as we receive the signal.
- self.app.current.medialibrary.connect("ready", self._projectClipsReady)
-
- def _projectClipsReady(self, medialibrary):
- """
- After the project is loaded along with its media files, update the UI.
- """
- self.log("Project clips are ready, update the UI")
- self.app.current.medialibrary.disconnect_by_func(self._projectClipsReady)
self._setProject()
#FIXME GES we should re-enable this when possible
@@ -795,7 +788,6 @@ class PitiviMainWindow(Gtk.Window, Loggable):
# Enable export functionality
self.main_actions.get_action("ExportProject").set_sensitive(True)
-
if self._missingUriOnLoading:
self.app.current.setModificationState(True)
self.main_actions.get_action("SaveProject").set_sensitive(True)
@@ -809,7 +801,7 @@ class PitiviMainWindow(Gtk.Window, Loggable):
self.recent_manager.add_item(uri)
self.log("A NEW project is loading, deactivate UI")
- def _projectManagerSaveProjectFailedCb(self, projectManager, uri, exception=None):
+ def _projectManagerSaveProjectFailedCb(self, projectManager, uri, unused, exception=None):
project_filename = unquote(uri.split("/")[-1])
dialog = Gtk.MessageDialog(self,
Gtk.DialogFlags.MODAL,
@@ -826,10 +818,13 @@ class PitiviMainWindow(Gtk.Window, Loggable):
self.error("failed to save project")
def _projectManagerProjectSavedCb(self, projectManager, project, uri):
- self.app.action_log.checkpoint()
- self._syncDoUndo(self.app.action_log)
+ # FIXME GES: Reimplement Undo/Redo
+ #self.app.action_log.checkpoint()
+ #self._syncDoUndo(self.app.action_log)
+ self.main_actions.get_action("SaveProject").set_sensitive(False)
if uri:
self.recent_manager.add_item(uri)
+
if project.uri is None:
project.uri = uri
@@ -954,7 +949,10 @@ class PitiviMainWindow(Gtk.Window, Loggable):
dialog.run()
dialog.destroy()
- def _projectManagerMissingUriCb(self, instance, formatter, tfs):
+ def _projectManagerMissingUriCb(self, u_project_manager, u_project,
+ error, asset):
+ uri = asset.get_id()
+ new_uri = None
dialog = Gtk.Dialog(_("Locate missing file..."),
self,
Gtk.DialogFlags.MODAL,
@@ -973,11 +971,11 @@ class PitiviMainWindow(Gtk.Window, Loggable):
# This can happen if the file was moved or deleted by an application
# that does not manage Freedesktop thumbnails. The user is in luck!
# This is based on medialibrary's addDiscovererInfo method.
- thumbnail_hash = md5(tfs.get_uri()).hexdigest()
+ thumbnail_hash = md5(uri).hexdigest()
thumb_dir = os.path.expanduser("~/.thumbnails/normal/")
thumb_path_normal = thumb_dir + thumbnail_hash + ".png"
if os.path.exists(thumb_path_normal):
- self.debug("A thumbnail file was found for %s" % tfs.get_uri())
+ self.debug("A thumbnail file was found for %s" % uri)
thumbnail = Gtk.Image.new_from_file(thumb_path_normal)
thumbnail.set_padding(0, SPACING)
hbox.pack_start(thumbnail, False, False, 0)
@@ -998,7 +996,7 @@ class PitiviMainWindow(Gtk.Window, Loggable):
text = _('The following file has moved: "<b>%s</b>"'
'\nPlease specify its new location:'
- % info_name(tfs))
+ % info_name(asset))
label = Gtk.Label()
label.set_markup(text)
@@ -1016,7 +1014,7 @@ class PitiviMainWindow(Gtk.Window, Loggable):
# Use a Gtk FileFilter to only show files with the same extension
# Note that splitext gives us the extension with the ".", no need to
# add it inside the filter string.
- filename, extension = os.path.splitext(tfs.get_uri())
+ filename, extension = os.path.splitext(uri)
filter = Gtk.FileFilter()
# Translators: this is a format filter in a filechooser. Ex: "AVI files"
filter.set_name(_("%s files" % extension))
@@ -1038,10 +1036,6 @@ class PitiviMainWindow(Gtk.Window, Loggable):
if response == Gtk.ResponseType.OK:
self.log("User chose a new URI for the missing file")
new_uri = chooser.get_uri()
- if new_uri:
- self.app.current.medialibrary.addUris([new_uri])
- formatter.update_source_uri(tfs, new_uri)
- self._missingUriOnLoading = True
else:
# Even if the user clicks Cancel, the discoverer keeps trying to
# import the rest of the clips...
@@ -1052,7 +1046,7 @@ class PitiviMainWindow(Gtk.Window, Loggable):
attempted_uri = self.app.current.uri
reason = _('No replacement file was provided for "<i>%s</i>".\n\n'
'PiTiVi does not currently support partial projects.'
- % info_name(tfs))
+ % info_name(asset))
# Put an end to the async signals spamming us with dialogs:
self.app.projectManager.disconnect_by_func(self._projectManagerMissingUriCb)
# Don't overlap the file chooser with our error dialog
@@ -1067,11 +1061,13 @@ class PitiviMainWindow(Gtk.Window, Loggable):
self.app.projectManager.emit("new-project-failed", attempted_uri, reason)
dialog.destroy()
+ return new_uri
- def _connectToProjectSources(self, medialibrary):
+ def _connectToProject(self, project):
#FIXME GES we should re-enable this when possible
#medialibrary.connect("missing-plugins", self._sourceListMissingPluginsCb)
- medialibrary.connect("source-removed", self._mediaLibrarySourceRemovedCb)
+ project.connect("asset-removed", self._mediaLibrarySourceRemovedCb)
+ project.connect("project-changed", self._projectChangedCb)
def _actionLogCommit(self, action_log, stack, nested):
if nested:
@@ -1116,31 +1112,39 @@ class PitiviMainWindow(Gtk.Window, Loggable):
self.warning("Current project instance does not exist")
return False
try:
- self.app.current.disconnect_by_func(self._settingsChangedCb)
+ self.app.current.disconnect_by_func(self._renderingSettingsChangedCb)
except:
# When loading the first project, the signal has never been
# connected before.
pass
self.viewer.setPipeline(self.app.current.pipeline)
- self._settingsChangedCb(self.app.current, None, self.app.current.settings)
+ self._renderingSettingsChangedCb(self.app.current, None, None)
if self.timeline_ui:
- self.timeline_ui.setProject(self.app.current)
+ #self.timeline_ui.setProject(self.app.current)
self.clipconfig.project = self.app.current
#FIXME GES port undo/redo
#self.app.timelineLogObserver.pipeline = self.app.current.pipeline
- self.app.current.connect("settings-changed", self._settingsChangedCb)
+
+ self.app.current.connect("rendering-settings-changed", self._renderingSettingsChangedCb)
# When creating a blank project, medialibrary will eventually trigger
# this _setProject method, but there's no project URI yet.
if self.app.current.uri:
folder_path = os.path.dirname(path_from_uri(self.app.current.uri))
self.settings.lastProjectFolder = folder_path
- def _settingsChangedCb(self, project, unused_old, new):
- # TODO: this method's signature should be changed:
- # project = self.app.current,
- # old is never used, and the new is equal to self.app.current.settings
- self.viewer.setDisplayAspectRatio(
- float(new.videopar.num / new.videopar.denom * new.videowidth) / float(new.videoheight))
+ def _renderingSettingsChangedCb(self, project, item, value):
+ """
+ Called when any Project metadata changes, we filter out the ones
+ we are interested in.
+
+ if @item is None, it mean we called it ourself, and want the
+ aspect-ratio to be set
+ """
+ self.main_actions.get_action("SaveProject").set_sensitive(False)
+ if item in ["videopar", "videowidth", "videoheight"] or item is None:
+ ratio = float(project.videopar.num / project.videopar.denom *
+ project.videowidth) / float(project.videoheight)
+ self.viewer.setDisplayAspectRatio(ratio)
def _sourceListMissingPluginsCb(self, project, uri, factory,
details, descriptions, missingPluginsCallback):
@@ -1199,23 +1203,26 @@ class PitiviMainWindow(Gtk.Window, Loggable):
def _showSaveAsDialog(self, project):
self.log("Save URI requested")
+
chooser = Gtk.FileChooserDialog(_("Save As..."),
self,
action=Gtk.FileChooserAction.SAVE,
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
+ asset = GES.Formatter.get_default()
+ filt = Gtk.FileFilter()
+ filt.set_name(asset.get_meta(GES.META_DESCRIPTION))
+ filt.add_pattern("*.%s" % asset.get_meta(GES.META_FORMATTER_EXTENSION))
+ chooser.add_filter(filt)
+
chooser.set_icon_name("pitivi")
chooser.set_select_multiple(False)
- chooser.set_current_name(_("Untitled") + ".xptv")
+ chooser.set_current_name(_("Untitled") + "." +
+ asset.get_meta(GES.META_FORMATTER_EXTENSION))
chooser.set_current_folder(self.settings.lastProjectFolder)
chooser.props.do_overwrite_confirmation = True
- for format in formats:
- filt = Gtk.FileFilter()
- filt.set_name(format[1])
- for ext in format[2]:
- filt.add_pattern("*.%s" % ext)
- chooser.add_filter(filt)
+
default = Gtk.FileFilter()
default.set_name(_("Detect automatically"))
default.add_pattern("*")
@@ -1227,8 +1234,8 @@ class PitiviMainWindow(Gtk.Window, Loggable):
# need to do this to work around bug in Gst.uri_construct
# which escapes all /'s in path!
uri = "file://" + chooser.get_filename()
- self.log("uri:%s , format:%s", uri, format)
format = chooser.get_filter().get_name()
+ self.log("uri:%s , format:%s", uri, format)
if format == _("Detect automatically"):
format = None
self.settings.lastProjectFolder = chooser.get_current_folder()
@@ -1257,7 +1264,7 @@ class PitiviMainWindow(Gtk.Window, Loggable):
preview_window.hide() # Hack to allow setting the window position
previewer.previewUri(uri)
previewer.setMinimal()
- info = self.app.current.medialibrary.getInfoFromUri(uri)
+ info = self.app.current.get_asset(uri, GES.TimelineFileSource).get_info()
try:
# For videos and images, automatically resize the window
# Try to keep it 1:1 if it can fit within 85% of the parent window
diff --git a/pitivi/mediafilespreviewer.py b/pitivi/mediafilespreviewer.py
index 0c00c55..202a959 100644
--- a/pitivi/mediafilespreviewer.py
+++ b/pitivi/mediafilespreviewer.py
@@ -170,6 +170,7 @@ class PreviewWidget(Gtk.VBox, Loggable):
self.show_error(uri)
else:
self.log("Call discoverer for " + uri)
+ self.fixme("Use a GESAsset here, and discover async with it")
try:
info = self.discoverer.discover_uri(uri)
except Exception, e:
@@ -322,7 +323,7 @@ class PreviewWidget(Gtk.VBox, Loggable):
self.player.set_state(Gst.State.NULL)
self.is_playing = False
err, dbg = message.parse_error()
- self.error("Error: %s " % err, dbg)
+ self.error("Error: %s %s" % (err, dbg))
def _update_position(self, *args):
if self.is_playing:
diff --git a/pitivi/medialibrary.py b/pitivi/medialibrary.py
index fd3033a..d12f394 100644
--- a/pitivi/medialibrary.py
+++ b/pitivi/medialibrary.py
@@ -36,7 +36,7 @@ import time
from urllib import unquote
from gettext import gettext as _
from hashlib import md5
-from gi.repository.GstPbutils import Discoverer, DiscovererVideoInfo
+from gi.repository.GstPbutils import DiscovererVideoInfo
from pitivi.configure import get_ui_dir, get_pixmap_dir
from pitivi.settings import GlobalSettings
@@ -44,11 +44,11 @@ from pitivi.mediafilespreviewer import PreviewWidget
from pitivi.dialogs.filelisterrordialog import FileListErrorDialog
from pitivi.dialogs.clipmediaprops import clipmediapropsDialog
from pitivi.utils.ui import beautify_length
-from pitivi.utils.misc import PathWalker, quote_uri
-from pitivi.utils.signal import SignalGroup, Signallable
+from pitivi.utils.misc import PathWalker
+from pitivi.utils.signal import SignalGroup
from pitivi.utils.loggable import Loggable
import pitivi.utils.ui as dnd
-from pitivi.utils.ui import beautify_info, info_name, SPACING, PADDING
+from pitivi.utils.ui import beautify_info, info_name, SPACING
from pitivi.utils.ui import TYPE_PITIVI_FILESOURCE
@@ -81,7 +81,7 @@ STORE_MODEL_STRUCTURE = (
(COL_ICON,
COL_ICON_LARGE,
COL_INFOTEXT,
- COL_FACTORY,
+ COL_ASSET,
COL_URI,
COL_LENGTH,
COL_SEARCH_TEXT,
@@ -109,146 +109,6 @@ SUPPORTED_FILE_FORMATS = {"video": ("3gpp", "3gpp2", "dv", "mp4", "mpeg", "ogg",
OTHER_KNOWN_FORMATS = ("video/mp2t")
-class MediaLibraryError(Exception):
- pass
-
-
-class MediaLibrary(Signallable, Loggable):
- """
- Contains the sources for a project, stored as SourceFactory objects.
-
- @ivar discoverer: The discoverer object used internally
- @type discoverer: L{Discoverer}
- @ivar nb_files_to_import: Total number of URIs on the last addUris call.
- @type nb_files_to_import: int
- @ivar nb_imported_files: Number of URIs loaded since the last addUris call.
- @type nb_imported_files: int
-
- Signals:
- - C{source-added} : A source has been discovered and added to the MediaLibrary.
- - C{source-removed} : A source was removed from the MediaLibrary.
- - C{discovery-error} : The given uri is not a media file.
- - C{nothing-to-import} : All the given uri were already imported
- - C{ready} : No more files are being discovered/added.
- - C{starting} : Some files are being discovered/added.
- """
-
- __signals__ = {
- "source-added": ["info"],
- "source-removed": ["uri"],
- "discovery-error": ["uri", "reason"],
- "nothing-to-import": [],
- "ready": [],
- "starting": [],
- }
-
- def __init__(self):
- Loggable.__init__(self)
- Signallable.__init__(self)
- # A (URI -> SourceFactory) map.
- self._sources = {}
- # A list of SourceFactory objects.
- self._ordered_sources = []
- self._resetImportCounters()
-
- self.discoverer = Discoverer.new(Gst.SECOND)
- self.discoverer.connect("discovered", self.addDiscovererInfo)
- self.discoverer.connect("finished", self.finishDiscovererCb)
- self.discoverer.start()
-
- def _resetImportCounters(self):
- self.nb_files_to_import = 0
- self.nb_imported_files = 0
-
- def finishDiscovererCb(self, unused_discoverer):
- self.debug("Got the discoverer's finished signal")
- self._resetImportCounters()
- self.emit("ready")
-
- def addUris(self, uris):
- """
- Add c{uris} to the source list.
-
- The uris will be analyzed before being added.
- """
- self.emit("starting")
- self.debug("Adding %s", uris)
- if type(uris) is str:
- # A single URI string was provided... make it a list, otherwise
- # we would loop over the characters of the string!
- uris = [uris]
- self.nb_files_to_import += len(uris)
- for uri in uris:
- # Ensure we have a correctly encoded URI according to RFC 2396.
- # Otherwise, in some cases we'd get rogue characters that break
- # searching for duplicates
- uri = quote_uri(uri)
- if uri not in self._sources:
- self.discoverer.discover_uri_async(uri)
- self.debug("Added a uri to discoverer async")
- else:
- self.nb_files_to_import -= 1
- self.debug('"%s" is already in the media library' % uri)
- if self.nb_files_to_import == 0:
- # This is a cornercase hack for when you try to import a bunch of
- # clips that are all present in the media library already.
- # This will allow the progressbar to hide.
- self.emit("nothing-to-import")
- else:
- self.debug("Done adding all URIs to discoverer async")
-
- def removeUri(self, uri):
- """
- Remove the info for c{uri} from the source list.
- """
- # In theory we don't need quote_uri here, but since removeUri is public,
- # we can never be too sure.
- uri = quote_uri(uri)
- try:
- info = self._sources.pop(uri)
- except KeyError:
- raise MediaLibraryError("URI not in the medialibrary", uri)
- try:
- self._ordered_sources.remove(info)
- except ValueError:
- # this can only happen if discoverer hasn't finished scanning the
- # source, so info must be None
- assert info is None
-
- self.debug("Removing %s", uri)
- self.emit("source-removed", uri, info)
-
- def getInfoFromUri(self, uri):
- """
- Get the source corresponding to C{uri}.
- """
- # Make sure the URI is properly quoted, as other modules calling this
- # method do not necessarily provide URIs encoded in the same way.
- uri = quote_uri(uri)
- info = self._sources.get(uri)
- if info is None:
- raise MediaLibraryError("URI not in the medialibrary", uri)
- return info
-
- def addDiscovererInfo(self, discoverer, info, error):
- """
- Add the specified SourceFactory to the list of sources.
- """
- if error:
- self.emit("discovery-error", info.get_uri(), error.message)
- else:
- uri = info.get_uri()
- if self._sources.get(uri, None) is not None:
- raise MediaLibraryError("We already have info for this URI", uri)
- self._sources[uri] = info
- self._ordered_sources.append(info)
- self.nb_imported_files += 1
- self.emit("source-added", info)
-
- def getSources(self):
- return self._ordered_sources
-
-
def compare_simple(model, iter1, iter2, user_data):
if model[iter1][user_data] < model[iter2][user_data]:
return -1
@@ -463,12 +323,7 @@ class MediaLibraryWidget(Gtk.VBox, Loggable):
self._removeSources()
def _insertEndCb(self, unused_action):
- sources = []
- for uri in self.getSelectedItems():
- sources.append(GES.TimelineFileSource(uri=uri))
-
- self.app.gui.timeline_ui.insertEnd(sources)
- self._sources_to_insert = self.getSelectedItems()
+ self.app.gui.timeline_ui.insertEnd(self.getSelectedAssets())
def _disableKeyboardShortcutsCb(self, *unused_args):
"""
@@ -544,18 +399,16 @@ class MediaLibraryWidget(Gtk.VBox, Loggable):
This first disconnects any handlers connected to an old project.
If project is None, this just disconnects any connected handlers.
"""
- self.project_signals.connect(project.medialibrary,
- "source-added", None, self._sourceAddedCb)
- self.project_signals.connect(project.medialibrary,
- "source-removed", None, self._sourceRemovedCb)
- self.project_signals.connect(project.medialibrary,
- "discovery-error", None, self._discoveryErrorCb)
- self.project_signals.connect(project.medialibrary,
- "nothing-to-import", None, self._hideProgressBarCb)
- self.project_signals.connect(project.medialibrary,
- "ready", None, self._sourcesStoppedImportingCb)
- self.project_signals.connect(project.medialibrary,
- "starting", None, self._sourcesStartedImportingCb)
+ self.project_signals.connect(project, "asset-added", None,
+ self._assetAddedCb)
+ self.project_signals.connect(project, "asset-removed", None,
+ self._assetRemovedCb)
+ self.project_signals.connect(project, "error-loading-asset",
+ None, self._errorCreatingAssetCb)
+ self.project_signals.connect(project, "done-importing", None,
+ self._sourcesStoppedImportingCb)
+ self.project_signals.connect(project, "start-importing", None,
+ self._sourcesStartedImportingCb)
def _setClipView(self, view_type):
"""
@@ -632,12 +485,14 @@ class MediaLibraryWidget(Gtk.VBox, Loggable):
self._importDialog.add_filter(default)
self._importDialog.show()
- def _updateProgressbar(self):
+ def _updateProgressbar(self, current_clip_iter=None, total_clips=None):
"""
Update the _progressbar with the ratio of clips imported vs the total
"""
- current_clip_iter = self.app.current.medialibrary.nb_imported_files
- total_clips = self.app.current.medialibrary.nb_files_to_import
+ if current_clip_iter is None and total_clips is None:
+ current_clip_iter = self.app.current.nb_imported_files
+ total_clips = self.app.current.nb_files_to_import
+
progressbar_text = _("Importing clip %(current_clip)d of %(total)d" %
{"current_clip": current_clip_iter,
"total": total_clips})
@@ -647,7 +502,9 @@ class MediaLibraryWidget(Gtk.VBox, Loggable):
elif total_clips != 0:
self._progressbar.set_fraction((current_clip_iter - 1) / float(total_clips))
- def _addDiscovererInfo(self, info):
+ def _addAsset(self, asset):
+ info = asset.get_info()
+
# The code below tries to read existing thumbnails from the freedesktop
# thumbnails directory (~/.thumbnails). The filenames are simply
# the file URI hashed with md5, so we can retrieve them easily.
@@ -690,7 +547,7 @@ class MediaLibraryWidget(Gtk.VBox, Loggable):
self.pending_rows.append((thumbnail,
thumbnail_large,
beautify_info(info),
- info,
+ asset,
info.get_uri(),
duration,
name,
@@ -705,15 +562,18 @@ class MediaLibraryWidget(Gtk.VBox, Loggable):
# medialibrary callbacks
- def _sourceAddedCb(self, unused_medialibrary, factory):
+ def _assetAddedCb(self, unused_project, asset,
+ current_clip_iter=None, total_clips=None):
""" a file was added to the medialibrary """
- self._updateProgressbar()
- self._addDiscovererInfo(factory)
+ if isinstance(asset, GES.AssetFileSource):
+ self._updateProgressbar()
+ self._addAsset(asset)
- def _sourceRemovedCb(self, unused_medialibrary, uri, unused_info):
+ def _assetRemovedCb(self, unsued_project, asset):
""" the given uri was removed from the medialibrary """
# find the good line in the storemodel and remove it
model = self.storemodel
+ uri = asset.get_id()
for row in model:
if uri == row[COL_URI]:
model.remove(row.iter)
@@ -722,17 +582,19 @@ class MediaLibraryWidget(Gtk.VBox, Loggable):
self._welcome_infobar.show_all()
self.debug("Removing %s", uri)
- def _discoveryErrorCb(self, unused_medialibrary, uri, reason, extra=None):
+ def _errorCreatingAssetCb(self, unsued_project, error, id, type):
""" The given uri isn't a media file """
- error = (uri, reason, extra)
- self._errors.append(error)
+ if GObject.type_is_a(type, GES.TimelineFileSource):
+ error = (id, str(error.domain), error)
+ self._errors.append(error)
+ self._updateProgressbar()
- def _sourcesStartedImportingCb(self, unused_medialibrary):
+ def _sourcesStartedImportingCb(self, unsued_project):
self.import_start_time = time.time()
self._welcome_infobar.hide()
self._progressbar.show()
- def _sourcesStoppedImportingCb(self, unused_medialibrary):
+ def _sourcesStoppedImportingCb(self, unsued_project):
self.debug("Importing took %.3f seconds" % (time.time() - self.import_start_time))
self.flush_pending_rows()
self._progressbar.hide()
@@ -746,14 +608,6 @@ class MediaLibraryWidget(Gtk.VBox, Loggable):
self._import_warning_infobar.show_all()
- def _hideProgressBarCb(self, unused_medialibrary):
- """
- This is only called when all the uris we tried to import were already
- present in the media library. We then need to hide the progressbar
- because the media library is not going to emit the "ready" signal.
- """
- self._progressbar.hide()
-
## Error Dialog Box callbacks
def _errorDialogBoxCloseCb(self, unused_dialog):
@@ -774,7 +628,7 @@ class MediaLibraryWidget(Gtk.VBox, Loggable):
self.app.settings.closeImportDialog = \
dialogbox.props.extra_widget.get_active()
filenames = dialogbox.get_uris()
- self.app.current.medialibrary.addUris(filenames)
+ self.app.current.addUris(filenames)
if self.app.settings.closeImportDialog:
dialogbox.destroy()
self._importDialog = None
@@ -803,17 +657,16 @@ class MediaLibraryWidget(Gtk.VBox, Loggable):
self.app.action_log.begin("remove clip from source list")
for row in rows:
- uri = model[row.get_path()][COL_URI]
- self.app.current.medialibrary.removeUri(uri)
+ asset = model[row.get_path()][COL_ASSET]
+ self.app.current.remove_asset(asset)
self.app.action_log.commit()
- def _sourceIsUsed(self, uri):
+ def _sourceIsUsed(self, asset):
"""Check if a given URI is present in the timeline"""
layers = self.app.current.timeline.get_layers()
for layer in layers:
for tlobj in layer.get_objects():
- tlobj_uri = quote_uri(tlobj.get_uri())
- if tlobj_uri == uri:
+ if tlobj.get_asset() == asset:
return True
return False
@@ -821,14 +674,14 @@ class MediaLibraryWidget(Gtk.VBox, Loggable):
"""
Select, in the media library, unused sources in the project.
"""
- sources = self.app.current.medialibrary.getSources()
+ assets = self.app.current.list_assets(GES.TimelineFileSource)
unused_sources_uris = []
model = self.treeview.get_model()
selection = self.treeview.get_selection()
- for source in sources:
- if not self._sourceIsUsed(source.get_uri()):
- unused_sources_uris.append(source.get_uri())
+ for asset in assets:
+ if not self._sourceIsUsed(asset):
+ unused_sources_uris.append(asset.get_uri())
# Hack around the fact that making selections (in a treeview/iconview)
# deselects what was previously selected
@@ -857,10 +710,10 @@ class MediaLibraryWidget(Gtk.VBox, Loggable):
"""
paths = self.getSelectedPaths()[0] # Only use the first item
model = self.treeview.get_model()
- factory = model[paths][COL_FACTORY]
+ info = model[paths][COL_ASSET].get_info()
d = clipmediapropsDialog(self.app.current,
- factory.get_audio_streams(),
- factory.get_video_streams())
+ info.get_audio_streams(),
+ info.get_video_streams())
d.run()
def _warningInfoBarDismissedCb(self, unused_button):
@@ -1058,6 +911,9 @@ class MediaLibraryWidget(Gtk.VBox, Loggable):
self.storemodel.clear()
self._connectToProject(project)
+ # Make sure that the sources added to the project are added added
+ self.flush_pending_rows()
+
def _newProjectFailedCb(self, unused_pitivi, unused_reason, unused_uri):
self.storemodel.clear()
self.project_signals.disconnectAll()
@@ -1089,12 +945,12 @@ class MediaLibraryWidget(Gtk.VBox, Loggable):
if len(directories):
# Recursively import from folders that were dragged into the library
self.app.threads.addThread(PathWalker, directories,
- self.app.current.medialibrary.addUris)
+ self.app.current.addUris)
if len(remote_files):
#TODO waiting for remote files downloader support to be implemented
pass
if len(filenames):
- self.app.current.medialibrary.addUris(filenames)
+ self.app.current.addUris(filenames)
#used with TreeView and IconView
def _dndDragBeginCb(self, view, context):
@@ -1134,3 +990,11 @@ class MediaLibraryWidget(Gtk.VBox, Loggable):
for path in self._draggedPaths]
return [self.modelFilter[path][COL_URI]
for path in self.getSelectedPaths()]
+
+ def getSelectedAssets(self):
+ """ Returns a list of selected items URIs """
+ if self._draggedPaths:
+ return [self.modelFilter[path][COL_ASSET]
+ for path in self._draggedPaths]
+ return [self.modelFilter[path][COL_ASSET]
+ for path in self.getSelectedPaths()]
diff --git a/pitivi/preset.py b/pitivi/preset.py
index c0006d4..0cf57a5 100644
--- a/pitivi/preset.py
+++ b/pitivi/preset.py
@@ -27,7 +27,7 @@ import json
from gettext import gettext as _
-from pitivi.render import available_muxers, available_video_encoders, available_audio_encoders
+from pitivi.render import CachedEncoderList
from pitivi.settings import xdg_data_home
from pitivi.utils.misc import isWritable
from pitivi.configure import get_renderpresets_dir, get_audiopresets_dir, get_videopresets_dir
@@ -395,9 +395,10 @@ class RenderPresetManager(PresetManager):
acodec = parser["acodec"]
vcodec = parser["vcodec"]
- if (acodec not in [fact.get_name() for fact in available_audio_encoders()]
- or vcodec not in [fact.get_name() for fact in available_video_encoders()]
- or container not in [fact.get_name() for fact in available_muxers()]):
+ cached_encs = CachedEncoderList()
+ if (acodec not in [fact.get_name() for fact in cached_encs.aencoders]
+ or vcodec not in [fact.get_name() for fact in cached_encs.vencoders]
+ or container not in [fact.get_name() for fact in cached_encs.muxers]):
return
try:
diff --git a/pitivi/project.py b/pitivi/project.py
index 75097cd..c833671 100644
--- a/pitivi/project.py
+++ b/pitivi/project.py
@@ -24,6 +24,7 @@ Project related classes
"""
import os
+from gi.repository import GstPbutils
from gi.repository import GES
from gi.repository import Gst
from gi.repository import Gtk
@@ -35,8 +36,6 @@ from datetime import datetime
from gettext import gettext as _
from pwd import getpwuid
-from pitivi.medialibrary import MediaLibrary
-from pitivi.settings import MultimediaSettings
from pitivi.undo.undo import UndoableAction
from pitivi.configure import get_ui_dir
@@ -53,15 +52,14 @@ from pitivi.utils.ui import frame_rates, audio_rates, audio_depths,\
pixel_aspect_ratios, display_aspect_ratios, SPACING
from pitivi.preset import AudioPresetManager, DuplicatePresetNameException,\
VideoPresetManager
+from pitivi.render import CachedEncoderList
+DEFAULT_MUXER = "oggmux"
+DEFAULT_VIDEO_ENCODER = "theoraenc"
+DEFAULT_AUDIO_ENCODER = "vorbisenc"
+
#------------------ Backend classes ------------------------------------------#
-class Timeline(GES.Timeline):
- def __init__(self):
- GES.Timeline.__init__(self)
- self.add_track(GES.Track.video_raw_new())
- self.add_track(GES.Track.audio_raw_new())
- self.selection = Selection()
class ProjectSettingsChanged(UndoableAction):
@@ -86,21 +84,25 @@ class ProjectLogObserver(UndoableAction):
self.log = log
def startObserving(self, project):
- project.connect("settings-changed", self._settingsChangedCb)
+ project.connect("notify-meta", self._settingsChangedCb)
def stopObserving(self, project):
try:
- project.disconnect_by_function(self._settingsChangedCb)
+ project.disconnect_by_func(self._settingsChangedCb)
except Exception:
# This can happen when we interrupt the loading of a project,
# such as in mainwindow's _projectManagerMissingUriCb
pass
- def _settingsChangedCb(self, project, old, new):
+ def _settingsChangedCb(self, project, item, value):
+ """
+ FIXME Renable undo/redo
action = ProjectSettingsChanged(project, old, new)
self.log.begin("change project settings")
self.log.push(action)
self.log.commit()
+ """
+ pass
class ProjectManager(Signallable, Loggable):
@@ -108,7 +110,7 @@ class ProjectManager(Signallable, Loggable):
"new-project-loading": ["uri"],
"new-project-created": ["project"],
"new-project-failed": ["uri", "exception"],
- "new-project-loaded": ["project"],
+ "new-project-loaded": ["project", "fully_ready"],
"save-project-failed": ["uri", "exception"],
"project-saved": ["project", "uri"],
"closing-project": ["project"],
@@ -125,7 +127,6 @@ class ProjectManager(Signallable, Loggable):
self.backup_lock = 0
self.avalaible_effects = avalaible_effects
self.formatter = None
- self._medialib_awaiting_discovery = []
def loadProject(self, uri):
"""
@@ -162,19 +163,19 @@ class ProjectManager(Signallable, Loggable):
# The "old" backup file will eventually be deleted or overwritten.
self.current = Project(uri=uri)
- self.emit("new-project-created", self.current)
-
- timeline = self.current.timeline
- self.formatter = GES.PitiviFormatter()
- self.formatter.connect("source-moved", self._formatterMissingURICb)
- self.formatter.connect("loaded", self._projectLoadedCb)
- if self.formatter.load_from_uri(timeline, uri):
+ self.current.connect("missing-uri", self._missingURICb)
+ self.current.connect("loaded", self._projectLoadedCb)
+ if self.current.createTimeline():
+ self.emit("new-project-created", self.current)
self.current.connect("project-changed", self._projectChangedCb)
- return True
- self.emit("new-project-failed", uri,
- _('This might be due to a bug or an unsupported project file format. '
- 'If you were trying to add a media file to your project, '
- 'use the "Import" button instead.'))
+ return
+ else:
+ self.emit("new-project-failed", uri,
+ _('This might be due to a bug or an unsupported project file format. '
+ 'If you were trying to add a media file to your project, '
+ 'use the "Import" button instead.'))
+ return
+
# Reset projectManager and disconnect all the signals:
self.newBlankProject()
return False
@@ -186,8 +187,8 @@ class ProjectManager(Signallable, Loggable):
@param time_diff: the difference, in seconds, between file mtimes
"""
dialog = Gtk.Dialog("", None, 0,
- (_("Ignore backup"), Gtk.ResponseType.REJECT,
- _("Restore from backup"), Gtk.ResponseType.YES))
+ (_("Ignore backup"), Gtk.ResponseType.REJECT,
+ _("Restore from backup"), Gtk.ResponseType.YES))
dialog.set_icon_name("pitivi")
dialog.set_resizable(False)
dialog.set_default_response(Gtk.ResponseType.YES)
@@ -209,7 +210,7 @@ class ProjectManager(Signallable, Loggable):
# make the [[image] text] hbox
image = Gtk.Image.new_from_stock(Gtk.STOCK_DIALOG_QUESTION,
- Gtk.IconSize.DIALOG)
+ Gtk.IconSize.DIALOG)
hbox = Gtk.HBox(False, SPACING * 2)
hbox.pack_start(image, False, True, 0)
hbox.pack_start(vbox, True, True, 0)
@@ -228,28 +229,25 @@ class ProjectManager(Signallable, Loggable):
else:
return False
- def saveProject(self, project, uri=None, overwrite=False, formatter=None, backup=False):
+ def saveProject(self, project, uri=None, overwrite=False, formatter_type=None,
+ backup=False):
"""
Save the L{Project} to the given location.
- If specified, use the given formatter.
-
@type project: L{Project}
@param project: The L{Project} to save.
@type uri: L{str}
@param uri: The absolute URI of the location to store the project to.
@param overwrite: Whether to overwrite existing location.
@type overwrite: C{bool}
- @type formatter: L{Formatter}
- @param formatter: The L{Formatter} to use to store the project if specified.
- If it is not specified, then it will be saved at its original format.
+ @type formatter_type: L{GType}
+ @param formatter: The type of the formatter to use to store the project if specified.
+ default is GES.XmlFormatter
@param backup: Whether the requested save operation is for a backup
@type backup: C{bool}
- @see: L{Formatter.saveProject}
+ @see: L{GES.Project.save}
"""
- if formatter is None:
- formatter = GES.PitiviFormatter()
if backup:
if project.uri and self.current.uri is not None:
# Ignore whatever URI that is passed on to us. It's a trap.
@@ -270,7 +268,7 @@ class ProjectManager(Signallable, Loggable):
if not isWritable(path_from_uri(uri)):
# TODO: this will not be needed when GTK+ bug #601451 is fixed
self.emit("save-project-failed", uri,
- _("You do not have permissions to write to this folder."))
+ _("You do not have permissions to write to this folder."))
return
# Update the project instance's uri for the "Save as" scenario.
@@ -278,22 +276,29 @@ class ProjectManager(Signallable, Loggable):
if not backup:
project.uri = uri
- if uri is None or not formatter.can_save_uri(uri):
+ if uri is None:
self.emit("save-project-failed", uri,
- _("Cannot save with this file format."))
+ _("Cannot save with this file format."))
return
- if overwrite or not os.path.exists(path_from_uri(uri)):
- formatter.set_sources(project.medialibrary.getSources())
- saved = formatter.save_to_uri(project.timeline, uri)
- if saved:
- if not backup:
- # Do not emit the signal when autosaving a backup file
- self.emit("project-saved", project, uri)
- self.debug('Saved project "%s"' % uri)
- else:
- self.debug('Saved backup "%s"' % uri)
- return saved
+ try:
+ saved = project.save(project.timeline, uri, formatter_type, overwrite)
+ except Exception, e:
+ self.emit("save-project-failed", uri,
+ _("Cannot save with this file format. %s"), e)
+
+ if saved:
+ if not backup:
+ # Do not emit the signal when autosaving a backup file
+ project.setModificationState(False)
+ self.emit("project-saved", project, uri)
+ self.debug('Saved project "%s"' % uri)
+ else:
+ self.debug('Saved backup "%s"' % uri)
+ else:
+ self.emit("save-project-failed", uri,
+ _("Cannot save with this file format"))
+ return saved
def exportProject(self, project, uri):
"""
@@ -301,8 +306,9 @@ class ProjectManager(Signallable, Loggable):
and all sources
"""
# write project file to temporary file
- project_name = project.name if project.name else "project"
- tmp_name = "%s.xptv" % project_name
+ project_name = project.name if project.name else _("project")
+ asset = GES.Formatter.get_default()
+ tmp_name = "%s.%s" % (project_name, asset.get_meta(GES.META_FORMATTER_EXTENSION))
try:
directory = os.path.dirname(uri)
@@ -317,7 +323,7 @@ class ProjectManager(Signallable, Loggable):
tar.add(path_from_uri(tmp_uri), os.path.join(top, tmp_name))
# get common path
- sources = project.medialibrary.getSources()
+ sources = project.listSources()
if self._allSourcesInHomedir(sources):
common = os.path.expanduser("~")
else:
@@ -325,7 +331,7 @@ class ProjectManager(Signallable, Loggable):
# add all sources
for source in sources:
- path = path_from_uri(source.get_uri())
+ path = path_from_uri(source.get_id())
tar.add(path, os.path.join(top, os.path.relpath(path, common)))
tar.close()
@@ -386,19 +392,12 @@ class ProjectManager(Signallable, Loggable):
# setting default values for project metadata
project.author = getpwuid(os.getuid()).pw_gecos.split(",")[0]
+ project.createTimeline()
self.emit("new-project-created", project)
self.current = project
- # Add default tracks to the timeline of the new project.
- # The tracks of the timeline determine what tracks
- # the rendered content will have. Pitivi currently supports
- # projects with exactly one video track and one audio track.
project.connect("project-changed", self._projectChangedCb)
- if emission:
- self.current.disconnect = False
- else:
- self.current.disconnect = True
- self.emit("new-project-loaded", self.current)
+ self.emit("new-project-loaded", self.current, emission)
self.time_loaded = time()
return True
@@ -461,41 +460,22 @@ class ProjectManager(Signallable, Loggable):
name, ext = os.path.splitext(uri)
return name + ext + "~"
- def _formatterMissingURICb(self, formatter, tfs):
- return self.emit("missing-uri", formatter, tfs)
+ def _missingURICb(self, project, error, asset, what=None):
+ return self.emit("missing-uri", project, error, asset)
- def _sourceAddedCb(self, unused_medialib, info):
- try:
- self._medialib_awaiting_discovery.remove(info.get_uri())
- except ValueError:
- self.error("%s not awaited, user is really fast", info.get_uri)
-
- if not self._medialib_awaiting_discovery:
- self.current.medialibrary.disconnect_by_function(self._sourceAddedCb)
- self.emit("new-project-loaded", self.current)
- self.time_loaded = time()
-
- def _projectLoadedCb(self, formatter, timeline):
- self.debug("Project loaded, starting media discovery")
- for uri in self.formatter.get_sources():
- self._medialib_awaiting_discovery.append(quote_uri(uri))
- self.current.medialibrary.addUris(self._medialib_awaiting_discovery)
- if self._medialib_awaiting_discovery:
- self.current.medialibrary.connect("source-added", self._sourceAddedCb)
- else:
- self.emit("new-project-loaded", self.current)
- self.time_loaded = time()
+ def _projectLoadedCb(self, project, timeline):
+ self.debug("Project loaded")
+ self.emit("new-project-loaded", self.current, True)
+ self.time_loaded = time()
-class Project(Signallable, Loggable):
+class Project(Loggable, GES.Project):
"""The base class for PiTiVi projects
@ivar name: The name of the project
@type name: C{str}
@ivar description: A description of the project
@type description: C{str}
- @ivar medialibrary: The sources used by this project
- @type medialibrary: L{MediaLibrary}
@ivar timeline: The timeline
@type timeline: L{GES.Timeline}
@ivar pipeline: The timeline's pipeline
@@ -506,13 +486,18 @@ class Project(Signallable, Loggable):
@type loaded: C{bool}
Signals:
- - C{settings-changed}: The project settings changed
- C{project-changed}: Modifications were made to the project
+ - C{start-importing}: Started to import files in bash
+ - C{done-importing}: Done importing files in bash
"""
- __signals__ = {
- "settings-changed": ['old', 'new'],
- "project-changed": [],
+ __gsignals__ = {
+ "start-importing": (GObject.SignalFlags.RUN_LAST, None, ()),
+ "done-importing": (GObject.SignalFlags.RUN_LAST, None, ()),
+ "project-changed": (GObject.SignalFlags.RUN_LAST, None, ()),
+ "rendering-settings-changed": (GObject.SignalFlags.RUN_LAST, None,
+ (GObject.TYPE_PYOBJECT,
+ GObject.TYPE_PYOBJECT,))
}
def __init__(self, name="", uri=None, **kwargs):
@@ -521,67 +506,364 @@ class Project(Signallable, Loggable):
@param uri: the uri of the project
"""
Loggable.__init__(self)
+ GES.Project.__init__(self, uri=uri, extractable_type=GES.Timeline)
self.log("name:%s, uri:%s", name, uri)
- self.name = name
- self.author = ""
- self.year = ""
- self.settings = None
- self.description = ""
+ self.pipeline = None
+ self.timeline = None
+ self.seeker = Seeker()
+
+ # FIXME Remove our URI and work more closely with GES.Project URI handling
self.uri = uri
- self.urichanged = False
- self.format = None
- self.medialibrary = MediaLibrary()
+ # Follow imports
self._dirty = False
- self.timeline = Timeline()
+ self._to_import_uris = []
+ self.nb_files_to_import = 0
+ self.nb_imported_files = 0
+
+ # Project property default values
+ self.register_meta(GES.MetaFlag.READWRITE, "name", name)
+ self.register_meta(GES.MetaFlag.READWRITE, "author",
+ getpwuid(os.getuid()).pw_gecos.split(",")[0])
+
+ # Handle rendering setting
+ self.set_meta("render-scale", 100.0)
+
+ container_profile = \
+ GstPbutils.EncodingContainerProfile.new("pitivi-profile",
+ _("Pitivi encoding profile"),
+ Gst.Caps("application/ogg"),
+ None)
+
+ # Create video profile (We use the same default seetings as the project settings)
+ video_profile = GstPbutils.EncodingVideoProfile.new(Gst.Caps("video/x-theora"),
+ None,
+ Gst.Caps("video/x-raw"),
+ 0)
+
+ # Create audio profile (We use the same default seetings as the project settings)
+ audio_profile = GstPbutils.EncodingAudioProfile.new(Gst.Caps("audio/x-vorbis"),
+ None,
+ Gst.Caps("audio/x-raw"),
+ 0)
+ container_profile.add_profile(video_profile)
+ container_profile.add_profile(audio_profile)
+ # Keep a reference to those profiles
+ # FIXME We should handle the case we have more than 1 audio and 1 video profiles
+ self.container_profile = container_profile
+ self.audio_profile = audio_profile
+ self.video_profile = video_profile
+
+ # Add the profile to ourself
+ self.add_encoding_profile(container_profile)
+
+ # Now set the presets/ GstElement that will be used
+ # FIXME We might want to add the default Container/video decoder/audio encoder
+ # into the application settings, for now we just make sure to pick one with
+ # eighest probably the user has installed ie ogg+vorbis+theora
+
+ self.muxer = DEFAULT_MUXER
+ self.vencoder = DEFAULT_VIDEO_ENCODER
+ self.aencoder = DEFAULT_AUDIO_ENCODER
+ self._ensureAudioRestrictions()
+ self._ensureVideoRestrictions()
+
+ # FIXME That does not really belong to here and should be savable into
+ # The serilized file. For now, just let it be here.
+ # A (muxer -> containersettings) map.
+ self._containersettings_cache = {}
+ # A (vencoder -> vcodecsettings) map.
+ self._vcodecsettings_cache = {}
+ # A (aencoder -> acodecsettings) map.
+ self._acodecsettings_cache = {}
+
+ #-----------------#
+ # Our properties #
+ #-----------------#
+
+ # Project specific properties
+ @property
+ def name(self):
+ return self.get_meta("name")
+
+ @name.setter
+ def name(self, name):
+ self.set_meta("name", name)
+ self.setModificationState(True)
+
+ @property
+ def year(self):
+ return self.get_meta("year")
+
+ @year.setter
+ def year(self, year):
+ self.set_meta("year", year)
+ self.setModificationState(True)
+
+ @property
+ def description(self):
+ return self.get_meta("description")
+
+ @description.setter
+ def description(self, description):
+ self.set_meta("description", description)
+ self.setModificationState(True)
+
+ @property
+ def author(self):
+ return self.get_meta("author")
+
+ @author.setter
+ def author(self, author):
+ self.set_meta("author", author)
+ self.setModificationState(True)
+
+ # Encoding related properties
+ @property
+ def videowidth(self):
+ return self.video_profile.get_restriction()[0]["videowidth"]
+
+ @videowidth.setter
+ def videowidth(self, value):
+ if self.video_profile.get_restriction()[0]["videowidth"] != value and value:
+ self.video_profile.get_restriction()[0]["videowidth"] = value
+ self._emitChange("rendering-settings-changed", "videowidth", value)
+
+ @property
+ def videoheight(self):
+ return self.video_profile.get_restriction()[0]["videoheight"]
+
+ @videoheight.setter
+ def videoheight(self, value):
+ if self.video_profile.get_restriction()[0]["videoheight"] != value and value:
+ self.video_profile.get_restriction()[0]["videoheight"] = value
+ self._emitChange("rendering-settings-changed", "videoheight", value)
+
+ @property
+ def videorate(self):
+ return self.video_profile.get_restriction()[0]["videorate"]
+
+ @videorate.setter
+ def videorate(self, value):
+ if self.video_profile.get_restriction()[0]["videorate"] != value and value:
+ self.video_profile.get_restriction()[0]["videorate"] = value
+
+ @property
+ def videopar(self):
+ return self.video_profile.get_restriction()[0]["videopar"]
+
+ @videopar.setter
+ def videopar(self, value):
+ if self.video_profile.get_restriction()[0]["videopar"] != value and value:
+ self.video_profile.get_restriction()[0]["videopar"] = value
+
+ @property
+ def audiochannels(self):
+ return self.audio_profile.get_restriction()[0]["audiochannels"]
+
+ @audiochannels.setter
+ def audiochannels(self, value):
+ if self.video_profile.get_restriction()[0]["audiochannels"] != value and value:
+ self.audio_profile.get_restriction()[0]["audiochannels"] = value
+ self._emitChange("rendering-settings-changed", "audiochannels", value)
+
+ @property
+ def audiorate(self):
+ return self.audio_profile.get_restriction()[0]["audiorate"]
+
+ @audiorate.setter
+ def audiorate(self, value):
+ if self.video_profile.get_restriction()[0]["audiorate"] != value and value:
+ self.audio_profile.get_restriction()[0]["audiorate"] = value
+ self._emitChange("rendering-settings-changed", "audiorate", value)
+
+ @property
+ def audiodepth(self):
+ return self.audio_profile.get_restriction()[0]["audiodepth"]
+
+ @audiodepth.setter
+ def audiodepth(self, value):
+ if self.video_profile.get_restriction()[0]["audiodepth"] != value and value:
+ self.audio_profile.get_restriction()[0]["audiodepth"] = value
+ self._emitChange("rendering-settings-changed", "audiodepth", value)
+
+ @property
+ def aencoder(self):
+ return self.audio_profile.get_preset_name()
+
+ @aencoder.setter
+ def aencoder(self, value):
+ if self.audio_profile.get_preset_name() != value and value:
+ feature = Gst.Registry.get().lookup_feature(value)
+ if feature is None:
+ self.error("%s not in registry", value)
+ else:
+ for template in feature.get_static_pad_templates():
+ if template.name_template == "src":
+ audiotype = template.get_caps()[0].get_name()
+ break
+ self.audio_profile.set_format(Gst.Caps(audiotype))
+ self.audio_profile.set_preset_name(value)
+
+ self._emitChange("rendering-settings-changed", "aencoder", value)
+
+ @property
+ def vencoder(self):
+ return self.video_profile.get_preset_name()
+
+ @vencoder.setter
+ def vencoder(self, value):
+ if self.video_profile.get_preset_name() != value and value:
+ feature = Gst.Registry.get().lookup_feature(value)
+ if feature is None:
+ self.error("%s not in registry", value)
+ else:
+ for template in feature.get_static_pad_templates():
+ if template.name_template == "src":
+ videotype = template.get_caps()[0].get_name()
+ break
+ self.video_profile.set_format(Gst.Caps(videotype))
+
+ self.video_profile.set_preset_name(value)
+
+ self._emitChange("rendering-settings-changed", "vencoder", value)
+
+ @property
+ def muxer(self):
+ return self.container_profile.get_preset_name()
+
+ @muxer.setter
+ def muxer(self, value):
+ if self.container_profile.get_preset_name() != value and value:
+ feature = Gst.Registry.get().lookup_feature(value)
+ if feature is None:
+ self.error("%s not in registry", value)
+ else:
+ for template in feature.get_static_pad_templates():
+ if template.name_template == "src":
+ muxertype = template.get_caps()[0].get_name()
+ break
+ self.container_profile.set_format(Gst.Caps(muxertype))
+ self.container_profile.set_preset_name(value)
+
+ self._emitChange("rendering-settings-changed", "muxer", value)
+
+ @property
+ def render_scale(self):
+ return self.get_meta("render-scale")
+
+ @render_scale.setter
+ def render_scale(self, value):
+ if value:
+ return self.set_meta("render-scale", value)
+
+ #--------------------------------------------#
+ # GES.Project virtual methods implementation #
+ #--------------------------------------------#
+ def _handle_asset_loaded(self, id):
+ try:
+ self._to_import_uris.remove(id)
+ self.nb_imported_files += 1
+ if not self._to_import_uris:
+ self.nb_imported_files = 0
+ self.nb_files_to_import = 0
+ self._emitChange("done-importing")
+ except ValueError:
+ # We care only about the assets we are tracking
+ pass
+
+ def do_asset_added(self, asset):
+ """
+ When GES.Project emit "asset-added" this vmethod
+ get calls
+ """
+ self._handle_asset_loaded(asset.get_id())
+
+ def do_loading_error(self, error, id, type):
+ """ vmethod, get called on "asset-loading-error"""
+ self._handle_asset_loaded(id)
+
+ def do_loaded(self, timeline):
+ """ vmethod, get called on "loaded" """
+ self._ensureTracks()
+ #self._ensureLayer()
+
+ encoders = CachedEncoderList()
+ # The project just loaded, we need to check the new
+ # encoding profiles and make use of it now.
+ container_profile = self.list_encoding_profiles()[0]
+ if container_profile is not self.container_profile:
+ # The encoding profile might have been reset from the
+ # Project file, we just take it as our
+ self.container_profile = container_profile
+ self.muxer = self._getElementFactoryName(encoders.muxers, container_profile)
+ if self.muxer is None:
+ self.muxer = DEFAULT_MUXER
+ for profile in container_profile.get_profiles():
+ if isinstance(profile, GstPbutils.EncodingVideoProfile):
+ self.video_profile = profile
+ if self.video_profile.get_restriction() is None:
+ self.video_profile.set_restriction(Gst.Caps("video/x-raw"))
+ self._ensureVideoRestrictions()
+
+ self.vencoder = self._getElementFactoryName(encoders.vencoders, profile)
+ elif isinstance(profile, GstPbutils.EncodingAudioProfile):
+ self.audio_profile = profile
+ if self.audio_profile.get_restriction() is None:
+ self.audio_profile.set_restriction(Gst.Caps("audio/x-raw"))
+ self._ensureAudioRestrictions()
+ self.aencoder = self._getElementFactoryName(encoders.aencoders, profile)
+ else:
+ self.warning("We do not handle profile: %s" % profile)
+ #--------------------------------------------#
+ # Our API #
+ #--------------------------------------------#
+ def createTimeline(self):
+ """
+ The pitivi.Project handle 1 timeline at a time
+ unlike GES.Project
+ """
+ self.timeline = self.extract()
+ if self.timeline is None:
+ return False
+
+ self.timeline.selection = Selection()
self.pipeline = Pipeline()
self.pipeline.add_timeline(self.timeline)
- self.seeker = Seeker()
- self.settings = MultimediaSettings()
+ return True
+
+ def addUris(self, uris):
+ """
+ Add c{uris} to the source list.
- def getUri(self):
- return self._uri
+ The uris will be analyzed before being added.
+ """
+ # Do not try to reload URIS that we already have loaded
+ uris = [uri for uri in uris if self.get_asset(uri, GES.TimelineFileSource) is None]
+ if not uris:
+ return
- def setUri(self, uri):
- # FIXME support not local project
- if uri and not Gst.uri_has_protocol(uri, "file"):
- # Note that this does *not* give the same result as quote_uri()
- self._uri = Gst.uri_construct("file", uri)
+ self.nb_files_to_import += len(uris)
+ if self._to_import_uris:
+ self._to_import_uris = set(uris.extend(list(self._to_import_uris)))
else:
- self._uri = uri
+ self._emitChange("start-importing")
+ self._to_import_uris = uris
- uri = property(getUri, setUri)
+ for uri in self._to_import_uris:
+ self.create_asset(uri, GES.TimelineFileSource)
+
+ def listSources(self):
+ return self.list_assets(GES.TimelineFileSource)
def release(self):
- self.pipeline.release()
+ if self.pipeline:
+ self.pipeline.release()
self.pipeline = None
self.timeline = None
- # Project settings methods
-
- def getSettings(self):
- """
- return the currently configured settings.
- """
- self.debug("self.settings %s", self.settings)
- return self.settings
-
- def setSettings(self, settings):
- """
- Sets the given settings as the project's settings.
- @param settings: The new settings for the project.
- @type settings: MultimediaSettings
- """
- assert settings
- self.log("Setting %s as the project's settings", settings)
- oldsettings = self.settings
- self.settings = settings
- self.emit('settings-changed', oldsettings, settings)
-
- # Save and Load features
-
def setModificationState(self, state):
self._dirty = state
if state:
@@ -590,13 +872,153 @@ class Project(Signallable, Loggable):
def hasUnsavedModifications(self):
return self._dirty
+ def getDAR(self):
+ return Gst.Fraction(self.videowidth, self.videoheight) * self.videopar
+
+ def getVideoWidthAndHeight(self, render=False):
+ """ Returns the video width and height as a tuple
+
+ @param render: Whether to apply self.render_scale to the returned values
+ @type render: bool
+ """
+ if render:
+ scale = self.render_scale
+ else:
+ scale = 100
+ return self.videowidth * scale / 100, self.videoheight * scale / 100
+
+ def getVideoCaps(self, render=False):
+ """ Returns the GstCaps corresponding to the video settings """
+ videowidth, videoheight = self.getVideoWidthAndHeight(render=render)
+ vstr = "width=%d,height=%d,pixel-aspect-ratio=%d/%d,framerate=%d/%d" % (
+ videowidth, videoheight,
+ self.videopar.num, self.videopar.denom,
+ self.videorate.num, self.videorate.denom)
+ caps_str = "video/x-raw,%s" % (vstr)
+ video_caps = Gst.caps_from_string(caps_str)
+ return video_caps
+
+ def getAudioCaps(self):
+ """ Returns the GstCaps corresponding to the audio settings """
+ # TODO: Figure out why including 'depth' causes pipeline failures:
+ astr = "rate=%d,channels=%d" % (self.audiorate, self.audiochannels)
+ caps_str = "audio/x-raw,%s" % (astr)
+ audio_caps = Gst.caps_from_string(caps_str)
+ return audio_caps
+
+ def setAudioProperties(self, nbchanns=-1, rate=-1, depth=-1):
+ """
+ Set the number of audio channels, rate and depth
+ """
+ self.info("%d x %dHz %dbits", nbchanns, rate, depth)
+ if not nbchanns == -1 and not nbchanns == self.audiochannels:
+ self.audiochannels = nbchanns
+ if not rate == -1 and not rate == self.audiorate:
+ self.audiorate = rate
+ if not depth == -1 and not depth == self.audiodepth:
+ self.audiodepth = depth
+
+ def setEncoders(self, muxer="", vencoder="", aencoder=""):
+ """ Set the video/audio encoder and muxer """
+ if not muxer == "" and not muxer == self.muxer:
+ self.muxer = muxer
+ if not vencoder == "" and not vencoder == self.vencoder:
+ self.vencoder = vencoder
+ if not aencoder == "" and not aencoder == self.aencoder:
+ self.aencoder = aencoder
+
+ @property
+ def containersettings(self):
+ return self._containersettings_cache.setdefault(self.muxer, {})
+
+ @containersettings.setter
+ def containersettings(self, value):
+ self._containersettings_cache[self.muxer] = value
+
+ @property
+ def vcodecsettings(self):
+ return self._vcodecsettings_cache.setdefault(self.vencoder, {})
+
+ @vcodecsettings.setter
+ def vcodecsettings(self, value):
+ self._vcodecsettings_cache[self.vencoder] = value
+
+ @property
+ def acodecsettings(self):
+ return self._acodecsettings_cache.setdefault(self.aencoder, {})
+
+ @acodecsettings.setter
+ def acodecsettings(self, value):
+ self._acodecsettings_cache[self.aencoder] = value
+
+ #--------------------------------------------#
+ # Private methods #
+ #--------------------------------------------#
+
+ def _ensureTracks(self):
+ if self.timeline is None:
+ self.warning("Can't ensure tracks if no timeline set")
+ return
+
+ track_types = [track.get_property("track-type")
+ for track in self.timeline.get_tracks()]
+
+ if GES.TrackType.VIDEO not in track_types:
+ self.timeline.add_track(GES.Track.video_raw_new())
+ if GES.TrackType.AUDIO not in track_types:
+ self.timeline.add_track(GES.Track.audio_raw_new())
+
+ def _ensureLayer(self):
+ if self.timeline is None:
+ self.warning("Can't ensure tracks if no timeline set")
+ return
+ if not self.timeline.get_layers():
+ self.timeline.append_layer()
+
+ def _ensureVideoRestrictions(self):
+ if not self.videowidth:
+ self.videowidth = 720
+ if not self.videoheight:
+ self.videoheight = 576
+ if not self.videorate:
+ self.videorate = Gst.Fraction(25, 1)
+ if not self.videopar:
+ self.videopar = Gst.Fraction(16, 15)
+
+ def _ensureAudioRestrictions(self):
+ if not self.audiochannels:
+ self.audiochannels = 2
+ if not self.audiorate:
+ self.audiorate = 44100
+ if not self.audiodepth:
+ self.audiodepth = 16
+
+ def _emitChange(self, signal, key=None, value=None):
+ if key and value:
+ self.emit(signal, key, value)
+ else:
+ self.emit(signal)
+ self.setModificationState(True)
+
+ def _getElementFactoryName(self, elements, profile):
+ if profile.get_preset_name():
+ return profile.get_preset_name()
+
+ factories = Gst.ElementFactory.list_filter(elements,
+ Gst.Caps(profile.get_format()),
+ Gst.PadDirection.SRC,
+ False)
+ if factories:
+ factories.sort(key=lambda x: - x.get_rank())
+ return factories[0].get_name()
+ return None
+
#----------------------- UI classes ------------------------------------------#
class ProjectSettingsDialog():
def __init__(self, parent, project):
self.project = project
- self.settings = project.getSettings()
self.builder = Gtk.Builder()
self.builder.add_from_file(os.path.join(get_ui_dir(), "projectsettings.ui"))
@@ -606,19 +1028,25 @@ class ProjectSettingsDialog():
# add custom display aspect ratio widget
self.dar_fraction_widget = FractionWidget()
self.video_properties_table.attach(self.dar_fraction_widget,
- 0, 1, 6, 7, xoptions=Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL, yoptions=0)
+ 0, 1, 6, 7,
+ xoptions=Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL,
+ yoptions=0)
self.dar_fraction_widget.show()
# add custom pixel aspect ratio widget
self.par_fraction_widget = FractionWidget()
self.video_properties_table.attach(self.par_fraction_widget,
- 1, 2, 6, 7, xoptions=Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL, yoptions=0)
+ 1, 2, 6, 7,
+ xoptions=Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL,
+ yoptions=0)
self.par_fraction_widget.show()
# add custom framerate widget
self.frame_rate_fraction_widget = FractionWidget()
self.video_properties_table.attach(self.frame_rate_fraction_widget,
- 1, 2, 2, 3, xoptions=Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL, yoptions=0)
+ 1, 2, 2, 3,
+ xoptions=Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL,
+ yoptions=0)
self.frame_rate_fraction_widget.show()
# populate coboboxes with appropriate data
@@ -633,13 +1061,13 @@ class ProjectSettingsDialog():
# behavior
self.wg = RippleUpdateGroup()
self.wg.addVertex(self.frame_rate_combo,
- signal="changed",
- update_func=self._updateCombo,
- update_func_args=(self.frame_rate_fraction_widget,))
+ signal="changed",
+ update_func=self._updateCombo,
+ update_func_args=(self.frame_rate_fraction_widget,))
self.wg.addVertex(self.frame_rate_fraction_widget,
- signal="value-changed",
- update_func=self._updateFraction,
- update_func_args=(self.frame_rate_combo,))
+ signal="value-changed",
+ update_func=self._updateFraction,
+ update_func_args=(self.frame_rate_combo,))
self.wg.addVertex(self.dar_combo, signal="changed")
self.wg.addVertex(self.dar_fraction_widget, signal="value-changed")
self.wg.addVertex(self.par_combo, signal="changed")
@@ -647,54 +1075,60 @@ class ProjectSettingsDialog():
self.wg.addVertex(self.width_spinbutton, signal="value-changed")
self.wg.addVertex(self.height_spinbutton, signal="value-changed")
self.wg.addVertex(self.save_audio_preset_button,
- update_func=self._updateAudioSaveButton)
+ update_func=self._updateAudioSaveButton)
self.wg.addVertex(self.save_video_preset_button,
- update_func=self._updateVideoSaveButton)
+ update_func=self._updateVideoSaveButton)
self.wg.addVertex(self.channels_combo, signal="changed")
self.wg.addVertex(self.sample_rate_combo, signal="changed")
self.wg.addVertex(self.sample_depth_combo, signal="changed")
# constrain width and height IFF constrain_sar_button is active
self.wg.addEdge(self.width_spinbutton, self.height_spinbutton,
- predicate=self.constrained, edge_func=self.updateHeight)
+ predicate=self.constrained,
+ edge_func=self.updateHeight)
self.wg.addEdge(self.height_spinbutton, self.width_spinbutton,
- predicate=self.constrained, edge_func=self.updateWidth)
+ predicate=self.constrained,
+ edge_func=self.updateWidth)
# keep framereate text field and combo in sync
self.wg.addBiEdge(self.frame_rate_combo, self.frame_rate_fraction_widget)
# keep dar text field and combo in sync
self.wg.addEdge(self.dar_combo, self.dar_fraction_widget,
- edge_func=self.updateDarFromCombo)
+ edge_func=self.updateDarFromCombo)
self.wg.addEdge(self.dar_fraction_widget, self.dar_combo,
- edge_func=self.updateDarFromFractionWidget)
+ edge_func=self.updateDarFromFractionWidget)
# keep par text field and combo in sync
self.wg.addEdge(self.par_combo, self.par_fraction_widget,
- edge_func=self.updateParFromCombo)
+ edge_func=self.updateParFromCombo)
self.wg.addEdge(self.par_fraction_widget, self.par_combo,
- edge_func=self.updateParFromFractionWidget)
+ edge_func=self.updateParFromFractionWidget)
# constrain DAR and PAR values. because the combo boxes are already
# linked, we only have to link the fraction widgets together.
self.wg.addEdge(self.par_fraction_widget, self.dar_fraction_widget,
- edge_func=self.updateDarFromPar)
+ edge_func=self.updateDarFromPar)
self.wg.addEdge(self.dar_fraction_widget, self.par_fraction_widget,
- edge_func=self.updateParFromDar)
+ edge_func=self.updateParFromDar)
# update PAR when width/height change and the DAR checkbutton is
# selected
self.wg.addEdge(self.width_spinbutton, self.par_fraction_widget,
- predicate=self.darSelected, edge_func=self.updateParFromDar)
+ predicate=self.darSelected,
+ edge_func=self.updateParFromDar)
self.wg.addEdge(self.height_spinbutton, self.par_fraction_widget,
- predicate=self.darSelected, edge_func=self.updateParFromDar)
+ predicate=self.darSelected,
+ edge_func=self.updateParFromDar)
# update DAR when width/height change and the PAR checkbutton is
# selected
self.wg.addEdge(self.width_spinbutton, self.dar_fraction_widget,
- predicate=self.parSelected, edge_func=self.updateDarFromPar)
+ predicate=self.parSelected,
+ edge_func=self.updateDarFromPar)
self.wg.addEdge(self.height_spinbutton, self.dar_fraction_widget,
- predicate=self.parSelected, edge_func=self.updateDarFromPar)
+ predicate=self.parSelected,
+ edge_func=self.updateDarFromPar)
# presets
self.audio_presets = AudioPresetManager()
@@ -702,12 +1136,12 @@ class ProjectSettingsDialog():
self.video_presets = VideoPresetManager()
self.video_presets.loadAll()
- self._fillPresetsTreeview(
- self.audio_preset_treeview, self.audio_presets,
- self._updateAudioPresetButtons)
- self._fillPresetsTreeview(
- self.video_preset_treeview, self.video_presets,
- self._updateVideoPresetButtons)
+ self._fillPresetsTreeview(self.audio_preset_treeview,
+ self.audio_presets,
+ self._updateAudioPresetButtons)
+ self._fillPresetsTreeview(self.video_preset_treeview,
+ self.video_presets,
+ self._updateVideoPresetButtons)
# A map which tells which infobar should be used when displaying
# an error for a preset manager.
@@ -753,22 +1187,20 @@ class ProjectSettingsDialog():
self.select_par_radiobutton.props.active = True
self.par_fraction_widget.setWidgetValue(value)
- mgr.bindWidget("par", updatePar,
- self.par_fraction_widget.getWidgetValue)
+ mgr.bindWidget("par", updatePar, self.par_fraction_widget.getWidgetValue)
def bindFractionWidget(self, mgr, name, widget):
- mgr.bindWidget(name, widget.setWidgetValue,
- widget.getWidgetValue)
+ mgr.bindWidget(name, widget.setWidgetValue, widget.getWidgetValue)
def bindCombo(self, mgr, name, widget):
mgr.bindWidget(name,
- lambda x: set_combo_value(widget, x),
- lambda: get_combo_value(widget))
+ lambda x: set_combo_value(widget, x),
+ lambda: get_combo_value(widget))
def bindSpinbutton(self, mgr, name, widget):
mgr.bindWidget(name,
- lambda x: widget.set_value(float(x)),
- lambda: int(widget.get_value()))
+ lambda x: widget.set_value(float(x)),
+ lambda: int(widget.get_value()))
def _fillPresetsTreeview(self, treeview, mgr, update_buttons_func):
"""Set up the specified treeview to display the specified presets.
@@ -792,7 +1224,7 @@ class ProjectSettingsDialog():
renderer.connect("edited", self._presetNameEditedCb, mgr)
renderer.connect("editing-started", self._presetNameEditingStartedCb, mgr)
treeview.get_selection().connect("changed", self._presetChangedCb, mgr,
- update_buttons_func)
+ update_buttons_func)
treeview.connect("focus-out-event", self._treeviewDefocusedCb, mgr)
def createAudioNoPreset(self, mgr):
@@ -806,7 +1238,7 @@ class ProjectSettingsDialog():
"par": Gst.Fraction(int(get_combo_value(self.par_combo).num),
int(get_combo_value(self.par_combo).denom)),
"frame-rate": Gst.Fraction(int(get_combo_value(self.frame_rate_combo).num),
- int(get_combo_value(self.frame_rate_combo).denom)),
+ int(get_combo_value(self.frame_rate_combo).denom)),
"height": int(self.height_spinbutton.get_value()),
"width": int(self.width_spinbutton.get_value())})
@@ -1028,17 +1460,17 @@ class ProjectSettingsDialog():
def updateUI(self):
- self.width_spinbutton.set_value(self.settings.videowidth)
- self.height_spinbutton.set_value(self.settings.videoheight)
+ self.width_spinbutton.set_value(self.project.videowidth)
+ self.height_spinbutton.set_value(self.project.videoheight)
# video
- self.frame_rate_fraction_widget.setWidgetValue(self.settings.videorate)
- self.par_fraction_widget.setWidgetValue(self.settings.videopar)
+ self.frame_rate_fraction_widget.setWidgetValue(self.project.videorate)
+ self.par_fraction_widget.setWidgetValue(self.project.videopar)
# audio
- set_combo_value(self.channels_combo, self.settings.audiochannels)
- set_combo_value(self.sample_rate_combo, self.settings.audiorate)
- set_combo_value(self.sample_depth_combo, self.settings.audiodepth)
+ 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)
self._selectDarRadiobuttonToggledCb(self.select_dar_radiobutton)
@@ -1057,19 +1489,14 @@ class ProjectSettingsDialog():
self.project.year = str(self.year_spinbutton.get_value_as_int())
def updateSettings(self):
- width = int(self.width_spinbutton.get_value())
- height = int(self.height_spinbutton.get_value())
- par = self.par_fraction_widget.getWidgetValue()
- frame_rate = self.frame_rate_fraction_widget.getWidgetValue()
-
- channels = get_combo_value(self.channels_combo)
- sample_rate = get_combo_value(self.sample_rate_combo)
- sample_depth = get_combo_value(self.sample_depth_combo)
-
- self.settings.setVideoProperties(width, height, frame_rate, par)
- self.settings.setAudioProperties(channels, sample_rate, sample_depth)
-
- self.project.setSettings(self.settings)
+ self.project.videowidth = int(self.width_spinbutton.get_value())
+ self.project.videoheight = int(self.height_spinbutton.get_value())
+ self.project.videopar = self.par_fraction_widget.getWidgetValue()
+ self.project.videorate = self.frame_rate_fraction_widget.getWidgetValue()
+
+ self.project.audiochannels = get_combo_value(self.channels_combo)
+ self.project.audiorate = get_combo_value(self.sample_rate_combo)
+ self.project.audiodepth = get_combo_value(self.sample_depth_combo)
def _responseCb(self, unused_widget, response):
if response == Gtk.ResponseType.OK:
diff --git a/pitivi/render.py b/pitivi/render.py
index f5113c5..5b65d07 100644
--- a/pitivi/render.py
+++ b/pitivi/render.py
@@ -30,12 +30,7 @@ from gi.repository import Gst
from gi.repository import GES
import time
-import pitivi.utils.loggable as log
-
from gettext import gettext as _
-from gi.repository.GstPbutils import EncodingContainerProfile, EncodingVideoProfile, \
- EncodingAudioProfile
-
from pitivi import configure
from pitivi.utils.signal import Signallable
@@ -57,79 +52,100 @@ except ImportError:
has_libnotify = False
-#---------------- Private utils ---------------------------------------#
-def get_compatible_sink_pad(factoryname, caps):
- """
- Returns the pad name of a (request) pad from factoryname which is
- compatible with the given caps.
- """
- factory = Gst.Registry.get().lookup_feature(factoryname)
- if factory is None:
- log.warning("render", "%s is not a valid factoryname", factoryname)
- return None
-
- res = []
- sinkpads = [x for x in factory.get_static_pad_templates() if x.direction == Gst.PadDirection.SINK]
- for p in sinkpads:
- c = p.get_caps()
- log.log("render", "sinkcaps %s", c.to_string())
- inter = caps.intersect(c)
- log.log("render", "intersection %s", inter.to_string())
- if inter:
- res.append(p.name_template)
- if len(res) > 0:
- return res[0]
- return None
-
-
-#FIME GES port make it obselete, handle it properly again
-def get_compatible_sink_caps(factoryname, caps):
- """
- Returns the compatible caps between 'caps' and the sink pad caps of 'factoryname'
+class CachedEncoderList(object):
"""
- log.log("render", "factoryname : %s , caps : %s", factoryname, caps.to_string())
- factory = Gst.Registry.get().lookup_feature(factoryname)
- if factory is None:
- log.warning("render", "%s is not a valid factoryname", factoryname)
- return None
-
- res = []
- sinkcaps = [x.get_caps() for x in factory.get_static_pad_templates() if x.direction == Gst.PadDirection.SINK]
- for c in sinkcaps:
- log.log("render", "sinkcaps %s", c.to_string())
- inter = caps.intersect(c)
- log.log("render", "intersection %s", inter.to_string())
- if inter:
- res.append(inter)
-
- if len(res) > 0:
- return res[0]
- return None
-
-
-def list_compat(a1, b1):
- for x1 in a1:
- if not x1 in b1:
- return False
- return True
+ Registry of avalaible Muxer/Audio encoder/Video Encoder. And
+ avalaible combinations of those.
+
+ You can acces directly the
+ @aencoders: List of avalaible audio encoders
+ @vencoders: List of avalaible video encoders
+ @muxers: List of avalaible muxers
+ @audio_combination: Dictionary from muxer names to compatible audio encoders ordered by Rank
+ @video_combination: Dictionary from muxer names to compatible video encoders ordered by Rank
-def my_can_sink_caps(muxer, ocaps, muxsinkcaps=[]):
- """ returns True if the given caps intersect with some of the muxer's
- sink pad templates' caps.
+
+ It is a singleton.
"""
- # fast version
- if muxsinkcaps != []:
- for c in muxsinkcaps:
- if not c.intersect(ocaps).is_empty():
- return True
+
+ _instance = None
+
+ def __new__(cls, *args, **kwargs):
+ """
+ Override the new method to return the singleton instance if available.
+ Otherwise, create one.
+ """
+ if not cls._instance:
+ cls._instance = super(CachedEncoderList, cls).__new__(cls, *args, **kwargs)
+ Gst.Registry.get().connect("feature-added", cls._instance._registryFeatureAddedCb)
+ cls._instance._buildEncoders()
+ cls._instance._buildCombinations()
+ return cls._instance
+
+ def _buildEncoders(self):
+ self.aencoders = []
+ self.vencoders = []
+ self.muxers = Gst.ElementFactory.list_get_elements(Gst.ELEMENT_FACTORY_TYPE_MUXER,
+ Gst.Rank.SECONDARY)
+
+ for fact in Gst.ElementFactory.list_get_elements(
+ Gst.ELEMENT_FACTORY_TYPE_ENCODER, Gst.Rank.SECONDARY):
+ klist = fact.get_klass().split('/')
+ if "Video" in klist or "Image" in klist:
+ self.vencoders.append(fact)
+ elif "Audio" in klist:
+ self.aencoders.append(fact)
+
+ def _buildCombinations(self):
+ self.audio_combination = {}
+ self.video_combination = {}
+ useless_muxers = set([])
+ for muxer in self.muxers:
+ mux = muxer.get_name()
+ aencs = self._findCompatibleEncoders(self.aencoders, muxer)
+ vencs = self._findCompatibleEncoders(self.vencoders, muxer)
+ # only include muxers with audio and video
+
+ if aencs and vencs:
+ self.audio_combination[mux] = sorted(aencs, key=lambda x: - x.get_rank())
+ self.video_combination[mux] = sorted(vencs, key=lambda x: - x.get_rank())
+ else:
+ useless_muxers.add(muxer)
+
+ for muxer in useless_muxers:
+ self.muxers.remove(muxer)
+
+ def _findCompatibleEncoders(self, encoders, muxer, muxsinkcaps=[]):
+ """ returns the list of encoders compatible with the given muxer """
+ res = []
+ if muxsinkcaps == []:
+ muxsinkcaps = [x.get_caps() for x in muxer.get_static_pad_templates()
+ if x.direction == Gst.PadDirection.SINK]
+ for encoder in encoders:
+ for tpl in encoder.get_static_pad_templates():
+ if tpl.direction == Gst.PadDirection.SRC:
+ if self._canSinkCaps(muxer, tpl.get_caps(), muxsinkcaps):
+ res.append(encoder)
+ break
+ return res
+
+ def _canSinkCaps(self, muxer, ocaps, muxsinkcaps=[]):
+ """ returns True if the given caps intersect with some of the muxer's
+ sink pad templates' caps.
+ """
+ # fast version
+ if muxsinkcaps != []:
+ for c in muxsinkcaps:
+ if not c.intersect(ocaps).is_empty():
+ return True
+ return False
+ # slower default
+ for x in muxer.get_static_pad_templates():
+ if x.direction == Gst.PadDirection.SINK:
+ if not x.get_caps().intersect(ocaps).is_empty():
+ return True
return False
- # slower default
- for x in muxer.get_static_pad_templates():
- if x.direction == Gst.PadDirection.SINK:
- if not x.get_caps().intersect(ocaps).is_empty():
- return True
- return False
# sinkcaps = (x.get_caps() for x in muxer.get_static_pad_templates() if x.direction == Gst.PadDirection.SINK)
# for x in sinkcaps:
@@ -137,131 +153,9 @@ def my_can_sink_caps(muxer, ocaps, muxsinkcaps=[]):
# return True
# return False
-
-class CachedEncoderList(object):
- def __init__(self):
- self._factories = None
- self._registry = Gst.Registry.get()
- self._registry.connect("feature-added", self._registryFeatureAddedCb)
-
- def _ensure_factories(self):
- if self._factories is None:
- self._buildFactories()
-
- def _buildFactories(self):
- self._factories = self._registry.get_feature_list(Gst.ElementFactory)
- self._audioEncoders = []
- self._videoEncoders = []
- self._muxers = []
- for fact in self._factories:
- klist = fact.get_klass().split('/')
- if list_compat(("Codec", "Muxer"), klist):
- self._muxers.append(fact)
- elif list_compat(("Codec", "Encoder", "Video"), klist) or list_compat(("Codec", "Encoder", "Image"), klist):
- self._videoEncoders.append(fact)
- elif list_compat(("Codec", "Encoder", "Audio"), klist):
- self._audioEncoders.append(fact)
-
- def available_muxers(self):
- if self._factories is None:
- self._buildFactories()
- return self._muxers
-
- def available_audio_encoders(self):
- if self._factories is None:
- self._buildFactories()
- return self._audioEncoders
-
- def available_video_encoders(self):
- if self._factories is None:
- self._buildFactories()
- return self._videoEncoders
-
def _registryFeatureAddedCb(self, registry, feature):
- self._factories = None
-
-_cached_encoder_list = None
-
-
-def encoderlist():
- global _cached_encoder_list
- if _cached_encoder_list is None:
- _cached_encoder_list = CachedEncoderList()
- return _cached_encoder_list
-
-
-def available_muxers():
- """ return all available muxers """
- enclist = encoderlist()
- return enclist.available_muxers()
-
-
-def available_video_encoders():
- """ returns all available video encoders """
- enclist = encoderlist()
- return enclist.available_video_encoders()
-
-
-def available_audio_encoders():
- """ returns all available audio encoders """
- enclist = encoderlist()
- return enclist.available_audio_encoders()
-
-
-def encoders_muxer_compatible(encoders, muxer, muxsinkcaps=[]):
- """ returns the list of encoders compatible with the given muxer """
- res = []
- if muxsinkcaps == []:
- muxsinkcaps = [x.get_caps() for x in muxer.get_static_pad_templates() if x.direction == Gst.PadDirection.SINK]
- for encoder in encoders:
- for tpl in encoder.get_static_pad_templates():
- if tpl.direction == Gst.PadDirection.SRC:
- if my_can_sink_caps(muxer, tpl.get_caps(), muxsinkcaps):
- res.append(encoder)
- break
- return res
-
-
-raw_audio_caps = Gst.caps_from_string("audio/x-raw")
-raw_video_caps = Gst.caps_from_string("video/x-raw")
-
-
-def muxer_can_sink_raw_audio(muxer):
- """ Returns True if given muxer can accept raw audio """
- return my_can_sink_caps(muxer, raw_audio_caps)
-
-
-def muxer_can_sink_raw_video(muxer):
- """ Returns True if given muxer can accept raw video """
- return my_can_sink_caps(muxer, raw_video_caps)
-
-
-def available_combinations():
- """Return a 3-tuple of (muxers, audio, video), where:
- - muxers is a list of muxer factories
- - audio is a dictionary from muxer names to compatible audio encoders
- - video is a dictionary from muxer names to compatible video encoders
- """
-
- aencoders = available_audio_encoders()
- vencoders = available_video_encoders()
- muxers = available_muxers()
-
- audio = {}
- video = {}
- containers = []
- for muxer in muxers:
- mux = muxer.get_name()
- aencs = encoders_muxer_compatible(aencoders, muxer)
- vencs = encoders_muxer_compatible(vencoders, muxer)
- # only include muxers with audio and video
-
- if aencs and vencs:
- audio[mux] = aencs
- video[mux] = vencs
- containers.append(muxer)
-
- return containers, audio, video
+ # TODO Check what feature has been added and update our lists
+ pass
def beautify_factoryname(factory):
@@ -404,8 +298,6 @@ class RenderDialog(Loggable):
@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")
@@ -425,7 +317,6 @@ class RenderDialog(Loggable):
self.outfile = None
self.notification = None
- self.settings = project.getSettings()
self.timestarted = 0
# Various gstreamer signal connection ID's
@@ -459,15 +350,15 @@ class RenderDialog(Loggable):
# 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.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.settings.connect("settings-changed", self._settingsChanged)
+ self.project.connect("rendering-settings-changed", self._settingsChanged)
# Monitor changes
@@ -522,8 +413,8 @@ class RenderDialog(Loggable):
"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")})
+ "height": self.project.videoheight,
+ "width": self.project.videowidth})
def bindCombo(self, mgr, name, widget):
if name == "container":
@@ -563,7 +454,7 @@ class RenderDialog(Loggable):
def muxer_setter(self, widget, value):
set_combo_value(widget, Gst.ElementFactory.find(value))
- self.settings.setEncoders(muxer=value)
+ self.project.setEncoders(muxer=value)
# Update the extension of the filename.
basename = os.path.splitext(self.fileentry.get_text())[0]
@@ -578,51 +469,40 @@ class RenderDialog(Loggable):
def acodec_setter(self, widget, value):
set_combo_value(widget, Gst.ElementFactory.find(value))
- self.settings.setEncoders(aencoder=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.settings.setEncoders(vencoder=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):
- set_combo_value(widget, value)
- self.settings.setAudioProperties(depth=value)
+ self.project.audiodepth = set_combo_value(widget, value)
def sample_rate_setter(self, widget, value):
- set_combo_value(widget, value)
- self.settings.setAudioProperties(rate=value)
+ self.project.audiorate = set_combo_value(widget, value)
def channels_setter(self, widget, value):
- set_combo_value(widget, value)
- self.settings.setAudioProperties(nbchanns=value)
+ self.project.audiochannels = set_combo_value(widget, value)
def framerate_setter(self, widget, value):
- set_combo_value(widget, value)
- self.settings.setVideoProperties(framerate=value)
+ self.project.videorate = set_combo_value(widget, value)
def bindHeight(self, mgr):
mgr.bindWidget("height",
- lambda x: self.settings.setVideoProperties(height=x),
+ lambda x: setattr(self.project, "videoheight", x),
lambda: 0)
def bindWidth(self, mgr):
mgr.bindWidget("width",
- lambda x: self.settings.setVideoProperties(width=x),
+ lambda x: setattr(self.project, "videowidth", 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.
@@ -781,38 +661,37 @@ class RenderDialog(Loggable):
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):
+ def _settingsChanged(self, project, key, value):
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))
+ 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.settings.videorate)
+ set_combo_value(self.frame_rate_combo, self.project.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)
+ 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.settings.render_scale)
+ 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.settings.muxer))
+ Gst.ElementFactory.find(self.project.muxer))
def _checkForExistingFile(self, *args):
"""
@@ -838,7 +717,7 @@ class RenderDialog(Loggable):
def updateFilename(self, basename):
"""Updates the filename UI element to show the specified file name."""
- extension = extension_for_muxer(self.settings.muxer)
+ extension = extension_for_muxer(self.project.muxer)
if extension:
name = "%s%s%s" % (basename, os.path.extsep, extension)
else:
@@ -847,13 +726,12 @@ class RenderDialog(Loggable):
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)
+ encoders = CachedEncoderList()
+ vencoder_model = factorylist(encoders.video_combination[self.project.muxer])
+ self.video_encoder_combo.set_model(vencoder_model)
- audio_encoders = self.settings.getAudioEncoders()
- audio_encoder_model = factorylist(audio_encoders)
- self.audio_encoder_combo.set_model(audio_encoder_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)
@@ -879,7 +757,7 @@ class RenderDialog(Loggable):
the properties.
@type settings_attr: str
"""
- properties = getattr(self.settings, settings_attr)
+ 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)
@@ -919,40 +797,14 @@ class RenderDialog(Loggable):
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())
+ setattr(self, settings_attr, self.dialog.getSettings())
self.dialog.window.destroy()
def _renderButtonClickedCb(self, unused_button):
@@ -965,23 +817,7 @@ class RenderDialog(Loggable):
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._pipeline.set_render_settings(self.outfile, self.project.container_profile)
self.startAction()
self.progress.window.show()
self.progress.connect("cancel", self._cancelRender)
@@ -1045,42 +881,36 @@ class RenderDialog(Loggable):
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])
+ """
+ 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.settings.setVideoProperties(render_scale=render_scale)
+ self.project.render_scale = render_scale
self.updateResolution()
def updateResolution(self):
- width, height = self.settings.getVideoWidthAndHeight(render=True)
+ 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.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:
@@ -1117,18 +947,12 @@ class RenderDialog(Loggable):
def _frameRateComboChangedCb(self, combo):
framerate = get_combo_value(combo)
- self.settings.setVideoProperties(framerate=framerate)
+ self.project.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)
+ self.project.vencoder = vencoder
+
if not self.muxer_combo_changing:
# The user directly changed the video encoder combo.
self.preferred_vencoder = vencoder
@@ -1138,24 +962,17 @@ class RenderDialog(Loggable):
self._elementSettingsDialog(factory, 'vcodecsettings')
def _channelsComboChangedCb(self, combo):
- self.settings.setAudioProperties(nbchanns=get_combo_value(combo))
+ self.project.audiochannels = get_combo_value(combo)
def _sampleDepthComboChangedCb(self, combo):
- self.settings.setAudioProperties(depth=get_combo_value(combo))
+ self.project.audiodepth = get_combo_value(combo)
def _sampleRateComboChangedCb(self, combo):
- self.settings.setAudioProperties(rate=get_combo_value(combo))
+ self.project.audiorate = 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
+ self.project.aencoder = aencoder
if not self.muxer_combo_changing:
# The user directly changed the audio encoder combo.
self.preferred_aencoder = aencoder
@@ -1166,11 +983,7 @@ class RenderDialog(Loggable):
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)
+ 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]
diff --git a/pitivi/settings.py b/pitivi/settings.py
index 7737362..0381a9b 100644
--- a/pitivi/settings.py
+++ b/pitivi/settings.py
@@ -20,13 +20,10 @@
# Boston, MA 02110-1301, USA.
import os
-from gi.repository import Gst
from ConfigParser import SafeConfigParser, ParsingError
import xdg.BaseDirectory as xdg_dirs # Freedesktop directories spec
from pitivi.utils.signal import Signallable
-from pitivi.render import available_combinations, get_compatible_sink_caps
-from pitivi.utils.loggable import Loggable
def get_bool_env(var):
@@ -329,212 +326,3 @@ class GlobalSettings(Signallable):
if section in cls.options:
raise ConfigError("Duplicate Section \"%s\"." % section)
cls.options[section] = {}
-
-
-class MultimediaSettings(Signallable, Loggable):
- """
- Multimedia rendering and previewing settings
-
- Signals:
- 'settings-changed' : the settings have changed
- 'encoders-changed' : The encoders or muxer have changed
-
- @ivar render_scale: The scale to be applied to the video width and height
- when rendering.
- @type render_scale: int
- """
- __signals__ = {
- "settings-changed": None,
- "encoders-changed": None,
- }
-
- # Audio/Video settings for processing/export
-
- # TODO : Add PAR/DAR for video
- # TODO : switch to using GstFraction internally where appliable
-
- muxers, aencoders, vencoders = available_combinations()
-
- def __init__(self, **unused_kw):
- Loggable.__init__(self)
- self.videowidth = 720
- self.videoheight = 576
- self.render_scale = 100
- self.videorate = Gst.Fraction(25, 1)
- self.videopar = Gst.Fraction(16, 15)
- self.audiochannels = 2
- self.audiorate = 44100
- self.audiodepth = 16
- self.vencoder = None
- self.aencoder = None
- self.muxer = "oggmux"
- # A (muxer -> containersettings) map.
- self._containersettings_cache = {}
- # A (vencoder -> vcodecsettings) map.
- self._vcodecsettings_cache = {}
- # A (aencoder -> acodecsettings) map.
- self._acodecsettings_cache = {}
-
- def copy(self):
- ret = MultimediaSettings()
- ret.videowidth = self.videowidth
- ret.videoheight = self.videoheight
- ret.render_scale = self.render_scale
- ret.videorate = Gst.Fraction(self.videorate.num, self.videorate.denom)
- ret.videopar = Gst.Fraction(self.videopar.num, self.videopar.denom)
- ret.audiochannels = self.audiochannels
- ret.audiorate = self.audiorate
- ret.audiodepth = self.audiodepth
- ret.vencoder = self.vencoder
- ret.aencoder = self.aencoder
- ret.muxer = self.muxer
- ret.containersettings = dict(self.containersettings)
- ret.acodecsettings = dict(self.acodecsettings)
- ret.vcodecsettings = dict(self.vcodecsettings)
- return ret
-
- def getDAR(self):
- return Gst.Fraction(self.videowidth, self.videoheight) * self.videopar
-
- def __str__(self):
- """
- Redefine __str__ to allow printing the project audio/video settings.
- This is used for debugging, do not make these strings translatable.
- """
- msg = "\n\n"
- msg += "\tVideo: " + str(self.videowidth) + "x" + str(self.videoheight) +\
- " " + str(self.videorate) + " fps, " + str(self.videopar) + " PAR"
- msg += "\n\t\tEncoder: " + str(self.vencoder)
- if self.vcodecsettings:
- msg += "\n\t\tCodec settings: " + str(self.vcodecsettings)
- msg += "\n\tAudio: " + str(self.audiochannels) + " channels, " +\
- str(self.audiorate) + " Hz, " + str(self.audiodepth) + " bits"
- msg += "\n\t\tEncoder: " + str(self.aencoder)
- if self.acodecsettings:
- msg += "\n\t\tCodec settings: " + str(self.acodecsettings)
- msg += "\n\tMuxer: " + str(self.muxer)
- if self.containersettings:
- msg += "\n\t\t" + str(self.containersettings)
- msg += "\n\n"
- return msg
-
- def getVideoWidthAndHeight(self, render=False):
- """ Returns the video width and height as a tuple
-
- @param render: Whether to apply self.render_scale to the returned values
- @type render: bool
- """
- if render:
- scale = self.render_scale
- else:
- scale = 100
- return self.videowidth * scale / 100, self.videoheight * scale / 100
-
- def getVideoCaps(self, render=False):
- """ Returns the GstCaps corresponding to the video settings """
- videowidth, videoheight = self.getVideoWidthAndHeight(render=render)
- vstr = "width=%d,height=%d,pixel-aspect-ratio=%d/%d,framerate=%d/%d" % (
- videowidth, videoheight,
- self.videopar.num, self.videopar.denom,
- self.videorate.num, self.videorate.denom)
- caps_str = "video/x-raw,%s" % (vstr)
- video_caps = Gst.caps_from_string(caps_str)
- if self.vencoder:
- return get_compatible_sink_caps(self.vencoder, video_caps)
- return video_caps
-
- def getAudioCaps(self):
- """ Returns the GstCaps corresponding to the audio settings """
- # TODO: Figure out why including 'depth' causes pipeline failures:
- astr = "rate=%d,channels=%d" % (self.audiorate, self.audiochannels)
- caps_str = "audio/x-raw,%s" % (astr)
- audio_caps = Gst.caps_from_string(caps_str)
- if self.aencoder:
- return get_compatible_sink_caps(self.aencoder, audio_caps)
- return audio_caps
-
- def setVideoProperties(self, width=-1, height=-1, framerate=-1, par=-1,
- render_scale=-1):
- """ Set the video width, height and framerate """
- self.info("set_video_props %d x %d @ %r fps", width, height, framerate)
- changed = False
- if not width == -1 and not width == self.videowidth:
- self.videowidth = width
- changed = True
- if not height == -1 and not height == self.videoheight:
- self.videoheight = height
- changed = True
- if not render_scale == -1 and not render_scale == self.render_scale:
- self.render_scale = render_scale
- changed = True
- if not framerate == -1 and not framerate == self.videorate:
- self.videorate = framerate
- changed = True
- if not par == -1 and not par == self.videopar:
- self.videopar = par
- changed = True
- if changed:
- self.emit("settings-changed")
-
- def setAudioProperties(self, nbchanns=-1, rate=-1, depth=-1):
- """ Set the number of audio channels, rate and depth """
- self.info("%d x %dHz %dbits", nbchanns, rate, depth)
- changed = False
- if not nbchanns == -1 and not nbchanns == self.audiochannels:
- self.audiochannels = nbchanns
- changed = True
- if not rate == -1 and not rate == self.audiorate:
- self.audiorate = rate
- changed = True
- if not depth == -1 and not depth == self.audiodepth:
- self.audiodepth = depth
- changed = True
- if changed:
- self.emit("settings-changed")
-
- def setEncoders(self, muxer="", vencoder="", aencoder=""):
- """ Set the video/audio encoder and muxer """
- changed = False
- if not muxer == "" and not muxer == self.muxer:
- self.muxer = muxer
- changed = True
- if not vencoder == "" and not vencoder == self.vencoder:
- self.vencoder = vencoder
- changed = True
- if not aencoder == "" and not aencoder == self.aencoder:
- self.aencoder = aencoder
- changed = True
- if changed:
- self.emit("encoders-changed")
-
- @property
- def containersettings(self):
- return self._containersettings_cache.setdefault(self.muxer, {})
-
- @containersettings.setter
- def containersettings(self, value):
- self._containersettings_cache[self.muxer] = value
-
- @property
- def vcodecsettings(self):
- return self._vcodecsettings_cache.setdefault(self.vencoder, {})
-
- @vcodecsettings.setter
- def vcodecsettings(self, value):
- self._vcodecsettings_cache[self.vencoder] = value
-
- @property
- def acodecsettings(self):
- return self._acodecsettings_cache.setdefault(self.aencoder, {})
-
- @acodecsettings.setter
- def acodecsettings(self, value):
- self._acodecsettings_cache[self.aencoder] = value
-
- def getAudioEncoders(self):
- """ List audio encoders compatible with the current muxer """
- return self.aencoders[self.muxer]
-
- def getVideoEncoders(self):
- """ List video encoders compatible with the current muxer """
- return self.vencoders[self.muxer]
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py
index a5ecba1..d58a899 100644
--- a/pitivi/timeline/timeline.py
+++ b/pitivi/timeline/timeline.py
@@ -545,6 +545,7 @@ class TimelineControls(Gtk.VBox, Loggable):
timeline = property(getTimeline, setTimeline, None, "The timeline property")
def _layerAddedCb(self, timeline, layer):
+ self.debug("Layer %s added", layer)
video_control = VideoLayerControl(self.app, layer)
audio_control = AudioLayerControl(self.app, layer)
@@ -945,13 +946,9 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
self._framerate = Gst.Fraction(1, 1)
self._timeline = None
- # Used to insert sources at the end of the timeline
- self._sources_to_insert = []
-
self.zoomed_fitted = True
# Timeline edition related fields
- self._creating_tckobjs_sigid = {}
self._move_context = None
self._project = None
@@ -1173,11 +1170,11 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
if not self._drag_started:
self._drag_started = True
elif context.list_targets() not in DND_EFFECT_LIST and self.app.gui.medialibrary.dragged:
- if not self._temp_objects and not self._creating_tckobjs_sigid:
+ if not self._temp_objects:
self._create_temp_source(x, y)
# Let some time for TrackObject-s to be created
- if self._temp_objects and not self._creating_tckobjs_sigid:
+ if self._temp_objects:
focus = self._temp_objects[0]
if self._move_context is None:
self._move_context = EditingContext(focus,
@@ -1204,10 +1201,6 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
If the user drags outside the timeline,
remove the temporary objects we had created during the drap operation.
"""
- # If TrackObject-s still being created, wait before deleting
- if self._creating_tckobjs_sigid:
- return True
-
# Clean up only if clip was not dropped already
if self._drag_started:
self.debug("Drag cleanup")
@@ -1361,31 +1354,19 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
drag-and-drop operation.
"""
layer = self._ensureLayer()[0]
- duration = 0
-
- for uri in self.app.gui.medialibrary.getSelectedItems():
- info = self._project.medialibrary.getInfoFromUri(uri)
- src = GES.TimelineFileSource(uri=uri)
- src.props.start = duration
- # Set image duration
- # FIXME: after GES Materials are merged, check if image instead
- if src.props.duration == 0:
- src.set_duration(long(self._settings.imageClipLength) * Gst.SECOND / 1000)
- duration += info.get_duration()
- layer.add_object(src)
- id = src.connect("track-object-added", self._trackObjectsCreatedCb, src, x, y)
- self._creating_tckobjs_sigid[src] = id
-
- def _trackObjectsCreatedCb(self, unused_tl, track_object, tlobj, x, y):
- # Make sure not to start the moving process before the TrackObject-s
- # are created. We concider that the time between the different
- # TrackObject-s creation is short enough so we are all good when the
- # first TrackObject is added to the TimelineObject
- if tlobj.is_image():
- tlobj.set_duration(long(self._settings.imageClipLength) * Gst.SECOND / 1000)
- self._temp_objects.insert(0, tlobj)
- tlobj.disconnect(self._creating_tckobjs_sigid[tlobj])
- del self._creating_tckobjs_sigid[tlobj]
+ start = 0
+
+ for asset in self.app.gui.medialibrary.getSelectedAssets():
+ if asset.is_image():
+ clip_duration = long(long(self._settings.imageClipLength) * Gst.SECOND / 1000)
+ else:
+ clip_duration = asset.get_duration()
+
+ source = layer.add_asset(asset, start, 0,
+ clip_duration, 1.0, asset.get_supported_types())
+
+ self._temp_objects.insert(0, source)
+ start += asset.get_duration()
def _move_temp_source(self, x, y):
x = self.hadj.props.value + x
@@ -1544,11 +1525,12 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
timeline_duration = duration + Gst.SECOND - 1
timeline_duration_s = int(timeline_duration / Gst.SECOND)
- self.debug("duration: %s, timeline duration: %s" % (duration,
+ self.debug("duration: %s, timeline duration: %s" % (print_ns(duration),
print_ns(timeline_duration)))
ideal_zoom_ratio = float(ruler_width) / timeline_duration_s
nearest_zoom_level = Zoomable.computeZoomLevel(ideal_zoom_ratio)
+ self.debug("Ideal zoom: %s, nearest_zoom_level %s", ideal_zoom_ratio, nearest_zoom_level)
Zoomable.setZoomLevel(nearest_zoom_level)
self.timeline.props.snapping_distance = \
Zoomable.pixelToNs(self.app.settings.edgeSnapDeadband)
@@ -1559,35 +1541,43 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
## Project callbacks
- def _projectChangedCb(self, app, project):
+ def _projectChangedCb(self, app, project, unused_fully_loaded):
"""
- When a new blank project is created, immediately clear the timeline.
-
- Otherwise, we would sit around until a clip gets imported to the
- media library, waiting for a "ready" signal.
+ When a project is loaded, we connect to its pipeline
"""
- self.debug("New blank project created, pre-emptively clearing the timeline")
- self.setProject(project)
+ self.debug("Project changed")
+
+ if project:
+ self.debug("Project is not None, connecting to its pipeline")
+ self._seeker = self._project.seeker
+ self._pipeline = self._project.pipeline
+ self._pipeline.connect("position", self.positionChangedCb)
+ self.ruler.setProjectFrameRate(self._project.videorate)
+ self.ruler.zoomChanged()
+ self._renderingSettingsChangedCb(self._project, None, None)
+
+ self._setBestZoomRatio()
- def setProject(self, project):
+ def _projectCreatedCb(self, app, project):
+ """
+ When a project is created, we connect to it timeline
+ """
self.debug("Setting project %s", project)
if self._project:
- self._project.disconnect_by_function(self._settingsChangedCb)
- self._pipeline.disconnect_by_func(self.positionChangedCb)
- self.pipeline = None
+ self._project.disconnect_by_func(self._renderingSettingsChangedCb)
+ try:
+ self._pipeline.disconnect_by_func(self.positionChangedCb)
+ except TypeError:
+ pass # We were not connected no problem
+
+ self._pipeline = None
+ self._seeker = None
self._project = project
if self._project:
+ self._project.connect("rendering-settings-changed",
+ self._renderingSettingsChangedCb)
self.setTimeline(project.timeline)
- self.ruler.setProjectFrameRate(self._project.getSettings().videorate)
- self.ruler.zoomChanged()
- self._settingsChangedCb(self._project, None, self._project.getSettings())
-
- self._seeker = self._project.seeker
- self._pipeline = self._project.pipeline
- self._pipeline.connect("position", self.positionChangedCb)
- self._project.connect("settings-changed", self._settingsChangedCb)
- self._setBestZoomRatio()
def setProjectManager(self, projectmanager):
if self._projectmanager is not None:
@@ -1595,11 +1585,22 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
self._projectmanager = projectmanager
if projectmanager is not None:
+ projectmanager.connect("new-project-created", self._projectCreatedCb)
projectmanager.connect("new-project-loaded", self._projectChangedCb)
- def _settingsChangedCb(self, project, old, new):
- self._framerate = new.videorate
- self.ruler.setProjectFrameRate(self._framerate)
+ def _renderingSettingsChangedCb(self, project, item, value):
+ """
+ Called when any Project metadata changes, we filter out the one
+ we are interested in.
+
+ if @item is None, it mean we called it ourself, and want to force
+ getting the project videorate value
+ """
+ if item == "videorate" or item is None:
+ if value is None:
+ value = project.videorate
+ self._framerate = value
+ self.ruler.setProjectFrameRate(self._framerate)
## Timeline callbacks
@@ -1839,66 +1840,22 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
path, mime = foo[0], foo[1]
self._project.pipeline.save_thumbnail(-1, -1, mime, path)
- def insertEnd(self, sources):
+ def insertEnd(self, assets):
"""
Add source at the end of the timeline
@type sources: An L{GES.TimelineSource}
@param x2: A list of sources to add to the timeline
"""
self.app.action_log.begin("add clip")
+ # FIXME we should find the longets layer instead of adding it to the
+ # first one
# Handle the case of a blank project
- self._ensureLayer()
- self._sources_to_insert = sources
-
- # Start adding sources in the timeline
- self._insertNextSource()
-
- def _insertNextSource(self):
- """ Insert a source at the end of the timeline's first track """
- timeline = self.app.current.timeline
-
- if not self._sources_to_insert:
- # We need to wait (100ms is enoug for sure) for TrackObject-s to
- # be added to the Tracks
- # FIXME remove this "hack" when Materials are merged
- GObject.timeout_add(100, self._finalizeSourceAdded)
- self.app.action_log.commit()
-
- # Update zoom level if needed
- return
- source = self._sources_to_insert.pop()
- layer = timeline.get_layers()[0] # FIXME Get the longest layer
- # Waiting for the TrackObject to be created because of a race
- # condition, and to know the real length of the timeline when
- # adding several sources at a time.
- # connecting before adding, as it signaled before connection
- source.connect("track-object-added", self._trackObjectAddedCb)
- layer.add_object(source)
-
- def _trackObjectAddedCb(self, source, trackobj):
- """ After an object has been added to the first track, position it
- correctly and request the next source to be processed. """
- timeline = self.app.current.timeline
- layer = timeline.get_layers()[0] # FIXME Get the longest layer
-
- # Set the duration of the clip if it is an image
- if hasattr(source, "is_image") and source.is_image():
- source.set_duration(long(self._settings.imageClipLength) * Gst.SECOND / 1000)
-
- # Handle the case where we just inserted the first clip
- if len(layer.get_objects()) == 1:
- source.props.start = 0
- else:
- source.props.start = timeline.props.duration
-
- # We only need one TrackObject to estimate the new duration.
- # Process the next source.
- source.disconnect_by_func(self._trackObjectAddedCb)
- self._insertNextSource()
+ layer = self._ensureLayer()[0]
+ for asset in assets:
+ if asset.is_image():
+ clip_duration = long(long(self._settings.imageClipLength) * Gst.SECOND / 1000)
+ else:
+ clip_duration = asset.get_duration()
- def _finalizeSourceAdded(self):
- timeline = self.app.current.timeline
- self.app.current.seeker.seek(timeline.props.duration)
- if self.zoomed_fitted is True:
- self._setBestZoomRatio()
- return False
+ layer.add_asset(asset, self.timeline.props.duration,
+ 0, clip_duration, 1.0, asset.get_supported_types())
diff --git a/pitivi/timeline/track.py b/pitivi/timeline/track.py
index 967413a..cd2f609 100644
--- a/pitivi/timeline/track.py
+++ b/pitivi/timeline/track.py
@@ -763,8 +763,7 @@ class TrackFileSource(TrackObject):
Set the human-readable file name as the clip's text label
"""
if self.element:
- uri = self.element.props.uri
- info = self.app.current.medialibrary.getInfoFromUri(uri)
+ info = element.get_timeline_object().get_asset().get_info()
self.name.props.text = info_name(info)
twidth, theight = text_size(self.name)
self.namewidth = twidth
diff --git a/pitivi/transitions.py b/pitivi/transitions.py
index ceeec27..f8f58bc 100644
--- a/pitivi/transitions.py
+++ b/pitivi/transitions.py
@@ -35,7 +35,7 @@ from pitivi.utils.loggable import Loggable
from pitivi.utils.signal import Signallable
from pitivi.utils.ui import SPACING, PADDING
-(COL_TRANSITION_ID,
+(COL_TRANSITION_ASSET,
COL_NAME_TEXT,
COL_DESC_TEXT,
COL_ICON) = range(4)
@@ -100,7 +100,7 @@ class TransitionsListWidget(Signallable, Gtk.VBox, Loggable):
self.infobar.add(txtlabel)
self.infobar.show_all()
- self.storemodel = Gtk.ListStore(str, str, str, GdkPixbuf.Pixbuf)
+ self.storemodel = Gtk.ListStore(GES.Asset, str, str, GdkPixbuf.Pixbuf)
self.iconview_scrollwin = Gtk.ScrolledWindow()
self.iconview_scrollwin.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
@@ -150,14 +150,13 @@ class TransitionsListWidget(Signallable, Gtk.VBox, Loggable):
# UI callbacks
def _transitionSelectedCb(self, event):
- selected_item = self.getSelectedItem()
- if not selected_item:
- # The user clicked between icons
+ transition_asset = self.getSelectedItem()
+ if not transition_asset:
+ # The user clicked between icons
return False
- transition_id = int(selected_item)
- transition = self.available_transitions.get(transition_id)
- self.debug("New transition type selected: %s" % transition)
- if transition.value_nick == "crossfade":
+
+ self.debug("New transition type selected: %s" % transition_asset.get_id())
+ if transition_asset.get_id() == "crossfade":
self.props_widgets.set_sensitive(False)
else:
self.props_widgets.set_sensitive(True)
@@ -166,7 +165,7 @@ class TransitionsListWidget(Signallable, Gtk.VBox, Loggable):
position = self.app.current.pipeline.getPosition()
self.app.current.pipeline.simple_seek(0)
- self.element.set_transition_type(transition)
+ self.element.get_timeline_object().set_asset(transition_asset)
# Seek back into the previous position, refreshing the preview
self.app.current.pipeline.simple_seek(position)
@@ -219,13 +218,12 @@ class TransitionsListWidget(Signallable, Gtk.VBox, Loggable):
# GES callbacks
def _transitionTypeChangedCb(self, element, unused_prop):
- transition = element.get_transition_type()
try:
self.iconview.disconnect_by_func(self._transitionSelectedCb)
except TypeError:
pass
finally:
- self.selectTransition(transition)
+ self.selectTransition(element.get_asset())
self.iconview.connect("button-release-event", self._transitionSelectedCb)
def _borderChangedCb(self, element, unused_prop):
@@ -260,27 +258,17 @@ class TransitionsListWidget(Signallable, Gtk.VBox, Loggable):
"""
Get the list of transitions from GES and load the associated thumbnails.
"""
- # TODO: rewrite this method when GESRegistry exists
- self.available_transitions = {}
- # GES currently has transitions IDs up to 512
- # Number 0 means "no transition", so we might just as well skip it.
- for i in range(1, 513):
- try:
- transition = GES.VideoStandardTransitionType(i)
- except ValueError:
- # We hit a gap in the enum
- pass
- else:
- self.available_transitions[transition.numerator] = transition
- self.storemodel.append([str(transition.numerator),
- str(transition.value_nick),
- str(transition.value_name),
- self._getIcon(transition.value_nick)])
+ for trans_asset in GES.list_assets(GES.TimelineTransition):
+ trans_asset.icon = self._getIcon(trans_asset.get_id())
+ self.storemodel.append([trans_asset,
+ str(trans_asset.get_id()),
+ str(trans_asset.get_meta(GES.META_DESCRIPTION)),
+ trans_asset.icon])
# Now that the UI is fully ready, enable searching
self.modelFilter.set_visible_func(self._setRowVisible, data=None)
# Alphabetical/name sorting instead of based on the ID number
- #self.storemodel.set_sort_column_id(COL_NAME_TEXT, Gtk.SortType.ASCENDING)
+ self.storemodel.set_sort_column_id(COL_NAME_TEXT, Gtk.SortType.ASCENDING)
def activate(self, element):
"""
@@ -290,8 +278,8 @@ class TransitionsListWidget(Signallable, Gtk.VBox, Loggable):
self.element.connect("notify::border", self._borderChangedCb)
self.element.connect("notify::invert", self._invertChangedCb)
self.element.connect("notify::type", self._transitionTypeChangedCb)
- transition = element.get_transition_type()
- if transition.value_nick == "crossfade":
+ transition_asset = element.get_timeline_object().get_asset()
+ if transition_asset.get_id() == "crossfade":
self.props_widgets.set_sensitive(False)
else:
self.props_widgets.set_sensitive(True)
@@ -299,16 +287,16 @@ class TransitionsListWidget(Signallable, Gtk.VBox, Loggable):
self.props_widgets.show_all()
self.infobar.hide()
self.searchbar.show_all()
- self.selectTransition(transition)
+ self.selectTransition(transition_asset)
self.app.gui.switchContextTab("transitions")
- def selectTransition(self, transition):
+ def selectTransition(self, transition_asset):
"""
For a given transition type, select it in the iconview if available.
"""
model = self.iconview.get_model()
for row in model:
- if int(transition.numerator) == int(row[COL_TRANSITION_ID]):
+ if transition_asset == row[COL_TRANSITION_ASSET]:
path = model.get_path(row.iter)
self.iconview.select_path(path)
self.iconview.scroll_to_path(path, False, 0, 0)
@@ -354,7 +342,7 @@ class TransitionsListWidget(Signallable, Gtk.VBox, Loggable):
view.set_tooltip_item(tooltip, path)
- name = model.get_value(iter_, COL_TRANSITION_ID)
+ name = model.get_value(iter_, COL_TRANSITION_ASSET).get_id()
if self._current_transition_name != name:
self._current_transition_name = name
icon = model.get_value(iter_, COL_ICON)
@@ -371,7 +359,7 @@ class TransitionsListWidget(Signallable, Gtk.VBox, Loggable):
path = self.iconview.get_selected_items()
if path == []:
return None
- return self.modelFilter[path[0]][COL_TRANSITION_ID]
+ return self.modelFilter[path[0]][COL_TRANSITION_ASSET]
def _setRowVisible(self, model, iter, data):
"""
diff --git a/pitivi/utils/ui.py b/pitivi/utils/ui.py
index 70f2de9..c8b2bd5 100644
--- a/pitivi/utils/ui.py
+++ b/pitivi/utils/ui.py
@@ -27,6 +27,7 @@ classes that help with UI drawing around the application
"""
import glib
from gi.repository import Gst
+from gi.repository import GES
from gi.repository import Gtk
import os
import cairo
@@ -190,7 +191,10 @@ def beautify_info(info):
def info_name(info):
"""Return a human-readable filename (without the path and quoting)."""
- filename = unquote(os.path.basename(info.get_uri()))
+ if isinstance(info, GES.Asset):
+ filename = unquote(os.path.basename(info.get_id()))
+ else:
+ filename = unquote(os.path.basename(info.get_uri()))
return glib.markup_escape_text(filename)
diff --git a/pitivi/viewer.py b/pitivi/viewer.py
index 49c920a..40586ea 100644
--- a/pitivi/viewer.py
+++ b/pitivi/viewer.py
@@ -29,7 +29,6 @@ from gi.repository import GES
GdkX11 # pyflakes
from gi.repository import GstVideo
GstVideo # pyflakes
-from gi.repository import GdkPixbuf
import cairo
from gettext import gettext as _
diff --git a/tests/test_settings.py b/tests/test_settings.py
index 2a13630..fa6ef6e 100644
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -1,12 +1,12 @@
import unittest
-from pitivi.settings import MultimediaSettings
+from pitivi.project import Project
class TestExportSettings(unittest.TestCase):
- """Test the settings.MultimediaSettings class."""
+ """Test the project.MultimediaSettings class."""
def setUp(self):
- self.settings = MultimediaSettings()
+ self.project = Project()
def testMasterAttributes(self):
self._testMasterAttribute('muxer', dependant_attr='containersettings')
@@ -18,30 +18,30 @@ class TestExportSettings(unittest.TestCase):
attr_value1 = "%s_value1" % attr
attr_value2 = "%s_value2" % attr
- setattr(self.settings, attr, attr_value1)
- setattr(self.settings, dependant_attr, {})
- getattr(self.settings, dependant_attr)["key1"] = "v1"
-
- setattr(self.settings, attr, attr_value2)
- setattr(self.settings, dependant_attr, {})
- getattr(self.settings, dependant_attr)["key2"] = "v2"
-
- setattr(self.settings, attr, attr_value1)
- self.assertTrue("key1" in getattr(self.settings, dependant_attr))
- self.assertFalse("key2" in getattr(self.settings, dependant_attr))
- self.assertEqual("v1", getattr(self.settings, dependant_attr)["key1"])
- setattr(self.settings, dependant_attr, {})
-
- setattr(self.settings, attr, attr_value2)
- self.assertFalse("key1" in getattr(self.settings, dependant_attr))
- self.assertTrue("key2" in getattr(self.settings, dependant_attr))
- self.assertEqual("v2", getattr(self.settings, dependant_attr)["key2"])
- setattr(self.settings, dependant_attr, {})
-
- setattr(self.settings, attr, attr_value1)
- self.assertFalse("key1" in getattr(self.settings, dependant_attr))
- self.assertFalse("key2" in getattr(self.settings, dependant_attr))
-
- setattr(self.settings, attr, attr_value2)
- self.assertFalse("key1" in getattr(self.settings, dependant_attr))
- self.assertFalse("key2" in getattr(self.settings, dependant_attr))
+ setattr(self.project, attr, attr_value1)
+ setattr(self.project, dependant_attr, {})
+ getattr(self.project, dependant_attr)["key1"] = "v1"
+
+ setattr(self.project, attr, attr_value2)
+ setattr(self.project, dependant_attr, {})
+ getattr(self.project, dependant_attr)["key2"] = "v2"
+
+ setattr(self.project, attr, attr_value1)
+ self.assertTrue("key1" in getattr(self.project, dependant_attr))
+ self.assertFalse("key2" in getattr(self.project, dependant_attr))
+ self.assertEqual("v1", getattr(self.project, dependant_attr)["key1"])
+ setattr(self.project, dependant_attr, {})
+
+ setattr(self.project, attr, attr_value2)
+ self.assertFalse("key1" in getattr(self.project, dependant_attr))
+ self.assertTrue("key2" in getattr(self.project, dependant_attr))
+ self.assertEqual("v2", getattr(self.project, dependant_attr)["key2"])
+ setattr(self.project, dependant_attr, {})
+
+ setattr(self.project, attr, attr_value1)
+ self.assertFalse("key1" in getattr(self.project, dependant_attr))
+ self.assertFalse("key2" in getattr(self.project, dependant_attr))
+
+ setattr(self.project, attr, attr_value2)
+ self.assertFalse("key1" in getattr(self.project, dependant_attr))
+ self.assertFalse("key2" in getattr(self.project, dependant_attr))
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]