|
@@ -0,0 +1,254 @@
|
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
|
+
|
|
|
|
+# Copyright (c) 2010, Stefan Parviainen <pafcu@iki.fi>
|
|
|
|
+#
|
|
|
|
+# Permission to use, copy, modify, and/or distribute this software for any
|
|
|
|
+# purpose with or without fee is hereby granted, provided that the above
|
|
|
|
+# copyright notice and this permission notice appear in all copies.
|
|
|
|
+#
|
|
|
|
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
|
|
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
|
|
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
|
|
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
|
|
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
|
|
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
|
|
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
|
+
|
|
|
|
+"""
|
|
|
|
+pymplb (PYthonMPLayerBingings) is a library that can be used to play media using an external MPlayer process.
|
|
|
|
+The library runs the MPlayer binary in slave mode as a subprocess and then sends slave-mode commands to the process.
|
|
|
|
+Commands are mapped to class methods and properties to class properties (by default prefixed with 'p_').
|
|
|
|
+Commands are discovered at runtime and thus these bindings should automatically also support any new commands added to MPlayer in the future.
|
|
|
|
+An example:
|
|
|
|
+>>> import pymplb
|
|
|
|
+>>> player = pymplb.MPlayer()
|
|
|
|
+>>> player.loadfile('test.ogv')
|
|
|
|
+>>> player.p_filename
|
|
|
|
+'test.ogv'
|
|
|
|
+"""
|
|
|
|
+
|
|
|
|
+from functools import partial
|
|
|
|
+import subprocess
|
|
|
|
+import atexit
|
|
|
|
+
|
|
|
|
+class PlayerNotFoundException(Exception):
|
|
|
|
+ """Exception which is raised when the external mplayer binary is not found."""
|
|
|
|
+ def __init__(self, player_path):
|
|
|
|
+ Exception.__init__(self, 'Player not found at %s'%player_path)
|
|
|
|
+
|
|
|
|
+def make_mplayer_class(mplayer_bin='mplayer', method_prefix='', property_prefix='p_'):
|
|
|
|
+ """
|
|
|
|
+ Construct a MPlayer class which user mplayer_bin as the player binary and prepends the given prefixes to property and method names.
|
|
|
|
+ Prefixes are needed because some properties and methods have the same name.
|
|
|
|
+ You only need to construct a new class if the default values are not suitable (i.e. mplayer is not in your path, or some new commands have been introduced that conflict with the default prefixes.
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ # Yes, I'm aware it's a bit messy to have a function in a function in a class in a function
|
|
|
|
+ # Decrease your indentation and bear with me here
|
|
|
|
+ class _MPlayer(object): #pylint: disable-msg=R0903
|
|
|
|
+ """
|
|
|
|
+ This is the main class used to play audio and video files by launching mplayer as a subprocess in slave mode.
|
|
|
|
+ Slave mode methods can be called directly (e.g. x.loadfile("somefile)") while properties are prefixed to avoid
|
|
|
|
+ name conflicts between methods and properties (e.g. x.p_looping = False).
|
|
|
|
+ Available methods and properties are determined at runtime when the class is instantiated. All methods and properties are
|
|
|
|
+ type-safe and properties respect minimum and maximum values given by mplayer.
|
|
|
|
+ """
|
|
|
|
+ _arg_types = {'Flag':type(False), 'String':type(''), 'Integer':type(0), 'Float':type(0.0), 'Position':type(0.0), 'Time':type(0.0)} # Mapping from mplayer -> Python types
|
|
|
|
+ _player_methods = {} # Need to keep track of methods because they must be modified after they have been added
|
|
|
|
+
|
|
|
|
+ def __init__(self, env=None, mplayer_args_d=None, **mplayer_args):
|
|
|
|
+ if mplayer_args_d: # Make pylint happy by not passing {} as an argument
|
|
|
|
+ mplayer_args.update(mplayer_args_d)
|
|
|
|
+ cmd_args = [mplayer_bin, '-slave', '-quiet', '-idle', '-msglevel', 'all=-1:global=4']
|
|
|
|
+ for (name, value) in mplayer_args.items():
|
|
|
|
+ cmd_args.append('-'+name)
|
|
|
|
+ if value != None and value != True:
|
|
|
|
+ cmd_args.append(str(value))
|
|
|
|
+
|
|
|
|
+ self.__player = _MPlayer._run_player(cmd_args, env=env)
|
|
|
|
+
|
|
|
|
+ # Partially apply methods to use the newly created player
|
|
|
|
+ for (name, func) in self._player_methods.items():
|
|
|
|
+ setattr(self, name, partial(func, self.__player))
|
|
|
|
+
|
|
|
|
+ atexit.register(self.close) # Make sure subprocess is killed
|
|
|
|
+
|
|
|
|
+ def close(self):
|
|
|
|
+ """Method that kills the MPlayer subprocess"""
|
|
|
|
+ try:
|
|
|
|
+ self.__player.terminate()
|
|
|
|
+ except:
|
|
|
|
+ pass
|
|
|
|
+
|
|
|
|
+ @staticmethod
|
|
|
|
+ def _run_player(args, env=None):
|
|
|
|
+ """Helper function that runs MPlayer with the given arguments"""
|
|
|
|
+ try:
|
|
|
|
+ player = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
|
|
|
|
+ except OSError as err:
|
|
|
|
+ if err.errno == 2:
|
|
|
|
+ raise PlayerNotFoundException(args[0])
|
|
|
|
+ else:
|
|
|
|
+ raise
|
|
|
|
+ return player
|
|
|
|
+
|
|
|
|
+ @classmethod
|
|
|
|
+ def _add_methods(cls, mplayer_bin):
|
|
|
|
+ """Discover which commands MPlayer understands and add them as class methods"""
|
|
|
|
+ def cmd(name, argtypes, obligatory, player, *args, **kwargs):
|
|
|
|
+ """Function which sends the given command to the MPlayer process"""
|
|
|
|
+ if len(args) < obligatory:
|
|
|
|
+ raise TypeError('TypeError: %s() takes at least %d arguments (%d given)'%(name, obligatory, len(args)))
|
|
|
|
+ if len(args) > len(argtypes):
|
|
|
|
+ raise TypeError('TypeError: %s() takes at most %d arguments (%d given)'%(name, len(argtypes), len(args)))
|
|
|
|
+ for i in range(len(args)):
|
|
|
|
+ if type(args[i]) != argtypes[i]:
|
|
|
|
+ raise TypeError('Argument %d of %s() has type %s, should be %s'%(i, name, type(args[i]).__name__, argtypes[i].__name__))
|
|
|
|
+
|
|
|
|
+ pausing = kwargs.get('pausing', '')
|
|
|
|
+ if pausing != '':
|
|
|
|
+ pausing = pausing + ' '
|
|
|
|
+
|
|
|
|
+ mplayer_command = '%s%s %s\n' % (pausing, name, ' '.join((str(x) for x in args)))
|
|
|
|
+ player.stdin.write(mplayer_command.encode('utf-8'))
|
|
|
|
+ player.stdin.flush()
|
|
|
|
+
|
|
|
|
+ # Read return value of commands that give one
|
|
|
|
+ # Hopefully this is smart enough ...
|
|
|
|
+ if name.startswith('get_'):
|
|
|
|
+ while True:
|
|
|
|
+ line = player.stdout.readline().decode('utf-8')
|
|
|
|
+ if line == '': # no more lines
|
|
|
|
+ return None
|
|
|
|
+
|
|
|
|
+ if not line[:3] == 'ANS':
|
|
|
|
+ continue
|
|
|
|
+
|
|
|
|
+ retval = line.split('=', 2)[1].rstrip()
|
|
|
|
+ if retval == 'PROPERTY_UNAVAILABLE':
|
|
|
|
+ return None
|
|
|
|
+
|
|
|
|
+ return retval
|
|
|
|
+
|
|
|
|
+ player = cls._run_player([mplayer_bin, '-input', 'cmdlist'])
|
|
|
|
+
|
|
|
|
+ # Add each command found
|
|
|
|
+ for line in player.stdout:
|
|
|
|
+ line = str(line.decode('utf-8'))
|
|
|
|
+ parts = line.strip().split()
|
|
|
|
+ name = parts[0]
|
|
|
|
+ args = parts[1:]
|
|
|
|
+ if len(parts) > 1:
|
|
|
|
+ obligatory = len([x for x in args if x[0] != '[']) # Number of obligatory args
|
|
|
|
+ try:
|
|
|
|
+ argtypes = [cls._arg_types[y] for y in [x.strip('[]') for x in args]]
|
|
|
|
+ except KeyError: # Unknown argument type
|
|
|
|
+ continue # Some garbage on the output (version?)
|
|
|
|
+
|
|
|
|
+ method = partial(cmd, name, argtypes, obligatory)
|
|
|
|
+ if not args:
|
|
|
|
+ method.__doc__ = 'Method taking no arguments'
|
|
|
|
+ elif len(args) == 1:
|
|
|
|
+ method.__doc__ = 'Method taking argument of type %s' % args[0]
|
|
|
|
+ else:
|
|
|
|
+ method.__doc__ = 'Method taking arguments of types %s' % ' '.join(args)
|
|
|
|
+
|
|
|
|
+ cls._player_methods[cls._method_prefix+name] = method
|
|
|
|
+ setattr(cls, cls._method_prefix+name, method)
|
|
|
|
+
|
|
|
|
+ @classmethod
|
|
|
|
+ def _add_properties(cls, mplayer_bin):
|
|
|
|
+ """Discover which properties MPlayer understands and add them as class properties"""
|
|
|
|
+ def get_prop(name, prop_type, islist, self):
|
|
|
|
+ """Function which calls the get_property method to get the property value and does some type checking"""
|
|
|
|
+ # self argument is needed to be property at the end because of partial
|
|
|
|
+ retval = getattr(self, cls._method_prefix+'get_property')(name)
|
|
|
|
+ if islist and retval == '(null)':
|
|
|
|
+ return []
|
|
|
|
+ if retval != None:
|
|
|
|
+ if prop_type != type(False):
|
|
|
|
+ if islist:
|
|
|
|
+ retval = [prop_type(x) for x in retval.split(',')]
|
|
|
|
+ else:
|
|
|
|
+ retval = prop_type(retval)
|
|
|
|
+ else:
|
|
|
|
+ if islist:
|
|
|
|
+ retval = [x == 'yes' for x in retval.split(',')]
|
|
|
|
+ else:
|
|
|
|
+ retval = (retval == 'yes')
|
|
|
|
+ return retval
|
|
|
|
+
|
|
|
|
+ # Function for getting and setting properties
|
|
|
|
+ def set_prop(name, prop_type, islist, prop_min, prop_max, self, value):
|
|
|
|
+ """Function which calls the set_property method to set the property value and does some type checking"""
|
|
|
|
+ if islist:
|
|
|
|
+ for elem in value:
|
|
|
|
+ if type(elem) != prop_type:
|
|
|
|
+ raise TypeError('TypeError: Element %s has wrong type %s, not %s'%(elem, type(elem).__name__, prop_type))
|
|
|
|
+ if prop_min != None and elem < prop_min:
|
|
|
|
+ raise ValueError('ValueError: Element %s must be at least %s'%(elem, prop_min))
|
|
|
|
+ if prop_max != None and elem > prop_max:
|
|
|
|
+ raise ValueError('ValueError: Element %s must be at most %s'%(elem, prop_max))
|
|
|
|
+ value = ','.join([str(elem) for elem in value])
|
|
|
|
+ else:
|
|
|
|
+ if type(value) != prop_type:
|
|
|
|
+ raise TypeError('TypeError: %s has type %s, not %s'%(name, prop_type.__name__, type(value).__name__))
|
|
|
|
+ if prop_min != None and value < prop_min:
|
|
|
|
+ raise ValueError('ValueError: %s must be at least %s (>%s)'%(name, prop_min, value))
|
|
|
|
+ if prop_max != None and value > prop_max:
|
|
|
|
+ raise ValueError('ValueError: %s must be at most %s (<%s)'%(name, prop_max, value))
|
|
|
|
+
|
|
|
|
+ getattr(self, cls._method_prefix+'set_property')(name, str(value))
|
|
|
|
+
|
|
|
|
+ player = cls._run_player([mplayer_bin, '-list-properties'])
|
|
|
|
+ # Add each property found
|
|
|
|
+ for line in player.stdout:
|
|
|
|
+ parts = line.strip().decode('utf-8').split()
|
|
|
|
+ if not (len(parts) == 4 or (len(parts) == 5 and parts[2] == 'list')):
|
|
|
|
+ continue
|
|
|
|
+ name = parts[0]
|
|
|
|
+ try:
|
|
|
|
+ prop_type = cls._arg_types[parts[1]]
|
|
|
|
+ except KeyError:
|
|
|
|
+ continue
|
|
|
|
+
|
|
|
|
+ if parts[2] == 'list': # Actually a list
|
|
|
|
+ prop_min = parts[3]
|
|
|
|
+ prop_max = parts[4]
|
|
|
|
+ islist = True
|
|
|
|
+ else:
|
|
|
|
+ prop_min = parts[2]
|
|
|
|
+ prop_max = parts[3]
|
|
|
|
+ islist = False
|
|
|
|
+
|
|
|
|
+ if prop_min == 'No':
|
|
|
|
+ prop_min = None
|
|
|
|
+ else:
|
|
|
|
+ prop_min = prop_type(prop_min)
|
|
|
|
+
|
|
|
|
+ if prop_max == 'No':
|
|
|
|
+ prop_max = None
|
|
|
|
+ else:
|
|
|
|
+ prop_max = prop_type(prop_max)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ getter = partial(get_prop, name, prop_type, islist)
|
|
|
|
+ setter = partial(set_prop, name, prop_type, islist, prop_min, prop_max)
|
|
|
|
+ setattr(cls, cls._property_prefix+name, property(getter, setter, doc='Property of type %s in range [%s, %s].'%(prop_type.__name__, prop_min, prop_max)))
|
|
|
|
+ # end of _MPlayer
|
|
|
|
+
|
|
|
|
+ _MPlayer._method_prefix = method_prefix
|
|
|
|
+ _MPlayer._property_prefix = property_prefix
|
|
|
|
+
|
|
|
|
+ _MPlayer._add_methods(mplayer_bin)
|
|
|
|
+ _MPlayer._add_properties(mplayer_bin)
|
|
|
|
+ return _MPlayer
|
|
|
|
+
|
|
|
|
+try:
|
|
|
|
+ MPlayer = make_mplayer_class() # pylint: disable-msg=C0103
|
|
|
|
+except PlayerNotFoundException as e:
|
|
|
|
+ print("Fatal:", e)
|
|
|
|
+
|
|
|
|
+if __name__ == "__main__":
|
|
|
|
+ import doctest
|
|
|
|
+ doctest.testmod(optionflags=doctest.ELLIPSIS)
|