1
# -*- coding: utf-8 -*-
4
# pitivi/timeline/previewers.py
6
# Copyright (c) 2013, Daniel Thul <daniel.thul@gmail.com>
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.
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.
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.
23
from datetime import datetime, timedelta
24
from gi.repository import Clutter, Gst, GLib, GdkPixbuf, Cogl, GES
25
from random import randrange
32
# Our C module optimizing waveforms rendering
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
45
WAVEFORMS_CPU_USAGE = 30
47
# A little lower as it's more fluctuating
48
THUMBNAILS_CPU_USAGE = 20
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.
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
61
class PreviewGeneratorManager():
63
Manage the execution of PreviewGenerators
67
GES.TrackType.AUDIO: None,
68
GES.TrackType.VIDEO: None
71
GES.TrackType.AUDIO: [],
72
GES.TrackType.VIDEO: []
75
def addPipeline(self, pipeline):
76
track_type = pipeline.track_type
78
if pipeline in self._pipelines[track_type] or \
79
pipeline is self._cpipeline[track_type]:
82
if not self._pipelines[track_type] and self._cpipeline[track_type] is None:
83
self._setPipeline(pipeline)
85
self._pipelines[track_type].insert(0, pipeline)
87
def _setPipeline(self, pipeline):
88
self._cpipeline[pipeline.track_type] = pipeline
89
PreviewGenerator.connect(pipeline, "done", self._nextPipeline)
90
pipeline.startGeneration()
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],
97
self._cpipeline[track_type] = None
99
if self._pipelines[track_type]:
100
self._setPipeline(self._pipelines[track_type].pop())
103
class PreviewGenerator(Signallable):
105
Interface to be implemented by classes that generate previews
106
It is need to implement it so PreviewGeneratorManager can manage
110
# We only want one instance of PreviewGeneratorManager to be used for
111
# all the generators.
112
__manager = PreviewGeneratorManager()
119
def __init__(self, track_type):
121
@param track_type : GES.TrackType.*
123
Signallable.__init__(self)
124
self.track_type = track_type
126
def startGeneration(self):
129
def stopGeneration(self):
132
def becomeControlled(self):
134
Let the PreviewGeneratorManager control our execution
136
PreviewGenerator.__manager.addPipeline(self)
139
class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
140
def __init__(self, bElement, timeline):
142
@param bElement : the backend GES.TrackElement
143
@param track : the track to which the bElement belongs
144
@param timeline : the containing graphic timeline.
146
Zoomable.__init__(self)
147
Clutter.ScrollActor.__init__(self)
148
Loggable.__init__(self)
149
PreviewGenerator.__init__(self, GES.TrackType.VIDEO)
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
157
# Variables related to thumbnailing
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()
169
# Maps (quantized) times to Thumbnail objects
171
self.thumb_cache = get_cache_for_uri(self.uri)
173
self.cpu_usage_tracker = CPUUsageTracker()
174
self.interval = 500 # Every 0.5 second, reevaluate the situation
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)
183
self.becomeControlled()
187
def _update(self, unused_msg_source=None):
188
if self._callback_id:
189
GLib.source_remove(self._callback_id)
192
self._addVisibleThumbnails()
194
self.becomeControlled()
196
def _setupPipeline(self):
200
It has the form "playbin ! thumbnailsink" where thumbnailsink
201
is a Bin made out of "videorate ! capsfilter ! gdkpixbufsink"
203
# TODO: don't hardcode framerate
204
self.pipeline = Gst.parse_launch(
205
"uridecodebin uri={uri} name=decode ! "
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))
213
# get the gdkpixbufsink and the sinkpad
214
self.gdkpixbufsink = self.pipeline.get_by_name("gdkpixbufsink")
215
sinkpad = self.gdkpixbufsink.get_static_pad("sink")
217
self.pipeline.set_state(Gst.State.PAUSED)
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"]
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
233
decode = self.pipeline.get_by_name("decode")
234
decode.connect("autoplug-select", self._autoplugSelectCb)
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():
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)
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.
250
usage_percent = self.cpu_usage_tracker.usage()
251
if usage_percent < THUMBNAILS_CPU_USAGE:
253
self.log('Thumbnailing sped up (+10%%) to a %.1f ms interval for "%s"' % (self.interval, filename_from_uri(self.uri)))
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)
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)
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
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
276
self.duration = duration
278
self.queue = range(0, duration, self.thumb_period)
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)
292
def _create_next_thumb(self):
293
if not self.wishlist or not self.queue:
295
self.debug("Thumbnails generation complete")
296
self.stopGeneration()
297
self.thumb_cache.commit()
300
self.debug("Missing %d thumbs", len(self.wishlist))
302
wish = self._get_wish()
305
self.queue.remove(wish)
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)
322
self.log("Periodic thumbnail autosave")
323
self.thumb_cache.commit()
326
return False # Stop the timer
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)
338
def _addVisibleThumbnails(self):
340
Get the thumbnails to be displayed in the currently visible clip portion
342
self.remove_all_children()
343
old_thumbs = self.thumbs
347
thumb_duration = self._get_thumb_duration()
348
element_left, element_right = self._get_visible_range()
349
element_left = quantize(element_left, thumb_duration)
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)
361
self.thumbs[current_time].set_from_gdkpixbuf(gdkpixbuf)
363
self.wishlist.append(current_time)
364
self._allAnimated = False
368
Returns a wish that is also in the queue, or None if no such wish exists
371
if not self.wishlist:
373
wish = self.wishlist.pop(0)
374
if wish in self.queue:
377
def _setThumbnail(self, time, pixbuf):
378
# Q: Is "time" guaranteed to be nanosecond precise?
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
386
if time in self.thumbs:
387
thumb = self.thumbs[time]
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
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))
403
thumb.set_from_gdkpixbuf_animated(pixbuf)
404
if time in self.queue:
405
self.queue.remove(time)
406
self.thumb_cache[time] = pixbuf
408
# Interface (Zoomable)
410
def zoomChanged(self):
411
self.remove_all_children()
412
self._allAnimated = True
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()
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)
427
return (element_left, element_right)
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
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
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)
447
stage_rect = Clutter.Rect()
448
stage_rect.init(stage_allocation.x1,
450
stage_allocation.x2 - stage_allocation.x1,
451
stage_allocation.y2 - stage_allocation.y1)
453
has_intersection, intersection = timeline_rect.intersection(stage_rect)
455
if not has_intersection:
458
timeline_width = intersection.size.width
460
# determine the visible right edge of the timeline
461
timeline_right = timeline_left + timeline_width
463
# convert to nanoseconds
464
time_left = Zoomable.pixelToNs(timeline_left)
465
time_right = Zoomable.pixelToNs(timeline_right)
467
return (time_left, time_right)
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:
483
return Gst.BusSyncReply.PASS
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():
491
def _scrollCb(self, unused):
494
def _startChangedCb(self, unused_bElement, unused_value):
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)
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
509
def startGeneration(self):
510
self._setupPipeline()
511
self._startThumbnailingWhenIdle()
513
def stopGeneration(self):
514
if self._thumb_cb_id:
515
GLib.source_remove(self._thumb_cb_id)
516
self._thumb_cb_id = None
519
self.pipeline.set_state(Gst.State.NULL)
520
self.pipeline.get_state(Gst.CLOCK_TIME_NONE)
522
PreviewGenerator.emit(self, "done")
525
self.stopGeneration()
526
Zoomable.__del__(self)
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
536
#self.set_background_color(Clutter.Color.new(0, 100, 150, 100))
538
self.set_size(self.width, self.height)
539
self.has_pixel_data = False
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
547
self.props.content.set_data(pixel_data, Cogl.PixelFormat.RGBA_8888,
548
self.width, self.height, row_stride)
550
self.props.content.set_data(pixel_data, Cogl.PixelFormat.RGB_888,
551
self.width, self.height, row_stride)
552
self.set_opacity(255)
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()
564
def get_cache_for_uri(uri):
568
cache = ThumbnailCache(uri)
573
class ThumbnailCache(Loggable):
575
"""Caches thumbnails by key using LRU policy, implemented with heapq.
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."""
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)")
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():
599
def __getitem__(self, key):
600
self._cur.execute("SELECT * FROM Thumbs WHERE Time = ?", (key,))
601
row = self._cur.fetchone()
605
loader = GdkPixbuf.PixbufLoader.new()
606
# TODO: what do to if any of the following calls fails?
609
pixbuf = loader.get_pixbuf()
612
def __setitem__(self, key, value):
613
success, jpeg = value.save_to_bufferv("jpeg", ["quality", None], ["90"])
615
self.warning("JPEG compression failed")
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,))
623
self.debug('Saving thumbnail cache file to disk for: %s', self._filename)
625
self.log("Saved thumbnail cache file: %s" % self._filehash)
628
class PipelineCpuAdapter(Loggable):
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.
635
def __init__(self, pipeline):
636
Loggable.__init__(self)
637
self.pipeline = pipeline
638
self.bus = self.pipeline.get_bus()
640
self.cpu_usage_tracker = CPUUsageTracker()
645
self._bus_cb_id = None
648
GLib.timeout_add(200, self._modulateRate)
649
self._bus_cb_id = self.bus.connect("message", self._messageCb)
653
if self._bus_cb_id is not None:
654
self.bus.disconnect(self._bus_cb_id)
655
self._bus_cb_id = None
659
def _modulateRate(self):
661
Adapt the rate of audio playback (analysis) depending on CPU usage.
666
usage_percent = self.cpu_usage_tracker.usage()
667
self.cpu_usage_tracker.reset()
668
if usage_percent >= WAVEFORMS_CPU_USAGE:
672
self.pipeline.set_state(Gst.State.READY)
673
res, self.lastPos = self.pipeline.query_position(Gst.Format.TIME)
678
self.log('Pipeline rate slowed down (-10%%) to %.3f' % self.rate)
681
self.log('Pipeline rate sped up (+10%%) to %.3f' % self.rate)
684
res, position = self.pipeline.query_position(Gst.Format.TIME)
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.
690
self.pipeline.set_state(Gst.State.PAUSED)
691
self.pipeline.seek(self.rate,
693
Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
698
self.pipeline.set_state(Gst.State.PLAYING)
700
# Keep the glib timer running:
703
def _messageCb(self, unused_bus, message):
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,
712
Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
720
class AudioPreviewer(Clutter.Actor, PreviewGenerator, Zoomable, Loggable):
722
Audio previewer based on the results from the "level" gstreamer element.
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)
730
self.discovered = False
731
self.bElement = bElement
732
self._uri = quote_uri(bElement.props.uri) # Guard against malformed URIs
733
self.timeline = timeline
736
self.set_content_scaling_filters(Clutter.ScalingFilter.NEAREST, Clutter.ScalingFilter.NEAREST)
737
self.canvas = Clutter.Canvas()
738
self.set_content(self.canvas)
740
self._num_failures = 0
741
self.lastUpdate = datetime.now()
743
self.current_geometry = (-1, -1)
747
self.timeline.connect("scrolled", self._scrolledCb)
748
self.canvas.connect("draw", self._drawContentCb)
749
self.canvas.invalidate()
751
self._callback_id = 0
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)
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
763
if os.path.exists(filename):
764
self.samples = pickle.load(open(filename, "rb"))
765
self._startRendering()
767
self.wavefile = filename
768
self._launchPipeline()
770
def _launchPipeline(self):
771
self.debug('Now generating waveforms for: %s', filename_from_uri(self._uri))
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()
782
self.nSamples = self.bElement.get_parent().get_asset().get_duration() / 10000000
783
bus.connect("message", self._busMessageCb)
784
self.becomeControlled()
786
def set_size(self, unused_width, unused_height):
790
def zoomChanged(self):
793
def _maybeUpdate(self):
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()
800
if self._callback_id:
801
GLib.source_remove(self._callback_id)
802
self._callback_id = GLib.timeout_add(500, self._compute_geometry)
804
def _compute_geometry(self):
805
self.log("Computing the clip's geometry for waveforms")
806
width_px = self.nsToPixel(self.bElement.props.duration)
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
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.
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))
825
nbSamples = self.nbSamples
826
if self.bElement.props.in_point:
827
startOffsetSamples = self.nbSamples / (float(asset_duration) / float(self.bElement.props.in_point))
829
startOffsetSamples = 0
831
self.start = int(start / width_px * nbSamples + startOffsetSamples)
832
self.end = int(end / width_px * nbSamples + startOffsetSamples)
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()
839
def _prepareSamples(self):
841
if len(self.peaks) > 1:
842
samples = (numpy.array(self.peaks[0]) + numpy.array(self.peaks[1])) / 2
844
samples = numpy.array(self.peaks[0])
846
self.samples = samples.tolist()
847
f = open(self.wavefile, 'w')
848
pickle.dump(self.samples, f)
850
def _startRendering(self):
851
self.nbSamples = len(self.samples)
852
self.discovered = True
854
self.end = self.nbSamples
855
self._compute_geometry()
859
def _busMessageCb(self, bus, message):
860
if message.src == self._wavelevel:
861
s = message.get_structure()
864
p = s.get_value("rms")
867
st = s.get_value("stream-time")
869
if self.peaks is None:
872
self.peaks.append([0] * self.nSamples)
874
pos = int(st / 10000000)
875
if pos >= len(self.peaks[0]):
878
for i, val in enumerate(p):
880
val = 10 ** (val / 20) * 100
881
self.peaks[i][pos] = val
883
self.peaks[i][pos] = self.peaks[i][pos - 1]
886
if message.type == Gst.MessageType.EOS:
887
self._prepareSamples()
888
self._startRendering()
889
self.stopGeneration()
891
elif message.type == Gst.MessageType.ERROR:
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(),
903
bus.disconnect_by_func(self._busMessageCb)
904
self._launchPipeline()
905
self.becomeControlled()
907
self.error("Issue during waveforms generation: %s"
908
"Abandonning", message.parse_error())
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,
916
Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
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)
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():
934
def _drawContentCb(self, unused_canvas, context, unused_surf_w, unused_surf_h):
935
context.set_operator(cairo.OPERATOR_CLEAR)
937
if not self.discovered:
941
self.surface.finish()
943
self.surface = renderer.fill_surface(self.samples[self.start:self.end], int(self.width), int(EXPANDED_SIZE))
945
context.set_operator(cairo.OPERATOR_OVER)
946
context.set_source_surface(self.surface, 0, 0)
949
def _scrolledCb(self, unused):
952
def startGeneration(self):
953
self.pipeline.set_state(Gst.State.PLAYING)
954
if self.adapter is not None:
957
def stopGeneration(self):
958
if self.adapter is not None:
963
self.pipeline.set_state(Gst.State.NULL)
964
self.pipeline.get_state(Gst.CLOCK_TIME_NONE)
966
PreviewGenerator.emit(self, "done")
969
self.stopGeneration()
970
self.canvas.disconnect_by_func(self._drawContentCb)
971
self.timeline.disconnect_by_func(self._scrolledCb)
972
Zoomable.__del__(self)