~ubuntu-branches/ubuntu/trusty/pitivi/trusty

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)