boris-behav-obs 8.9.16__py3-none-any.whl → 9.7.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of boris-behav-obs might be problematic. Click here for more details.

Files changed (129) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +36 -39
  4. boris/add_modifier.py +122 -109
  5. boris/add_modifier_ui.py +239 -135
  6. boris/advanced_event_filtering.py +81 -45
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_latency.py +59 -0
  9. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  10. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  11. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  13. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  14. boris/analysis_plugins/number_of_occurences.py +22 -0
  15. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  16. boris/analysis_plugins/time_budget.py +61 -0
  17. boris/behav_coding_map_creator.py +228 -229
  18. boris/behavior_binary_table.py +33 -50
  19. boris/behaviors_coding_map.py +17 -18
  20. boris/boris_cli.py +6 -25
  21. boris/cmd_arguments.py +12 -1
  22. boris/coding_pad.py +42 -49
  23. boris/config.py +161 -77
  24. boris/config_file.py +63 -83
  25. boris/connections.py +112 -57
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2511 -1824
  30. boris/core_qrc.py +15895 -10185
  31. boris/core_ui.py +946 -792
  32. boris/db_functions.py +21 -41
  33. boris/dev.py +134 -0
  34. boris/dialog.py +505 -244
  35. boris/duration_widget.py +15 -20
  36. boris/edit_event.py +84 -28
  37. boris/edit_event_ui.py +214 -78
  38. boris/event_operations.py +517 -415
  39. boris/events_cursor.py +25 -17
  40. boris/events_snapshots.py +36 -82
  41. boris/exclusion_matrix.py +4 -9
  42. boris/export_events.py +213 -583
  43. boris/export_observation.py +98 -611
  44. boris/external_processes.py +156 -97
  45. boris/geometric_measurement.py +652 -287
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +9 -9
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +26 -63
  51. boris/latency.py +34 -25
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +52 -84
  54. boris/menu_options.py +17 -6
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +7 -9
  57. boris/mpv.py +1 -0
  58. boris/mpv2.py +732 -705
  59. boris/observation.py +655 -310
  60. boris/observation_operations.py +1036 -404
  61. boris/observation_ui.py +584 -356
  62. boris/observations_list.py +71 -53
  63. boris/otx_parser.py +74 -80
  64. boris/param_panel.py +31 -16
  65. boris/param_panel_ui.py +254 -138
  66. boris/player_dock_widget.py +90 -60
  67. boris/plot_data_module.py +43 -46
  68. boris/plot_events.py +127 -90
  69. boris/plot_events_rt.py +17 -31
  70. boris/plot_spectrogram_rt.py +95 -30
  71. boris/plot_waveform_rt.py +32 -21
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +18 -8
  74. boris/portion/const.py +35 -18
  75. boris/portion/dict.py +5 -5
  76. boris/portion/func.py +2 -2
  77. boris/portion/interval.py +21 -41
  78. boris/portion/io.py +41 -32
  79. boris/preferences.py +306 -83
  80. boris/preferences_ui.py +685 -228
  81. boris/project.py +448 -293
  82. boris/project_functions.py +689 -254
  83. boris/project_import_export.py +213 -222
  84. boris/project_ui.py +674 -438
  85. boris/qrc_boris.py +6 -3
  86. boris/qrc_boris5.py +6 -3
  87. boris/select_modifiers.py +74 -48
  88. boris/select_observations.py +20 -199
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +53 -37
  91. boris/subjects_pad.py +6 -9
  92. boris/synthetic_time_budget.py +45 -28
  93. boris/time_budget_functions.py +171 -171
  94. boris/time_budget_widget.py +84 -114
  95. boris/transitions.py +41 -47
  96. boris/utilities.py +766 -266
  97. boris/version.py +3 -3
  98. boris/video_equalizer.py +16 -14
  99. boris/video_equalizer_ui.py +199 -130
  100. boris/video_operations.py +125 -28
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +538 -0
  104. boris_behav_obs-9.7.6.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.6.dist-info/RECORD +109 -0
  106. {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.dist-info}/WHEEL +1 -1
  107. boris_behav_obs-9.7.6.dist-info/entry_points.txt +2 -0
  108. boris/README.TXT +0 -22
  109. boris/add_modifier.ui +0 -323
  110. boris/boris_ui.py +0 -886
  111. boris/converters.ui +0 -289
  112. boris/core.qrc +0 -35
  113. boris/core.ui +0 -1543
  114. boris/edit_event.ui +0 -175
  115. boris/icons/logo_eye.ico +0 -0
  116. boris/map_creator.py +0 -850
  117. boris/observation.ui +0 -773
  118. boris/param_panel.ui +0 -379
  119. boris/preferences.ui +0 -537
  120. boris/project.ui +0 -1069
  121. boris/project_server.py +0 -236
  122. boris/vlc.py +0 -10343
  123. boris/vlc_local.py +0 -90
  124. boris_behav_obs-8.9.16.dist-info/LICENSE.TXT +0 -674
  125. boris_behav_obs-8.9.16.dist-info/METADATA +0 -129
  126. boris_behav_obs-8.9.16.dist-info/RECORD +0 -108
  127. boris_behav_obs-8.9.16.dist-info/entry_points.txt +0 -2
  128. {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
  129. {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.dist-info}/top_level.txt +0 -0
boris/mpv2.py CHANGED
@@ -2,23 +2,29 @@
2
2
  # vim: ts=4 sw=4 et
3
3
  #
4
4
  # Python MPV library module
5
- # Copyright (C) 2017-2022 Sebastian Götte <code@jaseg.net>
5
+ # Copyright (C) 2017-2024 Sebastian Götte <code@jaseg.net>
6
6
  #
7
- # This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
8
- # License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later
9
- # version.
7
+ # python-mpv inherits the underlying libmpv's license, which can be either GPLv2 or later (default) or LGPLv2.1 or
8
+ # later. For details, see the mpv copyright page here: https://github.com/mpv-player/mpv/blob/master/Copyright
10
9
  #
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 General Public License for more details.
10
+ # You may copy, modify, and redistribute this file under the terms of the GNU General Public License version 2 (or, at
11
+ # your option, any later version), or the GNU Lesser General Public License as published by the Free Software
12
+ # Foundation; either version 2.1 of the License, or (at your option) any later version.
13
13
  #
14
- # You should have received a copy of the GNU General Public License along with this program; if not, write to the Free
15
- # Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
14
+ # This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
15
+ # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License and the GNU
16
+ # Lesser General Public License for more details.
16
17
  #
18
+ # You can find copies of the GPLv2 and LGPLv2.1 licenses in the project repository's LICENSE.GPL and LICENSE.LGPL files.
19
+
20
+ __version__ = '1.0.7'
17
21
 
18
22
  from ctypes import *
19
23
  import ctypes.util
20
24
  import threading
25
+ import queue
21
26
  import os
27
+ import os.path
22
28
  import sys
23
29
  from warnings import warn
24
30
  from functools import partial, wraps
@@ -28,36 +34,42 @@ import collections
28
34
  import re
29
35
  import traceback
30
36
 
31
- if os.name == "nt":
37
+ if os.name == 'nt':
32
38
  # Note: mpv-2.dll with API version 2 corresponds to mpv v0.35.0. Most things should work with the fallback, too.
33
- dll = ctypes.util.find_library("mpv-2.dll") or ctypes.util.find_library("mpv-1.dll")
34
- print(dll)
35
- if dll is None:
36
- raise OSError(
37
- "Cannot find mpv-1.dll or mpv-2.dll in your system %PATH%. One way to deal with this is to ship "
38
- "the dll with your script and put the directory your script is in into %PATH% before "
39
- '"import mpv": os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"] '
40
- 'If mpv-1.dll is located elsewhere, you can add that path to os.environ["PATH"].'
41
- )
42
- backend = CDLL(dll)
43
- fs_enc = "utf-8"
39
+ names = ['mpv-2.dll', 'libmpv-2.dll', 'mpv-1.dll']
40
+ for name in names:
41
+ dll = ctypes.util.find_library(name)
42
+ if dll:
43
+ break
44
+ else:
45
+ for name in names:
46
+ dll = os.path.join(os.path.dirname(__file__), name)
47
+ if os.path.isfile(dll):
48
+ break
49
+ else:
50
+ raise OSError('Cannot find mpv-1.dll, mpv-2.dll or libmpv-2.dll in your system %PATH%. One way to deal with this is to ship the dll with your script and put the directory your script is in into %PATH% before "import mpv": os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"] If mpv-1.dll is located elsewhere, you can add that path to os.environ["PATH"].')
51
+
52
+ try:
53
+ # flags argument: LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR
54
+ # cf. https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexa
55
+ backend = CDLL(dll, 0x00001000 | 0x00000100)
56
+ except Exception as e:
57
+ if not os.path.isabs(dll): # can only be find_library, not the "look next to mpv.py" thing
58
+ raise OSError(f'ctypes.find_library found mpv.dll at {dll}, but ctypes.CDLL could not load it. It looks like find_library found mpv.dll under a relative path entry in %PATH%. Please make sure all paths in %PATH% are absolute. Instead of trying to load mpv.dll from the current working directory, put it somewhere next to your script and add that path to %PATH% using os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"]') from e
59
+ else:
60
+ raise OSError(f'ctypes.find_library found mpv.dll at {dll}, but ctypes.CDLL could not load it.') from e
61
+ fs_enc = 'utf-8'
62
+
44
63
  else:
45
64
  import locale
46
-
47
65
  lc, enc = locale.getlocale(locale.LC_NUMERIC)
48
66
  # libmpv requires LC_NUMERIC to be set to "C". Since messing with global variables everyone else relies upon is
49
67
  # still better than segfaulting, we are setting LC_NUMERIC to "C".
50
- locale.setlocale(locale.LC_NUMERIC, "C")
51
-
52
- sofile = ctypes.util.find_library("mpv")
68
+ locale.setlocale(locale.LC_NUMERIC, 'C')
53
69
 
70
+ sofile = ctypes.util.find_library('mpv')
54
71
  if sofile is None:
55
- raise OSError(
56
- "Cannot find libmpv in the usual places. Depending on your distro, you may try installing an "
57
- "mpv-devel or mpv-libs package. If you have libmpv around but this script can't find it, consult "
58
- "the documentation for ctypes.util.find_library which this script uses to look up the library "
59
- "filename."
60
- )
72
+ raise OSError("Cannot find libmpv in the usual places. Depending on your distro, you may try installing an mpv-devel or mpv-libs package. If you have libmpv around but this script can't find it, consult the documentation for ctypes.util.find_library which this script uses to look up the library filename.")
61
73
  backend = CDLL(sofile)
62
74
  fs_enc = sys.getfilesystemencoding()
63
75
 
@@ -65,83 +77,72 @@ else:
65
77
  class ShutdownError(SystemError):
66
78
  pass
67
79
 
68
-
69
80
  class EventOverflowError(SystemError):
70
81
  pass
71
82
 
72
-
73
83
  class MpvHandle(c_void_p):
74
84
  pass
75
85
 
76
-
77
86
  class MpvRenderCtxHandle(c_void_p):
78
87
  pass
79
88
 
80
-
81
89
  class PropertyUnavailableError(AttributeError):
82
90
  pass
83
91
 
84
-
85
92
  class ErrorCode(object):
86
93
  """For documentation on these, see mpv's libmpv/client.h."""
87
-
88
- SUCCESS = 0
89
- EVENT_QUEUE_FULL = -1
90
- NOMEM = -2
91
- UNINITIALIZED = -3
92
- INVALID_PARAMETER = -4
93
- OPTION_NOT_FOUND = -5
94
- OPTION_FORMAT = -6
95
- OPTION_ERROR = -7
96
- PROPERTY_NOT_FOUND = -8
97
- PROPERTY_FORMAT = -9
98
- PROPERTY_UNAVAILABLE = -10
99
- PROPERTY_ERROR = -11
100
- COMMAND = -12
101
- LOADING_FAILED = -13
102
- AO_INIT_FAILED = -14
103
- VO_INIT_FAILED = -15
104
- NOTHING_TO_PLAY = -16
105
- UNKNOWN_FORMAT = -17
106
- UNSUPPORTED = -18
107
- NOT_IMPLEMENTED = -19
108
- GENERIC = -20
94
+ SUCCESS = 0
95
+ EVENT_QUEUE_FULL = -1
96
+ NOMEM = -2
97
+ UNINITIALIZED = -3
98
+ INVALID_PARAMETER = -4
99
+ OPTION_NOT_FOUND = -5
100
+ OPTION_FORMAT = -6
101
+ OPTION_ERROR = -7
102
+ PROPERTY_NOT_FOUND = -8
103
+ PROPERTY_FORMAT = -9
104
+ PROPERTY_UNAVAILABLE = -10
105
+ PROPERTY_ERROR = -11
106
+ COMMAND = -12
107
+ LOADING_FAILED = -13
108
+ AO_INIT_FAILED = -14
109
+ VO_INIT_FAILED = -15
110
+ NOTHING_TO_PLAY = -16
111
+ UNKNOWN_FORMAT = -17
112
+ UNSUPPORTED = -18
113
+ NOT_IMPLEMENTED = -19
114
+ GENERIC = -20
109
115
 
110
116
  EXCEPTION_DICT = {
111
- 0: None,
112
- -1: lambda *a: MemoryError("mpv event queue full", *a),
113
- -2: lambda *a: MemoryError("mpv cannot allocate memory", *a),
114
- -3: lambda *a: ValueError("Uninitialized mpv handle used", *a),
115
- -4: lambda *a: ValueError("Invalid value for mpv parameter", *a),
116
- -5: lambda *a: AttributeError("mpv option does not exist", *a),
117
- -6: lambda *a: TypeError("Tried to set mpv option using wrong format", *a),
118
- -7: lambda *a: ValueError("Invalid value for mpv option", *a),
119
- -8: lambda *a: AttributeError("mpv property does not exist", *a),
120
- # Currently (mpv 0.18.1) there is a bug causing a PROPERTY_FORMAT error to be returned instead of
121
- # INVALID_PARAMETER when setting a property-mapped option to an invalid value.
122
- -9: lambda *a: TypeError("Tried to get/set mpv property using wrong format, or passed invalid value", *a),
123
- -10: lambda *a: PropertyUnavailableError("mpv property is not available", *a),
124
- -11: lambda *a: RuntimeError("Generic error getting or setting mpv property", *a),
125
- -12: lambda *a: SystemError("Error running mpv command", *a),
126
- -14: lambda *a: RuntimeError("Initializing the audio output failed", *a),
127
- -15: lambda *a: RuntimeError("Initializing the video output failed"),
128
- -16: lambda *a: RuntimeError(
129
- "There was no audio or video data to play. This also happens if the file "
130
- "was recognized, but did not contain any audio or video streams, or no "
131
- "streams were selected."
132
- ),
133
- -17: lambda *a: RuntimeError(
134
- "When trying to load the file, the file format could not be determined, "
135
- "or the file was too broken to open it"
136
- ),
137
- -18: lambda *a: ValueError("Generic error for signaling that certain system requirements are not fulfilled"),
138
- -19: lambda *a: NotImplementedError("The API function which was called is a stub only"),
139
- -20: lambda *a: RuntimeError("Unspecified error"),
140
- }
117
+ 0: None,
118
+ -1: lambda *a: MemoryError('mpv event queue full', *a),
119
+ -2: lambda *a: MemoryError('mpv cannot allocate memory', *a),
120
+ -3: lambda *a: ValueError('Uninitialized mpv handle used', *a),
121
+ -4: lambda *a: ValueError('Invalid value for mpv parameter', *a),
122
+ -5: lambda *a: AttributeError('mpv option does not exist', *a),
123
+ -6: lambda *a: TypeError('Tried to set mpv option using wrong format', *a),
124
+ -7: lambda *a: ValueError('Invalid value for mpv option', *a),
125
+ -8: lambda *a: AttributeError('mpv property does not exist', *a),
126
+ # Currently (mpv 0.18.1) there is a bug causing a PROPERTY_FORMAT error to be returned instead of
127
+ # INVALID_PARAMETER when setting a property-mapped option to an invalid value.
128
+ -9: lambda *a: TypeError('Tried to get/set mpv property using wrong format, or passed invalid value', *a),
129
+ -10: lambda *a: PropertyUnavailableError('mpv property is not available', *a),
130
+ -11: lambda *a: RuntimeError('Generic error getting or setting mpv property', *a),
131
+ -12: lambda *a: SystemError('Error running mpv command', *a),
132
+ -14: lambda *a: RuntimeError('Initializing the audio output failed', *a),
133
+ -15: lambda *a: RuntimeError('Initializing the video output failed'),
134
+ -16: lambda *a: RuntimeError('There was no audio or video data to play. This also happens if the file '
135
+ 'was recognized, but did not contain any audio or video streams, or no '
136
+ 'streams were selected.'),
137
+ -17: lambda *a: RuntimeError('When trying to load the file, the file format could not be determined, '
138
+ 'or the file was too broken to open it'),
139
+ -18: lambda *a: ValueError('Generic error for signaling that certain system requirements are not fulfilled'),
140
+ -19: lambda *a: NotImplementedError('The API function which was called is a stub only'),
141
+ -20: lambda *a: RuntimeError('Unspecified error') }
141
142
 
