Add future-based async command API

This commit is contained in:
jaseg 2022-04-17 23:52:12 +02:00
parent d28f135382
commit 8d448d49f1
2 changed files with 64 additions and 37 deletions

83
mpv.py
View file

@ -131,11 +131,17 @@ class ErrorCode(object):
return ValueError(ErrorCode.human_readable(ec), ec, *args) return ValueError(ErrorCode.human_readable(ec), ec, *args)
@classmethod @classmethod
def raise_for_ec(kls, ec, func, *args): def exception_for_ec(kls, ec, *args):
ec = 0 if ec > 0 else ec ec = 0 if ec > 0 else ec
ex = kls.EXCEPTION_DICT.get(ec , kls.default_error_handler) ex = kls.EXCEPTION_DICT.get(ec, kls.default_error_handler)
if ex: if ex:
raise ex(ec, *args) return ex(ec, *args)
@classmethod
def raise_for_ec(kls, ec, func, *args):
ex = kls.exception_for_ec(ec, *args)
if ex:
raise ex
MpvGlGetProcAddressFn = CFUNCTYPE(c_void_p, c_void_p, c_char_p) MpvGlGetProcAddressFn = CFUNCTYPE(c_void_p, c_void_p, c_char_p)
class MpvOpenGLInitParams(Structure): class MpvOpenGLInitParams(Structure):
@ -557,6 +563,8 @@ _handle_func('mpv_command', [POINTER(c_char_p)],
_handle_func('mpv_command_string', [c_char_p, c_char_p], c_int, ec_errcheck) _handle_func('mpv_command_string', [c_char_p, c_char_p], c_int, ec_errcheck)
_handle_func('mpv_command_async', [c_ulonglong, POINTER(c_char_p)], c_int, ec_errcheck) _handle_func('mpv_command_async', [c_ulonglong, POINTER(c_char_p)], c_int, ec_errcheck)
_handle_func('mpv_command_node', [POINTER(MpvNode), POINTER(MpvNode)], c_int, ec_errcheck) _handle_func('mpv_command_node', [POINTER(MpvNode), POINTER(MpvNode)], c_int, ec_errcheck)
_handle_func('mpv_command_node_async', [c_ulonglong, POINTER(MpvNode)], c_int, ec_errcheck)
_handle_func('mpv_abort_async_command', [c_ulonglong], None, errcheck=None)
_handle_func('mpv_set_property', [c_char_p, MpvFormat, c_void_p], c_int, ec_errcheck) _handle_func('mpv_set_property', [c_char_p, MpvFormat, c_void_p], c_int, ec_errcheck)
_handle_func('mpv_set_property_string', [c_char_p, c_char_p], c_int, ec_errcheck) _handle_func('mpv_set_property_string', [c_char_p, c_char_p], c_int, ec_errcheck)
@ -824,8 +832,6 @@ class MPV(object):
To make your program not barf hard the first time its used on a weird file system **always** access properties To make your program not barf hard the first time its used on a weird file system **always** access properties
containing file names or file tags through ``MPV.raw``. """ containing file names or file tags through ``MPV.raw``. """
_UINT_64_MAX = 2 ** 64 - 1
def __init__(self, *extra_mpv_flags, log_handler=None, start_event_thread=True, loglevel=None, **extra_mpv_opts): def __init__(self, *extra_mpv_flags, log_handler=None, start_event_thread=True, loglevel=None, **extra_mpv_opts):
"""Create an MPV instance. """Create an MPV instance.
@ -853,9 +859,7 @@ class MPV(object):
self.lazy = _DecoderPropertyProxy(self, lazy_decoder) self.lazy = _DecoderPropertyProxy(self, lazy_decoder)
self._event_callbacks = [] self._event_callbacks = []
self._event_async_callbacks = {} self._command_reply_callbacks = {}
self._event_async_callback_counter = 0
self._event_async_callback_counter_lock = threading.Lock()
self._event_handler_lock = threading.Lock() self._event_handler_lock = threading.Lock()
self._property_handlers = collections.defaultdict(lambda: []) self._property_handlers = collections.defaultdict(lambda: [])
self._quit_handlers = set() self._quit_handlers = set()
@ -910,9 +914,9 @@ class MPV(object):
if eid == MpvEventID.COMMAND_REPLY: if eid == MpvEventID.COMMAND_REPLY:
key = devent['reply_userdata'] key = devent['reply_userdata']
callback = self._event_async_callbacks.pop(key, None) callback = self._command_reply_callbacks.pop(key, None)
if callback: if callback:
callback(devent['error'], devent['event']['result']) callback(ErrorCode.exception_for_ec(devent['error']), devent['event']['result'])
if eid == MpvEventID.SHUTDOWN: if eid == MpvEventID.SHUTDOWN:
_mpv_destroy(self._event_handle) _mpv_destroy(self._event_handle)
@ -1106,25 +1110,41 @@ class MPV(object):
_mpv_command(self.handle, args) _mpv_command(self.handle, args)
def command_async(self, name, *args, callback=None): def command_async(self, name, *args, callback=None):
"""Same as mpv_command, but run the command asynchronously. Once the command ran, the callback will be invoked, """Same as mpv_command, but run the command asynchronously. If you provide a callback, that callback will be
if provided. The first argument of the callback will be an integer value. If no error occurred this value will called after completion or on error. This method returns a future that evaluates to the result of the callback
be >= 0. In case of an error this will be a mpv_error value (see mpv.ErrorCode for more information). (if given), and the result of the libmpv call otherwise.
The second argument will be the return value of the command or None if the command does not return a value.
Callback example:: Usage example:
def callback(error, result): future = player.command_async(...)
try: try:
mpv.ErrorCode.raise_for_ec(error) print('The result was', future.result())
... # handle normal case except Exception as e:
except MemoryError as e: # for example print('mpv returned an error:', e)
... # handle MemoryError
except Exception as e:
... # catch-all: handle all other exceptions
""" """
key = self._register_async_callback(name, args, callback)
args = _create_null_term_cmd_arg_array(name, args) future = Future()
_mpv_command_async(self._event_handle, key, args) future.set_running_or_notify_cancel()
if callback is None:
def callback(error, result):
if error:
raise error
return result
def wrapper(error, result):
try:
future.set_result(callback(error, result))
except Exception as e:
future.set_exception(e)
self._command_reply_callbacks[id(future)] = wrapper
_1, _2, _3, pointer = _make_node_str_list([name, *args])
ppointer = cast(pointer, POINTER(MpvNode))
_mpv_command_node_async(self._event_handle, id(future), ppointer)
return future
def node_command(self, name, *args, decoder=strict_decoder): def node_command(self, name, *args, decoder=strict_decoder):
self.command(name, *args, decoder=decoder) self.command(name, *args, decoder=decoder)
@ -1903,17 +1923,6 @@ class MPV(object):
except AttributeError: except AttributeError:
return None return None
def _register_async_callback(self, name, args, callback):
if callback is None:
def callback(err, _result):
if err < 0:
warn('Error executing async command \'{} {}\': \'{}\''
.format(name, ' '.join(repr(arg) for arg in args), ErrorCode.human_readable(err)))
with self._event_async_callback_counter_lock:
key = self._event_async_callback_counter = (self._event_async_callback_counter + 1) % MPV._UINT_64_MAX
self._event_async_callbacks[key] = callback
return key
class MpvRenderContext: class MpvRenderContext:
def __init__(self, mpv, api_type, **kwargs): def __init__(self, mpv, api_type, **kwargs):

View file

@ -697,6 +697,24 @@ class CommandTests(MpvTestCase):
handler.assert_any_call('sub-text', 'This is\na subtitle test.') handler.assert_any_call('sub-text', 'This is\na subtitle test.')
handler.assert_any_call('sub-text', 'This is the second subtitle line.') handler.assert_any_call('sub-text', 'This is the second subtitle line.')
def test_async_command(self):
handler = mock.Mock()
callback = mock.Mock()
self.m.property_observer('sub-text')(handler)
time.sleep(0.5)
self.m.loadfile(TESTVID)
self.m.wait_until_playing()
self.m.command_async('sub_add', TESTSRT, callback=callback)
reply = self.m.command_async('expand-text', 'test ${mute}')
assert reply.result() == 'test no'
self.m.wait_for_playback()
handler.assert_any_call('sub-text', 'This is\na subtitle test.')
handler.assert_any_call('sub-text', 'This is the second subtitle line.')
callback.assert_any_call(None, None)
class RegressionTests(MpvTestCase): class RegressionTests(MpvTestCase):