From 03ef29390fc13ccc92def59b4422e38a6b846dd4 Mon Sep 17 00:00:00 2001 From: Alberto Sottile Date: Sat, 17 Oct 2020 16:06:55 +0200 Subject: [PATCH] =?UTF-8?q?=C3=84dd=20support=20for=20IINA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- syncplay/constants.py | 10 ++ syncplay/players/__init__.py | 7 +- syncplay/players/iina.py | 77 ++++++++++++++ syncplay/players/mpv.py | 16 ++- .../python_mpv_jsonipc/python_mpv_jsonipc.py | 94 ++++++++++++++---- syncplay/resources/IINA.png | Bin 0 -> 1513 bytes 6 files changed, 183 insertions(+), 21 deletions(-) create mode 100644 syncplay/players/iina.py create mode 100644 syncplay/resources/IINA.png diff --git a/syncplay/constants.py b/syncplay/constants.py index 5a7a888..533bae5 100755 --- a/syncplay/constants.py +++ b/syncplay/constants.py @@ -139,6 +139,8 @@ USER_READY_MIN_VERSION = "1.3.0" SHARED_PLAYLIST_MIN_VERSION = "1.4.0" CHAT_MIN_VERSION = "1.5.0" FEATURE_LIST_MIN_VERSION = "1.5.0" + +IINA_PATHS = ['/Applications/IINA.app/Contents/MacOS/iina-cli'] MPC_PATHS = [ r"c:\program files (x86)\mpc-hc\mpc-hc.exe", r"c:\program files\mpc-hc\mpc-hc.exe", @@ -176,6 +178,7 @@ VLC_PATHS = [ ] VLC_ICONPATH = "vlc.png" +IINA_ICONPATH = "iina.png" MPLAYER_ICONPATH = "mplayer.png" MPV_ICONPATH = "mpv.png" MPVNET_ICONPATH = "mpvnet.png" @@ -250,6 +253,13 @@ MPV_ARGS = {'force-window': 'yes', 'keep-open-pause': 'yes' } +IINA_PROPERTIES = {'geometry': '25%+100+100', + 'idle': 'yes', + 'hr-seek': 'always', + 'input-terminal': 'no', + 'term-playing-msg': '\nANS_filename=${filename}\nANS_length=${=duration:${=length:0}}\nANS_path=${path}\n', + 'keep-open-pause': 'yes', + } MPV_NEW_VERSION = False MPV_OSC_VISIBILITY_CHANGE_VERSION = False diff --git a/syncplay/players/__init__.py b/syncplay/players/__init__.py index 8c812db..90fa73b 100755 --- a/syncplay/players/__init__.py +++ b/syncplay/players/__init__.py @@ -12,7 +12,12 @@ try: except ImportError: from syncplay.players.basePlayer import DummyPlayer MpcBePlayer = DummyPlayer +try: + from syncplay.players.iina import IinaPlayer +except ImportError: + from syncplay.players.basePlayer import DummyPlayer + IinaPlayer = DummyPlayer def getAvailablePlayers(): - return [MPCHCAPIPlayer, MpvPlayer, MpvnetPlayer, VlcPlayer, MpcBePlayer, MplayerPlayer] + return [MPCHCAPIPlayer, MpvPlayer, MpvnetPlayer, VlcPlayer, MpcBePlayer, MplayerPlayer, IinaPlayer] diff --git a/syncplay/players/iina.py b/syncplay/players/iina.py new file mode 100644 index 0000000..15b5bcb --- /dev/null +++ b/syncplay/players/iina.py @@ -0,0 +1,77 @@ +import os +from syncplay import constants +from syncplay.utils import findResourcePath +from syncplay.players.mpv import MpvPlayer +from syncplay.players.python_mpv_jsonipc.python_mpv_jsonipc import IINA + +class IinaPlayer(MpvPlayer): + + + @staticmethod + def run(client, playerPath, filePath, args): + constants.MPV_NEW_VERSION = True + constants.MPV_OSC_VISIBILITY_CHANGE_VERSION = True + return IinaPlayer(client, IinaPlayer.getExpandedPath(playerPath), filePath, args) + + @staticmethod + def getStartupArgs(userArgs): + args = {} + if userArgs: + for argToAdd in userArgs: + if argToAdd.startswith('--'): + argToAdd = argToAdd[2:] + elif argToAdd.startswith('-'): + argToAdd = argToAdd[1:] + if argToAdd.strip() == "": + continue + if "=" in argToAdd: + (argName, argValue) = argToAdd.split("=", 1) + else: + argName = argToAdd + argValue = "yes" + args[argName] = argValue + return args + + @staticmethod + def getDefaultPlayerPathsList(): + l = [] + for path in constants.IINA_PATHS: + p = IinaPlayer.getExpandedPath(path) + print(p) + if p: + l.append(p) + return l + + @staticmethod + def isValidPlayerPath(path): + if "iina-cli" in path and IinaPlayer.getExpandedPath(path): + return True + return False + + @staticmethod + def getExpandedPath(playerPath): + if os.access(playerPath, os.X_OK): + return playerPath + for path in os.environ['PATH'].split(':'): + path = os.path.join(os.path.realpath(path), playerPath) + if os.access(path, os.X_OK): + return path + + @staticmethod + def getIconPath(path): + return constants.IINA_ICONPATH + + def __init__(self, client, playerPath, filePath, args): + from twisted.internet import reactor + self.reactor = reactor + self._client = client + self._set_defaults() + + self._playerIPCHandler = IINA + self._create_listener(playerPath, filePath, args) + + def _preparePlayer(self): + for key, value in constants.IINA_PROPERTIES.items(): + self._setProperty(key, value) + self._listener.sendLine(["load-script", findResourcePath("syncplayintf.lua")]) + super()._preparePlayer() diff --git a/syncplay/players/mpv.py b/syncplay/players/mpv.py index 3971f83..22b3eb7 100755 --- a/syncplay/players/mpv.py +++ b/syncplay/players/mpv.py @@ -504,6 +504,12 @@ class MpvPlayer(BasePlayer): from twisted.internet import reactor self.reactor = reactor self._client = client + self._set_defaults() + + self._playerIPCHandler = MPV + self._create_listener(playerPath, filePath, args) + + def _set_defaults(self): self._paused = None self._position = 0.0 self._duration = None @@ -513,8 +519,10 @@ class MpvPlayer(BasePlayer): self.lastLoadedTime = None self.fileLoaded = False self.delayedFilePath = None + + def _create_listener(self, playerPath, filePath, args): try: - self._listener = self.__Listener(self, playerPath, filePath, args) + self._listener = self.__Listener(self, self._playerIPCHandler, playerPath, filePath, args) except ValueError: self._client.ui.showMessage(getMessage("mplayer-file-required-notification")) self._client.ui.showMessage(getMessage("mplayer-file-required-notification/example")) @@ -549,7 +557,8 @@ class MpvPlayer(BasePlayer): self.lineReceived(text) class __Listener(threading.Thread): - def __init__(self, playerController, playerPath, filePath, args): + def __init__(self, playerController, playerIPCHandler, playerPath, filePath, args): + self.playerIPCHandler = playerIPCHandler self.playerPath = playerPath self.mpv_arguments = playerController.getStartupArgs(args) self.mpv_running = True @@ -594,7 +603,7 @@ class MpvPlayer(BasePlayer): env['PYTHONPATH'] = pythonPath try: socket = self.mpv_arguments.get('input-ipc-server') - self.mpvpipe = MPV(mpv_location=self.playerPath, ipc_socket=socket, loglevel="info", log_handler=self.__playerController.mpv_log_handler, quit_callback=self.stop_client, **self.mpv_arguments) + self.mpvpipe = self.playerIPCHandler(mpv_location=self.playerPath, ipc_socket=socket, loglevel="info", log_handler=self.__playerController.mpv_log_handler, quit_callback=self.stop_client, **self.mpv_arguments) except Exception as e: self.quitReason = getMessage("media-player-error").format(str(e)) + " " + getMessage("mpv-failed-advice") self.__playerController.reactor.callFromThread(self.__playerController._client.ui.showErrorMessage, self.quitReason, True) @@ -721,5 +730,6 @@ class MpvPlayer(BasePlayer): except Exception as e: self.__playerController._client.ui.showDebugMessage("CANNOT SEND {} DUE TO {}".format(line, e)) self.stop_client() + raise except IOError: pass diff --git a/syncplay/players/python_mpv_jsonipc/python_mpv_jsonipc.py b/syncplay/players/python_mpv_jsonipc/python_mpv_jsonipc.py index 993606b..3b4513f 100644 --- a/syncplay/players/python_mpv_jsonipc/python_mpv_jsonipc.py +++ b/syncplay/players/python_mpv_jsonipc/python_mpv_jsonipc.py @@ -8,6 +8,8 @@ import random import queue import logging +from syncplay.utils import resourcespath + log = logging.getLogger('mpv-jsonipc') if os.name == "nt": @@ -207,13 +209,11 @@ class MPVProcess: 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()) + args = self._get_arglist(mpv_location, **kwargs) + + self._start_process(ipc_socket, args) + + def _start_process(self, ipc_socket, args): self.process = subprocess.Popen(args) ipc_exists = False for _ in range(100): # Give MPV 10 seconds to start. @@ -238,6 +238,16 @@ class MPVProcess: if key not in prop_dict: prop_dict[key] = value + def _get_arglist(self, exec_location, **kwargs): + args = [exec_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()) + return args + def _mpv_fmt(self, data): if data == True: return "yes" @@ -252,6 +262,40 @@ class MPVProcess: if os.name != 'nt' and os.path.exists(self.ipc_socket): os.remove(self.ipc_socket) +class IINAProcess(MPVProcess): + """ + Manages an IINA process, ensuring the socket or pipe is available. (Internal) + """ + + def _start_process(self, ipc_socket, args): + self.process = subprocess.Popen(args) + ipc_exists = False + for _ in range(100): # Give IINA 10 seconds to start. + time.sleep(0.1) + self.process.poll() + if os.path.exists(ipc_socket): + ipc_exists = True + log.debug("Found IINA socket.") + break + if self.process.returncode != 0: # iina-cli returns immediately after its start + log.error("IINA failed with returncode {0}.".format(self.process.returncode)) + break + else: + self.process.terminate() + raise MPVError("IINA start timed out.") + + if not ipc_exists or self.process.returncode != 0: + self.process.terminate() + raise MPVError("IINA not started.") + + def _get_arglist(self, exec_location, **kwargs): + args = [exec_location] + args.append(resourcespath + 'syncplay.png') + self._set_default(kwargs, "mpv-input-ipc-server", self.ipc_socket) + args.extend("--{0}={1}".format(v[0].replace("_", "-"), self._mpv_fmt(v[1])) + for v in kwargs.items()) + return args + class MPVInter: """ Low-level interface to MPV. Does NOT manage an mpv process. (Internal) @@ -405,16 +449,7 @@ class MPV: 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._start_mpv(ipc_socket, mpv_location, **kwargs) self.mpv_inter = MPVInter(ipc_socket, self._callback, self._quit_callback) self.properties = set(x.replace("-", "_") for x in self.command("get_property", "property-list")) @@ -451,6 +486,17 @@ class MPV: if len(args) == 2 and args[0] == "custom-bind": self.event_handler.put_task(self.key_bindings[args[1]]) + def _start_mpv(self, ipc_socket, mpv_location, **kwargs): + 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.") + def _quit_callback(self): """ Internal handler for quit events. @@ -638,3 +684,17 @@ class MPV: def __dir__(self): return self._dir + +class IINA(MPV): + """The main IINA interface class. Use this to control the MPV player instantiated by IINA.""" + + def _start_mpv(self, ipc_socket, mpv_location, **kwargs): + for i in range(3): + try: + self.mpv_process = IINAProcess(ipc_socket, mpv_location, **kwargs) + break + except MPVError: + log.warning("IINA start failed.", exc_info=1) + continue + else: + raise MPVError("IINA process retry limit reached.") \ No newline at end of file diff --git a/syncplay/resources/IINA.png b/syncplay/resources/IINA.png new file mode 100644 index 0000000000000000000000000000000000000000..525fb55397d9bb4982ab263f6b84c284537bbceb GIT binary patch literal 1513 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|CZtAqruq6Z zXaU(A3~Y>S49p-U3`{^m2+C#zvZWYU!Qu=IVT{snb{wMyLVbHC0}E758juD746gS7SYFT4t*I14-?iy0WWg+Z8+Vb&Z81_mbX%#etZ2wxwo zY3;nDA{o-C@9zzrKDK}xwt{K19`Se86_nJR{Hwo<>h+i#(Mch>H3D2 zmX`VkM*2oZx=P7{9O-#x!EwNQn0$BtH z5OH{+J zOY(uCP?VYMnOBlpR8RyA5wLC}sBYXU(9H@;EzZv=1!)e=%Pg^j8Gt5?uFE+;7iej5 zF|v!1#h~hK^g+Re6nv1l0gD17$BxTJA0E+mT+3p=832>TZBG}+5DUT9;IrAnjw1ix zrk8D=osoS>%J*+Zkl-qf+!HIe{co%Y`;fJ^Jx;44z)>)evm;I`j?4Ciqy8Fe{ADpIdkjq z)2C0@F3U_iwe*r*ZK-@7V&;C7h=+MTMD_1)Gci1o6 zUp~P_$#K$2m5+b_{bRJAetPQjXV2PZvAi-d))3*k{_gJX`X7&uo)(OpCNSZN{c;us zIi1T5sgV-0vR9MeZVX;ol68gu3P*sB*zL6R;?K^_hYcfdGWbnkn8uzM*;U{e(|v5i z!>hRwZH>Fj-`6BZmicQ<{pBObaJA<5sii(s9*1~rU7RpeV$G4t8ruqPZTGxIw~9Gj z`WP5RAF%fH%;B-_E57>t)4wIJf33TofA6F8Ve5m}zaL1R_Q195@J*rCs~7v`v->%& zkZChA=KT4Qc~8~M$E&Yq^`B4Oy?ggjucb;GdHBzZD({bIQxWKp8Ts`F15UtAG>QM>PXWr2*i-r|FWwg%U<&aS_@e~DqsA#o35ens0^ z4X=0oYZjV++TY*bdZy2@joE502OAFBIj&uH@b^DP4d$kFrc4gt>=K ziL~G9CMhm1etVv6t(kpAj>R!S_7`tvUN+NTuWBCsGk-yi-Tif$w@lxz-exb2LpC_J++4bYA$d#47er_uu!guHW4LwP8QQEeqEvp9>ex1C?=}u6{1-oD!M