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