142
143
  @staticmethod
143
144
  def human_readable(ec):
144
- return _mpv_error_string(ec).decode("utf-8")
145
+ return _mpv_error_string(ec).decode('utf-8')
145
146
 
146
147
  @staticmethod
147
148
  def default_error_handler(ec, *args):
@@ -160,61 +161,52 @@ class ErrorCode(object):
160
161
  if ex:
161
162
  raise ex
162
163
 
163
-
164
164
  MpvGlGetProcAddressFn = CFUNCTYPE(c_void_p, c_void_p, c_char_p)
165
-
166
-
167
165
  class MpvOpenGLInitParams(Structure):
168
- _fields_ = [
169
- ("get_proc_address", MpvGlGetProcAddressFn),
170
- ("get_proc_address_ctx", c_void_p),
171
- ("extra_exts", c_void_p),
172
- ]
166
+ _fields_ = [('get_proc_address', MpvGlGetProcAddressFn),
167
+ ('get_proc_address_ctx', c_void_p),
168
+ ('extra_exts', c_void_p)]
173
169
 
174
170
  def __init__(self, get_proc_address):
175
171
  self.get_proc_address = get_proc_address
176
172
  self.get_proc_address_ctx = None
177
173
  self.extra_exts = None
178
174
 
179
-
180
175
  class MpvOpenGLFBO(Structure):
181
- _fields_ = [("fbo", c_int), ("w", c_int), ("h", c_int), ("internal_format", c_int)]
176
+ _fields_ = [('fbo', c_int),
177
+ ('w', c_int),
178
+ ('h', c_int),
179
+ ('internal_format', c_int)]
182
180
 
183
181
  def __init__(self, w, h, fbo=0, internal_format=0):
184
182
  self.w, self.h = w, h
185
183
  self.fbo = fbo
186
184
  self.internal_format = internal_format
187
185
 
188
-
189
186
  class MpvRenderFrameInfo(Structure):
190
- _fields_ = [("flags", c_int64), ("target_time", c_int64)]
187
+ _fields_ = [('flags', c_int64),
188
+ ('target_time', c_int64)]
191
189
 
192
190
  def as_dict(self):
193
- return {"flags": self.flags, "target_time": self.target_time}
194
-
191
+ return {'flags': self.flags,
192
+ 'target_time': self.target_time}
195
193
 
196
194
  class MpvOpenGLDRMParams(Structure):
197
- _fields_ = [
198
- ("fd", c_int),
199
- ("crtc_id", c_int),
200
- ("connector_id", c_int),
201
- ("atomic_request_ptr", c_void_p),
202
- ("render_fd", c_int),
203
- ]
204
-
195
+ _fields_ = [('fd', c_int),
196
+ ('crtc_id', c_int),
197
+ ('connector_id', c_int),
198
+ ('atomic_request_ptr', c_void_p),
199
+ ('render_fd', c_int)]
205
200
 
206
201
  class MpvOpenGLDRMDrawSurfaceSize(Structure):
207
- _fields_ = [("width", c_int), ("height", c_int)]
208
-
202
+ _fields_ = [('width', c_int), ('height', c_int)]
209
203
 
210
204
  class MpvOpenGLDRMParamsV2(Structure):
211
- _fields_ = [
212
- ("fd", c_int),
213
- ("crtc_id", c_int),
214
- ("connector_id", c_int),
215
- ("atomic_request_ptr", c_void_p),
216
- ("render_fd", c_int),
217
- ]
205
+ _fields_ = [('fd', c_int),
206
+ ('crtc_id', c_int),
207
+ ('connector_id', c_int),
208
+ ('atomic_request_ptr', c_void_p),
209
+ ('render_fd', c_int)]
218
210
 
219
211
  def __init__(self, crtc_id, connector_id, atomic_request_ptr, fd=-1, render_fd=-1):
220
212
  self.crtc_id, self.connector_id = crtc_id, connector_id
@@ -223,29 +215,28 @@ class MpvOpenGLDRMParamsV2(Structure):
223
215
 
224
216
 
225
217
  class MpvRenderParam(Structure):
226
- _fields_ = [("type_id", c_int), ("data", c_void_p)]
218
+ _fields_ = [('type_id', c_int),
219
+ ('data', c_void_p)]
227
220
 
228
221
  # maps human-readable type name to (type_id, argtype) tuple.
229
222
  # The type IDs come from libmpv/render.h
230
- TYPES = {
231
- "invalid": (0, None),
232
- "api_type": (1, str),
233
- "opengl_init_params": (2, MpvOpenGLInitParams),
234
- "opengl_fbo": (3, MpvOpenGLFBO),
235
- "flip_y": (4, bool),
236
- "depth": (5, int),
237
- "icc_profile": (6, bytes),
238
- "ambient_light": (7, int),
239
- "x11_display": (8, c_void_p),
240
- "wl_display": (9, c_void_p),
241
- "advanced_control": (10, bool),
242
- "next_frame_info": (11, MpvRenderFrameInfo),
243
- "block_for_target_time": (12, bool),
244
- "skip_rendering": (13, bool),
245
- "drm_display": (14, MpvOpenGLDRMParams),
246
- "drm_draw_surface_size": (15, MpvOpenGLDRMDrawSurfaceSize),
247
- "drm_display_v2": (16, MpvOpenGLDRMParamsV2),
248
- }
223
+ TYPES = {"invalid" :(0, None),
224
+ "api_type" :(1, str),
225
+ "opengl_init_params" :(2, MpvOpenGLInitParams),
226
+ "opengl_fbo" :(3, MpvOpenGLFBO),
227
+ "flip_y" :(4, bool),
228
+ "depth" :(5, int),
229
+ "icc_profile" :(6, bytes),
230
+ "ambient_light" :(7, int),
231
+ "x11_display" :(8, c_void_p),
232
+ "wl_display" :(9, c_void_p),
233
+ "advanced_control" :(10, bool),
234
+ "next_frame_info" :(11, MpvRenderFrameInfo),
235
+ "block_for_target_time" :(12, bool),
236
+ "skip_rendering" :(13, bool),
237
+ "drm_display" :(14, MpvOpenGLDRMParams),
238
+ "drm_draw_surface_size" :(15, MpvOpenGLDRMDrawSurfaceSize),
239
+ "drm_display_v2" :(16, MpvOpenGLDRMParamsV2)}
249
240
 
250
241
  def __init__(self, name, value=None):
251
242
  if name not in self.TYPES:
@@ -256,7 +247,7 @@ class MpvRenderParam(Structure):
256
247
  self.data = c_void_p()
257
248
  elif cons is str:
258
249
  self.value = value
259
- self.data = cast(c_char_p(value.encode("utf-8")), c_void_p)
250
+ self.data = cast(c_char_p(value.encode('utf-8')), c_void_p)
260
251
  elif cons is bytes:
261
252
  self.value = MpvByteArray(value)
262
253
  self.data = cast(pointer(self.value), c_void_p)
@@ -270,110 +261,82 @@ class MpvRenderParam(Structure):
270
261
  self.value = cons(**value)
271
262
  self.data = cast(pointer(self.value), c_void_p)
272
263
 
273
-
274
264
  def kwargs_to_render_param_array(kwargs):
275
- t = MpvRenderParam * (len(kwargs) + 1)
276
- return t(*kwargs.items(), ("invalid", None))
277
-
265
+ t = MpvRenderParam * (len(kwargs)+1)
266
+ return t(*kwargs.items(), ('invalid', None))
278
267
 
279
268
  class MpvFormat(c_int):
280
- NONE = 0
281
- STRING = 1
282
- OSD_STRING = 2
283
- FLAG = 3
284
- INT64 = 4
285
- DOUBLE = 5
286
- NODE = 6
287
- NODE_ARRAY = 7
288
- NODE_MAP = 8
289
- BYTE_ARRAY = 9
269
+ NONE = 0
270
+ STRING = 1
271
+ OSD_STRING = 2
272
+ FLAG = 3
273
+ INT64 = 4
274
+ DOUBLE = 5
275
+ NODE = 6
276
+ NODE_ARRAY = 7
277
+ NODE_MAP = 8
278
+ BYTE_ARRAY = 9
290
279
 
291
280
  def __eq__(self, other):
292
281
  return self is other or self.value == other or self.value == int(other)
293
282
 
294
283
  def __repr__(self):
295
- return [
296
- "NONE",
297
- "STRING",
298
- "OSD_STRING",
299
- "FLAG",
300
- "INT64",
301
- "DOUBLE",
302
- "NODE",
303
- "NODE_ARRAY",
304
- "NODE_MAP",
305
- "BYTE_ARRAY",
306
- ][self.value]
284
+ return ['NONE', 'STRING', 'OSD_STRING', 'FLAG', 'INT64', 'DOUBLE', 'NODE', 'NODE_ARRAY', 'NODE_MAP',
285
+ 'BYTE_ARRAY'][self.value]
307
286
 
308
287
  def __hash__(self):
309
288
  return self.value
310
289
 
311
290
 
312
291
  class MpvEventID(c_int):
313
- NONE = 0
314
- SHUTDOWN = 1
315
- LOG_MESSAGE = 2
316
- GET_PROPERTY_REPLY = 3
317
- SET_PROPERTY_REPLY = 4
318
- COMMAND_REPLY = 5
319
- START_FILE = 6
320
- END_FILE = 7
321
- FILE_LOADED = 8
322
- CLIENT_MESSAGE = 16
323
- VIDEO_RECONFIG = 17
324
- AUDIO_RECONFIG = 18
325
- SEEK = 20
326
- PLAYBACK_RESTART = 21
327
- PROPERTY_CHANGE = 22
328
- QUEUE_OVERFLOW = 24
329
- HOOK = 25
330
-
331
- ANY = (
332
- SHUTDOWN,
333
- LOG_MESSAGE,
334
- GET_PROPERTY_REPLY,
335
- SET_PROPERTY_REPLY,
336
- COMMAND_REPLY,
337
- START_FILE,
338
- END_FILE,
339
- FILE_LOADED,
340
- CLIENT_MESSAGE,
341
- VIDEO_RECONFIG,
342
- AUDIO_RECONFIG,
343
- SEEK,
344
- PLAYBACK_RESTART,
345
- PROPERTY_CHANGE,
346
- )
292
+ NONE = 0
293
+ SHUTDOWN = 1
294
+ LOG_MESSAGE = 2
295
+ GET_PROPERTY_REPLY = 3
296
+ SET_PROPERTY_REPLY = 4
297
+ COMMAND_REPLY = 5
298
+ START_FILE = 6
299
+ END_FILE = 7
300
+ FILE_LOADED = 8
301
+ CLIENT_MESSAGE = 16
302
+ VIDEO_RECONFIG = 17
303
+ AUDIO_RECONFIG = 18
304
+ SEEK = 20
305
+ PLAYBACK_RESTART = 21
306
+ PROPERTY_CHANGE = 22
307
+ QUEUE_OVERFLOW = 24
308
+ HOOK = 25
309
+
310
+ ANY = ( SHUTDOWN, LOG_MESSAGE, GET_PROPERTY_REPLY, SET_PROPERTY_REPLY, COMMAND_REPLY, START_FILE, END_FILE,
311
+ FILE_LOADED, CLIENT_MESSAGE, VIDEO_RECONFIG, AUDIO_RECONFIG, SEEK, PLAYBACK_RESTART, PROPERTY_CHANGE)
347
312
 
348
313
  def __repr__(self):
349
314
  return f'<MpvEventID {self.value} {_mpv_event_name(self.value).decode("utf-8")}>'
350
315
 
351
316
  @classmethod
352
317
  def from_str(kls, s):
353
- return getattr(kls, s.upper().replace("-", "_"))
318
+ return getattr(kls, s.upper().replace('-', '_'))
354
319
 
355
320
 
356
321
  identity_decoder = lambda b: b
357
- strict_decoder = lambda b: b.decode("utf-8")
358
-
359
-
322
+ strict_decoder = lambda b: b.decode('utf-8')
360
323
  def lazy_decoder(b):
361
324
  try:
362
- return b.decode("utf-8")
325
+ return b.decode('utf-8')
363
326
  except UnicodeDecodeError:
364
327
  return b
365
328
 
366
-
367
329
  class MpvNodeList(Structure):
368
330
  def array_value(self, decoder=identity_decoder):
369
- return [self.values[i].node_value(decoder) for i in range(self.num)]
331
+ return [ self.values[i].node_value(decoder) for i in range(self.num) ]
370
332
 
371
333
  def dict_value(self, decoder=identity_decoder):
372
- return {self.keys[i].decode("utf-8"): self.values[i].node_value(decoder) for i in range(self.num)}
373
-
334
+ return { self.keys[i].decode('utf-8'):
335
+ self.values[i].node_value(decoder) for i in range(self.num) }
374
336
 
375
337
  class MpvByteArray(Structure):
376
- _fields_ = [("data", c_void_p), ("size", c_size_t)]
338
+ _fields_ = [('data', c_void_p),
339
+ ('size', c_size_t)]
377
340
 
378
341
  def __init__(self, value):
379
342
  self._value = value
@@ -381,8 +344,7 @@ class MpvByteArray(Structure):
381
344
  self.size = len(value)
382
345
 
383
346
  def bytes_value(self):
384
- return cast(self.data, POINTER(c_char))[: self.size]
385
-
347
+ return cast(self.data, POINTER(c_char))[:self.size]
386
348
 
387
349
  class MpvNode(Structure):
388
350
  def node_value(self, decoder=identity_decoder):
@@ -395,7 +357,7 @@ class MpvNode(Structure):
395
357
  elif fmt == MpvFormat.STRING:
396
358
  return decoder(v.string)
397
359
  elif fmt == MpvFormat.OSD_STRING:
398
- return v.string.decode("utf-8")
360
+ return v.string.decode('utf-8')
399
361
  elif fmt == MpvFormat.FLAG:
400
362
  return bool(v.flag)
401
363
  elif fmt == MpvFormat.INT64:
@@ -403,7 +365,7 @@ class MpvNode(Structure):
403
365
  elif fmt == MpvFormat.DOUBLE:
404
366
  return v.double
405
367
  else:
406
- if not v.node: # Check for null pointer
368
+ if not v.node: # Check for null pointer
407
369
  return None
408
370
  if fmt == MpvFormat.NODE:
409
371
  return v.node.contents.node_value(decoder)
@@ -414,42 +376,43 @@ class MpvNode(Structure):
414
376
  elif fmt == MpvFormat.BYTE_ARRAY:
415
377
  return v.byte_array.contents.bytes_value()
416
378
  else:
417
- raise TypeError("Unknown MPV node format {}. Please submit a bug report.".format(fmt))
418
-
379
+ raise TypeError('Unknown MPV node format {}. Please submit a bug report.'.format(fmt))
419
380
 
420
381
  class MpvNodeUnion(Union):
421
- _fields_ = [
422
- ("string", c_char_p),
423
- ("flag", c_int),
424
- ("int64", c_int64),
425
- ("double", c_double),
426
- ("node", POINTER(MpvNode)),
427
- ("list", POINTER(MpvNodeList)),
428
- ("map", POINTER(MpvNodeList)),
429
- ("byte_array", POINTER(MpvByteArray)),
430
- ]
431
-
432
-
433
- MpvNode._fields_ = [("val", MpvNodeUnion), ("format", MpvFormat)]
434
-
435
- MpvNodeList._fields_ = [("num", c_int), ("values", POINTER(MpvNode)), ("keys", POINTER(c_char_p))]
436
-
382
+ _fields_ = [('string', c_char_p),
383
+ ('flag', c_int),
384
+ ('int64', c_int64),
385
+ ('double', c_double),
386
+ ('node', POINTER(MpvNode)),
387
+ ('list', POINTER(MpvNodeList)),
388
+ ('map', POINTER(MpvNodeList)),
389
+ ('byte_array', POINTER(MpvByteArray))]
390
+
391
+ MpvNode._fields_ = [('val', MpvNodeUnion),
392
+ ('format', MpvFormat)]
393
+
394
+ MpvNodeList._fields_ = [('num', c_int),
395
+ ('values', POINTER(MpvNode)),
396
+ ('keys', POINTER(c_char_p))]
437
397
 
438
398
  class MpvEvent(Structure):
439
- _fields_ = [("event_id", MpvEventID), ("error", c_int), ("reply_userdata", c_ulonglong), ("_data", c_void_p)]
399
+ _fields_ = [('event_id', MpvEventID),
400
+ ('error', c_int),
401
+ ('reply_userdata', c_ulonglong),
402
+ ('_data', c_void_p)]
440
403
 
441
404
  @property
442
405
  def data(self):
443
406
  dtype = {
444
- MpvEventID.GET_PROPERTY_REPLY: MpvEventProperty,
445
- MpvEventID.PROPERTY_CHANGE: MpvEventProperty,
446
- MpvEventID.LOG_MESSAGE: MpvEventLogMessage,
447
- MpvEventID.CLIENT_MESSAGE: MpvEventClientMessage,
448
- MpvEventID.START_FILE: MpvEventStartFile,
449
- MpvEventID.END_FILE: MpvEventEndFile,
450
- MpvEventID.HOOK: MpvEventHook,
451
- MpvEventID.COMMAND_REPLY: MpvEventCommand,
452
- }.get(self.event_id.value)
407
+ MpvEventID.GET_PROPERTY_REPLY: MpvEventProperty,
408
+ MpvEventID.PROPERTY_CHANGE: MpvEventProperty,
409
+ MpvEventID.LOG_MESSAGE: MpvEventLogMessage,
410
+ MpvEventID.CLIENT_MESSAGE: MpvEventClientMessage,
411
+ MpvEventID.START_FILE: MpvEventStartFile,
412
+ MpvEventID.END_FILE: MpvEventEndFile,
413
+ MpvEventID.HOOK: MpvEventHook,
414
+ MpvEventID.COMMAND_REPLY: MpvEventCommand,
415
+ }.get(self.event_id.value)
453
416
  return cast(self._data, POINTER(dtype)).contents if dtype else None
454
417
 
455
418
  def as_dict(self, decoder=identity_decoder):
@@ -461,11 +424,12 @@ class MpvEvent(Structure):
461
424
 
462
425
  def __str__(self):
463
426
  d = self.data
464
- return f"<{type(d).__name__} ({self.event_id.value}) err={self.error} p={self.reply_userdata:016x} d={self.as_dict()}>"
465
-
427
+ return f'<{type(d).__name__} ({self.event_id.value}) err={self.error} p={self.reply_userdata:016x} d={self.as_dict()}>'
466
428
 
467
429
  class MpvEventProperty(Structure):
468
- _fields_ = [("_name", c_char_p), ("format", MpvFormat), ("data", MpvNodeUnion)]
430
+ _fields_ = [('_name', c_char_p),
431
+ ('format', MpvFormat),
432
+ ('data', MpvNodeUnion)]
469
433
 
470
434
  @property
471
435
  def name(self):
@@ -475,9 +439,10 @@ class MpvEventProperty(Structure):
475
439
  def value(self):
476
440
  return MpvNode.node_cast_value(self.data, self.format.value, decoder=lazy_decoder)
477
441
 
478
-
479
442
  class MpvEventLogMessage(Structure):
480
- _fields_ = [("_prefix", c_char_p), ("_level", c_char_p), ("_text", c_char_p)]
443
+ _fields_ = [('_prefix', c_char_p),
444
+ ('_level', c_char_p),
445
+ ('_text', c_char_p)]
481
446
 
482
447
  @property
483
448
  def prefix(self):
@@ -491,34 +456,35 @@ class MpvEventLogMessage(Structure):
491
456
  def text(self):
492
457
  return lazy_decoder(self._text)
493
458
 
494
-
495
459
  class MpvEventEndFile(Structure):
496
- _fields_ = [("reason", c_int), ("error", c_int)]
497
-
498
- EOF = 0
499
- RESTARTED = 1
500
- ABORTED = 2
501
- QUIT = 3
502
- ERROR = 4
503
- REDIRECT = 5
504
-
505
-
506
- class MpvEventStartFile(Structure):
507
460
  _fields_ = [
508
- ("playlist_entry_id", c_ulonglong),
461
+ ('reason', c_int),
462
+ ('error', c_int),
463
+ ('playlist_entry_id', c_ulonglong),
464
+ ('playlist_insert_id', c_ulonglong),
465
+ ('playlist_insert_num_entries', c_int),
509
466
  ]
467
+
468
+ EOF = 0
469
+ RESTARTED = 1
470
+ ABORTED = 2
471
+ QUIT = 3
472
+ ERROR = 4
473
+ REDIRECT = 5
510
474
 
475
+ class MpvEventStartFile(Structure):
476
+ _fields_ = [('playlist_entry_id', c_ulonglong),]
511
477
 
512
478
  class MpvEventClientMessage(Structure):
513
- _fields_ = [("_num_args", c_int), ("_args", POINTER(c_char_p))]
479
+ _fields_ = [('_num_args', c_int),
480
+ ('_args', POINTER(c_char_p))]
514
481
 
515
482
  @property
516
483
  def args(self):
517
- return [self._args[i] for i in range(self._num_args)]
518
-
484
+ return [ self._args[i] for i in range(self._num_args) ]
519
485
 
520
486
  class MpvEventCommand(Structure):
521
- _fields_ = [("_result", MpvNode)]
487
+ _fields_ = [('_result', MpvNode)]
522
488
 
523
489
  def unpack(self, decoder=identity_decoder):
524
490
  return self._result.node_value(decoder=decoder)
@@ -527,35 +493,28 @@ class MpvEventCommand(Structure):
527
493
  def result(self):
528
494
  return self.unpack()
529
495
 
530
-
531
496
  class MpvEventHook(Structure):
532
- _fields_ = [
533
- ("_name", c_char_p),
534
- ("id", c_ulonglong),
535
- ]
497
+ _fields_ = [('_name', c_char_p),
498
+ ('id', c_ulonglong),]
536
499
 
500
+
537
501
  @property
538
502
  def name(self):
539
503
  return self._name.decode("utf-8")
540
504
 
541
-
542
505
  StreamReadFn = CFUNCTYPE(c_int64, c_void_p, POINTER(c_char), c_uint64)
543
506
  StreamSeekFn = CFUNCTYPE(c_int64, c_void_p, c_int64)
544
507
  StreamSizeFn = CFUNCTYPE(c_int64, c_void_p)
545
508
  StreamCloseFn = CFUNCTYPE(None, c_void_p)
546
509
  StreamCancelFn = CFUNCTYPE(None, c_void_p)
547
510
 
548
-
549
511
  class StreamCallbackInfo(Structure):
550
- _fields_ = [
551
- ("cookie", c_void_p),
552
- ("read", StreamReadFn),
553
- ("seek", StreamSeekFn),
554
- ("size", StreamSizeFn),
555
- ("close", StreamCloseFn),
556
- ("cancel", StreamCancelFn),
557
- ]
558
-
512
+ _fields_ = [('cookie', c_void_p),
513
+ ('read', StreamReadFn),
514
+ ('seek', StreamSeekFn),
515
+ ('size', StreamSizeFn),
516
+ ('close', StreamCloseFn),
517
+ ('cancel', StreamCancelFn)]
559
518
 
560
519
  StreamOpenFn = CFUNCTYPE(c_int, c_void_p, c_char_p, POINTER(StreamCallbackInfo))
561
520
 
@@ -563,7 +522,6 @@ WakeupCallback = CFUNCTYPE(None, c_void_p)
563
522
 
564
523
  RenderUpdateFn = CFUNCTYPE(None, c_void_p)
565
524
 
566
-
567
525
  def _handle_func(name, args, restype, errcheck, ctx=MpvHandle, deprecated=False):
568
526
  func = getattr(backend, name)
569
527
  func.argtypes = [ctx] + args if ctx else args
@@ -572,20 +530,17 @@ def _handle_func(name, args, restype, errcheck, ctx=MpvHandle, deprecated=False)
572
530
  if errcheck is not None:
573
531
  func.errcheck = errcheck
574
532
  if deprecated:
575
-
576
533
  @wraps(func)
577
534
  def wrapper(*args, **kwargs):
578
- if not wrapper.warned: # Only warn on first invocation to prevent spamming
535
+ if not wrapper.warned: # Only warn on first invocation to prevent spamming
579
536
  warn("Backend C api has been deprecated: " + name, DeprecationWarning, stacklevel=2)
580
537
  wrapper.warned = True
581
538
  return func(*args, **kwargs)
582
-
583
539
  wrapper.warned = False
584
540
 
585
- globals()["_" + name] = wrapper
541
+ globals()['_'+name] = wrapper
586
542
  else:
587
- globals()["_" + name] = func
588
-
543
+ globals()['_'+name] = func
589
544
 
590
545
  def bytes_free_errcheck(res, func, *args):
591
546
  notnull_errcheck(res, func, *args)
@@ -593,32 +548,23 @@ def bytes_free_errcheck(res, func, *args):
593
548
  _mpv_free(res)
594
549
  return rv
595
550
 
596
-
597
551
  def notnull_errcheck(res, func, *args):
598
552
  if res is None:
599
- raise RuntimeError(
600
- "Underspecified error in MPV when calling {} with args {!r}: NULL pointer returned."
601
- "Please consult your local debugger.".format(func.__name__, args)
602
- )
553
+ raise RuntimeError('Underspecified error in MPV when calling {} with args {!r}: NULL pointer returned.'\
554
+ 'Please consult your local debugger.'.format(func.__name__, args))
603
555
  return res
604
556
 
605
-
606
557
  ec_errcheck = ErrorCode.raise_for_ec
607
558
 
608
559
  backend.mpv_client_api_version.restype = c_ulong
609
-
610
-
611
560
  def _mpv_client_api_version():
612
561
  ver = backend.mpv_client_api_version()
613
- return ver >> 16, ver & 0xFFFF
614
-
562
+ return ver>>16, ver&0xFFFF
615
563
 
616
564
  MPV_VERSION = _mpv_client_api_version()
617
565
  if MPV_VERSION < (1, 108):
618
- ver = ".".join(str(num) for num in MPV_VERSION)
619
- raise RuntimeError(
620
- f"python-mpv requires libmpv with an API version of 1.108 or higher (libmpv >= 0.33), but you have an older version ({ver})."
621
- )
566
+ ver = '.'.join(str(num) for num in MPV_VERSION)
567
+ raise RuntimeError(f"python-mpv requires libmpv with an API version of 1.108 or higher (libmpv >= 0.33), but you have an older version ({ver}).")
622
568
 
623
569
  backend.mpv_free.argtypes = [c_void_p]
624
570
  _mpv_free = backend.mpv_free
@@ -629,72 +575,67 @@ _mpv_free_node_contents = backend.mpv_free_node_contents
629
575
  backend.mpv_create.restype = MpvHandle
630
576
  _mpv_create = backend.mpv_create
631
577
 
632
- _handle_func("mpv_create_client", [c_char_p], MpvHandle, notnull_errcheck)
633
- _handle_func("mpv_create_weak_client", [c_char_p], MpvHandle, notnull_errcheck)
634
- _handle_func("mpv_client_name", [], c_char_p, errcheck=None)
635
- _handle_func("mpv_initialize", [], c_int, ec_errcheck)
636
- _handle_func("mpv_destroy", [], None, errcheck=None)
637
- _handle_func("mpv_terminate_destroy", [], None, errcheck=None)
638
- _handle_func("mpv_load_config_file", [c_char_p], c_int, ec_errcheck)
639
- _handle_func("mpv_get_time_us", [], c_ulonglong, errcheck=None)
640
-
641
- _handle_func("mpv_set_option", [c_char_p, MpvFormat, c_void_p], c_int, ec_errcheck)
642
- _handle_func("mpv_set_option_string", [c_char_p, c_char_p], c_int, ec_errcheck)
643
-
644
- _handle_func("mpv_command", [POINTER(c_char_p)], c_int, ec_errcheck)
645
- _handle_func("mpv_command_string", [c_char_p, c_char_p], c_int, ec_errcheck)
646
- _handle_func("mpv_command_async", [c_ulonglong, POINTER(c_char_p)], c_int, ec_errcheck)
647
- _handle_func("mpv_command_node", [POINTER(MpvNode), POINTER(MpvNode)], c_int, ec_errcheck)
648
- _handle_func("mpv_command_node_async", [c_ulonglong, POINTER(MpvNode)], c_int, ec_errcheck)
649
- _handle_func("mpv_abort_async_command", [c_ulonglong], None, errcheck=None)
650
-
651
- _handle_func("mpv_set_property", [c_char_p, MpvFormat, c_void_p], c_int, ec_errcheck)
652
- _handle_func("mpv_set_property_string", [c_char_p, c_char_p], c_int, ec_errcheck)
653
- _handle_func("mpv_set_property_async", [c_ulonglong, c_char_p, MpvFormat, c_void_p], c_int, ec_errcheck)
654
- _handle_func("mpv_get_property", [c_char_p, MpvFormat, c_void_p], c_int, ec_errcheck)
655
- _handle_func("mpv_get_property_string", [c_char_p], c_void_p, bytes_free_errcheck)
656
- _handle_func("mpv_get_property_osd_string", [c_char_p], c_void_p, bytes_free_errcheck)
657
- _handle_func("mpv_get_property_async", [c_ulonglong, c_char_p, MpvFormat], c_int, ec_errcheck)
658
- _handle_func("mpv_observe_property", [c_ulonglong, c_char_p, MpvFormat], c_int, ec_errcheck)
659
- _handle_func("mpv_unobserve_property", [c_ulonglong], c_int, ec_errcheck)
660
-
661
- _handle_func("mpv_event_name", [c_int], c_char_p, errcheck=None, ctx=None)
662
- _handle_func("mpv_event_to_node", [POINTER(MpvNode), POINTER(MpvEvent)], c_int, ec_errcheck, ctx=None)
663
- _handle_func("mpv_error_string", [c_int], c_char_p, errcheck=None, ctx=None)
664
-
665
- _handle_func("mpv_request_event", [MpvEventID, c_int], c_int, ec_errcheck)
666
- _handle_func("mpv_request_log_messages", [c_char_p], c_int, ec_errcheck)
667
- _handle_func("mpv_wait_event", [c_double], POINTER(MpvEvent), errcheck=None)
668
- _handle_func("mpv_wakeup", [], None, errcheck=None)
669
- _handle_func("mpv_set_wakeup_callback", [WakeupCallback, c_void_p], None, errcheck=None)
670
-
671
- _handle_func("mpv_stream_cb_add_ro", [c_char_p, c_void_p, StreamOpenFn], c_int, ec_errcheck)
672
-
673
- _handle_func(
674
- "mpv_render_context_create", [MpvRenderCtxHandle, MpvHandle, POINTER(MpvRenderParam)], c_int, ec_errcheck, ctx=None
675
- )
676
- _handle_func("mpv_render_context_set_parameter", [MpvRenderParam], c_int, ec_errcheck, ctx=MpvRenderCtxHandle)
677
- _handle_func("mpv_render_context_get_info", [MpvRenderParam], c_int, ec_errcheck, ctx=MpvRenderCtxHandle)
678
- _handle_func(
679
- "mpv_render_context_set_update_callback", [RenderUpdateFn, c_void_p], None, errcheck=None, ctx=MpvRenderCtxHandle
680
- )
681
- _handle_func("mpv_render_context_update", [], c_int64, errcheck=None, ctx=MpvRenderCtxHandle)
682
- _handle_func("mpv_render_context_render", [POINTER(MpvRenderParam)], c_int, ec_errcheck, ctx=MpvRenderCtxHandle)
683
- _handle_func("mpv_render_context_report_swap", [], None, errcheck=None, ctx=MpvRenderCtxHandle)
684
- _handle_func("mpv_render_context_free", [], None, errcheck=None, ctx=MpvRenderCtxHandle)
578
+ _handle_func('mpv_create_client', [c_char_p], MpvHandle, notnull_errcheck)
579
+ _handle_func('mpv_create_weak_client', [c_char_p], MpvHandle, notnull_errcheck)
580
+ _handle_func('mpv_client_name', [], c_char_p, errcheck=None)
581
+ _handle_func('mpv_initialize', [], c_int, ec_errcheck)
582
+ _handle_func('mpv_destroy', [], None, errcheck=None)
583
+ _handle_func('mpv_terminate_destroy', [], None, errcheck=None)
584
+ _handle_func('mpv_load_config_file', [c_char_p], c_int, ec_errcheck)
585
+ _handle_func('mpv_get_time_us', [], c_ulonglong, errcheck=None)
586
+
587
+ _handle_func('mpv_set_option', [c_char_p, MpvFormat, c_void_p], c_int, ec_errcheck)
588
+ _handle_func('mpv_set_option_string', [c_char_p, c_char_p], c_int, ec_errcheck)
589
+
590
+ _handle_func('mpv_command', [POINTER(c_char_p)], c_int, ec_errcheck)
591
+ _handle_func('mpv_command_string', [c_char_p, c_char_p], c_int, ec_errcheck)
592
+ _handle_func('mpv_command_async', [c_ulonglong, POINTER(c_char_p)], c_int, ec_errcheck)
593
+ _handle_func('mpv_command_node', [POINTER(MpvNode), POINTER(MpvNode)], c_int, ec_errcheck)
594
+ _handle_func('mpv_command_node_async', [c_ulonglong, POINTER(MpvNode)], c_int, ec_errcheck)
595
+ _handle_func('mpv_abort_async_command', [c_ulonglong], None, errcheck=None)
596
+
597
+ _handle_func('mpv_set_property', [c_char_p, MpvFormat, c_void_p], c_int, ec_errcheck)
598
+ _handle_func('mpv_set_property_string', [c_char_p, c_char_p], c_int, ec_errcheck)
599
+ _handle_func('mpv_set_property_async', [c_ulonglong, c_char_p, MpvFormat,c_void_p],c_int, ec_errcheck)
600
+ _handle_func('mpv_get_property', [c_char_p, MpvFormat, c_void_p], c_int, ec_errcheck)
601
+ _handle_func('mpv_get_property_string', [c_char_p], c_void_p, bytes_free_errcheck)
602
+ _handle_func('mpv_get_property_osd_string', [c_char_p], c_void_p, bytes_free_errcheck)
603
+ _handle_func('mpv_get_property_async', [c_ulonglong, c_char_p, MpvFormat], c_int, ec_errcheck)
604
+ _handle_func('mpv_observe_property', [c_ulonglong, c_char_p, MpvFormat], c_int, ec_errcheck)
605
+ _handle_func('mpv_unobserve_property', [c_ulonglong], c_int, ec_errcheck)
606
+
607
+ _handle_func('mpv_event_name', [c_int], c_char_p, errcheck=None, ctx=None)
608
+ _handle_func('mpv_event_to_node', [POINTER(MpvNode), POINTER(MpvEvent)], c_int, ec_errcheck, ctx=None)
609
+ _handle_func('mpv_error_string', [c_int], c_char_p, errcheck=None, ctx=None)
610
+
611
+ _handle_func('mpv_request_event', [MpvEventID, c_int], c_int, ec_errcheck)
612
+ _handle_func('mpv_request_log_messages', [c_char_p], c_int, ec_errcheck)
613
+ _handle_func('mpv_wait_event', [c_double], POINTER(MpvEvent), errcheck=None)
614
+ _handle_func('mpv_wakeup', [], None, errcheck=None)
615
+ _handle_func('mpv_set_wakeup_callback', [WakeupCallback, c_void_p], None, errcheck=None)
616
+
617
+ _handle_func('mpv_stream_cb_add_ro', [c_char_p, c_void_p, StreamOpenFn], c_int, ec_errcheck)
618
+
619
+ _handle_func('mpv_render_context_create', [MpvRenderCtxHandle, MpvHandle, POINTER(MpvRenderParam)], c_int, ec_errcheck, ctx=None)
620
+ _handle_func('mpv_render_context_set_parameter', [MpvRenderParam], c_int, ec_errcheck, ctx=MpvRenderCtxHandle)
621
+ _handle_func('mpv_render_context_get_info', [MpvRenderParam], c_int, ec_errcheck, ctx=MpvRenderCtxHandle)
622
+ _handle_func('mpv_render_context_set_update_callback', [RenderUpdateFn, c_void_p], None, errcheck=None, ctx=MpvRenderCtxHandle)
623
+ _handle_func('mpv_render_context_update', [], c_int64, errcheck=None, ctx=MpvRenderCtxHandle)
624
+ _handle_func('mpv_render_context_render', [POINTER(MpvRenderParam)], c_int, ec_errcheck, ctx=MpvRenderCtxHandle)
625
+ _handle_func('mpv_render_context_report_swap', [], None, errcheck=None, ctx=MpvRenderCtxHandle)
626
+ _handle_func('mpv_render_context_free', [], None, errcheck=None, ctx=MpvRenderCtxHandle)
685
627
 
686
628
 
687
629
  def _mpv_coax_proptype(value, proptype=str):
688
630
  """Intelligently coax the given python value into something that can be understood as a proptype property."""
689
631
  if type(value) is bytes:
690
- return value
632
+ return value;
691
633
  elif type(value) is bool:
692
- return b"yes" if value else b"no"
634
+ return b'yes' if value else b'no'
693
635
  elif proptype in (str, int, float):
694
- return str(proptype(value)).encode("utf-8")
636
+ return str(proptype(value)).encode('utf-8')
695
637
  else:
696
- raise TypeError("Cannot coax value of type {} into property type {}".format(type(value), proptype))
697
-
638
+ raise TypeError('Cannot coax value of type {} into property type {}'.format(type(value), proptype))
698
639
 
699
640
  def _make_node_str_list(l):
700
641
  """Take a list of python objects and make a MPV string node array from it.
@@ -714,25 +655,32 @@ def _make_node_str_list(l):
714
655
  }
715
656
  }
716
657
  """
717
- char_ps = [c_char_p(_mpv_coax_proptype(e, str)) for e in l]
658
+ char_ps = [ c_char_p(_mpv_coax_proptype(e, str)) for e in l ]
718
659
  node_list = MpvNodeList(
719
660
  num=len(l),
720
661
  keys=None,
721
- values=(MpvNode * len(l))(*[MpvNode(format=MpvFormat.STRING, val=MpvNodeUnion(string=p)) for p in char_ps]),
722
- )
723
- node = MpvNode(format=MpvFormat.NODE_ARRAY, val=MpvNodeUnion(list=pointer(node_list)))
662
+ values=( MpvNode * len(l))( *[ MpvNode(
663
+ format=MpvFormat.STRING,
664
+ val=MpvNodeUnion(string=p))
665
+ for p in char_ps ]))
666
+ node = MpvNode(
667
+ format=MpvFormat.NODE_ARRAY,
668
+ val=MpvNodeUnion(list=pointer(node_list)))
724
669
  return char_ps, node_list, node, cast(pointer(node), c_void_p)
725
670
 
726
-
727
671
  def _make_node_str_map(d):
728
- """Take a dict of python objects and make a MPV string node map from it."""
729
- char_ps = [(c_char_p(k.encode("utf-8")), c_char_p(_mpv_coax_proptype(v, str))) for k, v in d.items()]
672
+ """Take a dict of python objects and make a MPV string node map from it. """
673
+ char_ps = [ (c_char_p(k.encode('utf-8')), c_char_p(_mpv_coax_proptype(v, str))) for k, v in d.items() ]
730
674
  node_list = MpvNodeList(
731
675
  num=len(d),
732
- keys=(c_char_p * len(d))(*[k for k, v in char_ps]),
733
- values=(MpvNode * len(d))(*[MpvNode(format=MpvFormat.STRING, val=MpvNodeUnion(string=v)) for k, v in char_ps]),
734
- )
735
- node = MpvNode(format=MpvFormat.NODE_MAP, val=MpvNodeUnion(map=pointer(node_list)))
676
+ keys=( c_char_p * len(d))( *[k for k, v in char_ps] ),
677
+ values=( MpvNode * len(d))( *[ MpvNode(
678
+ format=MpvFormat.STRING,
679
+ val=MpvNodeUnion(string=v))
680
+ for k, v in char_ps ]))
681
+ node = MpvNode(
682
+ format=MpvFormat.NODE_MAP,
683
+ val=MpvNodeUnion(map=pointer(node_list)))
736
684
  return char_ps, node_list, node, cast(pointer(node), c_void_p)
737
685
 
738
686
 
@@ -745,29 +693,23 @@ def _event_generator(handle):
745
693
 
746
694
 
747
695
  def _create_null_term_cmd_arg_array(name, args):
748
- args = (
749
- [name.encode("utf-8")]
750
- + [(arg if type(arg) is bytes else str(arg).encode("utf-8")) for arg in args if arg is not None]
751
- + [None]
752
- )
696
+ args = [name.encode('utf-8')] + [(arg if type(arg) is bytes else str(arg).encode('utf-8'))
697
+ for arg in args if arg is not None] + [None]
753
698
  return (c_char_p * len(args))(*args)
754
699
 
755
700
 
756
- _py_to_mpv = lambda name: name.replace("_", "-")
757
- _mpv_to_py = lambda name: name.replace("-", "_")
758
-
759
- _drop_nones = lambda *args: [arg for arg in args if arg is not None]
701
+ _py_to_mpv = lambda name: name.replace('_', '-')
702
+ _mpv_to_py = lambda name: name.replace('-', '_')
760
703
 
704
+ _drop_nones = lambda *args: [ arg for arg in args if arg is not None ]
761
705
 
762
706
  class _Proxy:
763
707
  def __init__(self, mpv):
764
- super().__setattr__("mpv", mpv)
765
-
708
+ super().__setattr__('mpv', mpv)
766
709
 
767
710
  class _PropertyProxy(_Proxy):
768
711
  def __dir__(self):
769
- return super().__dir__() + [name.replace("-", "_") for name in self.mpv.property_list]
770
-
712
+ return super().__dir__() + [ name.replace('-', '_') for name in self.mpv.property_list ]
771
713
 
772
714
  class _FileLocalProxy(_Proxy):
773
715
  def __getitem__(self, name):
@@ -779,19 +721,17 @@ class _FileLocalProxy(_Proxy):
779
721
  def __iter__(self):
780
722
  return iter(self.mpv)
781
723
 
782
-
783
724
  class _OSDPropertyProxy(_PropertyProxy):
784
725
  def __getattr__(self, name):
785
726
  return self.mpv._get_property(_py_to_mpv(name), fmt=MpvFormat.OSD_STRING)
786
727
 
787
728
  def __setattr__(self, _name, _value):
788
- raise AttributeError("OSD properties are read-only. Please use the regular property API for writing.")
789
-
729
+ raise AttributeError('OSD properties are read-only. Please use the regular property API for writing.')
790
730
 
791
731
  class _DecoderPropertyProxy(_PropertyProxy):
792
732
  def __init__(self, mpv, decoder):
793
733
  super().__init__(mpv)
794
- super().__setattr__("_decoder", decoder)
734
+ super().__setattr__('_decoder', decoder)
795
735
 
796
736
  def __getattr__(self, name):
797
737
  return self.mpv._get_property(_py_to_mpv(name), decoder=self._decoder)
@@ -799,7 +739,6 @@ class _DecoderPropertyProxy(_PropertyProxy):
799
739
  def __setattr__(self, name, value):
800
740
  setattr(self.mpv, _py_to_mpv(name), value)
801
741
 
802
-
803
742
  class GeneratorStream:
804
743
  """Transform a python generator into an mpv-compatible stream object. The total size of the file can be indicated to
805
744
  mpv using the size argument to __init__. Seeking is not supported.
@@ -811,8 +750,8 @@ class GeneratorStream:
811
750
 
812
751
  def seek(self, offset):
813
752
  self._read_iter = iter(self._generator_fun())
814
- self._read_chunk = b""
815
- return 0 # We only support seeking to the first byte atm
753
+ self._read_chunk = b''
754
+ return 0 # We only support seeking to the first byte atm
816
755
  # implementation in case seeking to arbitrary offsets would be necessary
817
756
  # while offset > 0:
818
757
  # offset -= len(self.read(offset))
@@ -823,15 +762,15 @@ class GeneratorStream:
823
762
  try:
824
763
  self._read_chunk += next(self._read_iter)
825
764
  except StopIteration:
826
- return b""
765
+ return b''
827
766
  rv, self._read_chunk = self._read_chunk[:size], self._read_chunk[size:]
828
767
  return rv
829
768
 
830
769
  def close(self):
831
- self._read_iter = iter([]) # make next read() call return EOF
770
+ self._read_iter = iter([]) # make next read() call return EOF
832
771
 
833
772
  def cancel(self):
834
- self._read_iter = iter([]) # make next read() call return EOF
773
+ self._read_iter = iter([]) # make next read() call return EOF
835
774
 
836
775
 
837
776
  class ImageOverlay:
@@ -845,38 +784,37 @@ class ImageOverlay:
845
784
 
846
785
  def update(self, img=None, pos=None):
847
786
  from PIL import Image
848
-
849
787
  if img is not None:
850
788
  self.img = img
851
789
  img = self.img
852
790
 
853
791
  w, h = img.size
854
- stride = w * 4
792
+ stride = w*4
855
793
 
856
794
  if pos is not None:
857
795
  self.pos = pos
858
796
  x, y = self.pos
859
797
 
860
798
  # Pre-multiply alpha channel
861
- bg = Image.new("RGBA", (w, h), (0, 0, 0, 0))
799
+ bg = Image.new('RGBA', (w, h), (0, 0, 0, 0))
862
800
  out = Image.alpha_composite(bg, img)
863
801
 
864
802
  # Copy image to ctypes buffer
865
803
  if img.size != self._size:
866
- self._buf = create_string_buffer(w * h * 4)
804
+ self._buf = create_string_buffer(w*h*4)
867
805
  self._size = img.size
868
806
 
869
- ctypes.memmove(self._buf, out.tobytes("raw", "BGRA"), w * h * 4)
870
- source = "&" + str(addressof(self._buf))
807
+ ctypes.memmove(self._buf, out.tobytes('raw', 'BGRA'), w*h*4)
808
+ source = '&' + str(addressof(self._buf))
871
809
 
872
- self.m.overlay_add(self.overlay_id, x, y, source, 0, "bgra", w, h, stride)
810
+ self.m.overlay_add(self.overlay_id, x, y, source, 0, 'bgra', w, h, stride)
873
811
 
874
812
  def remove(self):
875
813
  self.m.remove_overlay(self.overlay_id)
876
814
 
877
815
 
878
816
  class FileOverlay:
879
- def __init__(self, m, overlay_id, filename=None, size=None, stride=None, pos=(0, 0)):
817
+ def __init__(self, m, overlay_id, filename=None, size=None, stride=None, pos=(0,0)):
880
818
  self.m = m
881
819
  self.overlay_id = overlay_id
882
820
  self.pos = pos
@@ -900,9 +838,9 @@ class FileOverlay:
900
838
 
901
839
  x, y = self.pos
902
840
  w, h = self.size
903
- stride = self.stride or 4 * w
841
+ stride = self.stride or 4*w
904
842
 
905
- self.m.overlay_add(self, self.overlay_id, x, y, self.filename, 0, "bgra", w, h, stride)
843
+ self.m.overlay_add(self, self.overlay_id, x, y, self.filename, 0, 'bgra', w, h, stride)
906
844
 
907
845
  def remove(self):
908
846
  self.m.remove_overlay(self.overlay_id)
@@ -922,7 +860,7 @@ class MPV(object):
922
860
  underscore_names exposed on the python object.
923
861
 
924
862
  To make your program not barf hard the first time its used on a weird file system **always** access properties
925
- containing file names or file tags through ``MPV.raw``."""
863
+ containing file names or file tags through ``MPV.raw``. """
926
864
 
927
865
  def __init__(self, *extra_mpv_flags, log_handler=None, start_event_thread=True, loglevel=None, **extra_mpv_opts):
928
866
  """Create an MPV instance.
@@ -934,21 +872,21 @@ class MPV(object):
934
872
  self._event_thread = None
935
873
  self._core_shutdown = False
936
874
 
937
- _mpv_set_option_string(self.handle, b"audio-display", b"no")
938
- istr = lambda o: ("yes" if o else "no") if type(o) is bool else str(o)
875
+ _mpv_set_option_string(self.handle, b'audio-display', b'no')
876
+ istr = lambda o: ('yes' if o else 'no') if type(o) is bool else str(o)
939
877
  try:
940
878
  for flag in extra_mpv_flags:
941
- _mpv_set_option_string(self.handle, flag.encode("utf-8"), b"")
942
- for k, v in extra_mpv_opts.items():
943
- _mpv_set_option_string(self.handle, k.replace("_", "-").encode("utf-8"), istr(v).encode("utf-8"))
879
+ _mpv_set_option_string(self.handle, flag.encode('utf-8'), b'')
880
+ for k,v in extra_mpv_opts.items():
881
+ _mpv_set_option_string(self.handle, k.replace('_', '-').encode('utf-8'), istr(v).encode('utf-8'))
944
882
  finally:
945
883
  _mpv_initialize(self.handle)
946
884
 
947
885
  self.osd = _OSDPropertyProxy(self)
948
886
  self.file_local = _FileLocalProxy(self)
949
- self.raw = _DecoderPropertyProxy(self, identity_decoder)
887
+ self.raw = _DecoderPropertyProxy(self, identity_decoder)
950
888
  self.strict = _DecoderPropertyProxy(self, strict_decoder)
951
- self.lazy = _DecoderPropertyProxy(self, lazy_decoder)
889
+ self.lazy = _DecoderPropertyProxy(self, lazy_decoder)
952
890
 
953
891
  self._event_callbacks = []
954
892
  self._command_reply_callbacks = {}
@@ -957,23 +895,40 @@ class MPV(object):
957
895
  self._quit_handlers = set()
958
896
  self._message_handlers = {}
