pymplb.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. # -*- coding: utf-8 -*-
  2. # Copyright (c) 2010, Stefan Parviainen <pafcu@iki.fi>
  3. #
  4. # Permission to use, copy, modify, and/or distribute this software for any
  5. # purpose with or without fee is hereby granted, provided that the above
  6. # copyright notice and this permission notice appear in all copies.
  7. #
  8. # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  9. # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  10. # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  11. # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  12. # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  13. # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  14. # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15. """
  16. pymplb (PYthonMPLayerBingings) is a library that can be used to play media using an external MPlayer process.
  17. The library runs the MPlayer binary in slave mode as a subprocess and then sends slave-mode commands to the process.
  18. Commands are mapped to class methods and properties to class properties (by default prefixed with 'p_').
  19. Commands are discovered at runtime and thus these bindings should automatically also support any new commands added to MPlayer in the future.
  20. An example:
  21. >>> import pymplb
  22. >>> player = pymplb.MPlayer()
  23. >>> player.loadfile('test.ogv')
  24. >>> player.p_filename
  25. 'test.ogv'
  26. """
  27. from functools import partial
  28. import subprocess
  29. import atexit
  30. class PlayerNotFoundException(Exception):
  31. """Exception which is raised when the external mplayer binary is not found."""
  32. def __init__(self, player_path):
  33. Exception.__init__(self, 'Player not found at %s'%player_path)
  34. def make_mplayer_class(mplayer_bin='mplayer', method_prefix='', property_prefix='p_'):
  35. """
  36. Construct a MPlayer class which user mplayer_bin as the player binary and prepends the given prefixes to property and method names.
  37. Prefixes are needed because some properties and methods have the same name.
  38. 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.
  39. """
  40. # Yes, I'm aware it's a bit messy to have a function in a function in a class in a function
  41. # Decrease your indentation and bear with me here
  42. class _MPlayer(object): #pylint: disable-msg=R0903
  43. """
  44. This is the main class used to play audio and video files by launching mplayer as a subprocess in slave mode.
  45. Slave mode methods can be called directly (e.g. x.loadfile("somefile)") while properties are prefixed to avoid
  46. name conflicts between methods and properties (e.g. x.p_looping = False).
  47. Available methods and properties are determined at runtime when the class is instantiated. All methods and properties are
  48. type-safe and properties respect minimum and maximum values given by mplayer.
  49. """
  50. _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
  51. _player_methods = {} # Need to keep track of methods because they must be modified after they have been added
  52. def __init__(self, env=None, mplayer_args_d=None, **mplayer_args):
  53. if mplayer_args_d: # Make pylint happy by not passing {} as an argument
  54. mplayer_args.update(mplayer_args_d)
  55. cmd_args = [mplayer_bin, '-slave', '-quiet', '-idle', '-msglevel', 'all=-1:global=4']
  56. for (name, value) in mplayer_args.items():
  57. cmd_args.append('-'+name)
  58. if value != None and value != True:
  59. cmd_args.append(str(value))
  60. self.__player = _MPlayer._run_player(cmd_args, env=env)
  61. # Partially apply methods to use the newly created player
  62. for (name, func) in self._player_methods.items():
  63. setattr(self, name, partial(func, self.__player))
  64. atexit.register(self.close) # Make sure subprocess is killed
  65. def close(self):
  66. """Method that kills the MPlayer subprocess"""
  67. try:
  68. self.__player.terminate()
  69. except:
  70. pass
  71. @staticmethod
  72. def _run_player(args, env=None):
  73. """Helper function that runs MPlayer with the given arguments"""
  74. try:
  75. player = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
  76. except OSError as err:
  77. if err.errno == 2:
  78. raise PlayerNotFoundException(args[0])
  79. else:
  80. raise
  81. return player
  82. @classmethod
  83. def _add_methods(cls, mplayer_bin):
  84. """Discover which commands MPlayer understands and add them as class methods"""
  85. def cmd(name, argtypes, obligatory, player, *args, **kwargs):
  86. """Function which sends the given command to the MPlayer process"""
  87. if len(args) < obligatory:
  88. raise TypeError('TypeError: %s() takes at least %d arguments (%d given)'%(name, obligatory, len(args)))
  89. if len(args) > len(argtypes):
  90. raise TypeError('TypeError: %s() takes at most %d arguments (%d given)'%(name, len(argtypes), len(args)))
  91. for i in range(len(args)):
  92. if type(args[i]) != argtypes[i]:
  93. raise TypeError('Argument %d of %s() has type %s, should be %s'%(i, name, type(args[i]).__name__, argtypes[i].__name__))
  94. pausing = kwargs.get('pausing', '')
  95. if pausing != '':
  96. pausing = pausing + ' '
  97. mplayer_command = '%s%s %s\n' % (pausing, name, ' '.join((str(x) for x in args)))
  98. player.stdin.write(mplayer_command.encode('utf-8'))
  99. player.stdin.flush()
  100. # Read return value of commands that give one
  101. # Hopefully this is smart enough ...
  102. if name.startswith('get_'):
  103. while True:
  104. line = player.stdout.readline().decode('utf-8')
  105. if line == '': # no more lines
  106. return None
  107. if not line[:3] == 'ANS':
  108. continue
  109. retval = line.split('=', 2)[1].rstrip()
  110. if retval == 'PROPERTY_UNAVAILABLE':
  111. return None
  112. return retval
  113. player = cls._run_player([mplayer_bin, '-input', 'cmdlist'])
  114. # Add each command found
  115. for line in player.stdout:
  116. line = str(line.decode('utf-8'))
  117. parts = line.strip().split()
  118. name = parts[0]
  119. args = parts[1:]
  120. if len(parts) > 1:
  121. obligatory = len([x for x in args if x[0] != '[']) # Number of obligatory args
  122. try:
  123. argtypes = [cls._arg_types[y] for y in [x.strip('[]') for x in args]]
  124. except KeyError: # Unknown argument type
  125. continue # Some garbage on the output (version?)
  126. method = partial(cmd, name, argtypes, obligatory)
  127. if not args:
  128. method.__doc__ = 'Method taking no arguments'
  129. elif len(args) == 1:
  130. method.__doc__ = 'Method taking argument of type %s' % args[0]
  131. else:
  132. method.__doc__ = 'Method taking arguments of types %s' % ' '.join(args)
  133. cls._player_methods[cls._method_prefix+name] = method
  134. setattr(cls, cls._method_prefix+name, method)
  135. @classmethod
  136. def _add_properties(cls, mplayer_bin):
  137. """Discover which properties MPlayer understands and add them as class properties"""
  138. def get_prop(name, prop_type, islist, self):
  139. """Function which calls the get_property method to get the property value and does some type checking"""
  140. # self argument is needed to be property at the end because of partial
  141. retval = getattr(self, cls._method_prefix+'get_property')(name)
  142. if islist and retval == '(null)':
  143. return []
  144. if retval != None:
  145. if prop_type != type(False):
  146. if islist:
  147. retval = [prop_type(x) for x in retval.split(',')]
  148. else:
  149. retval = prop_type(retval)
  150. else:
  151. if islist:
  152. retval = [x == 'yes' for x in retval.split(',')]
  153. else:
  154. retval = (retval == 'yes')
  155. return retval
  156. # Function for getting and setting properties
  157. def set_prop(name, prop_type, islist, prop_min, prop_max, self, value):
  158. """Function which calls the set_property method to set the property value and does some type checking"""
  159. if islist:
  160. for elem in value:
  161. if type(elem) != prop_type:
  162. raise TypeError('TypeError: Element %s has wrong type %s, not %s'%(elem, type(elem).__name__, prop_type))
  163. if prop_min != None and elem < prop_min:
  164. raise ValueError('ValueError: Element %s must be at least %s'%(elem, prop_min))
  165. if prop_max != None and elem > prop_max:
  166. raise ValueError('ValueError: Element %s must be at most %s'%(elem, prop_max))
  167. value = ','.join([str(elem) for elem in value])
  168. else:
  169. if type(value) != prop_type:
  170. raise TypeError('TypeError: %s has type %s, not %s'%(name, prop_type.__name__, type(value).__name__))
  171. if prop_min != None and value < prop_min:
  172. raise ValueError('ValueError: %s must be at least %s (>%s)'%(name, prop_min, value))
  173. if prop_max != None and value > prop_max:
  174. raise ValueError('ValueError: %s must be at most %s (<%s)'%(name, prop_max, value))
  175. getattr(self, cls._method_prefix+'set_property')(name, str(value))
  176. player = cls._run_player([mplayer_bin, '-list-properties'])
  177. # Add each property found
  178. for line in player.stdout:
  179. parts = line.strip().decode('utf-8').split()
  180. if not (len(parts) == 4 or (len(parts) == 5 and parts[2] == 'list')):
  181. continue
  182. name = parts[0]
  183. try:
  184. prop_type = cls._arg_types[parts[1]]
  185. except KeyError:
  186. continue
  187. if parts[2] == 'list': # Actually a list
  188. prop_min = parts[3]
  189. prop_max = parts[4]
  190. islist = True
  191. else:
  192. prop_min = parts[2]
  193. prop_max = parts[3]
  194. islist = False
  195. if prop_min == 'No':
  196. prop_min = None
  197. else:
  198. prop_min = prop_type(prop_min)
  199. if prop_max == 'No':
  200. prop_max = None
  201. else:
  202. prop_max = prop_type(prop_max)
  203. getter = partial(get_prop, name, prop_type, islist)
  204. setter = partial(set_prop, name, prop_type, islist, prop_min, prop_max)
  205. 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)))
  206. # end of _MPlayer
  207. _MPlayer._method_prefix = method_prefix
  208. _MPlayer._property_prefix = property_prefix
  209. _MPlayer._add_methods(mplayer_bin)
  210. _MPlayer._add_properties(mplayer_bin)
  211. return _MPlayer
  212. try:
  213. MPlayer = make_mplayer_class() # pylint: disable-msg=C0103
  214. except PlayerNotFoundException as e:
  215. print("Fatal:", e)
  216. if __name__ == "__main__":
  217. import doctest
  218. doctest.testmod(optionflags=doctest.ELLIPSIS)