1
# PiTiVi , Non-linear video editor
5
# Copyright (c) 2005, Edward Hervey <bilboed@bilboed.com>
7
# This program is free software; you can redistribute it and/or
8
# modify it under the terms of the GNU Lesser General Public
9
# License as published by the Free Software Foundation; either
10
# version 2.1 of the License, or (at your option) any later version.
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15
# Lesser General Public License for more details.
17
# You should have received a copy of the GNU Lesser General Public
18
# License along with this program; if not, write to the
19
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
20
# Boston, MA 02111-1307, USA.
23
Timeline and timeline objects
43
## | +---- Composition
47
## +---- Simple Effect (1->1)
51
## +---- Complex Effect (N->1)
53
class Timeline(gobject.GObject):
55
Fully fledged timeline
58
# TODO make the compositions more versatile
59
# for the time being we hardcode an audio and a video composition
61
def __init__(self, project):
62
gst.log("new Timeline for project %s" % project)
63
gobject.GObject.__init__(self)
64
self.project = project
66
self.timeline = gst.Bin("timeline-" + project.name)
69
self.project.settings.connect_after("settings-changed", self._settingsChangedCb)
71
def _fillContents(self):
72
# TODO create the initial timeline according to the project settings
73
self.audiocomp = TimelineComposition(media_type = MEDIA_TYPE_AUDIO, name="audiocomp")
74
self.videocomp = TimelineComposition(media_type = MEDIA_TYPE_VIDEO, name="videocomp")
75
self.videocomp.linkObject(self.audiocomp)
77
self.timeline.add(self.audiocomp.gnlobject,
78
self.videocomp.gnlobject)
79
self.audiocomp.gnlobject.connect("pad-added", self._newAudioPadCb)
80
self.videocomp.gnlobject.connect("pad-added", self._newVideoPadCb)
81
self.audiocomp.gnlobject.connect("pad-removed", self._removedAudioPadCb)
82
self.videocomp.gnlobject.connect("pad-removed", self._removedVideoPadCb)
84
def _newAudioPadCb(self, unused_audiocomp, pad):
85
self.timeline.add_pad(gst.GhostPad("asrc", pad))
87
def _newVideoPadCb(self, unused_videocomp, pad):
88
self.timeline.add_pad(gst.GhostPad("vsrc", pad))
90
def _removedAudioPadCb(self, unused_audiocomp, unused_pad):
91
self.timeline.remove_pad(self.timeline.get_pad("asrc"))
93
def _removedVideoPadCb(self, unused_audiocomp, unused_pad):
94
self.timeline.remove_pad(self.timeline.get_pad("vsrc"))
96
def _settingsChangedCb(self, unused_settings):
97
# reset the timeline !
98
result, pstate, pending = self.timeline.get_state(0)
99
self.timeline.set_state(gst.STATE_READY)
100
self.timeline.set_state(pstate)
103
class TimelineObject(gobject.GObject):
105
Base class for all timeline objects
108
_ Start/Duration Time
113
_ Must have same duration
115
_ This is the same object but with the other media_type
118
_ 'start-duration-changed' : start position, duration position
119
_ 'linked-changed' : new linked object
123
"start-duration-changed" : ( gobject.SIGNAL_RUN_LAST,
125
(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, )),
126
"linked-changed" : ( gobject.SIGNAL_RUN_LAST,
128
(gobject.TYPE_PYOBJECT, ))
131
## start = -1 # start time
132
## duration = -1 # duration time
133
## linked = None # linked object
134
## brother = None # brother object, the other-media equivalent of this object
135
## factory = None # the Factory with more details about this object
136
## gnlobject = None # The corresponding GnlObject
137
## media_type = MEDIA_TYPE_NONE # The Media Type of this object
139
def __init__(self, factory=None, start=-1, duration=-1,
140
media_type=MEDIA_TYPE_NONE, name=""):
141
gobject.GObject.__init__(self)
142
gst.log("new TimelineObject :%s" % name)
148
# Set factory and media_type and then create the gnlobject
149
self.factory = factory
150
self.media_type = media_type
151
self.gnlobject = self._makeGnlObject()
152
self.gnlobject.connect("notify::start", self._startDurationChangedCb)
153
self.gnlobject.connect("notify::duration", self._startDurationChangedCb)
154
self._setStartDurationTime(start, duration)
156
def _makeGnlObject(self):
157
""" create and return the gnl_object """
158
raise NotImplementedError
160
def _unlinkObject(self):
161
# really unlink the objects
164
self.emit("linked-changed", None)
166
def _linkObject(self, object):
169
self.emit("linked-changed", self.linked)
171
def linkObject(self, object):
173
link another object to this one.
174
If there already is a linked object ,it will unlink it
176
if self.linked and not self.linked == object:
178
self._linkObject(object)
181
def unlinkObject(self):
183
unlink from the current linked object
185
self.linked._unlinkObject()
188
def relinkBrother(self):
190
links the object back to it's brother
192
# if already linked, unlink from previous
198
self.linkObject(self.brother)
200
def getBrother(self, autolink=True):
202
returns the brother element if it's possible,
203
if autolink, then automatically link it to this element
206
self.brother = self._makeBrother()
209
if autolink and not self.linked == self.brother:
213
def _makeBrother(self):
215
Make the exact same object for the other media_type
216
implemented in subclasses
218
raise NotImplementedError
220
def _setStartDurationTime(self, start=-1, duration=-1):
221
# really modify the start/duration time
222
self.gnlobject.info("start:%s , duration:%s" %( gst.TIME_ARGS(start),
223
gst.TIME_ARGS(duration)))
224
if not duration == -1 and not self.duration == duration:
225
self.duration = duration
226
self.gnlobject.set_property("duration", long(duration))
227
if not start == -1 and not self.start == start:
229
self.gnlobject.set_property("start", long(start))
231
def setStartDurationTime(self, start=-1, duration=-1):
232
""" sets the start and/or duration time """
233
self._setStartDurationTime(start, duration)
235
self.linked._setStartDurationTime(start, duration)
237
def _startDurationChangedCb(self, gnlobject, property):
238
""" start/duration time has changed """
239
self.gnlobject.debug("property:%s" % property.name)
242
if property.name == "start":
243
start = gnlobject.get_property("start")
244
if start == self.start:
247
self.start = long(start)
248
elif property.name == "duration":
249
duration = gnlobject.get_property("duration")
250
if duration == self.duration:
253
self.gnlobject.debug("duration changed:%s" % gst.TIME_ARGS(duration))
254
self.duration = long(duration)
255
#if not start == -1 or not duration == -1:
256
self.emit("start-duration-changed", self.start, self.duration)
260
class TimelineSource(TimelineObject):
262
Base class for all sources (O input)
265
def __init__(self, **kw):
266
TimelineObject.__init__(self, **kw)
269
class TimelineFileSource(TimelineSource):
271
Seekable sources (mostly files)
274
"media-start-duration-changed" : ( gobject.SIGNAL_RUN_LAST,
276
(gobject.TYPE_UINT64, gobject.TYPE_UINT64))
282
def __init__(self, media_start=-1, media_duration=-1, **kw):
283
TimelineSource.__init__(self, **kw)
284
self.gnlobject.connect("notify::media-start", self._mediaStartDurationChangedCb)
285
self.gnlobject.connect("notify::media-duration", self._mediaStartDurationChangedCb)
286
if media_start == -1:
288
if media_duration == -1:
289
media_duration = self.factory.length
290
self.setMediaStartDurationTime(media_start, media_duration)
292
def _makeGnlObject(self):
293
if self.media_type == MEDIA_TYPE_AUDIO:
294
caps = gst.caps_from_string("audio/x-raw-int;audio/x-raw-float")
295
elif self.media_type == MEDIA_TYPE_VIDEO:
296
caps = gst.caps_from_string("video/x-raw-yuv;video/x-raw-rgb")
298
raise NameError, "media type is NONE !"
299
self.factory.lastbinid = self.factory.lastbinid + 1
300
gnlobject = gst.element_factory_make("gnlfilesource", "source-" + self.name + str(self.factory.lastbinid))
301
gnlobject.set_property("location", self.factory.name)
302
gnlobject.set_property("caps", caps)
303
gnlobject.set_property("start", long(0))
304
gnlobject.set_property("duration", long(self.factory.length))
307
def _makeBrother(self):
308
""" make the brother element """
309
self.gnlobject.info("making filesource brother")
310
# find out if the factory provides the other element type
311
if self.media_type == MEDIA_TYPE_NONE:
313
if self.media_type == MEDIA_TYPE_VIDEO:
314
if not self.factory.is_audio:
316
brother = TimelineFileSource(media_start=self.media_start, media_duration=self.media_duration,
317
factory=self.factory, start=self.start, duration=self.duration,
318
media_type=MEDIA_TYPE_AUDIO, name=self.name)
319
elif self.media_type == MEDIA_TYPE_AUDIO:
320
if not self.factory.is_video:
322
brother = TimelineFileSource(media_start=self.media_start, media_duration=self.media_duration,
323
factory=self.factory, start=self.start, duration=self.duration,
324
media_type=MEDIA_TYPE_VIDEO, name=self.name)
329
def _setMediaStartDurationTime(self, start=-1, duration=-1):
330
gst.info("TimelineFileSource start:%d , duration:%d" % (start, duration))
331
if not duration == -1 and not self.media_duration == duration:
332
self.media_duration = duration
333
self.gnlobject.set_property("media-duration", long(duration))
334
if not start == -1 and not self.media_start == start:
335
self.media_start = start
336
self.gnlobject.set_property("media-start", long(start))
338
def setMediaStartDurationTime(self, start=-1, duration=-1):
339
""" sets the media start/duration time """
340
self._setMediaStartDurationTime(start, duration)
341
if self.linked and isinstance(self.linked, TimelineFileSource):
342
self.linked._setMediaStartDurationTime(start, duration)
344
def _mediaStartDurationChangedCb(self, gnlobject, property):
347
if property.name == "media-start":
348
mstart = gnlobject.get_property("media-start")
349
if mstart == self.media_start:
352
self.media_start = mstart
353
elif property.name == "media-duration":
354
mduration = gnlobject.get_property("media-duration")
355
if mduration == self.media_duration:
358
self.media_duration = mduration
359
if mstart or mduration:
360
self.emit("media-start-duration-changed",
361
self.media_start, self.media_duration)
364
class TimelineLiveSource(TimelineSource):
366
Non-seekable sources (like cameras)
369
def __init__(self, **kw):
370
TimelineSource.__init__(self, **kw)
373
class TimelineComposition(TimelineSource):
375
Combines sources and effects
376
_ Sets the priority of the GnlObject(s) contained within
377
_ Effects have always got priorities higher than the sources
378
_ Can contain global effects that have the highest priority
379
_ Those global effect spread the whole duration of the composition
380
_ Simple effects can overlap each other
381
_ Complex Effect(s) have a lower priority than Simple Effect(s)
382
_ For sanity reasons, Complex Effect(s) can't overlap each other
383
_ Transitions have the lowest effect priority
384
_ Source(s) contained in it follow each other if possible
385
_ Source can overlap each other
386
_ Knows the "visibility" of the sources contained within
388
_ Provides a "condensed list" of the objects contained within
389
_ Allows to quickly show a top-level view of the composition
391
* Sandwich view example (top: high priority):
392
[ Global Simple Effect(s) (RGB, YUV, Speed,...) ]
393
[ Simple Effect(s), can be several layers ]
394
[ Complex Effect(s), non-overlapping ]
395
[ Transition(s), non-overlapping ]
396
[ Layers of sources ]
399
_ Global Simple Effect(s) (Optionnal)
406
_ 'condensed-list-changed' : condensed list
407
_ 'global-effect-added' : a global-effect was added to the composition
408
_ 'global-effect-removed' : a global-effect was removed from the composition
409
_ 'simple-effect-added' : a simple-effect was added to the composition
410
_ 'simple-effect-removed' : a simple-effect was removed from the composition
411
_ 'complex-effect-added' : a complex-effect was added to the composition
412
_ 'complex-effect-removed' : a complex-effect was removed from the composition
413
_ 'transition-added' : a transition was added to the composition
414
_ 'transition-removed' : a transitions was removed from the composition
415
_ 'source-added' : a TimelineSource was added to the composition
416
_ 'source-removed' : a TimelineSource was removed from the composition
420
'condensed-list-changed' : ( gobject.SIGNAL_RUN_LAST,
422
(gobject.TYPE_PYOBJECT, )),
423
'global-effect-added' : ( gobject.SIGNAL_RUN_LAST,
425
(gobject.TYPE_PYOBJECT, )),
426
'global-effect-removed' : ( gobject.SIGNAL_RUN_LAST,
428
(gobject.TYPE_PYOBJECT, )),
429
'simple-effect-added' : ( gobject.SIGNAL_RUN_LAST,
431
(gobject.TYPE_PYOBJECT, )),
432
'simple-effect-removed' : ( gobject.SIGNAL_RUN_LAST,
434
(gobject.TYPE_PYOBJECT, )),
435
'complex-effect-added' : ( gobject.SIGNAL_RUN_LAST,
437
(gobject.TYPE_PYOBJECT, )),
438
'complex-effect-removed' : ( gobject.SIGNAL_RUN_LAST,
440
(gobject.TYPE_PYOBJECT, )),
441
'transitions-added' : ( gobject.SIGNAL_RUN_LAST,
443
(gobject.TYPE_PYOBJECT, )),
444
'transition-removed' : ( gobject.SIGNAL_RUN_LAST,
446
(gobject.TYPE_PYOBJECT, )),
447
'source-added' : ( gobject.SIGNAL_RUN_LAST,
449
(gobject.TYPE_PYOBJECT, )),
450
'source-removed' : ( gobject.SIGNAL_RUN_LAST,
452
(gobject.TYPE_PYOBJECT, )),
456
def __init__(self, **kw):
457
self.global_effects = [] # list of effects starting from highest priority
458
self.simple_effects = [[]] # list of layers of simple effects (order: priority, then time)
459
self.complex_effects = [] # complex effect sorted by time
460
self.transitions = [] # transitions sorted by time
461
# list of layers of simple effects (order: priority, then time)
462
# each layer contains (min priority, max priority, list objects)
463
#sources = [(2048, 2060, [])]
464
self.condensed = [] # list of sources/transitions seen from a top-level view
465
self.sources = [(2048, 2060, [])]
466
TimelineSource.__init__(self, **kw)
468
def _makeGnlObject(self):
469
return gst.element_factory_make("gnlcomposition", "composition-" + self.name)
473
def addGlobalEffect(self, global_effect, order, auto_linked=True):
477
n : put at the given position (0: first)
478
-1 : put at the end (lowest priority)
479
auto_linked : if True will add the brother (if any) of the given effect
480
to the linked composition with the same order
482
raise NotImplementedError
484
def removeGlobalEffect(self, global_effect, remove_linked=True):
486
remove a global effect
487
If remove_linked is True and the effect has a linked effect, will remove
488
it from the linked composition
490
raise NotImplementedError
494
def addSimpleEffect(self, simple_effect, order, auto_linked=True):
498
order works if there's overlapping:
499
n : put at the given position (0: first)
500
-1 : put underneath all other simple effects
501
auto_linked : if True will add the brother (if any) of the given effect
502
to the linked composition with the same order
504
raise NotImplementedError
506
def removeSimpleEffect(self, simple_effect, remove_linked=True):
508
removes a simple effect
509
If remove_linked is True and the effect has a linked effect, will remove
510
it from the linked composition
512
raise NotImplementedError
516
def addComplexEffect(self, complex_effect, auto_linked=True):
518
adds a complex effect
519
auto_linked : if True will add the brother (if any) of the given effect
520
to the linked composition with the same order
522
# if it overlaps with existing complex effect, raise exception
523
raise NotImplementedError
525
def removeComplexEffect(self, complex_effect, remove_linked=True):
527
removes a complex effect
528
If remove_linked is True and the effect has a linked effect, will remove
529
it from the linked composition
531
raise NotImplementedError
533
def _makeCondensedList(self):
534
""" makes a condensed list """
535
def condensed_sum(list1, list2):
536
""" returns a condensed list of the two given lists """
537
self.gnlobject.info( "condensed_sum")
538
self.gnlobject.info( "comparing %s with %s" % (list1, list2))
546
# find the objects in list2 that go under list1 and insert them at
547
# the good position in res
549
# go through res to see if it can go somewhere
550
for pos in range(len(res)):
551
if obj.start <= res[pos].start:
554
if pos == len(res) and obj.start > res[-1].start:
556
self.gnlobject.info("returning %s" % res)
560
lists = [x[2] for x in self.sources]
561
lists.insert(0, self.transitions)
562
return reduce(condensed_sum, lists)
564
def _updateCondensedList(self):
565
""" updates the condensed list """
566
self.gnlobject.info("_update_condensed_list")
567
# build a condensed list
568
clist = self._makeCondensedList()
570
# compare it to the self.condensed
572
## print "comparing:"
573
## for i in self.condensed:
574
## print i.gnlobject, i.start, i.duration
577
## print i.gnlobject, i.start, i.duration
578
if not len(clist) == len(self.condensed):
581
for a, b in zip(clist, self.condensed):
587
# if it's different or new, set it to self.condensed and emit the signal
589
self.condensed = clist
590
self.emit("condensed-list-changed", self.condensed)
594
def addTransition(self, transition, source1, source2, auto_linked=True):
596
adds a transition between source1 and source2
597
auto_linked : if True will add the brother (if any) of the given transition
598
to the linked composition with the same parameters
600
# if it overlaps with existing transition, raise exception
601
raise NotImplementedError
603
def moveTransition(self, transition, source1, source2):
604
""" move a transition between source1 and source2 """
605
# if it overlays with existing transition, raise exception
606
raise NotImplementedError
608
def removeTransition(self, transition, reorder_sources=True, remove_linked=True):
610
removes a transition,
611
If reorder sources is True it puts the sources
612
between which the transition was back one after the other
613
If remove_linked is True and the transition has a linked effect, will remove
614
it from the linked composition
616
raise NotImplementedError
620
def _getSourcePosition(self, source):
623
for slist in self.sources:
624
if source in slist[2]:
627
position = position + 1
632
def _haveGotThisSource(self, source):
633
for slist in self.sources:
634
if source in slist[2]:
638
def addSource(self, source, position, auto_linked=True):
640
add a source (with correct start/duration time already set)
641
position : the vertical position
642
_ 0 : insert above all other layers
643
_ n : insert at the given position (1: top row)
644
_ -1 : insert at the bottom, under all sources
645
auto_linked : if True will add the brother (if any) of the given source
646
to the linked composition with the same parameters
648
self.gnlobject.info("source %s , position:%d, self.sources:%s" %(source, position, self.sources))
650
def my_add_sorted(sources, object):
654
if item.start > object.start:
657
object.gnlobject.set_property("priority", sources[0])
658
slist.insert(i, object)
660
# TODO : add functionnality to add above/under
661
# For the time being it's hardcoded to a single layer
664
# add it to the correct self.sources[position]
665
my_add_sorted(self.sources[position-1], source)
667
# add it to self.gnlobject
668
self.gnlobject.info("adding %s to our composition" % source.gnlobject)
669
self.gnlobject.add(source.gnlobject)
671
# update the condensed list
672
self._updateCondensedList()
674
# if auto_linked and self.linked, add brother to self.linked with same parameters
675
if auto_linked and self.linked:
676
if source.getBrother():
677
self.linked.addSource(source.brother, position, auto_linked=False)
678
self.gnlobject.info("added source %s" % source.gnlobject)
679
gst.info("%s" % str(self.sources))
680
self.emit('source-added', source)
682
def insertSourceAfter(self, source, existingsource, push_following=True, auto_linked=True):
684
inserts a source after the existingsource, pushing the following ones
685
if existingsource is None, it puts the source at the beginning
688
self.gnlobject.info("insert_source after %s" % existingsource.gnlobject)
690
self.gnlobject.info("insert_source at the beginning")
692
# find the time where it's going to be added
693
if not existingsource or not self._haveGotThisSource(existingsource):
698
start = existingsource.start + existingsource.duration
699
position = self._getSourcePosition(existingsource)
700
existorder = self.sources[position - 1][2].index(existingsource) + 1
702
gst.info("start=%s, position=%d, existorder=%d, sourcelength=%s" % (gst.TIME_ARGS(start),
705
gst.TIME_ARGS(source.factory.length)))
706
## for i in self.sources[position -1][2]:
707
## print i.gnlobject, i.start, i.duration
708
# set the correct start/duration time
709
duration = source.factory.length
710
source.setStartDurationTime(start, duration)
713
if push_following and not position in [-1, 0]:
714
#print self.gnlobject, "pushing following", existorder, len(self.sources[position - 1][2])
715
for i in range(existorder, len(self.sources[position - 1][2])):
716
mvsrc = self.sources[position - 1][2][i]
717
self.gnlobject.info("pushing following")
718
#print "run", i, "start", mvsrc.start, "duration", mvsrc.duration
719
# increment self.sources[position - 1][i] by source.factory.length
720
mvsrc.setStartDurationTime(mvsrc.start + source.factory.length)
722
self.addSource(source, position, auto_linked=auto_linked)
724
def appendSource(self, source, position=1, auto_linked=True):
726
puts a source after all the others
728
self.gnlobject.info("source:%s" % source.gnlobject)
729
# find the source with the highest duration time on the first layer
730
if self.sources[position - 1]:
731
existingsource = self.sources[position - 1][2][-1]
733
existingsource = None
735
self.insertSourceAfter(source, existingsource, push_following=False,
736
auto_linked=auto_linked)
738
def prependSource(self, source, push_following=True, auto_linked=True):
740
adds a source to the beginning of the sources
742
self.gnlobject.info("source:%s" % source.gnlobject)
743
self.insertSourceAfter(source, None, push_following, auto_linked)
745
def moveSource(self, source, newpos):
747
moves the source to the new position
749
raise NotImplementedError
751
def removeSource(self, source, remove_linked=True, collapse_neighbours=False):
754
If remove_linked is True and the source has a linked source, will remove
755
it from the linked composition
757
raise NotImplementedError
760
class TimelineEffect(TimelineObject):
762
Base class for effects (1->n input(s))
765
def __init__(self, nbinputs=1, **kw):
766
self.nbinputs = nbinputs
767
TimelineObject.__init__(self, **kw)
769
def _makeGnlObject(self):
770
gnlobject = gst.element_factory_make("gnloperation", "operation-" + self.name)
771
self._setUpGnlOperation(gnlobject)
774
def _setUpGnlOperation(self, gnlobject):
775
""" fill up the gnloperation for the first go """
776
raise NotImplementedError
778
class TimelineSimpleEffect(TimelineEffect):
780
Simple effects (1 input)
783
def __init__(self, factory, **kw):
784
self.factory = factory
785
TimelineEffect.__init__(self, **kw)
788
class TimelineTransition(TimelineEffect):
795
def __init__(self, factory, source1=None, source2=None, **kw):
796
self.factory = factory
797
TimelineEffect.__init__(self, nbinputs=2, **kw)
798
self.setSources(source1, source2)
800
def setSources(self, source1, source2):
801
""" changes the sources in between which the transition lies """
802
self.source1 = source1
803
self.source2 = source2
806
class TimelineComplexEffect(TimelineEffect):
811
def __init__(self, factory, **kw):
812
self.factory = factory
813
# Find out the number of inputs
815
TimelineEffect.__init__(self, nbinputs=nbinputs, **kw)