Finally add node handling, fix ALL THE THINGS

* New node handling
 * Add remaining properties
 * Improve property type handling (no more ynbool!)
 * Add pröper option access
 * Add a whole bunch of tests
This commit is contained in:
jaseg 2016-08-13 19:07:44 +02:00
parent 4d6c17d342
commit de7b671103
4 changed files with 298 additions and 328 deletions

View file

@ -32,9 +32,12 @@ def my_log(loglevel, component, message):
player = mpv.MPV(log_handler=my_log, ytdl=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)))
player.fullscreen = True
player.loop = 'inf'
# Option access, in general these require the core to reinitialize
player['vo'] = 'opengl'
player.play('https://youtu.be/DLzxrzFCyOs')
player.wait_for_playback()

View file

@ -31,7 +31,7 @@ class TestProperties(unittest.TestCase):
self.m = mpv.MPV()
def test_sanity(self):
for name, (ptype, access) in mpv.ALL_PROPERTIES.items():
for name, (ptype, access, *_args) in mpv.ALL_PROPERTIES.items():
self.assertTrue('r' in access or 'w' in access)
self.assertRegex(name, '^[-0-9a-z]+$')
# Types and MpvFormat values
@ -59,7 +59,7 @@ class TestProperties(unittest.TestCase):
self.m.play(TESTVID)
while self.m.core_idle:
time.sleep(0.05)
for name, (ptype, access) in sorted(mpv.ALL_PROPERTIES.items()):
for name, (ptype, access, *_args) in sorted(mpv.ALL_PROPERTIES.items()):
if 'r' in access:
name = name.replace('-', '_')
with self.subTest(property_name=name):
@ -72,9 +72,14 @@ class TestProperties(unittest.TestCase):
self.assertEqual(type(rv), type(ptype()))
def test_write(self):
for name, (ptype, access) in sorted(mpv.ALL_PROPERTIES.items()):
self.m.loop = 'inf'
self.m.play(TESTVID)
while self.m.core_idle:
time.sleep(0.05)
for name, (ptype, access, *_args) in sorted(mpv.ALL_PROPERTIES.items()):
if 'w' in access:
name = name.replace('-', '_')
with self.subTest(property_name=name):
with self.swallow_mpv_errors([
mpv.ErrorCode.PROPERTY_UNAVAILABLE,
mpv.ErrorCode.PROPERTY_ERROR,
@ -100,6 +105,21 @@ class TestProperties(unittest.TestCase):
elif ptype == bool:
setattr(self.m, name, True)
setattr(self.m, name, False)
def test_option_read(self):
self.m.loop = 'inf'
self.m.play(TESTVID)
while self.m.core_idle:
time.sleep(0.05)
for name in sorted(self.m):
with self.subTest(option_name=name):
with self.swallow_mpv_errors([
mpv.ErrorCode.PROPERTY_UNAVAILABLE,
mpv.ErrorCode.PROPERTY_NOT_FOUND,
mpv.ErrorCode.PROPERTY_ERROR]):
self.m[name]
def tearDown(self):
del self.m
@ -176,8 +196,7 @@ class TestLifecycle(unittest.TestCase):
handler.assert_has_calls([
mock.call('info', 'cplayer', 'Playing: test.webm'),
mock.call('info', 'cplayer', ' Video --vid=1 (*) (vp8)'),
mock.call('fatal', 'cplayer', 'No video or audio streams selected.'),
mock.call('info', 'cplayer', '')])
mock.call('fatal', 'cplayer', 'No video or audio streams selected.')])
if __name__ == '__main__':

220
mpv.py
View file

