1
# PiTiVi , Non-linear video editor
3
# pitivi/timeline/composition.py
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 Composition object
29
from pitivi.settings import ExportSettings
30
from source import TimelineSource
31
from objects import BrotherObjects, MEDIA_TYPE_AUDIO
33
class Layer(BrotherObjects):
35
Base class for composition layers (effects, sources, ...)
39
gobject.GObject.__init__(self)
42
class EffectsLayer(Layer):
44
Layers of the composition that have only one priority
47
def __init__(self, priority):
49
self._priority = priority
53
return len(self._effects)
55
def __getitem__(self, x):
56
return self._effects.__getitem__(x)
58
class SourcesLayer(Layer):
60
Layers of the composition that have minimum and maximum priority
61
Sources are sorted by start time and then by priority
64
def __init__(self, minprio, maxprio):
66
self._minprio = minprio
67
self._maxprio = maxprio
71
return len(self._sources)
73
def __contains__(self, source):
74
return self._sources.__contains__(source)
76
def index(self, source):
77
return self._sources.index(source)
80
class TimelineComposition(TimelineSource):
82
Combines sources and effects
83
_ Sets the priority of the GnlObject(s) contained within
84
_ Effects have always got priorities higher than the sources
85
_ Can contain global effects that have the highest priority
86
_ Those global effect spread the whole duration of the composition
87
_ Simple effects (applies on one source), can overlap each other
88
_ Complex Effect(s) have a lower priority than Simple Effect(s)
89
_ For sanity reasons, Complex Effect(s) can't overlap each other
90
_ Transitions have the lowest effect priority
91
_ Source(s) contained in it follow each other if possible
92
_ Source can overlap each other
93
_ Knows the "visibility" of the sources contained within
95
_ Provides a "condensed list" of the objects contained within
96
_ Allows to quickly show a top-level view of the composition
98
* Sandwich view example (top: high priority):
99
[ Global Simple Effect(s) (RGB, YUV, Speed,...) ]
100
[ Simple Effect(s), can be several layers ]
101
[ Complex Effect(s), non-overlapping ]
102
[ Transition(s), non-overlapping ]
103
[ Layers of sources ]
106
_ Global Simple Effect(s) (Optionnal)
113
_ 'condensed-list-changed' : condensed list
114
_ 'global-effect-added' : a global-effect was added to the composition
115
_ 'global-effect-removed' : a global-effect was removed from the composition
116
_ 'simple-effect-added' : a simple-effect was added to the composition
117
_ 'simple-effect-removed' : a simple-effect was removed from the composition
118
_ 'complex-effect-added' : a complex-effect was added to the composition
119
_ 'complex-effect-removed' : a complex-effect was removed from the composition
120
_ 'transition-added' : a transition was added to the composition
121
_ 'transition-removed' : a transitions was removed from the composition
122
_ 'source-added' : a TimelineSource was added to the composition
123
_ 'source-removed' : a TimelineSource was removed from the composition
127
'condensed-list-changed' : ( gobject.SIGNAL_RUN_LAST,
129
(gobject.TYPE_PYOBJECT, )),
130
'global-effect-added' : ( gobject.SIGNAL_RUN_LAST,
132
(gobject.TYPE_PYOBJECT, )),
133
'global-effect-removed' : ( gobject.SIGNAL_RUN_LAST,
135
(gobject.TYPE_PYOBJECT, )),
136
'simple-effect-added' : ( gobject.SIGNAL_RUN_LAST,
138
(gobject.TYPE_PYOBJECT, )),
139
'simple-effect-removed' : ( gobject.SIGNAL_RUN_LAST,
141
(gobject.TYPE_PYOBJECT, )),
142
'complex-effect-added' : ( gobject.SIGNAL_RUN_LAST,
144
(gobject.TYPE_PYOBJECT, )),
145
'complex-effect-removed' : ( gobject.SIGNAL_RUN_LAST,
147
(gobject.TYPE_PYOBJECT, )),
148
'transitions-added' : ( gobject.SIGNAL_RUN_LAST,
150
(gobject.TYPE_PYOBJECT, )),
151
'transition-removed' : ( gobject.SIGNAL_RUN_LAST,
153
(gobject.TYPE_PYOBJECT, )),
154
'source-added' : ( gobject.SIGNAL_RUN_LAST,
156
(gobject.TYPE_PYOBJECT, )),
157
'source-removed' : ( gobject.SIGNAL_RUN_LAST,
159
(gobject.TYPE_PYOBJECT, )),
162
# mid-level representation/storage of sources/effecst lists
165
# Apply on the whole duration of the composition.
166
# Sorted by priority (first: most important)
170
# Priority, then time
174
# Simple list sorted by time
179
# Handles priority attribution to contained sources
183
# _ list of sources sorted by time
185
def __init__(self, **kw):
186
self.global_effects = [] # list of effects starting from highest priority
187
self.simple_effects = [[]] # list of layers of simple effects (order: priority, then time)
188
self.complex_effects = [] # complex effect sorted by time
189
self.transitions = [] # transitions sorted by time
190
# list of layers of simple effects (order: priority, then time)
191
self.condensed = [] # list of sources/transitions seen from a top-level view
192
# each layer contains (min priority, max priority, list objects)
193
#sources = [(2048, 2060, [])]
194
self.sources = [(2048, 2060, [])]
195
self.defaultSource = None
196
TimelineSource.__init__(self, **kw)
199
""" return the number of sources in this composition """
201
for min, max, sources in self.sources:
205
def __nonzero__(self):
206
""" Always returns True, else bool(object) will return False if len(object) == 0 """
209
def _makeGnlObject(self):
210
return gst.element_factory_make("gnlcomposition", "composition-" + self.name)
214
def addGlobalEffect(self, global_effect, order, auto_linked=True):
218
n : put at the given position (0: first)
219
-1 : put at the end (lowest priority)
220
auto_linked : if True will add the brother (if any) of the given effect
221
to the linked composition with the same order
223
raise NotImplementedError
225
def removeGlobalEffect(self, global_effect, remove_linked=True):
227
remove a global effect
228
If remove_linked is True and the effect has a linked effect, will remove
229
it from the linked composition
231
raise NotImplementedError
235
def addSimpleEffect(self, simple_effect, order, auto_linked=True):
239
order works if there's overlapping:
240
n : put at the given position (0: first)
241
-1 : put underneath all other simple effects
242
auto_linked : if True will add the brother (if any) of the given effect
243
to the linked composition with the same order
245
raise NotImplementedError
247
def removeSimpleEffect(self, simple_effect, remove_linked=True):
249
removes a simple effect
250
If remove_linked is True and the effect has a linked effect, will remove
251
it from the linked composition
253
raise NotImplementedError
257
def addComplexEffect(self, complex_effect, auto_linked=True):
259
adds a complex effect
260
auto_linked : if True will add the brother (if any) of the given effect
261
to the linked composition with the same order
263
# if it overlaps with existing complex effect, raise exception
264
raise NotImplementedError
266
def removeComplexEffect(self, complex_effect, remove_linked=True):
268
removes a complex effect
269
If remove_linked is True and the effect has a linked effect, will remove
270
it from the linked composition
272
raise NotImplementedError
274
def _makeCondensedList(self):
275
""" makes a condensed list """
276
def condensed_sum(list1, list2):
277
""" returns a condensed list of the two given lists """
278
self.gnlobject.info( "condensed_sum")
279
self.gnlobject.info( "comparing %s with %s" % (list1, list2))
287
# find the objects in list2 that go under list1 and insert them at
288
# the good position in res
290
# go through res to see if it can go somewhere
291
for pos in range(len(res)):
292
if obj.start <= res[pos].start:
295
if pos == len(res) and obj.start > res[-1].start:
297
self.gnlobject.info("returning %s" % res)
301
lists = [x[2] for x in self.sources]
302
lists.insert(0, self.transitions)
303
return reduce(condensed_sum, lists)
305
def _updateCondensedList(self):
306
""" updates the condensed list """
307
self.gnlobject.info("_update_condensed_list")
308
# build a condensed list
309
clist = self._makeCondensedList()
310
self.gnlobject.info("clist:%r" % clist)
312
# compare it to the self.condensed
314
## print "comparing:"
315
## for i in self.condensed:
316
## print i.gnlobject, i.start, i.duration
319
## print i.gnlobject, i.start, i.duration
320
if not len(clist) == len(self.condensed):
323
for a, b in zip(clist, self.condensed):
329
self.gnlobject.log("list_change : %s" % list_changed)
330
# if it's different or new, set it to self.condensed and emit the signal
332
self.condensed = clist
333
self.emit("condensed-list-changed", self.condensed)
337
def addTransition(self, transition, source1, source2, auto_linked=True):
339
adds a transition between source1 and source2
340
auto_linked : if True will add the brother (if any) of the given transition
341
to the linked composition with the same parameters
343
# if it overlaps with existing transition, raise exception
344
raise NotImplementedError
346
def moveTransition(self, transition, source1, source2):
347
""" move a transition between source1 and source2 """
348
# if it overlays with existing transition, raise exception
349
raise NotImplementedError
351
def removeTransition(self, transition, reorder_sources=True, remove_linked=True):
353
removes a transition,
354
If reorder sources is True it puts the sources
355
between which the transition was back one after the other
356
If remove_linked is True and the transition has a linked effect, will remove
357
it from the linked composition
359
raise NotImplementedError
363
def _getSourcePosition(self, source):
366
for slist in self.sources:
367
if source in slist[2]:
370
position = position + 1
375
def _haveGotThisSource(self, source):
376
for slist in self.sources:
377
if source in slist[2]:
382
def _addSource(self, source, position):
383
""" private version of addSource """
384
def my_add_sorted(sources, object):
388
if item.start > object.start:
391
object.gnlobject.set_property("priority", sources[0])
392
slist.insert(i, object)
394
# TODO : add functionnality to add above/under
395
# For the time being it's hardcoded to a single layer
398
# add it to the correct self.sources[position]
399
my_add_sorted(self.sources[position-1], source)
401
# add it to self.gnlobject
402
self.gnlobject.info("adding %s to our composition" % source.gnlobject)
403
self.gnlobject.add(source.gnlobject)
405
self.gnlobject.info("added source %s" % source.gnlobject)
406
gst.info("%s" % str(self.sources))
407
self.emit('source-added', source)
409
# update the condensed list
410
self._updateCondensedList()
412
def addSource(self, source, position, auto_linked=True):
414
add a source (with correct start/duration time already set)
415
position : the vertical position
416
_ 0 : insert above all other layers
417
_ n : insert at the given position (1: top row)
418
_ -1 : insert at the bottom, under all sources
419
auto_linked : if True will add the brother (if any) of the given source
420
to the linked composition with the same parameters
422
self.gnlobject.info("source %s , position:%d, self.sources:%s" %(source, position, self.sources))
424
self._addSource(source, position)
426
# if auto_linked and self.linked, add brother to self.linked with same parameters
427
if auto_linked and self.linked:
428
if source.getBrother():
429
self.linked._addSource(source.brother, position)
431
def insertSourceAfter(self, source, existingsource, push_following=True, auto_linked=True):
433
inserts a source after the existingsource, pushing the following ones
434
if existingsource is None, it puts the source at the beginning
437
self.gnlobject.info("insert_source after %s" % existingsource.gnlobject)
439
self.gnlobject.info("insert_source at the beginning")
441
# find the time where it's going to be added
442
if not existingsource or not self._haveGotThisSource(existingsource):
447
start = existingsource.start + existingsource.duration
448
position = self._getSourcePosition(existingsource)
449
existorder = self.sources[position - 1][2].index(existingsource) + 1
451
gst.info("start=%s, position=%d, existorder=%d, sourcelength=%s" % (gst.TIME_ARGS(start),
454
gst.TIME_ARGS(source.factory.length)))
455
## for i in self.sources[position -1][2]:
456
## print i.gnlobject, i.start, i.duration
457
# set the correct start/duration time
458
duration = source.factory.length
459
source.setStartDurationTime(start, duration)
462
if push_following and not position in [-1, 0]:
463
#print self.gnlobject, "pushing following", existorder, len(self.sources[position - 1][2])
464
for i in range(existorder, len(self.sources[position - 1][2])):
465
mvsrc = self.sources[position - 1][2][i]
466
self.gnlobject.info("pushing following")
467
#print "run", i, "start", mvsrc.start, "duration", mvsrc.duration
468
# increment self.sources[position - 1][i] by source.factory.length
469
mvsrc.setStartDurationTime(mvsrc.start + source.factory.length)
471
self.addSource(source, position, auto_linked=auto_linked)
473
def appendSource(self, source, position=1, auto_linked=True):
475
puts a source after all the others
477
self.gnlobject.info("source:%s" % source.gnlobject)
478
# find the source with the highest duration time on the first layer
479
if self.sources[position - 1]:
480
existingsource = self.sources[position - 1][2][-1]
482
existingsource = None
484
self.insertSourceAfter(source, existingsource, push_following=False,
485
auto_linked=auto_linked)
487
def prependSource(self, source, push_following=True, auto_linked=True):
489
adds a source to the beginning of the sources
491
self.gnlobject.info("source:%s" % source.gnlobject)
492
self.insertSourceAfter(source, None, push_following, auto_linked)
494
def moveSource(self, source, newpos, move_linked=True, push_neighbours=True, collapse_neighbours=True):
496
Moves the source to the new position. The position is the existing source before which to move
499
If move_linked is True and the source has a linked source, the linked source will
500
be moved to the same position.
501
If collapse_neighbours is True, all sources located AFTER the OLD position of the
502
source will be shifted in the past by the duration of the removed source.
503
If push_neighbours is True, then sources located AFTER the NEW position will be shifted
504
forward in time, in order to have enough free space to insert the source.
506
self.gnlobject.info("source:%s , newpos:%d, move_linked:%s, push_neighbours:%s, collapse_neighbours:%s" % (source,
510
collapse_neighbours))
511
sources = self.sources[0][2]
512
oldpos = sources.index(source)
514
newpos = len(sources)
516
self.gnlobject.info("source was at position %d in his layer" % oldpos)
518
# if we're not moving, return
519
if (oldpos == newpos):
520
self.gnlobject.warning("source is already at the correct position, not moving")
523
# 0. Temporarily remove moving source from composition
524
self.gnlobject.log("Setting source priority at maximum [%d]" % self.sources[0][1])
525
source.gnlobject.set_property("priority", self.sources[0][1])
527
# 1. if collapse_neighbours, shift all downstream sources by duration
528
if collapse_neighbours and oldpos != len(sources) - 1:
529
self.gnlobject.log("collapsing all following neighbours after the old position [%d]" % oldpos)
530
for i in range(oldpos + 1, len(sources)):
532
self.gnlobject.log("moving source %d %s" % (i, obj))
533
obj.setStartDurationTime(start = (obj.start - source.duration))
535
# 2. if push_neighbours, make sure there's enough room at the new position
536
if push_neighbours and newpos != len(sources):
537
pushmin = source.duration
539
pushmin += sources[newpos - 1].start + sources[newpos - 1].duration
540
self.gnlobject.log("We need to make sure sources after newpos are at or after %s" % gst.TIME_ARGS(pushmin))
541
if sources[newpos].start < pushmin:
542
# don't push sources after old position
546
stoppos = len(sources)
547
self.gnlobject.log("pushing neighbours between new position [%d] and stop [%d]" % (newpos, stoppos))
548
for i in range(newpos, stoppos):
550
obj.setStartDurationTime(start = pushmin)
551
pushmin += obj.duration
556
newtimepos += sources[newpos - 1].start + sources[newpos - 1].duration
557
self.gnlobject.log("Setting source start position to %s" % gst.TIME_ARGS(newtimepos))
558
source.setStartDurationTime(start = newtimepos)
560
self.gnlobject.log("Removing source from position [%d] and putting it to position [%d]" % (oldpos, newpos - 1))
562
sources.insert(newpos - 1, source)
563
source.gnlobject.set_property("priority", self.sources[0][0])
565
# 4. same thing for brother
568
# 5. update condensed list
569
self.gnlobject.log("Done moving %s , updating condensed list" % source)
570
self._updateCondensedList()
572
def removeSource(self, source, remove_linked=True, collapse_neighbours=False):
576
If remove_linked is True and the source has a linked source, will remove
577
it from the linked composition.
578
If collapse_neighbours is True, then all object after the removed source
579
will be shifted in the past by the duration of the removed source.
581
self.gnlobject.info("source:%s, remove_linked:%s, collapse_neighbours:%s" % (source, remove_linked, collapse_neighbours))
582
sources = self.sources[0]
584
pos = sources[2].index(source)
585
self.gnlobject.info("source was at position %d in his layer" % pos)
588
self.gnlobject.info("Really removing %s from our composition" % source.gnlobject)
589
self.gnlobject.remove(source.gnlobject)
592
# collapse neighbours
593
if collapse_neighbours:
594
self.gnlobject.info("Collapsing neighbours")
595
for i in range(pos, len(sources[2])):
597
obj.setStartDurationTime(start = (obj.start - source.duration))
599
# if we have a brother
600
if remove_linked and self.linked and self.linked.gnlobject:
601
self.linked.gnlobject.remove(source.linked.gnlobject)
602
self.linked.emit('source-removed', source.linked)
603
self.linked._updateCondensedList()
605
self.emit('source-removed', source)
606
# update the condensed list
607
self._updateCondensedList()
610
def setDefaultSource(self, source):
612
Adds a default source to the composition.
613
Default sources will be used for gaps within the composition.
615
if self.defaultSource:
616
self.gnlobject.remove(self.defaultSource)
617
source.props.priority = 2 ** 32 - 1
618
self.gnlobject.add(source)
619
self.defaultSource = source
621
def getDefaultSource(self):
623
Returns the default source.
625
return self.defaultSource
628
# AutoSettings methods
630
def _autoVideoSettings(self):
631
# return a ExportSettings in which all videos of the composition
632
# will be able to be exported without loss
634
# FIXME : we suppose we only have only source layer !!!
635
# FIXME : we in fact return the first file's settings
636
for source in self.sources[0][2]:
638
biggest = source.getExportSettings()
640
sets = source.getExportSettings()
641
for prop in ['videowidth', 'videoheight',
642
'videopar', 'videorate']:
643
if sets.__getattribute__(prop) != biggest.__getattribute__(prop):
647
def _autoAudioSettings(self):
648
# return an ExportSettings in which all audio source of the composition
649
# will be able to be exported without (too much) loss
651
# FIXME : we suppose we only have only source layer !!!
652
# FIXME : we in fact return the first file's settings
653
for source in self.sources[0][2]:
655
biggest = source.getExportSettings()
657
sets = source.getExportSettings()
658
for prop in ['audiorate', 'audiochannels', 'audiodepth']:
659
if sets.__getattribute__(prop) != biggest.__getattribute__(prop):
664
def _getAutoSettings(self):
665
gst.log("len(self) : %d" % len(self))
669
# return the settings of our only source
670
return self.sources[0][2][0].getExportSettings()
672
if self.media_type == MEDIA_TYPE_AUDIO:
673
return self._autoAudioSettings()
675
return self._autoVideoSettings()