From ab8b8b54772d06416abf3ae36724b5272cddd59d Mon Sep 17 00:00:00 2001 From: jaseg Date: Wed, 17 Aug 2016 23:21:19 +0200 Subject: [PATCH] Improve event handling, add message handling, add key binding foo --- README.md | 7 +++++- mpv-test.py | 11 +++++---- mpv.py | 67 +++++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 73 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 2a3b61c..a533ee3 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ import mpv def my_log(loglevel, component, message): print('[{}] {}: {}'.format(loglevel, component, message)) -player = mpv.MPV(log_handler=my_log, ytdl=True) +player = mpv.MPV(log_handler=my_log, ytdl=True, input_default_bindings=True, input_vo_keyboard=True) # Property access, these can be changed at runtime player.observe_property('time-pos', lambda _property, pos: print('Now playing at {:.2f}s'.format(pos))) @@ -39,6 +39,11 @@ player.loop = 'inf' # Option access, in general these require the core to reinitialize player['vo'] = 'opengl' +def my_q_binding(state, key): + if state[0] == 'd': + print('THERE IS NO ESCAPE') +player.register_key_binding('q', my_q_binding) + player.play('https://youtu.be/DLzxrzFCyOs') player.wait_for_playback() diff --git a/mpv-test.py b/mpv-test.py index 2be2735..e5a51f2 100755 --- a/mpv-test.py +++ b/mpv-test.py @@ -175,17 +175,20 @@ class TestLifecycle(unittest.TestCase): def test_event_callback(self): handler = mock.Mock() m = mpv.MPV('no-video') - m.event_callbacks.append(handler) + m.register_event_callback(handler) m.play(TESTVID) m.wait_for_playback() - del m + + m.unregister_event_callback(handler) handler.assert_has_calls([ mock.call({'reply_userdata': 0, 'error': 0, 'event_id': 6, 'event': None}), mock.call({'reply_userdata': 0, 'error': 0, 'event_id': 9, 'event': None}), mock.call({'reply_userdata': 0, 'error': 0, 'event_id': 7, 'event': {'reason': 4}}), - mock.call({'reply_userdata': 0, 'error': 0, 'event_id': 11, 'event': None}), - mock.call({'reply_userdata': 0, 'error': 0, 'event_id': 1, 'event': None}) ], any_order=True) + handler.reset_mock() + + del m + handler.assert_not_called() def test_log_handler(self): handler = mock.Mock() diff --git a/mpv.py b/mpv.py index 82efafe..6448207 100644 --- a/mpv.py +++ b/mpv.py @@ -6,6 +6,7 @@ import os import sys from warnings import warn from functools import partial +import re # vim: ts=4 sw=4 et @@ -222,7 +223,7 @@ class MpvEventClientMessage(Structure): ('args', POINTER(c_char_p))] def as_dict(self): - return { 'args': [ self.args[i].value for i in range(self.num_args.value) ] } + return { 'args': [ self.args[i].decode('utf-8') for i in range(self.num_args) ] } WakeupCallback = CFUNCTYPE(None, c_void_p) @@ -333,7 +334,7 @@ def load_lua(): CDLL('liblua.so', mode=RTLD_GLOBAL) -def _event_loop(event_handle, playback_cond, event_callbacks, property_handlers, log_handler): +def _event_loop(event_handle, playback_cond, event_callbacks, message_handlers, property_handlers, log_handler): for event in _event_generator(event_handle): try: devent = event.as_dict() # copy data from ctypes @@ -353,12 +354,19 @@ def _event_loop(event_handle, playback_cond, event_callbacks, property_handlers, if eid == MpvEventID.LOG_MESSAGE and log_handler is not None: ev = devent['event'] log_handler(ev['level'], ev['prefix'], ev['text']) + if eid == MpvEventID.CLIENT_MESSAGE: + # {'event': {'args': ['key-binding', 'foo', 'u-', 'g']}, 'reply_userdata': 0, 'error': 0, 'event_id': 16} + target, *args = devent['event']['args'] + if target in message_handlers: + message_handlers[target](*args) for callback in event_callbacks: callback(devent) if eid == MpvEventID.SHUTDOWN: _mpv_detach_destroy(event_handle) return - except: + except Exception as e: + #import traceback + #traceback.print_exc() pass # It seems that when this thread runs into an exception, the MPV core is not able to terminate properly # anymore. FIXME @@ -380,12 +388,14 @@ class MPV(object): _mpv_set_option_string(self.handle, k.replace('_', '-').encode('utf-8'), istr(v).encode('utf-8')) _mpv_initialize(self.handle) - self.event_callbacks = [] + self._event_callbacks = [] self._property_handlers = {} + self._message_handlers = {} + self._key_binding_handlers = {} self._playback_cond = threading.Condition() - self._event_handle = _mpv_create_client(self.handle, b'mpv-python-event-handler-thread') - loop = partial(_event_loop, - self._event_handle, self._playback_cond, self.event_callbacks, self._property_handlers, log_handler) + self._event_handle = _mpv_create_client(self.handle, b'py_event_handler') + loop = partial(_event_loop, self._event_handle, self._playback_cond, self._event_callbacks, + self._message_handlers, self._property_handlers, log_handler) self._event_thread = threading.Thread(target=loop, name='MPVEventHandlerThread') self._event_thread.setDaemon(True) self._event_thread.start() @@ -527,6 +537,49 @@ class MPV(object): if handlerid in self._property_handlers: del self._property_handlers[handlerid] + def register_message_handler(self, target, handler): + self._message_handlers[target] = handler + + def unregister_message_handler(self, target): + del self._message_handlers[target] + + def register_event_callback(self, callback): + self._event_callbacks.append(callback) + + def unregister_event_callback(self, callback): + self._event_callbacks.remove(callback) + + @staticmethod + def _binding_name(callback): + return 'py_kb_{:016x}'.format(hash(callback)&0xffffffffffffffff) + + def register_key_binding(self, keydef, callback): + """ BIG FAT WARNING: mpv's key binding mechanism is pretty powerful. This means, you essentially get arbitrary + code exectution through key bindings. This interface makes some limited effort to sanitize the keydef given in + the first parameter, but YOU SHOULD NOT RELY ON THIS IN FOR SECURITY. If your input comes from config files, + this is completely fine--but, if you are about to pass untrusted input into this parameter, better double-check + whether this is secure in your case. """ + if not re.match(r'(Shift+)?(Ctrl+)?(Alt+)?(Meta+)?\w+', keydef): + raise ValueError('Invalid keydef. Expected format: [Shift+][Ctrl+][Alt+][Meta+]\n' + ' is either the literal character the key produces (ASCII or Unicode character), or a ' + 'symbolic name (as printed by --input-keylist') + binding_name = MPV._binding_name(callback) + self._key_binding_handlers[binding_name] = callback + print('Registering', binding_name) + self.command('define-section', + binding_name, '{} script-binding py_event_handler/{}'.format(keydef, binding_name), 'force') + self.command('enable-section', binding_name) + self.register_message_handler('key-binding', self._handle_key_binding_message) + + def _handle_key_binding_message(self, binding_name, key_state, key_name): + self._key_binding_handlers[binding_name](key_state, key_name) + + def unregister_key_binding(self, callback): + binding_name = MPV._binding_name(callback) + self.command('disable-section', binding_name) + self.command('define-section', binding_name, '') + del self._key_binding_handlers[binding_name] + # Convenience functions def play(self, filename): self.loadfile(filename)