Package elisa :: Package core :: Module player
[hide private]
[frames] | no frames]

Source Code for Module elisa.core.player

  1  # -*- coding: utf-8 -*- 
  2  # Elisa - Home multimedia server 
  3  # Copyright (C) 2006-2008 Fluendo Embedded S.L. (www.fluendo.com). 
  4  # All rights reserved. 
  5  # 
  6  # This file is available under one of two license agreements. 
  7  # 
  8  # This file is licensed under the GPL version 3. 
  9  # See "LICENSE.GPL" in the root of this distribution including a special 
 10  # exception to use Elisa with Fluendo's plugins. 
 11  # 
 12  # The GPL part of Elisa is also available under a commercial licensing 
 13  # agreement from Fluendo. 
 14  # See "LICENSE.Elisa" in the root directory of this distribution package 
 15  # for details on that license. 
 16   
 17   
 18  __maintainer__ = 'Benjamin Kampmann <benjamin@fluendo.com>' 
 19   
 20   
 21  from elisa.core.bus import bus, bus_message 
 22  from elisa.core import common, log 
 23  from elisa.extern import enum 
 24   
 25  from elisa.core.utils import classinit 
 26   
 27  from elisa.core.player_engine_registry import NoEngineFound 
 28   
 29  import pygst 
 30  pygst.require('0.10') 
 31  import gst 
 32   
 33  from gobject import GError 
 34   
35 -class PlayerPlaying(bus_message.Message):
36 """ 37 Sent over the message bus when the first frame or buffer of a media has 38 been outputted. 39 """
40
41 -class PlayerStopping(bus_message.Message):
42 """ 43 Sent over the message bus when the playing is stopped. 44 45 @ivar percent: how much of the media has been shown. 46 @type percent: float 47 """ 48
49 - def __init__(self, percent=100):
50 """ 51 Initialize the PlayerStopping Message. 52 53 @param percent: the percent of the media, that was been watched yet 54 @type percent: float 55 """ 56 bus_message.Message.__init__(self) 57 self.percent = percent
58 59 # FIXME: the reason why it was stopped could be useful (like EOS/EOF, 60 # Error, StreamBroke or Requested) 61
62 -class PlayerLoading(bus_message.Message):
63 """ 64 Sent over the message bus when the player is asked to play. When the 65 playback B{really} starts, that is when the first buffer reach the sink, 66 the L{elisa.core.player.PlayerPlaying} message is sent over the bus. 67 68 That message can be understood as the first answer after doing 69 L{elisa.core.player.Player.play}. 70 71 Warning: this message is B{not} sent when the uri is set. 72 """
73
74 -class PlayerBuffering(bus_message.Message):
75 """ 76 Sent if the player is loading before playing or if it is prebuffering during 77 playing. This message might be sent very often and is mainly useful for 78 user interface updates. 79 80 This message is optional and the frontend should not expect that every 81 player sends it or is able to send it. 82 83 @ivar progress: the progress in percent 84 @type progress: float 85 """ 86 87 __metaclass__ = classinit.ClassInitMeta 88 __classinit__ = classinit.build_properties 89
90 - def __init__(self,progress):
91 bus_message.Message.__init__(self) 92 self._progress = 0 93 self.progress = progress
94
95 - def progress__get(self):
96 return self._progress
97
98 - def progress__set(self, progress):
99 if progress > 100: 100 self._progress = 100 101 elif progress < 0: 102 self._progress = 0 103 else: 104 self._progress = progress
105
106 -class PlayerPausing(bus_message.Message):
107 """ 108 Sent over the message bus when the playing is paused. 109 """
110
111 -class NewClock(bus_message.Message):
112 """ 113 This message is triggered everytime the engine gets a new Clock. 114 It is independent from any trigger_message-settings. It is simply always 115 send, when gstreamer finds a new one. 116 Even this message is send by the engine, please think, that the 117 message_sender is maybe overritten. 118 119 @ivar clock: the new clock that was found 120 @type clock: L{gst.Clock} 121 """ 122 clock = None
123 - def __init__(self, clock):
124 """ 125 set the clock to clock. This is the new clock, the engine is using. 126 """ 127 bus_message.Message.__init__(self) 128 self.clock = clock
129
130 -class NewBaseTime(bus_message.Message):
131 """ 132 This message is triggered everytime, the engine got a new BaseTime. This 133 message simply is send everytime a gstreamer change-state message is 134 coming to the engine. 135 This message is used for synchronization. 136 137 @ivar base_time: the new base time 138 @type base_time: L{gst.ClockTime} 139 """ 140
141 - def __init__(self, base_time):
142 bus_message.Message.__init__(self) 143 self.base_time = base_time
144
145 -class PlayerError(bus_message.Message):
146 """ 147 Sent over the message bus if the player encountered an issue. 148 149 @ivar error: the error that occured 150 @type error: str 151 """
152 - def __init__(self, error):
153 bus_message.Message.__init__(self) 154 self.error = error
155 156 # FIXME: need for a "codec missing" message 157 158 STATES = enum.Enum("LOADING", "PLAYING", "PAUSED", "STOPPED") 159 """ 160 STATES.LOADING : The media is still loading. When it is finished the state 161 is switched to playing. 162 STATES.PLAYING : The player is currently playing a media. 163 STATES.PAUSED : The player is paused. It means the media is paused at the last 164 played position and will continue to play from there. 165 STATES.STOPPED : The player is stopped. It will restart playing from the 166 beginning of the media. 167 """ 168
169 -class Player(log.Loggable):
170 """ 171 A player can play one audio or video media at a time. All it needs is a 172 L{elisa.core.media_uri.MediaUri} and the sinks for the video and audio 173 output. It can also do audio only output and has support for subtitles. 174 175 @ivar video_sink: the video sink that this player outputs to 176 @type video_sink: L{gst.BaseSink} 177 178 @ivar name: the name of the player instance 179 @type name: string 180 181 @ivar audio_sink: the audio sink that this player outputs to 182 @type audio_sink: L{gst.BaseSink} 183 184 @ivar volume: the volume level between 0 and 10 185 @type volume: float 186 187 @ivar position: the position we are currently playing in 188 nanoseconds; when set, if the value passed is higher 189 than L{duration}, position is set to L{duration}. 190 If the value passed is lower than 0, position is 191 set to 0. 192 @type position: int 193 194 @ivar duration: (read-only) the total length of the loaded media in 195 nanoseconds 196 @type duration: int 197 198 @ivar speed: The speed of the current playback: 199 - Normal playback is 1.0 200 - a positive value means forward 201 - a negative one backward 202 - the value 0.0 (equivalent to pause) is not 203 allowed 204 @type speed: float 205 206 @ivar state: (read-only) The current state. See 207 L{elisa.core.player.STATES}. 208 @type state: L{elisa.core.player.STATES} 209 210 @ivar playing: (read-only) is the player currently playing? That 211 also returns False if the player is in LOADING 212 state. 213 @type playing: bool 214 215 @ivar uri: the uri of the media loaded in the player. 216 @type uri: L{elisa.core.media_uri.MediaUri} 217 218 @ivar subtitle_uri: the uri for subtitles 219 @type subtitle_uri: L{elisa.core.media_uri.MediaUri} 220 221 @ivar subtitle_callback: the callback, where the timed subtitle texts 222 should be sent to. The callback will get a 223 L{gst.Buffer}, containing the subtitle text to be 224 displayed encoded in text/plain or 225 text/x-pango-markup 226 @type subtitle_callback: callable 227 228 @ivar muted: True if the player is muted, False otherwise. This 229 is independent of the volume attribute (eg. can be 230 False even if volume is 0). 231 @type muted: bool 232 """ 233 234 __metaclass__ = classinit.ClassInitMeta 235 __classinit__ = classinit.build_properties 236
237 - def __init__(self, registry):
238 """ 239 @param registry: the registry to ask for new engines 240 @type registry: L{elisa.core.engine_registry.EngineRegistry} 241 """ 242 self._engine = None 243 self._unmuted_volume = -1 244 self._cache = False 245 246 # Subtitle support 247 self._make_subtitle_pipeline() 248 self._sub_uri = None 249 self._sub_callback = None 250 251 self._loaded = False 252 self._video_sink = None 253 self._audio_sink = None 254 255 self._player_engine_registry = registry 256 257 self._uri = None 258 self._visualisation = None 259 260 application = common.application 261 self._audiosink = application.config.get_option('audiosink', 262 section='player', 263 default='autoaudiosink') 264 265 self._audiosettings = application.config.get_option(self._audiosink, 266 section='player', 267 default={}) 268 269 application.bus.register(self._new_clock, NewClock) 270 application.bus.register(self._new_base_time, NewBaseTime) 271 application.bus.register(self._engine_stopped, PlayerStopping) 272 application.bus.register(self._engine_error, PlayerError)
273 274 275 # Main Player Functionality 276
277 - def play(self, trigger_message=True):
278 """ 279 Play the media. If trigger_message is set to True, this triggers first 280 the message L{elisa.core.player.PlayerLoading} message and if the 281 playback is really starting, it triggers 282 L{elisa.core.player.PlayerPlaying}. Otherwise it does not trigger any 283 messages. 284 285 @param trigger_message: should the player trigger messages here 286 @type trigger_message: bool 287 """ 288 if not self._loaded: 289 return 290 291 self._engine.play(trigger_message)
292
293 - def pause(self, trigger_message=True):
294 """ 295 Pause the playback. If trigger_message is set to True, this triggers 296 the L{elisa.core.player.PlayerPausing} message. 297 298 @param trigger_message: should the player trigger a message here 299 @type trigger_message: bool 300 """ 301 if not self._loaded: 302 return 303 304 self._engine.pause(trigger_message) 305 if self._sub_uri: 306 self._sub_pipeline.set_state(gst.STATE_PAUSED)
307
308 - def stop(self, trigger_message=True):
309 """ 310 Stop the playback. This is _not_ effecting the subtitles. If 311 trigger_message is set, this method triggers the 312 L{elisa.core.player.PlayerStopping} message. 313 314 @param trigger_message: should the player trigger a message here 315 @type trigger_message: bool 316 """ 317 if not self._loaded: 318 return 319 320 if self._engine.state != STATES.STOPPED: 321 self._engine.stop(trigger_message)
322 323
324 - def restart_from_beginning(self):
325 """ 326 Play the uri from the beginning. This is not triggering any 327 messages. 328 """ 329 if not self._loaded: 330 return 331 332 # FIXME: pause/play/stop are async. That is might not working. 333 334 self._engine.pause(trigger_message=False) 335 self._engine.position = 0 336 337 if self._engine.position != 0: 338 # This means that the position setting didn't work. 339 self._engine.stop(trigger_message=False) 340 341 self._engine.play(trigger_message=False)
342 343
344 - def toggle_play_pause(self, trigger_message=True):
345 """ 346 Toggle the player between play and pause state. If it is not playing 347 yet, then start it. If trigger_message is set, this method might 348 triggers L{elisa.core.player.PlayerPlaying} and 349 L{elisa.core.player.PlayerLoading} or 350 L{elisa.core.player.PlayerPausing}. 351 352 353 @param trigger_message: should the player trigger a message here 354 @type trigger_message: bool 355 """ 356 if not self._loaded: 357 return 358 359 self.debug("Toggle Play/Pause") 360 361 if self.playing: 362 self.pause(trigger_message) 363 else: 364 self.play(trigger_message)
365
366 - def playing__get(self):
367 return self.state == STATES.PLAYING
368 369 # Volume 370
371 - def volume__set(self, volume):
372 if not self._loaded: 373 return 374 375 self.debug("Volume set to %s" % volume) 376 377 if self.muted: 378 volume = self._unmuted_volume 379 self._unmuted_volume = -1 380 381 self._engine.volume = volume
382
383 - def volume__get(self):
384 if not self._loaded: 385 return 386 387 if self.muted: 388 return self._unmuted_volume 389 390 return self._engine.volume
391
392 - def muted__set(self, value):
393 if not self._loaded: 394 return 395 396 self.debug("Muting set to %s" % value) 397 398 if value == True: 399 old_volume = self.volume 400 self.volume = 0 401 self._unmuted_volume = old_volume 402 else: 403 if self._unmuted_volume >= 0: 404 self.volume = self._unmuted_volume 405 self._unmuted_volume = -1
406
407 - def muted__get(self):
408 if not self._loaded: 409 return 410 return self._unmuted_volume != -1
411 412 # For URI Support 413
414 - def uri__set(self, uri):
415 416 # FIXME: is that the right way to do it here? 417 if uri == self._uri and self.playing: 418 return 419 420 self._uri = uri 421 if uri == None: 422 return 423 e_reg = self._player_engine_registry 424 media_manager = common.application.media_manager 425 bus = common.application.bus 426 uri = media_manager.get_real_uri(uri) 427 scheme = uri.scheme 428 429 if self._engine: 430 # let's test, if this player supports this uri! 431 if scheme in self._engine.uri_schemes.keys(): 432 self._engine.uri = uri 433 self._loaded = True 434 else: 435 self._engine.stop(trigger_message=False) 436 try: 437 new_engine = e_reg.create_engine_for_scheme(scheme) 438 self._remove_engine() 439 # Catch Gstreamer-Exceptions here to be sure that the 440 # linking did work and the unlinking was also done correct 441 # maybe? 442 self._engine = new_engine 443 if self._video_sink: 444 self._engine.video_sink = self._video_sink 445 if self._audio_sink: 446 self._engine.audio_sink = self._audio_sink 447 else: 448 self._engine.audio_sink = self._new_audio_sink() 449 self._engine.message_sender = self 450 self._engine.uri = uri 451 self._loaded = True 452 except NoEngineFound: 453 # TODO: what if there is no engine found? 454 self.warning('No Engine found for the uri: %s' % uri) 455 self._loaded = False 456 bus.send_message(PlayerError('No engine found'), sender=self) 457 458 else: 459 new_engine = e_reg.create_engine_for_scheme(scheme) 460 try: 461 self._engine = new_engine 462 self._engine.message_sender = self 463 if self._video_sink: 464 self._engine.video_sink = self._video_sink 465 if self._audio_sink: 466 self._engine.audio_sink = self._audio_sink 467 else: 468 self._engine.audio_sink = self._new_audio_sink() 469 self._engine.uri = uri 470 self._engine.visualisation = self.visualisation 471 self._loaded = True 472 except NoEngineFound: 473 self.warning("No Engine found for the uri %s" % uri) 474 bus.send_message(PlayerError('No engine found'), sender=self)
475
476 - def uri__get(self):
477 return self._uri
478
479 - def _remove_engine(self):
480 self._loaded = False 481 if self._engine: 482 self._engine.stop(trigger_message=False) 483 self._video_sink = self._engine.video_sink 484 self._engine.video_sink = None 485 self._audio_sink = self._engine.audio_sink 486 self._engine.audio_sink = None 487 self._engine = None
488
489 - def subtitle_uri__set(self, uri):
490 self._sub_uri = uri 491 self.debug("Setting Subtitle URI to: '%s'" % uri) 492 if self._sub_uri == None: 493 return self._update_subtitle_handoff() 494 495 self._make_subtitle_pipeline() 496 497 decode = self._sub_pipeline.get_by_name('src') 498 if decode.get_factory().get_name() == "filesrc": 499 if uri.scheme != 'file': 500 self.warning("We only support file-scheme uris for" 501 " subtitles. If you want another scheme" 502 " please install gstreamer >= 0.10.14 with" 503 " uridecodebin!") 504 self._sub_uri = None 505 else: 506 decode.set_property('location', uri.path) 507 else: 508 # we were able to set uri in decodebin 509 decode.set_property('uri', str(uri)) 510 511 self._sub_pipeline.set_new_stream_time(gst.CLOCK_TIME_NONE) 512 513 self._update_subtitle_handoff() 514 515 if self.playing: 516 self.pause(trigger_message = False) 517 self.play(trigger_message = False)
518
519 - def subtitle_uri__get(self):
520 return self._sub_uri
521
522 - def visualisation__get(self):
523 return self._visualisation
524
525 - def visualisation__set(self, new_visu):
526 # That feels bad to me... 527 self._visualisation = new_visu
528 529
530 - def subtitle_callback__set(self, callback):
531 self._sub_callback = callback 532 self._update_subtitle_handoff()
533
534 - def subtitle_callback__get(self):
535 self._sub_callback
536
537 - def _engine_error(self, message, sender):
538 self._engine_stopped(message, sender)
539
540 - def _engine_stopped(self, message, sender):
541 if sender == self: 542 self._sub_pipeline.set_state(gst.STATE_NULL)
543
544 - def _update_subtitle_handoff(self):
545 sink = self._sub_pipeline.get_by_name('sink') 546 value = (self._sub_callback != None) and (self._sub_uri != None) 547 self.debug("Signal handoffs activated: %s" % value) 548 sink.set_property('signal-handoffs', value)
549 550
551 - def _new_clock(self, message, sender):
552 if sender != self: 553 return 554 555 new_clock = message.clock 556 self.debug("New clock %s" % new_clock) 557 self._sub_pipeline.use_clock(new_clock)
558
559 - def _new_base_time(self, message, sender):
560 if sender != self: 561 return 562 563 base_time = message.base_time 564 if self._sub_uri != None: 565 self._sub_pipeline.set_base_time(base_time) 566 self._sub_pipeline.seek(1, gst.FORMAT_TIME, 567 gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_KEY_UNIT, 568 gst.SEEK_TYPE_SET, self.position, 569 gst.SEEK_TYPE_SET, self.duration) 570 self._sub_pipeline.set_state(gst.STATE_PLAYING) 571 self.debug("Got new basetime, starting to play!")
572 573 # other states and attributes of the player 574
575 - def state__get(self):
576 if not self._engine: 577 return STATES.STOPPED 578 return self._engine.state
579
580 - def position__get(self):
581 if not self._engine: 582 return -1 583 return self._engine.position
584
585 - def position__set(self, position):
586 if not self._engine: 587 return 588 589 duration = self.duration 590 if position < 0: 591 position = 0 592 elif duration > 0 and position > duration: 593 position = duration 594 595 if self._sub_callback: 596 worki = gst.Buffer() 597 worki.duration = 1 598 self._sub_callback(worki) 599 self._engine.position = position
600
601 - def duration__get(self):
602 if not self._engine: 603 return -1 604 605 return self._engine.duration
606
607 - def speed__get(self):
608 if not self._engine: 609 return 1 610 611 return self._engine.speed
612
613 - def speed__set(self, speed):
614 if not self._engine: 615 return 616 617 # FIXME: we have to do this on the subtitles too 618 self._engine.speed = speed
619
620 - def video_sink__get(self):
621 return self._video_sink
622
623 - def video_sink__set(self, sink):
624 self._video_sink = sink 625 if self._engine: 626 self._engine.video_sink = self._video_sink
627
628 - def audio_sink__get(self):
629 return self._audio_sink
630
631 - def audio_sink__set(self, sink):
632 self._audio_sink = sink 633 if self._engine: 634 self._engine.audio_sink = self._audio_sink
635 636 # Internal methods 637
638 - def _new_handoff(self, element, buffer, pad):
639 if self._sub_callback: 640 self.debug("calling %s with buffer %s" % (self._sub_callback, buffer)) 641 self._sub_callback(buffer)
642
643 - def _new_audio_sink(self):
644 # make a new sink 645 audio_sink = gst.element_factory_make(self._audiosink) 646 for key, value in self._audiosettings.iteritems(): 647 audio_sink.set_property(key, value) 648 return audio_sink
649
650 - def _make_subtitle_pipeline(self):
651 uri_dec = "uridecodebin name=src ! "\ 652 "text/plain;text/x-pango-markup ! " \ 653 "fakesink name=sink sync=true" 654 filesrc = "filesrc name=src ! "\ 655 "decodebin ! text/plain;text/x-pango-markup ! "\ 656 "fakesink name=sink sync=true" 657 658 try: 659 self._sub_pipeline = gst.parse_launch(uri_dec) 660 except GError: 661 # using uridecode didn't work 662 self._sub_pipeline = gst.parse_launch(filesrc) 663 664 sink = self._sub_pipeline.get_by_name('sink') 665 sink.set_property('signal-handoffs', False) 666 sink.connect('handoff', self._new_handoff)
667