├── .gitignore
├── LICENSE.md
├── README.md
├── docs.md
├── python_mpv_jsonipc.py
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | dist
3 | build
4 | python_mpv_jsonipc.egg-info
5 | .vscode
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Apache License
2 | ==============
3 |
4 | _Version 2.0, January 2004_
5 | _<>_
6 |
7 | ### Terms and Conditions for use, reproduction, and distribution
8 |
9 | #### 1. Definitions
10 |
11 | “License” shall mean the terms and conditions for use, reproduction, and
12 | distribution as defined by Sections 1 through 9 of this document.
13 |
14 | “Licensor” shall mean the copyright owner or entity authorized by the copyright
15 | owner that is granting the License.
16 |
17 | “Legal Entity” shall mean the union of the acting entity and all other entities
18 | that control, are controlled by, or are under common control with that entity.
19 | For the purposes of this definition, “control” means **(i)** the power, direct or
20 | indirect, to cause the direction or management of such entity, whether by
21 | contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
22 | outstanding shares, or **(iii)** beneficial ownership of such entity.
23 |
24 | “You” (or “Your”) shall mean an individual or Legal Entity exercising
25 | permissions granted by this License.
26 |
27 | “Source” form shall mean the preferred form for making modifications, including
28 | but not limited to software source code, documentation source, and configuration
29 | files.
30 |
31 | “Object” form shall mean any form resulting from mechanical transformation or
32 | translation of a Source form, including but not limited to compiled object code,
33 | generated documentation, and conversions to other media types.
34 |
35 | “Work” shall mean the work of authorship, whether in Source or Object form, made
36 | available under the License, as indicated by a copyright notice that is included
37 | in or attached to the work (an example is provided in the Appendix below).
38 |
39 | “Derivative Works” shall mean any work, whether in Source or Object form, that
40 | is based on (or derived from) the Work and for which the editorial revisions,
41 | annotations, elaborations, or other modifications represent, as a whole, an
42 | original work of authorship. For the purposes of this License, Derivative Works
43 | shall not include works that remain separable from, or merely link (or bind by
44 | name) to the interfaces of, the Work and Derivative Works thereof.
45 |
46 | “Contribution” shall mean any work of authorship, including the original version
47 | of the Work and any modifications or additions to that Work or Derivative Works
48 | thereof, that is intentionally submitted to Licensor for inclusion in the Work
49 | by the copyright owner or by an individual or Legal Entity authorized to submit
50 | on behalf of the copyright owner. For the purposes of this definition,
51 | “submitted” means any form of electronic, verbal, or written communication sent
52 | to the Licensor or its representatives, including but not limited to
53 | communication on electronic mailing lists, source code control systems, and
54 | issue tracking systems that are managed by, or on behalf of, the Licensor for
55 | the purpose of discussing and improving the Work, but excluding communication
56 | that is conspicuously marked or otherwise designated in writing by the copyright
57 | owner as “Not a Contribution.”
58 |
59 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf
60 | of whom a Contribution has been received by Licensor and subsequently
61 | incorporated within the Work.
62 |
63 | #### 2. Grant of Copyright License
64 |
65 | Subject to the terms and conditions of this License, each Contributor hereby
66 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
67 | irrevocable copyright license to reproduce, prepare Derivative Works of,
68 | publicly display, publicly perform, sublicense, and distribute the Work and such
69 | Derivative Works in Source or Object form.
70 |
71 | #### 3. Grant of Patent License
72 |
73 | Subject to the terms and conditions of this License, each Contributor hereby
74 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
75 | irrevocable (except as stated in this section) patent license to make, have
76 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where
77 | such license applies only to those patent claims licensable by such Contributor
78 | that are necessarily infringed by their Contribution(s) alone or by combination
79 | of their Contribution(s) with the Work to which such Contribution(s) was
80 | submitted. If You institute patent litigation against any entity (including a
81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a
82 | Contribution incorporated within the Work constitutes direct or contributory
83 | patent infringement, then any patent licenses granted to You under this License
84 | for that Work shall terminate as of the date such litigation is filed.
85 |
86 | #### 4. Redistribution
87 |
88 | You may reproduce and distribute copies of the Work or Derivative Works thereof
89 | in any medium, with or without modifications, and in Source or Object form,
90 | provided that You meet the following conditions:
91 |
92 | * **(a)** You must give any other recipients of the Work or Derivative Works a copy of
93 | this License; and
94 | * **(b)** You must cause any modified files to carry prominent notices stating that You
95 | changed the files; and
96 | * **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
97 | all copyright, patent, trademark, and attribution notices from the Source form
98 | of the Work, excluding those notices that do not pertain to any part of the
99 | Derivative Works; and
100 | * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
101 | Derivative Works that You distribute must include a readable copy of the
102 | attribution notices contained within such NOTICE file, excluding those notices
103 | that do not pertain to any part of the Derivative Works, in at least one of the
104 | following places: within a NOTICE text file distributed as part of the
105 | Derivative Works; within the Source form or documentation, if provided along
106 | with the Derivative Works; or, within a display generated by the Derivative
107 | Works, if and wherever such third-party notices normally appear. The contents of
108 | the NOTICE file are for informational purposes only and do not modify the
109 | License. You may add Your own attribution notices within Derivative Works that
110 | You distribute, alongside or as an addendum to the NOTICE text from the Work,
111 | provided that such additional attribution notices cannot be construed as
112 | modifying the License.
113 |
114 | You may add Your own copyright statement to Your modifications and may provide
115 | additional or different license terms and conditions for use, reproduction, or
116 | distribution of Your modifications, or for any such Derivative Works as a whole,
117 | provided Your use, reproduction, and distribution of the Work otherwise complies
118 | with the conditions stated in this License.
119 |
120 | #### 5. Submission of Contributions
121 |
122 | Unless You explicitly state otherwise, any Contribution intentionally submitted
123 | for inclusion in the Work by You to the Licensor shall be under the terms and
124 | conditions of this License, without any additional terms or conditions.
125 | Notwithstanding the above, nothing herein shall supersede or modify the terms of
126 | any separate license agreement you may have executed with Licensor regarding
127 | such Contributions.
128 |
129 | #### 6. Trademarks
130 |
131 | This License does not grant permission to use the trade names, trademarks,
132 | service marks, or product names of the Licensor, except as required for
133 | reasonable and customary use in describing the origin of the Work and
134 | reproducing the content of the NOTICE file.
135 |
136 | #### 7. Disclaimer of Warranty
137 |
138 | Unless required by applicable law or agreed to in writing, Licensor provides the
139 | Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
140 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
141 | including, without limitation, any warranties or conditions of TITLE,
142 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
143 | solely responsible for determining the appropriateness of using or
144 | redistributing the Work and assume any risks associated with Your exercise of
145 | permissions under this License.
146 |
147 | #### 8. Limitation of Liability
148 |
149 | In no event and under no legal theory, whether in tort (including negligence),
150 | contract, or otherwise, unless required by applicable law (such as deliberate
151 | and grossly negligent acts) or agreed to in writing, shall any Contributor be
152 | liable to You for damages, including any direct, indirect, special, incidental,
153 | or consequential damages of any character arising as a result of this License or
154 | out of the use or inability to use the Work (including but not limited to
155 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or
156 | any and all other commercial damages or losses), even if such Contributor has
157 | been advised of the possibility of such damages.
158 |
159 | #### 9. Accepting Warranty or Additional Liability
160 |
161 | While redistributing the Work or Derivative Works thereof, You may choose to
162 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or
163 | other liability obligations and/or rights consistent with this License. However,
164 | in accepting such obligations, You may act only on Your own behalf and on Your
165 | sole responsibility, not on behalf of any other Contributor, and only if You
166 | agree to indemnify, defend, and hold each Contributor harmless for any liability
167 | incurred by, or claims asserted against, such Contributor by reason of your
168 | accepting any such warranty or additional liability.
169 |
170 | _END OF TERMS AND CONDITIONS_
171 |
172 | ### APPENDIX: How to apply the Apache License to your work
173 |
174 | To apply the Apache License to your work, attach the following boilerplate
175 | notice, with the fields enclosed by brackets `[]` replaced with your own
176 | identifying information. (Don't include the brackets!) The text should be
177 | enclosed in the appropriate comment syntax for the file format. We also
178 | recommend that a file or class name and description of purpose be included on
179 | the same “printed page” as the copyright notice for easier identification within
180 | third-party archives.
181 |
182 | Copyright [yyyy] [name of copyright owner]
183 |
184 | Licensed under the Apache License, Version 2.0 (the "License");
185 | you may not use this file except in compliance with the License.
186 | You may obtain a copy of the License at
187 |
188 | http://www.apache.org/licenses/LICENSE-2.0
189 |
190 | Unless required by applicable law or agreed to in writing, software
191 | distributed under the License is distributed on an "AS IS" BASIS,
192 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
193 | See the License for the specific language governing permissions and
194 | limitations under the License.
195 |
196 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Python MPV JSONIPC
2 |
3 | This implements an interface similar to `python-mpv`, but it uses the JSON IPC protocol instead of the C API. This means
4 | you can control external instances of MPV including players like SMPlayer, and it can use MPV players that are prebuilt
5 | instead of needing `libmpv1`. It may also be more resistant to crashes such as Segmentation Faults, but since it isn't
6 | directly communicating with MPV via the C API the performance will be worse.
7 |
8 | Please note that this only implements the subset of `python-mpv` that is used by `plex-mpv-shim` and
9 | `jellyfin-mpv-shim`. Other functionality has not been implemented.
10 |
11 | ## Installation
12 |
13 | ```bash
14 | sudo pip3 install python-mpv-jsonipc
15 | ```
16 |
17 | ## Basic usage
18 |
19 | Create an instance of MPV. You can use an already running MPV or have the library start a
20 | managed copy of MPV. Command arguments can be specified when initializing MPV if you are
21 | starting a managed copy of MPV.
22 |
23 | Please also see the [API Documentation](https://github.com/iwalton3/python-mpv-jsonipc/blob/master/docs.md).
24 |
25 | ```python
26 | from python_mpv_jsonipc import MPV
27 |
28 | # Uses MPV that is in the PATH.
29 | mpv = MPV()
30 |
31 | # Use MPV that is running and connected to /tmp/mpv-socket.
32 | mpv = MPV(start_mpv=False, ipc_socket="/tmp/mpv-socket")
33 |
34 | # Uses MPV that is found at /path/to/mpv.
35 | mpv = MPV(mpv_location="/path/to/mpv")
36 |
37 | # After you have an MPV, you can read and set (if applicable) properties.
38 | mpv.volume # 100.0 by default
39 | mpv.volume = 20
40 |
41 | # You can also send commands.
42 | mpv.command_name(arg1, arg2)
43 |
44 | # Bind to key press events with a decorator
45 | @mpv.on_key_press("space")
46 | def space_handler():
47 | pass
48 |
49 | # You can also observe and wait for properties.
50 | @mpv.property_observer("eof-reached")
51 | def handle_eof(name, value):
52 | pass
53 |
54 | # Or simply wait for the value to change once.
55 | mpv.wait_for_property("duration")
56 | ```
57 |
--------------------------------------------------------------------------------
/docs.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## python\_mpv\_jsonipc
4 |
5 |
6 |
7 | ### MPVError
8 |
9 | ``` python
10 | class MPVError(Exception):
11 | | MPVError(**args, ****kwargs)
12 | ```
13 |
14 | An error originating from MPV or due to a problem with MPV.
15 |
16 |
17 |
18 | ### WindowsSocket
19 |
20 | ``` python
21 | class WindowsSocket(threading.Thread)
22 | ```
23 |
24 | Wraps a Windows named pipe in a high-level interface. (Internal)
25 |
26 | Data is automatically encoded and decoded as JSON. The callback
27 | function will be called for each inbound message.
28 |
29 |
30 |
31 | #### \_\_init\_\_
32 |
33 | ``` python
34 | | __init__(ipc_socket, callback=None, quit_callback=None)
35 | ```
36 |
37 | Create the wrapper.
38 |
39 | **ipc\_socket** is the pipe name. (Not including \\\\.\\pipe\\)
40 | **callback(json\_data)** is the function for recieving events.
41 |
42 |
43 |
44 | #### stop
45 |
46 | ``` python
47 | | stop()
48 | ```
49 |
50 | Terminate the thread.
51 |
52 |
53 |
54 | #### send
55 |
56 | ``` python
57 | | send(data)
58 | ```
59 |
60 | Send **data** to the pipe, encoded as JSON.
61 |
62 |
63 |
64 | #### run
65 |
66 | ``` python
67 | | run()
68 | ```
69 |
70 | Process pipe events. Do not run this directly. Use **start**.
71 |
72 |
73 |
74 | ### UnixSocket
75 |
76 | ``` python
77 | class UnixSocket(threading.Thread)
78 | ```
79 |
80 | Wraps a Unix/Linux socket in a high-level interface. (Internal)
81 |
82 | Data is automatically encoded and decoded as JSON. The callback
83 | function will be called for each inbound message.
84 |
85 |
86 |
87 | #### \_\_init\_\_
88 |
89 | ``` python
90 | | __init__(ipc_socket, callback=None, quit_callback=None)
91 | ```
92 |
93 | Create the wrapper.
94 |
95 | **ipc\_socket** is the path to the socket.
96 | **callback(json\_data)** is the function for recieving events.
97 |
98 |
99 |
100 | #### stop
101 |
102 | ``` python
103 | | stop()
104 | ```
105 |
106 | Terminate the thread.
107 |
108 |
109 |
110 | #### send
111 |
112 | ``` python
113 | | send(data)
114 | ```
115 |
116 | Send **data** to the socket, encoded as JSON.
117 |
118 |
119 |
120 | #### run
121 |
122 | ``` python
123 | | run()
124 | ```
125 |
126 | Process socket events. Do not run this directly. Use **start**.
127 |
128 |
129 |
130 | ### MPVProcess
131 |
132 | ``` python
133 | class MPVProcess()
134 | ```
135 |
136 | Manages an MPV process, ensuring the socket or pipe is available. (Internal)
137 |
138 |
139 |
140 | #### \_\_init\_\_
141 |
142 | ``` python
143 | | __init__(ipc_socket, mpv_location=None, ****kwargs)
144 | ```
145 |
146 | Create and start the MPV process. Will block until socket/pipe is available.
147 |
148 | **ipc\_socket** is the path to the Unix/Linux socket or name of the Windows pipe.
149 | **mpv\_location** is the path to mpv. If left unset it tries the one in the PATH.
150 |
151 | All other arguments are forwarded to MPV as command-line arguments.
152 |
153 |
154 |
155 | #### stop
156 |
157 | ``` python
158 | | stop()
159 | ```
160 |
161 | Terminate the process.
162 |
163 |
164 |
165 | ### MPVInter
166 |
167 | ``` python
168 | class MPVInter()
169 | ```
170 |
171 | Low-level interface to MPV. Does NOT manage an mpv process. (Internal)
172 |
173 |
174 |
175 | #### \_\_init\_\_
176 |
177 | ``` python
178 | | __init__(ipc_socket, callback=None, quit_callback=None)
179 | ```
180 |
181 | Create the wrapper.
182 |
183 | **ipc\_socket** is the path to the Unix/Linux socket or name of the Windows pipe.
184 | **callback(event\_name, data)** is the function for recieving events.
185 |
186 |
187 |
188 | #### stop
189 |
190 | ``` python
191 | | stop()
192 | ```
193 |
194 | Terminate the underlying connection.
195 |
196 |
197 |
198 | #### event\_callback
199 |
200 | ``` python
201 | | event_callback(data)
202 | ```
203 |
204 | Internal callback for recieving events from MPV.
205 |
206 |
207 |
208 | #### command
209 |
210 | ``` python
211 | | command(command, **args)
212 | ```
213 |
214 | Issue a command to MPV. Will block until completed or timeout is reached.
215 |
216 | **command** is the name of the MPV command
217 |
218 | All further arguments are forwarded to the MPV command.
219 | Throws TimeoutError if timeout of 120 seconds is reached.
220 |
221 |
222 |
223 | ### EventHandler
224 |
225 | ``` python
226 | class EventHandler(threading.Thread)
227 | ```
228 |
229 | Event handling thread. (Internal)
230 |
231 |
232 |
233 | #### \_\_init\_\_
234 |
235 | ``` python
236 | | __init__()
237 | ```
238 |
239 | Create an instance of the thread.
240 |
241 |
242 |
243 | #### put\_task
244 |
245 | ``` python
246 | | put_task(func, **args)
247 | ```
248 |
249 | Put a new task to the thread.
250 |
251 | **func** is the function to call
252 |
253 | All further arguments are forwarded to **func**.
254 |
255 |
256 |
257 | #### stop
258 |
259 | ``` python
260 | | stop()
261 | ```
262 |
263 | Terminate the thread.
264 |
265 |
266 |
267 | #### run
268 |
269 | ``` python
270 | | run()
271 | ```
272 |
273 | Process socket events. Do not run this directly. Use **start**.
274 |
275 |
276 |
277 | ### MPV
278 |
279 | ``` python
280 | class MPV()
281 | ```
282 |
283 | The main MPV interface class. Use this to control MPV.
284 |
285 | This will expose all mpv commands as callable methods and all properties.
286 | You can set properties and call the commands directly.
287 |
288 | Please note that if you are using a really old MPV version, a fallback command
289 | list is used. Not all commands may actually work when this fallback is used.
290 |
291 |
292 |
293 | #### \_\_init\_\_
294 |
295 | ``` python
296 | | __init__(start_mpv=True, ipc_socket=None, mpv_location=None, log_handler=None, loglevel=None, quit_callback=None, ****kwargs)
297 | ```
298 |
299 | Create the interface to MPV and process instance.
300 |
301 | **start\_mpv** will start an MPV process if true. (Default: True)
302 | **ipc\_socket** is the path to the Unix/Linux socket or name of Windows pipe. (Default: Random Temp File)
303 | **mpv\_location** is the location of MPV for **start\_mpv**. (Default: Use MPV in PATH)
304 | **log\_handler(level, prefix, text)** is an optional handler for log events. (Default: Disabled)
305 | **loglevel** is the level for log messages. Levels are fatal, error, warn, info, v, debug, trace. (Default: Disabled)
306 |
307 | All other arguments are forwarded to MPV as command-line arguments if **start\_mpv** is used.
308 |
309 |
310 |
311 | #### bind\_event
312 |
313 | ``` python
314 | | bind_event(name, callback)
315 | ```
316 |
317 | Bind a callback to an MPV event.
318 |
319 | **name** is the MPV event name.
320 | **callback(event\_data)** is the function to call.
321 |
322 |
323 |
324 | #### on\_event
325 |
326 | ``` python
327 | | on_event(name)
328 | ```
329 |
330 | Decorator to bind a callback to an MPV event.
331 |
332 | @on\_event(name)
333 | def my\_callback(event\_data):
334 | pass
335 |
336 |
337 |
338 | #### event\_callback
339 |
340 | ``` python
341 | | event_callback(name)
342 | ```
343 |
344 | An alias for on\_event to maintain compatibility with python-mpv.
345 |
346 |
347 |
348 | #### on\_key\_press
349 |
350 | ``` python
351 | | on_key_press(name)
352 | ```
353 |
354 | Decorator to bind a callback to an MPV keypress event.
355 |
356 | @on\_key\_press(key\_name)
357 | def my\_callback():
358 | pass
359 |
360 |
361 |
362 | #### bind\_key\_press
363 |
364 | ``` python
365 | | bind_key_press(name, callback)
366 | ```
367 |
368 | Bind a callback to an MPV keypress event.
369 |
370 | **name** is the key symbol.
371 | **callback()** is the function to call.
372 |
373 |
374 |
375 | #### bind\_property\_observer
376 |
377 | ``` python
378 | | bind_property_observer(name, callback)
379 | ```
380 |
381 | Bind a callback to an MPV property change.
382 |
383 | **name** is the property name.
384 | **callback(name, data)** is the function to call.
385 |
386 | Returns a unique observer ID needed to destroy the observer.
387 |
388 |
389 |
390 | #### unbind\_property\_observer
391 |
392 | ``` python
393 | | unbind_property_observer(observer_id)
394 | ```
395 |
396 | Remove callback to an MPV property change.
397 |
398 | **observer\_id** is the id returned by bind\_property\_observer.
399 |
400 |
401 |
402 | #### property\_observer
403 |
404 | ``` python
405 | | property_observer(name)
406 | ```
407 |
408 | Decorator to bind a callback to an MPV property change.
409 |
410 | @property\_observer(property\_name)
411 | def my\_callback(name, data):
412 | pass
413 |
414 |
415 |
416 | #### wait\_for\_property
417 |
418 | ``` python
419 | | wait_for_property(name)
420 | ```
421 |
422 | Waits for the value of a property to change.
423 |
424 | **name** is the name of the property.
425 |
426 |
427 |
428 | #### play
429 |
430 | ``` python
431 | | play(url)
432 | ```
433 |
434 | Play the specified URL. An alias to loadfile().
435 |
436 |
437 |
438 | #### terminate
439 |
440 | ``` python
441 | | terminate()
442 | ```
443 |
444 | Terminate the connection to MPV and process (if **start\_mpv** is used).
445 |
446 |
447 |
448 | #### command
449 |
450 | ``` python
451 | | command(command, **args)
452 | ```
453 |
454 | Send a command to MPV. All commands are bound to the class by default,
455 | except JSON IPC specific commands. This may also be useful to retain
456 | compatibility with python-mpv, as it does not bind all of the commands.
457 |
458 | **command** is the command name.
459 |
460 | All further arguments are forwarded to the MPV command.
461 |
--------------------------------------------------------------------------------
/python_mpv_jsonipc.py:
--------------------------------------------------------------------------------
1 | import threading
2 | import socket
3 | import json
4 | import os
5 | import time
6 | import subprocess
7 | import random
8 | import queue
9 | import logging
10 |
11 | log = logging.getLogger('mpv-jsonipc')
12 |
13 | if os.name == "nt":
14 | import _winapi
15 | from multiprocessing.connection import PipeConnection
16 |
17 | TIMEOUT = 120
18 |
19 | # Older MPV versions do not allow us to dynamically retrieve the command list.
20 | FALLBACK_COMMAND_LIST = [
21 | 'ignore', 'seek', 'revert-seek', 'quit', 'quit-watch-later', 'stop', 'frame-step', 'frame-back-step',
22 | 'playlist-next', 'playlist-prev', 'playlist-shuffle', 'playlist-unshuffle', 'sub-step', 'sub-seek',
23 | 'print-text', 'show-text', 'expand-text', 'expand-path', 'show-progress', 'sub-add', 'audio-add',
24 | 'video-add', 'sub-remove', 'audio-remove', 'video-remove', 'sub-reload', 'audio-reload', 'video-reload',
25 | 'rescan-external-files', 'screenshot', 'screenshot-to-file', 'screenshot-raw', 'loadfile', 'loadlist',
26 | 'playlist-clear', 'playlist-remove', 'playlist-move', 'run', 'subprocess', 'set', 'change-list', 'add',
27 | 'cycle', 'multiply', 'cycle-values', 'enable-section', 'disable-section', 'define-section', 'ab-loop',
28 | 'drop-buffers', 'af', 'vf', 'af-command', 'vf-command', 'ao-reload', 'script-binding', 'script-message',
29 | 'script-message-to', 'overlay-add', 'overlay-remove', 'osd-overlay', 'write-watch-later-config',
30 | 'hook-add', 'hook-ack', 'mouse', 'keybind', 'keypress', 'keydown', 'keyup', 'apply-profile',
31 | 'load-script', 'dump-cache', 'ab-loop-dump-cache', 'ab-loop-align-cache']
32 |
33 | class MPVError(Exception):
34 | """An error originating from MPV or due to a problem with MPV."""
35 | def __init__(self, *args, **kwargs):
36 | super(MPVError, self).__init__(*args, **kwargs)
37 |
38 | class WindowsSocket(threading.Thread):
39 | """
40 | Wraps a Windows named pipe in a high-level interface. (Internal)
41 |
42 | Data is automatically encoded and decoded as JSON. The callback
43 | function will be called for each inbound message.
44 | """
45 | def __init__(self, ipc_socket, callback=None, quit_callback=None):
46 | """Create the wrapper.
47 |
48 | *ipc_socket* is the pipe name. (Not including \\\\.\\pipe\\)
49 | *callback(json_data)* is the function for recieving events.
50 | *quit_callback* is called when the socket connection dies.
51 | """
52 | ipc_socket = "\\\\.\\pipe\\" + ipc_socket
53 | self.callback = callback
54 | self.quit_callback = quit_callback
55 |
56 | access = _winapi.GENERIC_READ | _winapi.GENERIC_WRITE
57 | limit = 5 # Connection may fail at first. Try 5 times.
58 | for _ in range(limit):
59 | try:
60 | pipe_handle = _winapi.CreateFile(
61 | ipc_socket, access, 0, _winapi.NULL, _winapi.OPEN_EXISTING,
62 | _winapi.FILE_FLAG_OVERLAPPED, _winapi.NULL
63 | )
64 | break
65 | except OSError:
66 | time.sleep(1)
67 | else:
68 | raise MPVError("Cannot connect to pipe.")
69 | self.socket = PipeConnection(pipe_handle)
70 |
71 | if self.callback is None:
72 | self.callback = lambda data: None
73 |
74 | threading.Thread.__init__(self)
75 |
76 | def stop(self, join=True):
77 | """Terminate the thread."""
78 | if self.socket is not None:
79 | try:
80 | self.socket.close()
81 | except OSError:
82 | pass # Ignore socket close failure.
83 | if join:
84 | self.join()
85 |
86 | def send(self, data):
87 | """Send *data* to the pipe, encoded as JSON."""
88 | try:
89 | self.socket.send_bytes(json.dumps(data).encode('utf-8') + b'\n')
90 | except OSError as ex:
91 | if len(ex.args) == 1 and ex.args[0] == "handle is closed":
92 | raise BrokenPipeError("handle is closed")
93 | raise ex
94 |
95 | def run(self):
96 | """Process pipe events. Do not run this directly. Use *start*."""
97 | data = b''
98 | try:
99 | while True:
100 | current_data = self.socket.recv_bytes(2048)
101 | if current_data == b'':
102 | break
103 |
104 | data += current_data
105 | if data[-1] != 10:
106 | continue
107 |
108 | data = data.decode('utf-8', 'ignore').encode('utf-8')
109 | for item in data.split(b'\n'):
110 | if item == b'':
111 | continue
112 | json_data = json.loads(item)
113 | self.callback(json_data)
114 | data = b''
115 | except EOFError:
116 | if self.quit_callback:
117 | self.quit_callback()
118 | except Exception as ex:
119 | log.error("Pipe connection died.", exc_info=1)
120 | if self.quit_callback:
121 | self.quit_callback()
122 |
123 | class UnixSocket(threading.Thread):
124 | """
125 | Wraps a Unix/Linux socket in a high-level interface. (Internal)
126 |
127 | Data is automatically encoded and decoded as JSON. The callback
128 | function will be called for each inbound message.
129 | """
130 | def __init__(self, ipc_socket, callback=None, quit_callback=None):
131 | """Create the wrapper.
132 |
133 | *ipc_socket* is the path to the socket.
134 | *callback(json_data)* is the function for recieving events.
135 | *quit_callback* is called when the socket connection dies.
136 | """
137 | self.ipc_socket = ipc_socket
138 | self.callback = callback
139 | self.quit_callback = quit_callback
140 | self.socket = socket.socket(socket.AF_UNIX)
141 | self.socket.connect(self.ipc_socket)
142 |
143 | if self.callback is None:
144 | self.callback = lambda data: None
145 |
146 | threading.Thread.__init__(self)
147 |
148 | def stop(self, join=True):
149 | """Terminate the thread."""
150 | if self.socket is not None:
151 | try:
152 | self.socket.shutdown(socket.SHUT_WR)
153 | self.socket.close()
154 | self.socket = None
155 | except OSError:
156 | pass # Ignore socket close failure.
157 | if join:
158 | self.join()
159 |
160 | def send(self, data):
161 | """Send *data* to the socket, encoded as JSON."""
162 | if self.socket is None:
163 | raise BrokenPipeError("socket is closed")
164 | self.socket.send(json.dumps(data).encode('utf-8') + b'\n')
165 |
166 | def run(self):
167 | """Process socket events. Do not run this directly. Use *start*."""
168 | data = b''
169 | try:
170 | while True:
171 | current_data = self.socket.recv(1024)
172 | if current_data == b'':
173 | break
174 |
175 | data += current_data
176 | if data[-1] != 10:
177 | continue
178 |
179 | data = data.decode('utf-8', 'ignore').encode('utf-8')
180 | for item in data.split(b'\n'):
181 | if item == b'':
182 | continue
183 | json_data = json.loads(item)
184 | self.callback(json_data)
185 | data = b''
186 | except Exception as ex:
187 | log.error("Socket connection died.", exc_info=1)
188 | if self.quit_callback:
189 | self.quit_callback()
190 |
191 | class MPVProcess:
192 | """
193 | Manages an MPV process, ensuring the socket or pipe is available. (Internal)
194 | """
195 | def __init__(self, ipc_socket, mpv_location=None, **kwargs):
196 | """
197 | Create and start the MPV process. Will block until socket/pipe is available.
198 |
199 | *ipc_socket* is the path to the Unix/Linux socket or name of the Windows pipe.
200 | *mpv_location* is the path to mpv. If left unset it tries the one in the PATH.
201 |
202 | All other arguments are forwarded to MPV as command-line arguments.
203 | """
204 | if mpv_location is None:
205 | if os.name == 'nt':
206 | mpv_location = "mpv.exe"
207 | else:
208 | mpv_location = "mpv"
209 |
210 | log.debug("Staring MPV from {0}.".format(mpv_location))
211 | ipc_socket_name = ipc_socket
212 | if os.name == 'nt':
213 | ipc_socket = "\\\\.\\pipe\\" + ipc_socket
214 |
215 | if os.name != 'nt' and os.path.exists(ipc_socket):
216 | os.remove(ipc_socket)
217 |
218 | log.debug("Using IPC socket {0} for MPV.".format(ipc_socket))
219 | self.ipc_socket = ipc_socket
220 | args = [mpv_location]
221 | self._set_default(kwargs, "idle", True)
222 | self._set_default(kwargs, "input_ipc_server", ipc_socket_name)
223 | self._set_default(kwargs, "input_terminal", False)
224 | self._set_default(kwargs, "terminal", False)
225 |
226 | arg_pairs = []
227 | for key, value in kwargs.items():
228 | if type(value) == list:
229 | for v in value:
230 | arg_pairs.append((key, v))
231 | else:
232 | arg_pairs.append((key, value))
233 |
234 | args.extend("--{0}={1}".format(v[0].replace("_", "-"), self._mpv_fmt(v[1]))
235 | for v in arg_pairs)
236 | self.process = subprocess.Popen(args)
237 | ipc_exists = False
238 | for _ in range(100): # Give MPV 10 seconds to start.
239 | time.sleep(0.1)
240 | self.process.poll()
241 | if os.path.exists(ipc_socket):
242 | ipc_exists = True
243 | log.debug("Found MPV socket.")
244 | break
245 | if self.process.returncode is not None:
246 | log.error("MPV failed with returncode {0}.".format(self.process.returncode))
247 | break
248 | else:
249 | self.process.terminate()
250 | raise MPVError("MPV start timed out.")
251 |
252 | if not ipc_exists or self.process.returncode is not None:
253 | self.process.terminate()
254 | raise MPVError("MPV not started.")
255 |
256 | def _set_default(self, prop_dict, key, value):
257 | if key not in prop_dict:
258 | prop_dict[key] = value
259 |
260 | def _mpv_fmt(self, data):
261 | if data == True:
262 | return "yes"
263 | elif data == False:
264 | return "no"
265 | else:
266 | return data
267 |
268 | def stop(self):
269 | """Terminate the process."""
270 | self.process.terminate()
271 | if os.name != 'nt' and os.path.exists(self.ipc_socket):
272 | os.remove(self.ipc_socket)
273 |
274 | class MPVInter:
275 | """
276 | Low-level interface to MPV. Does NOT manage an mpv process. (Internal)
277 | """
278 | def __init__(self, ipc_socket, callback=None, quit_callback=None):
279 | """Create the wrapper.
280 |
281 | *ipc_socket* is the path to the Unix/Linux socket or name of the Windows pipe.
282 | *callback(event_name, data)* is the function for recieving events.
283 | *quit_callback* is called when the socket connection to MPV dies.
284 | """
285 | Socket = UnixSocket
286 | if os.name == 'nt':
287 | Socket = WindowsSocket
288 |
289 | self.callback = callback
290 | self.quit_callback = quit_callback
291 | if self.callback is None:
292 | self.callback = lambda event, data: None
293 |
294 | self.socket = Socket(ipc_socket, self.event_callback, self.quit_callback)
295 | self.socket.start()
296 | self.command_id = 1
297 | self.rid_lock = threading.Lock()
298 | self.socket_lock = threading.Lock()
299 | self.cid_result = {}
300 | self.cid_wait = {}
301 |
302 | def stop(self, join=True):
303 | """Terminate the underlying connection."""
304 | self.socket.stop(join)
305 |
306 | def event_callback(self, data):
307 | """Internal callback for recieving events from MPV."""
308 | if "request_id" in data:
309 | self.cid_result[data["request_id"]] = data
310 | self.cid_wait[data["request_id"]].set()
311 | elif "event" in data:
312 | self.callback(data["event"], data)
313 |
314 | def command(self, command, *args):
315 | """
316 | Issue a command to MPV. Will block until completed or timeout is reached.
317 |
318 | *command* is the name of the MPV command
319 |
320 | All further arguments are forwarded to the MPV command.
321 | Throws TimeoutError if timeout of 120 seconds is reached.
322 | """
323 | self.rid_lock.acquire()
324 | command_id = self.command_id
325 | self.command_id += 1
326 | self.rid_lock.release()
327 |
328 | event = threading.Event()
329 | self.cid_wait[command_id] = event
330 |
331 | command_list = [command]
332 | command_list.extend(args)
333 | try:
334 | self.socket_lock.acquire()
335 | self.socket.send({"command":command_list, "request_id": command_id})
336 | finally:
337 | self.socket_lock.release()
338 |
339 | has_event = event.wait(timeout=TIMEOUT)
340 | if has_event:
341 | data = self.cid_result[command_id]
342 | del self.cid_result[command_id]
343 | del self.cid_wait[command_id]
344 | if data["error"] != "success":
345 | if data["error"] == "property unavailable":
346 | return None
347 | raise MPVError(data["error"])
348 | else:
349 | return data.get("data")
350 | else:
351 | raise TimeoutError("No response from MPV.")
352 |
353 | class EventHandler(threading.Thread):
354 | """Event handling thread. (Internal)"""
355 | def __init__(self):
356 | """Create an instance of the thread."""
357 | self.queue = queue.Queue()
358 | threading.Thread.__init__(self)
359 |
360 | def put_task(self, func, *args):
361 | """
362 | Put a new task to the thread.
363 |
364 | *func* is the function to call
365 |
366 | All further arguments are forwarded to *func*.
367 | """
368 | self.queue.put((func, args))
369 |
370 | def stop(self, join=True):
371 | """Terminate the thread."""
372 | self.queue.put("quit")
373 | self.join(join)
374 |
375 | def run(self):
376 | """Process socket events. Do not run this directly. Use *start*."""
377 | while True:
378 | event = self.queue.get()
379 | if event == "quit":
380 | break
381 | try:
382 | event[0](*event[1])
383 | except Exception:
384 | log.error("EventHandler caught exception from {0}.".format(event), exc_info=1)
385 |
386 | class MPV:
387 | """
388 | The main MPV interface class. Use this to control MPV.
389 |
390 | This will expose all mpv commands as callable methods and all properties.
391 | You can set properties and call the commands directly.
392 |
393 | Please note that if you are using a really old MPV version, a fallback command
394 | list is used. Not all commands may actually work when this fallback is used.
395 | """
396 | def __init__(self, start_mpv=True, ipc_socket=None, mpv_location=None,
397 | log_handler=None, loglevel=None, quit_callback=None, start_retries=5, start_retry_delay_ms=1000, **kwargs):
398 | """
399 | Create the interface to MPV and process instance.
400 |
401 | *start_mpv* will start an MPV process if true. (Default: True)
402 | *ipc_socket* is the path to the Unix/Linux socket or name of Windows pipe. (Default: Random Temp File)
403 | *mpv_location* is the location of MPV for *start_mpv*. (Default: Use MPV in PATH)
404 | *log_handler(level, prefix, text)* is an optional handler for log events. (Default: Disabled)
405 | *loglevel* is the level for log messages. Levels are fatal, error, warn, info, v, debug, trace. (Default: Disabled)
406 | *quit_callback* is called when the socket connection to MPV dies.
407 |
408 | All other arguments are forwarded to MPV as command-line arguments if *start_mpv* is used.
409 | """
410 | self.properties = {}
411 | self.event_bindings = {}
412 | self.key_bindings = {}
413 | self.property_bindings = {}
414 | self.mpv_process = None
415 | self.mpv_inter = None
416 | self.quit_callback = quit_callback
417 | self.event_handler = EventHandler()
418 | self.event_handler.start()
419 | if ipc_socket is None:
420 | rand_file = "mpv{0}".format(random.randint(0, 2**48))
421 | if os.name == "nt":
422 | ipc_socket = rand_file
423 | else:
424 | ipc_socket = "/tmp/{0}".format(rand_file)
425 |
426 | if start_mpv:
427 | # Attempt to start MPV multiple times.
428 | for i in range(start_retries):
429 | try:
430 | self.mpv_process = MPVProcess(ipc_socket, mpv_location, **kwargs)
431 | break
432 | except MPVError:
433 | log.warning("MPV start failed.", exc_info=1)
434 | time.sleep(start_retry_delay_ms / 1000)
435 | continue
436 | else:
437 | raise MPVError("MPV process retry limit reached.")
438 |
439 | self.mpv_inter = MPVInter(ipc_socket, self._callback, self._quit_callback)
440 | self.properties = set(x.replace("-", "_") for x in self.command("get_property", "property-list"))
441 | try:
442 | command_list = [x["name"] for x in self.command("get_property", "command-list")]
443 | except MPVError:
444 | log.warning("Using fallback command list.")
445 | command_list = FALLBACK_COMMAND_LIST
446 | for command in command_list:
447 | command_name = command.replace("-", "_")
448 | if command_name in self.properties:
449 | command_name = f"{command_name}_cmd"
450 | object.__setattr__(self, command_name, self._get_wrapper(command))
451 |
452 | self._dir = list(self.properties)
453 | self._dir.extend(object.__dir__(self))
454 |
455 | self.observer_id = 1
456 | self.observer_lock = threading.Lock()
457 | self.keybind_id = 1
458 | self.keybind_lock = threading.Lock()
459 |
460 | if log_handler is not None and loglevel is not None:
461 | self.command("request_log_messages", loglevel)
462 | @self.on_event("log-message")
463 | def log_handler_event(data):
464 | self.event_handler.put_task(log_handler, data["level"], data["prefix"], data["text"].strip())
465 |
466 | @self.on_event("property-change")
467 | def event_handler(data):
468 | if data.get("id") in self.property_bindings:
469 | self.event_handler.put_task(self.property_bindings[data["id"]], data["name"], data.get("data"))
470 |
471 | @self.on_event("client-message")
472 | def client_message_handler(data):
473 | args = data["args"]
474 | if len(args) == 2 and args[0] == "custom-bind":
475 | self.event_handler.put_task(self.key_bindings[args[1]])
476 |
477 | def _quit_callback(self):
478 | """
479 | Internal handler for quit events.
480 | """
481 | if self.quit_callback:
482 | self.quit_callback()
483 | self.terminate(join=False)
484 |
485 | def bind_event(self, name, callback):
486 | """
487 | Bind a callback to an MPV event.
488 |
489 | *name* is the MPV event name.
490 | *callback(event_data)* is the function to call.
491 | """
492 | if name not in self.event_bindings:
493 | self.event_bindings[name] = set()
494 | self.event_bindings[name].add(callback)
495 |
496 | def on_event(self, name):
497 | """
498 | Decorator to bind a callback to an MPV event.
499 |
500 | @on_event(name)
501 | def my_callback(event_data):
502 | pass
503 | """
504 | def wrapper(func):
505 | self.bind_event(name, func)
506 | return func
507 | return wrapper
508 |
509 | # Added for compatibility.
510 | def event_callback(self, name):
511 | """An alias for on_event to maintain compatibility with python-mpv."""
512 | return self.on_event(name)
513 |
514 | def on_key_press(self, name):
515 | """
516 | Decorator to bind a callback to an MPV keypress event.
517 |
518 | @on_key_press(key_name)
519 | def my_callback():
520 | pass
521 | """
522 | def wrapper(func):
523 | self.bind_key_press(name, func)
524 | return func
525 | return wrapper
526 |
527 | def bind_key_press(self, name, callback):
528 | """
529 | Bind a callback to an MPV keypress event.
530 |
531 | *name* is the key symbol.
532 | *callback()* is the function to call.
533 | """
534 | self.keybind_lock.acquire()
535 | keybind_id = self.keybind_id
536 | self.keybind_id += 1
537 | self.keybind_lock.release()
538 |
539 | bind_name = "bind{0}".format(keybind_id)
540 | self.key_bindings["bind{0}".format(keybind_id)] = callback
541 | try:
542 | self.keybind(name, "script-message custom-bind {0}".format(bind_name))
543 | except MPVError:
544 | self.define_section(bind_name, "{0} script-message custom-bind {1}".format(name, bind_name))
545 | self.enable_section(bind_name)
546 |
547 | def bind_property_observer(self, name, callback):
548 | """
549 | Bind a callback to an MPV property change.
550 |
551 | *name* is the property name.
552 | *callback(name, data)* is the function to call.
553 |
554 | Returns a unique observer ID needed to destroy the observer.
555 | """
556 | self.observer_lock.acquire()
557 | observer_id = self.observer_id
558 | self.observer_id += 1
559 | self.observer_lock.release()
560 |
561 | self.property_bindings[observer_id] = callback
562 | self.command("observe_property", observer_id, name)
563 | return observer_id
564 |
565 | def unbind_property_observer(self, observer_id):
566 | """
567 | Remove callback to an MPV property change.
568 |
569 | *observer_id* is the id returned by bind_property_observer.
570 | """
571 | self.command("unobserve_property", observer_id)
572 | del self.property_bindings[observer_id]
573 |
574 | def property_observer(self, name):
575 | """
576 | Decorator to bind a callback to an MPV property change.
577 |
578 | @property_observer(property_name)
579 | def my_callback(name, data):
580 | pass
581 | """
582 | def wrapper(func):
583 | self.bind_property_observer(name, func)
584 | return func
585 | return wrapper
586 |
587 | def wait_for_property(self, name):
588 | """
589 | Waits for the value of a property to change.
590 |
591 | *name* is the name of the property.
592 | """
593 | event = threading.Event()
594 | first_event = True
595 | def handler(*_):
596 | nonlocal first_event
597 | if first_event == True:
598 | first_event = False
599 | else:
600 | event.set()
601 | observer_id = self.bind_property_observer(name, handler)
602 | event.wait()
603 | self.unbind_property_observer(observer_id)
604 |
605 | def _get_wrapper(self, name):
606 | def wrapper(*args):
607 | return self.command(name, *args)
608 | return wrapper
609 |
610 | def _callback(self, event, data):
611 | if event in self.event_bindings:
612 | for callback in self.event_bindings[event]:
613 | self.event_handler.put_task(callback, data)
614 |
615 | def play(self, url):
616 | """Play the specified URL. An alias to loadfile()."""
617 | self.loadfile(url)
618 |
619 | def __del__(self):
620 | self.terminate()
621 |
622 | def terminate(self, join=True):
623 | """Terminate the connection to MPV and process (if *start_mpv* is used)."""
624 | if self.mpv_process:
625 | self.mpv_process.stop()
626 | if self.mpv_inter:
627 | self.mpv_inter.stop(join)
628 | self.event_handler.stop(join)
629 |
630 | def command(self, command, *args):
631 | """
632 | Send a command to MPV. All commands are bound to the class by default,
633 | except JSON IPC specific commands. This may also be useful to retain
634 | compatibility with python-mpv, as it does not bind all of the commands.
635 |
636 | *command* is the command name.
637 |
638 | All further arguments are forwarded to the MPV command.
639 | """
640 | return self.mpv_inter.command(command, *args)
641 |
642 | def __getattr__(self, name):
643 | if name in self.properties:
644 | return self.command("get_property", name.replace("_", "-"))
645 | return object.__getattribute__(self, name)
646 |
647 | def __setattr__(self, name, value):
648 | if name not in {"properties", "command"} and name in self.properties:
649 | return self.command("set_property", name.replace("_", "-"), value)
650 | return object.__setattr__(self, name, value)
651 |
652 | def __hasattr__(self, name):
653 | if object.__hasattr__(self, name):
654 | return True
655 | else:
656 | try:
657 | getattr(self, name)
658 | return True
659 | except MPVError:
660 | return False
661 |
662 | def __dir__(self):
663 | return self._dir
664 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 | import os
3 |
4 | with open("README.md", "r") as fh:
5 | long_description = fh.read()
6 |
7 | setup(
8 | name="python-mpv-jsonipc",
9 | version="1.2.1",
10 | author="Izzie Walton",
11 | author_email="izzie@iwalton.com",
12 | description="Python API to MPV using JSON IPC",
13 | license="Apache-2.0",
14 | long_description=open("README.md").read(),
15 | long_description_content_type="text/markdown",
16 | url="https://github.com/iwalton3/python-mpv-jsonipc",
17 | py_modules=["python_mpv_jsonipc"],
18 | classifiers=[
19 | "Programming Language :: Python :: 3",
20 | "License :: OSI Approved :: Apache Software License",
21 | "Operating System :: OS Independent",
22 | ],
23 | python_requires=">=3.6",
24 | install_requires=[],
25 | )
26 |
--------------------------------------------------------------------------------