summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md7
-rwxr-xr-xmpv-test.py11
-rw-r--r--mpv.py67
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+]<key>\n'
+ '<key> 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)