~timo-jyrinki/ubuntu/trusty/pitivi/backport_utopic_fixes

« back to all changes in this revision

Viewing changes to pitivi/timeline/previewers.py

  • Committer: Package Import Robot
  • Author(s): Sebastian Dröge
  • Date: 2014-04-05 15:28:16 UTC
  • mfrom: (6.1.13 sid)
  • Revision ID: package-import@ubuntu.com-20140405152816-6lijoax4cngiz5j5
Tags: 0.93-3
* debian/control:
  + Depend on python-gi (>= 3.10), older versions do not work
    with pitivi (Closes: #732813).
  + Add missing dependency on gir1.2-clutter-gst-2.0 (Closes: #743692).
  + Add suggests on gir1.2-notify-0.7 and gir1.2-gnomedesktop-3.0.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
# Pitivi video editor
 
3
#
 
4
#       pitivi/timeline/previewers.py
 
5
#
 
6
# Copyright (c) 2013, Daniel Thul <daniel.thul@gmail.com>
 
7
#
 
8
# This program is free software; you can redistribute it and/or
 
9
# modify it under the terms of the GNU Lesser General Public
 
10
# License as published by the Free Software Foundation; either
 
11
# version 2.1 of the License, or (at your option) any later version.
 
12
#
 
13
# This program is distributed in the hope that it will be useful,
 
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 
16
# Lesser General Public License for more details.
 
17
#
 
18
# You should have received a copy of the GNU Lesser General Public
 
19
# License along with this program; if not, write to the
 
20
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 
21
# Boston, MA 02110-1301, USA.
 
22
 
 
23
from datetime import datetime, timedelta
 
24
from gi.repository import Clutter, Gst, GLib, GdkPixbuf, Cogl, GES
 
25
from random import randrange
 
26
import cairo
 
27
import numpy
 
28
import os
 
29
import pickle
 
30
import sqlite3
 
31
 
 
32
# Our C module optimizing waveforms rendering
 
33
import renderer
 
34
 
 
35
from pitivi.settings import get_dir, xdg_cache_home
 
36
from pitivi.utils.signal import Signallable
 
37
from pitivi.utils.loggable import Loggable
 
38
from pitivi.utils.misc import binary_search, filename_from_uri, quantize, quote_uri, hash_file, format_ns
 
39
from pitivi.utils.system import CPUUsageTracker
 
40
from pitivi.utils.timeline import Zoomable
 
41
from pitivi.utils.ui import CONTROL_WIDTH
 
42
from pitivi.utils.ui import EXPANDED_SIZE
 
43
 
 
44
 
 
45
WAVEFORMS_CPU_USAGE = 30
 
46
 
 
47
# A little lower as it's more fluctuating
 
48
THUMBNAILS_CPU_USAGE = 20
 
49
 
 
50
THUMB_MARGIN_PX = 3
 
51
WAVEFORM_UPDATE_INTERVAL = timedelta(microseconds=500000)
 
52
MARGIN = 500  # For the waveforms, ensures we always have a little extra surface when scrolling while playing.
 
53
 
 
54
"""
 
55
Convention throughout this file:
 
56
Every GES element which name could be mistaken with a UI element
 
57
is prefixed with a little b, example : bTimeline
 
58
"""
 
59
 
 
60
 
 
61
class PreviewGeneratorManager():
 
62
    """
 
63
    Manage the execution of PreviewGenerators
 
64
    """
 
65
    def __init__(self):
 
66
        self._cpipeline = {
 
67
            GES.TrackType.AUDIO: None,
 
68
            GES.TrackType.VIDEO: None
 
69
        }
 
70
        self._pipelines = {
 
71
            GES.TrackType.AUDIO: [],
 
72
            GES.TrackType.VIDEO: []
 
73
        }
 
74
 
 
75
    def addPipeline(self, pipeline):
 
76
        track_type = pipeline.track_type
 
77
 
 
78
        if pipeline in self._pipelines[track_type] or \
 
79
                pipeline is self._cpipeline[track_type]:
 
80
            return
 
81
 
 
82
        if not self._pipelines[track_type] and self._cpipeline[track_type] is None:
 
83
            self._setPipeline(pipeline)
 
84
        else:
 
85
            self._pipelines[track_type].insert(0, pipeline)
 
86
 
 
87
    def _setPipeline(self, pipeline):
 
88
        self._cpipeline[pipeline.track_type] = pipeline
 
89
        PreviewGenerator.connect(pipeline, "done", self._nextPipeline)
 
90
        pipeline.startGeneration()
 
91
 
 
92
    def _nextPipeline(self, controlled):
 
93
        track_type = controlled.track_type
 
94
        if self._cpipeline[track_type]:
 
95
            PreviewGenerator.disconnect_by_function(self._cpipeline[track_type],
 
96
                                                    self._nextPipeline)
 
97
            self._cpipeline[track_type] = None
 
98
 
 
99
        if self._pipelines[track_type]:
 
100
            self._setPipeline(self._pipelines[track_type].pop())
 
101
 
 
102
 
 
103
class PreviewGenerator(Signallable):
 
104
    """
 
105
    Interface to be implemented by classes that generate previews
 
106
    It is need to implement it so PreviewGeneratorManager can manage
 
107
    those classes
 
108
    """
 
109
 
 
110
    # We only want one instance of PreviewGeneratorManager to be used for
 
111
    # all the generators.
 
112
    __manager = PreviewGeneratorManager()
 
113
 
 
114
    __signals__ = {
 
115
        "done": [],
 
116
        "error": [],
 
117
    }
 
118
 
 
119
    def __init__(self, track_type):
 
120
        """
 
121
        @param track_type : GES.TrackType.*
 
122
        """
 
123
        Signallable.__init__(self)
 
124
        self.track_type = track_type
 
125
 
 
126
    def startGeneration(self):
 
127
        raise NotImplemented
 
128
 
 
129
    def stopGeneration(self):
 
130
        raise NotImplemented
 
131
 
 
132
    def becomeControlled(self):
 
133
        """
 
134
        Let the PreviewGeneratorManager control our execution
 
135
        """
 
136
        PreviewGenerator.__manager.addPipeline(self)
 
137
 
 
138
 
 
139
class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
 
140
    def __init__(self, bElement, timeline):
 
141
        """
 
142
        @param bElement : the backend GES.TrackElement
 
143
        @param track : the track to which the bElement belongs
 
144
        @param timeline : the containing graphic timeline.
 
145
        """
 
146
        Zoomable.__init__(self)
 
147
        Clutter.ScrollActor.__init__(self)
 
148
        Loggable.__init__(self)
 
149
        PreviewGenerator.__init__(self, GES.TrackType.VIDEO)
 
150
 
 
151
        # Variables related to the timeline objects
 
152
        self.timeline = timeline
 
153
        self.bElement = bElement
 
154
        self.uri = quote_uri(bElement.props.uri)  # Guard against malformed URIs
 
155
        self.duration = bElement.props.duration
 
156
 
 
157
        # Variables related to thumbnailing
 
158
        self.wishlist = []
 
159
        self._callback_id = None
 
160
        self._thumb_cb_id = None
 
161
        self._allAnimated = False
 
162
        self._running = False
 
163
        # We should have one thumbnail per thumb_period.
 
164
        # TODO: get this from the user settings
 
165
        self.thumb_period = long(0.5 * Gst.SECOND)
 
166
        self.thumb_height = EXPANDED_SIZE - 2 * THUMB_MARGIN_PX
 
167
        self.thumb_width = None  # will be set by self._setupPipeline()
 
168
 
 
169
        # Maps (quantized) times to Thumbnail objects
 
170
        self.thumbs = {}
 
171
        self.thumb_cache = get_cache_for_uri(self.uri)
 
172
 
 
173
        self.cpu_usage_tracker = CPUUsageTracker()
 
174
        self.interval = 500  # Every 0.5 second, reevaluate the situation
 
175
 
 
176
        # Connect signals and fire things up
 
177
        self.timeline.connect("scrolled", self._scrollCb)
 
178
        self.bElement.connect("notify::duration", self._durationChangedCb)
 
179
        self.bElement.connect("notify::in-point", self._inpointChangedCb)
 
180
        self.bElement.connect("notify::start", self._startChangedCb)
 
181
 
 
182
        self.pipeline = None
 
183
        self.becomeControlled()
 
184
 
 
185
    # Internal API
 
186
 
 
187
    def _update(self, unused_msg_source=None):
 
188
        if self._callback_id:
 
189
            GLib.source_remove(self._callback_id)
 
190
 
 
191
        if self.thumb_width:
 
192
            self._addVisibleThumbnails()
 
193
            if self.wishlist:
 
194
                self.becomeControlled()
 
195
 
 
196
    def _setupPipeline(self):
 
197
        """
 
198
        Create the pipeline.
 
199
 
 
200
        It has the form "playbin ! thumbnailsink" where thumbnailsink
 
201
        is a Bin made out of "videorate ! capsfilter ! gdkpixbufsink"
 
202
        """
 
203
        # TODO: don't hardcode framerate
 
204
        self.pipeline = Gst.parse_launch(
 
205
            "uridecodebin uri={uri} name=decode ! "
 
206
            "videoconvert ! "
 
207
            "videorate ! "
 
208
            "videoscale method=lanczos ! "
 
209
            "capsfilter caps=video/x-raw,format=(string)RGBA,height=(int){height},"
 
210
            "pixel-aspect-ratio=(fraction)1/1,framerate=2/1 ! "
 
211
            "gdkpixbufsink name=gdkpixbufsink".format(uri=self.uri, height=self.thumb_height))
 
212
 
 
213
        # get the gdkpixbufsink and the sinkpad
 
214
        self.gdkpixbufsink = self.pipeline.get_by_name("gdkpixbufsink")
 
215
        sinkpad = self.gdkpixbufsink.get_static_pad("sink")
 
216
 
 
217
        self.pipeline.set_state(Gst.State.PAUSED)
 
218
 
 
219
        # Wait for the pipeline to be prerolled so we can check the width
 
220
        # that the thumbnails will have and set the aspect ratio accordingly
 
221
        # as well as getting the framerate of the video:
 
222
        change_return = self.pipeline.get_state(Gst.CLOCK_TIME_NONE)
 
223
        if Gst.StateChangeReturn.SUCCESS == change_return[0]:
 
224
            neg_caps = sinkpad.get_current_caps()[0]
 
225
            self.thumb_width = neg_caps["width"]
 
226
        else:
 
227
            # the pipeline couldn't be prerolled so we can't determine the
 
228
            # correct values. Set sane defaults (this should never happen)
 
229
            self.warning("Couldn't preroll the pipeline")
 
230
            # assume 16:9 aspect ratio
 
231
            self.thumb_width = 16 * self.thumb_height / 9
 
232
 
 
233
        decode = self.pipeline.get_by_name("decode")
 
234
        decode.connect("autoplug-select", self._autoplugSelectCb)
 
235
 
 
236
        # pop all messages from the bus so we won't be flooded with messages
 
237
        # from the prerolling phase
 
238
        while self.pipeline.get_bus().pop():
 
239
            continue
 
240
        # add a message handler that listens for the created pixbufs
 
241
        self.pipeline.get_bus().add_signal_watch()
 
242
        self.pipeline.get_bus().connect("message", self.bus_message_handler)
 
243
 
 
244
    def _checkCPU(self):
 
245
        """
 
246
        Check the CPU usage and adjust the time interval (+10 or -10%) at
 
247
        which the next thumbnail will be generated. Even then, it will only
 
248
        happen when the gobject loop is idle to avoid blocking the UI.
 
249
        """
 
250
        usage_percent = self.cpu_usage_tracker.usage()
 
251
        if usage_percent < THUMBNAILS_CPU_USAGE:
 
252
            self.interval *= 0.9
 
253
            self.log('Thumbnailing sped up (+10%%) to a %.1f ms interval for "%s"' % (self.interval, filename_from_uri(self.uri)))
 
254
        else:
 
255
            self.interval *= 1.1
 
256
            self.log('Thumbnailing slowed down (-10%%) to a %.1f ms interval for "%s"' % (self.interval, filename_from_uri(self.uri)))
 
257
        self.cpu_usage_tracker.reset()
 
258
        self._thumb_cb_id = GLib.timeout_add(self.interval, self._create_next_thumb)
 
259
 
 
260
    def _startThumbnailingWhenIdle(self):
 
261
        self.debug('Waiting for UI to become idle for: %s', filename_from_uri(self.uri))
 
262
        GLib.idle_add(self._startThumbnailing, priority=GLib.PRIORITY_LOW)
 
263
 
 
264
    def _startThumbnailing(self):
 
265
        if not self.pipeline:
 
266
            # Can happen if stopGeneration is called because the clip has been
 
267
            # removed from the timeline after the PreviewGeneratorManager
 
268
            # started this job.
 
269
            return
 
270
        self.debug('Now generating thumbnails for: %s', filename_from_uri(self.uri))
 
271
        query_success, duration = self.pipeline.query_duration(Gst.Format.TIME)
 
272
        if not query_success or duration == -1:
 
273
            self.debug("Could not determine duration of: %s", self.uri)
 
274
            duration = self.duration
 
275
        else:
 
276
            self.duration = duration
 
277
 
 
278
        self.queue = range(0, duration, self.thumb_period)
 
279
 
 
280
        self._checkCPU()
 
281
 
 
282
        self._addVisibleThumbnails()
 
283
        # Save periodically to avoid the common situation where the user exits
 
284
        # the app before a long clip has been fully thumbnailed.
 
285
        # Spread timeouts between 30-80 secs to avoid concurrent disk writes.
 
286
        random_time = randrange(30, 80)
 
287
        GLib.timeout_add_seconds(random_time, self._autosave)
 
288
 
 
289
        # Remove the GSource
 
290
        return False
 
291
 
 
292
    def _create_next_thumb(self):
 
293
        if not self.wishlist or not self.queue:
 
294
            # nothing left to do
 
295
            self.debug("Thumbnails generation complete")
 
296
            self.stopGeneration()
 
297
            self.thumb_cache.commit()
 
298
            return
 
299
        else:
 
300
            self.debug("Missing %d thumbs", len(self.wishlist))
 
301
 
 
302
        wish = self._get_wish()
 
303
        if wish:
 
304
            time = wish
 
305
            self.queue.remove(wish)
 
306
        else:
 
307
            time = self.queue.pop(0)
 
308
        self.log('Creating thumb for "%s"' % filename_from_uri(self.uri))
 
309
        # append the time to the end of the queue so that if this seek fails
 
310
        # another try will be started later
 
311
        self.queue.append(time)
 
312
        self.pipeline.seek(1.0,
 
313
                           Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
 
314
                           Gst.SeekType.SET, time,
 
315
                           Gst.SeekType.NONE, -1)
 
316
 
 
317
        # Remove the GSource
 
318
        return False
 
319
 
 
320
    def _autosave(self):
 
321
        if self.wishlist:
 
322
            self.log("Periodic thumbnail autosave")
 
323
            self.thumb_cache.commit()
 
324
            return True
 
325
        else:
 
326
            return False  # Stop the timer
 
327
 
 
328
    def _get_thumb_duration(self):
 
329
        thumb_duration_tmp = Zoomable.pixelToNs(self.thumb_width + THUMB_MARGIN_PX)
 
330
        # quantize thumb length to thumb_period
 
331
        thumb_duration = quantize(thumb_duration_tmp, self.thumb_period)
 
332
        # make sure that the thumb duration after the quantization isn't smaller than before
 
333
        if thumb_duration < thumb_duration_tmp:
 
334
            thumb_duration += self.thumb_period
 
335
        # make sure that we don't show thumbnails more often than thumb_period
 
336
        return max(thumb_duration, self.thumb_period)
 
337
 
 
338
    def _addVisibleThumbnails(self):
 
339
        """
 
340
        Get the thumbnails to be displayed in the currently visible clip portion
 
341
        """
 
342
        self.remove_all_children()
 
343
        old_thumbs = self.thumbs
 
344
        self.thumbs = {}
 
345
        self.wishlist = []
 
346
 
 
347
        thumb_duration = self._get_thumb_duration()
 
348
        element_left, element_right = self._get_visible_range()
 
349
        element_left = quantize(element_left, thumb_duration)
 
350
 
 
351
        for current_time in range(element_left, element_right, thumb_duration):
 
352
            thumb = Thumbnail(self.thumb_width, self.thumb_height)
 
353
            thumb.set_position(Zoomable.nsToPixel(current_time), THUMB_MARGIN_PX)
 
354
            self.add_child(thumb)
 
355
            self.thumbs[current_time] = thumb
 
356
            if current_time in self.thumb_cache:
 
357
                gdkpixbuf = self.thumb_cache[current_time]
 
358
                if self._allAnimated or current_time not in old_thumbs:
 
359
                    self.thumbs[current_time].set_from_gdkpixbuf_animated(gdkpixbuf)
 
360
                else:
 
361
                    self.thumbs[current_time].set_from_gdkpixbuf(gdkpixbuf)
 
362
            else:
 
363
                self.wishlist.append(current_time)
 
364
        self._allAnimated = False
 
365
 
 
366
    def _get_wish(self):
 
367
        """
 
368
        Returns a wish that is also in the queue, or None if no such wish exists
 
369
        """
 
370
        while True:
 
371
            if not self.wishlist:
 
372
                return None
 
373
            wish = self.wishlist.pop(0)
 
374
            if wish in self.queue:
 
375
                return wish
 
376
 
 
377
    def _setThumbnail(self, time, pixbuf):
 
378
        # Q: Is "time" guaranteed to be nanosecond precise?
 
379
        # A: Not always.
 
380
        # => __tim says: "that's how it should be"
 
381
        # => also see gst-plugins-good/tests/icles/gdkpixbufsink-test
 
382
        # => Daniel: It is *not* nanosecond precise when we remove the videorate
 
383
        #            element from the pipeline
 
384
        # => thiblahute: not the case with mpegts
 
385
        original_time = time
 
386
        if time in self.thumbs:
 
387
            thumb = self.thumbs[time]
 
388
        else:
 
389
            sorted_times = sorted(self.thumbs.keys())
 
390
            index = binary_search(sorted_times, time)
 
391
            time = sorted_times[index]
 
392
            thumb = self.thumbs[time]
 
393
            if thumb.has_pixel_data:
 
394
                # If this happens, it means the precision of the thumbnail
 
395
                # generator is not good enough for the current thumbnail
 
396
                # interval.
 
397
                # We could consider shifting the thumbnails, but seems like
 
398
                # too much trouble for something which does not happen in
 
399
                # practice. My last words..
 
400
                self.fixme("Thumbnail is already set for time: %s, %s",
 
401
                           format_ns(time), format_ns(original_time))
 
402
                return
 
403
        thumb.set_from_gdkpixbuf_animated(pixbuf)
 
404
        if time in self.queue:
 
405
            self.queue.remove(time)
 
406
        self.thumb_cache[time] = pixbuf
 
407
 
 
408
    # Interface (Zoomable)
 
409
 
 
410
    def zoomChanged(self):
 
411
        self.remove_all_children()
 
412
        self._allAnimated = True
 
413
        self._update()
 
414
 
 
415
    def _get_visible_range(self):
 
416
        # Shortcut/convenience variables:
 
417
        start = self.bElement.props.start
 
418
        in_point = self.bElement.props.in_point
 
419
        duration = self.bElement.props.duration
 
420
        timeline_left, timeline_right = self._get_visible_timeline_range()
 
421
 
 
422
        element_left = timeline_left - start + in_point
 
423
        element_left = max(element_left, in_point)
 
424
        element_right = timeline_right - start + in_point
 
425
        element_right = min(element_right, in_point + duration)
 
426
 
 
427
        return (element_left, element_right)
 
428
 
 
429
    # TODO: move to Timeline or to utils
 
430
    def _get_visible_timeline_range(self):
 
431
        # determine the visible left edge of the timeline
 
432
        # TODO: isn't there some easier way to get the scroll point of the ScrollActor?
 
433
        # timeline_left = -(self.timeline.get_transform().xw - self.timeline.props.x)
 
434
        timeline_left = self.timeline.get_scroll_point().x
 
435
 
 
436
        # determine the width of the pipeline
 
437
        # by intersecting the timeline's and the stage's allocation
 
438
        timeline_allocation = self.timeline.props.allocation
 
439
        stage_allocation = self.timeline.get_stage().props.allocation
 
440
 
 
441
        timeline_rect = Clutter.Rect()
 
442
        timeline_rect.init(timeline_allocation.x1,
 
443
                           timeline_allocation.y1,
 
444
                           timeline_allocation.x2 - timeline_allocation.x1,
 
445
                           timeline_allocation.y2 - timeline_allocation.y1)
 
446
 
 
447
        stage_rect = Clutter.Rect()
 
448
        stage_rect.init(stage_allocation.x1,
 
449
                        stage_allocation.y1,
 
450
                        stage_allocation.x2 - stage_allocation.x1,
 
451
                        stage_allocation.y2 - stage_allocation.y1)
 
452
 
 
453
        has_intersection, intersection = timeline_rect.intersection(stage_rect)
 
454
 
 
455
        if not has_intersection:
 
456
            return (0, 0)
 
457
 
 
458
        timeline_width = intersection.size.width
 
459
 
 
460
        # determine the visible right edge of the timeline
 
461
        timeline_right = timeline_left + timeline_width
 
462
 
 
463
        # convert to nanoseconds
 
464
        time_left = Zoomable.pixelToNs(timeline_left)
 
465
        time_right = Zoomable.pixelToNs(timeline_right)
 
466
 
 
467
        return (time_left, time_right)
 
468
 
 
469
    # Callbacks
 
470
 
 
471
    def bus_message_handler(self, unused_bus, message):
 
472
        if message.type == Gst.MessageType.ELEMENT and \
 
473
                message.src == self.gdkpixbufsink:
 
474
            struct = message.get_structure()
 
475
            struct_name = struct.get_name()
 
476
            if struct_name == "preroll-pixbuf":
 
477
                stream_time = struct.get_value("stream-time")
 
478
                pixbuf = struct.get_value("pixbuf")
 
479
                self._setThumbnail(stream_time, pixbuf)
 
480
        elif message.type == Gst.MessageType.ASYNC_DONE and \
 
481
                message.src == self.pipeline:
 
482
            self._checkCPU()
 
483
        return Gst.BusSyncReply.PASS
 
484
 
 
485
    def _autoplugSelectCb(self, unused_decode, unused_pad, unused_caps, factory):
 
486
        # Don't plug audio decoders / parsers.
 
487
        if "Audio" in factory.get_klass():
 
488
            return True
 
489
        return False
 
490
 
 
491
    def _scrollCb(self, unused):
 
492
        self._update()
 
493
 
 
494
    def _startChangedCb(self, unused_bElement, unused_value):
 
495
        self._update()
 
496
 
 
497
    def _inpointChangedCb(self, unused_bElement, unused_value):
 
498
        position = Clutter.Point()
 
499
        position.x = Zoomable.nsToPixel(self.bElement.props.in_point)
 
500
        self.scroll_to_point(position)
 
501
        self._update()
 
502
 
 
503
    def _durationChangedCb(self, unused_bElement, unused_value):
 
504
        new_duration = max(self.duration, self.bElement.props.duration)
 
505
        if new_duration > self.duration:
 
506
            self.duration = new_duration
 
507
            self._update()
 
508
 
 
509
    def startGeneration(self):
 
510
        self._setupPipeline()
 
511
        self._startThumbnailingWhenIdle()
 
512
 
 
513
    def stopGeneration(self):
 
514
        if self._thumb_cb_id:
 
515
            GLib.source_remove(self._thumb_cb_id)
 
516
            self._thumb_cb_id = None
 
517
 
 
518
        if self.pipeline:
 
519
            self.pipeline.set_state(Gst.State.NULL)
 
520
            self.pipeline.get_state(Gst.CLOCK_TIME_NONE)
 
521
            self.pipeline = None
 
522
        PreviewGenerator.emit(self, "done")
 
523
 
 
524
    def cleanup(self):
 
525
        self.stopGeneration()
 
526
        Zoomable.__del__(self)
 
527
 
 
528
 
 
529
class Thumbnail(Clutter.Actor):
 
530
    def __init__(self, width, height):
 
531
        Clutter.Actor.__init__(self)
 
532
        image = Clutter.Image.new()
 
533
        self.props.content = image
 
534
        self.width = width
 
535
        self.height = height
 
536
        #self.set_background_color(Clutter.Color.new(0, 100, 150, 100))
 
537
        self.set_opacity(0)
 
538
        self.set_size(self.width, self.height)
 
539
        self.has_pixel_data = False
 
540
 
 
541
    def set_from_gdkpixbuf(self, gdkpixbuf):
 
542
        row_stride = gdkpixbuf.get_rowstride()
 
543
        pixel_data = gdkpixbuf.get_pixels()
 
544
        alpha = gdkpixbuf.get_has_alpha()
 
545
        self.has_pixel_data = True
 
546
        if alpha:
 
547
            self.props.content.set_data(pixel_data, Cogl.PixelFormat.RGBA_8888,
 
548
                                        self.width, self.height, row_stride)
 
549
        else:
 
550
            self.props.content.set_data(pixel_data, Cogl.PixelFormat.RGB_888,
 
551
                                        self.width, self.height, row_stride)
 
552
        self.set_opacity(255)
 
553
 
 
554
    def set_from_gdkpixbuf_animated(self, gdkpixbuf):
 
555
        self.save_easing_state()
 
556
        self.set_easing_duration(750)
 
557
        self.set_from_gdkpixbuf(gdkpixbuf)
 
558
        self.restore_easing_state()
 
559
 
 
560
 
 
561
caches = {}
 
562
 
 
563
 
 
564
def get_cache_for_uri(uri):
 
565
    if uri in caches:
 
566
        return caches[uri]
 
567
    else:
 
568
        cache = ThumbnailCache(uri)
 
569
        caches[uri] = cache
 
570
        return cache
 
571
 
 
572
 
 
573
class ThumbnailCache(Loggable):
 
574
 
 
575
    """Caches thumbnails by key using LRU policy, implemented with heapq.
 
576
 
 
577
    Uses a two stage caching mechanism. A limited number of elements are
 
578
    held in memory, the rest is being cached on disk using an sqlite db."""
 
579
 
 
580
    def __init__(self, uri):
 
581
        Loggable.__init__(self)
 
582
        self._filehash = hash_file(Gst.uri_get_location(uri))
 
583
        self._filename = filename_from_uri(uri)
 
584
        thumbs_cache_dir = get_dir(os.path.join(xdg_cache_home(), "thumbs"))
 
585
        dbfile = os.path.join(thumbs_cache_dir, self._filehash)
 
586
        self._db = sqlite3.connect(dbfile)
 
587
        self._cur = self._db.cursor()  # Use this for normal db operations
 
588
        self._cur.execute("CREATE TABLE IF NOT EXISTS Thumbs\
 
589
                          (Time INTEGER NOT NULL PRIMARY KEY,\
 
590
                          Jpeg BLOB NOT NULL)")
 
591
 
 
592
    def __contains__(self, key):
 
593
        # check if item is present in on disk cache
 
594
        self._cur.execute("SELECT Time FROM Thumbs WHERE Time = ?", (key,))
 
595
        if self._cur.fetchone():
 
596
            return True
 
597
        return False
 
598
 
 
599
    def __getitem__(self, key):
 
600
        self._cur.execute("SELECT * FROM Thumbs WHERE Time = ?", (key,))
 
601
        row = self._cur.fetchone()
 
602
        if not row:
 
603
            raise KeyError(key)
 
604
        jpeg = row[1]
 
605
        loader = GdkPixbuf.PixbufLoader.new()
 
606
        # TODO: what do to if any of the following calls fails?
 
607
        loader.write(jpeg)
 
608
        loader.close()
 
609
        pixbuf = loader.get_pixbuf()
 
610
        return pixbuf
 
611
 
 
612
    def __setitem__(self, key, value):
 
613
        success, jpeg = value.save_to_bufferv("jpeg", ["quality", None], ["90"])
 
614
        if not success:
 
615
            self.warning("JPEG compression failed")
 
616
            return
 
617
        blob = sqlite3.Binary(jpeg)
 
618
        #Replace if the key already existed
 
619
        self._cur.execute("DELETE FROM Thumbs WHERE  time=?", (key,))
 
620
        self._cur.execute("INSERT INTO Thumbs VALUES (?,?)", (key, blob,))
 
621
 
 
622
    def commit(self):
 
623
        self.debug('Saving thumbnail cache file to disk for: %s', self._filename)
 
624
        self._db.commit()
 
625
        self.log("Saved thumbnail cache file: %s" % self._filehash)
 
626
 
 
627
 
 
628
class PipelineCpuAdapter(Loggable):
 
629
    """
 
630
    This pipeline manager will modulate the rate of the provided pipeline.
 
631
    It is the responsibility of the caller to set the sync of the sink to True,
 
632
    disable QOS and provide a pipeline with a rate of 1.0.
 
633
    Doing otherwise would be cheating. Cheating is bad.
 
634
    """
 
635
    def __init__(self, pipeline):
 
636
        Loggable.__init__(self)
 
637
        self.pipeline = pipeline
 
638
        self.bus = self.pipeline.get_bus()
 
639
 
 
640
        self.cpu_usage_tracker = CPUUsageTracker()
 
641
        self.rate = 1.0
 
642
        self.done = False
 
643
        self.ready = False
 
644
        self.lastPos = 0
 
645
        self._bus_cb_id = None
 
646
 
 
647
    def start(self):
 
648
        GLib.timeout_add(200, self._modulateRate)
 
649
        self._bus_cb_id = self.bus.connect("message", self._messageCb)
 
650
        self.done = False
 
651
 
 
652
    def stop(self):
 
653
        if self._bus_cb_id is not None:
 
654
            self.bus.disconnect(self._bus_cb_id)
 
655
            self._bus_cb_id = None
 
656
        self.pipeline = None
 
657
        self.done = True
 
658
 
 
659
    def _modulateRate(self):
 
660
        """
 
661
        Adapt the rate of audio playback (analysis) depending on CPU usage.
 
662
        """
 
663
        if self.done:
 
664
            return False
 
665
 
 
666
        usage_percent = self.cpu_usage_tracker.usage()
 
667
        self.cpu_usage_tracker.reset()
 
668
        if usage_percent >= WAVEFORMS_CPU_USAGE:
 
669
            if self.rate < 0.1:
 
670
                if not self.ready:
 
671
                    self.ready = True
 
672
                    self.pipeline.set_state(Gst.State.READY)
 
673
                    res, self.lastPos = self.pipeline.query_position(Gst.Format.TIME)
 
674
                return True
 
675
 
 
676
            if self.rate > 0.0:
 
677
                self.rate *= 0.9
 
678
                self.log('Pipeline rate slowed down (-10%%) to %.3f' % self.rate)
 
679
        else:
 
680
            self.rate *= 1.1
 
681
            self.log('Pipeline rate sped up (+10%%) to %.3f' % self.rate)
 
682
 
 
683
        if not self.ready:
 
684
            res, position = self.pipeline.query_position(Gst.Format.TIME)
 
685
        else:
 
686
            if self.rate > 0.5:  # This to avoid going back and forth from READY to PAUSED
 
687
                self.pipeline.set_state(Gst.State.PAUSED)  # The message handler will unset ready and seek correctly.
 
688
            return True
 
689
 
 
690
        self.pipeline.set_state(Gst.State.PAUSED)
 
691
        self.pipeline.seek(self.rate,
 
692
                           Gst.Format.TIME,
 
693
                           Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
 
694
                           Gst.SeekType.SET,
 
695
                           position,
 
696
                           Gst.SeekType.NONE,
 
697
                           -1)
 
698
        self.pipeline.set_state(Gst.State.PLAYING)
 
699
        self.ready = False
 
700
        # Keep the glib timer running:
 
701
        return True
 
702
 
 
703
    def _messageCb(self, unused_bus, message):
 
704
        if not self.ready:
 
705
            return
 
706
        if message.type == Gst.MessageType.STATE_CHANGED:
 
707
            prev, new, pending = message.parse_state_changed()
 
708
            if message.src == self.pipeline:
 
709
                if prev == Gst.State.READY and new == Gst.State.PAUSED:
 
710
                    self.pipeline.seek(1.0,
 
711
                                       Gst.Format.TIME,
 
712
                                       Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
 
713
                                       Gst.SeekType.SET,
 
714
                                       self.lastPos,
 
715
                                       Gst.SeekType.NONE,
 
716
                                       -1)
 
717
                    self.ready = False
 
718
 
 
719
 
 
720
class AudioPreviewer(Clutter.Actor, PreviewGenerator, Zoomable, Loggable):
 
721
    """
 
722
    Audio previewer based on the results from the "level" gstreamer element.
 
723
    """
 
724
    def __init__(self, bElement, timeline):
 
725
        Clutter.Actor.__init__(self)
 
726
        Zoomable.__init__(self)
 
727
        Loggable.__init__(self)
 
728
        PreviewGenerator.__init__(self, GES.TrackType.AUDIO)
 
729
        self.pipeline = None
 
730
        self.discovered = False
 
731
        self.bElement = bElement
 
732
        self._uri = quote_uri(bElement.props.uri)  # Guard against malformed URIs
 
733
        self.timeline = timeline
 
734
        self.actors = []
 
735
 
 
736
        self.set_content_scaling_filters(Clutter.ScalingFilter.NEAREST, Clutter.ScalingFilter.NEAREST)
 
737
        self.canvas = Clutter.Canvas()
 
738
        self.set_content(self.canvas)
 
739
        self.width = 0
 
740
        self._num_failures = 0
 
741
        self.lastUpdate = datetime.now()
 
742
 
 
743
        self.current_geometry = (-1, -1)
 
744
 
 
745
        self.adapter = None
 
746
        self.surface = None
 
747
        self.timeline.connect("scrolled", self._scrolledCb)
 
748
        self.canvas.connect("draw", self._drawContentCb)
 
749
        self.canvas.invalidate()
 
750
 
 
751
        self._callback_id = 0
 
752
 
 
753
    def startLevelsDiscoveryWhenIdle(self):
 
754
        self.debug('Waiting for UI to become idle for: %s', filename_from_uri(self._uri))
 
755
        GLib.idle_add(self._startLevelsDiscovery, priority=GLib.PRIORITY_LOW)
 
756
 
 
757
    def _startLevelsDiscovery(self):
 
758
        self.log('Preparing waveforms for "%s"' % filename_from_uri(self._uri))
 
759
        filename = hash_file(Gst.uri_get_location(self._uri)) + ".wave"
 
760
        cache_dir = get_dir(os.path.join(xdg_cache_home(), "waves"))
 
761
        filename = cache_dir + "/" + filename
 
762
 
 
763
        if os.path.exists(filename):
 
764
            self.samples = pickle.load(open(filename, "rb"))
 
765
            self._startRendering()
 
766
        else:
 
767
            self.wavefile = filename
 
768
            self._launchPipeline()
 
769
 
 
770
    def _launchPipeline(self):
 
771
        self.debug('Now generating waveforms for: %s', filename_from_uri(self._uri))
 
772
        self.peaks = None
 
773
        self.pipeline = Gst.parse_launch("uridecodebin name=decode uri=" + self._uri + " ! audioconvert ! level name=wavelevel interval=10000000 post-messages=true ! fakesink qos=false name=faked")
 
774
        faked = self.pipeline.get_by_name("faked")
 
775
        faked.props.sync = True
 
776
        self._wavelevel = self.pipeline.get_by_name("wavelevel")
 
777
        decode = self.pipeline.get_by_name("decode")
 
778
        decode.connect("autoplug-select", self._autoplugSelectCb)
 
779
        bus = self.pipeline.get_bus()
 
780
        bus.add_signal_watch()
 
781
 
 
782
        self.nSamples = self.bElement.get_parent().get_asset().get_duration() / 10000000
 
783
        bus.connect("message", self._busMessageCb)
 
784
        self.becomeControlled()
 
785
 
 
786
    def set_size(self, unused_width, unused_height):
 
787
        if self.discovered:
 
788
            self._maybeUpdate()
 
789
 
 
790
    def zoomChanged(self):
 
791
        self._maybeUpdate()
 
792
 
 
793
    def _maybeUpdate(self):
 
794
        if self.discovered:
 
795
            self.log('Checking if the waveform for "%s" needs to be redrawn' % self._uri)
 
796
            if datetime.now() - self.lastUpdate > WAVEFORM_UPDATE_INTERVAL:
 
797
                self.lastUpdate = datetime.now()
 
798
                self._compute_geometry()
 
799
            else:
 
800
                if self._callback_id:
 
801
                    GLib.source_remove(self._callback_id)
 
802
                self._callback_id = GLib.timeout_add(500, self._compute_geometry)
 
803
 
 
804
    def _compute_geometry(self):
 
805
        self.log("Computing the clip's geometry for waveforms")
 
806
        width_px = self.nsToPixel(self.bElement.props.duration)
 
807
        if width_px <= 0:
 
808
            return
 
809
        start = self.timeline.get_scroll_point().x - self.nsToPixel(self.bElement.props.start)
 
810
        start = max(0, start)
 
811
        # Take into account the timeline width, to avoid building
 
812
        # huge clips when the timeline is zoomed in a lot.
 
813
        timeline_width = self.timeline._container.get_allocation().width - CONTROL_WIDTH
 
814
        end = min(width_px,
 
815
                  self.timeline.get_scroll_point().x + timeline_width + MARGIN)
 
816
        self.width = int(end - start)
 
817
        if self.width < 0:  # We've been called at a moment where size was updated but not scroll_point.
 
818
            return
 
819
 
 
820
        # We need to take duration and inpoint into account.
 
821
        asset_duration = self.bElement.get_parent().get_asset().get_duration()
 
822
        if self.bElement.props.duration:
 
823
            nbSamples = self.nbSamples / (float(asset_duration) / float(self.bElement.props.duration))
 
824
        else:
 
825
            nbSamples = self.nbSamples
 
826
        if self.bElement.props.in_point:
 
827
            startOffsetSamples = self.nbSamples / (float(asset_duration) / float(self.bElement.props.in_point))
 
828
        else:
 
829
            startOffsetSamples = 0
 
830
 
 
831
        self.start = int(start / width_px * nbSamples + startOffsetSamples)
 
832
        self.end = int(end / width_px * nbSamples + startOffsetSamples)
 
833
 
 
834
        self.canvas.set_size(self.width, EXPANDED_SIZE)
 
835
        Clutter.Actor.set_size(self, self.width, EXPANDED_SIZE)
 
836
        self.set_position(start, self.props.y)
 
837
        self.canvas.invalidate()
 
838
 
 
839
    def _prepareSamples(self):
 
840
        # Let's go mono.
 
841
        if len(self.peaks) > 1:
 
842
            samples = (numpy.array(self.peaks[0]) + numpy.array(self.peaks[1])) / 2
 
843
        else:
 
844
            samples = numpy.array(self.peaks[0])
 
845
 
 
846
        self.samples = samples.tolist()
 
847
        f = open(self.wavefile, 'w')
 
848
        pickle.dump(self.samples, f)
 
849
 
 
850
    def _startRendering(self):
 
851
        self.nbSamples = len(self.samples)
 
852
        self.discovered = True
 
853
        self.start = 0
 
854
        self.end = self.nbSamples
 
855
        self._compute_geometry()
 
856
        if self.adapter:
 
857
            self.adapter.stop()
 
858
 
 
859
    def _busMessageCb(self, bus, message):
 
860
        if message.src == self._wavelevel:
 
861
            s = message.get_structure()
 
862
            p = None
 
863
            if s:
 
864
                p = s.get_value("rms")
 
865
 
 
866
            if p:
 
867
                st = s.get_value("stream-time")
 
868
 
 
869
                if self.peaks is None:
 
870
                    self.peaks = []
 
871
                    for channel in p:
 
872
                        self.peaks.append([0] * self.nSamples)
 
873
 
 
874
                pos = int(st / 10000000)
 
875
                if pos >= len(self.peaks[0]):
 
876
                    return
 
877
 
 
878
                for i, val in enumerate(p):
 
879
                    if val < 0:
 
880
                        val = 10 ** (val / 20) * 100
 
881
                        self.peaks[i][pos] = val
 
882
                    else:
 
883
                        self.peaks[i][pos] = self.peaks[i][pos - 1]
 
884
            return
 
885
 
 
886
        if message.type == Gst.MessageType.EOS:
 
887
            self._prepareSamples()
 
888
            self._startRendering()
 
889
            self.stopGeneration()
 
890
 
 
891
        elif message.type == Gst.MessageType.ERROR:
 
892
            if self.adapter:
 
893
                self.adapter.stop()
 
894
                self.adapter = None
 
895
            # Something went wrong TODO : recover
 
896
            self.stopGeneration()
 
897
            self._num_failures += 1
 
898
            if self._num_failures < 2:
 
899
                self.warning("Issue during waveforms generation: %s"
 
900
                             " for the %ith time, trying again with no rate "
 
901
                             " modulation", message.parse_error(),
 
902
                             self._num_failures)
 
903
                bus.disconnect_by_func(self._busMessageCb)
 
904
                self._launchPipeline()
 
905
                self.becomeControlled()
 
906
            else:
 
907
                self.error("Issue during waveforms generation: %s"
 
908
                           "Abandonning", message.parse_error())
 
909
 
 
910
        elif message.type == Gst.MessageType.STATE_CHANGED:
 
911
            prev, new, pending = message.parse_state_changed()
 
912
            if message.src == self.pipeline:
 
913
                if prev == Gst.State.READY and new == Gst.State.PAUSED:
 
914
                    self.pipeline.seek(1.0,
 
915
                                       Gst.Format.TIME,
 
916
                                       Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
 
917
                                       Gst.SeekType.SET,
 
918
                                       0,
 
919
                                       Gst.SeekType.NONE,
 
920
                                       -1)
 
921
 
 
922
                # In case we failed previously, we won't modulate next time
 
923
                elif not self.adapter and prev == Gst.State.PAUSED and \
 
924
                        new == Gst.State.PLAYING and self._num_failures == 0:
 
925
                    self.adapter = PipelineCpuAdapter(self.pipeline)
 
926
                    self.adapter.start()
 
927
 
 
928
    def _autoplugSelectCb(self, unused_decode, unused_pad, unused_caps, factory):
 
929
        # Don't plug video decoders / parsers.
 
930
        if "Video" in factory.get_klass():
 
931
            return True
 
932
        return False
 
933
 
 
934
    def _drawContentCb(self, unused_canvas, context, unused_surf_w, unused_surf_h):
 
935
        context.set_operator(cairo.OPERATOR_CLEAR)
 
936
        context.paint()
 
937
        if not self.discovered:
 
938
            return
 
939
 
 
940
        if self.surface:
 
941
            self.surface.finish()
 
942
 
 
943
        self.surface = renderer.fill_surface(self.samples[self.start:self.end], int(self.width), int(EXPANDED_SIZE))
 
944
 
 
945
        context.set_operator(cairo.OPERATOR_OVER)
 
946
        context.set_source_surface(self.surface, 0, 0)
 
947
        context.paint()
 
948
 
 
949
    def _scrolledCb(self, unused):
 
950
        self._maybeUpdate()
 
951
 
 
952
    def startGeneration(self):
 
953
        self.pipeline.set_state(Gst.State.PLAYING)
 
954
        if self.adapter is not None:
 
955
            self.adapter.start()
 
956
 
 
957
    def stopGeneration(self):
 
958
        if self.adapter is not None:
 
959
            self.adapter.stop()
 
960
            self.adapter = None
 
961
 
 
962
        if self.pipeline:
 
963
            self.pipeline.set_state(Gst.State.NULL)
 
964
            self.pipeline.get_state(Gst.CLOCK_TIME_NONE)
 
965
 
 
966
        PreviewGenerator.emit(self, "done")
 
967
 
 
968
    def cleanup(self):
 
969
        self.stopGeneration()
 
970
        self.canvas.disconnect_by_func(self._drawContentCb)
 
971
        self.timeline.disconnect_by_func(self._scrolledCb)
 
972
        Zoomable.__del__(self)