959
897
  self._key_binding_handlers = {}
960
- self._event_handle = _mpv_create_client(self.handle, b"py_event_handler")
898
+ self._event_handle = _mpv_create_client(self.handle, b'py_event_handler')
961
899
  self._log_handler = log_handler
962
900
  self._stream_protocol_cbs = {}
963
901
  self._stream_protocol_frontends = collections.defaultdict(lambda: {})
964
- self.register_stream_protocol("python", self._python_stream_open)
902
+ self.register_stream_protocol('python', self._python_stream_open)
965
903
  self._python_streams = {}
966
904
  self._python_stream_catchall = None
905
+ self._exception_futures = set()
967
906
  self.overlay_ids = set()
968
907
  self.overlays = {}
969
908
  if loglevel is not None or log_handler is not None:
970
- self.set_loglevel(loglevel or "terminal-default")
909
+ self.set_loglevel(loglevel or 'terminal-default')
971
910
  if start_event_thread:
972
- self._event_thread = threading.Thread(target=self._loop, name="MPVEventHandlerThread")
911
+ self._event_thread = threading.Thread(target=self._loop, name='MPVEventHandlerThread')
973
912
  self._event_thread.daemon = True
974
913
  self._event_thread.start()
975
914
  else:
976
915
  self._event_thread = None
916
+ if (m := re.search(r'(\d+)\.(\d+)\.(\d+)', self.mpv_version)):
917
+ self.mpv_version_tuple = tuple(map(int, m.groups()))
918
+
919
+ @contextmanager
920
+ def _enqueue_exceptions(self):
921
+ try:
922
+ yield
923
+ except Exception as e:
924
+ for fut in self._exception_futures:
925
+ try:
926
+ fut.set_exception(e)
927
+ break
928
+ except InvalidStateError:
929
+ pass
930
+ else:
931
+ warn(f'Unhandled exception on python-mpv event loop: {e}\n{traceback.format_exc()}', RuntimeWarning)
977
932
 
978
933
  def _loop(self):
979
934
  for event in _event_generator(self._event_handle):
@@ -985,50 +940,51 @@ class MPV(object):
985
940
  self._core_shutdown = True
986
941
 
987
942
  for callback in self._event_callbacks:
988
- callback(event)
943
+ with self._enqueue_exceptions():
944
+ callback(event)
989
945
 
990
946
  if eid == MpvEventID.PROPERTY_CHANGE:
991
947
  pc = event.data
992
948
  name, value, _fmt = pc.name, pc.value, pc.format
993
949
  for handler in self._property_handlers[name]:
994
- handler(name, value)
950
+ with self._enqueue_exceptions():
951
+ handler(name, value)
995
952
 
996
953
  if eid == MpvEventID.LOG_MESSAGE and self._log_handler is not None:
997
954
  ev = event.data
998
- self._log_handler(ev.level, ev.prefix, ev.text)
955
+ with self._enqueue_exceptions():
956
+ self._log_handler(ev.level, ev.prefix, ev.text)
999
957
 
1000
958
  if eid == MpvEventID.CLIENT_MESSAGE:
1001
959
  # {'event': {'args': ['key-binding', 'foo', 'u-', 'g']}, 'reply_userdata': 0, 'error': 0, 'event_id': 16}
1002
960
  target, *args = event.data.args
1003
961
  target = target.decode("utf-8")
1004
962
  if target in self._message_handlers:
1005
- self._message_handlers[target](*args)
963
+ with self._enqueue_exceptions():
964
+ self._message_handlers[target](*args)
1006
965
 
1007
966
  if eid == MpvEventID.COMMAND_REPLY:
1008
967
  key = event.reply_userdata
1009
968
  callback = self._command_reply_callbacks.pop(key, None)
1010
969
  if callback:
1011
- callback(ErrorCode.exception_for_ec(event.error), event.data)
970
+ with self._enqueue_exceptions():
971
+ callback(ErrorCode.exception_for_ec(event.error), event.data)
1012
972
 
1013
973
  if eid == MpvEventID.QUEUE_OVERFLOW:
1014
974
  # cache list, since error handlers will unregister themselves
1015
975
  for cb in list(self._command_reply_callbacks.values()):
1016
- cb(
1017
- EventOverflowError(
1018
- "libmpv event queue has flown over because events have not been processed fast enough"
1019
- ),
1020
- None,
1021
- )
976
+ with self._enqueue_exceptions():
977
+ cb(EventOverflowError('libmpv event queue has flown over because events have not been processed fast enough'), None)
1022
978
 
1023
979
  if eid == MpvEventID.SHUTDOWN:
1024
980
  _mpv_destroy(self._event_handle)
1025
981
  for cb in list(self._command_reply_callbacks.values()):
1026
- cb(ShutdownError("libmpv core has been shutdown"), None)
982
+ with self._enqueue_exceptions():
983
+ cb(ShutdownError('libmpv core has been shutdown'), None)
1027
984
  return
1028
985
 
1029
986
  except Exception as e:
1030
- print("Exception inside python-mpv event loop:", file=sys.stderr)
1031
- traceback.print_exc()
987
+ warn(f'Unhandled {e} inside python-mpv event loop!\n{traceback.format_exc()}', RuntimeWarning)
1032
988
 
1033
989
  @property
1034
990
  def core_shutdown(self):
@@ -1037,63 +993,58 @@ class MPV(object):
1037
993
  return self._core_shutdown
1038
994
 
1039
995
  def check_core_alive(self):
1040
- """This method can be used as a sanity check to tests whether the core is still alive at the time it is
996
+ """ This method can be used as a sanity check to tests whether the core is still alive at the time it is
1041
997
  called."""
1042
998
  if self._core_shutdown:
1043
- raise ShutdownError("libmpv core has been shutdown")
999
+ raise ShutdownError('libmpv core has been shutdown')
1044
1000
 
1045
- def wait_until_paused(self, timeout=None):
1001
+ def wait_until_paused(self, timeout=None, catch_errors=True):
1046
1002
  """Waits until playback of the current title is paused or done. Raises a ShutdownError if the core is shutdown while
1047
1003
  waiting."""
1048
- self.wait_for_property("core-idle", timeout=timeout)
1004
+ self.wait_for_property('core-idle', timeout=timeout, catch_errors=catch_errors)
1049
1005
 
1050
- def wait_for_playback(self, timeout=None):
1006
+ def wait_for_playback(self, timeout=None, catch_errors=True):
1051
1007
  """Waits until playback of the current title is finished. Raises a ShutdownError if the core is shutdown while
1052
1008
  waiting.
1053
1009
  """
1054
- self.wait_for_event("end_file", timeout=timeout)
1010
+ self.wait_for_event('end_file', timeout=timeout, catch_errors=catch_errors)
1055
1011
 
1056
- def wait_until_playing(self, timeout=None):
1012
+ def wait_until_playing(self, timeout=None, catch_errors=True):
1057
1013
  """Waits until playback of the current title has started. Raises a ShutdownError if the core is shutdown while
1058
1014
  waiting."""
1059
- self.wait_for_property("core-idle", lambda idle: not idle, timeout=timeout)
1015
+ self.wait_for_property('core-idle', lambda idle: not idle, timeout=timeout, catch_errors=catch_errors)
1060
1016
 
1061
- def wait_for_property(self, name, cond=lambda val: val, level_sensitive=True, timeout=None):
1017
+ def wait_for_property(self, name, cond=lambda val: val, level_sensitive=True, timeout=None, catch_errors=True):
1062
1018
  """Waits until ``cond`` evaluates to a truthy value on the named property. This can be used to wait for
1063
1019
  properties such as ``idle_active`` indicating the player is done with regular playback and just idling around.
1064
1020
  Raises a ShutdownError when the core is shutdown while waiting.
1065
1021
  """
1066
- with self.prepare_and_wait_for_property(name, cond, level_sensitive, timeout=timeout) as result:
1022
+ with self.prepare_and_wait_for_property(name, cond, level_sensitive, timeout=timeout, catch_errors=catch_errors) as result:
1067
1023
  pass
1068
1024
  return result.result()
1069
1025
 
1070
- def wait_for_shutdown(self, timeout=None):
1071
- """Wait for core to shutdown (e.g. through quit() or terminate())."""
1026
+ def wait_for_shutdown(self, timeout=None, catch_errors=True):
1027
+ '''Wait for core to shutdown (e.g. through quit() or terminate()).'''
1072
1028
  try:
1073
- self.wait_for_event(None, timeout=timeout)
1029
+ self.wait_for_event(None, timeout=timeout, catch_errors=catch_errors)
1074
1030
  except ShutdownError:
1075
1031
  return
1076
1032
 
1077
1033
  def _set_error_handler(self, future):
1078
- @self.event_callback("shutdown", "queue-overflow")
1034
+ @self.event_callback('shutdown', 'queue-overflow')
1079
1035
  def shutdown_handler(event):
1080
1036
  nonlocal future
1081
1037
  try:
1082
1038
  if event.event_id.value == MpvEventID.SHUTDOWN:
1083
- future.set_exception(ShutdownError("libmpv core has been shutdown"))
1039
+ future.set_exception(ShutdownError('libmpv core has been shutdown'))
1084
1040
  else:
1085
- future.set_exception(
1086
- EventOverflowError(
1087
- "libmpv event queue has flown over because events have not been processed fast enough"
1088
- )
1089
- )
1041
+ future.set_exception(EventOverflowError('libmpv event queue has flown over because events have not been processed fast enough'))
1090
1042
  except InvalidStateError:
1091
1043
  pass
1092
-
1093
1044
  return shutdown_handler.unregister_mpv_events
1094
1045
 
1095
1046
  @contextmanager
1096
- def prepare_and_wait_for_property(self, name, cond=lambda val: val, level_sensitive=True, timeout=None):
1047
+ def prepare_and_wait_for_property(self, name, cond=lambda val: val, level_sensitive=True, timeout=None, catch_errors=True):
1097
1048
  """Context manager that waits until ``cond`` evaluates to a truthy value on the named property. See
1098
1049
  prepare_and_wait_for_event for usage.
1099
1050
  Raises a ShutdownError when the core is shutdown while waiting. Re-raises any errors inside ``cond``.
@@ -1105,43 +1056,54 @@ class MPV(object):
1105
1056
  rv = cond(val)
1106
1057
  if rv:
1107
1058
  result.set_result(rv)
1059
+
1060
+ except InvalidStateError:
1061
+ pass
1062
+
1108
1063
  except Exception as e:
1109
1064
  try:
1110
1065
  result.set_exception(e)
1111
- except InvalidStateError:
1066
+ except:
1112
1067
  pass
1113
- except InvalidStateError:
1114
- pass
1115
-
1116
- self.observe_property(name, observer)
1117
- err_unregister = self._set_error_handler(result)
1118
1068
 
1119
1069
  try:
1120
1070
  result.set_running_or_notify_cancel()
1071
+
1072
+ self.observe_property(name, observer)
1073
+ err_unregister = self._set_error_handler(result)
1074
+ if catch_errors:
1075
+ self._exception_futures.add(result)
1076
+
1121
1077
  yield result
1122
1078
 
1123
- rv = cond(getattr(self, name.replace("-", "_")))
1124
- if level_sensitive and rv:
1125
- result.set_result(rv)
1079
+ if level_sensitive:
1080
+ rv = cond(getattr(self, name.replace('-', '_')))
1081
+ if rv:
1082
+ result.set_result(rv)
1083
+ return
1084
+
1085
+ self.check_core_alive()
1086
+ result.result(timeout)
1087
+
1088
+ except InvalidStateError:
1089
+ pass
1126
1090
 
1127
- else:
1128
- self.check_core_alive()
1129
- result.result(timeout)
1130
1091
  finally:
1131
1092
  err_unregister()
1132
1093
  self.unobserve_property(name, observer)
1094
+ self._exception_futures.discard(result)
1133
1095
 
1134
- def wait_for_event(self, *event_types, cond=lambda evt: True, timeout=None):
1096
+ def wait_for_event(self, *event_types, cond=lambda evt: True, timeout=None, catch_errors=True):
1135
1097
  """Waits for the indicated event(s). If cond is given, waits until cond(event) is true. Raises a ShutdownError
1136
1098
  if the core is shutdown while waiting. This also happens when 'shutdown' is in event_types. Re-raises any error
1137
1099
  inside ``cond``.
1138
1100
  """
1139
- with self.prepare_and_wait_for_event(*event_types, cond=cond, timeout=timeout) as result:
1101
+ with self.prepare_and_wait_for_event(*event_types, cond=cond, timeout=timeout, catch_errors=catch_errors) as result:
1140
1102
  pass
1141
1103
  return result.result()
1142
1104
 
1143
1105
  @contextmanager
1144
- def prepare_and_wait_for_event(self, *event_types, cond=lambda evt: True, timeout=None):
1106
+ def prepare_and_wait_for_event(self, *event_types, cond=lambda evt: True, timeout=None, catch_errors=True):
1145
1107
  """Context manager that waits for the indicated event(s) like wait_for_event after running. If cond is given,
1146
1108
  waits until cond(event) is true. Raises a ShutdownError if the core is shutdown while waiting. This also happens
1147
1109
  when 'shutdown' is in event_types. Re-raises any error inside ``cond``.
@@ -1159,7 +1121,6 @@ class MPV(object):
1159
1121
 
1160
1122
  @self.event_callback(*event_types)
1161
1123
  def target_handler(evt):
1162
-
1163
1124
  try:
1164
1125
  rv = cond(evt)
1165
1126
  if rv:
@@ -1176,13 +1137,18 @@ class MPV(object):
1176
1137
 
1177
1138
  try:
1178
1139
  result.set_running_or_notify_cancel()
1140
+ if catch_errors:
1141
+ self._exception_futures.add(result)
1142
+
1179
1143
  yield result
1144
+
1180
1145
  self.check_core_alive()
1181
1146
  result.result(timeout)
1182
1147
 
1183
1148
  finally:
1184
1149
  err_unregister()
1185
1150
  target_handler.unregister_mpv_events()
1151
+ self._exception_futures.discard(result)
1186
1152
 
1187
1153
  def __del__(self):
1188
1154
  if self.handle:
@@ -1196,12 +1162,10 @@ class MPV(object):
1196
1162
  """
1197
1163
  self.handle, handle = None, self.handle
1198
1164
  if threading.current_thread() is self._event_thread:
1199
- raise UserWarning(
1200
- "terminate() should not be called from event thread (e.g. from a callback function). If "
1201
- "you want to terminate mpv from here, please call quit() instead, then sync the main thread "
1202
- "against the event thread using e.g. wait_for_shutdown(), then terminate() from the main thread. "
1203
- "This call has been transformed into a call to quit()."
1204
- )
1165
+ raise UserWarning('terminate() should not be called from event thread (e.g. from a callback function). If '
1166
+ 'you want to terminate mpv from here, please call quit() instead, then sync the main thread '
1167
+ 'against the event thread using e.g. wait_for_shutdown(), then terminate() from the main thread. '
1168
+ 'This call has been transformed into a call to quit().')
1205
1169
  self.quit()
1206
1170
  else:
1207
1171
  _mpv_terminate_destroy(handle)
@@ -1216,7 +1180,7 @@ class MPV(object):
1216
1180
  Valid log levels are "no", "fatal", "error", "warn", "info", "v" "debug" and "trace". For details see your mpv's
1217
1181
  client.h header file.
