def __init__(self): self.xid = None self.mute_volume=None # fullscreen gtk.Window self.fullscreen_window=None self.snapshotter=Snapshotter(self.snapshot_taken, width=config.data.player['snapshot-width']) #self.snapshotter.start() # This method should be set by caller: self.snapshot_notify=None self.build_pipeline() self.caption=Caption() self.caption.text="" self.caption.begin=-1 self.caption.end=-1 self.overlay=Caption() self.overlay.data='' self.overlay.begin=-1 self.overlay.end=-1 self.videofile=None self.status=Player.UndefinedStatus self.current_position_value = 0 self.stream_duration = 0 self.relative_position=self.create_position(0, origin=self.RelativePosition) self.position_update()
def __init__(self): self.xid = None self.mute_volume = None self.rate = 1.0 # fullscreen Gtk.Window self.fullscreen_window = None self.fullscreen_drawable = None # Fullscreen timestamp display - cache data self.last_timestamp = 0 self.last_timestamp_update = 0 try: self.snapshotter = Snapshotter( self.snapshot_taken, width=config.data.player['snapshot-width']) except Exception as e: self.log("Could not initialize snapshotter:" + str(e)) self.snapshotter = None self.fullres_snapshotter = None # This method has the following signature: # self.fullres_snapshot_callback(snapshot=None, message=None) # If snapshot is None, then there should be an explanation (string) in msg. self.fullres_snapshot_callback = None #self.snapshotter.start() # This method should be set by caller: self.snapshot_notify = None self.build_pipeline() self.caption = Caption() self.caption.text = "" self.caption.begin = -1 self.caption.end = -1 self.overlay = Caption() self.overlay.data = '' self.overlay.begin = -1 self.overlay.end = -1 self.videofile = None self.status = Player.UndefinedStatus self.current_position_value = 0 self.stream_duration = 0 self.position_update()
def async_fullres_snapshot(self, position, callback): """Take full-resolution snapshots. This method is not reentrant: as long as there is a call pending, it is not available for another position. """ if self.fullres_snapshot_callback is not None: callback(message=_( "Cannot capture full-resolution snapshot, another capture is ongoing." )) return if self.fullres_snapshotter is None: # Initialise it. self.fullres_snapshotter = Snapshotter(self.fullres_snapshot_taken) self.fullres_snapshotter.set_uri(self.player.get_property('uri')) self.fullres_snapshot_callback = callback if not self.fullres_snapshotter.thread_running: self.fullres_snapshotter.start() self.fullres_snapshotter.enqueue(position)
def async_fullres_snapshot(self, position, callback): """Take full-resolution snapshots. This method is not reentrant: as long as there is a call pending, it is not available for another position. """ if self.fullres_snapshot_callback is not None: callback(message=_("Cannot capture full-resolution snapshot, another capture is ongoing.")) return if self.fullres_snapshotter is None: # Initialise it. self.fullres_snapshotter = Snapshotter(self.fullres_snapshot_taken) self.fullres_snapshotter.set_uri(self.player.get_property('uri')) self.fullres_snapshot_callback = callback if not self.fullres_snapshotter.thread_running: self.fullres_snapshotter.start() self.fullres_snapshotter.enqueue(position)
def __init__(self): self.xid = None self.mute_volume=None self.rate = 1.0 # fullscreen Gtk.Window self.fullscreen_window = None self.fullscreen_drawable = None # Fullscreen timestamp display - cache data self.last_timestamp = 0 self.last_timestamp_update = 0 try: self.snapshotter = Snapshotter(self.snapshot_taken, width=config.data.player['snapshot-width']) except Exception as e: self.log("Could not initialize snapshotter:" + str(e)) self.snapshotter = None self.fullres_snapshotter = None # This method has the following signature: # self.fullres_snapshot_callback(snapshot=None, message=None) # If snapshot is None, then there should be an explanation (string) in msg. self.fullres_snapshot_callback = None #self.snapshotter.start() # This method should be set by caller: self.snapshot_notify=None self.build_pipeline() self.caption=Caption() self.caption.text="" self.caption.begin=-1 self.caption.end=-1 self.overlay=Caption() self.overlay.data='' self.overlay.begin=-1 self.overlay.end=-1 self.videofile = None self.status = Player.UndefinedStatus self.current_position_value = 0 self.stream_duration = 0 self.position_update()
class Player: player_id = 'gstreamer' player_capabilities = [ 'seek', 'pause', 'caption', 'frame-by-frame', 'async-snapshot', 'set-rate', 'svg' ] # Status PlayingStatus = 0 PauseStatus = 1 InitStatus = 2 EndStatus = 3 UndefinedStatus = 4 def __init__(self): self.xid = None self.mute_volume = None self.rate = 1.0 # fullscreen Gtk.Window self.fullscreen_window = None self.fullscreen_drawable = None # Fullscreen timestamp display - cache data self.last_timestamp = 0 self.last_timestamp_update = 0 try: self.snapshotter = Snapshotter( self.snapshot_taken, width=config.data.player['snapshot-width']) except Exception as e: self.log("Could not initialize snapshotter:" + str(e)) self.snapshotter = None self.fullres_snapshotter = None # This method has the following signature: # self.fullres_snapshot_callback(snapshot=None, message=None) # If snapshot is None, then there should be an explanation (string) in msg. self.fullres_snapshot_callback = None #self.snapshotter.start() # This method should be set by caller: self.snapshot_notify = None self.build_pipeline() self.caption = Caption() self.caption.text = "" self.caption.begin = -1 self.caption.end = -1 self.overlay = Caption() self.overlay.data = '' self.overlay.begin = -1 self.overlay.end = -1 self.videofile = None self.status = Player.UndefinedStatus self.current_position_value = 0 self.stream_duration = 0 self.position_update() def log(self, msg): """Display a message. """ logger.warn(msg) def build_pipeline(self): sink = 'xvimagesink' if config.data.player['vout'] == 'x11': sink = 'ximagesink' elif config.data.player['vout'] == 'gl': sink = 'glimagesink' if config.data.os == 'win32': sink = 'd3dvideosink' self.player = Gst.ElementFactory.make("playbin", "player") self.video_sink = Gst.Bin() # TextOverlay does not seem to be present in win32 installer. Do without it. try: self.captioner = Gst.ElementFactory.make('textoverlay', 'captioner') # FIXME: move to config.data self.captioner.props.font_desc = 'Sans 24' #self.caption.props.text="Foobar" except: self.captioner = None self.imageoverlay = None if config.data.player['svg'] and svgelement: try: self.imageoverlay = Gst.ElementFactory.make( svgelement, 'overlay') self.imageoverlay.props.fit_to_frame = True except: logger.error("Gstreamer SVG overlay element is not available", exc_info=True) self.imagesink = Gst.ElementFactory.make(sink, 'sink') try: self.imagesink.set_property('force-aspect-ratio', True) except TypeError: logger.warn("Cannot set force-aspect-ratio on video sink") elements = [] elements.append(Gst.ElementFactory.make('videoconvert', None)) elements.append(Gst.ElementFactory.make('videoscale', None)) if self.imageoverlay is not None: # FIXME: Issue: rsvgoverlay.fit_to_frame expects that the # dimensions of the input buffers match the aspect ratio # of the original video, which is currently not the case. elements.append(Gst.ElementFactory.make('queue', None)) elements.append(self.imageoverlay) if self.captioner is not None: elements.append(self.captioner) csp = Gst.ElementFactory.make('videoconvert', None) elements.extend((csp, self.imagesink)) for el in elements: self.video_sink.add(el) if len(elements) >= 2: for src, dst in zip(elements, elements[1:]): src.link(dst) self.log("using " + sink) # Note: it is crucial to make ghostpad an attribute, so that # it is not garbage-collected at the end of the build_pipeline # method. self._video_ghostpad = Gst.GhostPad.new( 'sink', elements[0].get_static_pad('video_sink') or elements[0].get_static_pad('sink')) # Idem for elements self._video_elements = elements logger.debug("Using video sink pipeline %s", self._video_elements) self.video_sink.add_pad(self._video_ghostpad) self.player.props.video_sink = self.video_sink self.player.props.force_aspect_ratio = True self.audio_sink = Gst.parse_launch( 'scaletempo name=scaletempo ! audioconvert ! audioresample ! autoaudiosink' ) self.audio_sink.add_pad( Gst.GhostPad.new( 'sink', self.audio_sink.get_child_by_name('scaletempo').get_static_pad( 'sink'))) self.player.props.audio_sink = self.audio_sink bus = self.player.get_bus() bus.enable_sync_message_emission() bus.connect('sync-message::element', self.on_sync_message) bus.add_signal_watch() bus.connect('message::error', self.on_bus_message_error) bus.connect('message::warning', self.on_bus_message_warning) def current_status(self): st = self.player.get_state(100)[1] if st == Gst.State.PLAYING: return self.PlayingStatus elif st == Gst.State.PAUSED: return self.PauseStatus else: return self.UndefinedStatus def current_position(self): """Returns the current position in ms. """ try: pos = self.player.query_position(Gst.Format.TIME)[1] except Exception: logger.error("Current position exception", exc_info=True) position = 0 else: position = pos * 1.0 / Gst.MSECOND return position def dvd_uri(self, title=None, chapter=None): # FIXME: find a way to specify chapter/title # resindvd does not allow to specify it in the URI return "dvd://" def set_uri(self, item): self.videofile = item item = path2uri(item) self.player.set_property('uri', item) if self.snapshotter: self.snapshotter.set_uri(item) if self.fullres_snapshotter: self.fullres_snapshotter.set_uri(item) return self.get_video_info() def get_uri(self): return self.player.get_property( 'current-uri') or self.player.get_property('uri') or "" def get_video_info(self): """Return information about the current video. """ uri = self.get_uri() default = { 'uri': uri, 'framerate_denom': 1, 'framerate_num': config.data.preferences['default-fps'], 'width': 640, 'height': 480, 'duration': 0, } if not uri: return default d = GstPbutils.Discoverer() try: info = d.discover_uri(uri) except Exception as e: logger.error("Cannot find video info: %s", e.message) info = None if info is None: # Return default data. logger.warn( "Could not find information about video, using absurd defaults." ) return default if not info.get_video_streams(): # Could be an audio file. default['duration'] = info.get_duration() / Gst.MSECOND return default stream = info.get_video_streams()[0] return { 'uri': uri, 'framerate_denom': stream.get_framerate_denom(), 'framerate_num': stream.get_framerate_num(), 'width': stream.get_width(), 'height': stream.get_height(), 'duration': info.get_duration() / Gst.MSECOND, } def check_uri(self): uri = self.get_uri() if uri and Gst.uri_is_valid(uri): return True else: self.log("Invalid URI " + str(uri)) return False def get_position(self): return self.current_position() def set_position(self, position): if not self.check_uri(): return if self.current_status() == self.UndefinedStatus: self.player.set_state(Gst.State.PAUSED) p = int(position) * Gst.MSECOND event = Gst.Event.new_seek( self.rate, Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE, Gst.SeekType.SET, int(p), Gst.SeekType.NONE, 0) res = None if event: res = self.player.send_event(event) if not res or not event: logger.warn(_("Problem when seeking into media")) def start(self, position=0): if not self.check_uri(): return if position != 0: self.set_position(position) self.player.set_state(Gst.State.PLAYING) def pause(self, position=0): if not self.check_uri(): return if self.status == self.PlayingStatus: self.player.set_state(Gst.State.PAUSED) else: self.player.set_state(Gst.State.PLAYING) def resume(self, position=0): self.pause(position) def stop(self, position=0): if not self.check_uri(): return self.player.set_state(Gst.State.READY) def exit(self): self.player.set_state(Gst.State.NULL) def fullres_snapshot_taken(self, data): if self.fullres_snapshot_callback: s = Snapshot(data) self.fullres_snapshot_callback(snapshot=s) self.fullres_snapshot_callback = None def async_fullres_snapshot(self, position, callback): """Take full-resolution snapshots. This method is not reentrant: as long as there is a call pending, it is not available for another position. """ if self.fullres_snapshot_callback is not None: callback(message=_( "Cannot capture full-resolution snapshot, another capture is ongoing." )) return if self.fullres_snapshotter is None: # Initialise it. self.fullres_snapshotter = Snapshotter(self.fullres_snapshot_taken) self.fullres_snapshotter.set_uri(self.player.get_property('uri')) self.fullres_snapshot_callback = callback if not self.fullres_snapshotter.thread_running: self.fullres_snapshotter.start() self.fullres_snapshotter.enqueue(position) def snapshot_taken(self, data): s = Snapshot(data) logger.debug("-------------------------------- snapshot taken %d %s", s.date, self.snapshot_taken) if self.snapshot_notify: self.snapshot_notify(s) def async_snapshot(self, position, notify=None): t = int(position) if notify is not None and self.snapshot_notify is None: self.snapshot_notify = notify if self.snapshotter: if not self.snapshotter.thread_running: self.snapshotter.start() self.snapshotter.enqueue(t) else: logger.error("snapshotter not present") def display_text(self, message, begin, end): if not self.check_uri(): return if message.startswith('<svg') or (message.startswith('<?xml') and '<svg' in message): if self.imageoverlay is None: self.log("Cannot overlay SVG") return True self.overlay.begin = begin self.overlay.end = end self.overlay.data = message self.imageoverlay.props.data = message return True if not self.captioner: self.log("Cannot caption " + str(message)) return self.caption.begin = begin self.caption.end = end self.caption.text = message self.captioner.props.text = message def get_stream_information(self): s = StreamInformation() s.uri = self.get_uri() try: dur = self.player.query_duration(Gst.Format.TIME)[1] except: duration = 0 else: duration = dur * 1.0 / Gst.MSECOND s.length = duration s.position = self.current_position() s.status = self.current_status() return s def sound_get_volume(self): """Get the current volume. The result is a long value in [0, 100] """ v = self.player.get_property('volume') * 100 / 4 return int(v) def sound_set_volume(self, v): if v > 100: v = 100 elif v < 0: v = 0 v = v * 4.0 / 100 self.player.set_property('volume', v) def update_status(self, status=None, position=None): """Update the player status. Defined status: - C{start} - C{pause} - C{resume} - C{stop} - C{seek} - C{seek_relative} If no status is given, it only updates the value of self.status If C{position} is None, it will be considered as the current position. @param status: the new status @type status: string @param position: the position @type position: int """ logger.debug("update_status %s %s ", status, str(position)) if position is None: position = 0 if status == "start" or status == "seek" or status == "seek_relative": self.position_update() if status == "seek_relative": position = self.current_position() + position if status == "start": if self.status == self.PauseStatus: self.resume(position) elif self.status != self.PlayingStatus: self.start(position) time.sleep(0.005) self.set_position(position) else: if status == "pause": self.position_update() if self.status == self.PauseStatus: self.resume(position) else: self.pause(position) elif status == "resume": self.resume(position) elif status == "stop": self.stop(position) elif status == "" or status == None: pass else: self.log( "******* Error : unknown status %s in gstreamer player" % status) self.position_update() def is_playing(self): """Is the player in Playing or Paused status? """ s = self.get_stream_information() return s.status == self.PlayingStatus or s.status == self.PauseStatus def check_player(self): self.log("check player") return True def position_update(self): s = self.get_stream_information() if s.position == 0: # Try again once. timestamp sometimes goes through 0 when # modifying the player position. s = self.get_stream_information() self.status = s.status self.stream_duration = s.length self.current_position_value = int(s.position) if self.caption.text and (s.position < self.caption.begin or s.position > self.caption.end): self.display_text('', -1, -1) if self.overlay.data and (s.position < self.overlay.begin or s.position > self.overlay.end): self.imageoverlay.props.data = None self.overlay.begin = -1 self.overlay.end = -1 self.overlay.data = None elif not self.overlay.data and self.imageoverlay is not None and self.is_fullscreen( ) and config.data.player.get('fullscreen-timestamp', False): t = time.time() # Update timestamp every half second if t - self.last_timestamp_update > .5 and abs( s.position - self.last_timestamp) > 10: self.imageoverlay.props.data = '''<svg:svg width="640pt" height="480pt" preserveAspectRatio="xMinYMin meet" version="1" viewBox="0 0 640 480" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svg="http://www.w3.org/2000/svg"> <text fill="white" stroke="white" style="stroke-width:1; font-family: sans-serif; font-size: 22" x="5" y="475">%s</text> </svg:svg>''' % format_time(s.position) self.last_timestamp = s.position self.last_timestamp_update = t def reparent(self, xid): logger.debug("Reparent %s", xid) # See https://bugzilla.gnome.org/show_bug.cgi?id=599885 if xid: self.log("Reparent " + hex(xid)) Gdk.Display().get_default().sync() self.imagesink.set_window_handle(xid) self.imagesink.set_property('force-aspect-ratio', True) self.imagesink.expose() def set_visual(self, xid): if not xid: return True self.xid = xid self.reparent(xid) return True def set_widget(self, widget): handle = None if config.data.os == "win32": # From # http://stackoverflow.com/questions/25823541/get-the-window-handle-in-pygi if not widget.ensure_native(): logger.error( "Cannot embed video player - it requires a native window") return ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object] drawingarea_gpointer = ctypes.pythonapi.PyCapsule_GetPointer( widget.__gpointer__, None) gdkdll = ctypes.CDLL("libgdk-3-0.dll") handle = gdkdll.gdk_win32_window_get_handle(drawingarea_gpointer) else: handle = widget.get_id() self.set_visual(handle) def restart_player(self): # FIXME: properly destroy the previous player self.player.set_state(Gst.State.READY) # Rebuild the pipeline self.build_pipeline() if self.videofile is not None: self.set_uri(self.videofile) self.position_update() return True def on_sync_message(self, bus, message): s = message.get_structure() logger.debug("sync message %s", s) if s is None: return True if s.get_name() == 'prepare-window-handle': self.reparent(self.xid) return True def on_bus_message_error(self, bus, message): s = message.get_structure() if s is None: return True title, message = message.parse_error() logger.error("%s: %s", title, message) return True def on_bus_message_warning(self, bus, message): s = message.get_structure() if s is None: return True title, message = message.parse_warning() logger.warn("%s: %s", title, message) return True def sound_mute(self): if self.mute_volume is None: self.mute_volume = self.sound_get_volume() self.sound_set_volume(0) return def sound_unmute(self): if self.mute_volume is not None: self.sound_set_volume(self.mute_volume) self.mute_volume = None return def sound_is_muted(self): return (self.mute_volume is not None) def set_rate(self, rate=1.0): if not self.check_uri(): return try: p = self.player.query_position(Gst.Format.TIME)[1] except Gst.QueryError: self.log("Error in set_rate (query position)") return event = Gst.Event.new_seek(self.rate, Gst.Format.TIME, Gst.SeekFlags.FLUSH, Gst.SeekType.SET, int(p), Gst.SeekType.NONE, 0) if event: res = self.player.send_event(event) if not res: self.log("Could not set rate") else: self.rate = rate else: self.log("Cannot build set_rate event") def get_rate(self): return self.rate def is_fullscreen(self): return self.fullscreen_window and self.fullscreen_window.is_active() def fullscreen(self, connect=None): def keypress(widget, event): if event.keyval == Gdk.KEY_Escape: self.unfullscreen() return True return False def buttonpress(widget, event): if event.button == 1 and event.type == Gdk.EventType._2BUTTON_PRESS: self.unfullscreen() return True return False if self.fullscreen_window is None: self.fullscreen_window = Gtk.Window() self.fullscreen_window.set_name("fullscreen_player") self.fullscreen_window.connect('key-press-event', keypress) self.fullscreen_window.connect('button-press-event', buttonpress) self.fullscreen_window.connect('destroy', self.unfullscreen) self.fullscreen_drawable = get_drawable() self.fullscreen_drawable.add_events( Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK | Gdk.EventMask.SCROLL_MASK) self.fullscreen_window.add(self.fullscreen_drawable) self.fullscreen_window.show_all() if connect is not None: connect(self.fullscreen_drawable) # Use black background css_provider = Gtk.CssProvider() css_provider.load_from_data( b"#fullscreen_player { color:#fff; background-color: #000; }") context = Gtk.StyleContext() context.add_provider_for_screen( Gdk.Screen.get_default(), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) self.fullscreen_window.show_all() self.fullscreen_window.get_window().fullscreen() self.fullscreen_window.grab_focus() # Do not use set_visual/set_widget so that the player does not # update self.xid and keep it as a reference self.reparent(self.fullscreen_drawable.get_id()) def unfullscreen(self, *p): self.reparent(self.xid) if not self.overlay.data and self.imageoverlay: # Reset imageoverlay data in any case self.imageoverlay.props.data = None if self.fullscreen_window.get_window(): self.fullscreen_window.hide() else: # It has been destroyed self.fullscreen_window = None self.fullscreen_drawable = None return True # relpath, dump_bin and dump_element implementation based on Daniel Lenski <*****@*****.**> # posted on gst-dev mailing list on 20070913 def relpath(self, p1, p2): sep = os.path.sep # get common prefix (up to a slash) common = os.path.commonprefix((p1, p2)) common = common[:common.rfind(sep)] # remove common prefix p1 = p1[len(common) + 1:] p2 = p2[len(common) + 1:] # number of seps in p1 is # of ..'s needed return "../" * p1.count(sep) + p2 def dump_bin(self, bin, depth=0, recurse=-1, showcaps=True): return [ l for e in reversed(list(bin.iterate_elements())) for l in self.dump_element(e, depth, recurse - 1) ] def dump_element(self, e, depth=0, recurse=-1, showcaps=True): ret = [] indentstr = depth * 8 * ' ' # print element path and factory path = e.get_path_string() + (isinstance(e, Gst.Bin) and '/' or '') factory = e.get_factory() if factory is not None: ret.append('%s%s (%s)' % (indentstr, path, factory.get_name())) else: ret.append('%s%s (No factory)' % (indentstr, path)) # print info about each pad for p in e.pads: name = p.get_name() # negotiated capabilities caps = p.get_current_caps() if caps: capsname = caps.get_structure(0).get_name() elif showcaps: capsname = '; '.join(s.to_string() for s in set(p.get_current_caps() or [])) else: capsname = None # flags flags = [] if not p.is_active(): flags.append('INACTIVE') if p.is_blocked(): flags.append('BLOCKED') # direction direc = (p.get_direction() is Gst.PadDirection.SRC) and "=>" or "<=" # peer peer = p.get_peer() if peer: peerpath = self.relpath(path, peer.get_path_string()) else: peerpath = None # ghost target if isinstance(p, Gst.GhostPad): target = p.get_target() if target: ghostpath = target.get_path_string() else: ghostpath = None else: ghostpath = None line = [indentstr, " "] if flags: line.append(','.join(flags)) line.append(".%s" % name) if capsname: line.append('[%s]' % capsname) if ghostpath: line.append("ghosts %s" % self.relpath(path, ghostpath)) line.append("%s %s" % (direc, peerpath)) #if peerpath and peerpath.find('proxy')!=-1: print peer ret.append(''.join(line)) if recurse and isinstance(e, Gst.Bin): ret.extend(self.dump_bin(e, depth + 1, recurse)) return ret def str_element(self, element): return "\n".join(self.dump_element(element))
class Player: player_id='gstreamer' player_capabilities=[ 'seek', 'pause', 'caption', 'frame-by-frame', 'async-snapshot', 'set-rate', 'svg' ] # Status PlayingStatus=0 PauseStatus=1 InitStatus=2 EndStatus=3 UndefinedStatus=4 def __init__(self): self.xid = None self.mute_volume=None self.rate = 1.0 # fullscreen Gtk.Window self.fullscreen_window = None self.fullscreen_drawable = None # Fullscreen timestamp display - cache data self.last_timestamp = 0 self.last_timestamp_update = 0 try: self.snapshotter = Snapshotter(self.snapshot_taken, width=config.data.player['snapshot-width']) except Exception as e: self.log("Could not initialize snapshotter:" + str(e)) self.snapshotter = None self.fullres_snapshotter = None # This method has the following signature: # self.fullres_snapshot_callback(snapshot=None, message=None) # If snapshot is None, then there should be an explanation (string) in msg. self.fullres_snapshot_callback = None #self.snapshotter.start() # This method should be set by caller: self.snapshot_notify=None self.build_pipeline() self.caption=Caption() self.caption.text="" self.caption.begin=-1 self.caption.end=-1 self.overlay=Caption() self.overlay.data='' self.overlay.begin=-1 self.overlay.end=-1 self.videofile = None self.status = Player.UndefinedStatus self.current_position_value = 0 self.stream_duration = 0 self.position_update() def log (self, msg): """Display a message. """ logger.warn(msg) def build_pipeline(self): sink='autovideosink' if config.data.player['vout'] == 'x11': sink='ximagesink' elif config.data.player['vout'] == 'xvideo': sink='xvimagesink' elif config.data.player['vout'] == 'gtk': sink='gtksink' elif config.data.player['vout'] == 'd3d': sink='d3dvideosink' elif config.data.player['vout'] == 'gl': sink='glimagesinkelement' self.player = Gst.ElementFactory.make("playbin", "player") self.video_sink = Gst.Bin() # TextOverlay does not seem to be present in win32 installer. Do without it. try: self.captioner=Gst.ElementFactory.make('textoverlay', 'captioner') # FIXME: move to config.data self.captioner.props.font_desc='Sans 24' except: self.captioner=None self.imageoverlay=None if config.data.player['svg'] and svgelement: try: self.imageoverlay=Gst.ElementFactory.make(svgelement, 'overlay') self.imageoverlay.props.fit_to_frame = True except: logger.error("Gstreamer SVG overlay element is not available", exc_info=True) self.imagesink = Gst.ElementFactory.make(sink, 'sink') try: self.imagesink.set_property('force-aspect-ratio', True) except TypeError: logger.warn("Cannot set force-aspect-ratio on video sink") self.real_imagesink = self.imagesink elements=[] elements.append(Gst.ElementFactory.make('videoconvert', None)) elements.append(Gst.ElementFactory.make('videoscale', None)) if self.imageoverlay is not None: # FIXME: Issue: rsvgoverlay.fit_to_frame expects that the # dimensions of the input buffers match the aspect ratio # of the original video, which is currently not the case. elements.append(Gst.ElementFactory.make('queue', None)) elements.append(self.imageoverlay) if self.captioner is not None: elements.append(self.captioner) if sink == 'glimagesinkelement': upload = Gst.ElementFactory.make('glupload', None) csp=Gst.ElementFactory.make('glcolorconvert', None) elements.extend( (upload, csp, self.imagesink) ) else: csp=Gst.ElementFactory.make('videoconvert', None) elements.extend( (csp, self.imagesink) ) for el in elements: self.video_sink.add(el) if len(elements) >= 2: for src, dst in zip(elements, elements[1:]): src.link(dst) self.log("using " + sink) # Note: it is crucial to make ghostpad an attribute, so that # it is not garbage-collected at the end of the build_pipeline # method. self._video_ghostpad = Gst.GhostPad.new('sink', elements[0].get_static_pad('video_sink') or elements[0].get_static_pad('sink')) # Idem for elements self._video_elements = elements logger.debug("Using video sink pipeline %s", self._video_elements) self.video_sink.add_pad(self._video_ghostpad) self.player.props.video_sink=self.video_sink self.player.props.force_aspect_ratio = True self.audio_sink = Gst.parse_launch('scaletempo name=scaletempo ! audioconvert ! audioresample ! autoaudiosink') self.audio_sink.add_pad(Gst.GhostPad.new('sink', self.audio_sink.get_child_by_name('scaletempo').get_static_pad('sink'))) self.player.props.audio_sink=self.audio_sink bus = self.player.get_bus() bus.enable_sync_message_emission() bus.connect('sync-message::element', self.on_sync_message) bus.add_signal_watch() bus.connect('message::error', self.on_bus_message_error) bus.connect('message::warning', self.on_bus_message_warning) def current_status(self): st = self.player.get_state(100)[1] if st == Gst.State.PLAYING: return self.PlayingStatus elif st == Gst.State.PAUSED: return self.PauseStatus else: return self.UndefinedStatus def current_position(self): """Returns the current position in ms. """ try: pos = self.player.query_position(Gst.Format.TIME)[1] except Exception: logger.error("Current position exception", exc_info=True) position = 0 else: position = pos * 1.0 / Gst.MSECOND return position def dvd_uri(self, title=None, chapter=None): # FIXME: find a way to specify chapter/title # resindvd does not allow to specify it in the URI return "dvd://" def set_uri(self, item): self.videofile = item item = path2uri(item) self.player.set_property('uri', item) if self.snapshotter: self.snapshotter.set_uri(item) if self.fullres_snapshotter: self.fullres_snapshotter.set_uri(item) return self.get_video_info() def get_uri(self): return self.player.get_property('current-uri') or self.player.get_property('uri') or "" def get_video_info(self): """Return information about the current video. """ uri = self.get_uri() default = { 'uri': uri, 'framerate_denom': 1, 'framerate_num': config.data.preferences['default-fps'], 'width': 640, 'height': 480, 'duration': 0, } if not uri: return default d = GstPbutils.Discoverer() try: info = d.discover_uri(uri) except Exception as e: logger.error("Cannot find video info: %s", e.message) info = None if info is None: # Return default data. logger.warn("Could not find information about video, using absurd defaults.") return default if not info.get_video_streams(): # Could be an audio file. default['duration'] = info.get_duration() / Gst.MSECOND return default stream = info.get_video_streams()[0] return { 'uri': uri, 'framerate_denom': stream.get_framerate_denom(), 'framerate_num': stream.get_framerate_num(), 'width': stream.get_width(), 'height': stream.get_height(), 'duration': info.get_duration() / Gst.MSECOND, } def check_uri(self): uri = self.get_uri() if uri and Gst.uri_is_valid(uri): return True else: self.log("Invalid URI " + str(uri)) return False def get_position(self): return self.current_position() def set_position(self, position): if not self.check_uri(): return if self.current_status() == self.UndefinedStatus: self.player.set_state(Gst.State.PAUSED) p = int(position) * Gst.MSECOND event = Gst.Event.new_seek(self.rate, Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE, Gst.SeekType.SET, int(p), Gst.SeekType.NONE, 0) res = None if event: res = self.player.send_event(event) if not res or not event: logger.warn(_("Problem when seeking into media")) def start(self, position=0): if not self.check_uri(): return if position != 0: self.set_position(position) self.player.set_state(Gst.State.PLAYING) def pause(self, position=0): if not self.check_uri(): return if self.status == self.PlayingStatus: self.player.set_state(Gst.State.PAUSED) else: self.player.set_state(Gst.State.PLAYING) def resume(self, position=0): self.pause(position) def stop(self, position=0): if not self.check_uri(): return self.player.set_state(Gst.State.READY) def exit(self): self.player.set_state(Gst.State.NULL) def fullres_snapshot_taken(self, data): if self.fullres_snapshot_callback: s = Snapshot(data) self.fullres_snapshot_callback(snapshot=s) self.fullres_snapshot_callback = None def async_fullres_snapshot(self, position, callback): """Take full-resolution snapshots. This method is not reentrant: as long as there is a call pending, it is not available for another position. """ if self.fullres_snapshot_callback is not None: callback(message=_("Cannot capture full-resolution snapshot, another capture is ongoing.")) return if self.fullres_snapshotter is None: # Initialise it. self.fullres_snapshotter = Snapshotter(self.fullres_snapshot_taken) self.fullres_snapshotter.set_uri(self.player.get_property('uri')) self.fullres_snapshot_callback = callback if not self.fullres_snapshotter.thread_running: self.fullres_snapshotter.start() self.fullres_snapshotter.enqueue(position) def snapshot_taken(self, data): s = Snapshot(data) logger.debug("-------------------------------- snapshot taken %d %s", s.date, self.snapshot_taken) if self.snapshot_notify: self.snapshot_notify(s) def async_snapshot(self, position, notify=None): t = int(position) if notify is not None and self.snapshot_notify is None: self.snapshot_notify = notify if self.snapshotter: if not self.snapshotter.thread_running: self.snapshotter.start() self.snapshotter.enqueue(t) else: logger.error("snapshotter not present") def display_text (self, message, begin, end): if not self.check_uri(): return if message.startswith('<svg') or (message.startswith('<?xml') and '<svg' in message): if self.imageoverlay is None: self.log("Cannot overlay SVG") return True self.overlay.begin=begin self.overlay.end=end self.overlay.data=message self.imageoverlay.props.data=message return True if not self.captioner: self.log("Cannot caption " + str(message)) return self.caption.begin=begin self.caption.end=end self.caption.text=message self.captioner.props.text=message def get_stream_information(self): s = StreamInformation() s.uri = self.get_uri() try: dur = self.player.query_duration(Gst.Format.TIME)[1] except: duration = 0 else: duration = dur * 1.0 / Gst.MSECOND s.length=duration s.position=self.current_position() s.status=self.current_status() return s def sound_get_volume(self): """Get the current volume. The result is a long value in [0, 100] """ v = self.player.get_property('volume') * 100 / 4 return int(v) def sound_set_volume(self, v): if v > 100: v = 100 elif v < 0: v = 0 v = v * 4.0 / 100 self.player.set_property('volume', v) def update_status (self, status=None, position=None): """Update the player status. Defined status: - C{start} - C{pause} - C{resume} - C{stop} - C{seek} - C{seek_relative} If no status is given, it only updates the value of self.status If C{position} is None, it will be considered as the current position. @param status: the new status @type status: string @param position: the position @type position: int """ logger.debug("update_status %s %s ", status, str(position)) if position is None: position = 0 if status == "start" or status == "seek" or status == "seek_relative": self.position_update() if status == "seek_relative": position = self.current_position() + position if status == "start": if self.status == self.PauseStatus: self.resume(position) elif self.status != self.PlayingStatus: self.start(position) time.sleep(0.005) self.set_position(position) else: if status == "pause": self.position_update() if self.status == self.PauseStatus: self.resume (position) else: self.pause(position) elif status == "resume": self.resume (position) elif status == "stop": self.stop (position) elif status == "" or status == None: pass else: self.log("******* Error : unknown status %s in gstreamer player" % status) self.position_update () def is_playing(self): """Is the player in Playing or Paused status? """ s = self.get_stream_information () return s.status == self.PlayingStatus or s.status == self.PauseStatus def check_player(self): self.log("check player") return True def position_update(self): s = self.get_stream_information () if s.position == 0: # Try again once. timestamp sometimes goes through 0 when # modifying the player position. s = self.get_stream_information() self.status = s.status self.stream_duration = s.length self.current_position_value = int(s.position) if self.caption.text and (s.position < self.caption.begin or s.position > self.caption.end): self.display_text('', -1, -1) if self.overlay.data and (s.position < self.overlay.begin or s.position > self.overlay.end): self.imageoverlay.props.data=None self.overlay.begin=-1 self.overlay.end=-1 self.overlay.data=None elif not self.overlay.data and self.imageoverlay is not None and self.is_fullscreen() and config.data.player.get('fullscreen-timestamp', False): t = time.time() # Update timestamp every half second if t - self.last_timestamp_update > .5 and abs(s.position - self.last_timestamp) > 10: self.imageoverlay.props.data = '''<svg:svg width="640pt" height="480pt" preserveAspectRatio="xMinYMin meet" version="1" viewBox="0 0 640 480" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svg="http://www.w3.org/2000/svg"> <text fill="white" stroke="white" style="stroke-width:1; font-family: sans-serif; font-size: 22" x="5" y="475">%s</text> </svg:svg>''' % format_time(s.position) self.last_timestamp = s.position self.last_timestamp_update = t def reparent(self, xid, sink=None): if sink is None: sink = self.real_imagesink else: self.real_imagesink = sink logger.debug("Reparent %s", xid) # See https://bugzilla.gnome.org/show_bug.cgi?id=599885 if xid: self.log("Reparent " + hex(xid)) Gdk.Display().get_default().sync() try: sink.set_window_handle(xid) sink.set_property('force-aspect-ratio', True) sink.expose() except AttributeError: logger.warn("cannot set video output widget") def set_visual(self, xid): if not xid: return True self.xid = xid self.reparent(xid) return True def set_widget(self, widget, container): handle = None if config.data.player['vout'] == 'gtk': # Special case: we use a gtk sink, so we get a Gtk widget # and not the XOverlay API try: container.pack_start(self.imagesink.props.widget, True, True, 0) self.imagesink.props.widget.show() widget.hide() except: logger.exception("Embedding error") return if config.data.os == "win32": # From # http://stackoverflow.com/questions/25823541/get-the-window-handle-in-pygi if not widget.ensure_native(): logger.error("Cannot embed video player - it requires a native window") return ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object] drawingarea_gpointer = ctypes.pythonapi.PyCapsule_GetPointer(widget.__gpointer__, None) gdkdll = ctypes.CDLL("libgdk-3-0.dll") handle = gdkdll.gdk_win32_window_get_handle(drawingarea_gpointer) else: handle = widget.get_id() widget.show() self.set_visual(handle) def restart_player(self): # FIXME: properly destroy the previous player self.player.set_state(Gst.State.READY) # Rebuild the pipeline self.build_pipeline() if self.videofile is not None: self.set_uri(self.videofile) self.position_update() return True def on_sync_message(self, bus, message): s = message.get_structure() logger.debug("sync message %s", s) if s is None: return True if GstVideo.is_video_overlay_prepare_window_handle_message(message): imagesink = message.src imagesink.set_property("force-aspect-ratio", True) self.reparent(self.xid, imagesink) return True def on_bus_message_error(self, bus, message): s = message.get_structure() if s is None: return True title, message = message.parse_error() logger.error("%s: %s", title, message) return True def on_bus_message_warning(self, bus, message): s = message.get_structure() if s is None: return True title, message = message.parse_warning() logger.warn("%s: %s", title, message) return True def sound_mute(self): if self.mute_volume is None: self.mute_volume=self.sound_get_volume() self.sound_set_volume(0) return def sound_unmute(self): if self.mute_volume is not None: self.sound_set_volume(self.mute_volume) self.mute_volume=None return def sound_is_muted(self): return (self.mute_volume is not None) def set_rate(self, rate=1.0): if not self.check_uri(): return try: p = self.player.query_position(Gst.Format.TIME)[1] except Gst.QueryError: self.log("Error in set_rate (query position)") return event = Gst.Event.new_seek(self.rate, Gst.Format.TIME, Gst.SeekFlags.FLUSH, Gst.SeekType.SET, int(p), Gst.SeekType.NONE, 0) if event: res = self.player.send_event(event) if not res: self.log("Could not set rate") else: self.rate = rate else: self.log("Cannot build set_rate event") def get_rate(self): return self.rate def is_fullscreen(self): return self.fullscreen_window and self.fullscreen_window.is_active() def fullscreen(self, connect=None): def keypress(widget, event): if event.keyval == Gdk.KEY_Escape: self.unfullscreen() return True return False def buttonpress(widget, event): if event.button == 1 and event.type == Gdk.EventType._2BUTTON_PRESS: self.unfullscreen() return True return False if self.fullscreen_window is None: self.fullscreen_window = Gtk.Window() self.fullscreen_window.set_name("fullscreen_player") self.fullscreen_window.connect('key-press-event', keypress) self.fullscreen_window.connect('button-press-event', buttonpress) self.fullscreen_window.connect('destroy', self.unfullscreen) self.fullscreen_drawable = get_drawable() self.fullscreen_drawable.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK | Gdk.EventMask.SCROLL_MASK) self.fullscreen_window.add(self.fullscreen_drawable) self.fullscreen_window.show_all() if connect is not None: connect(self.fullscreen_drawable) # Use black background css_provider = Gtk.CssProvider() css_provider.load_from_data(b"#fullscreen_player { color:#fff; background-color: #000; }") context = Gtk.StyleContext() context.add_provider_for_screen(Gdk.Screen.get_default(), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) self.fullscreen_window.show_all() self.fullscreen_window.get_window().fullscreen() self.fullscreen_window.grab_focus() # Do not use set_visual/set_widget so that the player does not # update self.xid and keep it as a reference self.reparent(self.fullscreen_drawable.get_id()) def unfullscreen(self, *p): self.reparent(self.xid) if not self.overlay.data and self.imageoverlay: # Reset imageoverlay data in any case self.imageoverlay.props.data = None if self.fullscreen_window.get_window(): self.fullscreen_window.hide() else: # It has been destroyed self.fullscreen_window = None self.fullscreen_drawable = None return True # relpath, dump_bin and dump_element implementation based on Daniel Lenski <*****@*****.**> # posted on gst-dev mailing list on 20070913 def relpath(self, p1, p2): sep = os.path.sep # get common prefix (up to a slash) common = os.path.commonprefix((p1, p2)) common = common[:common.rfind(sep)] # remove common prefix p1 = p1[len(common)+1:] p2 = p2[len(common)+1:] # number of seps in p1 is # of ..'s needed return "../" * p1.count(sep) + p2 def dump_bin(self, bin, depth=0, recurse=-1, showcaps=True): return [ l for e in reversed(list(bin.iterate_elements())) for l in self.dump_element(e, depth, recurse - 1) ] def dump_element(self, e, depth=0, recurse=-1, showcaps=True): ret=[] indentstr = depth * 8 * ' ' # print element path and factory path = e.get_path_string() + (isinstance(e, Gst.Bin) and '/' or '') factory = e.get_factory() if factory is not None: ret.append( '%s%s (%s)' % (indentstr, path, factory.get_name()) ) else: ret.append( '%s%s (No factory)' % (indentstr, path) ) # print info about each pad for p in e.pads: name = p.get_name() # negotiated capabilities caps = p.get_current_caps() if caps: capsname = caps.get_structure(0).get_name() elif showcaps: capsname = '; '.join(s.to_string() for s in set(p.get_current_caps() or [])) else: capsname = None # flags flags = [] if not p.is_active(): flags.append('INACTIVE') if p.is_blocked(): flags.append('BLOCKED') # direction direc = (p.get_direction() is Gst.PadDirection.SRC) and "=>" or "<=" # peer peer = p.get_peer() if peer: peerpath = self.relpath(path, peer.get_path_string()) else: peerpath = None # ghost target if isinstance(p, Gst.GhostPad): target = p.get_target() if target: ghostpath = target.get_path_string() else: ghostpath = None else: ghostpath = None line=[ indentstr, " " ] if flags: line.append( ','.join(flags) ) line.append(".%s" % name) if capsname: line.append( '[%s]' % capsname ) if ghostpath: line.append( "ghosts %s" % self.relpath(path, ghostpath) ) line.append( "%s %s" % (direc, peerpath) ) #if peerpath and peerpath.find('proxy')!=-1: print peer ret.append( ''.join(line) ) if recurse and isinstance(e, Gst.Bin): ret.extend( self.dump_bin(e, depth+1, recurse) ) return ret def str_element(self, element): return "\n".join(self.dump_element(element))
class Player: player_id='gstreamer' player_capabilities=[ 'seek', 'pause', 'caption', 'frame-by-frame', 'async-snapshot' ] # Class attributes AbsolutePosition=0 RelativePosition=1 ModuloPosition=2 ByteCount=0 SampleCount=1 MediaTime=2 # Status PlayingStatus=0 PauseStatus=1 InitStatus=2 EndStatus=3 UndefinedStatus=4 PositionKeyNotSupported=Exception("Position key not supported") PositionOriginNotSupported=Exception("Position origin not supported") InvalidPosition=Exception("Invalid position") PlaylistException=Exception("Playlist error") InternalException=Exception("Internal player error") def __init__(self): self.xid = None self.mute_volume=None # fullscreen gtk.Window self.fullscreen_window=None self.snapshotter=Snapshotter(self.snapshot_taken, width=config.data.player['snapshot-width']) #self.snapshotter.start() # This method should be set by caller: self.snapshot_notify=None self.build_pipeline() self.caption=Caption() self.caption.text="" self.caption.begin=-1 self.caption.end=-1 self.overlay=Caption() self.overlay.data='' self.overlay.begin=-1 self.overlay.end=-1 self.videofile=None self.status=Player.UndefinedStatus self.current_position_value = 0 self.stream_duration = 0 self.relative_position=self.create_position(0, origin=self.RelativePosition) self.position_update() def build_pipeline(self): sink='xvimagesink' if config.data.player['vout'] == 'x11': sink='ximagesink' if config.data.os == 'win32': sink='directdrawsink' self.player = gst.element_factory_make("playbin", "player") self.video_sink = gst.Bin() # TextOverlay does not seem to be present in win32 installer. Do without it. try: self.captioner=gst.element_factory_make('textoverlay', 'captioner') # FIXME: move to config.data self.captioner.props.font_desc='Sans 24' #self.caption.props.text="Foobar" except: self.captioner=None self.imageoverlay=None if config.data.player['svg']: try: self.imageoverlay=gst.element_factory_make('svgoverlay', 'overlay') except: pass self.imagesink = gst.element_factory_make(sink, 'sink') elements=[] if self.captioner is not None: elements.append(self.captioner) if self.imageoverlay is not None: elements.append(gst.element_factory_make('queue')) elements.append(gst.element_factory_make('ffmpegcolorspace')) elements.append(self.imageoverlay) elements.append(gst.element_factory_make('ffmpegcolorspace')) if sink == 'xvimagesink': # Imagesink accepts both rgb/yuv and is able to do scaling itself. elements.append( self.imagesink ) else: csp=gst.element_factory_make('ffmpegcolorspace') # The scaling did not work before 2008-10-11, cf # http://bugzilla.gnome.org/show_bug.cgi?id=339201 scale=gst.element_factory_make('videoscale') self.videoscale=scale elements.extend( (csp, scale, self.imagesink) ) self.video_sink.add(*elements) if len(elements) >= 2: gst.element_link_many(*elements) print "gstreamer: using", sink print "adding ghostpad for", elements[0] self.video_sink.add_pad(gst.GhostPad('sink', elements[0].get_pad('video_sink') or elements[0].get_pad('sink'))) self.player.props.video_sink=self.video_sink bus = self.player.get_bus() bus.enable_sync_message_emission() bus.connect('sync-message::element', self.on_sync_message) def position2value(self, p): """Returns a position in ms. """ if isinstance(p, Position): v=p.value if p.key != self.MediaTime: print "gstreamer: unsupported key ", p.key return 0 if p.origin != self.AbsolutePosition: v += self.current_position() else: v=p return long(v) def current_status(self): st=self.player.get_state()[1] if st == gst.STATE_PLAYING: return self.PlayingStatus elif st == gst.STATE_PAUSED: return self.PauseStatus else: return self.UndefinedStatus def current_position(self): """Returns the current position in ms. """ try: pos, format = self.player.query_position(gst.FORMAT_TIME) except: position = 0 else: position = pos * 1.0 / gst.MSECOND return position def dvd_uri(self, title=None, chapter=None): # FIXME: find the syntax to specify chapter return "dvd://%s" % str(title) def check_uri(self): uri=self.player.get_property('uri') if uri and gst.uri_is_valid(uri): return True else: print "Invalid URI", str(uri) return False def log(self, *p): print "gstreamer player: %s" % p def get_media_position(self, origin, key): return self.current_position() def set_media_position(self, position): if not self.check_uri(): return if self.current_status() == self.UndefinedStatus: self.player.set_state(gst.STATE_PAUSED) p = long(self.position2value(position) * gst.MSECOND) event = gst.event_new_seek(1.0, gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_ACCURATE, gst.SEEK_TYPE_SET, p, gst.SEEK_TYPE_NONE, 0) res = self.player.send_event(event) if not res: raise InternalException def start(self, position=0): if not self.check_uri(): return if position != 0: self.set_media_position(position) self.player.set_state(gst.STATE_PLAYING) def pause(self, position=0): if not self.check_uri(): return if self.status == self.PlayingStatus: self.player.set_state(gst.STATE_PAUSED) else: self.player.set_state(gst.STATE_PLAYING) def resume(self, position=0): self.pause(position) def stop(self, position=0): if not self.check_uri(): return self.player.set_state(gst.STATE_READY) def exit(self): self.player.set_state(gst.STATE_NULL) def playlist_add_item(self, item): self.videofile=item if os.path.exists(item): if config.data.os == 'win32': item="file:///" + os.path.abspath(item) else: item="file://" + os.path.abspath(item) self.player.set_property('uri', item) if self.snapshotter: self.snapshotter.set_uri(item) def playlist_clear(self): self.videofile=None self.player.set_property('uri', '') def playlist_get_list(self): if self.videofile: return [ self.videofile ] else: return [ ] def snapshot_taken(self, buffer): if self.snapshot_notify: s=Snapshot( { 'data': buffer.data, 'type': 'PNG', 'date': buffer.timestamp / gst.MSECOND, # Hardcoded size values. They are not used # by the application, since they are # encoded in the PNG file anyway. 'width': 160, 'height': 100 } ) self.snapshot_notify(s) def async_snapshot(self, position): t=long(self.position2value(position)) if not self.snapshotter.thread_running: self.snapshotter.start() if self.snapshotter: self.snapshotter.enqueue(t) def snapshot(self, position): if not self.check_uri(): return None # Return None in all cases. return None def all_snapshots(self): self.log("all_snapshots %s") return [ None ] def display_text (self, message, begin, end): if not self.check_uri(): return if message.startswith('<svg') or (message.startswith('<?xml') and '<svg' in message): if self.imageoverlay is None: print "Cannot overlay SVG" return True self.overlay.begin=self.position2value(begin) self.overlay.end=self.position2value(end) self.overlay.data=message self.imageoverlay.props.data=message return True if not self.captioner: print "Cannot caption ", message.encode('utf8') return self.caption.begin=self.position2value(begin) self.caption.end=self.position2value(end) self.caption.text=message self.captioner.props.text=message def get_stream_information(self): s=StreamInformation() if self.videofile: s.url='' else: s.url=self.videofile try: dur, format = self.player.query_duration(gst.FORMAT_TIME) except: duration = 0 else: duration = dur * 1.0 / gst.MSECOND s.length=duration s.position=self.current_position() s.status=self.current_status() return s def sound_get_volume(self): """Get the current volume. The result is a long value in [0, 100] """ v = self.player.get_property('volume') * 100 / 4 return long(v) def sound_set_volume(self, v): if v > 100: v = 100 elif v < 0: v = 0 v = v * 4.0 / 100 self.player.set_property('volume', v) # Helper methods def create_position (self, value=0, key=None, origin=None): """Create a Position. """ if key is None: key=self.MediaTime if origin is None: origin=self.AbsolutePosition p=Position() p.value = value p.origin = origin p.key = key return p def update_status (self, status=None, position=None): """Update the player status. Defined status: - C{start} - C{pause} - C{resume} - C{stop} - C{set} If no status is given, it only updates the value of self.status If C{position} is None, it will be considered as zero for the "start" action, and as the current relative position for other actions. @param status: the new status @type status: string @param position: the position @type position: long """ #print "gst - update_status ", status, str(position) if position is None: position=0 else: position=self.position2value(position) if status == "start" or status == "set": self.position_update() if status == "start": if self.status == self.PauseStatus: self.resume (position) elif self.status != self.PlayingStatus: self.start(position) time.sleep(0.005) # print "Before s_m_p", position self.set_media_position(position) # print "After s_m_p" else: if status == "pause": self.position_update() if self.status == self.PauseStatus: self.resume (position) else: self.pause(position) elif status == "resume": self.resume (position) elif status == "stop": self.stop (position) elif status == "" or status == None: pass else: print "******* Error : unknown status %s in gstreamer player" % status self.position_update () def is_active(self): return True def check_player(self): print "check player" return True def position_update(self): s = self.get_stream_information () if s.position == 0: # Try again once. timestamp sometimes goes through 0 when # modifying the player position. s = self.get_stream_information() self.status = s.status self.stream_duration = s.length self.current_position_value = long(s.position) if self.caption.text and (s.position < self.caption.begin or s.position > self.caption.end): self.display_text('', -1, -1) if self.overlay.data and (s.position < self.overlay.begin or s.position > self.overlay.end): self.imageoverlay.props.data=None self.overlay.begin=-1 self.overlay.end=-1 self.overlay.data=None def reparent(self, xid): # See https://bugzilla.gnome.org/show_bug.cgi?id=599885 #gtk.gdk.threads_enter() print "Reparent", hex(xid) gtk.gdk.display_get_default().sync() self.imagesink.set_xwindow_id(xid) self.imagesink.set_property('force-aspect-ratio', True) self.imagesink.expose() #gtk.gdk.threads_leave() def set_visual(self, xid): if not xid: return True self.xid = xid self.reparent(xid) return True def set_widget(self, widget): self.set_visual( widget.get_id() ) def restart_player(self): # FIXME: destroy the previous player self.player.set_state(gst.STATE_READY) # Rebuild the pipeline self.build_pipeline() self.playlist_add_item(self.videofile) self.position_update() return True def on_sync_message(self, bus, message): if message.structure is None: return if message.structure.get_name() == 'prepare-xwindow-id': self.reparent(self.xid) def sound_mute(self): if self.mute_volume is None: self.mute_volume=self.sound_get_volume() self.sound_set_volume(0) return def sound_unmute(self): if self.mute_volume is not None: self.sound_set_volume(self.mute_volume) self.mute_volume=None return def sound_is_muted(self): return (self.mute_volume is not None) def disp(self, e, indent=" "): l=[str(e)] if hasattr(e, 'elements'): i=indent+" " l.extend( [ self.disp(c, i) for c in e.elements() ]) return ("\n"+indent).join(l) def fullscreen(self, connect=None): def keypress(widget, event): if event.keyval == gtk.keysyms.Escape: self.unfullscreen() return True elif event.keyval == gtk.keysyms.space: # Since we are in fullscreen, there can be no # confusion with other widgets. self.pause() return True return False def buttonpress(widget, event): if event.button == 1 and event.type == gtk.gdk._2BUTTON_PRESS: self.unfullscreen() return True return False if self.fullscreen_window is None: self.fullscreen_window=gtk.Window() self.fullscreen_window.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK | gtk.gdk.KEY_PRESS_MASK | gtk.gdk.KEY_RELEASE_MASK | gtk.gdk.SCROLL_MASK) self.fullscreen_window.connect('key-press-event', keypress) self.fullscreen_window.connect('button-press-event', buttonpress) self.fullscreen_window.connect('destroy', self.unfullscreen) if connect is not None: connect(self.fullscreen_window) style=self.fullscreen_window.get_style().copy() black=gtk.gdk.color_parse('black') for state in (gtk.STATE_ACTIVE, gtk.STATE_NORMAL, gtk.STATE_SELECTED, gtk.STATE_INSENSITIVE, gtk.STATE_PRELIGHT): style.bg[state]=black style.base[state]=black self.fullscreen_window.set_style(style) if config.data.os == 'darwin': self.fullscreen_window.set_size_request(gtk.gdk.screen_width(), gtk.gdk.screen_height()) self.fullscreen_window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_SPLASHSCREEN) self.fullscreen_window.set_position(gtk.WIN_POS_CENTER) self.fullscreen_window.show() else: self.fullscreen_window.show() self.fullscreen_window.window.fullscreen() self.fullscreen_window.grab_focus() if config.data.os == 'win32': self.reparent(self.fullscreen_window.window.handle) else: self.reparent(self.fullscreen_window.window.xid) def unfullscreen(self, *p): self.reparent(self.xid) if self.fullscreen_window.window: self.fullscreen_window.hide() else: # It has been destroyed self.fullscreen_window = None return True # relpath, dump_bin and dump_element implementation based on Daniel Lenski <*****@*****.**> # posted on gst-dev mailing list on 20070913 def relpath(self, p1, p2): sep = os.path.sep # get common prefix (up to a slash) common = os.path.commonprefix((p1, p2)) common = common[:common.rfind(sep)] # remove common prefix p1 = p1[len(common)+1:] p2 = p2[len(common)+1:] # number of seps in p1 is # of ..'s needed return "../" * p1.count(sep) + p2 def dump_bin(self, bin, depth=0, recurse=-1, showcaps=True): return [ l for e in reversed(list(bin)) for l in self.dump_element(e, depth, recurse - 1) ] def dump_element(self, e, depth=0, recurse=-1, showcaps=True): ret=[] indentstr = depth * 8 * ' ' # print element path and factory path = e.get_path_string() + (isinstance(e, gst.Bin) and '/' or '') factory = e.get_factory() if factory is not None: ret.append( '%s%s (%s)' % (indentstr, path, factory.get_name()) ) else: ret.append( '%s%s (No factory)' % (indentstr, path) ) # print info about each pad for p in e.pads(): name = p.get_name() # negotiated capabilities caps = p.get_negotiated_caps() if caps: capsname = caps[0].get_name() elif showcaps: capsname = '; '.join(s.to_string() for s in set(p.get_caps())) else: capsname = None # flags flags = [] if not p.is_active(): flags.append('INACTIVE') if p.is_blocked(): flags.append('BLOCKED') # direction direc = (p.get_direction() is gst.PAD_SRC) and "=>" or "<=" # peer peer = p.get_peer() if peer: peerpath = self.relpath(path, peer.get_path_string()) else: peerpath = None # ghost target if isinstance(p, gst.GhostPad): target = p.get_target() if target: ghostpath = target.get_path_string() else: ghostpath = None else: ghostpath = None line=[ indentstr, " " ] if flags: line.append( ','.join(flags) ) line.append(".%s" % name) if capsname: line.append( '[%s]' % capsname ) if ghostpath: line.append( "ghosts %s" % self.relpath(path, ghostpath) ) line.append( "%s %s" % (direc, peerpath) ) #if peerpath and peerpath.find('proxy')!=-1: print peer ret.append( ''.join(line) ) if recurse and isinstance(e, gst.Bin): ret.extend( self.dump_bin(e, depth+1, recurse) ) return ret def str_element(self, element): return "\n".join(self.dump_element(element))