Add iwalton3's Python MPV JSONIPC library (Apache 2.0)
This commit is contained in:
parent
da6ec880bb
commit
7887c2bf14
195
syncplay/players/python_mpv_jsonipc/LICENSE.md
Normal file
195
syncplay/players/python_mpv_jsonipc/LICENSE.md
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
Apache License
|
||||||
|
==============
|
||||||
|
|
||||||
|
_Version 2.0, January 2004_
|
||||||
|
_<<http://www.apache.org/licenses/>>_
|
||||||
|
|
||||||
|
### Terms and Conditions for use, reproduction, and distribution
|
||||||
|
|
||||||
|
#### 1. Definitions
|
||||||
|
|
||||||
|
“License” shall mean the terms and conditions for use, reproduction, and
|
||||||
|
distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||||
|
owner that is granting the License.
|
||||||
|
|
||||||
|
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||||
|
that control, are controlled by, or are under common control with that entity.
|
||||||
|
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||||
|
indirect, to cause the direction or management of such entity, whether by
|
||||||
|
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||||
|
|
||||||
|
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||||
|
permissions granted by this License.
|
||||||
|
|
||||||
|
“Source” form shall mean the preferred form for making modifications, including
|
||||||
|
but not limited to software source code, documentation source, and configuration
|
||||||
|
files.
|
||||||
|
|
||||||
|
“Object” form shall mean any form resulting from mechanical transformation or
|
||||||
|
translation of a Source form, including but not limited to compiled object code,
|
||||||
|
generated documentation, and conversions to other media types.
|
||||||
|
|
||||||
|
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||||
|
available under the License, as indicated by a copyright notice that is included
|
||||||
|
in or attached to the work (an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||||
|
is based on (or derived from) the Work and for which the editorial revisions,
|
||||||
|
annotations, elaborations, or other modifications represent, as a whole, an
|
||||||
|
original work of authorship. For the purposes of this License, Derivative Works
|
||||||
|
shall not include works that remain separable from, or merely link (or bind by
|
||||||
|
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
“Contribution” shall mean any work of authorship, including the original version
|
||||||
|
of the Work and any modifications or additions to that Work or Derivative Works
|
||||||
|
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||||
|
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||||
|
on behalf of the copyright owner. For the purposes of this definition,
|
||||||
|
“submitted” means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems, and
|
||||||
|
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||||
|
the purpose of discussing and improving the Work, but excluding communication
|
||||||
|
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||||
|
owner as “Not a Contribution.”
|
||||||
|
|
||||||
|
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||||
|
of whom a Contribution has been received by Licensor and subsequently
|
||||||
|
incorporated within the Work.
|
||||||
|
|
||||||
|
#### 2. Grant of Copyright License
|
||||||
|
|
||||||
|
Subject to the terms and conditions of this License, each Contributor hereby
|
||||||
|
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||||
|
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||||
|
Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
#### 3. Grant of Patent License
|
||||||
|
|
||||||
|
Subject to the terms and conditions of this License, each Contributor hereby
|
||||||
|
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||||
|
irrevocable (except as stated in this section) patent license to make, have
|
||||||
|
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||||
|
such license applies only to those patent claims licensable by such Contributor
|
||||||
|
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||||
|
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||||
|
submitted. If You institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||||
|
Contribution incorporated within the Work constitutes direct or contributory
|
||||||
|
patent infringement, then any patent licenses granted to You under this License
|
||||||
|
for that Work shall terminate as of the date such litigation is filed.
|
||||||
|
|
||||||
|
#### 4. Redistribution
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||||
|
in any medium, with or without modifications, and in Source or Object form,
|
||||||
|
provided that You meet the following conditions:
|
||||||
|
|
||||||
|
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||||
|
this License; and
|
||||||
|
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||||
|
changed the files; and
|
||||||
|
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||||
|
all copyright, patent, trademark, and attribution notices from the Source form
|
||||||
|
of the Work, excluding those notices that do not pertain to any part of the
|
||||||
|
Derivative Works; and
|
||||||
|
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||||
|
Derivative Works that You distribute must include a readable copy of the
|
||||||
|
attribution notices contained within such NOTICE file, excluding those notices
|
||||||
|
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||||
|
following places: within a NOTICE text file distributed as part of the
|
||||||
|
Derivative Works; within the Source form or documentation, if provided along
|
||||||
|
with the Derivative Works; or, within a display generated by the Derivative
|
||||||
|
Works, if and wherever such third-party notices normally appear. The contents of
|
||||||
|
the NOTICE file are for informational purposes only and do not modify the
|
||||||
|
License. You may add Your own attribution notices within Derivative Works that
|
||||||
|
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||||
|
provided that such additional attribution notices cannot be construed as
|
||||||
|
modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and may provide
|
||||||
|
additional or different license terms and conditions for use, reproduction, or
|
||||||
|
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||||
|
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||||
|
with the conditions stated in this License.
|
||||||
|
|
||||||
|
#### 5. Submission of Contributions
|
||||||
|
|
||||||
|
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||||
|
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||||
|
conditions of this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||||
|
any separate license agreement you may have executed with Licensor regarding
|
||||||
|
such Contributions.
|
||||||
|
|
||||||
|
#### 6. Trademarks
|
||||||
|
|
||||||
|
This License does not grant permission to use the trade names, trademarks,
|
||||||
|
service marks, or product names of the Licensor, except as required for
|
||||||
|
reasonable and customary use in describing the origin of the Work and
|
||||||
|
reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
#### 7. Disclaimer of Warranty
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||||
|
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||||
|
including, without limitation, any warranties or conditions of TITLE,
|
||||||
|
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||||
|
solely responsible for determining the appropriateness of using or
|
||||||
|
redistributing the Work and assume any risks associated with Your exercise of
|
||||||
|
permissions under this License.
|
||||||
|
|
||||||
|
#### 8. Limitation of Liability
|
||||||
|
|
||||||
|
In no event and under no legal theory, whether in tort (including negligence),
|
||||||
|
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||||
|
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special, incidental,
|
||||||
|
or consequential damages of any character arising as a result of this License or
|
||||||
|
out of the use or inability to use the Work (including but not limited to
|
||||||
|
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||||
|
any and all other commercial damages or losses), even if such Contributor has
|
||||||
|
been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
#### 9. Accepting Warranty or Additional Liability
|
||||||
|
|
||||||
|
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||||
|
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||||
|
other liability obligations and/or rights consistent with this License. However,
|
||||||
|
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||||
|
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||||
|
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason of your
|
||||||
|
accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
_END OF TERMS AND CONDITIONS_
|
||||||
|
|
||||||
|
### APPENDIX: How to apply the Apache License to your work
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following boilerplate
|
||||||
|
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||||
|
identifying information. (Don't include the brackets!) The text should be
|
||||||
|
enclosed in the appropriate comment syntax for the file format. We also
|
||||||
|
recommend that a file or class name and description of purpose be included on
|
||||||
|
the same “printed page” as the copyright notice for easier identification within
|
||||||
|
third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
56
syncplay/players/python_mpv_jsonipc/README.md
Normal file
56
syncplay/players/python_mpv_jsonipc/README.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Python MPV JSONIPC
|
||||||
|
|
||||||
|
This implements an interface similar to `python-mpv`, but it uses the JSON IPC protocol instead of the C API. This means
|
||||||
|
you can control external instances of MPV including players like SMPlayer, and it can use MPV players that are prebuilt
|
||||||
|
instead of needing `libmpv1`. It may also be more resistant to crashes such as Segmentation Faults, but since it isn't
|
||||||
|
directly communicating with MPV via the C API the performance will be worse.
|
||||||
|
|
||||||
|
Please note that this only implements the subset of `python-mpv` that is used by `plex-mpv-shim` and
|
||||||
|
`jellyfin-mpv-shim`. Other functionality has not been implemented.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo pip3 install python-mpv-jsonipc
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic usage
|
||||||
|
|
||||||
|
Create an instance of MPV. You can use an already running MPV or have the library start a
|
||||||
|
managed copy of MPV. Command arguments can be specified when initializing MPV if you are
|
||||||
|
starting a managed copy of MPV.
|
||||||
|
|
||||||
|
Please also see the [API Documentation](https://github.com/iwalton3/python-mpv-jsonipc/blob/master/docs.md).
|
||||||
|
|
||||||
|
```python
|
||||||
|
from python_mpv_jsonipc import MPV
|
||||||
|
|
||||||
|
# Uses MPV that is in the PATH.
|
||||||
|
mpv = MPV()
|
||||||
|
|
||||||
|
# Use MPV that is running and connected to /tmp/mpv-socket.
|
||||||
|
mpv = MPV(start_mpv=False, ipc_socket="/tmp/mpv-socket")
|
||||||
|
|
||||||
|
# Uses MPV that is found at /path/to/mpv.
|
||||||
|
mpv = MPV(mpv_location="/path/to/mpv")
|
||||||
|
|
||||||
|
# After you have an MPV, you can read and set (if applicable) properties.
|
||||||
|
mpv.volume # 100.0 by default
|
||||||
|
mpv.volume = 20
|
||||||
|
|
||||||
|
# You can also send commands.
|
||||||
|
mpv.command_name(arg1, arg2)
|
||||||
|
|
||||||
|
# Bind to key press events with a decorator
|
||||||
|
@mpv.on_key_press("space")
|
||||||
|
def space_handler():
|
||||||
|
pass
|
||||||
|
|
||||||
|
# You can also observe and wait for properties.
|
||||||
|
@mpv.property_observer("eof-reached")
|
||||||
|
def handle_eof(name, value):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Or simply wait for the value to change once.
|
||||||
|
mpv.wait_for_property("duration")
|
||||||
|
```
|
||||||
460
syncplay/players/python_mpv_jsonipc/docs.md
Normal file
460
syncplay/players/python_mpv_jsonipc/docs.md
Normal file
@ -0,0 +1,460 @@
|
|||||||
|
<a name=".python_mpv_jsonipc"></a>
|
||||||
|
|
||||||
|
## python\_mpv\_jsonipc
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPVError"></a>
|
||||||
|
|
||||||
|
### MPVError
|
||||||
|
|
||||||
|
``` python
|
||||||
|
class MPVError(Exception):
|
||||||
|
| MPVError(**args, ****kwargs)
|
||||||
|
```
|
||||||
|
|
||||||
|
An error originating from MPV or due to a problem with MPV.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.WindowsSocket"></a>
|
||||||
|
|
||||||
|
### WindowsSocket
|
||||||
|
|
||||||
|
``` python
|
||||||
|
class WindowsSocket(threading.Thread)
|
||||||
|
```
|
||||||
|
|
||||||
|
Wraps a Windows named pipe in a high-level interface. (Internal)
|
||||||
|
|
||||||
|
Data is automatically encoded and decoded as JSON. The callback
|
||||||
|
function will be called for each inbound message.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.WindowsSocket.__init__"></a>
|
||||||
|
|
||||||
|
#### \_\_init\_\_
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| __init__(ipc_socket, callback=None)
|
||||||
|
```
|
||||||
|
|
||||||
|
Create the wrapper.
|
||||||
|
|
||||||
|
**ipc\_socket** is the pipe name. (Not including \\\\.\\pipe\\)
|
||||||
|
**callback(json\_data)** is the function for recieving events.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.WindowsSocket.stop"></a>
|
||||||
|
|
||||||
|
#### stop
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| stop()
|
||||||
|
```
|
||||||
|
|
||||||
|
Terminate the thread.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.WindowsSocket.send"></a>
|
||||||
|
|
||||||
|
#### send
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| send(data)
|
||||||
|
```
|
||||||
|
|
||||||
|
Send **data** to the pipe, encoded as JSON.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.WindowsSocket.run"></a>
|
||||||
|
|
||||||
|
#### run
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| run()
|
||||||
|
```
|
||||||
|
|
||||||
|
Process pipe events. Do not run this directly. Use **start**.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.UnixSocket"></a>
|
||||||
|
|
||||||
|
### UnixSocket
|
||||||
|
|
||||||
|
``` python
|
||||||
|
class UnixSocket(threading.Thread)
|
||||||
|
```
|
||||||
|
|
||||||
|
Wraps a Unix/Linux socket in a high-level interface. (Internal)
|
||||||
|
|
||||||
|
Data is automatically encoded and decoded as JSON. The callback
|
||||||
|
function will be called for each inbound message.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.UnixSocket.__init__"></a>
|
||||||
|
|
||||||
|
#### \_\_init\_\_
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| __init__(ipc_socket, callback=None)
|
||||||
|
```
|
||||||
|
|
||||||
|
Create the wrapper.
|
||||||
|
|
||||||
|
**ipc\_socket** is the path to the socket.
|
||||||
|
**callback(json\_data)** is the function for recieving events.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.UnixSocket.stop"></a>
|
||||||
|
|
||||||
|
#### stop
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| stop()
|
||||||
|
```
|
||||||
|
|
||||||
|
Terminate the thread.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.UnixSocket.send"></a>
|
||||||
|
|
||||||
|
#### send
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| send(data)
|
||||||
|
```
|
||||||
|
|
||||||
|
Send **data** to the socket, encoded as JSON.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.UnixSocket.run"></a>
|
||||||
|
|
||||||
|
#### run
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| run()
|
||||||
|
```
|
||||||
|
|
||||||
|
Process socket events. Do not run this directly. Use **start**.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPVProcess"></a>
|
||||||
|
|
||||||
|
### MPVProcess
|
||||||
|
|
||||||
|
``` python
|
||||||
|
class MPVProcess()
|
||||||
|
```
|
||||||
|
|
||||||
|
Manages an MPV process, ensuring the socket or pipe is available. (Internal)
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPVProcess.__init__"></a>
|
||||||
|
|
||||||
|
#### \_\_init\_\_
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| __init__(ipc_socket, mpv_location=None, ****kwargs)
|
||||||
|
```
|
||||||
|
|
||||||
|
Create and start the MPV process. Will block until socket/pipe is available.
|
||||||
|
|
||||||
|
**ipc\_socket** is the path to the Unix/Linux socket or name of the Windows pipe.
|
||||||
|
**mpv\_location** is the path to mpv. If left unset it tries the one in the PATH.
|
||||||
|
|
||||||
|
All other arguments are forwarded to MPV as command-line arguments.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPVProcess.stop"></a>
|
||||||
|
|
||||||
|
#### stop
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| stop()
|
||||||
|
```
|
||||||
|
|
||||||
|
Terminate the process.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPVInter"></a>
|
||||||
|
|
||||||
|
### MPVInter
|
||||||
|
|
||||||
|
``` python
|
||||||
|
class MPVInter()
|
||||||
|
```
|
||||||
|
|
||||||
|
Low-level interface to MPV. Does NOT manage an mpv process. (Internal)
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPVInter.__init__"></a>
|
||||||
|
|
||||||
|
#### \_\_init\_\_
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| __init__(ipc_socket, callback=None)
|
||||||
|
```
|
||||||
|
|
||||||
|
Create the wrapper.
|
||||||
|
|
||||||
|
**ipc\_socket** is the path to the Unix/Linux socket or name of the Windows pipe.
|
||||||
|
**callback(event\_name, data)** is the function for recieving events.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPVInter.stop"></a>
|
||||||
|
|
||||||
|
#### stop
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| stop()
|
||||||
|
```
|
||||||
|
|
||||||
|
Terminate the underlying connection.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPVInter.event_callback"></a>
|
||||||
|
|
||||||
|
#### event\_callback
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| event_callback(data)
|
||||||
|
```
|
||||||
|
|
||||||
|
Internal callback for recieving events from MPV.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPVInter.command"></a>
|
||||||
|
|
||||||
|
#### command
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| command(command, **args)
|
||||||
|
```
|
||||||
|
|
||||||
|
Issue a command to MPV. Will block until completed or timeout is reached.
|
||||||
|
|
||||||
|
**command** is the name of the MPV command
|
||||||
|
|
||||||
|
All further arguments are forwarded to the MPV command.
|
||||||
|
Throws TimeoutError if timeout of 120 seconds is reached.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.EventHandler"></a>
|
||||||
|
|
||||||
|
### EventHandler
|
||||||
|
|
||||||
|
``` python
|
||||||
|
class EventHandler(threading.Thread)
|
||||||
|
```
|
||||||
|
|
||||||
|
Event handling thread. (Internal)
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.EventHandler.__init__"></a>
|
||||||
|
|
||||||
|
#### \_\_init\_\_
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| __init__()
|
||||||
|
```
|
||||||
|
|
||||||
|
Create an instance of the thread.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.EventHandler.put_task"></a>
|
||||||
|
|
||||||
|
#### put\_task
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| put_task(func, **args)
|
||||||
|
```
|
||||||
|
|
||||||
|
Put a new task to the thread.
|
||||||
|
|
||||||
|
**func** is the function to call
|
||||||
|
|
||||||
|
All further arguments are forwarded to **func**.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.EventHandler.stop"></a>
|
||||||
|
|
||||||
|
#### stop
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| stop()
|
||||||
|
```
|
||||||
|
|
||||||
|
Terminate the thread.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.EventHandler.run"></a>
|
||||||
|
|
||||||
|
#### run
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| run()
|
||||||
|
```
|
||||||
|
|
||||||
|
Process socket events. Do not run this directly. Use **start**.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPV"></a>
|
||||||
|
|
||||||
|
### MPV
|
||||||
|
|
||||||
|
``` python
|
||||||
|
class MPV()
|
||||||
|
```
|
||||||
|
|
||||||
|
The main MPV interface class. Use this to control MPV.
|
||||||
|
|
||||||
|
This will expose all mpv commands as callable methods and all properties.
|
||||||
|
You can set properties and call the commands directly.
|
||||||
|
|
||||||
|
Please note that if you are using a really old MPV version, a fallback command
|
||||||
|
list is used. Not all commands may actually work when this fallback is used.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPV.__init__"></a>
|
||||||
|
|
||||||
|
#### \_\_init\_\_
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| __init__(start_mpv=True, ipc_socket=None, mpv_location=None, log_handler=None, loglevel=None, ****kwargs)
|
||||||
|
```
|
||||||
|
|
||||||
|
Create the interface to MPV and process instance.
|
||||||
|
|
||||||
|
**start\_mpv** will start an MPV process if true. (Default: True)
|
||||||
|
**ipc\_socket** is the path to the Unix/Linux socket or name of Windows pipe. (Default: Random Temp File)
|
||||||
|
**mpv\_location** is the location of MPV for **start\_mpv**. (Default: Use MPV in PATH)
|
||||||
|
**log\_handler(level, prefix, text)** is an optional handler for log events. (Default: Disabled)
|
||||||
|
**loglevel** is the level for log messages. Levels are fatal, error, warn, info, v, debug, trace. (Default: Disabled)
|
||||||
|
|
||||||
|
All other arguments are forwarded to MPV as command-line arguments if **start\_mpv** is used.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPV.bind_event"></a>
|
||||||
|
|
||||||
|
#### bind\_event
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| bind_event(name, callback)
|
||||||
|
```
|
||||||
|
|
||||||
|
Bind a callback to an MPV event.
|
||||||
|
|
||||||
|
**name** is the MPV event name.
|
||||||
|
**callback(event\_data)** is the function to call.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPV.on_event"></a>
|
||||||
|
|
||||||
|
#### on\_event
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| on_event(name)
|
||||||
|
```
|
||||||
|
|
||||||
|
Decorator to bind a callback to an MPV event.
|
||||||
|
|
||||||
|
@on\_event(name)
|
||||||
|
def my\_callback(event\_data):
|
||||||
|
pass
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPV.event_callback"></a>
|
||||||
|
|
||||||
|
#### event\_callback
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| event_callback(name)
|
||||||
|
```
|
||||||
|
|
||||||
|
An alias for on\_event to maintain compatibility with python-mpv.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPV.on_key_press"></a>
|
||||||
|
|
||||||
|
#### on\_key\_press
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| on_key_press(name)
|
||||||
|
```
|
||||||
|
|
||||||
|
Decorator to bind a callback to an MPV keypress event.
|
||||||
|
|
||||||
|
@on\_key\_press(key\_name)
|
||||||
|
def my\_callback():
|
||||||
|
pass
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPV.bind_key_press"></a>
|
||||||
|
|
||||||
|
#### bind\_key\_press
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| bind_key_press(name, callback)
|
||||||
|
```
|
||||||
|
|
||||||
|
Bind a callback to an MPV keypress event.
|
||||||
|
|
||||||
|
**name** is the key symbol.
|
||||||
|
**callback()** is the function to call.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPV.bind_property_observer"></a>
|
||||||
|
|
||||||
|
#### bind\_property\_observer
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| bind_property_observer(name, callback)
|
||||||
|
```
|
||||||
|
|
||||||
|
Bind a callback to an MPV property change.
|
||||||
|
|
||||||
|
**name** is the property name.
|
||||||
|
**callback(name, data)** is the function to call.
|
||||||
|
|
||||||
|
Returns a unique observer ID needed to destroy the observer.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPV.unbind_property_observer"></a>
|
||||||
|
|
||||||
|
#### unbind\_property\_observer
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| unbind_property_observer(observer_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
Remove callback to an MPV property change.
|
||||||
|
|
||||||
|
**observer\_id** is the id returned by bind\_property\_observer.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPV.property_observer"></a>
|
||||||
|
|
||||||
|
#### property\_observer
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| property_observer(name)
|
||||||
|
```
|
||||||
|
|
||||||
|
Decorator to bind a callback to an MPV property change.
|
||||||
|
|
||||||
|
@on\_key\_press(property\_name)
|
||||||
|
def my\_callback(name, data):
|
||||||
|
pass
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPV.wait_for_property"></a>
|
||||||
|
|
||||||
|
#### wait\_for\_property
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| wait_for_property(name)
|
||||||
|
```
|
||||||
|
|
||||||
|
Waits for the value of a property to change.
|
||||||
|
|
||||||
|
**name** is the name of the property.
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPV.play"></a>
|
||||||
|
|
||||||
|
#### play
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| play(url)
|
||||||
|
```
|
||||||
|
|
||||||
|
Play the specified URL. An alias to loadfile().
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPV.terminate"></a>
|
||||||
|
|
||||||
|
#### terminate
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| terminate()
|
||||||
|
```
|
||||||
|
|
||||||
|
Terminate the connection to MPV and process (if started by this module).
|
||||||
|
|
||||||
|
<a name=".python_mpv_jsonipc.MPV.command"></a>
|
||||||
|
|
||||||
|
#### command
|
||||||
|
|
||||||
|
``` python
|
||||||
|
| command(command, **args)
|
||||||
|
```
|
||||||
|
|
||||||
|
Send a command to MPV. All commands are bound to the class by default,
|
||||||
|
except JSON IPC specific commands. This may also be useful to retain
|
||||||
|
compatibility with python-mpv, as it does not bind all of the commands.
|
||||||
|
|
||||||
|
**command** is the command name.
|
||||||
|
|
||||||
|
All further arguments are forwarded to the MPV command.
|
||||||
607
syncplay/players/python_mpv_jsonipc/python_mpv_jsonipc.py
Normal file
607
syncplay/players/python_mpv_jsonipc/python_mpv_jsonipc.py
Normal file
@ -0,0 +1,607 @@
|
|||||||
|
import threading
|
||||||
|
import socket
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import subprocess
|
||||||
|
import random
|
||||||
|
import queue
|
||||||
|
import logging
|
||||||
|
|
||||||
|
log = logging.getLogger('mpv-jsonipc')
|
||||||
|
|
||||||
|
if os.name == "nt":
|
||||||
|
import _winapi
|
||||||
|
from multiprocessing.connection import PipeConnection
|
||||||
|
|
||||||
|
TIMEOUT = 120
|
||||||
|
|
||||||
|
# Older MPV versions do not allow us to dynamically retrieve the command list.
|
||||||
|
FALLBACK_COMMAND_LIST = [
|
||||||
|
'ignore', 'seek', 'revert-seek', 'quit', 'quit-watch-later', 'stop', 'frame-step', 'frame-back-step',
|
||||||
|
'playlist-next', 'playlist-prev', 'playlist-shuffle', 'playlist-unshuffle', 'sub-step', 'sub-seek',
|
||||||
|
'print-text', 'show-text', 'expand-text', 'expand-path', 'show-progress', 'sub-add', 'audio-add',
|
||||||
|
'video-add', 'sub-remove', 'audio-remove', 'video-remove', 'sub-reload', 'audio-reload', 'video-reload',
|
||||||
|
'rescan-external-files', 'screenshot', 'screenshot-to-file', 'screenshot-raw', 'loadfile', 'loadlist',
|
||||||
|
'playlist-clear', 'playlist-remove', 'playlist-move', 'run', 'subprocess', 'set', 'change-list', 'add',
|
||||||
|
'cycle', 'multiply', 'cycle-values', 'enable-section', 'disable-section', 'define-section', 'ab-loop',
|
||||||
|
'drop-buffers', 'af', 'vf', 'af-command', 'vf-command', 'ao-reload', 'script-binding', 'script-message',
|
||||||
|
'script-message-to', 'overlay-add', 'overlay-remove', 'osd-overlay', 'write-watch-later-config',
|
||||||
|
'hook-add', 'hook-ack', 'mouse', 'keybind', 'keypress', 'keydown', 'keyup', 'apply-profile',
|
||||||
|
'load-script', 'dump-cache', 'ab-loop-dump-cache', 'ab-loop-align-cache']
|
||||||
|
|
||||||
|
class MPVError(Exception):
|
||||||
|
"""An error originating from MPV or due to a problem with MPV."""
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(MPVError, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
class WindowsSocket(threading.Thread):
|
||||||
|
"""
|
||||||
|
Wraps a Windows named pipe in a high-level interface. (Internal)
|
||||||
|
|
||||||
|
Data is automatically encoded and decoded as JSON. The callback
|
||||||
|
function will be called for each inbound message.
|
||||||
|
"""
|
||||||
|
def __init__(self, ipc_socket, callback=None):
|
||||||
|
"""Create the wrapper.
|
||||||
|
|
||||||
|
*ipc_socket* is the pipe name. (Not including \\\\.\\pipe\\)
|
||||||
|
*callback(json_data)* is the function for recieving events.
|
||||||
|
"""
|
||||||
|
ipc_socket = "\\\\.\\pipe\\" + ipc_socket
|
||||||
|
self.callback = callback
|
||||||
|
|
||||||
|
access = _winapi.GENERIC_READ | _winapi.GENERIC_WRITE
|
||||||
|
limit = 5 # Connection may fail at first. Try 5 times.
|
||||||
|
for _ in range(limit):
|
||||||
|
try:
|
||||||
|
pipe_handle = _winapi.CreateFile(
|
||||||
|
ipc_socket, access, 0, _winapi.NULL, _winapi.OPEN_EXISTING,
|
||||||
|
_winapi.FILE_FLAG_OVERLAPPED, _winapi.NULL
|
||||||
|
)
|
||||||
|
break
|
||||||
|
except OSError:
|
||||||
|
time.sleep(1)
|
||||||
|
else:
|
||||||
|
raise MPVError("Cannot connect to pipe.")
|
||||||
|
self.socket = PipeConnection(pipe_handle)
|
||||||
|
|
||||||
|
if self.callback is None:
|
||||||
|
self.callback = lambda data: None
|
||||||
|
|
||||||
|
threading.Thread.__init__(self)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Terminate the thread."""
|
||||||
|
if self.socket is not None:
|
||||||
|
self.socket.close()
|
||||||
|
self.join()
|
||||||
|
|
||||||
|
def send(self, data):
|
||||||
|
"""Send *data* to the pipe, encoded as JSON."""
|
||||||
|
self.socket.send_bytes(json.dumps(data).encode('utf-8') + b'\n')
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Process pipe events. Do not run this directly. Use *start*."""
|
||||||
|
data = b''
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
current_data = self.socket.recv_bytes(2048)
|
||||||
|
if current_data == b'':
|
||||||
|
break
|
||||||
|
|
||||||
|
data += current_data
|
||||||
|
if data[-1] != 10:
|
||||||
|
continue
|
||||||
|
|
||||||
|
data = data.decode('utf-8', 'ignore').encode('utf-8')
|
||||||
|
for item in data.split(b'\n'):
|
||||||
|
if item == b'':
|
||||||
|
continue
|
||||||
|
json_data = json.loads(item)
|
||||||
|
self.callback(json_data)
|
||||||
|
data = b''
|
||||||
|
except EOFError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class UnixSocket(threading.Thread):
|
||||||
|
"""
|
||||||
|
Wraps a Unix/Linux socket in a high-level interface. (Internal)
|
||||||
|
|
||||||
|
Data is automatically encoded and decoded as JSON. The callback
|
||||||
|
function will be called for each inbound message.
|
||||||
|
"""
|
||||||
|
def __init__(self, ipc_socket, callback=None):
|
||||||
|
"""Create the wrapper.
|
||||||
|
|
||||||
|
*ipc_socket* is the path to the socket.
|
||||||
|
*callback(json_data)* is the function for recieving events.
|
||||||
|
"""
|
||||||
|
self.ipc_socket = ipc_socket
|
||||||
|
self.callback = callback
|
||||||
|
self.socket = socket.socket(socket.AF_UNIX)
|
||||||
|
self.socket.connect(self.ipc_socket)
|
||||||
|
|
||||||
|
if self.callback is None:
|
||||||
|
self.callback = lambda data: None
|
||||||
|
|
||||||
|
threading.Thread.__init__(self)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Terminate the thread."""
|
||||||
|
if self.socket is not None:
|
||||||
|
self.socket.shutdown(socket.SHUT_WR)
|
||||||
|
self.socket.close()
|
||||||
|
self.join()
|
||||||
|
|
||||||
|
def send(self, data):
|
||||||
|
"""Send *data* to the socket, encoded as JSON."""
|
||||||
|
self.socket.send(json.dumps(data).encode('utf-8') + b'\n')
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Process socket events. Do not run this directly. Use *start*."""
|
||||||
|
data = b''
|
||||||
|
while True:
|
||||||
|
current_data = self.socket.recv(1024)
|
||||||
|
if current_data == b'':
|
||||||
|
break
|
||||||
|
|
||||||
|
data += current_data
|
||||||
|
if data[-1] != 10:
|
||||||
|
continue
|
||||||
|
|
||||||
|
data = data.decode('utf-8', 'ignore').encode('utf-8')
|
||||||
|
for item in data.split(b'\n'):
|
||||||
|
if item == b'':
|
||||||
|
continue
|
||||||
|
json_data = json.loads(item)
|
||||||
|
self.callback(json_data)
|
||||||
|
data = b''
|
||||||
|
|
||||||
|
class MPVProcess:
|
||||||
|
"""
|
||||||
|
Manages an MPV process, ensuring the socket or pipe is available. (Internal)
|
||||||
|
"""
|
||||||
|
def __init__(self, ipc_socket, mpv_location=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Create and start the MPV process. Will block until socket/pipe is available.
|
||||||
|
|
||||||
|
*ipc_socket* is the path to the Unix/Linux socket or name of the Windows pipe.
|
||||||
|
*mpv_location* is the path to mpv. If left unset it tries the one in the PATH.
|
||||||
|
|
||||||
|
All other arguments are forwarded to MPV as command-line arguments.
|
||||||
|
"""
|
||||||
|
if mpv_location is None:
|
||||||
|
if os.name == 'nt':
|
||||||
|
mpv_location = "mpv.exe"
|
||||||
|
else:
|
||||||
|
mpv_location = "mpv"
|
||||||
|
|
||||||
|
log.debug("Staring MPV from {0}.".format(mpv_location))
|
||||||
|
ipc_socket_name = ipc_socket
|
||||||
|
if os.name == 'nt':
|
||||||
|
ipc_socket = "\\\\.\\pipe\\" + ipc_socket
|
||||||
|
|
||||||
|
if os.name != 'nt' and os.path.exists(ipc_socket):
|
||||||
|
os.remove(ipc_socket)
|
||||||
|
|
||||||
|
log.debug("Using IPC socket {0} for MPV.".format(ipc_socket))
|
||||||
|
self.ipc_socket = ipc_socket
|
||||||
|
args = [mpv_location]
|
||||||
|
self._set_default(kwargs, "idle", True)
|
||||||
|
self._set_default(kwargs, "input_ipc_server", ipc_socket_name)
|
||||||
|
self._set_default(kwargs, "input_terminal", False)
|
||||||
|
self._set_default(kwargs, "terminal", False)
|
||||||
|
args.extend("--{0}={1}".format(v[0].replace("_", "-"), self._mpv_fmt(v[1]))
|
||||||
|
for v in kwargs.items())
|
||||||
|
self.process = subprocess.Popen(args)
|
||||||
|
ipc_exists = False
|
||||||
|
for _ in range(100): # Give MPV 10 seconds to start.
|
||||||
|
time.sleep(0.1)
|
||||||
|
self.process.poll()
|
||||||
|
if os.path.exists(ipc_socket):
|
||||||
|
ipc_exists = True
|
||||||
|
log.debug("Found MPV socket.")
|
||||||
|
break
|
||||||
|
if self.process.returncode is not None:
|
||||||
|
log.error("MPV failed with returncode {0}.".format(self.process.returncode))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.process.terminate()
|
||||||
|
raise MPVError("MPV start timed out.")
|
||||||
|
|
||||||
|
if not ipc_exists or self.process.returncode is not None:
|
||||||
|
self.process.terminate()
|
||||||
|
raise MPVError("MPV not started.")
|
||||||
|
|
||||||
|
def _set_default(self, prop_dict, key, value):
|
||||||
|
if key not in prop_dict:
|
||||||
|
prop_dict[key] = value
|
||||||
|
|
||||||
|
def _mpv_fmt(self, data):
|
||||||
|
if data == True:
|
||||||
|
return "yes"
|
||||||
|
elif data == False:
|
||||||
|
return "no"
|
||||||
|
else:
|
||||||
|
return data
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Terminate the process."""
|
||||||
|
self.process.terminate()
|
||||||
|
if os.name != 'nt' and os.path.exists(self.ipc_socket):
|
||||||
|
os.remove(self.ipc_socket)
|
||||||
|
|
||||||
|
class MPVInter:
|
||||||
|
"""
|
||||||
|
Low-level interface to MPV. Does NOT manage an mpv process. (Internal)
|
||||||
|
"""
|
||||||
|
def __init__(self, ipc_socket, callback=None):
|
||||||
|
"""Create the wrapper.
|
||||||
|
|
||||||
|
*ipc_socket* is the path to the Unix/Linux socket or name of the Windows pipe.
|
||||||
|
*callback(event_name, data)* is the function for recieving events.
|
||||||
|
"""
|
||||||
|
Socket = UnixSocket
|
||||||
|
if os.name == 'nt':
|
||||||
|
Socket = WindowsSocket
|
||||||
|
|
||||||
|
self.callback = callback
|
||||||
|
if self.callback is None:
|
||||||
|
self.callback = lambda event, data: None
|
||||||
|
|
||||||
|
self.socket = Socket(ipc_socket, self.event_callback)
|
||||||
|
self.socket.start()
|
||||||
|
self.command_id = 1
|
||||||
|
self.rid_lock = threading.Lock()
|
||||||
|
self.socket_lock = threading.Lock()
|
||||||
|
self.cid_result = {}
|
||||||
|
self.cid_wait = {}
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Terminate the underlying connection."""
|
||||||
|
self.socket.stop()
|
||||||
|
|
||||||
|
def event_callback(self, data):
|
||||||
|
"""Internal callback for recieving events from MPV."""
|
||||||
|
if "request_id" in data:
|
||||||
|
self.cid_result[data["request_id"]] = data
|
||||||
|
self.cid_wait[data["request_id"]].set()
|
||||||
|
elif "event" in data:
|
||||||
|
self.callback(data["event"], data)
|
||||||
|
|
||||||
|
def command(self, command, *args):
|
||||||
|
"""
|
||||||
|
Issue a command to MPV. Will block until completed or timeout is reached.
|
||||||
|
|
||||||
|
*command* is the name of the MPV command
|
||||||
|
|
||||||
|
All further arguments are forwarded to the MPV command.
|
||||||
|
Throws TimeoutError if timeout of 120 seconds is reached.
|
||||||
|
"""
|
||||||
|
self.rid_lock.acquire()
|
||||||
|
command_id = self.command_id
|
||||||
|
self.command_id += 1
|
||||||
|
self.rid_lock.release()
|
||||||
|
|
||||||
|
event = threading.Event()
|
||||||
|
self.cid_wait[command_id] = event
|
||||||
|
|
||||||
|
command_list = [command]
|
||||||
|
command_list.extend(args)
|
||||||
|
try:
|
||||||
|
self.socket_lock.acquire()
|
||||||
|
self.socket.send({"command":command_list, "request_id": command_id})
|
||||||
|
finally:
|
||||||
|
self.socket_lock.release()
|
||||||
|
|
||||||
|
has_event = event.wait(timeout=TIMEOUT)
|
||||||
|
if has_event:
|
||||||
|
data = self.cid_result[command_id]
|
||||||
|
del self.cid_result[command_id]
|
||||||
|
del self.cid_wait[command_id]
|
||||||
|
if data["error"] != "success":
|
||||||
|
if data["error"] == "property unavailable":
|
||||||
|
return None
|
||||||
|
raise MPVError(data["error"])
|
||||||
|
else:
|
||||||
|
return data.get("data")
|
||||||
|
else:
|
||||||
|
raise TimeoutError("No response from MPV.")
|
||||||
|
|
||||||
|
class EventHandler(threading.Thread):
|
||||||
|
"""Event handling thread. (Internal)"""
|
||||||
|
def __init__(self):
|
||||||
|
"""Create an instance of the thread."""
|
||||||
|
self.queue = queue.Queue()
|
||||||
|
threading.Thread.__init__(self)
|
||||||
|
|
||||||
|
def put_task(self, func, *args):
|
||||||
|
"""
|
||||||
|
Put a new task to the thread.
|
||||||
|
|
||||||
|
*func* is the function to call
|
||||||
|
|
||||||
|
All further arguments are forwarded to *func*.
|
||||||
|
"""
|
||||||
|
self.queue.put((func, args))
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Terminate the thread."""
|
||||||
|
self.queue.put("quit")
|
||||||
|
self.join()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Process socket events. Do not run this directly. Use *start*."""
|
||||||
|
while True:
|
||||||
|
event = self.queue.get()
|
||||||
|
if event == "quit":
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
event[0](*event[1])
|
||||||
|
except Exception:
|
||||||
|
log.error("EventHandler caught exception from {0}.".format(event), exc_info=1)
|
||||||
|
|
||||||
|
class MPV:
|
||||||
|
"""
|
||||||
|
The main MPV interface class. Use this to control MPV.
|
||||||
|
|
||||||
|
This will expose all mpv commands as callable methods and all properties.
|
||||||
|
You can set properties and call the commands directly.
|
||||||
|
|
||||||
|
Please note that if you are using a really old MPV version, a fallback command
|
||||||
|
list is used. Not all commands may actually work when this fallback is used.
|
||||||
|
"""
|
||||||
|
def __init__(self, start_mpv=True, ipc_socket=None, mpv_location=None, log_handler=None, loglevel=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Create the interface to MPV and process instance.
|
||||||
|
|
||||||
|
*start_mpv* will start an MPV process if true. (Default: True)
|
||||||
|
*ipc_socket* is the path to the Unix/Linux socket or name of Windows pipe. (Default: Random Temp File)
|
||||||
|
*mpv_location* is the location of MPV for *start_mpv*. (Default: Use MPV in PATH)
|
||||||
|
*log_handler(level, prefix, text)* is an optional handler for log events. (Default: Disabled)
|
||||||
|
*loglevel* is the level for log messages. Levels are fatal, error, warn, info, v, debug, trace. (Default: Disabled)
|
||||||
|
|
||||||
|
All other arguments are forwarded to MPV as command-line arguments if *start_mpv* is used.
|
||||||
|
"""
|
||||||
|
self.properties = {}
|
||||||
|
self.event_bindings = {}
|
||||||
|
self.key_bindings = {}
|
||||||
|
self.property_bindings = {}
|
||||||
|
self.mpv_process = None
|
||||||
|
self.mpv_inter = None
|
||||||
|
self.event_handler = EventHandler()
|
||||||
|
self.event_handler.start()
|
||||||
|
if ipc_socket is None:
|
||||||
|
rand_file = "mpv{0}".format(random.randint(0, 2**48))
|
||||||
|
if os.name == "nt":
|
||||||
|
ipc_socket = rand_file
|
||||||
|
else:
|
||||||
|
ipc_socket = "/tmp/{0}".format(rand_file)
|
||||||
|
|
||||||
|
if start_mpv:
|
||||||
|
# Attempt to start MPV 3 times.
|
||||||
|
for i in range(3):
|
||||||
|
try:
|
||||||
|
self.mpv_process = MPVProcess(ipc_socket, mpv_location, **kwargs)
|
||||||
|
break
|
||||||
|
except MPVError:
|
||||||
|
log.warning("MPV start failed.", exc_info=1)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise MPVError("MPV process retry limit reached.")
|
||||||
|
|
||||||
|
self.mpv_inter = MPVInter(ipc_socket, self._callback)
|
||||||
|
self.properties = set(x.replace("-", "_") for x in self.command("get_property", "property-list"))
|
||||||
|
try:
|
||||||
|
command_list = [x["name"] for x in self.command("get_property", "command-list")]
|
||||||
|
except MPVError:
|
||||||
|
log.warning("Using fallback command list.")
|
||||||
|
command_list = FALLBACK_COMMAND_LIST
|
||||||
|
for command in command_list:
|
||||||
|
object.__setattr__(self, command.replace("-", "_"), self._get_wrapper(command))
|
||||||
|
|
||||||
|
self._dir = list(self.properties)
|
||||||
|
self._dir.extend(object.__dir__(self))
|
||||||
|
|
||||||
|
self.observer_id = 1
|
||||||
|
self.observer_lock = threading.Lock()
|
||||||
|
self.keybind_id = 1
|
||||||
|
self.keybind_lock = threading.Lock()
|
||||||
|
|
||||||
|
if log_handler is not None and loglevel is not None:
|
||||||
|
self.command("request_log_messages", loglevel)
|
||||||
|
@self.on_event("log-message")
|
||||||
|
def log_handler_event(data):
|
||||||
|
self.event_handler.put_task(log_handler, data["level"], data["prefix"], data["text"].strip())
|
||||||
|
|
||||||
|
@self.on_event("property-change")
|
||||||
|
def event_handler(data):
|
||||||
|
if data.get("id") in self.property_bindings:
|
||||||
|
self.event_handler.put_task(self.property_bindings[data["id"]], data["name"], data.get("data"))
|
||||||
|
|
||||||
|
@self.on_event("client-message")
|
||||||
|
def client_message_handler(data):
|
||||||
|
args = data["args"]
|
||||||
|
if len(args) == 2 and args[0] == "custom-bind":
|
||||||
|
self.event_handler.put_task(self.key_bindings[args[1]])
|
||||||
|
|
||||||
|
def bind_event(self, name, callback):
|
||||||
|
"""
|
||||||
|
Bind a callback to an MPV event.
|
||||||
|
|
||||||
|
*name* is the MPV event name.
|
||||||
|
*callback(event_data)* is the function to call.
|
||||||
|
"""
|
||||||
|
if name not in self.event_bindings:
|
||||||
|
self.event_bindings[name] = set()
|
||||||
|
self.event_bindings[name].add(callback)
|
||||||
|
|
||||||
|
def on_event(self, name):
|
||||||
|
"""
|
||||||
|
Decorator to bind a callback to an MPV event.
|
||||||
|
|
||||||
|
@on_event(name)
|
||||||
|
def my_callback(event_data):
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
def wrapper(func):
|
||||||
|
self.bind_event(name, func)
|
||||||
|
return func
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
# Added for compatibility.
|
||||||
|
def event_callback(self, name):
|
||||||
|
"""An alias for on_event to maintain compatibility with python-mpv."""
|
||||||
|
return self.on_event(name)
|
||||||
|
|
||||||
|
def on_key_press(self, name):
|
||||||
|
"""
|
||||||
|
Decorator to bind a callback to an MPV keypress event.
|
||||||
|
|
||||||
|
@on_key_press(key_name)
|
||||||
|
def my_callback():
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
def wrapper(func):
|
||||||
|
self.bind_key_press(name, func)
|
||||||
|
return func
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
def bind_key_press(self, name, callback):
|
||||||
|
"""
|
||||||
|
Bind a callback to an MPV keypress event.
|
||||||
|
|
||||||
|
*name* is the key symbol.
|
||||||
|
*callback()* is the function to call.
|
||||||
|
"""
|
||||||
|
self.keybind_lock.acquire()
|
||||||
|
keybind_id = self.keybind_id
|
||||||
|
self.keybind_id += 1
|
||||||
|
self.keybind_lock.release()
|
||||||
|
|
||||||
|
bind_name = "bind{0}".format(keybind_id)
|
||||||
|
self.key_bindings["bind{0}".format(keybind_id)] = callback
|
||||||
|
try:
|
||||||
|
self.keybind(name, "script-message custom-bind {0}".format(bind_name))
|
||||||
|
except MPVError:
|
||||||
|
self.define_section(bind_name, "{0} script-message custom-bind {1}".format(name, bind_name))
|
||||||
|
self.enable_section(bind_name)
|
||||||
|
|
||||||
|
def bind_property_observer(self, name, callback):
|
||||||
|
"""
|
||||||
|
Bind a callback to an MPV property change.
|
||||||
|
|
||||||
|
*name* is the property name.
|
||||||
|
*callback(name, data)* is the function to call.
|
||||||
|
|
||||||
|
Returns a unique observer ID needed to destroy the observer.
|
||||||
|
"""
|
||||||
|
self.observer_lock.acquire()
|
||||||
|
observer_id = self.observer_id
|
||||||
|
self.observer_id += 1
|
||||||
|
self.observer_lock.release()
|
||||||
|
|
||||||
|
self.property_bindings[observer_id] = callback
|
||||||
|
self.command("observe_property", observer_id, name)
|
||||||
|
return observer_id
|
||||||
|
|
||||||
|
def unbind_property_observer(self, observer_id):
|
||||||
|
"""
|
||||||
|
Remove callback to an MPV property change.
|
||||||
|
|
||||||
|
*observer_id* is the id returned by bind_property_observer.
|
||||||
|
"""
|
||||||
|
self.command("unobserve_property", observer_id)
|
||||||
|
del self.property_bindings[observer_id]
|
||||||
|
|
||||||
|
def property_observer(self, name):
|
||||||
|
"""
|
||||||
|
Decorator to bind a callback to an MPV property change.
|
||||||
|
|
||||||
|
@property_observer(property_name)
|
||||||
|
def my_callback(name, data):
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
def wrapper(func):
|
||||||
|
self.bind_property_observer(name, func)
|
||||||
|
return func
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
def wait_for_property(self, name):
|
||||||
|
"""
|
||||||
|
Waits for the value of a property to change.
|
||||||
|
|
||||||
|
*name* is the name of the property.
|
||||||
|
"""
|
||||||
|
event = threading.Event()
|
||||||
|
first_event = True
|
||||||
|
def handler(*_):
|
||||||
|
nonlocal first_event
|
||||||
|
if first_event == True:
|
||||||
|
first_event = False
|
||||||
|
else:
|
||||||
|
event.set()
|
||||||
|
observer_id = self.bind_property_observer(name, handler)
|
||||||
|
event.wait()
|
||||||
|
self.unbind_property_observer(observer_id)
|
||||||
|
|
||||||
|
def _get_wrapper(self, name):
|
||||||
|
def wrapper(*args):
|
||||||
|
return self.command(name, *args)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
def _callback(self, event, data):
|
||||||
|
if event in self.event_bindings:
|
||||||
|
for callback in self.event_bindings[event]:
|
||||||
|
self.event_handler.put_task(callback, data)
|
||||||
|
|
||||||
|
def play(self, url):
|
||||||
|
"""Play the specified URL. An alias to loadfile()."""
|
||||||
|
self.loadfile(url)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.terminate()
|
||||||
|
|
||||||
|
def terminate(self):
|
||||||
|
"""Terminate the connection to MPV and process (if *start_mpv* is used)."""
|
||||||
|
if self.mpv_process:
|
||||||
|
self.mpv_process.stop()
|
||||||
|
if self.mpv_inter:
|
||||||
|
self.mpv_inter.stop()
|
||||||
|
self.event_handler.stop()
|
||||||
|
|
||||||
|
def command(self, command, *args):
|
||||||
|
"""
|
||||||
|
Send a command to MPV. All commands are bound to the class by default,
|
||||||
|
except JSON IPC specific commands. This may also be useful to retain
|
||||||
|
compatibility with python-mpv, as it does not bind all of the commands.
|
||||||
|
|
||||||
|
*command* is the command name.
|
||||||
|
|
||||||
|
All further arguments are forwarded to the MPV command.
|
||||||
|
"""
|
||||||
|
return self.mpv_inter.command(command, *args)
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
if name in self.properties:
|
||||||
|
return self.command("get_property", name.replace("_", "-"))
|
||||||
|
return object.__getattribute__(self, name)
|
||||||
|
|
||||||
|
def __setattr__(self, name, value):
|
||||||
|
if name not in {"properties", "command"} and name in self.properties:
|
||||||
|
return self.command("set_property", name.replace("_", "-"), value)
|
||||||
|
return object.__setattr__(self, name, value)
|
||||||
|
|
||||||
|
def __hasattr__(self, name):
|
||||||
|
if object.__hasattr__(self, name):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
getattr(self, name)
|
||||||
|
return True
|
||||||
|
except MPVError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __dir__(self):
|
||||||
|
return self._dir
|
||||||
25
syncplay/players/python_mpv_jsonipc/setup.py
Normal file
25
syncplay/players/python_mpv_jsonipc/setup.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from setuptools import setup
|
||||||
|
import os
|
||||||
|
|
||||||
|
with open("README.md", "r") as fh:
|
||||||
|
long_description = fh.read()
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='python-mpv-jsonipc',
|
||||||
|
version='1.1.10',
|
||||||
|
author="Ian Walton",
|
||||||
|
author_email="iwalton3@gmail.com",
|
||||||
|
description="Python API to MPV using JSON IPC",
|
||||||
|
license='Apache-2.0',
|
||||||
|
long_description=open('README.md').read(),
|
||||||
|
long_description_content_type="text/markdown",
|
||||||
|
url="https://github.com/iwalton3/python-mpv-jsonipc",
|
||||||
|
py_modules=['python_mpv_jsonipc'],
|
||||||
|
classifiers=[
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"License :: OSI Approved :: Apache Software License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
],
|
||||||
|
python_requires='>=3.6',
|
||||||
|
install_requires=[]
|
||||||
|
)
|
||||||
Loading…
x
Reference in New Issue
Block a user