2
# PiTiVi , Non-linear video editor
6
# Copyright (c) 2005, Edward Hervey <bilboed@bilboed.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., 59 Temple Place - Suite 330,
21
# Boston, MA 02111-1307, USA.
29
from gettext import gettext as _
31
from pitivi.action import ViewAction
33
from pitivi.stream import VideoStream
34
from pitivi.utils import time_to_string, Seeker
35
from pitivi.log.loggable import Loggable
36
from pitivi.pipeline import PipelineError
37
from pitivi.ui.common import SPACING
39
class ViewerError(Exception):
42
# TODO : Switch to using Pipeline and Action
44
class PitiviViewer(gtk.VBox, Loggable):
46
__gtype_name__ = 'PitiviViewer'
48
"activate-playback-controls" : (gobject.SIGNAL_RUN_LAST,
49
gobject.TYPE_NONE, (gobject.TYPE_BOOLEAN,)),
53
A Widget to control and visualize a Pipeline
55
@cvar pipeline: The current pipeline
56
@type pipeline: L{Pipeline}
57
@cvar action: The action controlled by this Pipeline
58
@type action: L{ViewAction}
61
def __init__(self, action=None, pipeline=None):
63
@param action: Specific action to use instead of auto-created one
64
@type action: L{ViewAction}
66
gtk.VBox.__init__(self)
67
self.set_border_width(SPACING)
69
Loggable.__init__(self)
70
self.log("New PitiviViewer")
72
self.seeker = Seeker(80)
73
self.seeker.connect('seek', self._seekerSeekCb)
75
self.pipeline = pipeline
77
self.current_time = long(0)
78
self._initial_seek = None
79
self.current_frame = -1
81
self.currentState = gst.STATE_PAUSED
85
self.setAction(action)
86
self.setPipeline(pipeline)
88
def setPipeline(self, pipeline):
90
Set the Viewer to the given Pipeline.
92
Properly switches the currently set action to that new Pipeline.
94
@param pipeline: The Pipeline to switch to.
95
@type pipeline: L{Pipeline}.
97
self.debug("self.pipeline:%r, pipeline:%r", self.pipeline, pipeline)
99
if pipeline is not None and pipeline == self.pipeline:
102
if self.pipeline != None:
103
# remove previously set Pipeline
104
self._disconnectFromPipeline()
106
self._setUiActive(False)
107
# finally remove previous pipeline
109
self.currentState = gst.STATE_PAUSED
110
self.playpause_button.setPause()
111
self._connectToPipeline(pipeline)
112
self.pipeline = pipeline
113
if self.pipeline is not None:
116
def setAction(self, action):
118
Set the controlled action.
120
@param action: The Action to set. If C{None}, a default L{ViewAction}
122
@type action: L{ViewAction} or C{None}
124
self.debug("self.action:%r, action:%r", self.action, action)
125
if action is not None and action == self.action:
128
if self.action != None:
129
# if there was one previously, remove it
130
self._disconnectFromAction()
132
# get the default action
133
action = self._getDefaultAction()
134
self._connectToAction(action)
137
def _connectToPipeline(self, pipeline):
138
self.debug("pipeline:%r", pipeline)
139
if self.pipeline != None:
140
raise ViewerError("previous pipeline wasn't disconnected")
141
self.pipeline = pipeline
142
if self.pipeline == None:
144
self.pipeline.connect('position', self._posCb)
145
self.pipeline.activatePositionListener()
146
self.pipeline.connect('state-changed', self._currentStateCb)
147
self.pipeline.connect('element-message', self._elementMessageCb)
148
self.pipeline.connect('duration-changed', self._durationChangedCb)
149
self.pipeline.connect('eos', self._eosCb)
150
# if we have an action set it to that new pipeline
152
self.pipeline.setAction(self.action)
153
self.action.activate()
155
def _disconnectFromPipeline(self):
156
self.debug("pipeline:%r", self.pipeline)
157
if self.pipeline == None:
158
# silently return, there's nothing to disconnect from
160
if self.action and (self.action in self.pipeline.actions):
161
# if we have an action, properly remove it from pipeline
162
if self.action.isActive():
164
self.action.deactivate()
165
self.pipeline.removeAction(self.action)
167
self.pipeline.disconnect_by_function(self._posCb)
168
self.pipeline.disconnect_by_function(self._currentStateCb)
169
self.pipeline.disconnect_by_function(self._elementMessageCb)
170
self.pipeline.disconnect_by_function(self._durationChangedCb)
171
self.pipeline.disconnect_by_function(self._eosCb)
176
def _connectToAction(self, action):
177
self.debug("action: %r", action)
178
# not sure what we need to do ...
180
# FIXME: fix this properly?
181
self.drawingarea.action = action
184
producer = action.producers[0]
185
self.debug("producer:%r", producer)
186
for stream in producer.output_streams:
187
self.warning("stream:%r", stream)
188
for stream in producer.getOutputStreams(VideoStream):
189
self.debug("stream:%r", stream)
195
self.setDisplayAspectRatio(dar)
198
def _disconnectFromAction(self):
201
def _setUiActive(self, active=True):
202
self.debug("active %r", active)
203
self.set_sensitive(active)
205
for item in [self.slider, self.goToStart_button, self.back_button,
206
self.playpause_button, self.forward_button,
207
self.goToEnd_button, self.timelabel]:
208
item.set_sensitive(active)
210
self.emit("activate-playback-controls", True)
212
def _getDefaultAction(self):
216
""" Creates the Viewer GUI """
218
self.aframe = gtk.AspectFrame(xalign=0.5, yalign=0.5, ratio=4.0/3.0,
220
self.pack_start(self.aframe, expand=True)
221
self.drawingarea = ViewerWidget(self.action)
222
self.aframe.add(self.drawingarea)
225
self.posadjust = gtk.Adjustment()
226
self.slider = gtk.HScale(self.posadjust)
227
self.slider.set_draw_value(False)
228
self.slider.connect("button-press-event", self._sliderButtonPressCb)
229
self.slider.connect("button-release-event", self._sliderButtonReleaseCb)
230
self.slider.connect("scroll-event", self._sliderScrollCb)
231
self.pack_start(self.slider, expand=False)
232
self.moving_slider = False
233
self.slider.set_sensitive(False)
237
boxalign = gtk.Alignment(xalign=0.5, yalign=0.5)
239
self.pack_start(boxalign, expand=False)
241
self.goToStart_button = gtk.ToolButton(gtk.STOCK_MEDIA_PREVIOUS)
242
self.goToStart_button.connect("clicked", self._goToStartCb)
243
self.goToStart_button.set_tooltip_text(_("Go to the beginning of the timeline"))
244
self.goToStart_button.set_sensitive(False)
245
bbox.pack_start(self.goToStart_button, expand=False)
247
self.back_button = gtk.ToolButton(gtk.STOCK_MEDIA_REWIND)
248
self.back_button.connect("clicked", self._backCb)
249
self.back_button.set_tooltip_text(_("Go back one second"))
250
self.back_button.set_sensitive(False)
251
bbox.pack_start(self.back_button, expand=False)
253
self.playpause_button = PlayPauseButton()
254
self.playpause_button.connect("play", self._playButtonCb)
255
bbox.pack_start(self.playpause_button, expand=False)
256
self.playpause_button.set_sensitive(False)
258
self.forward_button = gtk.ToolButton(gtk.STOCK_MEDIA_FORWARD)
259
self.forward_button.connect("clicked", self._forwardCb)
260
self.forward_button.set_tooltip_text(_("Go forward one second"))
261
self.forward_button.set_sensitive(False)
262
bbox.pack_start(self.forward_button, expand=False)
264
self.goToEnd_button = gtk.ToolButton(gtk.STOCK_MEDIA_NEXT)
265
self.goToEnd_button.connect("clicked", self._goToEndCb)
266
self.goToEnd_button.set_tooltip_text(_("Go to the end of the timeline"))
267
self.goToEnd_button.set_sensitive(False)
268
bbox.pack_start(self.goToEnd_button, expand=False)
271
self.timelabel = gtk.Label()
272
self.timelabel.set_markup("<tt>00:00:00.000</tt>")
273
self.timelabel.set_alignment(1.0, 0.5)
274
bbox.pack_start(self.timelabel, expand=False, padding=10)
277
screen = gdk.screen_get_default()
278
height = screen.get_height()
280
# show the controls and force the aspect frame to have at least the same
281
# width (+110, which is a magic number to minimize dead padding).
283
width, height = bbox.size_request()
285
height = int(width / self.aframe.props.ratio)
286
self.aframe.set_size_request(width , height)
289
_showingSlider = True
291
def showSlider(self):
292
self._showingSlider = True
295
def hideSlider(self):
296
self._showingSlider = False
299
def showControls(self):
303
self.goToStart_button.show()
304
self.back_button.show()
305
self.playpause_button.show()
306
self.forward_button.show()
307
self.goToEnd_button.show()
308
if self._showingSlider:
311
self.goToStart_button.hide()
312
self.back_button.hide()
313
self.playpause_button.hide()
314
self.forward_button.hide()
315
self.goToEnd_button.hide()
318
def setDisplayAspectRatio(self, ratio):
320
Sets the DAR of the Viewer to the given ratio.
322
@arg ratio: The aspect ratio to set on the viewer
323
@type ratio: L{float}
325
self.debug("Setting ratio of %f [%r]", float(ratio), ratio)
327
self.aframe.set_property("ratio", float(ratio))
329
self.warning("could not set ratio !")
331
## gtk.HScale callbacks for self.slider
333
def _sliderButtonPressCb(self, slider, event):
334
# borrow totem hack for seek-on-click behavior
336
self.info("button pressed")
337
self.moving_slider = True
338
self.valuechangedid = slider.connect("value-changed", self._sliderValueChangedCb)
339
self.pipeline.pause()
342
def _sliderButtonReleaseCb(self, slider, event):
343
# borrow totem hack for seek-on-click behavior
345
self.info("slider button release at %s", time_to_string(long(slider.get_value())))
346
self.moving_slider = False
347
if self.valuechangedid:
348
slider.disconnect(self.valuechangedid)
349
self.valuechangedid = 0
350
# revert to previous state
351
if self.currentState == gst.STATE_PAUSED:
352
self.pipeline.pause()
357
def _sliderValueChangedCb(self, slider):
358
""" seeks when the value of the slider has changed """
359
value = long(slider.get_value())
360
self.info(gst.TIME_ARGS(value))
361
if self.moving_slider:
364
def _sliderScrollCb(self, unused_slider, event):
365
if event.direction == gtk.gdk.SCROLL_LEFT:
369
self.seekRelative(amount)
371
def seek(self, position, format=gst.FORMAT_TIME):
373
self.seeker.seek(position, format)
375
self.warning("seek failed")
377
def _seekerSeekCb(self, seeker, position, format):
379
self.pipeline.seek(position, format)
380
except PipelineError:
381
self.error("seek failed %s %s", gst.TIME_ARGS(position), format)
383
def _newTime(self, value, frame=-1):
384
self.info("value:%s, frame:%d", gst.TIME_ARGS(value), frame)
385
self.current_time = value
386
self.current_frame = frame
387
self.timelabel.set_markup("<tt>%s</tt>" % time_to_string(value))
388
if not self.moving_slider:
389
self.posadjust.set_value(float(value))
393
## active Timeline calllbacks
395
def _durationChangedCb(self, unused_pipeline, duration):
396
self.debug("duration : %s", gst.TIME_ARGS(duration))
397
position = self.posadjust.get_value()
398
if duration < position:
399
self.posadjust.set_value(float(duration))
400
self.posadjust.upper = float(duration)
403
self._setUiActive(False)
405
self._setUiActive(True)
407
if self._initial_seek is not None:
408
seek, self._initial_seek = self._initial_seek, None
409
self.pipeline.seek(seek)
411
## Control gtk.Button callbacks
413
def _goToStartCb(self, unused_button):
416
def _backCb(self, unused_button):
417
self.seekRelative(-gst.SECOND)
419
def _playButtonCb(self, unused_button, isplaying):
420
self.togglePlayback()
422
def _forwardCb(self, unused_button):
423
self.seekRelative(gst.SECOND)
425
def _goToEndCb(self, unused_button):
427
dur = self.pipeline.getDuration()
430
self.warning("couldn't get duration")
432
## public methods for controlling playback
438
self.pipeline.pause()
440
def togglePlayback(self):
441
if self.pipeline is None:
443
self.pipeline.togglePlayback()
445
def seekRelative(self, time):
447
self.pipeline.seekRelative(time)
449
self.warning("seek failed")
451
def _posCb(self, unused_pipeline, pos):
454
def _currentStateCb(self, unused_pipeline, state):
455
self.info("current state changed : %s", state)
456
if state == int(gst.STATE_PLAYING):
457
self.playpause_button.setPause()
458
elif state == int(gst.STATE_PAUSED):
459
self.playpause_button.setPlay()
460
self.currentState = state
462
def _eosCb(self, unused_pipeline):
463
self.playpause_button.setPlay()
465
def _elementMessageCb(self, unused_pipeline, message):
466
name = message.structure.get_name()
467
self.log('message:%s / %s', message, name)
468
if name == 'prepare-xwindow-id':
470
sink.set_xwindow_id(self.drawingarea.window_xid)
473
class ViewerWidget(gtk.DrawingArea, Loggable):
475
Widget for displaying properly GStreamer video sink
480
def __init__(self, action):
481
gtk.DrawingArea.__init__(self)
482
Loggable.__init__(self)
483
self.action = action # FIXME : Check if it's a view action
484
self.unset_flags(gtk.SENSITIVE)
485
for state in range(gtk.STATE_INSENSITIVE + 1):
486
self.modify_bg(state, self.style.black)
488
def do_realize(self):
489
gtk.DrawingArea.do_realize(self)
490
if platform.system() == 'Windows':
491
self.window_xid = self.window.handle
493
self.window_xid = self.window.xid
495
class PlayPauseButton(gtk.Button, Loggable):
496
""" Double state gtk.Button which displays play/pause """
499
"play" : ( gobject.SIGNAL_RUN_LAST,
501
(gobject.TYPE_BOOLEAN, ))
505
gtk.Button.__init__(self, label="")
506
Loggable.__init__(self)
507
self.get_settings().props.gtk_button_images = True
510
self.connect('clicked', self._clickedCb)
512
def set_sensitive(self, value):
513
gtk.Button.set_sensitive(self, value)
515
def _clickedCb(self, unused):
516
self.emit("play", self.playing)
519
""" display the play image """
522
self.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_BUTTON))
523
self.set_tooltip_text(_("Play"))
528
""" display the pause image """
530
self.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PAUSE, gtk.ICON_SIZE_BUTTON))
531
self.set_tooltip_text(_("Pause"))