1218
1182
  """
1219
- _mpv_request_log_messages(self._event_handle, level.encode("utf-8"))
1183
+ _mpv_request_log_messages(self._event_handle, level.encode('utf-8'))
1220
1184
 
1221
1185
  def string_command(self, name, *args):
1222
1186
  """Execute a raw command."""
@@ -1241,7 +1205,6 @@ class MPV(object):
1241
1205
  future.set_running_or_notify_cancel()
1242
1206
 
1243
1207
  if callback is None:
1244
-
1245
1208
  def callback(error, result):
1246
1209
  if error:
1247
1210
  raise error
@@ -1260,17 +1223,14 @@ class MPV(object):
1260
1223
  def abort():
1261
1224
  _mpv_abort_async_command(self._event_handle, id(future))
1262
1225
  del self._command_reply_callbacks[id(future)]
1263
-
1264
1226
  future.cancel = abort
1265
1227
 
1266
1228
  self._command_reply_callbacks[id(future)] = wrapper
1267
1229
 
1268
1230
  if kwargs:
1269
1231
  if args:
1270
- raise ValueError(
1271
- "Can only call mpv commands either using positional or using named arguments, not a mix of both."
1272
- )
1273
- kwargs["name"] = name
1232
+ raise ValueError('Can only call mpv commands either using positional or using named arguments, not a mix of both.')
1233
+ kwargs['name'] = name
1274
1234
  _1, _2, _3, pointer = _make_node_str_map(kwargs)
1275
1235
  else:
1276
1236
  _1, _2, _3, pointer = _make_node_str_list([name, *args])
@@ -1279,16 +1239,15 @@ class MPV(object):
1279
1239
  _mpv_command_node_async(self._event_handle, id(future), ppointer)
1280
1240
  return future
1281
1241
 
1242
+
1282
1243
  def node_command(self, name, *args, decoder=strict_decoder):
1283
1244
  self.command(name, *args, decoder=decoder)
1284
1245
 
1285
1246
  def command(self, name, *args, decoder=strict_decoder, **kwargs):
1286
1247
  if kwargs:
1287
1248
  if args:
1288
- raise ValueError(
1289
- "Can only call mpv commands either using positional or using named arguments, not a mix of both."
1290
- )
1291
- kwargs["name"] = name
1249
+ raise ValueError('Can only call mpv commands either using positional or using named arguments, not a mix of both.')
1250
+ kwargs['name'] = name
1292
1251
  _1, _2, _3, pointer = _make_node_str_map(kwargs)
1293
1252
  else:
1294
1253
  _1, _2, _3, pointer = _make_node_str_list([name, *args])
@@ -1302,61 +1261,59 @@ class MPV(object):
1302
1261
 
1303
1262
  def seek(self, amount, reference="relative", precision="keyframes"):
1304
1263
  """Mapped mpv seek command, see man mpv(1)."""
1305
- self.command("seek", amount, reference, precision)
1264
+ self.command('seek', amount, reference, precision)
1306
1265
 
1307
1266
  def revert_seek(self):
1308
1267
  """Mapped mpv revert_seek command, see man mpv(1)."""
1309
- self.command("revert_seek")
1268
+ self.command('revert_seek');
1310
1269
 
1311
1270
  def frame_step(self):
1312
1271
  """Mapped mpv frame-step command, see man mpv(1)."""
1313
- self.command("frame-step")
1272
+ self.command('frame-step')
1314
1273
 
1315
1274
  def frame_back_step(self):
1316
1275
  """Mapped mpv frame_back_step command, see man mpv(1)."""
1317
- self.command("frame_back_step")
1276
+ self.command('frame_back_step')
1318
1277
 
1319
1278
  def property_add(self, name, value=1):
1320
1279
  """Add the given value to the property's value. On overflow or underflow, clamp the property to the maximum. If
1321
1280
  ``value`` is omitted, assume ``1``.
1322
1281
  """
1323
- self.command("add", name, value)
1282
+ self.command('add', name, value)
1324
1283
 
1325
1284
  def property_multiply(self, name, factor):
1326
1285
  """Multiply the value of a property with a numeric factor."""
1327
- self.command("multiply", name, factor)
1286
+ self.command('multiply', name, factor)
1328
1287
 
1329
- def cycle(self, name, direction="up"):
1288
+ def cycle(self, name, direction='up'):
1330
1289
  """Cycle the given property. ``up`` and ``down`` set the cycle direction. On overflow, set the property back to
1331
1290
  the minimum, on underflow set it to the maximum. If ``up`` or ``down`` is omitted, assume ``up``.
1332
1291
  """
1333
- self.command("cycle", name, direction)
1292
+ self.command('cycle', name, direction)
1334
1293
 
1335
- def screenshot(self, includes="subtitles", mode="single"):
1294
+ def screenshot(self, includes='subtitles', mode='single'):
1336
1295
  """Mapped mpv screenshot command, see man mpv(1)."""
1337
- self.command("screenshot", includes, mode)
1296
+ self.command('screenshot', includes, mode)
1338
1297
 
1339
- def screenshot_to_file(self, filename, includes="subtitles"):
1298
+ def screenshot_to_file(self, filename, includes='subtitles'):
1340
1299
  """Mapped mpv screenshot_to_file command, see man mpv(1)."""
1341
- self.command("screenshot_to_file", filename.encode(fs_enc), includes)
1300
+ self.command('screenshot_to_file', filename.encode(fs_enc), includes)
1342
1301
 
1343
- def screenshot_raw(self, includes="subtitles"):
1302
+ def screenshot_raw(self, includes='subtitles'):
1344
1303
  """Mapped mpv screenshot_raw command, see man mpv(1). Returns a pillow Image object."""
1345
1304
  from PIL import Image
1346
-
1347
- res = self.command("screenshot-raw", includes)
1348
- if res["format"] != "bgr0":
1349
- raise ValueError(
1350
- 'Screenshot in unknown format "{}". Currently, only bgr0 is supported.'.format(res["format"])
1351
- )
1352
- img = Image.frombytes("RGBA", (res["stride"] // 4, res["h"]), res["data"])
1353
- b, g, r, a = img.split()
1354
- return Image.merge("RGB", (r, g, b))
1305
+ res = self.command('screenshot-raw', includes)
1306
+ if res['format'] != 'bgr0':
1307
+ raise ValueError('Screenshot in unknown format "{}". Currently, only bgr0 is supported.'
1308
+ .format(res['format']))
1309
+ img = Image.frombytes('RGBA', (res['stride']//4, res['h']), res['data'])
1310
+ b,g,r,a = img.split()
1311
+ return Image.merge('RGB', (r,g,b))
1355
1312
 
1356
1313
  def allocate_overlay_id(self):
1357
1314
  free_ids = set(range(64)) - self.overlay_ids
1358
1315
  if not free_ids:
1359
- raise IndexError("All overlay IDs are in use")
1316
+ raise IndexError('All overlay IDs are in use')
1360
1317
  next_id, *_ = sorted(free_ids)
1361
1318
  self.overlay_ids.add(next_id)
1362
1319
  return next_id
@@ -1364,13 +1321,13 @@ class MPV(object):
1364
1321
  def free_overlay_id(self, overlay_id):
1365
1322
  self.overlay_ids.remove(overlay_id)
1366
1323
 
1367
- def create_file_overlay(self, filename=None, size=None, stride=None, pos=(0, 0)):
1324
+ def create_file_overlay(self, filename=None, size=None, stride=None, pos=(0,0)):
1368
1325
  overlay_id = self.allocate_overlay_id()
1369
1326
  overlay = FileOverlay(self, overlay_id, filename, size, stride, pos)
1370
1327
  self.overlays[overlay_id] = overlay
1371
1328
  return overlay
1372
1329
 
1373
- def create_image_overlay(self, img=None, pos=(0, 0)):
1330
+ def create_image_overlay(self, img=None, pos=(0,0)):
1374
1331
  overlay_id = self.allocate_overlay_id()
1375
1332
  overlay = ImageOverlay(self, overlay_id, img, pos)
1376
1333
  self.overlays[overlay_id] = overlay
@@ -1381,207 +1338,219 @@ class MPV(object):
1381
1338
  self.free_overlay_id(overlay_id)
1382
1339
  del self.overlays[overlay_id]
1383
1340
 
1384
- def playlist_next(self, mode="weak"):
1341
+ def playlist_next(self, mode='weak'):
1385
1342
  """Mapped mpv playlist_next command, see man mpv(1)."""
1386
- self.command("playlist_next", mode)
1343
+ self.command('playlist_next', mode)
1387
1344
 
1388
- def playlist_prev(self, mode="weak"):
1345
+ def playlist_prev(self, mode='weak'):
1389
1346
  """Mapped mpv playlist_prev command, see man mpv(1)."""
1390
- self.command("playlist_prev", mode)
1347
+ self.command('playlist_prev', mode)
1391
1348
 
1392
1349
  def playlist_play_index(self, idx):
1393
1350
  """Mapped mpv playlist-play-index command, see man mpv(1)."""
1394
- self.command("playlist-play-index", idx)
1351
+ self.command('playlist-play-index', idx)
1395
1352
 
1396
1353
  @staticmethod
1397
1354
  def _encode_options(options):
1398
- return ",".join("{}={}".format(_py_to_mpv(str(key)), str(val)) for key, val in options.items())
1355
+ return ','.join('{}={}'.format(_py_to_mpv(str(key)), str(val)) for key, val in options.items())
1399
1356
 
1400
- def loadfile(self, filename, mode="replace", **options):
1357
+ def loadfile(self, filename, mode='replace', index=None, **options):
1401
1358
  """Mapped mpv loadfile command, see man mpv(1)."""
1402
- self.command("loadfile", filename.encode(fs_enc), mode, MPV._encode_options(options))
1359
+ if self.mpv_version_tuple >= (0, 38, 0):
1360
+ if index is None:
1361
+ index = -1
1362
+ self.command('loadfile', filename.encode(fs_enc), mode, index, MPV._encode_options(options))
1363
+ else:
1364
+ if index is not None:
1365
+ warn(f'The index argument to the loadfile command is only supported on mpv >= 0.38.0')
1366
+ self.command('loadfile', filename.encode(fs_enc), mode, MPV._encode_options(options))
1403
1367
 
1404
- def loadlist(self, playlist, mode="replace"):
1368
+ def loadlist(self, playlist, mode='replace'):
1405
1369
  """Mapped mpv loadlist command, see man mpv(1)."""
1406
- self.command("loadlist", playlist.encode(fs_enc), mode)
1370
+ self.command('loadlist', playlist.encode(fs_enc), mode)
1407
1371
 
1408
1372
  def playlist_clear(self):
1409
1373
  """Mapped mpv playlist_clear command, see man mpv(1)."""
1410
- self.command("playlist_clear")
1374
+ self.command('playlist_clear')
1411
1375
 
1412
- def playlist_remove(self, index="current"):
1376
+ def playlist_remove(self, index='current'):
1413
1377
  """Mapped mpv playlist_remove command, see man mpv(1)."""
1414
- self.command("playlist_remove", index)
1378
+ self.command('playlist_remove', index)
1415
1379
 
1416
1380
  def playlist_move(self, index1, index2):
1417
1381
  """Mapped mpv playlist_move command, see man mpv(1)."""
1418
- self.command("playlist_move", index1, index2)
1382
+ self.command('playlist_move', index1, index2)
1419
1383
 
1420
1384
  def playlist_shuffle(self):
1421
1385
  """Mapped mpv playlist-shuffle command, see man mpv(1)."""
1422
- self.command("playlist-shuffle")
1386
+ self.command('playlist-shuffle')
1423
1387
 
1424
1388
  def playlist_unshuffle(self):
1425
1389
  """Mapped mpv playlist-unshuffle command, see man mpv(1)."""
1426
- self.command("playlist-unshuffle")
1390
+ self.command('playlist-unshuffle')
1427
1391
 
1428
1392
  def run(self, command, *args):
1429
1393
  """Mapped mpv run command, see man mpv(1)."""
1430
- self.command("run", command, *args)
1394
+ self.command('run', command, *args)
1431
1395
 
1432
1396
  def quit(self, code=None):
1433
1397
  """Mapped mpv quit command, see man mpv(1)."""
1434
- self.command("quit", code)
1398
+ if code is not None:
1399
+ self.command('quit', code)
1400
+ else:
1401
+ self.command('quit')
1435
1402
 
1436
1403
  def quit_watch_later(self, code=None):
1437
1404
  """Mapped mpv quit_watch_later command, see man mpv(1)."""
1438
- self.command("quit_watch_later", code)
1405
+ if code is not None:
1406
+ self.command('quit_watch_later', code)
1407
+ else:
1408
+ self.command('quit_watch_later')
1439
1409
 
1440
1410
  def stop(self, keep_playlist=False):
1441
1411
  """Mapped mpv stop command, see man mpv(1)."""
1442
1412
  if keep_playlist:
1443
- self.command("stop", "keep-playlist")
1413
+ self.command('stop', 'keep-playlist')
1444
1414
  else:
1445
- self.command("stop")
1415
+ self.command('stop')
1446
1416
 
1447
- def audio_add(self, url, flags="select", title=None, lang=None):
1417
+ def audio_add(self, url, flags='select', title=None, lang=None):
1448
1418
  """Mapped mpv audio_add command, see man mpv(1)."""
1449
- self.command("audio_add", url.encode(fs_enc), *_drop_nones(flags, title, lang))
1419
+ self.command('audio_add', url.encode(fs_enc), *_drop_nones(flags, title, lang))
1450
1420
 
1451
1421
  def audio_remove(self, audio_id=None):
1452
1422
  """Mapped mpv audio_remove command, see man mpv(1)."""
1453
- self.command("audio_remove", audio_id)
1423
+ self.command('audio_remove', audio_id)
1454
1424
 
1455
1425
  def audio_reload(self, audio_id=None):
1456
1426
  """Mapped mpv audio_reload command, see man mpv(1)."""
1457
- self.command("audio_reload", audio_id)
1427
+ self.command('audio_reload', audio_id)
1458
1428
 
1459
- def video_add(self, url, flags="select", title=None, lang=None, albumart=None):
1429
+ def video_add(self, url, flags='select', title=None, lang=None, albumart=None):
1460
1430
  """Mapped mpv video_add command, see man mpv(1)."""
1461
- self.command("video_add", url.encode(fs_enc), *_drop_nones(flags, title, lang, albumart))
1431
+ self.command('video_add', url.encode(fs_enc), *_drop_nones(flags, title, lang, albumart))
1462
1432
 
1463
1433
  def video_remove(self, video_id=None):
1464
1434
  """Mapped mpv video_remove command, see man mpv(1)."""
1465
- self.command("video_remove", video_id)
1435
+ self.command('video_remove', video_id)
1466
1436
 
1467
1437
  def video_reload(self, video_id=None):
1468
1438
  """Mapped mpv video_reload command, see man mpv(1)."""
1469
- self.command("video_reload", video_id)
1439
+ self.command('video_reload', video_id)
1470
1440
 
1471
- def sub_add(self, url, flags="select", title=None, lang=None):
1441
+ def sub_add(self, url, flags='select', title=None, lang=None):
1472
1442
  """Mapped mpv sub_add command, see man mpv(1)."""
1473
- self.command("sub_add", url.encode(fs_enc), *_drop_nones(flags, title, lang))
1443
+ self.command('sub_add', url.encode(fs_enc), *_drop_nones(flags, title, lang))
1474
1444
 
1475
1445
  def sub_remove(self, sub_id=None):
1476
1446
  """Mapped mpv sub_remove command, see man mpv(1)."""
1477
- self.command("sub_remove", sub_id)
1447
+ self.command('sub_remove', sub_id)
1478
1448
 
1479
1449
  def sub_reload(self, sub_id=None):
1480
1450
  """Mapped mpv sub_reload command, see man mpv(1)."""
1481
- self.command("sub_reload", sub_id)
1451
+ self.command('sub_reload', sub_id)
1482
1452
 
1483
1453
  def sub_step(self, skip):
1484
1454
  """Mapped mpv sub_step command, see man mpv(1)."""
1485
- self.command("sub_step", skip)
1455
+ self.command('sub_step', skip)
1486
1456
 
1487
1457
  def sub_seek(self, skip):
1488
1458
  """Mapped mpv sub_seek command, see man mpv(1)."""
1489
- self.command("sub_seek", skip)
1459
+ self.command('sub_seek', skip)
1490
1460
 
1491
1461
  def toggle_osd(self):
1492
1462
  """Mapped mpv osd command, see man mpv(1)."""
1493
- self.command("osd")
1463
+ self.command('osd')
1494
1464
 
1495
1465
  def print_text(self, text):
1496
1466
  """Mapped mpv print-text command, see man mpv(1)."""
1497
- self.command("print-text", text)
1467
+ self.command('print-text', text)
1498
1468
 
1499
- def show_text(self, string, duration="-1", level=0):
1469
+ def show_text(self, string, duration='-1', level=0):
1500
1470
  """Mapped mpv show_text command, see man mpv(1)."""
1501
- self.command("show_text", string, duration, level)
1471
+ self.command('show_text', string, duration, level)
1502
1472
 
1503
1473
  def expand_text(self, text):
1504
1474
  """Mapped mpv expand-text command, see man mpv(1)."""
1505
- return self.command("expand-text", text)
1475
+ return self.command('expand-text', text)
1506
1476
 
1507
1477
  def expand_path(self, path):
1508
1478
  """Mapped mpv expand-path command, see man mpv(1)."""
1509
- return self.command("expand-path", path)
1479
+ return self.command('expand-path', path)
1510
1480
 
1511
1481
  def show_progress(self):
1512
1482
  """Mapped mpv show_progress command, see man mpv(1)."""
1513
- self.command("show_progress")
1483
+ self.command('show_progress')
1514
1484
 
1515
- def rescan_external_files(self, mode="reselect"):
1485
+ def rescan_external_files(self, mode='reselect'):
1516
1486
  """Mapped mpv rescan-external-files command, see man mpv(1)."""
1517
- self.command("rescan-external-files", mode)
1487
+ self.command('rescan-external-files', mode)
1518
1488
 
1519
1489
  def discnav(self, command):
1520
1490
  """Mapped mpv discnav command, see man mpv(1)."""
1521
- self.command("discnav", command)
1491
+ self.command('discnav', command)
1522
1492
 
1523
- def mouse(x, y, button=None, mode="single"):
1493
+ def mouse(self, x, y, button=None, mode='single'):
1524
1494
  """Mapped mpv mouse command, see man mpv(1)."""
1525
1495
  if button is None:
1526
- self.command("mouse", x, y, mode)
1496
+ self.command('mouse', x, y, mode)
1527
1497
  else:
1528
- self.command("mouse", x, y, button, mode)
1498
+ self.command('mouse', x, y, button, mode)
1529
1499
 
1530
1500
  def keypress(self, name):
1531
1501
  """Mapped mpv keypress command, see man mpv(1)."""
1532
- self.command("keypress", name)
1502
+ self.command('keypress', name)
1533
1503
 
1534
1504
  def keydown(self, name):
1535
1505
  """Mapped mpv keydown command, see man mpv(1)."""
1536
- self.command("keydown", name)
1506
+ self.command('keydown', name)
1537
1507
 
1538
1508
  def keyup(self, name=None):
1539
1509
  """Mapped mpv keyup command, see man mpv(1)."""
1540
1510
  if name is None:
1541
- self.command("keyup")
1511
+ self.command('keyup')
1542
1512
  else:
1543
- self.command("keyup", name)
1513
+ self.command('keyup', name)
1544
1514
 
1545
1515
  def keybind(self, name, command):
1546
1516
  """Mapped mpv keybind command, see man mpv(1)."""
1547
- self.command("keybind", name, command)
1517
+ self.command('keybind', name, command)
1548
1518
 
1549
1519
  def write_watch_later_config(self):
1550
1520
  """Mapped mpv write_watch_later_config command, see man mpv(1)."""
1551
- self.command("write_watch_later_config")
1521
+ self.command('write_watch_later_config')
1552
1522
 
1553
1523
  def overlay_add(self, overlay_id, x, y, file_or_fd, offset, fmt, w, h, stride):
1554
1524
  """Mapped mpv overlay_add command, see man mpv(1)."""
1555
- self.command("overlay_add", overlay_id, x, y, file_or_fd, offset, fmt, w, h, stride)
1525
+ self.command('overlay_add', overlay_id, x, y, file_or_fd, offset, fmt, w, h, stride)
1556
1526
 
1557
1527
  def overlay_remove(self, overlay_id):
1558
1528
  """Mapped mpv overlay_remove command, see man mpv(1)."""
1559
- self.command("overlay_remove", overlay_id)
1529
+ self.command('overlay_remove', overlay_id)
1560
1530
 
1561
1531
  def osd_overlay(self, overlay_id, data, res_x=0, res_y=720, z=0, hidden=False):
1562
- self.command(
1563
- "osd_overlay", id=overlay_id, data=data, res_x=res_x, res_y=res_Y, z=z, hidden=hidden, format="ass-events"
1564
- )
1532
+ self.command('osd_overlay', id=overlay_id, data=data, res_x=res_x, res_y=res_Y, z=z, hidden=hidden,
1533
+ format='ass-events')
1565
1534
 
1566
1535
  def osd_overlay_remove(self, overlay_id):
1567
- self.command("osd_overlay", id=overlay_id, format="none")
1536
+ self.command('osd_overlay', id=overlay_id, format='none')
1568
1537
 
1569
1538
  def script_message(self, *args):
1570
1539
  """Mapped mpv script_message command, see man mpv(1)."""
1571
- self.command("script_message", *args)
1540
+ self.command('script_message', *args)
1572
1541
 
1573
1542
  def script_message_to(self, target, *args):
1574
1543
  """Mapped mpv script_message_to command, see man mpv(1)."""
1575
- self.command("script_message_to", target, *args)
1544
+ self.command('script_message_to', target, *args)
1576
1545
 
1577
1546
  def drop_buffers(self):
1578
- self.command("drop_buffers")
1547
+ self.command('drop_buffers')
1579
1548
 
1580
1549
  def vf_command(self, label, command, argument):
1581
- self.command("vf_command", label, command, argument)
1550
+ self.command('vf_command', label, command, argument)
1582
1551
 
1583
1552
  def af_command(self, label, command, argument):
1584
- self.command("af_command", label, command, argument)
1553
+ self.command('af_command', label, command, argument)
1585
1554
 
1586
1555
  def observe_property(self, name, handler):
1587
1556
  """Register an observer on the named property. An observer is a function that is called with the new property
