Improve event handling, add message handling, add key binding foo
This commit is contained in:
parent
de7b671103
commit
ab8b8b5477
3 changed files with 73 additions and 12 deletions
|
|
@ -30,7 +30,7 @@ import mpv
|
||||||
def my_log(loglevel, component, message):
|
def my_log(loglevel, component, message):
|
||||||
print('[{}] {}: {}'.format(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
|
# Property access, these can be changed at runtime
|
||||||
player.observe_property('time-pos', lambda _property, pos: print('Now playing at {:.2f}s'.format(pos)))
|
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
|
# Option access, in general these require the core to reinitialize
|
||||||
player['vo'] = 'opengl'
|
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.play('https://youtu.be/DLzxrzFCyOs')
|
||||||
player.wait_for_playback()
|
player.wait_for_playback()
|
||||||
|
|
||||||
|
|
|
||||||
11
mpv-test.py
11
mpv-test.py
|
|
@ -175,17 +175,20 @@ class TestLifecycle(unittest.TestCase):
|
||||||
def test_event_callback(self):
|
def test_event_callback(self):
|
||||||
handler = mock.Mock()
|
handler = mock.Mock()
|
||||||
m = mpv.MPV('no-video')
|
m = mpv.MPV('no-video')
|
||||||
m.event_callbacks.append(handler)
|
m.register_event_callback(handler)
|
||||||
m.play(TESTVID)
|
m.play(TESTVID)
|
||||||
m.wait_for_playback()
|
m.wait_for_playback()
|
||||||
del m
|
|
||||||
|
m.unregister_event_callback(handler)
|
||||||
handler.assert_has_calls([
|
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': 6, 'event': None}),
|
||||||
mock.call({'reply_userdata': 0, 'error': 0, 'event_id': 9, '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': 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)
|
], any_order=True)
|
||||||
|
handler.reset_mock()
|
||||||
|
|
||||||
|
del m
|
||||||
|
handler.assert_not_called()
|
||||||
|
|
||||||
def test_log_handler(self):
|
def test_log_handler(self):
|
||||||
handler = mock.Mock()
|
handler = mock.Mock()
|
||||||
|
|
|
||||||
67
mpv.py
67
mpv.py
|
|
@ -6,6 +6,7 @@ import os
|
||||||
import sys
|
import sys
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
import re
|
||||||
|
|
||||||
# vim: ts=4 sw=4 et
|
# vim: ts=4 sw=4 et
|
||||||
|
|
||||||
|
|
@ -222,7 +223,7 @@ class MpvEventClientMessage(Structure):
|
||||||
('args', POINTER(c_char_p))]
|
('args', POINTER(c_char_p))]
|
||||||
|
|
||||||
def as_dict(self):
|
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)
|
WakeupCallback = CFUNCTYPE(None, c_void_p)
|
||||||
|
|
||||||
|
|
@ -333,7 +334,7 @@ def load_lua():
|
||||||
CDLL('liblua.so', mode=RTLD_GLOBAL)
|
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):
|
for event in _event_generator(event_handle):
|
||||||
try:
|
try:
|
||||||
devent = event.as_dict() # copy data from ctypes
|
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:
|
if eid == MpvEventID.LOG_MESSAGE and log_handler is not None:
|
||||||
ev = devent['event']
|
ev = devent['event']
|
||||||
log_handler(ev['level'], ev['prefix'], ev['text'])
|
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:
|
for callback in event_callbacks:
|
||||||
callback(devent)
|
callback(devent)
|
||||||
if eid == MpvEventID.SHUTDOWN:
|
if eid == MpvEventID.SHUTDOWN:
|
||||||
_mpv_detach_destroy(event_handle)
|
_mpv_detach_destroy(event_handle)
|
||||||
return
|
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
|
pass # It seems that when this thread runs into an exception, the MPV core is not able to terminate properly
|
||||||
# anymore. FIXME
|
# 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_set_option_string(self.handle, k.replace('_', '-').encode('utf-8'), istr(v).encode('utf-8'))
|
||||||
_mpv_initialize(self.handle)
|
_mpv_initialize(self.handle)
|
||||||
|
|
||||||
self.event_callbacks = []
|
self._event_callbacks = []
|
||||||
self._property_handlers = {}
|
self._property_handlers = {}
|
||||||
|
self._message_handlers = {}
|
||||||
|
self._key_binding_handlers = {}
|
||||||
self._playback_cond = threading.Condition()
|
self._playback_cond = threading.Condition()
|
||||||
self._event_handle = _mpv_create_client(self.handle, b'mpv-python-event-handler-thread')
|
self._event_handle = _mpv_create_client(self.handle, b'py_event_handler')
|
||||||
loop = partial(_event_loop,
|
loop = partial(_event_loop, self._event_handle, self._playback_cond, self._event_callbacks,
|
||||||
self._event_handle, self._playback_cond, self.event_callbacks, self._property_handlers, log_handler)
|
self._message_handlers, self._property_handlers, log_handler)
|
||||||
self._event_thread = threading.Thread(target=loop, name='MPVEventHandlerThread')
|
self._event_thread = threading.Thread(target=loop, name='MPVEventHandlerThread')
|
||||||
self._event_thread.setDaemon(True)
|
self._event_thread.setDaemon(True)
|
||||||
self._event_thread.start()
|
self._event_thread.start()
|
||||||
|
|
@ -527,6 +537,49 @@ class MPV(object):
|
||||||
if handlerid in self._property_handlers:
|
if handlerid in self._property_handlers:
|
||||||
del self._property_handlers[handlerid]
|
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
|
# Convenience functions
|
||||||
def play(self, filename):
|
def play(self, filename):
|
||||||
self.loadfile(filename)
|
self.loadfile(filename)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue