123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- # -*- 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)
|