@@ -1602,16 +1571,14 @@ class MPV(object):
1602
1571
  from calling MPV.terminate() or issuing a "quit" input command).
1603
1572
  """
1604
1573
  self._property_handlers[name].append(handler)
1605
- _mpv_observe_property(self._event_handle, hash(name) & 0xFFFFFFFFFFFFFFFF, name.encode("utf-8"), MpvFormat.NODE)
1574
+ _mpv_observe_property(self._event_handle, hash(name)&0xffffffffffffffff, name.encode('utf-8'), MpvFormat.NODE)
1606
1575
 
1607
1576
  def property_observer(self, name):
1608
1577
  """Function decorator to register a property observer. See ``MPV.observe_property`` for details."""
1609
-
1610
1578
  def wrapper(fun):
1611
1579
  self.observe_property(name, fun)
1612
1580
  fun.unobserve_mpv_properties = lambda: self.unobserve_property(name, fun)
1613
1581
  return fun
1614
-
1615
1582
  return wrapper
1616
1583
 
1617
1584
  def unobserve_property(self, name, handler):
@@ -1621,7 +1588,7 @@ class MPV(object):
1621
1588
  """
1622
1589
  self._property_handlers[name].remove(handler)
1623
1590
  if not self._property_handlers[name]:
1624
- _mpv_unobserve_property(self._event_handle, hash(name) & 0xFFFFFFFFFFFFFFFF)
1591
+ _mpv_unobserve_property(self._event_handle, hash(name)&0xffffffffffffffff)
1625
1592
 
1626
1593
  def unobserve_all_properties(self, handler):
1627
1594
  """Unregister a property observer from *all* observed properties."""
@@ -1675,12 +1642,10 @@ class MPV(object):
1675
1642
 
1676
1643
  my_handler.unregister_mpv_messages()
1677
1644
  """
1678
-
1679
1645
  def register(handler):
1680
1646
  self._register_message_handler_internal(target, handler)
1681
1647
  handler.unregister_mpv_messages = lambda: self.unregister_message_handler(handler)
1682
1648
  return handler
1683
-
1684
1649
  return register
1685
1650
 
1686
1651
  def register_event_callback(self, callback):
@@ -1716,28 +1681,24 @@ class MPV(object):
1716
1681
 
1717
1682
  my_handler.unregister_mpv_events()
1718
1683
  """
1719
-
1720
1684
  def register(callback):
1721
1685
  with self._event_handler_lock:
1722
1686
  self.check_core_alive()
1723
1687
  types = [MpvEventID.from_str(t) if isinstance(t, str) else t for t in event_types] or MpvEventID.ANY
1724
-
1725
1688
  @wraps(callback)
1726
1689
  def wrapper(event, *args, **kwargs):
1727
1690
  if event.event_id.value in types:
1728
1691
  callback(event, *args, **kwargs)
1729
-
1730
1692
  self._event_callbacks.append(wrapper)
1731
1693
  wrapper.unregister_mpv_events = partial(self.unregister_event_callback, wrapper)
1732
1694
  return wrapper
1733
-
1734
1695
  return register
1735
1696
 
1736
1697
  @staticmethod
1737
1698
  def _binding_name(callback_or_cmd):
1738
- return "py_kb_{:016x}".format(hash(callback_or_cmd) & 0xFFFFFFFFFFFFFFFF)
1699
+ return 'py_kb_{:016x}'.format(hash(callback_or_cmd)&0xffffffffffffffff)
1739
1700
 
1740
- def on_key_press(self, keydef, mode="force"):
1701
+ def on_key_press(self, keydef, mode='force'):
1741
1702
  """Function decorator to register a simplified key binding. The callback is called whenever the key given is
1742
1703
  *pressed*.
1743
1704
 
@@ -1756,19 +1717,16 @@ class MPV(object):
1756
1717
 
1757
1718
  The BIG FAT WARNING regarding untrusted keydefs from the key_binding method applies here as well.
1758
1719
  """
1759
-
1760
1720
  def register(fun):
1761
1721
  @self.key_binding(keydef, mode)
1762
1722
  @wraps(fun)
1763
- def wrapper(state="p-", name=None, char=None):
1764
- if state[0] in ("d", "p"):
1723
+ def wrapper(state='p-', name=None, char=None):
1724
+ if state[0] in ('d', 'p'):
1765
1725
  fun()
1766
-
1767
1726
  return wrapper
1768
-
1769
1727
  return register
1770
1728
 
1771
- def key_binding(self, keydef, mode="force"):
1729
+ def key_binding(self, keydef, mode='force'):
1772
1730
  """Function decorator to register a low-level key binding.
1773
1731
 
1774
1732
  The callback function signature is ``fun(key_state, key_name)`` where ``key_state`` is either ``'U'`` for "key
@@ -1796,98 +1754,88 @@ class MPV(object):
1796
1754
  completely fine--but, if you are about to pass untrusted input into this parameter, better double-check whether
1797
1755
  this is secure in your case.
1798
1756
  """
1799
-
1800
1757
  def register(fun):
1801
- fun.mpv_key_bindings = getattr(fun, "mpv_key_bindings", []) + [keydef]
1802
-
1758
+ fun.mpv_key_bindings = getattr(fun, 'mpv_key_bindings', []) + [keydef]
1803
1759
  def unregister_all():
1804
1760
  for keydef in fun.mpv_key_bindings:
1805
1761
  self.unregister_key_binding(keydef)
1806
-
1807
1762
  fun.unregister_mpv_key_bindings = unregister_all
1808
1763
 
1809
1764
  self.register_key_binding(keydef, fun, mode)
1810
1765
  return fun
1811
-
1812
1766
  return register
1813
1767
 
1814
- def register_key_binding(self, keydef, callback_or_cmd, mode="force"):
1768
+ def register_key_binding(self, keydef, callback_or_cmd, mode='force'):
1815
1769
  """Register a key binding. This takes an mpv keydef and either a string containing a mpv command or a python
1816
1770
  callback function. See ``MPV.key_binding`` for details.
1817
1771
  """
1818
- if not re.match(r"(Shift+)?(Ctrl+)?(Alt+)?(Meta+)?(.|\w+)", keydef):
1819
- raise ValueError(
1820
- "Invalid keydef. Expected format: [Shift+][Ctrl+][Alt+][Meta+]<key>\n"
1821
- "<key> is either the literal character the key produces (ASCII or Unicode character), or a "
1822
- "symbolic name (as printed by --input-keylist"
1823
- )
1772
+ if not re.match(r'(Shift+)?(Ctrl+)?(Alt+)?(Meta+)?(.|\w+)', keydef):
1773
+ raise ValueError('Invalid keydef. Expected format: [Shift+][Ctrl+][Alt+][Meta+]<key>\n'
1774
+ '<key> is either the literal character the key produces (ASCII or Unicode character), or a '
1775
+ 'symbolic name (as printed by --input-keylist')
1824
1776
  binding_name = MPV._binding_name(keydef)
1825
1777
  if callable(callback_or_cmd):
1826
1778
  self._key_binding_handlers[binding_name] = callback_or_cmd
1827
- self.register_message_handler("key-binding", self._handle_key_binding_message)
1828
- self.command(
1829
- "define-section",
1830
- binding_name,
1831
- "{} script-binding py_event_handler/{}".format(keydef, binding_name),
1832
- mode,
1833
- )
1779
+ self.register_message_handler('key-binding', self._handle_key_binding_message)
1780
+ self.command('define-section',
1781
+ binding_name, '{} script-binding py_event_handler/{}'.format(keydef, binding_name), mode)
1834
1782
  elif isinstance(callback_or_cmd, str):
1835
- self.command("define-section", binding_name, "{} {}".format(keydef, callback_or_cmd), mode)
1783
+ self.command('define-section', binding_name, '{} {}'.format(keydef, callback_or_cmd), mode)
1836
1784
  else:
1837
- raise TypeError("register_key_binding expects either an str with an mpv command or a python callable.")
1838
- self.command("enable-section", binding_name, "allow-hide-cursor+allow-vo-dragging")
1785
+ raise TypeError('register_key_binding expects either an str with an mpv command or a python callable.')
1786
+ self.command('enable-section', binding_name, 'allow-hide-cursor+allow-vo-dragging')
1839
1787
 
1840
1788
  def _handle_key_binding_message(self, binding_name, key_state, key_name=None, key_char=None):
1841
- binding_name = binding_name.decode("utf-8")
1842
- key_state = key_state.decode("utf-8")
1843
- key_name = key_name.decode("utf-8") if key_name is not None else None
1844
- key_char = key_char.decode("utf-8") if key_char is not None else None
1789
+ binding_name = binding_name.decode('utf-8')
1790
+ key_state = key_state.decode('utf-8')
1791
+ key_name = key_name.decode('utf-8') if key_name is not None else None
1792
+ key_char = key_char.decode('utf-8') if key_char is not None else None
1845
1793
  self._key_binding_handlers[binding_name](key_state, key_name, key_char)
1846
1794
 
1847
1795
  def unregister_key_binding(self, keydef):
1848
1796
  """Unregister a key binding by keydef."""
1849
1797
  binding_name = MPV._binding_name(keydef)
1850
- self.command("disable-section", binding_name)
1851
- self.command("define-section", binding_name, "")
1798
+ self.command('disable-section', binding_name)
1799
+ self.command('define-section', binding_name, '')
1852
1800
  if binding_name in self._key_binding_handlers:
1853
1801
  del self._key_binding_handlers[binding_name]
1854
1802
  if not self._key_binding_handlers:
1855
- self.unregister_message_handler("key-binding")
1803
+ self.unregister_message_handler('key-binding')
1856
1804
 
1857
1805
  def register_stream_protocol(self, proto, open_fn=None):
1858
- """Register a custom stream protocol as documented in libmpv/stream_cb.h:
1859
- https://github.com/mpv-player/mpv/blob/master/libmpv/stream_cb.h
1806
+ """ Register a custom stream protocol as documented in libmpv/stream_cb.h:
1807
+ https://github.com/mpv-player/mpv/blob/master/libmpv/stream_cb.h
1860
1808
 
1861
- proto is the protocol scheme, e.g. "foo" for "foo://" urls.
1809
+ proto is the protocol scheme, e.g. "foo" for "foo://" urls.
1862
1810
 
1863
- This function can either be used with two parameters or it can be used as a decorator on the target
1864
- function.
1811
+ This function can either be used with two parameters or it can be used as a decorator on the target
1812
+ function.
1865
1813
 
1866
- open_fn is a function taking an URI string and returning an mpv stream object.
1867
- open_fn may raise a ValueError to signal libmpv the URI could not be opened.
1814
+ open_fn is a function taking an URI string and returning an mpv stream object.
1815
+ open_fn may raise a ValueError to signal libmpv the URI could not be opened.
1868
1816
 
1869
- The mpv stream protocol is as follows:
1870
- class Stream:
1871
- @property
1872
- def size(self):
1873
- return None # unknown size
1874
- return size # int with size in bytes
1817
+ The mpv stream protocol is as follows:
1818
+ class Stream:
1819
+ @property
1820
+ def size(self):
1821
+ return None # unknown size
1822
+ return size # int with size in bytes
1875
1823
 
1876
- def read(self, size):
1877
- ...
1878
- return read # non-empty bytes object with input
1879
- return b'' # empty byte object signals permanent EOF
1824
+ def read(self, size):
1825
+ ...
1826
+ return read # non-empty bytes object with input
1827
+ return b'' # empty byte object signals permanent EOF
1880
1828
 
1881
- def seek(self, pos): # optional
1882
- return new_offset # integer with new byte offset. The new offset may be before the requested offset
1883
- in case an exact seek is inconvenient.
1829
+ def seek(self, pos): # optional
1830
+ return new_offset # integer with new byte offset. The new offset may be before the requested offset
1831
+ in case an exact seek is inconvenient.
1884
1832
 
1885
- def close(self): # optional
1886
- ...
1833
+ def close(self): # optional
1834
+ ...
1887
1835
 
1888
- def cancel(self): # optional
1889
- Abort a running read() or seek() operation
1890
- ...
1836
+ def cancel(self): # optional
1837
+ Abort a running read() or seek() operation
1838
+ ...
1891
1839
 
