1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
|
# PiTiVi , Non-linear video editor
#
# previewer.py
#
# Copyright (c) 2005, Edward Hervey <bilboed@bilboed.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
"""
Utility tools and classes for easy generation of previews
"""
import gobject
import gst
import cairo
import os
from gettext import gettext as _
import pitivi.utils as utils
from pitivi.configure import get_pixmap_dir
from pitivi.elements.singledecodebin import SingleDecodeBin
from pitivi.elements.thumbnailsink import CairoSurfaceThumbnailSink
from pitivi.elements.arraysink import ArraySink
from pitivi.signalinterface import Signallable
import pitivi.stream as stream
from pitivi.settings import GlobalSettings
from pitivi.ui.zoominterface import Zoomable
from pitivi.log.loggable import Loggable
from pitivi.factories.file import PictureFileSourceFactory
from pitivi.thumbnailcache import ThumbnailCache
from pitivi.ui.prefs import PreferencesDialog
from pitivi.receiver import receiver, handler
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):
__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()
print "Event bus error:", 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)
|