25
25
from pitivi.signalinterface import Signallable
26
from pitivi.utils import UNKNOWN_DURATION
26
from pitivi.utils import get_controllable_properties, getPreviousObject, \
27
getNextObject, start_insort_right
28
from pitivi.log.loggable import Loggable
27
29
from pitivi.stream import VideoStream, AudioStream
28
30
from pitivi.factories.test import VideoTestSourceFactory, \
29
31
AudioTestSourceFactory
32
from pitivi.elements.mixer import SmartAdderBin
31
34
class TrackError(Exception):
34
class TrackObject(Signallable):
37
class Keyframe(Signallable):
39
"""Represents a single point on an interpolation curve"""
42
"value-changed" : ['value'],
43
"time-changed" : ['time'],
44
"mode-changed" : ['mode'],
47
def __init__(self, parent):
52
_mode = gst.INTERPOLATE_LINEAR
54
def setMode(self, mode):
56
self.parent.setKeyframeMode(self, mode)
58
self.setObjectMode(mode)
60
def setObjectMode(self, mode):
62
self.emit("mode-changed", mode)
67
mode = property(getMode, setMode)
71
def setTime(self, time):
73
self.parent.setKeyframeTime(self, time)
75
self.setObjectTime(time)
77
def setObjectTime(self, time):
79
self.emit("time-changed", time)
84
time = property(getTime, setTime)
88
def setValue(self, value):
90
self.parent.setKeyframeValue(self, value)
92
self.setObjectValue(value)
94
def setObjectValue(self, value):
96
self.emit("value-changed", value)
101
value = property(getValue, setValue)
103
def __cmp__(self, other):
105
return cmp(self.time, other.time)
108
class FixedKeyframe(Keyframe):
110
def setTime(self, time):
113
time = property(Keyframe.getTime, setTime)
115
class Interpolator(Signallable, Loggable):
117
"""The bridge between the gstreamer dynamic property API and pitivi track
120
* creates gnl.Controller() objects
121
* binds controller to track object's gnlobject
122
* allows client code to manipulate the interpolation curve by adding,
123
removing, and mutating discrete keyframe objects
125
There are two special control points: the start and end points, which are
126
"fixed" to the start and end of the clip in the timeline.
128
Timestamps given are assumed to be relative to the start of the clip. This
129
seems to be the normal behavior when the element being controlled is
130
internal to the gnlsource or gnloperation.
134
'keyframe-added' : ['keyframe'],
135
'keyframe-removed' : ['keyframe'],
136
'keyframe-moved' : ['keyframe'],
139
def __init__(self, trackobject, element, prop):
140
Loggable.__init__(self)
141
self.debug("track:%r, element:%r, property:%r", trackobject, element, prop)
143
# FIXME: get this from the property's param spec
144
# NOTE: keyframes necessarily work only on a closed range
148
# FIXME: don't necessarily want to create separate controllers for
149
# each Interpolator. We should instead create a ControlSource for each
150
# element. We can't do this until the new controller interface is
151
# exposed in gst-python.
152
self.attachToElementProperty(prop, element)
153
self._default = self._element.get_property(prop.name)
154
self.start = FixedKeyframe(self)
155
self.end = FixedKeyframe(self)
156
self.start.value = self._default
157
self.start.setObjectTime(0)
158
self.end.value = self._default
159
self.end.setObjectTime(trackobject.factory.duration)
161
def attachToElementProperty(self, prop, element):
162
self._element = element
163
self._property = prop
164
self.debug("Creating a GstController for element %r and property %s",
165
self._element, prop.name)
166
self._controller = gst.Controller(self._element, prop.name)
167
self._controller.set_interpolation_mode(prop.name, gst.INTERPOLATE_LINEAR)
169
def newKeyframe(self, time_or_keyframe, value=None, mode=None):
170
"""add a new keyframe at the specified time, optionally with specified
171
value and specified mode. If not specified, these will be computed so
172
that the new keyframe likes on the existing curve at that timestampi
174
returns: the keyframe object"""
176
if isinstance(time_or_keyframe, Keyframe):
177
keyframe = time_or_keyframe
179
# TODO: calculate value so that the new point doesn't change the shape
180
# of the curve when added. This might be tricky to achieve with cubic
181
# interpolation, but should work fine for linear and step
184
value = self._default
186
# FIXME: Controller.get_interpolation_mode is not wrapped in
187
# gst-python, so for now we assume the default is linear.
188
# Use the following code to get the current mode when this method becomes
190
# mode = self._controller.get_interpolation_mode()
191
mode = gst.INTERPOLATE_LINEAR
193
keyframe = Keyframe(self)
194
keyframe._time = time_or_keyframe
195
keyframe._value = value
196
keyframe._mode = mode
198
self.debug("time:%s, value:%r, mode:%r",
199
gst.TIME_ARGS(keyframe.time), keyframe.value, keyframe.mode)
201
self._keyframes.append(keyframe)
203
self._controller.set(self._property.name, keyframe.time, keyframe.value)
205
self.emit("keyframe-added", keyframe)
209
def removeKeyframe(self, keyframe):
210
self._controller.unset(self._property.name, keyframe.time)
211
if keyframe is not self.start and keyframe is not self.end:
212
self._keyframes.remove(keyframe)
213
self.emit("keyframe-removed", keyframe)
215
def setKeyframeMode(self, kf, mode):
216
# FIXME: currently InterpolationSourceControllers only support a
217
# single mode. Suporting per-keyframe modes would require implementing
218
# a custom interplation source controller.
219
# For now, whenever one keyframe's mode changes, we'll set the mode
221
for keyframe in self.keyframes:
222
keyframe.setObjectMode(mode)
223
self._controller.set_interpolation_mode(self._property.name, mode)
225
def setKeyframeTime(self, kf, time):
226
time = max(self.start.time, min(self.end.time, time))
227
self._keyframeTimeValueChanged(kf, time, kf.value)
228
kf.setObjectTime(time)
230
def setKeyframeValue(self, kf, value):
231
value = max(self.lower, min(self.upper, value))
232
self._keyframeTimeValueChanged(kf, kf.time, value)
233
kf.setObjectValue(value)
235
def _keyframeTimeValueChanged(self, kf, ptime, value):
236
self.debug("kf.time:%s, ptime:%s, value:%r",
237
gst.TIME_ARGS(kf.time),
238
gst.TIME_ARGS(ptime), value)
240
self._controller.unset(self._property.name, kf.time)
241
self._controller.set(self._property.name, ptime, value)
242
self.emit("keyframe-moved", kf)
244
def getKeyframes(self):
245
# TODO: This could be more efficient. We are re-sorting all the keyframes
246
# every time we iterate over them. One thought would be to keep the
247
# list sorted in-place as keyframes are added and moved.
249
for kf in sorted(self._keyframes):
253
def getInteriorKeyframes(self):
254
"""Same as above but does not include start, or end points"""
255
for kf in sorted(self._keyframes):
258
keyframes = property(getKeyframes)
260
class TrackObject(Signallable, Loggable):
36
263
'start-changed': ['start'],
37
264
'duration-changed': ['duration'],
74
305
self._connectToSignals(obj)
307
def getInterpolator(self, property_name):
308
self._maybeBuildInterpolators()
311
return self.interpolators[property_name][1]
313
raise TrackError("no interpolator for '%s'" % property_name)
315
def getInterpolators(self):
316
self._maybeBuildInterpolators()
317
return self.interpolators
319
def _maybeBuildInterpolators(self):
320
if not list(self.gnl_object.elements()):
321
raise TrackError("makeBin hasn't been called yet")
323
if not self._rebuild_interpolators:
326
self._rebuild_interpolators = False
328
factory_properties = self.factory.getInterpolatedProperties(self.stream).keys()
330
old_interpolators = self.interpolators
331
self.interpolators = {}
332
for gst_object, gst_object_property in \
333
get_controllable_properties(self.gnl_object):
334
if gst_object_property.name not in factory_properties:
338
interpolator = old_interpolators[gst_object_property.name][1]
340
interpolator = Interpolator(self, gst_object, gst_object_property)
342
interpolator.attachToElementProperty(gst_object_property,
345
# remove and add again the keyframes so they are set on the
347
for keyframe in list(interpolator.keyframes):
348
interpolator.removeKeyframe(keyframe)
349
interpolator.newKeyframe(keyframe)
351
self.interpolators[gst_object_property.name] = \
352
(gst_object_property, interpolator)
76
354
def release(self):
77
355
self._disconnectFromSignals()
84
362
other = cls(self.factory, self.stream, start=self.start,
85
363
duration=self.duration, in_point=self.in_point,
86
364
media_duration=self.media_duration, priority=self.priority)
87
other.trimmed_start = self.trimmed_start
366
if self.track is not None:
367
self.track.addTrackObject(other)
369
interpolators = self.getInterpolators()
370
for property, interpolator in interpolators.itervalues():
371
other_interpolator = other.getInterpolator(property.name)
372
other_interpolator.start.value = interpolator.start.value
373
other_interpolator.start.mode = interpolator.start.mode
374
other_interpolator.end.value = interpolator.end.value
375
other_interpolator.end.mode = interpolator.end.mode
376
for kf in interpolator.getInteriorKeyframes():
377
other_interpolator.newKeyframe(kf.time,
91
383
def snapStartDurationTime(self, *args):
94
# FIXME: there's a lot of boilerplate here that could be factored in a
95
# metaclass. Do we like metaclasses in pitivi?
96
386
def _getStart(self):
97
387
return self.gnl_object.props.start
99
def setStart(self, time, snap=False):
389
def setStart(self, position, snap=False):
100
390
if self.timeline_object is not None:
101
self.timeline_object.setStart(time, snap)
391
self.timeline_object.setStart(position, snap)
104
394
raise TrackError()
106
self.setObjectStart(time)
396
self.setObjectStart(position)
108
def setObjectStart(self, time):
109
self.gnl_object.props.start = time
398
def setObjectStart(self, position):
399
if self.gnl_object.props.start != position:
400
self.gnl_object.props.start = position
111
402
start = property(_getStart, setStart)
113
404
def _getDuration(self):
114
405
return self.gnl_object.props.duration
116
def setDuration(self, time, snap=False):
407
def setDuration(self, position, snap=False):
117
408
if self.timeline_object is not None:
118
self.timeline_object.setDuration(time, snap)
409
self.timeline_object.setDuration(position, snap)
121
412
raise TrackError()
123
self.setObjectDuration(time)
414
self.setObjectDuration(position)
125
def setObjectDuration(self, time):
126
self.gnl_object.props.duration = time
416
def setObjectDuration(self, position):
417
if self.gnl_object.props.duration != position:
418
self.gnl_object.props.duration = position
128
420
duration = property(_getDuration, setDuration)
130
422
def _getInPoint(self):
131
423
return self.gnl_object.props.media_start
133
def setInPoint(self, time, snap=False):
425
def setInPoint(self, position, snap=False):
134
426
if self.timeline_object is not None:
135
self.timeline_object.setInPoint(time, snap)
427
self.timeline_object.setInPoint(position, snap)
137
self.setObjectInPoint(time)
429
self.setObjectInPoint(position)
139
431
def setObjectInPoint(self, value):
140
self.gnl_object.props.media_start = value
432
if self.gnl_object.props.media_start != value:
433
self.gnl_object.props.media_start = value
142
435
in_point = property(_getInPoint, setInPoint)
175
469
self.setObjectPriority(priority)
177
471
def setObjectPriority(self, priority):
178
self.gnl_object.props.priority = priority
472
if self.gnl_object.props.priority != priority:
473
self.gnl_object.props.priority = priority
180
475
priority = property(_getPriority, setPriority)
182
def trimStart(self, time, snap=False):
477
def trimStart(self, position, snap=False):
183
478
if self.timeline_object is not None:
184
self.timeline_object.trimStart(time, snap)
479
self.timeline_object.trimStart(position, snap)
186
self.trimObjectStart(time)
188
def trimObjectStart(self, time):
189
# clamp time to be inside the object
190
time = max(self.start - self.trimmed_start, time)
191
time = min(time, self.start + self.duration)
192
new_duration = max(0, self.start + self.duration - time)
194
delta = time - self.start
195
self.trimmed_start += delta
196
self.setObjectStart(time)
481
self.trimObjectStart(position)
483
def trimObjectStart(self, position):
484
# clamp position to be inside the object
485
position = max(self.start - self.in_point, position)
486
position = min(position, self.start + self.duration)
487
new_duration = max(0, self.start + self.duration - position)
489
delta = position - self.start
490
in_point = self.in_point
492
self.setObjectStart(position)
197
493
self.setObjectDuration(new_duration)
198
self.setObjectInPoint(self.trimmed_start)
494
self.setObjectInPoint(in_point)
199
495
self.setObjectMediaDuration(new_duration)
201
def split(self, time, snap=False):
497
def split(self, position, snap=False):
202
498
if self.timeline_object is not None:
203
return self.timeline_object.split(time, snap)
499
return self.timeline_object.split(position, snap)
205
return self.splitObject(time)
501
return self.splitObject(position)
207
def splitObject(self, time):
503
def splitObject(self, position):
208
504
start = self.gnl_object.props.start
209
505
duration = self.gnl_object.props.duration
210
if time <= start or time >= start + duration:
211
raise TrackError("can't split at time %s" % gst.TIME_ARGS(time))
506
if position <= start or position >= start + duration:
507
raise TrackError("can't split at position %s" % gst.TIME_ARGS(position))
213
509
other = self.copy()
215
other.trimObjectStart(time)
216
self.setObjectDuration(time - self.gnl_object.props.start)
217
self.setObjectMediaDuration(time - self.gnl_object.props.start)
511
other.trimObjectStart(position)
512
self.setObjectDuration(position - self.gnl_object.props.start)
513
self.setObjectMediaDuration(position - self.gnl_object.props.start)