@ -55,8 +55,7 @@ class ErrorCode(object):
-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),
-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) }
@staticmethod
def default_error_handler(ec, *args):
@ -121,34 +120,32 @@ class MpvEventID(c_int):
class MpvNodeList(Structure):
@property
def array_value(self):
return [ self.values[i].node_value for i in range(self.num) ]
def array_value(self, decode_str=False):
return [ self.values[i].node_value(decode_str) for i in range(self.num) ]
@property
def dict_value(self):
return { self.keys[i].decode('utf-8'): self.values[i].node_value for i in range(self.num) }
def dict_value(self, decode_str=False):
return { self.keys[i].decode('utf-8'): self.values[i].node_value(decode_str) for i in range(self.num) }
class MpvNode(Structure):
_fields_ = [('val', c_longlong),
('format', MpvFormat)]
@property
def node_value(self):
return MpvNode.node_cast_value(byref(c_void_p(self.val)), self.format.value)
def node_value(self, decode_str=False):
return MpvNode.node_cast_value(byref(c_void_p(self.val)), self.format.value, decode_str)
@staticmethod
def node_cast_value(v, fmt):
def node_cast_value(v, fmt, decode_str=False):
dwrap = lambda s: s.decode('utf-8') if decode_str else s
return {
MpvFormat.NONE: lambda v: None,
MpvFormat.STRING: lambda v: cast(v, POINTER(c_char_p)).contents.value, # We can't decode here as this might contain file names
MpvFormat.STRING: lambda v: dwrap(cast(v, POINTER(c_char_p)).contents.value),
MpvFormat.OSD_STRING: lambda v: cast(v, POINTER(c_char_p)).contents.value.decode('utf-8'),
MpvFormat.FLAG: lambda v: bool(cast(v, POINTER(c_int)).contents.value),
MpvFormat.INT64: lambda v: cast(v, POINTER(c_longlong)).contents.value,
MpvFormat.DOUBLE: lambda v: cast(v, POINTER(c_double)).contents.value,
MpvFormat.NODE: lambda v: cast(v, POINTER(MpvNode)).contents.node_value,
MpvFormat.NODE_ARRAY: lambda v: cast(v, POINTER(POINTER(MpvNodeList))).contents.contents.array_value,
MpvFormat.NODE_MAP: lambda v: cast(v, POINTER(POINTER(MpvNodeList))).contents.contents.dict_value,
MpvFormat.NODE: lambda v: cast(v, POINTER(MpvNode)).contents.node_value(decode_str),
MpvFormat.NODE_ARRAY: lambda v: cast(v, POINTER(POINTER(MpvNodeList))).contents.contents.array_value(decode_str),
MpvFormat.NODE_MAP: lambda v: cast(v, POINTER(POINTER(MpvNodeList))).contents.contents.dict_value(decode_str),
MpvFormat.BYTE_ARRAY: lambda v: cast(v, POINTER(c_char_p)).contents.value,
}[fmt](v)
@ -432,9 +429,6 @@ class MPV(object):
def frame_back_step(self):
self.command('frame_back_step')
def _set_property(self, name, value):
self.command('set_property', name, str(value))
def _add_property(self, name, value=None):
self.command('add_property', name, value)
@ -537,88 +531,58 @@ class MPV(object):
def play(self, filename):
self.loadfile(filename)
# Complex properties
# Property accessors
def _get_property(self, name, proptype=str, decode_str=False):
fmt = {int: MpvFormat.INT64,
float: MpvFormat.DOUBLE,
bool: MpvFormat.FLAG,
str: MpvFormat.STRING,
bytes: MpvFormat.STRING,
commalist: MpvFormat.STRING,
MpvFormat.NODE: MpvFormat.NODE}[proptype]
def _get_dict(self, prefix, props):
return { name: proptype(_ensure_encoding(_mpv_get_property_string(self.handle, (prefix+name).encode('utf-8')))) for name, proptype in props }
out = cast(create_string_buffer(sizeof(c_void_p)), c_void_p)
outptr = byref(out)
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))
def _get_list(self, prefix, props):
count = int(_ensure_encoding(_mpv_get_property_string(self.handle, (prefix+'count').encode('utf-8'))))
return [ self._get_dict(prefix+str(index)+'/', props) for index in range(count)]
if proptype is commalist:
rv = proptype(rv)
@property
def filtered_metadata(self):
raise NotImplementedError
if proptype is str:
_mpv_free(out)
elif proptype is MpvFormat.NODE:
_mpv_free_node_contents(outptr)
@property
def metadata(self):
raise NotImplementedError
return rv
@property
def chapter_metadata(self):
raise NotImplementedError
def _set_property(self, name, value, proptype=str):
ename = name.encode('utf-8')
if type(value) is bytes:
_mpv_set_property_string(self.handle, ename, value)
elif type(value) is bool:
_mpv_set_property_string(self.handle, ename, b'yes' if value else b'no')
elif proptype in (str, int, float):
_mpv_set_property_string(self.handle, ename, str(proptype(value)).encode('utf-8'))
else:
raise TypeError('Cannot set {} property {} to value of type {}'.format(proptype, name, type(value)))
@property
def vf_metadata(self):
raise NotImplementedError
# Dict-like option access
def __getitem__(self, name, file_local=False):
""" Get an option value """
prefix = 'file-local-options/' if file_local else 'options/'
return self._get_property(prefix+name)
@property
def af_metadata(self):
raise NotImplementedError
def __setitem__(self, name, value, file_local=False):
""" Get an option value """
prefix = 'file-local-options/' if file_local else 'options/'
return self._set_property(prefix+name, value)
@property
def edition_list(self):
raise NotImplementedError
def __iter__(self):
return iter(self.options)
@property
def _disc_titles(self):
raise NotImplementedError
@property
def audio_params(self):
raise NotImplementedError
@property
def audio_out_params(self):
raise NotImplementedError
@property
def audio_device_list(self):
raise NotImplementedError
@property
def video_frame_info(self):
raise NotImplementedError
@property
def vf(self):
raise NotImplementedError
@property
def af(self):
raise NotImplementedError
@property
def decoder_list(self):
raise NotImplementedError
@property
def encoder_list(self):
raise NotImplementedError
@property
def options(self):
raise NotImplementedError
@property
def file_local_options(self):
raise NotImplementedError
@property
def option_info(self):
raise NotImplementedError
# TODO: audio-device-list, decoder-list, encoder-list
def option_info(self, name):
return self._get_property('option-info/'+name)
def commalist(propval=''):
return str(propval).split(',')
@ -798,56 +762,40 @@ ALL_PROPERTIES = {
'display-fps': (float, 'r'), # access apparently misdocumented in the manpage
'estimated-display-fps': (float, 'r'),
'vsync-jitter': (float, 'r'),
'video-params': (node, 'r'),
'video-out-params': (node, 'r'),
'track-list': (node, 'r'),
'playlist': (node, 'r'),
'chapter-list': (node, 'r'),
'vo-performance': (node, 'r'),
'video-params': (node, 'r', True),
'video-out-params': (node, 'r', True),
'track-list': (node, 'r', False),
'playlist': (node, 'r', False),
'chapter-list': (node, 'r', False),
'vo-performance': (node, 'r', True),
'filtered-metadata': (node, 'r', False),
'metadata': (node, 'r', False),
'chapter-metadata': (node, 'r', False),
'vf-metadata': (node, 'r', False),
'af-metadata': (node, 'r', False),
'edition-list': (node, 'r', False),
'disc-titles': (node, 'r', False),
'audio-params': (node, 'r', True),
'audio-out-params': (node, 'r', True),
'audio-device-list': (node, 'r', True),
'video-frame-info': (node, 'r', True),
'decoder-list': (node, 'r', True),
'encoder-list': (node, 'r', True),
'vf': (node, 'r', True),
'af': (node, 'r', True),
'options': (node, 'r', True),
'file-local-options': (node, 'r', True),
'property-list': (commalist,'r')}
def bindproperty(MPV, name, proptype, access):
def getter(self):
fmt = {int: MpvFormat.INT64,
float: MpvFormat.DOUBLE,
bool: MpvFormat.FLAG,
str: MpvFormat.STRING,
bytes: MpvFormat.STRING,
commalist: MpvFormat.STRING,
MpvFormat.NODE: MpvFormat.NODE}[proptype]
out = cast(create_string_buffer(sizeof(c_void_p)), c_void_p)
outptr = byref(out)
cval = _mpv_get_property(self.handle, name.encode('utf-8'), fmt, outptr)
rv = MpvNode.node_cast_value(outptr, fmt)
if proptype is str:
rv = rv.decode('utf-8')
elif proptype is commalist:
rv = proptype(rv.decode('utf-8'))
if proptype is str:
_mpv_free(out)
elif proptype is MpvFormat.NODE:
_mpv_free_node_contents(outptr)
return rv
def setter(self, value):
ename = name.encode('utf-8')
if type(value) is bytes:
_mpv_set_property_string(self.handle, ename, value)
elif type(value) is bool:
_mpv_set_property_string(self.handle, ename, b'yes' if value else b'no')
elif proptype in (str, int, float):
_mpv_set_property_string(self.handle, ename, str(proptype(value)).encode('utf-8'))
else:
raise TypeError('Cannot set {} property {} to value of type {}'.format(proptype, name, type(value)))
def bindproperty(MPV, name, proptype, access, decode_str=False):
getter = lambda self: self._get_property(name, proptype, decode_str)
setter = lambda self, value: self._set_property(name, value, proptype)
def barf(*args):
raise NotImplementedError('Access denied')
setattr(MPV, name.replace('-', '_'), property(getter if 'r' in access else barf, setter if 'w' in access else barf))
for name, (proptype, access) in ALL_PROPERTIES.items():
bindproperty(MPV, name, proptype, access)
for name, (proptype, access, *args) in ALL_PROPERTIES.items():
bindproperty(MPV, name, proptype, access, *args)

BIN
test.webm Normal file

Binary file not shown.