From 7fbbd417e50c0ad06070dba975502d5537812bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandru=20B=C4=83lu=C8=9B?= Date: Wed, 16 Jan 2019 09:23:58 +0100 Subject: [PATCH 1/5] viewer: Improve trim preview precision The `clipTrimPreview` method is called whenever the clip trim updates, and until now it was ignoring updates if the last performed seek was too recent. This means the trim preview was up to 200ms old when the mouse stopped moving. --- pitivi/viewer/viewer.py | 43 ++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/pitivi/viewer/viewer.py b/pitivi/viewer/viewer.py index aa727befc..492d1bf46 100644 --- a/pitivi/viewer/viewer.py +++ b/pitivi/viewer/viewer.py @@ -16,8 +16,8 @@ # License along with this program; if not, write to the # Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, # Boston, MA 02110-1301, USA. +import time from gettext import gettext as _ -from time import time from gi.repository import Gdk from gi.repository import GES @@ -61,6 +61,10 @@ GlobalSettings.addConfigOption("pointColor", section="viewer", default='49a0e0') +# The trim preview is updated at most once per this interval. +TRIM_PREVIEW_UPDATE_INTERVAL_MS = 200 + + class ViewerContainer(Gtk.Box, Loggable): """Wiget holding a viewer and the controls. @@ -90,6 +94,11 @@ class ViewerContainer(Gtk.Box, Loggable): self.target = None self._compactMode = False + # When was the last seek performed while previewing a clip trim. + self._last_trim_ns = 0 + # The delayed seek timeout ID, in case the last seek is too recent. + self.__trim_seek_id = 0 + # Only used for restoring the pipeline position after a live clip trim # preview: self._oldTimelinePos = None @@ -519,7 +528,8 @@ class ViewerContainer(Gtk.Box, Loggable): return False clip_uri = clip.props.uri - cur_time = time() + time_ns = time.monotonic_ns() + if self.pipeline == self.app.project_manager.current_project.pipeline: self.debug("Creating temporary pipeline for clip %s, position %s", clip_uri, format_ns(position)) @@ -527,15 +537,34 @@ class ViewerContainer(Gtk.Box, Loggable): self.pipeline.set_state(Gst.State.NULL) self.setPipeline(AssetPipeline(clip)) self.__owning_pipeline = True - self._lastClipTrimTime = cur_time - if (cur_time - self._lastClipTrimTime) > 0.2 and self.pipeline.getState() == Gst.State.PAUSED: - # Do not seek more than once every 200 ms (for performance) - self.pipeline.simple_seek(position) - self._lastClipTrimTime = cur_time + if self.__trim_seek_id: + # A seek is scheduled. Update the position where it will be done. + self.__trim_seek_position = position + else: + # Avoid seeking too often, for performance. + delta_ns = time_ns - self._last_trim_ns + if delta_ns > TRIM_PREVIEW_UPDATE_INTERVAL_MS * 1000 * 1000: + # The last seek is not recent, we can seek right away. + self.pipeline.simple_seek(position) + self._last_trim_ns = time_ns + else: + # The previous seek was too recent. Schedule a seek at this position. + self.__trim_seek_position = position + self.__trim_seek_id = GLib.timeout_add(delta_ns / 1000 / 1000, + self.__trim_seek_timeout_cb, None) + + def __trim_seek_timeout_cb(self, unused_data): + self.__trim_seek_id = 0 + self.pipeline.simple_seek(self.__trim_seek_position) + self._last_trim_ns = time.monotonic_ns() + return False def clipTrimPreviewFinished(self): """Switches back to the project pipeline following a clip trimming.""" + if self.__trim_seek_id: + GLib.source_remove(self.__trim_seek_id) + self.__trim_seek_id = 0 if self.pipeline is not self.app.project_manager.current_project.pipeline: self.debug("Going back to the project's pipeline") self.pipeline.setState(Gst.State.NULL) -- GitLab From c6f2b55bc416e9f2e578d93e9e49c2c3a1a1b722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandru=20B=C4=83lu=C8=9B?= Date: Tue, 15 Jan 2019 04:36:20 +0100 Subject: [PATCH 2/5] viewer: Avoid messing with the project pipeline when previewing trims Once the viewer container is connected to a project, it does not even have to keep a reference to the project pipeline, now that it keeps a separate reference to the temporary AssetPipeline used when trimming. Gets rid of a few fields, and avoids setting the project pipeline to state NULL, thus avoiding a flicker when switching state from NULL to PAUSED. --- pitivi/viewer/viewer.py | 134 +++++++++++++++++++--------------------- 1 file changed, 64 insertions(+), 70 deletions(-) diff --git a/pitivi/viewer/viewer.py b/pitivi/viewer/viewer.py index 492d1bf46..59dfd291b 100644 --- a/pitivi/viewer/viewer.py +++ b/pitivi/viewer/viewer.py @@ -28,7 +28,6 @@ from gi.repository import Gtk from pitivi.settings import GlobalSettings from pitivi.utils.loggable import Loggable -from pitivi.utils.misc import format_ns from pitivi.utils.pipeline import AssetPipeline from pitivi.utils.ui import SPACING from pitivi.utils.widgets import TimeWidget @@ -89,7 +88,7 @@ class ViewerContainer(Gtk.Box, Loggable): self.log("New ViewerContainer") self.project = None - self.pipeline = None + self.trim_pipeline = None self.docked = True self.target = None self._compactMode = False @@ -99,15 +98,10 @@ class ViewerContainer(Gtk.Box, Loggable): # The delayed seek timeout ID, in case the last seek is too recent. self.__trim_seek_id = 0 - # Only used for restoring the pipeline position after a live clip trim - # preview: - self._oldTimelinePos = None - self._haveUI = False self._createUi() - self.__owning_pipeline = False if not self.settings.viewerDocked: self.undock() @@ -121,8 +115,7 @@ class ViewerContainer(Gtk.Box, Loggable): def _project_manager_new_project_loaded_cb(self, unused_project_manager, project): project.connect("rendering-settings-changed", self._project_rendering_settings_changed_cb) - self.project = project - self.setPipeline(project.pipeline) + self.set_project(project) def _projectManagerProjectClosedCb(self, unused_project_manager, project): if self.project == project: @@ -138,16 +131,13 @@ class ViewerContainer(Gtk.Box, Loggable): self.target.update_aspect_ratio(project) self.timecode_entry.setFramerate(project.videorate) - def setPipeline(self, pipeline, position=None): - """Sets the displayed pipeline. - - Properly switches the currently set action to that new Pipeline. + def set_project(self, project): + """Sets the displayed project. Args: - pipeline (Pipeline): The Pipeline to switch to. - position (Optional[int]): The position to seek to initially. + project (Project): The Project to switch to. """ - self.debug("Setting pipeline: %r", pipeline) + self.debug("Setting project: %r", project) self._disconnectFromPipeline() if self.target: @@ -155,21 +145,20 @@ class ViewerContainer(Gtk.Box, Loggable): if parent: parent.remove(self.target) - self.pipeline = pipeline - self.pipeline.connect("state-change", self._pipelineStateChangedCb) - self.pipeline.connect("position", self._positionCb) - self.pipeline.connect("duration-changed", self._durationChangedCb) + project.pipeline.connect("state-change", self._pipelineStateChangedCb) + project.pipeline.connect("position", self._positionCb) + project.pipeline.connect("duration-changed", self._durationChangedCb) + self.project = project - self.__owning_pipeline = False self.__createNewViewer() self._setUiActive() - if position: - self.pipeline.simple_seek(position) - self.pipeline.pause() + # This must be done at the end, otherwise the created sink widget + # appears in a separate window. + project.pipeline.pause() def __createNewViewer(self): - _, sink_widget = self.pipeline.create_sink() + _, sink_widget = self.project.pipeline.create_sink() self.overlay_stack = OverlayStack(self.app, sink_widget) self.target = ViewerWidget(self.overlay_stack) @@ -188,18 +177,14 @@ class ViewerContainer(Gtk.Box, Loggable): GLib.timeout_add(1000, self.__viewer_realization_done_cb, None) def _disconnectFromPipeline(self): - if self.pipeline is None: - # silently return, there's nothing to disconnect from + if self.project is None: return - self.debug("Disconnecting from: %r", self.pipeline) - self.pipeline.disconnect_by_func(self._pipelineStateChangedCb) - self.pipeline.disconnect_by_func(self._positionCb) - self.pipeline.disconnect_by_func(self._durationChangedCb) - - if self.__owning_pipeline: - self.pipeline.release() - self.pipeline = None + pipeline = self.project.pipeline + self.debug("Disconnecting from: %r", pipeline) + pipeline.disconnect_by_func(self._pipelineStateChangedCb) + pipeline.disconnect_by_func(self._positionCb) + pipeline.disconnect_by_func(self._durationChangedCb) def _setUiActive(self, active=True): self.debug("active %r", active) @@ -438,10 +423,10 @@ class ViewerContainer(Gtk.Box, Loggable): self.settings.viewerDocked = False self.remove(self.buttons_container) position = None - if self.pipeline: + if self.project: self.overlay_stack.enable_resize_status(False) - position = self.pipeline.getPosition() - self.pipeline.setState(Gst.State.NULL) + position = self.project.pipeline.getPosition() + self.project.pipeline.setState(Gst.State.NULL) self.remove(self.target) self.__createNewViewer() self.buttons_container.set_margin_bottom(SPACING) @@ -462,9 +447,9 @@ class ViewerContainer(Gtk.Box, Loggable): self.external_window.move(self.settings.viewerX, self.settings.viewerY) self.external_window.resize( self.settings.viewerWidth, self.settings.viewerHeight) - if self.pipeline: - self.pipeline.pause() - self.pipeline.simple_seek(position) + if self.project: + self.project.pipeline.pause() + self.project.pipeline.simple_seek(position) def __viewer_realization_done_cb(self, unused_data): self.overlay_stack.enable_resize_status(True) @@ -479,10 +464,10 @@ class ViewerContainer(Gtk.Box, Loggable): self.settings.viewerDocked = True position = None - if self.pipeline: + if self.project: self.overlay_stack.enable_resize_status(False) - position = self.pipeline.getPosition() - self.pipeline.setState(Gst.State.NULL) + position = self.project.pipeline.getPosition() + self.project.pipeline.setState(Gst.State.NULL) self.external_vbox.remove(self.target) self.__createNewViewer() @@ -494,9 +479,9 @@ class ViewerContainer(Gtk.Box, Loggable): self.show() self.external_window.hide() - if self.pipeline: - self.pipeline.pause() - self.pipeline.simple_seek(position) + if self.project.pipeline: + self.project.pipeline.pause() + self.project.pipeline.simple_seek(position) def _toggleFullscreen(self, widget): if widget.get_active(): @@ -527,26 +512,31 @@ class ViewerContainer(Gtk.Box, Loggable): self.log("Not previewing trim for image or title clip: %s", clip) return False - clip_uri = clip.props.uri - time_ns = time.monotonic_ns() + if self.project.pipeline.getState() == Gst.State.PLAYING: + self.project.pipeline.setState(Gst.State.PAUSED) - if self.pipeline == self.app.project_manager.current_project.pipeline: - self.debug("Creating temporary pipeline for clip %s, position %s", - clip_uri, format_ns(position)) - self._oldTimelinePos = self.pipeline.getPosition(False) - self.pipeline.set_state(Gst.State.NULL) - self.setPipeline(AssetPipeline(clip)) - self.__owning_pipeline = True + if self.trim_pipeline and clip is not self.trim_pipeline.clip: + # Seems to be the trim preview pipeline for a different clip. + self.trim_pipeline.release() + self.trim_pipeline = None + + if not self.trim_pipeline: + self.debug("Creating temporary pipeline for clip %s", clip.props.uri) + self.trim_pipeline = AssetPipeline(clip) + video_sink, sink_widget = self.trim_pipeline.create_sink() + self.target.switch_widget(sink_widget) + self.trim_pipeline.setState(Gst.State.PAUSED) if self.__trim_seek_id: # A seek is scheduled. Update the position where it will be done. self.__trim_seek_position = position else: # Avoid seeking too often, for performance. + time_ns = time.monotonic_ns() delta_ns = time_ns - self._last_trim_ns if delta_ns > TRIM_PREVIEW_UPDATE_INTERVAL_MS * 1000 * 1000: # The last seek is not recent, we can seek right away. - self.pipeline.simple_seek(position) + self.trim_pipeline.simple_seek(position) self._last_trim_ns = time_ns else: # The previous seek was too recent. Schedule a seek at this position. @@ -556,7 +546,7 @@ class ViewerContainer(Gtk.Box, Loggable): def __trim_seek_timeout_cb(self, unused_data): self.__trim_seek_id = 0 - self.pipeline.simple_seek(self.__trim_seek_position) + self.trim_pipeline.simple_seek(self.__trim_seek_position) self._last_trim_ns = time.monotonic_ns() return False @@ -565,16 +555,13 @@ class ViewerContainer(Gtk.Box, Loggable): if self.__trim_seek_id: GLib.source_remove(self.__trim_seek_id) self.__trim_seek_id = 0 - if self.pipeline is not self.app.project_manager.current_project.pipeline: - self.debug("Going back to the project's pipeline") - self.pipeline.setState(Gst.State.NULL) - # Using pipeline.getPosition() here does not work because for some - # reason it's a bit off, that's why we need self._oldTimelinePos. - self.setPipeline( - self.app.project_manager.current_project.pipeline, self._oldTimelinePos) - self._oldTimelinePos = None - - def _pipelineStateChangedCb(self, unused_pipeline, state, old_state): + if not self.trim_pipeline: + return + self.target.switch_widget(self.overlay_stack) + self.trim_pipeline.release() + self.trim_pipeline = None + + def _pipelineStateChangedCb(self, pipeline, state, old_state): """Updates the widgets when the playback starts or stops.""" if state == Gst.State.PLAYING: st = Gst.Structure.new_empty("play") @@ -588,8 +575,8 @@ class ViewerContainer(Gtk.Box, Loggable): if old_state != Gst.State.PAUSED: st = Gst.Structure.new_empty("pause") if old_state == Gst.State.PLAYING: - st.set_value("playback_time", - self.pipeline.getPosition() / Gst.SECOND) + position_seconds = pipeline.getPosition() / Gst.SECOND + st.set_value("playback_time", position_seconds) self.app.write_action(st) self.playpause_button.setPlay() @@ -624,6 +611,13 @@ class ViewerWidget(Gtk.AspectFrame, Loggable): # would show through the non-double-buffered widget! self.hide() + def switch_widget(self, widget): + child = self.get_child() + if child: + self.remove(child) + widget.show_all() + self.add(widget) + def update_aspect_ratio(self, project): """Forces the DAR of the project on the child widget.""" ratio_fraction = project.getDAR() -- GitLab From 340c57587613ae224a1dcf396511f2e72c8d6dc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandru=20B=C4=83lu=C8=9B?= Date: Thu, 17 Jan 2019 00:49:01 +0100 Subject: [PATCH 3/5] viewer: Fix preview flicker when trimming clip When we show the video widget of the asset backing the trimmed clip, the first frame of the asset is shown for a brief moment until the initial seek is performed and the desired frame is shown. This causes an unwanted flicker. We have to prepare the trim preview video widget and switch the project video widget with it only when it's ready. --- pitivi/viewer/viewer.py | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/pitivi/viewer/viewer.py b/pitivi/viewer/viewer.py index 59dfd291b..38b61fcbc 100644 --- a/pitivi/viewer/viewer.py +++ b/pitivi/viewer/viewer.py @@ -304,6 +304,14 @@ class ViewerContainer(Gtk.Box, Loggable): _("Detach the viewer\nYou can re-attach it by closing the newly created window.")) bbox.pack_start(self.undock_button, False, False, 0) + self.show_all() + + # Create a hidden container for the clip trim preview video widget. + self.hidden_chest = Gtk.Frame() + # It has to be added to the window, otherwise when we add + # a video widget to it, it will create a new window! + self.pack_end(self.hidden_chest, False, False, 0) + self._haveUI = True # Identify widgets for AT-SPI, making our test suite easier to develop @@ -317,7 +325,6 @@ class ViewerContainer(Gtk.Box, Loggable): self.undock_button.get_accessible().set_name("undock_button") self.buttons_container = bbox - self.show_all() self.external_vbox.show_all() def __corner_draw_cb(self, unused_widget, cr, lines, space, margin): @@ -523,9 +530,22 @@ class ViewerContainer(Gtk.Box, Loggable): if not self.trim_pipeline: self.debug("Creating temporary pipeline for clip %s", clip.props.uri) self.trim_pipeline = AssetPipeline(clip) - video_sink, sink_widget = self.trim_pipeline.create_sink() - self.target.switch_widget(sink_widget) + unused_video_sink, sink_widget = self.trim_pipeline.create_sink() + # Add the widget to a hidden container and make it appear later + # when it's ready. If we show it before the initial seek completion, + # there is a flicker when the first frame of the asset is shown for + # a brief moment until the initial seek to the frame we actually + # want to show is performed. + # First make sure the container itself is ready. + widget = self.hidden_chest.get_child() + if widget: + self.warning("The previous trim preview video widget should have been removed already") + self.hidden_chest.remove(widget) + self.hidden_chest.add(sink_widget) + sink_widget.show() + self.trim_pipeline.connect("state-change", self._state_change_cb) self.trim_pipeline.setState(Gst.State.PAUSED) + self._last_trim_ns = 0 if self.__trim_seek_id: # A seek is scheduled. Update the position where it will be done. @@ -544,6 +564,20 @@ class ViewerContainer(Gtk.Box, Loggable): self.__trim_seek_id = GLib.timeout_add(delta_ns / 1000 / 1000, self.__trim_seek_timeout_cb, None) + def _state_change_cb(self, trim_pipeline, state, prev_state): + if self.trim_pipeline is not trim_pipeline: + self.warning("State change reported for previous trim preview pipeline") + trim_pipeline.disconnect_by_func(self._state_change_cb) + return + # First the pipeline goes from READY to PAUSED, and then it goes + # from PAUSED to PAUSED, and this is a good moment. + if prev_state == Gst.State.PAUSED and state == Gst.State.PAUSED: + sink_widget = self.hidden_chest.get_child() + if sink_widget: + self.hidden_chest.remove(sink_widget) + self.target.switch_widget(sink_widget) + trim_pipeline.disconnect_by_func(self._state_change_cb) + def __trim_seek_timeout_cb(self, unused_data): self.__trim_seek_id = 0 self.trim_pipeline.simple_seek(self.__trim_seek_position) @@ -615,7 +649,6 @@ class ViewerWidget(Gtk.AspectFrame, Loggable): child = self.get_child() if child: self.remove(child) - widget.show_all() self.add(widget) def update_aspect_ratio(self, project): -- GitLab From 9902e061acb251e55526fe8f09a8b119772b7bcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandru=20B=C4=83lu=C8=9B?= Date: Sun, 20 Jan 2019 01:57:38 +0100 Subject: [PATCH 4/5] pipeline: Simplify AssetPipeline constructor It does not handle clips anymore, because it's easy enough to pass `clip.props.uri` instead. --- pitivi/mediafilespreviewer.py | 6 +++--- pitivi/utils/pipeline.py | 25 ++++++++++++++++++------- pitivi/viewer/viewer.py | 7 ++++--- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/pitivi/mediafilespreviewer.py b/pitivi/mediafilespreviewer.py index a2f266df3..090f6000a 100644 --- a/pitivi/mediafilespreviewer.py +++ b/pitivi/mediafilespreviewer.py @@ -84,7 +84,7 @@ class PreviewWidget(Gtk.Grid, Loggable): self.error_message = None # playbin for play pics - self.player = AssetPipeline(clip=None, name="preview-player") + self.player = AssetPipeline(name="preview-player") self.player.connect('eos', self._pipelineEosCb) self.player.connect('error', self._pipelineErrorCb) self.player._bus.connect('message::tag', self._tag_found_cb) @@ -253,7 +253,7 @@ class PreviewWidget(Gtk.Grid, Loggable): else: self.current_preview_type = 'video' self.preview_image.hide() - self.player.setClipUri(self.current_selected_uri) + self.player.uri = self.current_selected_uri self.player.setState(Gst.State.PAUSED) self.pos_adj.props.upper = duration video_width = video.get_square_width() @@ -289,7 +289,7 @@ class PreviewWidget(Gtk.Grid, Loggable): beautify_stream(audio), _("Duration: %s") % pretty_duration]) self.player.setState(Gst.State.NULL) - self.player.setClipUri(self.current_selected_uri) + self.player.uri = self.current_selected_uri self.player.setState(Gst.State.PAUSED) self.play_button.show() self.seeker.show() diff --git a/pitivi/utils/pipeline.py b/pitivi/utils/pipeline.py index 44757de0d..3f9f6eaa2 100644 --- a/pitivi/utils/pipeline.py +++ b/pitivi/utils/pipeline.py @@ -506,24 +506,35 @@ class SimplePipeline(GObject.Object, Loggable): class AssetPipeline(SimplePipeline): - """Pipeline for playing a single clip.""" + """Pipeline for playing a single asset. - def __init__(self, clip=None, name=None): + Attributes: + uri (str): The low-level pipeline. + """ + + def __init__(self, uri=None, name=None): ges_pipeline = Gst.ElementFactory.make("playbin", name) SimplePipeline.__init__(self, ges_pipeline) - self.clip = clip - if self.clip: - self.setClipUri(self.clip.props.uri) + self.__uri = None + if uri: + self.uri = uri def create_sink(self): video_sink, sink_widget = SimplePipeline.create_sink(self) self._pipeline.set_property("video_sink", video_sink) - return video_sink, sink_widget - def setClipUri(self, uri): + @property + def uri(self): + # We could maybe get it using `self._pipeline.get_property`, but + # after setting the state to Gst.State.PAUSED, it becomes None. + return self.__uri + + @uri.setter + def uri(self, uri): self._pipeline.set_property("uri", uri) + self.__uri = uri class Pipeline(GES.Pipeline, SimplePipeline): diff --git a/pitivi/viewer/viewer.py b/pitivi/viewer/viewer.py index 38b61fcbc..16627bcd6 100644 --- a/pitivi/viewer/viewer.py +++ b/pitivi/viewer/viewer.py @@ -522,14 +522,15 @@ class ViewerContainer(Gtk.Box, Loggable): if self.project.pipeline.getState() == Gst.State.PLAYING: self.project.pipeline.setState(Gst.State.PAUSED) - if self.trim_pipeline and clip is not self.trim_pipeline.clip: + uri = clip.props.uri + if self.trim_pipeline and uri != self.trim_pipeline.uri: # Seems to be the trim preview pipeline for a different clip. self.trim_pipeline.release() self.trim_pipeline = None if not self.trim_pipeline: - self.debug("Creating temporary pipeline for clip %s", clip.props.uri) - self.trim_pipeline = AssetPipeline(clip) + self.debug("Creating temporary pipeline for clip %s", uri) + self.trim_pipeline = AssetPipeline(uri) unused_video_sink, sink_widget = self.trim_pipeline.create_sink() # Add the widget to a hidden container and make it appear later # when it's ready. If we show it before the initial seek completion, -- GitLab From 5e1653642f79604da3ea1e4e8205011fd3070a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandru=20B=C4=83lu=C8=9B?= Date: Sun, 20 Jan 2019 03:25:52 +0100 Subject: [PATCH 5/5] viewer: Cache the trim preview pipelines Setting a trim preview pipeline to PAUSED can take some time. The trim preview apears faster when the cache is hit. --- pitivi/viewer/viewer.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/pitivi/viewer/viewer.py b/pitivi/viewer/viewer.py index 16627bcd6..f8d6f4ae0 100644 --- a/pitivi/viewer/viewer.py +++ b/pitivi/viewer/viewer.py @@ -16,6 +16,7 @@ # License along with this program; if not, write to the # Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, # Boston, MA 02110-1301, USA. +import collections import time from gettext import gettext as _ @@ -89,6 +90,7 @@ class ViewerContainer(Gtk.Box, Loggable): self.project = None self.trim_pipeline = None + self.trim_pipelines_cache = collections.OrderedDict() self.docked = True self.target = None self._compactMode = False @@ -529,9 +531,7 @@ class ViewerContainer(Gtk.Box, Loggable): self.trim_pipeline = None if not self.trim_pipeline: - self.debug("Creating temporary pipeline for clip %s", uri) - self.trim_pipeline = AssetPipeline(uri) - unused_video_sink, sink_widget = self.trim_pipeline.create_sink() + self.trim_pipeline, sink_widget = self.get_trim_preview_pipeline(uri) # Add the widget to a hidden container and make it appear later # when it's ready. If we show it before the initial seek completion, # there is a flicker when the first frame of the asset is shown for @@ -565,6 +565,22 @@ class ViewerContainer(Gtk.Box, Loggable): self.__trim_seek_id = GLib.timeout_add(delta_ns / 1000 / 1000, self.__trim_seek_timeout_cb, None) + def get_trim_preview_pipeline(self, uri): + try: + trim_pipeline, sink_widget = self.trim_pipelines_cache[uri] + self.debug("Reusing temporary pipeline for clip %s", uri) + except KeyError: + self.debug("Creating temporary pipeline for clip %s", uri) + trim_pipeline = AssetPipeline(uri) + unused_video_sink, sink_widget = trim_pipeline.create_sink() + self.trim_pipelines_cache[uri] = trim_pipeline, sink_widget + if len(self.trim_pipelines_cache) > 4: + # Pop the first inserted item. + expired_uri, (expired_pipeline, unused_expired_widget) = self.trim_pipelines_cache.popitem(last=False) + self.debug("Releasing temporary pipeline for clip %s", expired_uri) + expired_pipeline.release() + return trim_pipeline, sink_widget + def _state_change_cb(self, trim_pipeline, state, prev_state): if self.trim_pipeline is not trim_pipeline: self.warning("State change reported for previous trim preview pipeline") @@ -593,7 +609,6 @@ class ViewerContainer(Gtk.Box, Loggable): if not self.trim_pipeline: return self.target.switch_widget(self.overlay_stack) - self.trim_pipeline.release() self.trim_pipeline = None def _pipelineStateChangedCb(self, pipeline, state, old_state): -- GitLab