[pitivi/ges: 189/287] ui: Merge previewer.py into preview.py
- From: Jean-FranÃois Fortin Tam <jfft src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi/ges: 189/287] ui: Merge previewer.py into preview.py
- Date: Thu, 15 Mar 2012 16:41:43 +0000 (UTC)
commit d3dcd42bddd5e5fc0c75d9a826d9135b1124e5a2
Author: Thibault Saunier <thibault saunier collabora com>
Date: Tue Jan 10 19:15:54 2012 -0300
ui: Merge previewer.py into preview.py
This is not used anymore, but we keep it so we have the basis to reimplement it
with GES
pitivi/ui/Makefile.am | 1 -
pitivi/ui/preview.py | 588 +++++++++++++++++++++++++++++++++++++++++++++++-
pitivi/ui/previewer.py | 591 ------------------------------------------------
po/POTFILES.in | 2 +-
4 files changed, 583 insertions(+), 599 deletions(-)
---
diff --git a/pitivi/ui/Makefile.am b/pitivi/ui/Makefile.am
index 3b38cdc..e1eb0d0 100644
--- a/pitivi/ui/Makefile.am
+++ b/pitivi/ui/Makefile.am
@@ -8,7 +8,6 @@ ui_PYTHON = \
mainwindow.py \
prefs.py \
preset.py \
- previewer.py \
preview.py \
basetabs.py \
startupwizard.py \
diff --git a/pitivi/ui/preview.py b/pitivi/ui/preview.py
index 1822fe4..f28df28 100644
--- a/pitivi/ui/preview.py
+++ b/pitivi/ui/preview.py
@@ -19,17 +19,587 @@
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
-"""
-Custom canvas item for timeline object previews. This code is just a thin
-canvas-item wrapper which ensures that the preview is updated appropriately.
-The actual drawing is done by the pitivi.previewer.Previewer class. """
+#FIXME GES port Reimplement me
-import goocanvas
+import gst
+import os
+import cairo
import gobject
+import goocanvas
+
+from gettext import gettext as _
+
+from pitivi.settings import GlobalSettings
+from pitivi.configure import get_pixmap_dir
+from pitivi.thumbnailcache import ThumbnailCache
+
+import pitivi.ui.previewer as previewer
+
+
+import pitivi.utils as utils
from pitivi.utils.receiver import receiver, handler
from pitivi.utils.timeline import Zoomable
-import pitivi.ui.previewer as previewer
+from pitivi.utils.signal import Signallable
+from pitivi.utils.loggable import Loggable
+
+from pitivi.ui.prefs import PreferencesDialog
+
+GlobalSettings.addConfigSection("thumbnailing")
+GlobalSettings.addConfigOption("thumbnailSpacingHint",
+ section="thumbnailing",
+ key="spacing-hint",
+ default=2,
+ notify=True)
+
+GlobalSettings.addConfigOption("thumbnailPeriod",
+ section="thumbnailing",
+ key="thumbnail-period",
+ default=gst.SECOND,
+ notify=True)
+
+PreferencesDialog.addNumericPreference("thumbnailSpacingHint",
+ section=_("Appearance"),
+ label=_("Thumbnail gap"),
+ lower=0,
+ description=_("The spacing between thumbnails, in pixels"))
+
+PreferencesDialog.addChoicePreference("thumbnailPeriod",
+ section=_("Performance"),
+ label=_("Thumbnail every"),
+ choices=(
+ # Note that we cannot use "%s second" or ngettext, because fractions
+ # are not supported by ngettext and their plurality is ambiguous
+ # in many languages.
+ # See http://www.gnu.org/software/hello/manual/gettext/Plural-forms.html
+ (_("1/100 second"), gst.SECOND / 100),
+ (_("1/10 second"), gst.SECOND / 10),
+ (_("1/4 second"), gst.SECOND / 4),
+ (_("1/2 second"), gst.SECOND / 2),
+ (_("1 second"), gst.SECOND),
+ (_("5 seconds"), 5 * gst.SECOND),
+ (_("10 seconds"), 10 * gst.SECOND),
+ (_("minute"), 60 * gst.SECOND)),
+ description=_("The interval, in seconds, between thumbnails."))
+
+# this default works out to a maximum of ~ 1.78 MiB per factory, assuming:
+# 4:3 aspect ratio
+# 4 bytes per pixel
+# 50 pixel height
+GlobalSettings.addConfigOption("thumbnailCacheSize",
+ section="thumbnailing",
+ key="cache-size",
+ default=250)
+
+# the maximum number of thumbnails to enqueue at a given time. setting this to
+# a larger value will increase latency after large operations, such as zooming
+GlobalSettings.addConfigOption("thumbnailMaxRequests",
+ section="thumbnailing",
+ key="max-requests",
+ default=10)
+
+GlobalSettings.addConfigOption('showThumbnails',
+ section='user-interface',
+ key='show-thumbnails',
+ default=True,
+ notify=True)
+
+PreferencesDialog.addTogglePreference('showThumbnails',
+ section=_("Performance"),
+ label=_("Enable video thumbnails"),
+ description=_("Show thumbnails on video clips"))
+
+GlobalSettings.addConfigOption('showWaveforms',
+ section='user-interface',
+ key='show-waveforms',
+ default=True,
+ notify=True)
+
+PreferencesDialog.addTogglePreference('showWaveforms',
+ section=_("Performance"),
+ label=_("Enable audio waveforms"),
+ description=_("Show waveforms on audio clips"))
+
+# Previewer -- abstract base class with public interface for UI
+# |_DefaultPreviewer -- draws a default thumbnail for UI
+# |_LivePreviewer -- draws a continuously updated preview
+# | |_LiveAudioPreviwer -- a continously updating level meter
+# | |_LiveVideoPreviewer -- a continously updating video monitor
+# |_RandomAccessPreviewer -- asynchronous fetching and caching
+# |_RandomAccessAudioPreviewer -- audio-specific pipeline and rendering code
+# |_RandomAccessVideoPreviewer -- video-specific pipeline and rendering
+# |_StillImagePreviewer -- only uses one segment
+
+previewers = {}
+
+
+def get_preview_for_object(instance, trackobject):
+ factory = trackobject.factory
+ stream_ = trackobject.stream
+ stream_type = type(stream_)
+ key = factory, stream_
+ 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
+ # 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_)
+ else:
+ previewers[key] = RandomAccessVideoPreviewer(instance, factory, stream_)
+ else:
+ previewers[key] = DefaultPreviewer(instance, factory, stream_)
+ return previewers[key]
+
+
+class Previewer(Signallable, Loggable):
+ """
+ Utility for easy generation of previews
+ """
+
+ __signals__ = {
+ "update": ("segment",),
+ }
+
+ # TODO: parameterize height, instead of assuming self.theight pixels.
+ # NOTE: dymamically changing thumbnail height would involve flushing the
+ # thumbnail cache.
+
+ __DEFAULT_THUMB__ = "processing-clip.png"
+
+ aspect = 4.0 / 3.0
+
+ def __init__(self, instance, factory, stream_):
+ Loggable.__init__(self)
+ # create default thumbnail
+ path = os.path.join(get_pixmap_dir(), self.__DEFAULT_THUMB__)
+ self.default_thumb = cairo.ImageSurface.create_from_png(path)
+ self._connectSettings(instance.settings)
+
+ def render_cairo(self, cr, bounds, element, hscroll_pos, y1):
+ """Render a preview of element onto a cairo context within the current
+ bounds, which may or may not be the entire object and which may or may
+ not intersect the visible portion of the object"""
+ raise NotImplementedError
+
+ def _connectSettings(self, settings):
+ self._settings = settings
+
+
+class DefaultPreviewer(Previewer):
+
+ def render_cairo(self, cr, bounds, element, hscroll_pos, y1):
+ # TODO: draw a single thumbnail
+ pass
+
+
+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_):
+ self._view = True
+ Previewer.__init__(self, instance, factory, stream_)
+ 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_)
+
+ # assume 50 pixel height
+ self.theight = 50
+ self.waiting_timestamp = None
+
+ self._pipelineInit(factory, bin)
+
+ def _pipelineInit(self, factory, 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."""
+ raise NotImplementedError
+
+## public interface
+
+ def render_cairo(self, cr, bounds, element, hscroll_pos, y1):
+ if not self._view:
+ return
+ # The idea is to conceptually divide the clip into a sequence of
+ # rectangles beginning at the start of the file, and
+ # pixelsToNs(twidth) nanoseconds long. The thumbnail within the
+ # rectangle is the frame produced from the timestamp corresponding to
+ # rectangle's left edge. We speed things up by only drawing the
+ # rectangles which intersect the given bounds. FIXME: how would we
+ # handle timestretch?
+ height = bounds.y2 - bounds.y1
+ width = bounds.x2 - bounds.x1
+
+ # we actually draw the rectangles just to the left of the clip's in
+ # point and just to the right of the clip's out-point, so we need to
+ # mask off the actual bounds.
+ cr.rectangle(bounds.x1, bounds.y1, width, height)
+ cr.clip()
+
+ # tdur = duration in ns of thumbnail
+ # sof = start of file in pixel coordinates
+ x1 = bounds.x1
+ sof = Zoomable.nsToPixel(element.start - element.in_point) +\
+ hscroll_pos
+
+ # i = left edge of thumbnail to be drawn. We start with x1 and
+ # subtract the distance to the nearest leftward rectangle.
+ # Justification of the following:
+ # i = sof + k * twidth
+ # i = x1 - delta
+ # sof + k * twidth = x1 - delta
+ # i * tw = (x1 - sof) - delta
+ # <=> delta = x1 - sof (mod twidth).
+ # Fortunately for us, % works on floats in python.
+
+ i = x1 - ((x1 - sof) % (self.twidth + self._spacing()))
+
+ # j = timestamp *within the element* of thumbnail to be drawn. we want
+ # timestamps to be numerically stable, but in practice this seems to
+ # give good enough results. It might be possible to improve this
+ # further, which would result in fewer thumbnails needing to be
+ # generated.
+ j = Zoomable.pixelToNs(i - sof)
+ istep = self.twidth + self._spacing()
+ jstep = self.tdur + Zoomable.pixelToNs(self.spacing)
+
+ while i < bounds.x2:
+ self._thumbForTime(cr, j, i, y1)
+ cr.rectangle(i - 1, y1, self.twidth + 2, self.theight)
+ i += istep
+ j += jstep
+ cr.fill()
+
+ def _spacing(self):
+ return self.spacing
+
+ def _segmentForTime(self, time):
+ """Return the segment for the specified time stamp. For some stream
+ types, the segment duration will depend on the current zoom ratio,
+ while others may only care about the timestamp. The value returned
+ here will be used as the key which identifies the thumbnail in the
+ thumbnail cache"""
+
+ raise NotImplementedError
+
+ def _thumbForTime(self, cr, time, x, y):
+ segment = self._segment_for_time(time)
+ if segment in self._cache:
+ surface = self._cache[segment]
+ else:
+ self._requestThumbnail(segment)
+ surface = self.default_thumb
+ cr.set_source_surface(surface, x, y)
+
+ def _finishThumbnail(self, surface, segment):
+ """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
+
+ if segment != waiting:
+ segment = waiting
+
+ self._cache[segment] = surface
+ self.emit("update", segment)
+
+ if segment in self._queue:
+ self._queue.remove(segment)
+ self._nextThumbnail()
+ return False
+
+ 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
+
+ def _requestThumbnail(self, segment):
+ """Queue a thumbnail request for the given segment"""
+
+ if (segment not in self._queue) and (len(self._queue) <=
+ self.max_requests):
+ if self._queue:
+ self._queue.append(segment)
+ else:
+ self._queue.append(segment)
+ self._nextThumbnail()
+
+ def _startThumbnail(self, segment):
+ """Start processing segment. Subclasses should override
+ this method to perform whatever action on the pipeline is necessary.
+ 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
+ message handler or other callback."""
+ self.waiting_timestamp = segment
+
+ def _connectSettings(self, settings):
+ Previewer._connectSettings(self, settings)
+ self.spacing = settings.thumbnailSpacingHint
+ self._cache = ThumbnailCache(size=settings.thumbnailCacheSize)
+ self.max_requests = settings.thumbnailMaxRequests
+ settings.connect("thumbnailSpacingHintChanged",
+ self._thumbnailSpacingHintChanged)
+
+ def _thumbnailSpacingHintChanged(self, settings):
+ self.spacing = settings.thumbnailSpacingHint
+ self.emit("update", None)
+
+
+class RandomAccessVideoPreviewer(RandomAccessPreviewer):
+
+ @property
+ def twidth(self):
+ return int(self.aspect * self.theight)
+
+ @property
+ 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_)
+ self.tstep = Zoomable.pixelToNsAt(self.twidth, Zoomable.max_zoom)
+ if rate.num:
+ frame_duration = (gst.SECOND * rate.denom) / rate.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)
+ self.videopipeline.set_state(gst.STATE_PAUSED)
+
+ def _segment_for_time(self, time):
+ # quantize thumbnail timestamps to maximum granularity
+ return utils.quantize(time, self.tperiod)
+
+ def _thumbnailCb(self, unused_thsink, pixbuf, timestamp):
+ gobject.idle_add(self._finishThumbnail, pixbuf, timestamp)
+
+ def _startThumbnail(self, timestamp):
+ RandomAccessPreviewer._startThumbnail(self, timestamp)
+ return self.videopipeline.seek(1.0,
+ gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_ACCURATE,
+ gst.SEEK_TYPE_SET, timestamp,
+ gst.SEEK_TYPE_NONE, -1)
+
+ def _connectSettings(self, settings):
+ RandomAccessPreviewer._connectSettings(self, settings)
+ settings.connect("showThumbnailsChanged", self._showThumbsChanged)
+ settings.connect("thumbnailPeriodChanged",
+ self._thumbnailPeriodChanged)
+ self._view = settings.showThumbnails
+ self.tperiod = settings.thumbnailPeriod
+
+ def _showThumbsChanged(self, settings):
+ self._view = settings.showThumbnails
+ self.emit("update", None)
+
+ def _thumbnailPeriodChanged(self, settings):
+ self.tperiod = settings.thumbnailPeriod
+ self.emit("update", None)
+
+
+class StillImagePreviewer(RandomAccessVideoPreviewer):
+ def _thumbForTime(self, cr, time, x, y):
+ return RandomAccessVideoPreviewer._thumbForTime(self, cr, 0L, x, y)
+
+
+class RandomAccessAudioPreviewer(RandomAccessPreviewer):
+
+ def __init__(self, instance, factory, stream_):
+ self.tdur = 30 * gst.SECOND
+ self.base_width = int(Zoomable.max_zoom)
+ RandomAccessPreviewer.__init__(self, instance, factory, stream_)
+
+ @property
+ def twidth(self):
+ return Zoomable.nsToPixel(self.tdur)
+
+ def _pipelineInit(self, factory, sbin):
+ self.spacing = 0
+
+ self.audioSink = ArraySink()
+ conv = gst.element_factory_make("audioconvert")
+ self.audioPipeline = utils.pipeline({
+ sbin: conv,
+ conv: self.audioSink,
+ self.audioSink: None})
+ bus = self.audioPipeline.get_bus()
+ bus.add_signal_watch()
+ bus.connect("message::segment-done", self._busMessageSegmentDoneCb)
+ bus.connect("message::error", self._busMessageErrorCb)
+
+ self._audio_cur = None
+ self.audioPipeline.set_state(gst.STATE_PAUSED)
+
+ def _spacing(self):
+ return 0
+
+ def _segment_for_time(self, time):
+ # for audio files, we need to know the duration the segment spans
+ return time - (time % self.tdur), self.tdur
+
+ def _busMessageSegmentDoneCb(self, bus, message):
+ self.debug("segment done")
+ self._finishWaveform()
+
+ def _busMessageErrorCb(self, bus, message):
+ error, debug = message.parse_error()
+ self.error("Event bus error: %s: %s", str(error), str(debug))
+
+ return gst.BUS_PASS
+
+ def _startThumbnail(self, (timestamp, duration)):
+ RandomAccessPreviewer._startThumbnail(self, (timestamp, duration))
+ self._audio_cur = timestamp, duration
+ res = self.audioPipeline.seek(1.0,
+ gst.FORMAT_TIME,
+ gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_ACCURATE | gst.SEEK_FLAG_SEGMENT,
+ gst.SEEK_TYPE_SET, timestamp,
+ gst.SEEK_TYPE_SET, timestamp + duration)
+ if not res:
+ self.warning("seek failed %s", timestamp)
+ self.audioPipeline.set_state(gst.STATE_PLAYING)
+
+ return res
+
+ def _finishWaveform(self):
+ surfaces = []
+ surface = cairo.ImageSurface(cairo.FORMAT_A8,
+ self.base_width, self.theight)
+ cr = cairo.Context(surface)
+ self._plotWaveform(cr, self.base_width)
+ self.audioSink.reset()
+
+ for width in [25, 100, 200]:
+ scaled = cairo.ImageSurface(cairo.FORMAT_A8,
+ width, self.theight)
+ cr = cairo.Context(scaled)
+ matrix = cairo.Matrix()
+ matrix.scale(self.base_width / width, 1.0)
+ cr.set_source_surface(surface)
+ cr.get_source().set_matrix(matrix)
+ cr.rectangle(0, 0, width, self.theight)
+ cr.fill()
+ surfaces.append(scaled)
+ surfaces.append(surface)
+ gobject.idle_add(self._finishThumbnail, surfaces, self._audio_cur)
+
+ def _plotWaveform(self, cr, base_width):
+ # clear background
+ cr.set_source_rgba(1, 1, 1, 0.0)
+ cr.rectangle(0, 0, base_width, self.theight)
+ cr.fill()
+
+ samples = self.audioSink.samples
+
+ if not samples:
+ return
+
+ # find the samples-per-pixel ratio
+ spp = len(samples) / base_width
+ if spp == 0:
+ spp = 1
+ channels = self.audioSink.channels
+ stride = spp * channels
+ hscale = self.theight / (2 * channels)
+
+ # plot points from min to max over a given hunk
+ chan = 0
+ y = hscale
+ while chan < channels:
+ i = chan
+ x = 0
+ while i < len(samples):
+ slice = samples[i:i + stride:channels]
+ min_ = min(slice)
+ max_ = max(slice)
+ cr.move_to(x, y - (min_ * hscale))
+ cr.line_to(x, y - (max_ * hscale))
+ i += spp
+ x += 1
+ y += 2 * hscale
+ chan += 1
+
+ # Draw!
+ cr.set_source_rgba(0, 0, 0, 1.0)
+ cr.stroke()
+
+ def _thumbForTime(self, cr, time, x, y):
+ segment = self._segment_for_time(time)
+ twidth = self.twidth
+ if segment in self._cache:
+ surfaces = self._cache[segment]
+ if twidth > 200:
+ surface = surfaces[3]
+ base_width = self.base_width
+ elif twidth <= 200:
+ surface = surfaces[2]
+ base_width = 200
+ elif twidth <= 100:
+ surface = surfaces[1]
+ base_width = 100
+ elif twidth <= 25:
+ surface = surfaces[0]
+ base_width = 25
+ x_scale = float(base_width) / self.twidth
+ cr.set_source_surface(surface)
+ matrix = cairo.Matrix()
+ matrix.scale(x_scale, 1.0)
+ matrix.translate(-x, -y)
+ cr.get_source().set_matrix(matrix)
+ else:
+ self._requestThumbnail(segment)
+ cr.set_source_rgba(0.0, 0.0, 0.0, 0.0)
+
+ def _connectSettings(self, settings):
+ RandomAccessPreviewer._connectSettings(self, settings)
+ self._view = settings.showWaveforms
+ settings.connect("showWaveformsChanged", self._showWaveformsChanged)
+
+ def _showWaveformsChanged(self, settings):
+ self._view = settings.showWaveforms
+ self.emit("update", None)
def between(a, b, c):
@@ -43,6 +613,12 @@ def intersect(b1, b2):
class Preview(goocanvas.ItemSimple, goocanvas.Item, Zoomable):
+ """
+ Custom canvas item for timeline object previews. This code is just a thin
+ canvas-item wrapper which ensures that the preview is updated appropriately.
+ The actual drawing is done by the pitivi.previewer.Previewer class.
+ """
+
__gtype_name__ = 'Preview'
def __init__(self, instance, element, height=46, **kwargs):
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 209a18d..395d17e 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -29,7 +29,7 @@ pitivi/ui/filechooserpreview.py
pitivi/ui/filelisterrordialog.py
pitivi/ui/mainwindow.py
pitivi/ui/prefs.py
-pitivi/ui/previewer.py
+pitivi/ui/preview.py
pitivi/ui/viewer.py
pitivi/utils/misc.py
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]