BREAKING 💥 Improve property handling
This commit is contained in:
parent
be8d6897eb
commit
669c4bbfec
2 changed files with 65 additions and 34 deletions
|
|
@ -139,12 +139,12 @@ class ObservePropertyTest(unittest.TestCase):
|
||||||
self.assertEqual(m.loop, 'inf')
|
self.assertEqual(m.loop, 'inf')
|
||||||
|
|
||||||
time.sleep(0.02)
|
time.sleep(0.02)
|
||||||
m.unobserve_property(handler)
|
m.unobserve_property('loop', handler)
|
||||||
|
|
||||||
m.loop = 'no'
|
m.loop = 'no'
|
||||||
m.loop = 'inf'
|
m.loop = 'inf'
|
||||||
m.terminate() # needed for synchronization of event thread
|
m.terminate() # needed for synchronization of event thread
|
||||||
handler.assert_has_calls([mock.call('loop', 'no'), mock.call('loop', 'inf')])
|
handler.assert_has_calls([mock.call('no'), mock.call('inf')])
|
||||||
|
|
||||||
|
|
||||||
class TestLifecycle(unittest.TestCase):
|
class TestLifecycle(unittest.TestCase):
|
||||||
|
|
@ -197,7 +197,7 @@ class TestLifecycle(unittest.TestCase):
|
||||||
m.wait_for_playback()
|
m.wait_for_playback()
|
||||||
del m
|
del m
|
||||||
handler.assert_has_calls([
|
handler.assert_has_calls([
|
||||||
mock.call('info', 'cplayer', 'Playing: test.webm'),
|
mock.call('info', 'cplayer', 'Playing: ./test.webm'),
|
||||||
mock.call('info', 'cplayer', ' Video --vid=1 (*) (vp8)'),
|
mock.call('info', 'cplayer', ' Video --vid=1 (*) (vp8)'),
|
||||||
mock.call('fatal', 'cplayer', 'No video or audio streams selected.')])
|
mock.call('fatal', 'cplayer', 'No video or audio streams selected.')])
|
||||||
|
|
||||||
|
|
|
||||||
69
mpv.py
69
mpv.py
|
|
@ -6,7 +6,9 @@ import os
|
||||||
import sys
|
import sys
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
import collections
|
||||||
import re
|
import re
|
||||||
|
import traceback
|
||||||
|
|
||||||
# vim: ts=4 sw=4 et
|
# vim: ts=4 sw=4 et
|
||||||
|
|
||||||
|
|
@ -31,6 +33,9 @@ class MpvOpenGLCbContext(c_void_p):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyUnavailableError(AttributeError):
|
||||||
|
pass
|
||||||
|
|
||||||
class ErrorCode(object):
|
class ErrorCode(object):
|
||||||
""" For documentation on these, see mpv's libmpv/client.h """
|
""" For documentation on these, see mpv's libmpv/client.h """
|
||||||
SUCCESS = 0
|
SUCCESS = 0
|
||||||
|
|
@ -60,7 +65,7 @@ class ErrorCode(object):
|
||||||
# Currently (mpv 0.18.1) there is a bug causing a PROPERTY_FORMAT error to be returned instead of
|
# Currently (mpv 0.18.1) there is a bug causing a PROPERTY_FORMAT error to be returned instead of
|
||||||
# INVALID_PARAMETER when setting a property-mapped option to an invalid value.
|
# INVALID_PARAMETER when setting a property-mapped option to an invalid value.
|
||||||
-9: lambda *a: TypeError('Tried to get/set mpv property using wrong format, or passed invalid value', *a),
|
-9: lambda *a: TypeError('Tried to get/set mpv property using wrong format, or passed invalid value', *a),
|
||||||
-10: lambda *a: AttributeError('mpv property is not available', *a),
|
-10: lambda *a: PropertyUnavailableError('mpv property is not available', *a),
|
||||||
-11: lambda *a: RuntimeError('Generic error getting or setting mpv property', *a),
|
-11: lambda *a: RuntimeError('Generic error getting or setting mpv property', *a),
|
||||||
-12: lambda *a: SystemError('Error running mpv command', *a) }
|
-12: lambda *a: SystemError('Error running mpv command', *a) }
|
||||||
|
|
||||||
|
|
@ -88,6 +93,9 @@ class MpvFormat(c_int):
|
||||||
NODE_MAP = 8
|
NODE_MAP = 8
|
||||||
BYTE_ARRAY = 9
|
BYTE_ARRAY = 9
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self is other or self.value == other or self.value == int(other)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return ['NONE', 'STRING', 'OSD_STRING', 'FLAG', 'INT64', 'DOUBLE', 'NODE', 'NODE_ARRAY', 'NODE_MAP',
|
return ['NONE', 'STRING', 'OSD_STRING', 'FLAG', 'INT64', 'DOUBLE', 'NODE', 'NODE_ARRAY', 'NODE_MAP',
|
||||||
'BYTE_ARRAY'][self.value]
|
'BYTE_ARRAY'][self.value]
|
||||||
|
|
@ -125,6 +133,12 @@ class MpvEventID(c_int):
|
||||||
CLIENT_MESSAGE, VIDEO_RECONFIG, AUDIO_RECONFIG, METADATA_UPDATE, SEEK, PLAYBACK_RESTART, PROPERTY_CHANGE,
|
CLIENT_MESSAGE, VIDEO_RECONFIG, AUDIO_RECONFIG, METADATA_UPDATE, SEEK, PLAYBACK_RESTART, PROPERTY_CHANGE,
|
||||||
CHAPTER_CHANGE )
|
CHAPTER_CHANGE )
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return ['NONE', 'SHUTDOWN', 'LOG_MESSAGE', 'GET_PROPERTY_REPLY', 'SET_PROPERTY_REPLY', 'COMMAND_REPLY',
|
||||||
|
'START_FILE', 'END_FILE', 'FILE_LOADED', 'TRACKS_CHANGED', 'TRACK_SWITCHED', 'IDLE', 'PAUSE', 'UNPAUSE',
|
||||||
|
'TICK', 'SCRIPT_INPUT_DISPATCH', 'CLIENT_MESSAGE', 'VIDEO_RECONFIG', 'AUDIO_RECONFIG',
|
||||||
|
'METADATA_UPDATE', 'SEEK', 'PLAYBACK_RESTART', 'PROPERTY_CHANGE', 'CHAPTER_CHANGE'][self.value]
|
||||||
|
|
||||||
|
|
||||||
class MpvNodeList(Structure):
|
class MpvNodeList(Structure):
|
||||||
def array_value(self, decode_str=False):
|
def array_value(self, decode_str=False):
|
||||||
|
|
@ -349,14 +363,22 @@ def _event_loop(event_handle, playback_cond, event_callbacks, message_handlers,
|
||||||
with playback_cond:
|
with playback_cond:
|
||||||
playback_cond.notify_all()
|
playback_cond.notify_all()
|
||||||
if eid == MpvEventID.PROPERTY_CHANGE:
|
if eid == MpvEventID.PROPERTY_CHANGE:
|
||||||
pc, handlerid = devent['event'], devent['reply_userdata']&0Xffffffffffffffff
|
pc = devent['event']
|
||||||
if handlerid in property_handlers:
|
|
||||||
name = pc['name']
|
name = pc['name']
|
||||||
|
|
||||||
if 'value' in pc:
|
if 'value' in pc:
|
||||||
proptype, _access = ALL_PROPERTIES[name]
|
proptype, _access = ALL_PROPERTIES[name]
|
||||||
property_handlers[handlerid](name, proptype(_ensure_encoding(pc['value'])))
|
if proptype is bytes:
|
||||||
|
args = (pc['value'],)
|
||||||
else:
|
else:
|
||||||
property_handlers[handlerid](name, pc['data'], pc['format'])
|
args = (proptype(_ensure_encoding(pc['value'])),)
|
||||||
|
elif pc['format'] == MpvFormat.NONE:
|
||||||
|
args = (None,)
|
||||||
|
else:
|
||||||
|
args = (pc['data'], pc['format'])
|
||||||
|
|
||||||
|
for handler in property_handlers[name]:
|
||||||
|
handler(*args)
|
||||||
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'])
|
||||||
|
|
@ -371,10 +393,7 @@ def _event_loop(event_handle, playback_cond, event_callbacks, message_handlers,
|
||||||
_mpv_detach_destroy(event_handle)
|
_mpv_detach_destroy(event_handle)
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
#import traceback
|
traceback.print_exc()
|
||||||
#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
|
|
||||||
|
|
||||||
class MPV(object):
|
class MPV(object):
|
||||||
""" See man mpv(1) for the details of the implemented commands. """
|
""" See man mpv(1) for the details of the implemented commands. """
|
||||||
|
|
@ -395,7 +414,7 @@ class MPV(object):
|
||||||
_mpv_initialize(self.handle)
|
_mpv_initialize(self.handle)
|
||||||
|
|
||||||
self._event_callbacks = []
|
self._event_callbacks = []
|
||||||
self._property_handlers = {}
|
self._property_handlers = collections.defaultdict(lambda: [])
|
||||||
self._message_handlers = {}
|
self._message_handlers = {}
|
||||||
self._key_binding_handlers = {}
|
self._key_binding_handlers = {}
|
||||||
self._playback_cond = threading.Condition()
|
self._playback_cond = threading.Condition()
|
||||||
|
|
@ -414,6 +433,16 @@ class MPV(object):
|
||||||
with self._playback_cond:
|
with self._playback_cond:
|
||||||
self._playback_cond.wait()
|
self._playback_cond.wait()
|
||||||
|
|
||||||
|
def wait_for_property(self, name, cond=lambda val: val, level_sensitive=True):
|
||||||
|
sema = threading.Semaphore(value=0)
|
||||||
|
def observer(val):
|
||||||
|
if cond(val):
|
||||||
|
sema.release()
|
||||||
|
self.observe_property(name, observer)
|
||||||
|
if not level_sensitive or not cond(getattr(self, name.replace('-', '_'))):
|
||||||
|
sema.acquire()
|
||||||
|
self.unobserve_property(name, observer)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
if self.handle:
|
if self.handle:
|
||||||
self.terminate()
|
self.terminate()
|
||||||
|
|
@ -533,15 +562,14 @@ class MPV(object):
|
||||||
self.command('script_message_to', target, *args)
|
self.command('script_message_to', target, *args)
|
||||||
|
|
||||||
def observe_property(self, name, handler):
|
def observe_property(self, name, handler):
|
||||||
hashval = c_ulonglong(hash(handler))
|
self._property_handlers[name].append(handler)
|
||||||
self._property_handlers[hashval.value] = handler
|
_mpv_observe_property(self._event_handle, hash(name)&0xffffffffffffffff, name.encode('utf-8'), MpvFormat.STRING)
|
||||||
_mpv_observe_property(self._event_handle, hashval, name.encode('utf-8'), MpvFormat.STRING)
|
|
||||||
|
|
||||||
def unobserve_property(self, handler):
|
def unobserve_property(self, name, handler):
|
||||||
handlerid = hash(handler)
|
handlers = self._property_handlers[name]
|
||||||
_mpv_unobserve_property(self._event_handle, handlerid)
|
handlers.remove(handler)
|
||||||
if handlerid in self._property_handlers:
|
if not handlers:
|
||||||
del self._property_handlers[handlerid]
|
_mpv_unobserve_property(self._event_handle, hash(name)&0xffffffffffffffff)
|
||||||
|
|
||||||
def register_message_handler(self, target, handler):
|
def register_message_handler(self, target, handler):
|
||||||
self._message_handlers[target] = handler
|
self._message_handlers[target] = handler
|
||||||
|
|
@ -565,7 +593,7 @@ class MPV(object):
|
||||||
the first parameter, but YOU SHOULD NOT RELY ON THIS IN FOR SECURITY. If your input comes from config files,
|
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
|
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. """
|
whether this is secure in your case. """
|
||||||
if not re.match(r'(Shift+)?(Ctrl+)?(Alt+)?(Meta+)?\w+', keydef):
|
if not re.match(r'(Shift+)?(Ctrl+)?(Alt+)?(Meta+)?(.|\w+)', keydef):
|
||||||
raise ValueError('Invalid keydef. Expected format: [Shift+][Ctrl+][Alt+][Meta+]<key>\n'
|
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 '
|
'<key> is either the literal character the key produces (ASCII or Unicode character), or a '
|
||||||
'symbolic name (as printed by --input-keylist')
|
'symbolic name (as printed by --input-keylist')
|
||||||
|
|
@ -609,6 +637,7 @@ class MPV(object):
|
||||||
|
|
||||||
out = cast(create_string_buffer(sizeof(c_void_p)), c_void_p)
|
out = cast(create_string_buffer(sizeof(c_void_p)), c_void_p)
|
||||||
outptr = byref(out)
|
outptr = byref(out)
|
||||||
|
try:
|
||||||
cval = _mpv_get_property(self.handle, name.encode('utf-8'), fmt, outptr)
|
cval = _mpv_get_property(self.handle, name.encode('utf-8'), fmt, outptr)
|
||||||
rv = MpvNode.node_cast_value(outptr, fmt, decode_str or proptype in (str, commalist))
|
rv = MpvNode.node_cast_value(outptr, fmt, decode_str or proptype in (str, commalist))
|
||||||
|
|
||||||
|
|
@ -621,6 +650,8 @@ class MPV(object):
|
||||||
_mpv_free_node_contents(outptr)
|
_mpv_free_node_contents(outptr)
|
||||||
|
|
||||||
return rv
|
return rv
|
||||||
|
except PropertyUnavailableError as ex:
|
||||||
|
return None
|
||||||
|
|
||||||
def _set_property(self, name, value, proptype=str):
|
def _set_property(self, name, value, proptype=str):
|
||||||
ename = name.encode('utf-8')
|
ename = name.encode('utf-8')
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue