def start_method(self, attrs): self.member = interface.Method(str(attrs['name'])) self.member.nargs = 0 self.member.nret = 0 self.isMethod = True
class DBusObject(object): """ Straight-forward L{IDBusObject} implementation. This implementation provides an API similar to that of L{twisted.spread.pb.Referenceable}. Classes to be exported over DBus may simply derive from L{DBusObject}, specify their object path and supported DBus interfaces in the constructor, and implement methods named 'dbus_<methodName>' for each method they wish to support. @ivar dbusInterfaces: List of L{interface.DBusInterface} objects this class supports. If one or more superclasses also define dbusInterfaces, the contents of those lists will be added to the total interfaces the object supports. """ implements(IDBusObject) _objectHandler = None dbusInterfaces = [ interface.DBusInterface( 'org.freedesktop.DBus.Properties', interface.Method('Get', 'ss', 'v'), interface.Method('Set', 'ssv'), interface.Method('GetAll', 's', 'a{sv}'), interface.Signal('PropertiesChanged', 'sa{sv}as')), ] def __init__(self, objectPath): """ @type objectPath: C{string} @param objectPath: The DBus path of the object. The format of the path must comply with the DBus specification. """ self._objectPath = objectPath self._objectHandler = None marshal.validateObjectPath(objectPath) def _iterIFaceCaches(self): for base in self.__class__.__mro__: if base is object: return cache = base.__dict__.get('_dbusIfaceCache', None) if cache is None: cache = dict() for name, obj in base.__dict__.iteritems(): self._cacheInterfaces(base, cache, name, obj) setattr(base, '_dbusIfaceCache', cache) yield cache def _cacheInterfaces(self, cls, cache, cls_attr_name, obj): def get_ic(interface_name): if not interface_name in cache: cache[interface_name] = _IfaceCache(interface_name) return cache[interface_name] if inspect.isfunction(obj) and hasattr(obj, '_dbusInterface'): get_ic(obj._dbusInterface).methods[obj._dbusMethod] = getattr( cls, obj.func_name) elif isinstance(obj, DBusProperty): if obj.interface is None: for iface in self.getInterfaces(): if obj.pname in iface.properties: obj.interface = iface.name break if obj.interface is None: raise AttributeError( 'No supported DBus interfaces contain a property named "%s"' % obj.pname) for iface in self.getInterfaces(): if obj.interface == iface.name: obj.iprop = iface.properties[obj.pname] break get_ic(obj.interface).properties[obj.pname] = obj obj.attr_name = cls_attr_name def _searchCache(self, interfaceName, cacheAttr, key): for cache in self._iterIFaceCaches(): if interfaceName: if interfaceName in cache: d = getattr(cache[interfaceName], cacheAttr) if key in d: return d[key] else: for ic in cache.itervalues(): d = getattr(ic, cacheAttr) if key in d: return d[key] def _getDecoratedMethod(self, interfaceName, methodName): f = self._searchCache(interfaceName, 'methods', methodName) if f: return getattr(self, f.func_name) def _getProperty(self, interfaceName, propertyName): return self._searchCache(interfaceName, 'properties', propertyName) def getConnection(self): if self._objectHandler: return self._objectHandler.conn def getInterfaces(self): for base in self.__class__.__mro__: if 'dbusInterfaces' in base.__dict__: for iface in base.dbusInterfaces: yield iface def getObjectPath(self): return self._objectPath def setObjectHandler(self, objectHandler): self._objectHandler = objectHandler def _set_method_flags(self, method_obj): """ Sets the \"_dbusCaller\" boolean on the \"dbus_*\" methods. This is a one-time operation used to flag each method with a boolean indicating whether or not they accept the \"dbusCaller\" keyword argument """ args = inspect.getargspec(method_obj)[0] needs_caller = False if len(args) >= 1 and args[-1] == 'dbusCaller': needs_caller = True method_obj.im_func._dbusCaller = needs_caller def executeMethod(self, interfaceObj, methodName, methodArguments, sender): m = getattr(self, 'dbus_' + methodName, None) iname = interfaceObj.name if m is None: m = self._getDecoratedMethod(iname, methodName) if m is None: raise NotImplementedError if hasattr(m, '_dbusInterface') and m._dbusInterface != iname: m = self._getDecoratedMethod(iname, methodName) if m is None: raise NotImplementedError if not hasattr(m, '_dbusCaller'): self._set_method_flags(m) if m._dbusCaller: if methodArguments: return m(*methodArguments, dbusCaller=sender) else: return m(dbusCaller=sender) else: if methodArguments: return m(*methodArguments) else: return m() def emitSignal(self, signalName, *args, **kwargs): """ Emits the specified signal with the supplied arguments @type signalName: C{string} @param signalName: Name of the signal to emit. This must match the name of a signal in one of the objects supported interfaces. @type interface: C{string} @keyword interface: Optional keyword argument specifying the DBus interface to use. This is only needed if more than one interface defines a signal with the same name. @param args: Positional arguments for the signal content """ if self._objectHandler is None: return iface = kwargs.get('interface', None) s = None for i in self.getInterfaces(): if iface and not iface == i.name: continue t = i.signals.get(signalName, None) if isinstance(t, interface.Signal): s = t break if s is None: raise AttributeError( 'Signal "%s" not found in any supported interface.' % (signalName, )) msig = message.SignalMessage(self._objectPath, signalName, i.name, signature=s.sig, body=args) self._objectHandler.conn.sendMessage(msig) def getAllProperties(self, interfaceName): r = dict() def addp(p): if p.iprop.access != 'write': v = getattr(self, p.attr_name) if p.iprop.sig in marshal.variantClassMap: v = marshal.variantClassMap[p.iprop.sig](v) r[p.pname] = v if interfaceName: for cache in self._iterIFaceCaches(): ifc = cache.get(interfaceName, None) if ifc: for p in ifc.properties.itervalues(): addp(p) break else: for cache in self._iterIFaceCaches(): for ifc in cache.itervalues(): for p in ifc.properties.itervalues(): addp(p) return r @dbusMethod('org.freedesktop.DBus.Properties', 'Get') def _dbus_PropertyGet(self, interfaceName, propertyName): p = self._getProperty(interfaceName, propertyName) if p is None: raise Exception('Invalid Property') if p.iprop.access == 'write': raise Exception('Property is not readable') v = getattr(self, p.attr_name) if p.iprop.sig in marshal.variantClassMap: return marshal.variantClassMap[p.iprop.sig](v) else: return v @dbusMethod('org.freedesktop.DBus.Properties', 'Set') def _dbus_PropertySet(self, interfaceName, propertyName, value): p = self._getProperty(interfaceName, propertyName) if p is None: raise Exception('Invalid Property') if p.iprop.access not in ('write', 'readwrite'): raise Exception('Property is not Writeable') return setattr(self, p.attr_name, value) @dbusMethod('org.freedesktop.DBus.Properties', 'GetAll') def _dbus_PropertyGetAll(self, interfaceName): return self.getAllProperties(interfaceName)
class OMXPlayer(object): """ Asyncronous, Twisted based, wrapper/proxy for omxplayer processes. """ def __init__(self, filename, player_mgr, *, layer=0, loop=False, alpha=255, fadein=0, fadeout=0): """ Initialization arguments: - `filename`: the movie filename to play. - `player_mgr`: provides access to DBus, reactor, and more. - `layer`: used with omxplayer --layer argument. - `loop`: if true, omxplayer is passed the --loop argument. - `alpha`: used with omxplayer --alpha argument. - `fadein`: fade in duration, in seconds. - `fadeout`: fade out duration, in seconds. """ self._filename = filename self._player_mgr = player_mgr self._dbus_mgr = player_mgr.dbus_mgr self._layer = layer self._loop = loop self._alpha = alpha self._fadein = fadein self._fadeout = fadeout # Will be obtained by querying the omxplayer process via DBus. self._duration = None # Use a known name so that we can track omxplayer's DBus presence. self._dbus_player_name = self.generate_player_name(filename) self._log = logger.Logger(namespace='player.each.%s' % (self._dbus_player_name, )) self._reactor = self._player_mgr.reactor self._dbus_conn = self._dbus_mgr.dbus_conn # Used to track omxplayer process startup/termination. self._process_protocol = None # DBus proxy object for the player: used to control it. self._dbus_player = None # Lifecycle tracking. self._ready = defer.Deferred() self._stop_in_progress = False self._fadeout_dc = None self._fading_out = False def __repr__(self): return '<OMXPlayer %r filename=%r>' % ( self._dbus_player_name, self._filename, ) _player_id = 0 @staticmethod def generate_player_name(filename): """ Generate unique player name. """ OMXPlayer._player_id = (OMXPlayer._player_id + 1) % 1000 return 'c.p%s-%03i' % ( os.path.splitext(os.path.basename(filename))[0], OMXPlayer._player_id, ) # Maybe if txdbus and/or omxplayer's introspection abilities were better # these declarations wouldn't be needed; as of this writing, they are. # For the nitty gritty details, see: # - https://dbus.freedesktop.org/doc/dbus-specification.html # - https://github.com/popcornmix/omxplayer # - https://github.com/cocagne/txdbus _OMX_DBUS_PLAYER_PROPERTIES = txdbus_interface.DBusInterface( 'org.freedesktop.DBus.Properties', txdbus_interface.Method('Get', arguments='ss', returns='x'), ) _OMX_DBUS_PLAYER_INTERFACE = txdbus_interface.DBusInterface( 'org.mpris.MediaPlayer2.Player', txdbus_interface.Method('PlayPause', arguments='', returns=''), txdbus_interface.Method('Stop', arguments='', returns=''), txdbus_interface.Method('SetAlpha', arguments='ox', returns='x'), txdbus_interface.Method('SetPosition', arguments='ox', returns='x'), ) @defer.inlineCallbacks def spawn(self, end_callable=None): """ Spawns the omxplayer process associated to this instance, returning a deferred that fires after the process is started and ready to be controlled via the other instance methods; the spawned omxplayer will be paused. The optional `end_callable` will be called when the spawned omxplayer process terminates, and passed in a single argument with the omxplayer's exit code. """ player_name = self._dbus_player_name self._log.info('spawning') # Ask DBus manager to track this player's name bus presence. self._dbus_mgr.track_dbus_name(player_name) self._spawn_process() # Wait for process started confirmation. yield self._process_protocol.started # Wait until the player name shows up on DBus. yield self._dbus_mgr.wait_dbus_name_start(player_name) # Setup the optional notification of process termination. if end_callable: self._process_protocol.stopped.addCallback(end_callable) yield self._get_dbus_player_object() yield self._determine_duration() # Player is now ready to be controlled. self._log.info('ready') self._ready.callback(None) # Since omxplayer defaults to starting in play mode, ask it to # play/pause straight away; we promised to have it paused when # done. yield self.play_pause() def _spawn_process(self): # Spawn the omxplayer.bin process. args = [self._player_mgr.executable] if self._loop: args.append('--loop') args.extend(('--dbus_name', str(self._dbus_player_name))) args.extend(('--layer', str(self._layer))) args.extend(('--orientation', str(180))) args.append('--no-osd') args.extend(('--alpha', str(self._alpha))) args.append(str(self._filename)) self._process_protocol = process.spawn( self._reactor, args, 'player.proc.%s' % (self._dbus_player_name, ), ) @defer.inlineCallbacks def _get_dbus_player_object(self): # Get the DBus object for this player. self._log.debug('getting dbus object') # Hardcoded data from omxplayer documentation. ifaces = [ self._OMX_DBUS_PLAYER_PROPERTIES, self._OMX_DBUS_PLAYER_INTERFACE, ] self._dbus_player = yield self._dbus_conn.getRemoteObject( self._dbus_player_name, '/org/mpris/MediaPlayer2', interfaces=ifaces, ) self._log.debug('got dbus object') @defer.inlineCallbacks def _determine_duration(self): # Ask omxplayer for the duration of the video file. duration_microsecs = yield self._dbus_player.callRemote( 'Get', 'org.mpris.MediaPlayer2.Player', 'Duration', ) self._duration = duration_microsecs / 1000000 self._log.debug('duration is {d:.1f}s', d=self._duration) @defer.inlineCallbacks def _wait_ready(self, action): # Returns a deferred that fires when the spawned omxplayer is # ready to be controlled via DBus; methods exposing such type of # controls will wait on this before issuing actual control commands # towards omxplayer; this prevents race conditions that exist between # the `spawn` method (that can take quite a while to complete) and # the remaining player control methods. # TODO: Convert this into a decorator? if not self._ready.called: self._log.info('wait ready: {a}', a=action) yield self._ready @defer.inlineCallbacks def stop(self, skip_dbus=False, timeout=1): """ Stops the spawned omxplayer process. If `skip_dbus` is False, starts by asking it to stop via DBus, waiting for it to cleanly stop. In that case, returns a deferred that fires with the exit code. If that fails, tries to send a SIGTERM signal to the process. If it works, waits for the process to cleanly stop. In the non DBus controlled clean stop, returns a deferred that fires with None, when completed. """ player_name = self._dbus_player_name self._log.info('stopping') self._cancel_scheduled_fadeout() if self._process_protocol.stopped.called: # Prevent race condition: do nothing if process is gone. self._log.info('no process to stop', p=player_name) exit_code = yield self._process_protocol.stopped defer.returnValue(exit_code) return exit_code = None stop_via_sigterm = skip_dbus if not skip_dbus: try: exit_code = yield self._stop_via_dbus(timeout=timeout) except Exception as e: # May have failed due to timeout or any other reason. # The best we can do is ensuring we try stopping it via SIGTERM # and prevent exception propagation to caller, letting it assume # stop() completed successfully. stop_via_sigterm = True self._log.warn('stopping failed: {e!r}', e=e) if stop_via_sigterm: yield self._stop_via_sigterm() self._log.info('stopped') defer.returnValue(exit_code) @defer.inlineCallbacks def _stop_via_dbus(self, timeout): # Asks the spawned process to stop via a DBus command, waits # for it to be gone from DBus and for process exit. Returns a # deferred that fires with the process exit code. # May raise exceptions if, for example, DBus is unreachable. yield self._wait_ready('stop') if not self._stop_in_progress: self._stop_in_progress = True self._log.debug('requesting stop') try: # Prevent race condition with timeout: process might have # terminated or DBus may have become unreachable. yield self._dbus_player.callRemote( 'Stop', interface='org.mpris.MediaPlayer2.Player', timeout=timeout, ) except error.TimeOut: self._log.info('stop request timed out') raise except Exception as e: self._log.warn('stop request failed: {e!r}', e=e) raise else: self._log.debug('requested stop') player_name = self._dbus_player_name if not self._process_protocol.stopped.called: # Process still there: wait until it disappears from DBus. yield self._dbus_mgr.wait_dbus_name_stop(player_name) # Finally, wait for the actual process to end and get exit code. exit_code = yield self._process_protocol.stopped defer.returnValue(exit_code) @defer.inlineCallbacks def _stop_via_sigterm(self): # Sends a SIGTERM to the spawned process and waits for it to exit. self._log.debug('signalling process termination') try: self._process_protocol.terminate() except OSError as e: self._log.warn('signalling process failed: {e!r}', e=e) else: self._log.debug('signalled process termination') # Finally, wait for the process to end, discarding the exit code. yield self._process_protocol.stopped @defer.inlineCallbacks def play_pause(self): """ Asks the spawned omxplayer to play/pause. Returns a deferred that fires once the command is acknowledged. """ yield self._wait_ready('play/pause') # Based on https://github.com/popcornmix/omxplayer self._log.debug('requesting play/pause') yield self._dbus_player.callRemote( 'PlayPause', interface='org.mpris.MediaPlayer2.Player') self._log.debug('requested play/pause') @defer.inlineCallbacks def play(self, skip_fadein=False): """ To be used right after `spawn`, asks the spawned omxplayer to play. Immediately after that: - Schedules the video fade out, if not looping. - Initiates the video fade in. Returns a deferred that fires when the video as faded in completely. """ yield self.play_pause() if not self._loop: self._schedule_fadeout() yield self.fadein(immediate=skip_fadein) def _schedule_fadeout(self): """ Schedules end-of-video automatic fadeout. Assumes video has just started playing from the beginning. """ self._cancel_scheduled_fadeout() delta_t = self._duration - self._fadeout - 0.1 self._fadeout_dc = self._reactor.callLater(delta_t, self.fadeout) self._log.debug('will fade out in {d:.1f} seconds', d=delta_t) def _cancel_scheduled_fadeout(self): """ Cancel any eventually scheduled fadeout. """ if self._fadeout_dc: try: self._fadeout_dc.cancel() except Exception: pass @defer.inlineCallbacks def _set_alpha(self, int64): """ Asks the spawned omxplayer to change its alpha value. Returns a deferred that fires once the command is acknowledged. """ result = yield self._dbus_player.callRemote( 'SetAlpha', '/not/used', int64, interface='org.mpris.MediaPlayer2.Player') defer.returnValue(result) @defer.inlineCallbacks def _fade(self, duration, from_alpha, to_alpha): # Issues timed calls to `_set_alpha` to ensure that the spawned # omxplayer's alpha is faded between the passed in alpha values: # - `duration` represents time in seconds. # Notes: # - Assumes 25fps video. # - Uses delay < 20ms which is 2x framerate. if duration: delay = 0.019 start_time = time() delta_alpha = to_alpha - from_alpha relative_time = 0 while relative_time < 1: alpha = from_alpha + delta_alpha * relative_time self._log.debug('alpha {s}', s=alpha) yield self._set_alpha(round(alpha)) yield sleep(delay, self._reactor) relative_time = (time() - start_time) / duration result = yield self._set_alpha(round(to_alpha)) defer.returnValue(result) @defer.inlineCallbacks def fadein(self, immediate=False): """ Triggers a fade in of the spawned omxplayer. Returns a deferred that fires once the fade in is completed. """ yield self._wait_ready('fade in') self._log.info('fade in starting') result = yield self._fade(0 if immediate else self._fadein, 0, 255) self._log.info('fade in completed') defer.returnValue(result) @defer.inlineCallbacks def fadeout(self): """ Triggers a fade out of the spawned omxplayer. Returns a deferred that fires once the fade out is completed. """ yield self._wait_ready('fade out') if self._fading_out: self._log.info('fade out in progress') return self._log.info('fade out starting') self._cancel_scheduled_fadeout() self._fading_out = True result = yield self._fade(self._fadeout, 255, 0) self._fading_out = False self._log.info('fade out completed') defer.returnValue(result) @defer.inlineCallbacks def fadeout_and_stop(self): """ Stops after a fade out. """ yield self.fadeout() yield self.stop()