[pitivi/ges] Reactivate video thumbnails on the timeline
- From: Jean-FranÃois Fortin Tam <jfft src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi/ges] Reactivate video thumbnails on the timeline
- Date: Sun, 15 Apr 2012 21:50:25 +0000 (UTC)
commit db549f42426ef6a47f5b3ae33922ca33e067128b
Author: Daniel Thul <daniel thul googlemail com>
Date: Fri Apr 13 22:36:26 2012 +0200
Reactivate video thumbnails on the timeline
pitivi/timeline/thumbnailer.py | 228 ++++++++++++++++++++++++++++------------
pitivi/timeline/track.py | 9 +-
pitivi/utils/receiver.py | 2 +-
3 files changed, 169 insertions(+), 70 deletions(-)
---
diff --git a/pitivi/timeline/thumbnailer.py b/pitivi/timeline/thumbnailer.py
index 4592b6b..b568043 100644
--- a/pitivi/timeline/thumbnailer.py
+++ b/pitivi/timeline/thumbnailer.py
@@ -25,12 +25,14 @@
Handle thumbnails in the UI timeline
"""
+import ges
import gst
import os
import cairo
import gobject
import goocanvas
import collections
+import array
from gettext import gettext as _
@@ -39,6 +41,7 @@ from pitivi.configure import get_pixmap_dir
import pitivi.utils as utils
+from pitivi.utils.misc import big_to_cairo_alpha_mask, big_to_cairo_red_mask, big_to_cairo_green_mask, big_to_cairo_blue_mask
from pitivi.utils.receiver import receiver, handler
from pitivi.utils.timeline import Zoomable
from pitivi.utils.signal import Signallable
@@ -174,24 +177,25 @@ previewers = {}
def get_preview_for_object(instance, trackobject):
- factory = trackobject.factory
- stream_ = trackobject.stream
- stream_type = type(stream_)
- key = factory, stream_
+ uri = trackobject.props.uri
+ track_type = trackobject.get_track().props.track_type
+ key = uri, track_type
if not key in previewers:
# TODO: handle non-random access factories
# TODO: handle non-source factories
- # note that we switch on the stream_type, but we hash on the stream
+ # Note that we switch on the track_type, but we hash on the uri
# itself.
- if stream_type == stream.AudioStream:
- previewers[key] = RandomAccessAudioPreviewer(instance, factory, stream_)
- elif stream_type == stream.VideoStream:
- if type(factory) == PictureFileSourceFactory:
- previewers[key] = StillImagePreviewer(instance, factory, stream_)
+ if track_type == ges.TRACK_TYPE_AUDIO:
+ # FIXME: RandomAccessAudioPreviewer doesn't work yet
+ # previewers[key] = RandomAccessAudioPreviewer(instance, uri)
+ previewers[key] = DefaultPreviewer(instance, uri)
+ elif track_type == ges.TRACK_TYPE_VIDEO:
+ if trackobject.get_timeline_object().is_image():
+ previewers[key] = StillImagePreviewer(instance, uri)
else:
- previewers[key] = RandomAccessVideoPreviewer(instance, factory, stream_)
+ previewers[key] = RandomAccessVideoPreviewer(instance, uri)
else:
- previewers[key] = DefaultPreviewer(instance, factory, stream_)
+ previewers[key] = DefaultPreviewer(instance, uri)
return previewers[key]
@@ -212,7 +216,7 @@ class Previewer(Signallable, Loggable):
aspect = 4.0 / 3.0
- def __init__(self, instance, factory, stream_):
+ def __init__(self, instance, uri):
Loggable.__init__(self)
# create default thumbnail
path = os.path.join(get_pixmap_dir(), self.__DEFAULT_THUMB__)
@@ -238,34 +242,29 @@ class DefaultPreviewer(Previewer):
class RandomAccessPreviewer(Previewer):
""" Handles loading, caching, and drawing preview data for segments of
- random-access streams. There is one Previewer per stream per
- ObjectFactory. Preview data is read from an instance of an
- ObjectFactory's Object, and when requested, drawn into a given cairo
- context. If the requested data is not cached, an appropriate filler will
- be substituted, and an asyncrhonous request for the data will be issued.
- When the data becomes available, the update signal is emitted, along with
- the stream, and time segments. This allows the UI to re-draw the affected
- portion of a thumbnail sequence or audio waveform."""
-
- def __init__(self, instance, factory, stream_):
+ random-access streams. There is one Previewer per track_type per
+ TrackObject. Preview data is read from a uri, and when requested, drawn
+ into a given cairo context. If the requested data is not cached, an
+ appropriate filler will be substituted, and an asyncrhonous request
+ for the data will be issued. When the data becomes available, the update
+ signal is emitted, along with the stream, and time segments. This allows
+ the UI to re-draw the affected portion of a thumbnail sequence or audio waveform."""
+
+ def __init__(self, instance, uri):
self._view = True
- Previewer.__init__(self, instance, factory, stream_)
+ Previewer.__init__(self, instance, uri)
self._queue = []
- # FIXME:
- # why doesn't this work?
- # bin = factory.makeBin(stream_)
- uri = factory.uri
- caps = stream_.caps
- bin = SingleDecodeBin(uri=uri, caps=caps, stream=stream_)
+ bin = gst.element_factory_make("playbin2")
+ bin.props.uri = uri
# assume 50 pixel height
self.theight = 50
self.waiting_timestamp = None
- self._pipelineInit(factory, bin)
+ self._pipelineInit(uri, bin)
- def _pipelineInit(self, factory, bin):
+ def _pipelineInit(self, uri, bin):
"""Create the pipeline for the preview process. Subclasses should
override this method and create a pipeline, connecting to callbacks to
the appropriate signals, and prerolling the pipeline if necessary."""
@@ -295,7 +294,7 @@ class RandomAccessPreviewer(Previewer):
# tdur = duration in ns of thumbnail
# sof = start of file in pixel coordinates
x1 = bounds.x1
- sof = Zoomable.nsToPixel(element.start - element.in_point) +\
+ sof = Zoomable.nsToPixel(element.get_start() - element.get_inpoint()) +\
hscroll_pos
# i = left edge of thumbnail to be drawn. We start with x1 and
@@ -393,7 +392,7 @@ class RandomAccessPreviewer(Previewer):
Typically this will be a flushing seek(). When the
current segment has finished processing, subclasses should call
_nextThumbnail() with the resulting cairo surface. Since seeking and
- playback are asyncrhonous, you may have to call _nextThumbnail() in a
+ playback are asynchronous, you may have to call _nextThumbnail() in a
message handler or other callback."""
self.waiting_timestamp = segment
@@ -420,37 +419,62 @@ class RandomAccessVideoPreviewer(RandomAccessPreviewer):
def tdur(self):
return Zoomable.pixelToNs(self.twidth)
- def __init__(self, instance, factory, stream_):
- if stream_.dar and stream_.par:
- self.aspect = float(stream_.dar)
- rate = stream_.framerate
- RandomAccessPreviewer.__init__(self, instance, factory, stream_)
+ def __init__(self, instance, uri):
+ RandomAccessPreviewer.__init__(self, instance, uri)
self.tstep = Zoomable.pixelToNsAt(self.twidth, Zoomable.max_zoom)
- if rate.num:
- frame_duration = (gst.SECOND * rate.denom) / rate.num
+
+ if self.framerate.num:
+ frame_duration = (gst.SECOND * self.framerate.denom) / self.framerate.num
self.tstep = max(frame_duration, self.tstep)
def _pipelineInit(self, factory, sbin):
- csp = gst.element_factory_make("ffmpegcolorspace")
- sink = CairoSurfaceThumbnailSink()
- scale = gst.element_factory_make("videoscale")
- scale.props.method = 0
- caps = ("video/x-raw-rgb,height=(int) %d,width=(int) %d" %
- (self.theight, self.twidth + 2))
- filter_ = utils.filter_(caps)
- self.videopipeline = utils.pipeline({
- sbin: csp,
- csp: scale,
- scale: filter_,
- filter_: sink,
- sink: None
- })
- sink.connect('thumbnail', self._thumbnailCb)
+ """
+ Create the pipeline.
+
+ It has the form "sbin ! thumbnailsink" where thumbnailsink
+ is a Bin made out of "capsfilter ! cairosink"
+ """
+ self.videopipeline = sbin
+ self.videopipeline.props.flags = 1 # Only render video
+
+ # Use a capsfilter to scale the video to the desired size
+ # (fixed height and par, variable width)
+ caps = gst.Caps("video/x-raw-rgb, height=(int)%d, pixel-aspect-ratio=(fraction)1/1" %
+ self.theight)
+ capsfilter = gst.element_factory_make("capsfilter", "thumbnailcapsfilter")
+ capsfilter.props.caps = caps
+ cairosink = CairoSurfaceThumbnailSink()
+ cairosink.connect("thumbnail", self._thumbnailCb)
+
+ # Set up the thumbnailsink and add a sink pad
+ thumbnailsink = gst.Bin("thumbnailsink")
+ thumbnailsink.add(capsfilter)
+ thumbnailsink.add(cairosink)
+ capsfilter.link(cairosink)
+ sinkpad = gst.GhostPad("sink", thumbnailsink.find_unlinked_pad(gst.PAD_SINK))
+ thumbnailsink.add_pad(sinkpad)
+
+ # Connect sbin and thumbnailsink
+ self.videopipeline.props.video_sink = thumbnailsink
+
self.videopipeline.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:
+ if gst.STATE_CHANGE_SUCCESS == self.videopipeline.get_state(gst.CLOCK_TIME_NONE)[0]:
+ neg_caps = sinkpad.get_negotiated_caps()[0]
+ self.aspect = neg_caps["width"] / float(self.theight)
+ self.framerate = neg_caps["framerate"]
+ 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.aspect = 16.0 / 9
+ self.framerate = gst.Fraction(24, 1)
def _segment_for_time(self, time):
# quantize thumbnail timestamps to maximum granularity
- return utils.quantize(time, self.tperiod)
+ return utils.misc.quantize(time, self.tperiod)
def _thumbnailCb(self, unused_thsink, pixbuf, timestamp):
gobject.idle_add(self._finishThumbnail, pixbuf, timestamp)
@@ -486,10 +510,10 @@ class StillImagePreviewer(RandomAccessVideoPreviewer):
class RandomAccessAudioPreviewer(RandomAccessPreviewer):
- def __init__(self, instance, factory, stream_):
+ def __init__(self, instance, uri):
self.tdur = 30 * gst.SECOND
self.base_width = int(Zoomable.max_zoom)
- RandomAccessPreviewer.__init__(self, instance, factory, stream_)
+ RandomAccessPreviewer.__init__(self, instance, uri)
@property
def twidth(self):
@@ -642,6 +666,78 @@ class RandomAccessAudioPreviewer(RandomAccessPreviewer):
self.emit("update", None)
+class CairoSurfaceThumbnailSink(gst.BaseSink):
+ """
+ GStreamer thumbnailing sink element.
+
+ Can be used in pipelines to generates gtk.gdk.Pixbuf automatically.
+ """
+
+ __gsignals__ = {
+ "thumbnail": (gobject.SIGNAL_RUN_LAST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_PYOBJECT, gobject.TYPE_UINT64))
+ }
+
+ __gsttemplates__ = (
+ gst.PadTemplate("sink",
+ gst.PAD_SINK,
+ gst.PAD_ALWAYS,
+ gst.Caps("video/x-raw-rgb,"
+ "bpp = (int) 32, depth = (int) 32,"
+ "endianness = (int) BIG_ENDIAN,"
+ "alpha_mask = (int) %i, "
+ "red_mask = (int) %i, "
+ "green_mask = (int) %i, "
+ "blue_mask = (int) %i, "
+ "width = (int) [ 1, max ], "
+ "height = (int) [ 1, max ], "
+ "framerate = (fraction) [ 0, max ]"
+ % (big_to_cairo_alpha_mask,
+ big_to_cairo_red_mask,
+ big_to_cairo_green_mask,
+ big_to_cairo_blue_mask)))
+ )
+
+ def __init__(self):
+ gst.BaseSink.__init__(self)
+ self._width = 1
+ self._height = 1
+ self.set_sync(False)
+
+ def do_set_caps(self, caps):
+ self.log("caps %s" % caps.to_string())
+ self.log("padcaps %s" % self.get_pad("sink").get_caps().to_string())
+ self.width = caps[0]["width"]
+ self.height = caps[0]["height"]
+ if not caps[0].get_name() == "video/x-raw-rgb":
+ return False
+ return True
+
+ def do_render(self, buf):
+ self.log("buffer %s %d" % (gst.TIME_ARGS(buf.timestamp),
+ len(buf.data)))
+ b = array.array("b")
+ b.fromstring(buf)
+ pixb = cairo.ImageSurface.create_for_data(b,
+ # We don't use FORMAT_ARGB32 because Cairo uses premultiplied
+ # alpha, and gstreamer does not. Discarding the alpha channel
+ # is not ideal, but the alternative would be to compute the
+ # conversion in python (slow!).
+ cairo.FORMAT_RGB24,
+ self.width,
+ self.height,
+ self.width * 4)
+
+ self.emit("thumbnail", pixb, buf.timestamp)
+ return gst.FLOW_OK
+
+ def do_preroll(self, buf):
+ return self.do_render(buf)
+
+gobject.type_register(CairoSurfaceThumbnailSink)
+
+
def between(a, b, c):
return (a <= b) and (b <= c)
@@ -669,7 +765,7 @@ class Preview(goocanvas.ItemSimple, goocanvas.Item, Zoomable):
self.element = element
self.props.pointer_events = False
# ghetto hack
- self.hadj = instance.gui.timeline.hadj
+ self.hadj = instance.gui.timeline_ui.hadj
## properties
@@ -688,8 +784,8 @@ class Preview(goocanvas.ItemSimple, goocanvas.Item, Zoomable):
self.element)
element = receiver(setter=_set_element)
- @handler(element, "in-point-changed")
- @handler(element, "media-duration-changed")
+ @handler(element, "notify::in-point")
+ @handler(element, "notify::duration")
def _media_props_changed(self, obj, unused_start_duration):
self.changed(True)
@@ -715,19 +811,19 @@ class Preview(goocanvas.ItemSimple, goocanvas.Item, Zoomable):
def do_simple_update(self, cr):
cr.identity_matrix()
- if self.element.factory:
+ if issubclass(self.previewer.__class__, RandomAccessPreviewer):
border_width = self.previewer._spacing()
self.bounds = goocanvas.Bounds(border_width, 4,
- max(0, Zoomable.nsToPixel(self.element.duration) -
+ max(0, Zoomable.nsToPixel(self.element.get_duration()) -
border_width), self.height)
def do_simple_paint(self, cr, bounds):
x1 = -self.hadj.get_value()
cr.identity_matrix()
- if self.element.factory:
+ if issubclass(self.previewer.__class__, RandomAccessPreviewer):
self.previewer.render_cairo(cr, intersect(self.bounds, bounds),
self.element, x1, self.bounds.y1)
def do_simple_is_item_at(self, x, y, cr, pointer_event):
- return (between(0, x, self.nsToPixel(self.element.duration)) and
+ return (between(0, x, self.nsToPixel(self.element.get_duration())) and
between(0, y, self.height))
diff --git a/pitivi/timeline/track.py b/pitivi/timeline/track.py
index 79a1b2c..ff143ba 100644
--- a/pitivi/timeline/track.py
+++ b/pitivi/timeline/track.py
@@ -45,6 +45,7 @@ from pitivi.utils.timeline import SELECT, SELECT_ADD, UNSELECT, \
from pitivi.utils.ui import LAYER_HEIGHT_EXPANDED,\
LAYER_HEIGHT_COLLAPSED, LAYER_SPACING, \
unpack_cairo_pattern, unpack_cairo_gradient
+from thumbnailer import Preview
#--------------------------------------------------------------#
@@ -358,6 +359,8 @@ class TrackObject(View, goocanvas.Group, Zoomable):
height=self.height,
line_width=1)
+ self.preview = Preview(self.app, element)
+
self.name = goocanvas.Text(
x=NAME_HOFFSET + NAME_PADDING,
y=NAME_VOFFSET + NAME_PADDING,
@@ -381,7 +384,7 @@ class TrackObject(View, goocanvas.Group, Zoomable):
height=self.height)
if not self.is_transition:
- for thing in (self.bg, self._selec_indic,
+ for thing in (self.bg, self.preview, self._selec_indic,
self.start_handle, self.end_handle, self.namebg, self.name):
self.add_child(thing)
else:
@@ -422,13 +425,13 @@ class TrackObject(View, goocanvas.Group, Zoomable):
self._expanded = expanded
if not self._expanded:
self.height = LAYER_HEIGHT_COLLAPSED
- self.content.props.visibility = goocanvas.ITEM_INVISIBLE
+ self.preview.props.visibility = goocanvas.ITEM_INVISIBLE
self.namebg.props.visibility = goocanvas.ITEM_INVISIBLE
self.bg.props.height = LAYER_HEIGHT_COLLAPSED
self.name.props.y = 0
else:
self.height = LAYER_HEIGHT_EXPANDED
- self.content.props.visibility = goocanvas.ITEM_VISIBLE
+ self.preview.props.visibility = goocanvas.ITEM_VISIBLE
self.namebg.props.visibility = goocanvas.ITEM_VISIBLE
self.bg.props.height = LAYER_HEIGHT_EXPANDED
self.height = LAYER_HEIGHT_EXPANDED
diff --git a/pitivi/utils/receiver.py b/pitivi/utils/receiver.py
index d4e0a35..0ed42f2 100644
--- a/pitivi/utils/receiver.py
+++ b/pitivi/utils/receiver.py
@@ -9,7 +9,7 @@ class _receiver_data(object):
class receiver(object):
- """A descriptor which wrapps signal connect and disconnect for a single
+ """A descriptor which wraps signal connect and disconnect for a single
object (the sender). Signal handlers may be registered with the
add_handler() method, after which point the handler will be automatically
connected when the property value is set. Prior to connecting new signal
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]