diff --git a/syncplay/players/python_mpv_jsonipc/LICENSE.md b/syncplay/players/python_mpv_jsonipc/LICENSE.md
new file mode 100644
index 0000000..f5f4b8b
--- /dev/null
+++ b/syncplay/players/python_mpv_jsonipc/LICENSE.md
@@ -0,0 +1,195 @@
+Apache License
+==============
+
+_Version 2.0, January 2004_
+_<>_
+
+### 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.
+
diff --git a/syncplay/players/python_mpv_jsonipc/README.md b/syncplay/players/python_mpv_jsonipc/README.md
new file mode 100644
index 0000000..cc3cea7
--- /dev/null
+++ b/syncplay/players/python_mpv_jsonipc/README.md
@@ -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")
+```
diff --git a/syncplay/players/python_mpv_jsonipc/docs.md b/syncplay/players/python_mpv_jsonipc/docs.md
new file mode 100644
index 0000000..3e01b3d
--- /dev/null
+++ b/syncplay/players/python_mpv_jsonipc/docs.md
@@ -0,0 +1,460 @@
+
+
+## python\_mpv\_jsonipc
+
+
+
+### MPVError
+
+``` python
+class MPVError(Exception):
+ | MPVError(**args, ****kwargs)
+```
+
+An error originating from MPV or due to a problem with MPV.
+
+
+
+### 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.
+
+
+
+#### \_\_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.
+
+
+
+#### stop
+
+``` python
+ | stop()
+```
+
+Terminate the thread.
+
+
+
+#### send
+
+``` python
+ | send(data)
+```
+
+Send **data** to the pipe, encoded as JSON.
+
+
+
+#### run
+
+``` python
+ | run()
+```
+
+Process pipe events. Do not run this directly. Use **start**.
+
+
+
+### 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.
+
+
+
+#### \_\_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.
+
+
+
+#### stop
+
+``` python
+ | stop()
+```
+
+Terminate the thread.
+
+
+
+#### send
+
+``` python
+ | send(data)
+```
+
+Send **data** to the socket, encoded as JSON.
+
+
+
+#### run
+
+``` python
+ | run()
+```
+
+Process socket events. Do not run this directly. Use **start**.
+
+
+
+### MPVProcess
+
+``` python
+class MPVProcess()
+```
+
+Manages an MPV process, ensuring the socket or pipe is available. (Internal)
+
+
+
+#### \_\_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.
+
+
+
+#### stop
+
+``` python
+ | stop()
+```
+
+Terminate the process.
+
+
+
+### MPVInter
+
+``` python
+class MPVInter()
+```
+
+Low-level interface to MPV. Does NOT manage an mpv process. (Internal)
+
+
+
+#### \_\_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.
+
+
+
+#### stop
+
+``` python
+ | stop()
+```
+
+Terminate the underlying connection.
+
+
+
+#### event\_callback
+
+``` python
+ | event_callback(data)
+```
+
+Internal callback for recieving events from MPV.
+
+
+
+#### 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.
+
+
+
+### EventHandler
+
+``` python
+class EventHandler(threading.Thread)
+```
+
+Event handling thread. (Internal)
+
+
+
+#### \_\_init\_\_
+
+``` python
+ | __init__()
+```
+
+Create an instance of the thread.
+
+
+
+#### 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**.
+
+
+
+#### stop
+
+``` python
+ | stop()
+```
+
+Terminate the thread.
+
+
+
+#### run
+
+``` python
+ | run()
+```
+
+Process socket events. Do not run this directly. Use **start**.
+
+
+
+### 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.
+
+
+
+#### \_\_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.
+
+
+
+#### 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.
+
+
+
+#### on\_event
+
+``` python
+ | on_event(name)
+```
+
+Decorator to bind a callback to an MPV event.
+
+@on\_event(name)
+def my\_callback(event\_data):
+pass
+
+
+
+#### event\_callback
+
+``` python
+ | event_callback(name)
+```
+
+An alias for on\_event to maintain compatibility with python-mpv.
+
+
+
+#### 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
+
+
+
+#### 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.
+
+
+
+#### 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.
+
+
+
+#### 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.
+
+
+
+#### 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
+
+
+
+#### wait\_for\_property
+
+``` python
+ | wait_for_property(name)
+```
+
+Waits for the value of a property to change.
+
+**name** is the name of the property.
+
+
+
+#### play
+
+``` python
+ | play(url)
+```
+
+Play the specified URL. An alias to loadfile().
+
+
+
+#### terminate
+
+``` python
+ | terminate()
+```
+
+Terminate the connection to MPV and process (if started by this module).
+
+
+
+#### 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.
diff --git a/syncplay/players/python_mpv_jsonipc/python_mpv_jsonipc.py b/syncplay/players/python_mpv_jsonipc/python_mpv_jsonipc.py
new file mode 100644
index 0000000..f4a4b5a
--- /dev/null
+++ b/syncplay/players/python_mpv_jsonipc/python_mpv_jsonipc.py
@@ -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
diff --git a/syncplay/players/python_mpv_jsonipc/setup.py b/syncplay/players/python_mpv_jsonipc/setup.py
new file mode 100644
index 0000000..11be65a
--- /dev/null
+++ b/syncplay/players/python_mpv_jsonipc/setup.py
@@ -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=[]
+)