1# -*- coding: utf-8 -*-
2# vim: ts=4 sw=4 et
3#
4# Python MPV library module
5# Copyright (C) 2017-2020 Sebastian Götte <code@jaseg.net>
6#
7# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
8# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
9# later version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
12# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
13# details.
14#
15# You should have received a copy of the GNU Affero General Public License along with this program.  If not, see
16# <http://www.gnu.org/licenses/>.
17#
18
19from ctypes import *
20import ctypes.util
21import threading
22import os
23import sys
24from warnings import warn
25from functools import partial, wraps
26from contextlib import contextmanager
27import collections
28import re
29import traceback
30
31if os.name == 'nt':
32    dll = ctypes.util.find_library('mpv-1.dll')
33    if dll is None:
34        raise OSError('Cannot find mpv-1.dll in your system %PATH%. One way to deal with this is to ship mpv-1.dll '
35                      'with your script and put the directory your script is in into %PATH% before "import mpv": '
36                      'os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"] '
37                      'If mpv-1.dll is located elsewhere, you can add that path to os.environ["PATH"].')
38    backend = CDLL(dll)
39    fs_enc = 'utf-8'
40else:
41    import locale
42    lc, enc = locale.getlocale(locale.LC_NUMERIC)
43    # libmpv requires LC_NUMERIC to be set to "C". Since messing with global variables everyone else relies upon is
44    # still better than segfaulting, we are setting LC_NUMERIC to "C".
45    locale.setlocale(locale.LC_NUMERIC, 'C')
46
47    sofile = ctypes.util.find_library('mpv')
48    if sofile is None:
49        raise OSError("Cannot find libmpv in the usual places. Depending on your distro, you may try installing an "
50                "mpv-devel or mpv-libs package. If you have libmpv around but this script can't find it, consult "
51                "the documentation for ctypes.util.find_library which this script uses to look up the library "
52                "filename.")
53    backend = CDLL(sofile)
54    fs_enc = sys.getfilesystemencoding()
55
56
57class ShutdownError(SystemError):
58    pass
59
60class MpvHandle(c_void_p):
61    pass
62
63class MpvRenderCtxHandle(c_void_p):
64    pass
65
66class MpvOpenGLCbContext(c_void_p):
67    pass
68
69
70class PropertyUnavailableError(AttributeError):
71    pass
72
73class ErrorCode(object):
74    """For documentation on these, see mpv's libmpv/client.h."""
75    SUCCESS                 = 0
76    EVENT_QUEUE_FULL        = -1
77    NOMEM                   = -2
78    UNINITIALIZED           = -3
79    INVALID_PARAMETER       = -4
80    OPTION_NOT_FOUND        = -5
81    OPTION_FORMAT           = -6
82    OPTION_ERROR            = -7
83    PROPERTY_NOT_FOUND      = -8
84    PROPERTY_FORMAT         = -9
85    PROPERTY_UNAVAILABLE    = -10
86    PROPERTY_ERROR          = -11
87    COMMAND                 = -12
88    LOADING_FAILED          = -13
89    AO_INIT_FAILED          = -14
90    VO_INIT_FAILED          = -15
91    NOTHING_TO_PLAY         = -16
92    UNKNOWN_FORMAT          = -17
93    UNSUPPORTED             = -18
94    NOT_IMPLEMENTED         = -19
95    GENERIC                 = -20
96
97    EXCEPTION_DICT = {
98             0:     None,
99            -1:     lambda *a: MemoryError('mpv event queue full', *a),
100            -2:     lambda *a: MemoryError('mpv cannot allocate memory', *a),
101            -3:     lambda *a: ValueError('Uninitialized mpv handle used', *a),
102            -4:     lambda *a: ValueError('Invalid value for mpv parameter', *a),
103            -5:     lambda *a: AttributeError('mpv option does not exist', *a),
104            -6:     lambda *a: TypeError('Tried to set mpv option using wrong format', *a),
105            -7:     lambda *a: ValueError('Invalid value for mpv option', *a),
106            -8:     lambda *a: AttributeError('mpv property does not exist', *a),
107            # Currently (mpv 0.18.1) there is a bug causing a PROPERTY_FORMAT error to be returned instead of
108            # INVALID_PARAMETER when setting a property-mapped option to an invalid value.
109            -9:     lambda *a: TypeError('Tried to get/set mpv property using wrong format, or passed invalid value', *a),
110            -10:    lambda *a: PropertyUnavailableError('mpv property is not available', *a),
111            -11:    lambda *a: RuntimeError('Generic error getting or setting mpv property', *a),
112            -12:    lambda *a: SystemError('Error running mpv command', *a),
113            -14:    lambda *a: RuntimeError('Initializing the audio output failed', *a),
114            -15:    lambda *a: RuntimeError('Initializing the video output failed'),
115            -16:    lambda *a: RuntimeError('There was no audio or video data to play. This also happens if the file '
116                                            'was recognized, but did not contain any audio or video streams, or no '
117                                            'streams were selected.'),
118            -17:    lambda *a: RuntimeError('When trying to load the file, the file format could not be determined, '
119                                            'or the file was too broken to open it'),
120            -18:    lambda *a: ValueError('Generic error for signaling that certain system requirements are not fulfilled'),
121            -19:    lambda *a: NotImplementedError('The API function which was called is a stub only'),
122            -20:    lambda *a: RuntimeError('Unspecified error') }
123
124    @staticmethod
125    def default_error_handler(ec, *args):
126        return ValueError(_mpv_error_string(ec).decode('utf-8'), ec, *args)
127
128    @classmethod
129    def raise_for_ec(kls, ec, func, *args):
130        ec = 0 if ec > 0 else ec
131        ex = kls.EXCEPTION_DICT.get(ec , kls.default_error_handler)
132        if ex:
133            raise ex(ec, *args)
134
135MpvGlGetProcAddressFn = CFUNCTYPE(c_void_p, c_void_p, c_char_p)
136class MpvOpenGLInitParams(Structure):
137    _fields_ = [('get_proc_address', MpvGlGetProcAddressFn),
138            ('get_proc_address_ctx', c_void_p),
139            ('extra_exts', c_void_p)]
140
141    def __init__(self, get_proc_address):
142        self.get_proc_address = get_proc_address
143        self.get_proc_address_ctx = None
144        self.extra_exts = None
145
146class MpvOpenGLFBO(Structure):
147    _fields_ = [('fbo', c_int),
148            ('w', c_int),
149            ('h', c_int),
150            ('internal_format', c_int)]
151
152    def __init__(self, w, h, fbo=0, internal_format=0):
153        self.w, self.h = w, h
154        self.fbo = fbo
155        self.internal_format = internal_format
156
157class MpvRenderFrameInfo(Structure):
158    _fields_ = [('flags', c_int64),
159            ('target_time', c_int64)]
160
161    def as_dict(self):
162        return {'flags': self.flags,
163                'target_time': self.target_time}
164
165class MpvOpenGLDRMParams(Structure):
166    _fields_ = [('fd', c_int),
167        ('crtc_id', c_int),
168        ('connector_id', c_int),
169        ('atomic_request_ptr', c_void_p),
170        ('render_fd', c_int)]
171
172class MpvOpenGLDRMDrawSurfaceSize(Structure):
173    _fields_ = [('width', c_int), ('height', c_int)]
174
175class MpvOpenGLDRMParamsV2(Structure):
176    _fields_ = [('fd', c_int),
177        ('crtc_id', c_int),
178        ('connector_id', c_int),
179        ('atomic_request_ptr', c_void_p),
180        ('render_fd', c_int)]
181
182    def __init__(self, crtc_id, connector_id, atomic_request_ptr, fd=-1, render_fd=-1):
183        self.crtc_id, self.connector_id = crtc_id, connector_id
184        self.atomic_request_ptr = atomic_request_ptr
185        self.fd, self.render_fd = fd, render_fd
186
187
188class MpvRenderParam(Structure):
189    _fields_ = [('type_id', c_int),
190                ('data', c_void_p)]
191
192    # maps human-readable type name to (type_id, argtype) tuple.
193    # The type IDs come from libmpv/render.h
194    TYPES = {"invalid"                 :(0, None),
195            "api_type"                 :(1, str),
196            "opengl_init_params"       :(2, MpvOpenGLInitParams),
197            "opengl_fbo"               :(3, MpvOpenGLFBO),
198            "flip_y"                   :(4, bool),
199            "depth"                    :(5, int),
200            "icc_profile"              :(6, bytes),
201            "ambient_light"            :(7, int),
202            "x11_display"              :(8, c_void_p),
203            "wl_display"               :(9, c_void_p),
204            "advanced_control"         :(10, bool),
205            "next_frame_info"          :(11, MpvRenderFrameInfo),
206            "block_for_target_time"    :(12, bool),
207            "skip_rendering"           :(13, bool),
208            "drm_display"              :(14, MpvOpenGLDRMParams),
209            "drm_draw_surface_size"    :(15, MpvOpenGLDRMDrawSurfaceSize),
210            "drm_display_v2"           :(16, MpvOpenGLDRMParamsV2)}
211
212    def __init__(self, name, value=None):
213        if name not in self.TYPES:
214            raise ValueError('unknown render param type "{}"'.format(name))
215        self.type_id, cons = self.TYPES[name]
216        if cons is None:
217            self.value = None
218            self.data = c_void_p()
219        elif cons is str:
220            self.value = value
221            self.data = cast(c_char_p(value.encode('utf-8')), c_void_p)
222        elif cons is bytes:
223            self.value = MpvByteArray(value)
224            self.data = cast(pointer(self.value), c_void_p)
225        elif cons is bool:
226            self.value = c_int(int(bool(value)))
227            self.data = cast(pointer(self.value), c_void_p)
228        else:
229            self.value = cons(**value)
230            self.data = cast(pointer(self.value), c_void_p)
231
232def kwargs_to_render_param_array(kwargs):
233    t = MpvRenderParam * (len(kwargs)+1)
234    return t(*kwargs.items(), ('invalid', None))
235
236class MpvFormat(c_int):
237    NONE        = 0
238    STRING      = 1
239    OSD_STRING  = 2
240    FLAG        = 3
241    INT64       = 4
242    DOUBLE      = 5
243    NODE        = 6
244    NODE_ARRAY  = 7
245    NODE_MAP    = 8
246    BYTE_ARRAY  = 9
247
248    def __eq__(self, other):
249        return self is other or self.value == other or self.value == int(other)
250
251    def __repr__(self):
252        return ['NONE', 'STRING', 'OSD_STRING', 'FLAG', 'INT64', 'DOUBLE', 'NODE', 'NODE_ARRAY', 'NODE_MAP',
253                'BYTE_ARRAY'][self.value]
254
255    def __hash__(self):
256        return self.value
257
258
259class MpvEventID(c_int):
260    NONE                    = 0
261    SHUTDOWN                = 1
262    LOG_MESSAGE             = 2
263    GET_PROPERTY_REPLY      = 3
264    SET_PROPERTY_REPLY      = 4
265    COMMAND_REPLY           = 5
266    START_FILE              = 6
267    END_FILE                = 7
268    FILE_LOADED             = 8
269    TRACKS_CHANGED          = 9
270    TRACK_SWITCHED          = 10
271    IDLE                    = 11
272    PAUSE                   = 12
273    UNPAUSE                 = 13
274    TICK                    = 14
275    SCRIPT_INPUT_DISPATCH   = 15
276    CLIENT_MESSAGE          = 16
277    VIDEO_RECONFIG          = 17
278    AUDIO_RECONFIG          = 18
279    METADATA_UPDATE         = 19
280    SEEK                    = 20
281    PLAYBACK_RESTART        = 21
282    PROPERTY_CHANGE         = 22
283    CHAPTER_CHANGE          = 23
284
285    ANY = ( SHUTDOWN, LOG_MESSAGE, GET_PROPERTY_REPLY, SET_PROPERTY_REPLY, COMMAND_REPLY, START_FILE, END_FILE,
286            FILE_LOADED, TRACKS_CHANGED, TRACK_SWITCHED, IDLE, PAUSE, UNPAUSE, TICK, SCRIPT_INPUT_DISPATCH,
287            CLIENT_MESSAGE, VIDEO_RECONFIG, AUDIO_RECONFIG, METADATA_UPDATE, SEEK, PLAYBACK_RESTART, PROPERTY_CHANGE,
288            CHAPTER_CHANGE )
289
290    def __repr__(self):
291        return ['NONE', 'SHUTDOWN', 'LOG_MESSAGE', 'GET_PROPERTY_REPLY', 'SET_PROPERTY_REPLY', 'COMMAND_REPLY',
292                'START_FILE', 'END_FILE', 'FILE_LOADED', 'TRACKS_CHANGED', 'TRACK_SWITCHED', 'IDLE', 'PAUSE', 'UNPAUSE',
293                'TICK', 'SCRIPT_INPUT_DISPATCH', 'CLIENT_MESSAGE', 'VIDEO_RECONFIG', 'AUDIO_RECONFIG',
294                'METADATA_UPDATE', 'SEEK', 'PLAYBACK_RESTART', 'PROPERTY_CHANGE', 'CHAPTER_CHANGE'][self.value]
295
296    @classmethod
297    def from_str(kls, s):
298        return getattr(kls, s.upper().replace('-', '_'))
299
300
301identity_decoder = lambda b: b
302strict_decoder = lambda b: b.decode('utf-8')
303def lazy_decoder(b):
304    try:
305        return b.decode('utf-8')
306    except UnicodeDecodeError:
307        return b
308
309class MpvNodeList(Structure):
310    def array_value(self, decoder=identity_decoder):
311        return [ self.values[i].node_value(decoder) for i in range(self.num) ]
312
313    def dict_value(self, decoder=identity_decoder):
314        return { self.keys[i].decode('utf-8'):
315                self.values[i].node_value(decoder) for i in range(self.num) }
316
317class MpvByteArray(Structure):
318    _fields_ = [('data', c_void_p),
319                ('size', c_size_t)]
320
321    def __init__(self, value):
322        self._value = value
323        self.data = cast(c_char_p(value), c_void_p)
324        self.size = len(value)
325
326    def bytes_value(self):
327        return cast(self.data, POINTER(c_char))[:self.size]
328
329class MpvNode(Structure):
330    def node_value(self, decoder=identity_decoder):
331        return MpvNode.node_cast_value(self.val, self.format.value, decoder)
332
333    @staticmethod
334    def node_cast_value(v, fmt=MpvFormat.NODE, decoder=identity_decoder):
335        if fmt == MpvFormat.NONE:
336            return None
337        elif fmt == MpvFormat.STRING:
338            return decoder(v.string)
339        elif fmt == MpvFormat.OSD_STRING:
340            return v.string.decode('utf-8')
341        elif fmt == MpvFormat.FLAG:
342            return bool(v.flag)
343        elif fmt == MpvFormat.INT64:
344            return v.int64
345        elif fmt == MpvFormat.DOUBLE:
346            return v.double
347        else:
348            if not v.node: # Check for null pointer
349                return None
350            if fmt == MpvFormat.NODE:
351                return v.node.contents.node_value(decoder)
352            elif fmt == MpvFormat.NODE_ARRAY:
353                return v.list.contents.array_value(decoder)
354            elif fmt == MpvFormat.NODE_MAP:
355                return v.map.contents.dict_value(decoder)
356            elif fmt == MpvFormat.BYTE_ARRAY:
357                return v.byte_array.contents.bytes_value()
358            else:
359                raise TypeError('Unknown MPV node format {}. Please submit a bug report.'.format(fmt))
360
361class MpvNodeUnion(Union):
362    _fields_ = [('string', c_char_p),
363                ('flag', c_int),
364                ('int64', c_int64),
365                ('double', c_double),
366                ('node', POINTER(MpvNode)),
367                ('list', POINTER(MpvNodeList)),
368                ('map', POINTER(MpvNodeList)),
369                ('byte_array', POINTER(MpvByteArray))]
370
371MpvNode._fields_ = [('val', MpvNodeUnion),
372                    ('format', MpvFormat)]
373
374MpvNodeList._fields_ = [('num', c_int),
375                        ('values', POINTER(MpvNode)),
376                        ('keys', POINTER(c_char_p))]
377
378class MpvSubApi(c_int):
379    MPV_SUB_API_OPENGL_CB   = 1
380
381class MpvEvent(Structure):
382    _fields_ = [('event_id', MpvEventID),
383                ('error', c_int),
384                ('reply_userdata', c_ulonglong),
385                ('data', c_void_p)]
386
387    def as_dict(self, decoder=identity_decoder):
388        dtype = {MpvEventID.END_FILE:               MpvEventEndFile,
389                MpvEventID.PROPERTY_CHANGE:         MpvEventProperty,
390                MpvEventID.GET_PROPERTY_REPLY:      MpvEventProperty,
391                MpvEventID.LOG_MESSAGE:             MpvEventLogMessage,
392                MpvEventID.SCRIPT_INPUT_DISPATCH:   MpvEventScriptInputDispatch,
393                MpvEventID.CLIENT_MESSAGE:          MpvEventClientMessage
394            }.get(self.event_id.value, None)
395        return {'event_id': self.event_id.value,
396                'error': self.error,
397                'reply_userdata': self.reply_userdata,
398                'event': cast(self.data, POINTER(dtype)).contents.as_dict(decoder=decoder) if dtype else None}
399
400class MpvEventProperty(Structure):
401    _fields_ = [('name', c_char_p),
402                ('format', MpvFormat),
403                ('data', MpvNodeUnion)]
404    def as_dict(self, decoder=identity_decoder):
405        value = MpvNode.node_cast_value(self.data, self.format.value, decoder)
406        return {'name': self.name.decode('utf-8'),
407                'format': self.format,
408                'data': self.data,
409                'value': value}
410
411class MpvEventLogMessage(Structure):
412    _fields_ = [('prefix', c_char_p),
413                ('level', c_char_p),
414                ('text', c_char_p)]
415
416    def as_dict(self, decoder=identity_decoder):
417        return { 'prefix': self.prefix.decode('utf-8'),
418                 'level':  self.level.decode('utf-8'),
419                 'text':   decoder(self.text).rstrip() }
420
421class MpvEventEndFile(Structure):
422    _fields_ = [('reason', c_int),
423                ('error', c_int)]
424
425    EOF                 = 0
426    RESTARTED           = 1
427    ABORTED             = 2
428    QUIT                = 3
429    ERROR               = 4
430    REDIRECT            = 5
431
432    # For backwards-compatibility
433    @property
434    def value(self):
435        return self.reason
436
437    def as_dict(self, decoder=identity_decoder):
438        return {'reason': self.reason, 'error': self.error}
439
440class MpvEventScriptInputDispatch(Structure):
441    _fields_ = [('arg0', c_int),
442                ('type', c_char_p)]
443
444    def as_dict(self, decoder=identity_decoder):
445        pass # TODO
446
447class MpvEventClientMessage(Structure):
448    _fields_ = [('num_args', c_int),
449                ('args', POINTER(c_char_p))]
450
451    def as_dict(self, decoder=identity_decoder):
452        return { 'args': [ self.args[i].decode('utf-8') for i in range(self.num_args) ] }
453
454StreamReadFn = CFUNCTYPE(c_int64, c_void_p, POINTER(c_char), c_uint64)
455StreamSeekFn = CFUNCTYPE(c_int64, c_void_p, c_int64)
456StreamSizeFn = CFUNCTYPE(c_int64, c_void_p)
457StreamCloseFn = CFUNCTYPE(None, c_void_p)
458StreamCancelFn = CFUNCTYPE(None, c_void_p)
459
460class StreamCallbackInfo(Structure):
461    _fields_ = [('cookie', c_void_p),
462                ('read', StreamReadFn),
463                ('seek', StreamSeekFn),
464                ('size', StreamSizeFn),
465                ('close', StreamCloseFn), ]
466#                ('cancel', StreamCancelFn)]
467
468StreamOpenFn = CFUNCTYPE(c_int, c_void_p, c_char_p, POINTER(StreamCallbackInfo))
469
470WakeupCallback = CFUNCTYPE(None, c_void_p)
471
472RenderUpdateFn = CFUNCTYPE(None, c_void_p)
473
474OpenGlCbUpdateFn = CFUNCTYPE(None, c_void_p)
475OpenGlCbGetProcAddrFn = CFUNCTYPE(c_void_p, c_void_p, c_char_p)
476
477def _handle_func(name, args, restype, errcheck, ctx=MpvHandle, deprecated=False):
478    func = getattr(backend, name)
479    func.argtypes = [ctx] + args if ctx else args
480    if restype is not None:
481        func.restype = restype
482    if errcheck is not None:
483        func.errcheck = errcheck
484    if deprecated:
485        @wraps(func)
486        def wrapper(*args, **kwargs):
487            if not wrapper.warned: # Only warn on first invocation to prevent spamming
488                warn("Backend C api has been deprecated: " + name, DeprecationWarning, stacklevel=2)
489                wrapper.warned = True
490            return func(*args, **kwargs)
491        wrapper.warned = False
492
493        globals()['_'+name] = wrapper
494    else:
495        globals()['_'+name] = func
496
497def bytes_free_errcheck(res, func, *args):
498    notnull_errcheck(res, func, *args)
499    rv = cast(res, c_void_p).value
500    _mpv_free(res)
501    return rv
502
503def notnull_errcheck(res, func, *args):
504    if res is None:
505        raise RuntimeError('Underspecified error in MPV when calling {} with args {!r}: NULL pointer returned.'\
506                'Please consult your local debugger.'.format(func.__name__, args))
507    return res
508
509ec_errcheck = ErrorCode.raise_for_ec
510
511def _handle_gl_func(name, args=[], restype=None, deprecated=False):
512    _handle_func(name, args, restype, errcheck=None, ctx=MpvOpenGLCbContext, deprecated=deprecated)
513
514backend.mpv_client_api_version.restype = c_ulong
515def _mpv_client_api_version():
516    ver = backend.mpv_client_api_version()
517    return ver>>16, ver&0xFFFF
518
519backend.mpv_free.argtypes = [c_void_p]
520_mpv_free = backend.mpv_free
521
522backend.mpv_free_node_contents.argtypes = [c_void_p]
523_mpv_free_node_contents = backend.mpv_free_node_contents
524
525backend.mpv_create.restype = MpvHandle
526_mpv_create = backend.mpv_create
527
528_handle_func('mpv_create_client',           [c_char_p],                                 MpvHandle, notnull_errcheck)
529_handle_func('mpv_client_name',             [],                                         c_char_p, errcheck=None)
530_handle_func('mpv_initialize',              [],                                         c_int, ec_errcheck)
531_handle_func('mpv_detach_destroy',          [],                                         None, errcheck=None)
532_handle_func('mpv_terminate_destroy',       [],                                         None, errcheck=None)
533_handle_func('mpv_load_config_file',        [c_char_p],                                 c_int, ec_errcheck)
534_handle_func('mpv_get_time_us',             [],                                         c_ulonglong, errcheck=None)
535
536_handle_func('mpv_set_option',              [c_char_p, MpvFormat, c_void_p],            c_int, ec_errcheck)
537_handle_func('mpv_set_option_string',       [c_char_p, c_char_p],                       c_int, ec_errcheck)
538
539_handle_func('mpv_command',                 [POINTER(c_char_p)],                        c_int, ec_errcheck)
540_handle_func('mpv_command_string',          [c_char_p, c_char_p],                       c_int, ec_errcheck)
541_handle_func('mpv_command_async',           [c_ulonglong, POINTER(c_char_p)],           c_int, ec_errcheck)
542_handle_func('mpv_command_node',            [POINTER(MpvNode), POINTER(MpvNode)],       c_int, ec_errcheck)
543_handle_func('mpv_command_async',           [c_ulonglong, POINTER(MpvNode)],            c_int, ec_errcheck)
544
545_handle_func('mpv_set_property',            [c_char_p, MpvFormat, c_void_p],            c_int, ec_errcheck)
546_handle_func('mpv_set_property_string',     [c_char_p, c_char_p],                       c_int, ec_errcheck)
547_handle_func('mpv_set_property_async',      [c_ulonglong, c_char_p, MpvFormat,c_void_p],c_int, ec_errcheck)
548_handle_func('mpv_get_property',            [c_char_p, MpvFormat, c_void_p],            c_int, ec_errcheck)
549_handle_func('mpv_get_property_string',     [c_char_p],                                 c_void_p, bytes_free_errcheck)
550_handle_func('mpv_get_property_osd_string', [c_char_p],                                 c_void_p, bytes_free_errcheck)
551_handle_func('mpv_get_property_async',      [c_ulonglong, c_char_p, MpvFormat],         c_int, ec_errcheck)
552_handle_func('mpv_observe_property',        [c_ulonglong, c_char_p, MpvFormat],         c_int, ec_errcheck)
553_handle_func('mpv_unobserve_property',      [c_ulonglong],                              c_int, ec_errcheck)
554
555_handle_func('mpv_event_name',              [c_int],                                    c_char_p, errcheck=None, ctx=None)
556_handle_func('mpv_error_string',            [c_int],                                    c_char_p, errcheck=None, ctx=None)
557
558_handle_func('mpv_request_event',           [MpvEventID, c_int],                        c_int, ec_errcheck)
559_handle_func('mpv_request_log_messages',    [c_char_p],                                 c_int, ec_errcheck)
560_handle_func('mpv_wait_event',              [c_double],                                 POINTER(MpvEvent), errcheck=None)
561_handle_func('mpv_wakeup',                  [],                                         None, errcheck=None)
562_handle_func('mpv_set_wakeup_callback',     [WakeupCallback, c_void_p],                 None, errcheck=None)
563_handle_func('mpv_get_wakeup_pipe',         [],                                         c_int, errcheck=None)
564
565_handle_func('mpv_stream_cb_add_ro',        [c_char_p, c_void_p, StreamOpenFn],         c_int, ec_errcheck)
566
567_handle_func('mpv_render_context_create',               [MpvRenderCtxHandle, MpvHandle, POINTER(MpvRenderParam)],   c_int, ec_errcheck,     ctx=None)
568_handle_func('mpv_render_context_set_parameter',        [MpvRenderParam],                                           c_int, ec_errcheck,     ctx=MpvRenderCtxHandle)
569_handle_func('mpv_render_context_get_info',             [MpvRenderParam],                                           c_int, ec_errcheck,     ctx=MpvRenderCtxHandle)
570_handle_func('mpv_render_context_set_update_callback',  [RenderUpdateFn, c_void_p],                                 None, errcheck=None,    ctx=MpvRenderCtxHandle)
571_handle_func('mpv_render_context_update',               [],                                                         c_int64, errcheck=None, ctx=MpvRenderCtxHandle)
572_handle_func('mpv_render_context_render',               [POINTER(MpvRenderParam)],                                  c_int, ec_errcheck,     ctx=MpvRenderCtxHandle)
573_handle_func('mpv_render_context_report_swap',          [],                                                         None, errcheck=None,    ctx=MpvRenderCtxHandle)
574_handle_func('mpv_render_context_free',                 [],                                                         None, errcheck=None,    ctx=MpvRenderCtxHandle)
575
576
577# Deprecated in v0.29.0 and may disappear eventually
578if hasattr(backend, 'mpv_get_sub_api'):
579    _handle_func('mpv_get_sub_api',             [MpvSubApi],                                c_void_p, notnull_errcheck, deprecated=True)
580
581    _handle_gl_func('mpv_opengl_cb_set_update_callback',    [OpenGlCbUpdateFn, c_void_p], deprecated=True)
582    _handle_gl_func('mpv_opengl_cb_init_gl',                [c_char_p, OpenGlCbGetProcAddrFn, c_void_p],    c_int, deprecated=True)
583    _handle_gl_func('mpv_opengl_cb_draw',                   [c_int, c_int, c_int],                          c_int, deprecated=True)
584    _handle_gl_func('mpv_opengl_cb_render',                 [c_int, c_int],                                 c_int, deprecated=True)
585    _handle_gl_func('mpv_opengl_cb_report_flip',            [c_ulonglong],                                  c_int, deprecated=True)
586    _handle_gl_func('mpv_opengl_cb_uninit_gl',              [],                                             c_int, deprecated=True)
587
588
589def _mpv_coax_proptype(value, proptype=str):
590    """Intelligently coax the given python value into something that can be understood as a proptype property."""
591    if type(value) is bytes:
592        return value;
593    elif type(value) is bool:
594        return b'yes' if value else b'no'
595    elif proptype in (str, int, float):
596        return str(proptype(value)).encode('utf-8')
597    else:
598        raise TypeError('Cannot coax value of type {} into property type {}'.format(type(value), proptype))
599
600def _make_node_str_list(l):
601    """Take a list of python objects and make a MPV string node array from it.
602
603    As an example, the python list ``l = [ "foo", 23, false ]`` will result in the following MPV node object::
604
605        struct mpv_node {
606            .format = MPV_NODE_ARRAY,
607            .u.list = *(struct mpv_node_array){
608                .num = len(l),
609                .keys = NULL,
610                .values = struct mpv_node[len(l)] {
611                    { .format = MPV_NODE_STRING, .u.string = l[0] },
612                    { .format = MPV_NODE_STRING, .u.string = l[1] },
613                    ...
614                }
615            }
616        }
617    """
618    char_ps = [ c_char_p(_mpv_coax_proptype(e, str)) for e in l ]
619    node_list = MpvNodeList(
620        num=len(l),
621        keys=None,
622        values=( MpvNode * len(l))( *[ MpvNode(
623                format=MpvFormat.STRING,
624                val=MpvNodeUnion(string=p))
625            for p in char_ps ]))
626    node = MpvNode(
627        format=MpvFormat.NODE_ARRAY,
628        val=MpvNodeUnion(list=pointer(node_list)))
629    return char_ps, node_list, node, cast(pointer(node), c_void_p)
630
631
632def _event_generator(handle):
633    while True:
634        event = _mpv_wait_event(handle, -1).contents
635        if event.event_id.value == MpvEventID.NONE:
636            raise StopIteration()
637        yield event
638
639
640_py_to_mpv = lambda name: name.replace('_', '-')
641_mpv_to_py = lambda name: name.replace('-', '_')
642
643_drop_nones = lambda *args: [ arg for arg in args if arg is not None ]
644
645class _Proxy:
646    def __init__(self, mpv):
647        super().__setattr__('mpv', mpv)
648
649class _PropertyProxy(_Proxy):
650    def __dir__(self):
651        return super().__dir__() + [ name.replace('-', '_') for name in self.mpv.property_list ]
652
653class _FileLocalProxy(_Proxy):
654    def __getitem__(self, name):
655        return self.mpv.__getitem__(name, file_local=True)
656
657    def __setitem__(self, name, value):
658        return self.mpv.__setitem__(name, value, file_local=True)
659
660    def __iter__(self):
661        return iter(self.mpv)
662
663class _OSDPropertyProxy(_PropertyProxy):
664    def __getattr__(self, name):
665        return self.mpv._get_property(_py_to_mpv(name), fmt=MpvFormat.OSD_STRING)
666
667    def __setattr__(self, _name, _value):
668        raise AttributeError('OSD properties are read-only. Please use the regular property API for writing.')
669
670class _DecoderPropertyProxy(_PropertyProxy):
671    def __init__(self, mpv, decoder):
672        super().__init__(mpv)
673        super().__setattr__('_decoder', decoder)
674
675    def __getattr__(self, name):
676        return self.mpv._get_property(_py_to_mpv(name), decoder=self._decoder)
677
678    def __setattr__(self, name, value):
679        setattr(self.mpv, _py_to_mpv(name), value)
680
681class GeneratorStream:
682    """Transform a python generator into an mpv-compatible stream object. This only supports size() and read(), and
683    does not support seek(), close() or cancel().
684    """
685
686    def __init__(self, generator_fun, size=None):
687        self._generator_fun = generator_fun
688        self.size = size
689
690    def seek(self, offset):
691        self._read_iter = iter(self._generator_fun())
692        self._read_chunk = b''
693        return 0 # We only support seeking to the first byte atm
694        # implementation in case seeking to arbitrary offsets would be necessary
695        # while offset > 0:
696        #     offset -= len(self.read(offset))
697        # return offset
698
699    def read(self, size):
700        if not self._read_chunk:
701            try:
702                self._read_chunk += next(self._read_iter)
703            except StopIteration:
704                return b''
705        rv, self._read_chunk = self._read_chunk[:size], self._read_chunk[size:]
706        return rv
707
708    def close(self):
709        self._read_iter = iter([]) # make next read() call return EOF
710
711    def cancel(self):
712        self._read_iter = iter([]) # make next read() call return EOF
713        # TODO?
714
715
716class ImageOverlay:
717    def __init__(self, m, overlay_id, img=None, pos=(0, 0)):
718        self.m = m
719        self.overlay_id = overlay_id
720        self.pos = pos
721        self._size = None
722        if img is not None:
723            self.update(img)
724
725    def update(self, img=None, pos=None):
726        from PIL import Image
727        if img is not None:
728            self.img = img
729        img = self.img
730
731        w, h = img.size
732        stride = w*4
733
734        if pos is not None:
735            self.pos = pos
736        x, y = self.pos
737
738        # Pre-multiply alpha channel
739        bg = Image.new('RGBA', (w, h),  (0, 0, 0, 0))
740        out = Image.alpha_composite(bg, img)
741
742        # Copy image to ctypes buffer
743        if img.size != self._size:
744            self._buf = create_string_buffer(w*h*4)
745            self._size = img.size
746
747        self._buf[:] = out.tobytes('raw', 'BGRA')
748        source = '&' + str(addressof(self._buf))
749
750        self.m.overlay_add(self.overlay_id, x, y, source, 0, 'bgra', w, h, stride)
751
752    def remove(self):
753        self.m.remove_overlay(self.overlay_id)
754
755
756class FileOverlay:
757    def __init__(self, m, overlay_id, filename=None, size=None, stride=None, pos=(0,0)):
758        self.m = m
759        self.overlay_id = overlay_id
760        self.pos = pos
761        self.size = size
762        self.stride = stride
763        if filename is not None:
764            self.update(filename)
765
766    def update(self, filename=None, size=None, stride=None, pos=None):
767        if filename is not None:
768            self.filename = filename
769
770        if pos is not None:
771            self.pos = pos
772
773        if size is not None:
774            self.size = size
775
776        if stride is not None:
777            self.stride = stride
778
779        x, y = self.pos
780        w, h = self.size
781        stride = self.stride or 4*w
782
783        self.m.overlay_add(self, self.overlay_id, x, y, self.filename, 0, 'bgra', w, h, stride)
784
785    def remove(self):
786        self.m.remove_overlay(self.overlay_id)
787
788
789class MPV(object):
790    """See man mpv(1) for the details of the implemented commands. All mpv properties can be accessed as
791    ``my_mpv.some_property`` and all mpv options can be accessed as ``my_mpv['some-option']``.
792
793    By default, properties are returned as decoded ``str`` and an error is thrown if the value does not contain valid
794    utf-8. To get a decoded ``str`` if possibly but ``bytes`` instead of an error if not, use
795    ``my_mpv.lazy.some_property``. To always get raw ``bytes``, use ``my_mpv.raw.some_property``.  To access a
796    property's decoded OSD value, use ``my_mpv.osd.some_property``.
797
798    To get API information on an option, use ``my_mpv.option_info('option-name')``. To get API information on a
799    property, use ``my_mpv.properties['property-name']``. Take care to use mpv's dashed-names instead of the
800    underscore_names exposed on the python object.
801
802    To make your program not barf hard the first time its used on a weird file system **always** access properties
803    containing file names or file tags through ``MPV.raw``.  """
804    def __init__(self, *extra_mpv_flags, log_handler=None, start_event_thread=True, loglevel=None, **extra_mpv_opts):
805        """Create an MPV instance.
806
807        Extra arguments and extra keyword arguments will be passed to mpv as options.
808        """
809
810        self.handle = _mpv_create()
811        self._event_thread = None
812        self._core_shutdown = False
813
814        _mpv_set_option_string(self.handle, b'audio-display', b'no')
815        istr = lambda o: ('yes' if o else 'no') if type(o) is bool else str(o)
816        try:
817            for flag in extra_mpv_flags:
818                _mpv_set_option_string(self.handle, flag.encode('utf-8'), b'')
819            for k,v in extra_mpv_opts.items():
820                _mpv_set_option_string(self.handle, k.replace('_', '-').encode('utf-8'), istr(v).encode('utf-8'))
821        finally:
822            _mpv_initialize(self.handle)
823
824        self.osd = _OSDPropertyProxy(self)
825        self.file_local = _FileLocalProxy(self)
826        self.raw    = _DecoderPropertyProxy(self, identity_decoder)
827        self.strict = _DecoderPropertyProxy(self, strict_decoder)
828        self.lazy   = _DecoderPropertyProxy(self, lazy_decoder)
829
830        self._event_callbacks = []
831        self._event_handler_lock = threading.Lock()
832        self._property_handlers = collections.defaultdict(lambda: [])
833        self._quit_handlers = set()
834        self._message_handlers = {}
835        self._key_binding_handlers = {}
836        self._event_handle = _mpv_create_client(self.handle, b'py_event_handler')
837        self._log_handler = log_handler
838        self._stream_protocol_cbs = {}
839        self._stream_protocol_frontends = collections.defaultdict(lambda: {})
840        self.register_stream_protocol('python', self._python_stream_open)
841        self._python_streams = {}
842        self._python_stream_catchall = None
843        self.overlay_ids = set()
844        self.overlays = {}
845        if loglevel is not None or log_handler is not None:
846            self.set_loglevel(loglevel or 'terminal-default')
847        if start_event_thread:
848            self._event_thread = threading.Thread(target=self._loop, name='MPVEventHandlerThread')
849            self._event_thread.setDaemon(True)
850            self._event_thread.start()
851        else:
852            self._event_thread = None
853
854    def _loop(self):
855        for event in _event_generator(self._event_handle):
856            try:
857                devent = event.as_dict(decoder=lazy_decoder) # copy data from ctypes
858                eid = devent['event_id']
859
860                with self._event_handler_lock:
861                    if eid == MpvEventID.SHUTDOWN:
862                        self._core_shutdown = True
863
864                for callback in self._event_callbacks:
865                    callback(devent)
866
867                if eid == MpvEventID.PROPERTY_CHANGE:
868                    pc = devent['event']
869                    name, value, _fmt = pc['name'], pc['value'], pc['format']
870                    for handler in self._property_handlers[name]:
871                        handler(name, value)
872
873                if eid == MpvEventID.LOG_MESSAGE and self._log_handler is not None:
874                    ev = devent['event']
875                    self._log_handler(ev['level'], ev['prefix'], ev['text'])
876
877                if eid == MpvEventID.CLIENT_MESSAGE:
878                    # {'event': {'args': ['key-binding', 'foo', 'u-', 'g']}, 'reply_userdata': 0, 'error': 0, 'event_id': 16}
879                    target, *args = devent['event']['args']
880                    if target in self._message_handlers:
881                        self._message_handlers[target](*args)
882
883                if eid == MpvEventID.SHUTDOWN:
884                    _mpv_detach_destroy(self._event_handle)
885                    return
886
887            except Exception as e:
888                print('Exception inside python-mpv event loop:', file=sys.stderr)
889                traceback.print_exc()
890
891    @property
892    def core_shutdown(self):
893        """Property indicating whether the core has been shut down. Possible causes for this are e.g. the `quit` command
894        or a user closing the mpv window."""
895        return self._core_shutdown
896
897    def check_core_alive(self):
898        """ This method can be used as a sanity check to tests whether the core is still alive at the time it is
899        called."""
900        if self._core_shutdown:
901            raise ShutdownError('libmpv core has been shutdown')
902
903    def wait_until_paused(self):
904        """Waits until playback of the current title is paused or done. Raises a ShutdownError if the core is shutdown while
905        waiting."""
906        self.wait_for_property('core-idle')
907
908    def wait_for_playback(self):
909        """Waits until playback of the current title is finished. Raises a ShutdownError if the core is shutdown while
910        waiting.
911        """
912        self.wait_for_event('end_file')
913
914    def wait_until_playing(self):
915        """Waits until playback of the current title has started. Raises a ShutdownError if the core is shutdown while
916        waiting."""
917        self.wait_for_property('core-idle', lambda idle: not idle)
918
919    def wait_for_property(self, name, cond=lambda val: val, level_sensitive=True):
920        """Waits until ``cond`` evaluates to a truthy value on the named property. This can be used to wait for
921        properties such as ``idle_active`` indicating the player is done with regular playback and just idling around.
922        Raises a ShutdownError when the core is shutdown while waiting.
923        """
924        with self.prepare_and_wait_for_property(name, cond, level_sensitive):
925            pass
926
927    def wait_for_shutdown(self):
928        '''Wait for core to shutdown (e.g. through quit() or terminate()).'''
929        sema = threading.Semaphore(value=0)
930
931        @self.event_callback('shutdown')
932        def shutdown_handler(event):
933            sema.release()
934
935        sema.acquire()
936        shutdown_handler.unregister_mpv_events()
937
938    @contextmanager
939    def prepare_and_wait_for_property(self, name, cond=lambda val: val, level_sensitive=True):
940        """Context manager that waits until ``cond`` evaluates to a truthy value on the named property. See
941        prepare_and_wait_for_event for usage.
942        Raises a ShutdownError when the core is shutdown while waiting.
943        """
944        sema = threading.Semaphore(value=0)
945
946        def observer(name, val):
947            if cond(val):
948                sema.release()
949        self.observe_property(name, observer)
950
951        @self.event_callback('shutdown')
952        def shutdown_handler(event):
953            sema.release()
954
955        yield
956        if not level_sensitive or not cond(getattr(self, name.replace('-', '_'))):
957            sema.acquire()
958
959        self.check_core_alive()
960
961        shutdown_handler.unregister_mpv_events()
962        self.unobserve_property(name, observer)
963
964    def wait_for_event(self, *event_types, cond=lambda evt: True):
965        """Waits for the indicated event(s). If cond is given, waits until cond(event) is true. Raises a ShutdownError
966        if the core is shutdown while waiting. This also happens when 'shutdown' is in event_types.
967        """
968        with self.prepare_and_wait_for_event(*event_types, cond=cond):
969            pass
970
971    @contextmanager
972    def prepare_and_wait_for_event(self, *event_types, cond=lambda evt: True):
973        """Context manager that waits for the indicated event(s) like wait_for_event after running. If cond is given,
974        waits until cond(event) is true. Raises a ShutdownError if the core is shutdown while waiting. This also happens
975        when 'shutdown' is in event_types.
976
977        Compared to wait_for_event this handles the case where a thread waits for an event it itself causes in a
978        thread-safe way. An example from the testsuite is:
979
980        with self.m.prepare_and_wait_for_event('client_message'):
981            self.m.keypress(key)
982
983        Using just wait_for_event it would be impossible to ensure the event is caught since it may already have been
984        handled in the interval between keypress(...) running and a subsequent wait_for_event(...) call.
985        """
986        sema = threading.Semaphore(value=0)
987
988        @self.event_callback('shutdown')
989        def shutdown_handler(event):
990            sema.release()
991
992        @self.event_callback(*event_types)
993        def target_handler(evt):
994            if cond(evt):
995                sema.release()
996
997        yield
998        sema.acquire()
999
1000        self.check_core_alive()
1001
1002        shutdown_handler.unregister_mpv_events()
1003        target_handler.unregister_mpv_events()
1004
1005    def __del__(self):
1006        if self.handle:
1007            self.terminate()
1008
1009    def terminate(self):
1010        """Properly terminates this player instance. Preferably use this instead of relying on python's garbage
1011        collector to cause this to be called from the object's destructor.
1012
1013        This method will detach the main libmpv handle and wait for mpv to shut down and the event thread to finish.
1014        """
1015        self.handle, handle = None, self.handle
1016        if threading.current_thread() is self._event_thread:
1017            raise UserWarning('terminate() should not be called from event thread (e.g. from a callback function). If '
1018                    'you want to terminate mpv from here, please call quit() instead, then sync the main thread '
1019                    'against the event thread using e.g. wait_for_shutdown(), then terminate() from the main thread. '
1020                    'This call has been transformed into a call to quit().')
1021            self.quit()
1022        else:
1023            _mpv_terminate_destroy(handle)
1024            if self._event_thread:
1025                self._event_thread.join()
1026
1027    def set_loglevel(self, level):
1028        """Set MPV's log level. This adjusts which output will be sent to this object's log handlers. If you just want
1029        mpv's regular terminal output, you don't need to adjust this but just need to pass a log handler to the MPV
1030        constructur such as ``MPV(log_handler=print)``.
1031
1032        Valid log levels are "no", "fatal", "error", "warn", "info", "v" "debug" and "trace". For details see your mpv's
1033        client.h header file.
1034        """
1035        _mpv_request_log_messages(self._event_handle, level.encode('utf-8'))
1036
1037    def command(self, name, *args):
1038        """Execute a raw command."""
1039        args = [name.encode('utf-8')] + [ (arg if type(arg) is bytes else str(arg).encode('utf-8'))
1040                for arg in args if arg is not None ] + [None]
1041        _mpv_command(self.handle, (c_char_p*len(args))(*args))
1042
1043    def node_command(self, name, *args, decoder=strict_decoder):
1044        _1, _2, _3, pointer = _make_node_str_list([name, *args])
1045        out = cast(create_string_buffer(sizeof(MpvNode)), POINTER(MpvNode))
1046        ppointer = cast(pointer, POINTER(MpvNode))
1047        _mpv_command_node(self.handle, ppointer, out)
1048        rv = out.contents.node_value(decoder=decoder)
1049        _mpv_free_node_contents(out)
1050        return rv
1051
1052    def seek(self, amount, reference="relative", precision="default-precise"):
1053        """Mapped mpv seek command, see man mpv(1)."""
1054        self.command('seek', amount, reference, precision)
1055
1056    def revert_seek(self):
1057        """Mapped mpv revert_seek command, see man mpv(1)."""
1058        self.command('revert_seek');
1059
1060    def frame_step(self):
1061        """Mapped mpv frame-step command, see man mpv(1)."""
1062        self.command('frame-step')
1063
1064    def frame_back_step(self):
1065        """Mapped mpv frame_back_step command, see man mpv(1)."""
1066        self.command('frame_back_step')
1067
1068    def property_add(self, name, value=1):
1069        """Add the given value to the property's value. On overflow or underflow, clamp the property to the maximum. If
1070        ``value`` is omitted, assume ``1``.
1071        """
1072        self.command('add', name, value)
1073
1074    def property_multiply(self, name, factor):
1075        """Multiply the value of a property with a numeric factor."""
1076        self.command('multiply', name, factor)
1077
1078    def cycle(self, name, direction='up'):
1079        """Cycle the given property. ``up`` and ``down`` set the cycle direction. On overflow, set the property back to
1080        the minimum, on underflow set it to the maximum. If ``up`` or ``down`` is omitted, assume ``up``.
1081        """
1082        self.command('cycle', name, direction)
1083
1084    def screenshot(self, includes='subtitles', mode='single'):
1085        """Mapped mpv screenshot command, see man mpv(1)."""
1086        self.command('screenshot', includes, mode)
1087
1088    def screenshot_to_file(self, filename, includes='subtitles'):
1089        """Mapped mpv screenshot_to_file command, see man mpv(1)."""
1090        self.command('screenshot_to_file', filename.encode(fs_enc), includes)
1091
1092    def screenshot_raw(self, includes='subtitles'):
1093        """Mapped mpv screenshot_raw command, see man mpv(1). Returns a pillow Image object."""
1094        from PIL import Image
1095        res = self.node_command('screenshot-raw', includes)
1096        if res['format'] != 'bgr0':
1097            raise ValueError('Screenshot in unknown format "{}". Currently, only bgr0 is supported.'
1098                    .format(res['format']))
1099        img = Image.frombytes('RGBA', (res['stride']//4, res['h']), res['data'])
1100        b,g,r,a = img.split()
1101        return Image.merge('RGB', (r,g,b))
1102
1103    def allocate_overlay_id(self):
1104        free_ids = set(range(64)) - self.overlay_ids
1105        if not free_ids:
1106            raise IndexError('All overlay IDs are in use')
1107        next_id, *_ = sorted(free_ids)
1108        self.overlay_ids.add(next_id)
1109        return next_id
1110
1111    def free_overlay_id(self, overlay_id):
1112        self.overlay_ids.remove(overlay_id)
1113
1114    def create_file_overlay(self, filename=None, size=None, stride=None, pos=(0,0)):
1115        overlay_id = self.allocate_overlay_id()
1116        overlay = FileOverlay(self, overlay_id, filename, size, stride, pos)
1117        self.overlays[overlay_id] = overlay
1118        return overlay
1119
1120    def create_image_overlay(self, img=None, pos=(0,0)):
1121        overlay_id = self.allocate_overlay_id()
1122        overlay = ImageOverlay(self, overlay_id, img, pos)
1123        self.overlays[overlay_id] = overlay
1124        return overlay
1125
1126    def remove_overlay(self, overlay_id):
1127        self.overlay_remove(overlay_id)
1128        self.free_overlay_id(overlay_id)
1129        del self.overlays[overlay_id]
1130
1131    def playlist_next(self, mode='weak'):
1132        """Mapped mpv playlist_next command, see man mpv(1)."""
1133        self.command('playlist_next', mode)
1134
1135    def playlist_prev(self, mode='weak'):
1136        """Mapped mpv playlist_prev command, see man mpv(1)."""
1137        self.command('playlist_prev', mode)
1138
1139    def playlist_play_index(self, idx):
1140        """Mapped mpv playlist-play-index command, see man mpv(1)."""
1141        self.command('playlist-play-index', idx)
1142
1143    @staticmethod
1144    def _encode_options(options):
1145        return ','.join('{}={}'.format(_py_to_mpv(str(key)), str(val)) for key, val in options.items())
1146
1147    def loadfile(self, filename, mode='replace', **options):
1148        """Mapped mpv loadfile command, see man mpv(1)."""
1149        self.command('loadfile', filename.encode(fs_enc), mode, MPV._encode_options(options))
1150
1151    def loadlist(self, playlist, mode='replace'):
1152        """Mapped mpv loadlist command, see man mpv(1)."""
1153        self.command('loadlist', playlist.encode(fs_enc), mode)
1154
1155    def playlist_clear(self):
1156        """Mapped mpv playlist_clear command, see man mpv(1)."""
1157        self.command('playlist_clear')
1158
1159    def playlist_remove(self, index='current'):
1160        """Mapped mpv playlist_remove command, see man mpv(1)."""
1161        self.command('playlist_remove', index)
1162
1163    def playlist_move(self, index1, index2):
1164        """Mapped mpv playlist_move command, see man mpv(1)."""
1165        self.command('playlist_move', index1, index2)
1166
1167    def playlist_shuffle(self):
1168        """Mapped mpv playlist-shuffle command, see man mpv(1)."""
1169        self.command('playlist-shuffle')
1170
1171    def playlist_unshuffle(self):
1172        """Mapped mpv playlist-unshuffle command, see man mpv(1)."""
1173        self.command('playlist-unshuffle')
1174
1175    def run(self, command, *args):
1176        """Mapped mpv run command, see man mpv(1)."""
1177        self.command('run', command, *args)
1178
1179    def quit(self, code=None):
1180        """Mapped mpv quit command, see man mpv(1)."""
1181        self.command('quit', code)
1182
1183    def quit_watch_later(self, code=None):
1184        """Mapped mpv quit_watch_later command, see man mpv(1)."""
1185        self.command('quit_watch_later', code)
1186
1187    def stop(self, keep_playlist=False):
1188        """Mapped mpv stop command, see man mpv(1)."""
1189        if keep_playlist:
1190            self.command('stop', 'keep-playlist')
1191        else:
1192            self.command('stop')
1193
1194    def audio_add(self, url, flags='select', title=None, lang=None):
1195        """Mapped mpv audio_add command, see man mpv(1)."""
1196        self.command('audio_add', url.encode(fs_enc), *_drop_nones(flags, title, lang))
1197
1198    def audio_remove(self, audio_id=None):
1199        """Mapped mpv audio_remove command, see man mpv(1)."""
1200        self.command('audio_remove', audio_id)
1201
1202    def audio_reload(self, audio_id=None):
1203        """Mapped mpv audio_reload command, see man mpv(1)."""
1204        self.command('audio_reload', audio_id)
1205
1206    def video_add(self, url, flags='select', title=None, lang=None):
1207        """Mapped mpv video_add command, see man mpv(1)."""
1208        self.command('video_add', url.encode(fs_enc), *_drop_nones(flags, title, lang))
1209
1210    def video_remove(self, video_id=None):
1211        """Mapped mpv video_remove command, see man mpv(1)."""
1212        self.command('video_remove', video_id)
1213
1214    def video_reload(self, video_id=None):
1215        """Mapped mpv video_reload command, see man mpv(1)."""
1216        self.command('video_reload', video_id)
1217
1218    def sub_add(self, url, flags='select', title=None, lang=None):
1219        """Mapped mpv sub_add command, see man mpv(1)."""
1220        self.command('sub_add', url.encode(fs_enc), *_drop_nones(flags, title, lang))
1221
1222    def sub_remove(self, sub_id=None):
1223        """Mapped mpv sub_remove command, see man mpv(1)."""
1224        self.command('sub_remove', sub_id)
1225
1226    def sub_reload(self, sub_id=None):
1227        """Mapped mpv sub_reload command, see man mpv(1)."""
1228        self.command('sub_reload', sub_id)
1229
1230    def sub_step(self, skip):
1231        """Mapped mpv sub_step command, see man mpv(1)."""
1232        self.command('sub_step', skip)
1233
1234    def sub_seek(self, skip):
1235        """Mapped mpv sub_seek command, see man mpv(1)."""
1236        self.command('sub_seek', skip)
1237
1238    def toggle_osd(self):
1239        """Mapped mpv osd command, see man mpv(1)."""
1240        self.command('osd')
1241
1242    def print_text(self, text):
1243        """Mapped mpv print-text command, see man mpv(1)."""
1244        self.command('print-text', text)
1245
1246    def show_text(self, string, duration='-1', level=None):
1247        """Mapped mpv show_text command, see man mpv(1)."""
1248        self.command('show_text', string, duration, level)
1249
1250    def expand_text(self, text):
1251        """Mapped mpv expand-text command, see man mpv(1)."""
1252        return self.node_command('expand-text', text)
1253
1254    def expand_path(self, path):
1255        """Mapped mpv expand-path command, see man mpv(1)."""
1256        return self.node_command('expand-path', path)
1257
1258    def show_progress(self):
1259        """Mapped mpv show_progress command, see man mpv(1)."""
1260        self.command('show_progress')
1261
1262    def rescan_external_files(self, mode='reselect'):
1263        """Mapped mpv rescan-external-files command, see man mpv(1)."""
1264        self.command('rescan-external-files', mode)
1265
1266    def discnav(self, command):
1267        """Mapped mpv discnav command, see man mpv(1)."""
1268        self.command('discnav', command)
1269
1270    def mouse(x, y, button=None, mode='single'):
1271        """Mapped mpv mouse command, see man mpv(1)."""
1272        if button is None:
1273            self.command('mouse', x, y, mode)
1274        else:
1275            self.command('mouse', x, y, button, mode)
1276
1277    def keypress(self, name):
1278        """Mapped mpv keypress command, see man mpv(1)."""
1279        self.command('keypress', name)
1280
1281    def keydown(self, name):
1282        """Mapped mpv keydown command, see man mpv(1)."""
1283        self.command('keydown', name)
1284
1285    def keyup(self, name=None):
1286        """Mapped mpv keyup command, see man mpv(1)."""
1287        if name is None:
1288            self.command('keyup')
1289        else:
1290            self.command('keyup', name)
1291
1292    def keybind(self, name, command):
1293        """Mapped mpv keybind command, see man mpv(1)."""
1294        self.command('keybind', name, command)
1295
1296    def write_watch_later_config(self):
1297        """Mapped mpv write_watch_later_config command, see man mpv(1)."""
1298        self.command('write_watch_later_config')
1299
1300    def overlay_add(self, overlay_id, x, y, file_or_fd, offset, fmt, w, h, stride):
1301        """Mapped mpv overlay_add command, see man mpv(1)."""
1302        self.command('overlay_add', overlay_id, x, y, file_or_fd, offset, fmt, w, h, stride)
1303
1304    def overlay_remove(self, overlay_id):
1305        """Mapped mpv overlay_remove command, see man mpv(1)."""
1306        self.command('overlay_remove', overlay_id)
1307
1308    def script_message(self, *args):
1309        """Mapped mpv script_message command, see man mpv(1)."""
1310        self.command('script_message', *args)
1311
1312    def script_message_to(self, target, *args):
1313        """Mapped mpv script_message_to command, see man mpv(1)."""
1314        self.command('script_message_to', target, *args)
1315
1316    def observe_property(self, name, handler):
1317        """Register an observer on the named property. An observer is a function that is called with the new property
1318        value every time the property's value is changed. The basic function signature is ``fun(property_name,
1319        new_value)`` with new_value being the decoded property value as a python object. This function can be used as a
1320        function decorator if no handler is given.
1321
1322        To unregister the observer, call either of ``mpv.unobserve_property(name, handler)``,
1323        ``mpv.unobserve_all_properties(handler)`` or the handler's ``unregister_mpv_properties`` attribute::
1324
1325            @player.observe_property('volume')
1326            def my_handler(new_volume, *):
1327                print("It's loud!", volume)
1328
1329            my_handler.unregister_mpv_properties()
1330
1331        exit_handler is a function taking no arguments that is called when the underlying mpv handle is terminated (e.g.
1332        from calling MPV.terminate() or issuing a "quit" input command).
1333        """
1334        self._property_handlers[name].append(handler)
1335        _mpv_observe_property(self._event_handle, hash(name)&0xffffffffffffffff, name.encode('utf-8'), MpvFormat.NODE)
1336
1337    def property_observer(self, name):
1338        """Function decorator to register a property observer. See ``MPV.observe_property`` for details."""
1339        def wrapper(fun):
1340            self.observe_property(name, fun)
1341            fun.unobserve_mpv_properties = lambda: self.unobserve_property(name, fun)
1342            return fun
1343        return wrapper
1344
1345    def unobserve_property(self, name, handler):
1346        """Unregister a property observer. This requires both the observed property's name and the handler function that
1347        was originally registered as one handler could be registered for several properties. To unregister a handler
1348        from *all* observed properties see ``unobserve_all_properties``.
1349        """
1350        self._property_handlers[name].remove(handler)
1351        if not self._property_handlers[name]:
1352            _mpv_unobserve_property(self._event_handle, hash(name)&0xffffffffffffffff)
1353
1354    def unobserve_all_properties(self, handler):
1355        """Unregister a property observer from *all* observed properties."""
1356        for name in self._property_handlers:
1357            self.unobserve_property(name, handler)
1358
1359    def register_message_handler(self, target, handler=None):
1360        """Register a mpv script message handler. This can be used to communicate with embedded lua scripts. Pass the
1361        script message target name this handler should be listening to and the handler function.
1362
1363        WARNING: Only one handler can be registered at a time for any given target.
1364
1365        To unregister the message handler, call its ``unregister_mpv_messages`` function::
1366
1367            player = mpv.MPV()
1368            @player.message_handler('foo')
1369            def my_handler(some, args):
1370                print(args)
1371
1372            my_handler.unregister_mpv_messages()
1373        """
1374        self._register_message_handler_internal(target, handler)
1375
1376    def _register_message_handler_internal(self, target, handler):
1377        self._message_handlers[target] = handler
1378
1379    def unregister_message_handler(self, target_or_handler):
1380        """Unregister a mpv script message handler for the given script message target name.
1381
1382        You can also call the ``unregister_mpv_messages`` function attribute set on the handler function when it is
1383        registered.
1384        """
1385        if isinstance(target_or_handler, str):
1386            del self._message_handlers[target_or_handler]
1387        else:
1388            for key, val in self._message_handlers.items():
1389                if val == target_or_handler:
1390                    del self._message_handlers[key]
1391
1392    def message_handler(self, target):
1393        """Decorator to register a mpv script message handler.
1394
1395        WARNING: Only one handler can be registered at a time for any given target.
1396
1397        To unregister the message handler, call its ``unregister_mpv_messages`` function::
1398
1399            player = mpv.MPV()
1400            @player.message_handler('foo')
1401            def my_handler(some, args):
1402                print(args)
1403
1404            my_handler.unregister_mpv_messages()
1405        """
1406        def register(handler):
1407            self._register_message_handler_internal(target, handler)
1408            handler.unregister_mpv_messages = lambda: self.unregister_message_handler(handler)
1409            return handler
1410        return register
1411
1412    def register_event_callback(self, callback):
1413        """Register a blanket event callback receiving all event types.
1414
1415        To unregister the event callback, call its ``unregister_mpv_events`` function::
1416
1417            player = mpv.MPV()
1418            @player.event_callback('shutdown')
1419            def my_handler(event):
1420                print('It ded.')
1421
1422            my_handler.unregister_mpv_events()
1423        """
1424        self._event_callbacks.append(callback)
1425
1426    def unregister_event_callback(self, callback):
1427        """Unregiser an event callback."""
1428        self._event_callbacks.remove(callback)
1429
1430    def event_callback(self, *event_types):
1431        """Function decorator to register a blanket event callback for the given event types. Event types can be given
1432        as str (e.g.  'start-file'), integer or MpvEventID object.
1433
1434        WARNING: Due to the way this is filtering events, this decorator cannot be chained with itself.
1435
1436        To unregister the event callback, call its ``unregister_mpv_events`` function::
1437
1438            player = mpv.MPV()
1439            @player.event_callback('shutdown')
1440            def my_handler(event):
1441                print('It ded.')
1442
1443            my_handler.unregister_mpv_events()
1444        """
1445        def register(callback):
1446            with self._event_handler_lock:
1447                self.check_core_alive()
1448                types = [MpvEventID.from_str(t) if isinstance(t, str) else t for t in event_types] or MpvEventID.ANY
1449                @wraps(callback)
1450                def wrapper(event, *args, **kwargs):
1451                    if event['event_id'] in types:
1452                        callback(event, *args, **kwargs)
1453                self._event_callbacks.append(wrapper)
1454                wrapper.unregister_mpv_events = partial(self.unregister_event_callback, wrapper)
1455                return wrapper
1456        return register
1457
1458    @staticmethod
1459    def _binding_name(callback_or_cmd):
1460        return 'py_kb_{:016x}'.format(hash(callback_or_cmd)&0xffffffffffffffff)
1461
1462    def on_key_press(self, keydef, mode='force'):
1463        """Function decorator to register a simplified key binding. The callback is called whenever the key given is
1464        *pressed*.
1465
1466        To unregister the callback function, you can call its ``unregister_mpv_key_bindings`` attribute::
1467
1468            player = mpv.MPV()
1469            @player.on_key_press('Q')
1470            def binding():
1471                print('blep')
1472
1473            binding.unregister_mpv_key_bindings()
1474
1475        WARNING: For a single keydef only a single callback/command can be registered at the same time. If you register
1476        a binding multiple times older bindings will be overwritten and there is a possibility of references leaking. So
1477        don't do that.
1478
1479        The BIG FAT WARNING regarding untrusted keydefs from the key_binding method applies here as well.
1480        """
1481        def register(fun):
1482            @self.key_binding(keydef, mode)
1483            @wraps(fun)
1484            def wrapper(state='p-', name=None, char=None):
1485                if state[0] in ('d', 'p'):
1486                    fun()
1487            return wrapper
1488        return register
1489
1490    def key_binding(self, keydef, mode='force'):
1491        """Function decorator to register a low-level key binding.
1492
1493        The callback function signature is ``fun(key_state, key_name)`` where ``key_state`` is either ``'U'`` for "key
1494        up" or ``'D'`` for "key down".
1495
1496        The keydef format is: ``[Shift+][Ctrl+][Alt+][Meta+]<key>`` where ``<key>`` is either the literal character the
1497        key produces (ASCII or Unicode character), or a symbolic name (as printed by ``mpv --input-keylist``).
1498
1499        To unregister the callback function, you can call its ``unregister_mpv_key_bindings`` attribute::
1500
1501            player = mpv.MPV()
1502            @player.key_binding('Q')
1503            def binding(state, name, char):
1504                print('blep')
1505
1506            binding.unregister_mpv_key_bindings()
1507
1508        WARNING: For a single keydef only a single callback/command can be registered at the same time. If you register
1509        a binding multiple times older bindings will be overwritten and there is a possibility of references leaking. So
1510        don't do that.
1511
1512        BIG FAT WARNING: mpv's key binding mechanism is pretty powerful.  This means, you essentially get arbitrary code
1513        exectution through key bindings. This interface makes some limited effort to sanitize the keydef given in the
1514        first parameter, but YOU SHOULD NOT RELY ON THIS IN FOR SECURITY. If your input comes from config files, this is
1515        completely fine--but, if you are about to pass untrusted input into this parameter, better double-check whether
1516        this is secure in your case.
1517        """
1518        def register(fun):
1519            fun.mpv_key_bindings = getattr(fun, 'mpv_key_bindings', []) + [keydef]
1520            def unregister_all():
1521                for keydef in fun.mpv_key_bindings:
1522                    self.unregister_key_binding(keydef)
1523            fun.unregister_mpv_key_bindings = unregister_all
1524
1525            self.register_key_binding(keydef, fun, mode)
1526            return fun
1527        return register
1528
1529    def register_key_binding(self, keydef, callback_or_cmd, mode='force'):
1530        """Register a key binding. This takes an mpv keydef and either a string containing a mpv command or a python
1531        callback function.  See ``MPV.key_binding`` for details.
1532        """
1533        if not re.match(r'(Shift+)?(Ctrl+)?(Alt+)?(Meta+)?(.|\w+)', keydef):
1534            raise ValueError('Invalid keydef. Expected format: [Shift+][Ctrl+][Alt+][Meta+]<key>\n'
1535                    '<key> is either the literal character the key produces (ASCII or Unicode character), or a '
1536                    'symbolic name (as printed by --input-keylist')
1537        binding_name = MPV._binding_name(keydef)
1538        if callable(callback_or_cmd):
1539            self._key_binding_handlers[binding_name] = callback_or_cmd
1540            self.register_message_handler('key-binding', self._handle_key_binding_message)
1541            self.command('define-section',
1542                    binding_name, '{} script-binding py_event_handler/{}'.format(keydef, binding_name), mode)
1543        elif isinstance(callback_or_cmd, str):
1544            self.command('define-section', binding_name, '{} {}'.format(keydef, callback_or_cmd), mode)
1545        else:
1546            raise TypeError('register_key_binding expects either an str with an mpv command or a python callable.')
1547        self.command('enable-section', binding_name, 'allow-hide-cursor+allow-vo-dragging')
1548
1549    def _handle_key_binding_message(self, binding_name, key_state, key_name=None, key_char=None):
1550        self._key_binding_handlers[binding_name](key_state, key_name, key_char)
1551
1552    def unregister_key_binding(self, keydef):
1553        """Unregister a key binding by keydef."""
1554        binding_name = MPV._binding_name(keydef)
1555        self.command('disable-section', binding_name)
1556        self.command('define-section', binding_name, '')
1557        if binding_name in self._key_binding_handlers:
1558            del self._key_binding_handlers[binding_name]
1559            if not self._key_binding_handlers:
1560                self.unregister_message_handler('key-binding')
1561
1562    def register_stream_protocol(self, proto, open_fn=None):
1563        """ Register a custom stream protocol as documented in libmpv/stream_cb.h:
1564            https://github.com/mpv-player/mpv/blob/master/libmpv/stream_cb.h
1565
1566            proto is the protocol scheme, e.g. "foo" for "foo://" urls.
1567
1568            This function can either be used with two parameters or it can be used as a decorator on the target
1569            function.
1570
1571            open_fn is a function taking an URI string and returning an mpv stream object.
1572            open_fn may raise a ValueError to signal libmpv the URI could not be opened.
1573
1574            The mpv stream protocol is as follows:
1575            class Stream:
1576                @property
1577                def size(self):
1578                    return None # unknown size
1579                    return size # int with size in bytes
1580
1581                def read(self, size):
1582                    ...
1583                    return read # non-empty bytes object with input
1584                    return b'' # empty byte object signals permanent EOF
1585
1586                def seek(self, pos):
1587                    return new_offset # integer with new byte offset. The new offset may be before the requested offset
1588                    in case an exact seek is inconvenient.
1589
1590                def close(self):
1591                    ...
1592
1593                # def cancel(self): (future API versions only)
1594                #     Abort a running read() or seek() operation
1595                #     ...
1596
1597        """
1598
1599        def decorator(open_fn):
1600            @StreamOpenFn
1601            def open_backend(_userdata, uri, cb_info):
1602                try:
1603                    frontend = open_fn(uri.decode('utf-8'))
1604                except ValueError:
1605                    return ErrorCode.LOADING_FAILED
1606
1607                def read_backend(_userdata, buf, bufsize):
1608                    data = frontend.read(bufsize)
1609                    for i in range(len(data)):
1610                        buf[i] = data[i]
1611                    return len(data)
1612
1613                cb_info.contents.cookie = None
1614                read = cb_info.contents.read = StreamReadFn(read_backend)
1615                close = cb_info.contents.close = StreamCloseFn(lambda _userdata: frontend.close())
1616
1617                seek, size, cancel = None, None, None
1618                if hasattr(frontend, 'seek'):
1619                    seek = cb_info.contents.seek = StreamSeekFn(lambda _userdata, offx: frontend.seek(offx))
1620                if hasattr(frontend, 'size') and frontend.size is not None:
1621                    size = cb_info.contents.size = StreamSizeFn(lambda _userdata: frontend.size)
1622
1623                # Future API versions only
1624                # if hasattr(frontend, 'cancel'):
1625                #     cb_info.contents.cancel = StreamCancelFn(lambda _userdata: frontend.cancel())
1626
1627                # keep frontend and callbacks in memory forever (TODO)
1628                frontend._registered_callbacks = [read, close, seek, size, cancel]
1629                self._stream_protocol_frontends[proto][uri] = frontend
1630                return 0
1631
1632            if proto in self._stream_protocol_cbs:
1633                raise KeyError('Stream protocol already registered')
1634            self._stream_protocol_cbs[proto] = [open_backend]
1635            _mpv_stream_cb_add_ro(self.handle, proto.encode('utf-8'), c_void_p(), open_backend)
1636
1637            return open_fn
1638
1639        if open_fn is not None:
1640            decorator(open_fn)
1641        return decorator
1642
1643    # Convenience functions
1644    def play(self, filename):
1645        """Play a path or URL (requires ``ytdl`` option to be set)."""
1646        self.loadfile(filename)
1647
1648    @property
1649    def playlist_filenames(self):
1650        """Return all playlist item file names/URLs as a list of strs."""
1651        return [element['filename'] for element in self.playlist]
1652
1653    def playlist_append(self, filename, **options):
1654        """Append a path or URL to the playlist. This does not start playing the file automatically. To do that, use
1655        ``MPV.loadfile(filename, 'append-play')``."""
1656        self.loadfile(filename, 'append', **options)
1657
1658    # "Python stream" logic. This is some porcelain for directly playing data from python generators.
1659
1660    def _python_stream_open(self, uri):
1661        """Internal handler for python:// protocol streams registered through @python_stream(...) and
1662        @python_stream_catchall
1663        """
1664        name, = re.fullmatch('python://(.*)', uri).groups()
1665
1666        if name in self._python_streams:
1667            generator_fun, size = self._python_streams[name]
1668        else:
1669            if self._python_stream_catchall is not None:
1670                generator_fun, size = self._python_stream_catchall(name)
1671            else:
1672                raise ValueError('Python stream name not found and no catch-all defined')
1673
1674        return GeneratorStream(generator_fun, size)
1675
1676    def python_stream(self, name=None, size=None):
1677        """Register a generator for the python stream with the given name.
1678
1679        name is the name, i.e. the part after the "python://" in the URI, that this generator is registered as.
1680        size is the total number of bytes in the stream (if known).
1681
1682        Any given name can only be registered once. The catch-all can also only be registered once. To unregister a
1683        stream, call the .unregister function set on the callback.
1684
1685        The generator signals EOF by returning, manually raising StopIteration or by yielding b'', an empty bytes
1686        object.
1687
1688        The generator may be called multiple times if libmpv seeks or loops.
1689
1690        See also: @mpv.python_stream_catchall
1691
1692        @mpv.python_stream('foobar')
1693        def reader():
1694            for chunk in chunks:
1695                yield chunk
1696        mpv.play('python://foobar')
1697        mpv.wait_for_playback()
1698        reader.unregister()
1699        """
1700        def register(cb):
1701            if name in self._python_streams:
1702                raise KeyError('Python stream name "{}" is already registered'.format(name))
1703            self._python_streams[name] = (cb, size)
1704            def unregister():
1705                if name not in self._python_streams or\
1706                        self._python_streams[name][0] is not cb: # This is just a basic sanity check
1707                    raise RuntimeError('Python stream has already been unregistered')
1708                del self._python_streams[name]
1709            cb.unregister = unregister
1710            return cb
1711        return register
1712
1713    def python_stream_catchall(self, cb):
1714        """ Register a catch-all python stream to be called when no name matches can be found. Use this decorator on a
1715        function that takes a name argument and returns a (generator, size) tuple (with size being None if unknown).
1716
1717        An invalid URI can be signalled to libmpv by raising a ValueError inside the callback.
1718
1719        See also: @mpv.python_stream(name, size)
1720
1721        @mpv.python_stream_catchall
1722        def catchall(name):
1723            if not name.startswith('foo'):
1724                raise ValueError('Unknown Name')
1725
1726            def foo_reader():
1727                with open(name, 'rb') as f:
1728                    while True:
1729                        chunk = f.read(1024)
1730                        if not chunk:
1731                            break
1732                        yield chunk
1733            return foo_reader, None
1734        mpv.play('python://foo23')
1735        mpv.wait_for_playback()
1736        catchall.unregister()
1737        """
1738        if self._python_stream_catchall is not None:
1739            raise KeyError('A catch-all python stream is already registered')
1740
1741        self._python_stream_catchall = cb
1742        def unregister():
1743            if self._python_stream_catchall is not cb:
1744                    raise RuntimeError('This catch-all python stream has already been unregistered')
1745            self._python_stream_catchall = None
1746        cb.unregister = unregister
1747        return cb
1748
1749    # Property accessors
1750    def _get_property(self, name, decoder=strict_decoder, fmt=MpvFormat.NODE):
1751        self.check_core_alive()
1752        out = create_string_buffer(sizeof(MpvNode))
1753        try:
1754            cval = _mpv_get_property(self.handle, name.encode('utf-8'), fmt, out)
1755
1756            if fmt is MpvFormat.OSD_STRING:
1757                return cast(out, POINTER(c_char_p)).contents.value.decode('utf-8')
1758            elif fmt is MpvFormat.NODE:
1759                rv = cast(out, POINTER(MpvNode)).contents.node_value(decoder=decoder)
1760                _mpv_free_node_contents(out)
1761                return rv
1762            else:
1763                raise TypeError('_get_property only supports NODE and OSD_STRING formats.')
1764        except PropertyUnavailableError as ex:
1765            return None
1766
1767    def _set_property(self, name, value):
1768        self.check_core_alive()
1769        ename = name.encode('utf-8')
1770        if isinstance(value, (list, set, dict)):
1771            _1, _2, _3, pointer = _make_node_str_list(value)
1772            _mpv_set_property(self.handle, ename, MpvFormat.NODE, pointer)
1773        else:
1774            _mpv_set_property_string(self.handle, ename, _mpv_coax_proptype(value))
1775
1776    def __getattr__(self, name):
1777        return self._get_property(_py_to_mpv(name), lazy_decoder)
1778
1779    def __setattr__(self, name, value):
1780            try:
1781                if name != 'handle' and not name.startswith('_'):
1782                    self._set_property(_py_to_mpv(name), value)
1783                else:
1784                    super().__setattr__(name, value)
1785            except AttributeError:
1786                super().__setattr__(name, value)
1787
1788    def __dir__(self):
1789        return super().__dir__() + [ name.replace('-', '_') for name in self.property_list ]
1790
1791    @property
1792    def properties(self):
1793        return { name: self.option_info(name) for name in self.property_list }
1794
1795    # Dict-like option access
1796    def __getitem__(self, name, file_local=False):
1797        """Get an option value."""
1798        prefix = 'file-local-options/' if file_local else 'options/'
1799        return self._get_property(prefix+name, lazy_decoder)
1800
1801    def __setitem__(self, name, value, file_local=False):
1802        """Set an option value."""
1803        prefix = 'file-local-options/' if file_local else 'options/'
1804        return self._set_property(prefix+name, value)
1805
1806    def __iter__(self):
1807        """Iterate over all option names."""
1808        return iter(self.options)
1809
1810    def option_info(self, name):
1811        """Get information on the given option."""
1812        try:
1813            return self._get_property('option-info/'+name)
1814        except AttributeError:
1815            return None
1816
1817class MpvRenderContext:
1818    def __init__(self, mpv, api_type, **kwargs):
1819        self._mpv = mpv
1820        kwargs['api_type'] = api_type
1821
1822        buf = cast(create_string_buffer(sizeof(MpvRenderCtxHandle)), POINTER(MpvRenderCtxHandle))
1823        _mpv_render_context_create(buf, mpv.handle, kwargs_to_render_param_array(kwargs))
1824        self._handle = buf.contents
1825
1826    def free(self):
1827        _mpv_render_context_free(self._handle)
1828
1829    def __setattr__(self, name, value):
1830        if name.startswith('_'):
1831            super().__setattr__(name, value)
1832
1833        elif name == 'update_cb':
1834            func = value if value else (lambda: None)
1835            self._update_cb = value
1836            self._update_fn_wrapper = RenderUpdateFn(lambda _userdata: func())
1837            _mpv_render_context_set_update_callback(self._handle, self._update_fn_wrapper, None)
1838
1839        else:
1840            param = MpvRenderParam(name, value)
1841            _mpv_render_context_set_parameter(self._handle, param)
1842
1843    def __getattr__(self, name):
1844        if name == 'update_cb':
1845            return self._update_cb
1846
1847        elif name == 'handle':
1848            return self._handle
1849
1850        param = MpvRenderParam(name)
1851        data_type = type(param.data.contents)
1852        buf = cast(create_string_buffer(sizeof(data_type)), POINTER(data_type))
1853        param.data = buf
1854        _mpv_render_context_get_info(self._handle, param)
1855        return buf.contents.as_dict()
1856
1857    def update(self):
1858        """ Calls mpv_render_context_update and returns the MPV_RENDER_UPDATE_FRAME flag (see render.h) """
1859        return bool(_mpv_render_context_update(self._handle) & 1)
1860
1861    def render(self, **kwargs):
1862        _mpv_render_context_render(self._handle, kwargs_to_render_param_array(kwargs))
1863
1864    def report_swap(self):
1865        _mpv_render_context_report_swap(self._handle)
1866
1867