1892
1840
  """
1893
1841
 
@@ -1895,37 +1843,70 @@ class MPV(object):
1895
1843
  @StreamOpenFn
1896
1844
  def open_backend(_userdata, uri, cb_info):
1897
1845
  try:
1898
- frontend = open_fn(uri.decode("utf-8"))
1846
+ frontend = open_fn(uri.decode('utf-8'))
1899
1847
  except ValueError:
1900
1848
  return ErrorCode.LOADING_FAILED
1901
-
1902
- def read_backend(_userdata, buf, bufsize):
1903
- data = frontend.read(bufsize)
1904
- for i in range(len(data)):
1905
- buf[i] = data[i]
1906
- return len(data)
1849
+ except Exception as e:
1850
+ for fut in self._exception_futures:
1851
+ try:
1852
+ fut.set_exception(e)
1853
+ break
1854
+ except InvalidStateError:
1855
+ pass
1856
+ else:
1857
+ warnings.warn(f'Unhandled exception {e} inside stream open callback for URI {uri}\n{traceback.format_exc()}')
1858
+ return ErrorCode.LOADING_FAILED
1907
1859
 
1908
1860
  cb_info.contents.cookie = None
1861
+
1862
+ def read_backend(_userdata, buf, bufsize):
1863
+ with self._enqueue_exceptions():
1864
+ data = frontend.read(bufsize)
1865
+ for i in range(len(data)):
1866
+ buf[i] = data[i]
1867
+ return len(data)
1868
+ return -1
1909
1869
  read = cb_info.contents.read = StreamReadFn(read_backend)
1910
- close = cb_info.contents.close = StreamCloseFn(lambda _userdata: frontend.close())
1870
+
1871
+ def close_backend(_userdata):
1872
+ with self._enqueue_exceptions():
1873
+ del self._stream_protocol_frontends[proto][uri]
1874
+ if hasattr(frontend, 'close'):
1875
+ frontend.close()
1876
+ close = cb_info.contents.close = StreamCloseFn(close_backend)
1911
1877
 
1912
1878
  seek, size, cancel = None, None, None
1913
- if hasattr(frontend, "seek"):
1914
- seek = cb_info.contents.seek = StreamSeekFn(lambda _userdata, offx: frontend.seek(offx))
1915
- if hasattr(frontend, "size") and frontend.size is not None:
1916
- size = cb_info.contents.size = StreamSizeFn(lambda _userdata: frontend.size)
1917
- if hasattr(frontend, "cancel"):
1918
- cancel = cb_info.contents.cancel = StreamCancelFn(lambda _userdata: frontend.cancel())
1919
-
1920
- # keep frontend and callbacks in memory forever (TODO)
1879
+
1880
+ if hasattr(frontend, 'seek'):
1881
+ def seek_backend(_userdata, offx):
1882
+ with self._enqueue_exceptions():
1883
+ return frontend.seek(offx)
1884
+ return ErrorCode.GENERIC
1885
+ seek = cb_info.contents.seek = StreamSeekFn(seek_backend)
1886
+
1887
+ if hasattr(frontend, 'size') and frontend.size is not None:
1888
+ def size_backend(_userdata):
1889
+ with self._enqueue_exceptions():
1890
+ return frontend.size
1891
+ return 0
1892
+ size = cb_info.contents.size = StreamSizeFn(size_backend)
1893
+
1894
+ if hasattr(frontend, 'cancel'):
1895
+ def cancel_backend(_userdata):
1896
+ with self._enqueue_exceptions():
1897
+ frontend.cancel()
1898
+ cancel = cb_info.contents.cancel = StreamCancelFn(cancel_backend)
1899
+
1900
+ # keep frontend and callbacks in memory until closed
1921
1901
  frontend._registered_callbacks = [read, close, seek, size, cancel]
1922
1902
  self._stream_protocol_frontends[proto][uri] = frontend
1923
1903
  return 0
1924
1904
 
1925
1905
  if proto in self._stream_protocol_cbs:
1926
- raise KeyError("Stream protocol already registered")
1906
+ raise KeyError('Stream protocol already registered')
1907
+ # keep backend in memory forever
1927
1908
  self._stream_protocol_cbs[proto] = [open_backend]
1928
- _mpv_stream_cb_add_ro(self.handle, proto.encode("utf-8"), c_void_p(), open_backend)
1909
+ _mpv_stream_cb_add_ro(self.handle, proto.encode('utf-8'), c_void_p(), open_backend)
1929
1910
 
1930
1911
  return open_fn
1931
1912
 
@@ -1941,12 +1922,12 @@ class MPV(object):
1941
1922
  @property
1942
1923
  def playlist_filenames(self):
1943
1924
  """Return all playlist item file names/URLs as a list of strs."""
1944
- return [element["filename"] for element in self.playlist]
1925
+ return [element['filename'] for element in self.playlist]
1945
1926
 
1946
1927
  def playlist_append(self, filename, **options):
1947
1928
  """Append a path or URL to the playlist. This does not start playing the file automatically. To do that, use
1948
1929
  ``MPV.loadfile(filename, 'append-play')``."""
1949
- self.loadfile(filename, "append", **options)
1930
+ self.loadfile(filename, 'append', **options)
1950
1931
 
1951
1932
  # "Python stream" logic. This is some porcelain for directly playing data from python generators.
1952
1933
 
@@ -1954,7 +1935,7 @@ class MPV(object):
1954
1935
  """Internal handler for python:// protocol streams registered through @python_stream(...) and
1955
1936
  @python_stream_catchall
1956
1937
  """
1957
- (name,) = re.fullmatch("python://(.*)", uri).groups()
1938
+ name, = re.fullmatch('python://(.*)', uri).groups()
1958
1939
 
1959
1940
  if name in self._python_streams:
1960
1941
  generator_fun, size = self._python_streams[name]
@@ -1962,7 +1943,7 @@ class MPV(object):
1962
1943
  if self._python_stream_catchall is not None:
1963
1944
  generator_fun, size = self._python_stream_catchall(name)
1964
1945
  else:
1965
- raise ValueError("Python stream name not found and no catch-all defined")
1946
+ raise ValueError('Python stream name not found and no catch-all defined')
1966
1947
 
1967
1948
  return GeneratorStream(generator_fun, size)
1968
1949
 
@@ -1990,26 +1971,70 @@ class MPV(object):
1990
1971
  mpv.wait_for_playback()
1991
1972
  reader.unregister()
1992
1973
  """
1993
-
1994
1974
  def register(cb):
1995
1975
  if name in self._python_streams:
1996
1976
  raise KeyError('Python stream name "{}" is already registered'.format(name))
1997
1977
  self._python_streams[name] = (cb, size)
1998
-
1999
1978
  def unregister():
2000
- if (
2001
- name not in self._python_streams or self._python_streams[name][0] is not cb
2002
- ): # This is just a basic sanity check
2003
- raise RuntimeError("Python stream has already been unregistered")
1979
+ if name not in self._python_streams or\
1980
+ self._python_streams[name][0] is not cb: # This is just a basic sanity check
1981
+ raise RuntimeError('Python stream has already been unregistered')
2004
1982
  del self._python_streams[name]
2005
-
2006
1983
  cb.unregister = unregister
2007
1984
  return cb
2008
-
2009
1985
  return register
2010
1986
 
1987
+ @contextmanager
1988
+ def play_context(self):
1989
+ """ Context manager for streaming bytes straight into libmpv.
1990
+
1991
+ This is a convenience wrapper around python_stream. play_context returns a write method, which you can use in
1992
+ the body of the context manager to feed libmpv bytes. All bytes you feed in with write() in the body of a single
1993
+ call of this context manager are treated as one single file. A queue is used internally, so this function is
1994
+ thread-safe. The queue is unlimited, so it cannot block and is safe to call from async code. You can use this
1995
+ function to stream chunked data, e.g. from the network.
1996
+
1997
+ Use it like this:
1998
+
1999
+ with m.play_context() as write:
2000
+ with open(TESTVID, 'rb') as f:
2001
+ while (chunk := f.read(65536)): # Get some chunks of bytes
2002
+ write(chunk)
2003
+ """
2004
+ q = queue.Queue()
2005
+
2006
+ frame = sys._getframe()
2007
+ stream_name = f'__python_mpv_play_generator_{hash(frame)}'
2008
+ EOF = frame # Get some unique object as EOF marker
2009
+ @self.python_stream(stream_name)
2010
+ def reader():
2011
+ while (chunk := q.get()) is not EOF:
2012
+ if chunk:
2013
+ yield chunk
2014
+ reader.unregister()
2015
+
2016
+ def write(chunk):
2017
+ q.put(chunk)
2018
+
2019
+ # Start playback before yielding, the first call to reader() will block until write is called at least once.
2020
+ self.play(f'python://{stream_name}')
2021
+ yield write
2022
+ q.put(EOF)
2023
+
2024
+ def play_bytes(self, data):
2025
+ """ Play the given bytes object as a single file. """
2026
+ frame = sys._getframe()
2027
+ stream_name = f'__python_mpv_play_generator_{hash(frame)}'
2028
+
2029
+ @self.python_stream(stream_name)
2030
+ def reader():
2031
+ yield data
2032
+ reader.unregister() # unregister itself
2033
+
2034
+ self.play(f'python://{stream_name}')
2035
+
2011
2036
  def python_stream_catchall(self, cb):
2012
- """Register a catch-all python stream to be called when no name matches can be found. Use this decorator on a
2037
+ """ Register a catch-all python stream to be called when no name matches can be found. Use this decorator on a
2013
2038
  function that takes a name argument and returns a (generator, size) tuple (with size being None if unknown).
2014
2039
 
2015
2040
  An invalid URI can be signalled to libmpv by raising a ValueError inside the callback.
@@ -2034,15 +2059,13 @@ class MPV(object):
2034
2059
  catchall.unregister()
2035
2060
  """
2036
2061
  if self._python_stream_catchall is not None:
2037
- raise KeyError("A catch-all python stream is already registered")
2062
+ raise KeyError('A catch-all python stream is already registered')
2038
2063
 
2039
2064
  self._python_stream_catchall = cb
2040
-
2041
2065
  def unregister():
2042
2066
  if self._python_stream_catchall is not cb:
2043
- raise RuntimeError("This catch-all python stream has already been unregistered")
2067
+ raise RuntimeError('This catch-all python stream has already been unregistered')
2044
2068
  self._python_stream_catchall = None
2045
-
2046
2069
  cb.unregister = unregister
2047
2070
  return cb
2048
2071
 
@@ -2051,23 +2074,26 @@ class MPV(object):
2051
2074
  self.check_core_alive()
2052
2075
  out = create_string_buffer(sizeof(MpvNode))
2053
2076
  try:
2054
- cval = _mpv_get_property(self.handle, name.encode("utf-8"), fmt, out)
2077
+ cval = _mpv_get_property(self.handle, name.encode('utf-8'), fmt, out)
2055
2078
 
2056
2079
  if fmt is MpvFormat.OSD_STRING:
2057
- return cast(out, POINTER(c_char_p)).contents.value.decode("utf-8")
2080
+ return cast(out, POINTER(c_char_p)).contents.value.decode('utf-8')
2058
2081
  elif fmt is MpvFormat.NODE:
2059
2082
  rv = cast(out, POINTER(MpvNode)).contents.node_value(decoder=decoder)
2060
2083
  _mpv_free_node_contents(out)
2061
2084
  return rv
2062
2085
  else:
2063
- raise TypeError("_get_property only supports NODE and OSD_STRING formats.")
2086
+ raise TypeError('_get_property only supports NODE and OSD_STRING formats.')
2064
2087
  except PropertyUnavailableError as ex:
2065
2088
  return None
2066
2089
 
2067
2090
  def _set_property(self, name, value):
2068
2091
  self.check_core_alive()
2069
- ename = name.encode("utf-8")
2070
- if isinstance(value, (list, set, dict)):
2092
+ ename = name.encode('utf-8')
2093
+ if isinstance(value, dict):
2094
+ _1, _2, _3, pointer = _make_node_str_map(value)
2095
+ _mpv_set_property(self.handle, ename, MpvFormat.NODE, pointer)
2096
+ elif isinstance(value, (list, set)):
2071
2097
  _1, _2, _3, pointer = _make_node_str_list(value)
2072
2098
  _mpv_set_property(self.handle, ename, MpvFormat.NODE, pointer)
2073
2099
  else:
@@ -2077,31 +2103,31 @@ class MPV(object):
2077
2103
  return self._get_property(_py_to_mpv(name), lazy_decoder)
2078
2104
 
2079
2105
  def __setattr__(self, name, value):
2080
- try:
2081
- if name != "handle" and not name.startswith("_"):
2082
- self._set_property(_py_to_mpv(name), value)
2083
- else:
2106
+ try:
2107
+ if name != 'handle' and not name.startswith('_'):
2108
+ self._set_property(_py_to_mpv(name), value)
2109
+ else:
2110
+ super().__setattr__(name, value)
2111
+ except AttributeError:
2084
2112
  super().__setattr__(name, value)
2085
- except AttributeError:
2086
- super().__setattr__(name, value)
2087
2113
 
2088
2114
  def __dir__(self):
2089
- return super().__dir__() + [name.replace("-", "_") for name in self.property_list]
2115
+ return super().__dir__() + [ name.replace('-', '_') for name in self.property_list ]
2090
2116
 
2091
2117
  @property
2092
2118
  def properties(self):
2093
- return {name: self.option_info(name) for name in self.property_list}
2119
+ return { name: self.option_info(name) for name in self.property_list }
2094
2120
 
2095
2121
  # Dict-like option access
2096
2122
  def __getitem__(self, name, file_local=False):
2097
2123
  """Get an option value."""
2098
- prefix = "file-local-options/" if file_local else "options/"
2099
- return self._get_property(prefix + name, lazy_decoder)
2124
+ prefix = 'file-local-options/' if file_local else 'options/'
2125
+ return self._get_property(prefix+name, lazy_decoder)
2100
2126
 
2101
2127
  def __setitem__(self, name, value, file_local=False):
2102
2128
  """Set an option value."""
2103
- prefix = "file-local-options/" if file_local else "options/"
2104
- return self._set_property(prefix + name, value)
2129
+ prefix = 'file-local-options/' if file_local else 'options/'
2130
+ return self._set_property(prefix+name, value)
2105
2131
 
2106
2132
  def __iter__(self):
2107
2133
  """Iterate over all option names."""
@@ -2110,7 +2136,7 @@ class MPV(object):
2110
2136
  def option_info(self, name):
2111
2137
  """Get information on the given option."""
2112
2138
  try:
2113
- return self._get_property("option-info/" + name)
2139
+ return self._get_property('option-info/'+name)
2114
2140
  except AttributeError:
2115
2141
  return None
2116
2142
 
@@ -2118,7 +2144,7 @@ class MPV(object):
2118
2144
  class MpvRenderContext:
2119
2145
  def __init__(self, mpv, api_type, **kwargs):
2120
2146
  self._mpv = mpv
2121
- kwargs["api_type"] = api_type
2147
+ kwargs['api_type'] = api_type
2122
2148
 
2123
2149
  buf = cast(create_string_buffer(sizeof(MpvRenderCtxHandle)), POINTER(MpvRenderCtxHandle))
2124
2150
  _mpv_render_context_create(buf, mpv.handle, kwargs_to_render_param_array(kwargs))
@@ -2128,10 +2154,10 @@ class MpvRenderContext:
2128
2154
  _mpv_render_context_free(self._handle)
2129
2155
 
2130
2156
  def __setattr__(self, name, value):
2131
- if name.startswith("_"):
2157
+ if name.startswith('_'):
2132
2158
  super().__setattr__(name, value)
2133
2159
 
2134
- elif name == "update_cb":
2160
+ elif name == 'update_cb':
2135
2161
  func = value if value else (lambda: None)
2136
2162
  self._update_cb = value
2137
2163
  self._update_fn_wrapper = RenderUpdateFn(lambda _userdata: func())
@@ -2142,10 +2168,10 @@ class MpvRenderContext:
2142
2168
  _mpv_render_context_set_parameter(self._handle, param)
2143
2169
 
2144
2170
  def __getattr__(self, name):
2145
- if name == "update_cb":
2171
+ if name == 'update_cb':
2146
2172
  return self._update_cb
2147
2173
 
2148
- elif name == "handle":
2174
+ elif name == 'handle':
2149
2175
  return self._handle
2150
2176
 
2151
2177
  param = MpvRenderParam(name)
@@ -2156,7 +2182,7 @@ class MpvRenderContext:
2156
2182
  return buf.contents.as_dict()
2157
2183
 
2158
2184
  def update(self):
2159
- """Calls mpv_render_context_update and returns the MPV_RENDER_UPDATE_FRAME flag (see render.h)"""
2185
+ """ Calls mpv_render_context_update and returns the MPV_RENDER_UPDATE_FRAME flag (see render.h) """
2160
2186
  return bool(_mpv_render_context_update(self._handle) & 1)
2161
2187
 
2162
2188
  def render(self, **kwargs):
@@ -2164,3 +2190,4 @@ class MpvRenderContext:
2164
2190
 
2165
2191
  def report_swap(self):
2166
2192
  _mpv_render_context_report_swap(self._handle)
2193
+