[pitivi] Various performance improvements to the timeline clip thumbnailer
- From: Jean-François Fortin Tam <jfft src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] Various performance improvements to the timeline clip thumbnailer
- Date: Wed, 24 Apr 2013 18:01:21 +0000 (UTC)
commit f47b1f954b33c50707f3b227dd3a4412bc806bec
Author: Daniel Thul <daniel thul gmail com>
Date: Mon Apr 15 18:31:20 2013 +0200
Various performance improvements to the timeline clip thumbnailer
- Create thumbnails in the background (even when not visible at the moment)
- Draw only visible thumbnails, respect the clip's in-point
- Prioritize visible thumbnails over background thumbnail generation
- Use JPEG compression for thumbnails
- Scale the thumbnail in the pipeline instead of doing it manually
pitivi/timeline/timeline.py | 345 ++++++++++++++++++++++++++-----------------
1 files changed, 208 insertions(+), 137 deletions(-)
---
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py
index c510979..9aeec0d 100644
--- a/pitivi/timeline/timeline.py
+++ b/pitivi/timeline/timeline.py
@@ -4,7 +4,6 @@ from gi.repository import Gst
from gi.repository import GES
from gi.repository import GObject
-import collections
import hashlib
import os
import sqlite3
@@ -473,7 +472,7 @@ class TimelineElement(Clutter.Actor, Zoomable):
self.leftHandle.set_position(0, 0)
def _createPreview(self):
- self.preview = get_preview_for_object(self.bElement)
+ self.preview = get_preview_for_object(self.bElement, self.timeline)
self.add_child(self.preview)
def _createMarquee(self):
@@ -576,6 +575,10 @@ class TimelineElement(Clutter.Actor, Zoomable):
class TimelineStage(Clutter.ScrollActor, Zoomable):
+ __gsignals__ = {
+ 'scrolled': (GObject.SIGNAL_RUN_FIRST, None, ())
+ }
+
def __init__(self, container):
Clutter.ScrollActor.__init__(self)
Zoomable.__init__(self)
@@ -585,6 +588,7 @@ class TimelineStage(Clutter.ScrollActor, Zoomable):
self._createPlayhead()
self._container = container
self.lastPosition = 0
+ self._scroll_point = Clutter.Point()
# Public API
@@ -714,6 +718,18 @@ class TimelineStage(Clutter.ScrollActor, Zoomable):
self._container.controls.addLayerControl(layer)
self._updatePlayHead()
+ # Clutter Override
+
+ # TODO: remove self._scroll_point and get_scroll_point as soon as the Clutter API
+ # offers a way to query a ScrollActor for its current scroll point
+ def scroll_to_point(self, point):
+ Clutter.ScrollActor.scroll_to_point(self, point)
+ self._scroll_point = point.copy()
+ self.emit("scrolled")
+
+ def get_scroll_point(self):
+ return self._scroll_point
+
# Callbacks
def _layerAddedCb(self, timeline, layer):
@@ -1585,7 +1601,7 @@ class Timeline(Gtk.VBox, Zoomable):
self.project.create_asset("file://" + sys.argv[1], GES.UriClip)
-def get_preview_for_object(bElement):
+def get_preview_for_object(bElement, timeline):
track_type = bElement.get_track_type()
if track_type == GES.TrackType.AUDIO:
# FIXME: RandomAccessAudioPreviewer doesn't work yet
@@ -1597,96 +1613,136 @@ def get_preview_for_object(bElement):
# TODO: return still image previewer
return Clutter.Actor()
else:
- return VideoPreviewer(bElement)
+ return VideoPreviewer(bElement, timeline)
else:
return Clutter.Actor()
-class VideoPreviewer(Clutter.Actor, Zoomable):
- def __init__(self, bElement):
+class VideoPreviewer(Clutter.ScrollActor, Zoomable):
+ def __init__(self, bElement, timeline):
"""
@param bElement : the backend GES.TrackElement
@param track : the track to which the bElement belongs
@param timeline : the containing graphic timeline.
"""
Zoomable.__init__(self)
- Clutter.Actor.__init__(self)
+ Clutter.ScrollActor.__init__(self)
self.uri = bElement.props.uri
- self.layoutManager = Clutter.BinLayout()
- self.set_layout_manager(self.layoutManager)
-
self.bElement = bElement
+ self.timeline = timeline
-# self.bElement.connect("notify::duration", self.element_changed)
-# self.bElement.connect("notify::in-point", self.element_changed)
+ self.bElement.connect("notify::duration", self.element_changed)
+ self.bElement.connect("notify::in-point", self.element_changed)
+ self.bElement.connect("notify::start", self.element_changed)
- self.duration = self.bElement.get_duration()
- self.in_point = self.bElement.get_inpoint()
+ self.timeline.connect("scrolled", self._update)
+
+ self.duration = self.bElement.props.duration
self.thumb_margin = BORDER_WIDTH
self.thumb_height = EXPANDED_SIZE - 2 * self.thumb_margin
# self.thumb_width will be set by self._setupPipeline()
# TODO: read this property from the settings
- self.thumb_period = long(0.1 * Gst.SECOND)
+ self.thumb_period = long(0.5 * Gst.SECOND)
# maps (quantized) times to Thumbnail objects
self.thumbs = {}
self.thumb_cache = ThumbnailCache(uri=self.uri)
- self.queue = []
-
- self.waiting_timestamp = None
+ self.wishlist = []
self._setupPipeline()
- self.callback_id = None
+ self._startThumbnailing()
# Internal API
+ def _update(self, unused_msg_source):
+ self._addThumbnails()
+
def _setupPipeline(self):
"""
Create the pipeline.
It has the form "playbin ! thumbnailsink" where thumbnailsink
- is a Bin made out of "capsfilter ! gdkpixbufsink"
+ is a Bin made out of "videorate ! capsfilter ! gdkpixbufsink"
"""
- self.pipeline = Gst.ElementFactory.make("playbin", None)
- self.pipeline.props.uri = self.uri
- self.pipeline.props.flags = 1 # Only render video
-
- # Set up the thumbnailsink
- thumbnailsink = Gst.parse_bin_from_description("capsfilter
caps=video/x-raw,format=(string)RGB,pixel-aspect-ratio=(fraction)1/1 ! gdkpixbufsink name=gdkpixbufsink",
True)
-
- # get the gdkpixbufsink and the automatically created ghostpad
- self.gdkpixbufsink = thumbnailsink.get_by_name("gdkpixbufsink")
- sinkpad = thumbnailsink.get_static_pad("sink")
-
- # Connect the playbin and the thumbnailsink
- self.pipeline.props.video_sink = thumbnailsink
-
- # add a message handler that listens for the created pixbufs
- self.pipeline.get_bus().add_signal_watch()
- self.pipeline.get_bus().connect("message", self.bus_message_handler)
+ # TODO: don't hardcode the framerate but compute from thumb_period
+ self.pipeline = Gst.parse_launch(
+ "uridecodebin uri={uri} ! "
+ "videoconvert ! "
+ "videorate ! "
+ "videoscale method=lanczos ! "
+ "capsfilter caps=video/x-raw,format=(string)RGBA,height=(int){height},"
+ "pixel-aspect-ratio=(fraction)1/1,framerate=(fraction)2/1 ! "
+ "gdkpixbufsink name=gdkpixbufsink".format(uri=self.uri, height=self.thumb_height))
+
+ # get the gdkpixbufsink and the sinkpad
+ self.gdkpixbufsink = self.pipeline.get_by_name("gdkpixbufsink")
+ sinkpad = self.gdkpixbufsink.get_static_pad("sink")
self.pipeline.set_state(Gst.State.PAUSED)
+
# Wait for the pipeline to be prerolled so we can check the width
# that the thumbnails will have and set the aspect ratio accordingly
# as well as getting the framerate of the video:
change_return = self.pipeline.get_state(Gst.CLOCK_TIME_NONE)
if Gst.StateChangeReturn.SUCCESS == change_return[0]:
neg_caps = sinkpad.get_current_caps()[0]
- video_width = neg_caps["width"]
- video_height = neg_caps["height"]
- self.thumb_width = video_width * self.thumb_height / video_height
+ self.thumb_width = neg_caps["width"]
else:
# the pipeline couldn't be prerolled so we can't determine the
# correct values. Set sane defaults (this should never happen)
self.warning("Couldn't preroll the pipeline")
- self.thumb_width = 16 * self.thumb_height / 9 # assume 16:9 aspect ratio
+ # assume 16:9 aspect ratio
+ self.thumb_width = 16 * self.thumb_height / 9
+
+ # pop all messages from the bus so we won't be flooded with messages
+ # from the prerolling phase
+ while self.pipeline.get_bus().pop():
+ continue
+ # add a message handler that listens for the created pixbufs
+ self.pipeline.get_bus().add_signal_watch()
+ self.pipeline.get_bus().connect("message", self.bus_message_handler)
+
+ def _startThumbnailing(self):
+ self.queue = []
+ query_success, duration = self.pipeline.query_duration(Gst.Format.TIME)
+ if not query_success:
+ print("Could not determine the duration of the file {}".format(self.uri))
+ duration = self.duration
+ else:
+ self.duration = duration
+
+ current_time = 0
+ while current_time < duration:
+ self.queue.append(current_time)
+ current_time += self.thumb_period
+
+ self._create_next_thumb()
+
+ def _create_next_thumb(self):
+ if not self.queue:
+ # nothing left to do
+ return
+ wish = self._get_wish()
+ if wish:
+ time = wish
+ self.queue.remove(wish)
+ else:
+ time = self.queue.pop(0)
+ # append the time to the end of the queue so that if this seek fails
+ # another try will be started later
+ self.queue.append(time)
+
+ self.pipeline.seek(1.0,
+ Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
+ Gst.SeekType.SET, time,
+ Gst.SeekType.NONE, -1)
def _addThumbnails(self):
"""
@@ -1698,6 +1754,7 @@ class VideoPreviewer(Clutter.Actor, Zoomable):
# TODO: check if duration or zoomratio really changed?
self.remove_all_children()
self.thumbs = {}
+ self.wishlist = []
# calculate unquantized length of a thumb in nano seconds
thumb_duration_tmp = Zoomable.pixelToNs(self.thumb_width + self.thumb_margin)
@@ -1712,86 +1769,104 @@ class VideoPreviewer(Clutter.Actor, Zoomable):
# make sure that we don't show thumbnails more often than thumb_period
thumb_duration = max(thumb_duration, self.thumb_period)
- number_of_thumbs = self.duration / thumb_duration
+ element_left, element_right = self._get_visible_range()
+ # TODO: replace with a call to utils.misc.quantize:
+ element_left = (element_left // thumb_duration) * thumb_duration
- current_time = 0
- # +1 because wa want to draw the rightmost thumbnail even if it will be clipped
- for i in range(0, number_of_thumbs + 1):
+ current_time = element_left
+ while current_time < element_right:
thumb = Thumbnail(self.thumb_width, self.thumb_height)
thumb.set_position(Zoomable.nsToPixel(current_time), self.thumb_margin)
self.add_child(thumb)
self.thumbs[current_time] = thumb
- self._thumbForTime(current_time)
+ if current_time in self.thumb_cache:
+ gdkpixbuf = self.thumb_cache[current_time]
+ self.thumbs[current_time].set_from_gdkpixbuf(gdkpixbuf)
+ else:
+ if not current_time in self.wishlist:
+ self.wishlist.append(current_time)
current_time += thumb_duration
- def _thumbForTime(self, time):
- if time in self.thumb_cache:
- gdkpixbuf = self.thumb_cache[time]
- self.thumbs[time].set_from_gdkpixbuf(gdkpixbuf)
- else:
- self._requestThumbnail(time)
+ position = Clutter.Point()
+ position.x = Zoomable.nsToPixel(self.bElement.props.in_point)
+ self.scroll_to_point(position)
+
+ def _get_wish(self):
+ """Returns a wish that is also in the queue or None
+ if no such wish exists"""
+ while True:
+ if not self.wishlist:
+ return None
+ wish = self.wishlist.pop(0)
+ if wish in self.queue:
+ return wish
+
+ def _setThumbnail(self, time, thumbnail):
+ # TODO: is "time" guaranteed to be nanosecond precise?
+ # => __tim says: "that's how it should be"
+ # => also see gst-plugins-good/tests/icles/gdkpixbufsink-test
+ if time in self.queue:
+ self.queue.remove(time)
- def _requestThumbnail(self, time):
- """Queue a thumbnail request for the given time"""
- if time not in self.queue: # and len(self._queue) <= self.max_requests:
- if self.queue:
- self.queue.append(time)
- else:
- self.queue.append(time)
- self._nextThumbnail()
-
- def _nextThumbnail(self):
- """Notifies the preview object that the pipeline is ready to process
- the next thumbnail in the queue. This should always be called from the
- main application thread."""
- if self.queue:
- if not self._startThumbnail(self.queue[0]):
- self.queue.pop(0)
- self._nextThumbnail()
- return False
+ self.thumb_cache[time] = thumbnail
- def _startThumbnail(self, time):
- self.waiting_timestamp = time
- return self.pipeline.seek(1.0,
- Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
- Gst.SeekType.SET, time,
- Gst.SeekType.NONE, -1)
+ if time in self.thumbs:
+ self.thumbs[time].set_from_gdkpixbuf(thumbnail)
- def _finishThumbnail(self, gdkpixbuf, time):
- """Notifies the preview object that the a new thumbnail is ready to be
- cached. This should be called by subclasses when they have finished
- processing the thumbnail for the current segment. This function should
- always be called from the main thread of the application."""
- waiting = self.waiting_timestamp
- self.waiting_timestamp = None
+ # Interface (Zoomable)
- if time != waiting:
- time = waiting
+ def zoomChanged(self):
+ self._addThumbnails()
- thumbnail = gdkpixbuf.scale_simple(self.thumb_width, self.thumb_height, 3)
+ def _get_visible_range(self):
+ timeline_left, timeline_right = self._get_visible_timeline_range()
+ element_left = timeline_left - self.bElement.props.start + self.bElement.props.in_point
+ element_left = max(element_left, self.bElement.props.in_point)
- self.thumb_cache[time] = thumbnail
+ element_right = timeline_right - self.bElement.props.start + self.bElement.props.in_point
+ element_right = min(element_right, self.bElement.props.in_point + self.bElement.props.duration)
- if time in self.thumbs:
- self.thumbs[time].set_from_gdkpixbuf(thumbnail)
- #self.emit("update", time)
+ return (element_left, element_right)
- if time in self.queue:
- self.queue.remove(time)
- self._nextThumbnail()
- return False
+ # TODO: move to Timeline or to utils
+ def _get_visible_timeline_range(self):
+ # determine the visible left edge of the timeline
+ # TODO: isn't there some easier way to get the scroll point of the ScrollActor?
+ # timeline_left = -(self.timeline.get_transform().xw - self.timeline.props.x)
+ timeline_left = self.timeline.get_scroll_point().x
- # Interface (Zoomable)
+ # determine the width of the pipeline
+ # by intersecting the timeline's and the stage's allocation
+ timeline_allocation = self.timeline.props.allocation
+ stage_allocation = self.timeline.get_stage().props.allocation
- def _maybeUpdate(self):
- self._addThumbnails()
- self.callback_id = None
- return False
+ timeline_rect = Clutter.Rect()
+ timeline_rect.init(timeline_allocation.x1,
+ timeline_allocation.y1,
+ timeline_allocation.x2 - timeline_allocation.x1,
+ timeline_allocation.y2 - timeline_allocation.y1)
- def zoomChanged(self):
- if self.callback_id is not None:
- GObject.source_remove(self.callback_id)
- self.callback_id = GObject.timeout_add(100, self._maybeUpdate)
+ stage_rect = Clutter.Rect()
+ stage_rect.init(stage_allocation.x1,
+ stage_allocation.y1,
+ stage_allocation.x2 - stage_allocation.x1,
+ stage_allocation.y2 - stage_allocation.y1)
+
+ has_intersection, intersection = timeline_rect.intersection(stage_rect)
+
+ if not has_intersection:
+ return (0, 0)
+
+ timeline_width = intersection.size.width
+
+ # determine the visible right edge of the timeline
+ timeline_right = timeline_left + timeline_width
+
+ # convert to nanoseconds
+ time_left = Zoomable.pixelToNs(timeline_left)
+ time_right = Zoomable.pixelToNs(timeline_right)
+
+ return (time_left, time_right)
# Callbacks
@@ -1799,28 +1874,19 @@ class VideoPreviewer(Clutter.Actor, Zoomable):
if message.type == Gst.MessageType.ELEMENT and \
message.src == self.gdkpixbufsink:
struct = message.get_structure()
-
- # TODO: does struct.get_name() work?
- #if struct.get_name() == "pixbuf":
-
- # TODO: there exists no value named "timestamp"
- #self._finishThumbnail(struct.get_value("pixbuf"), struct.get_value("timestamp"))
- GLib.idle_add(self._finishThumbnail, struct.get_value("pixbuf"),
- struct.get_value("timestamp"))
+ struct_name = struct.get_name()
+ if struct_name == "preroll-pixbuf":
+ self._setThumbnail(struct.get_value("stream-time"), struct.get_value("pixbuf"))
+ elif message.type == Gst.MessageType.ASYNC_DONE:
+ self._create_next_thumb()
return Gst.BusSyncReply.PASS
- #bElement = receiver()
-
- # handler(bElement, "notify::duration")
- # handler(bElement, "notify::in-point")
- def element_changed(self, unused_bElement, unused_start_duration):
- self.duration = self.bElement.get_duration()
- self.in_point = self.bElement.get_inpoint()
- GLib.idle_add(self._addThumbnails)
+ def element_changed(self, unused_bElement, unused_value):
+ self.duration = max(self.duration, self.bElement.props.duration)
+ self._addThumbnails()
class Thumbnail(Clutter.Actor):
-
def __init__(self, width, height):
Clutter.Actor.__init__(self)
image = Clutter.Image.new()
@@ -1833,8 +1899,11 @@ class Thumbnail(Clutter.Actor):
def set_from_gdkpixbuf(self, gdkpixbuf):
row_stride = gdkpixbuf.get_rowstride()
pixel_data = gdkpixbuf.get_pixels()
- # Cogl.PixelFormat.RGB_888 := 2
- self.props.content.set_data(pixel_data, Cogl.PixelFormat.RGB_888, self.width, self.height,
row_stride)
+ alpha = gdkpixbuf.get_has_alpha()
+ if alpha:
+ self.props.content.set_data(pixel_data, Cogl.PixelFormat.RGBA_8888, self.width, self.height,
row_stride)
+ else:
+ self.props.content.set_data(pixel_data, Cogl.PixelFormat.RGB_888, self.width, self.height,
row_stride)
# TODO: replace with utils.misc.hash_file
@@ -1870,14 +1939,15 @@ class ThumbnailCache(object):
def __init__(self, uri):
object.__init__(self)
# TODO: replace with utils.misc.hash_file
- self.hash = hash_file(Gst.uri_get_location(uri))
+ filehash = hash_file(Gst.uri_get_location(uri))
# TODO: replace with pitivi.settings.xdg_cache_home()
cache_dir = get_dir(os.path.join(xdg_dirs.xdg_cache_home, "pitivi"), autocreate)
- dbfile = os.path.join(get_dir(os.path.join(cache_dir, "thumbs")), self.hash)
+ dbfile = os.path.join(get_dir(os.path.join(cache_dir, "thumbs")), filehash)
self.conn = sqlite3.connect(dbfile)
self.cur = self.conn.cursor()
- self.cur.execute("CREATE TABLE IF NOT EXISTS Thumbs (Time INTEGER NOT NULL PRIMARY KEY,\
- Data BLOB NOT NULL, Width INTEGER NOT NULL, Height INTEGER NOT NULL, Stride INTEGER NOT NULL)")
+ self.cur.execute("CREATE TABLE IF NOT EXISTS Thumbs\
+ (Time INTEGER NOT NULL PRIMARY KEY,\
+ Jpeg BLOB NOT NULL)")
def __contains__(self, key):
# check if item is present in on disk cache
@@ -1890,23 +1960,24 @@ class ThumbnailCache(object):
self.cur.execute("SELECT * FROM Thumbs WHERE Time = ?", (key,))
row = self.cur.fetchone()
if row:
- pixbuf = GdkPixbuf.Pixbuf.new_from_data(row[1],
- GdkPixbuf.Colorspace.RGB,
- False,
- 8,
- row[2],
- row[3],
- row[4],
- None,
- None)
+ jpeg = row[1]
+ loader = GdkPixbuf.PixbufLoader.new()
+ # TODO: what do to if any of the following calls fails?
+ loader.write(jpeg)
+ loader.close()
+ pixbuf = loader.get_pixbuf()
return pixbuf
raise KeyError(key)
def __setitem__(self, key, value):
- blob = sqlite3.Binary(bytearray(value.get_pixels()))
+ success, jpeg = value.save_to_bufferv("jpeg", ["quality", None], ["90"])
+ if not success:
+ self.warning("JPEG compression failed")
+ return
+ blob = sqlite3.Binary(jpeg)
#Replace if the key already existed
self.cur.execute("DELETE FROM Thumbs WHERE time=?", (key,))
- self.cur.execute("INSERT INTO Thumbs VALUES (?,?,?,?,?)", (key, blob, value.get_width(),
value.get_height(), value.get_rowstride()))
+ self.cur.execute("INSERT INTO Thumbs VALUES (?,?)", (key, blob,))
self.conn.commit()
if __name__ == "__main__":
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]