734 lines
32 KiB
Python
Executable File
734 lines
32 KiB
Python
Executable File
# coding:utf8
|
|
import os
|
|
import re
|
|
import sys
|
|
import time
|
|
import subprocess
|
|
import threading
|
|
import ast
|
|
|
|
from syncplay import constants
|
|
from syncplay.messages import getMessage
|
|
from syncplay.players.basePlayer import BasePlayer
|
|
from syncplay.utils import isURL, findResourcePath
|
|
from syncplay.utils import isMacOS, isWindows, isASCII
|
|
from syncplay.vendor.python_mpv_jsonipc.python_mpv_jsonipc import MPV
|
|
|
|
class MpvPlayer(BasePlayer):
|
|
RE_VERSION = re.compile(r'.*mpv (\d+)\.(\d+)\.\d+.*')
|
|
osdMessageSeparator = "\\n"
|
|
osdMessageSeparator = "; " # TODO: Make conditional
|
|
POSITION_QUERY = 'time-pos'
|
|
OSD_QUERY = 'show_text'
|
|
RE_ANSWER = re.compile(constants.MPLAYER_ANSWER_REGEX)
|
|
lastResetTime = None
|
|
lastMPVPositionUpdate = None
|
|
alertOSDSupported = True
|
|
chatOSDSupported = True
|
|
speedSupported = True
|
|
customOpenDialog = False
|
|
|
|
@staticmethod
|
|
def run(client, playerPath, filePath, args):
|
|
try:
|
|
ver = MpvPlayer.RE_VERSION.search(subprocess.check_output([playerPath, '--version']).decode('utf-8'))
|
|
except:
|
|
ver = None
|
|
constants.MPV_NEW_VERSION = ver is None or int(ver.group(1)) > 0 or int(ver.group(2)) >= 23
|
|
if not constants.MPV_NEW_VERSION:
|
|
from twisted.internet import reactor
|
|
the_reactor = reactor
|
|
the_reactor.callFromThread(client.ui.showErrorMessage,
|
|
"This version of mpv is not compatible with Syncplay. "
|
|
"Please use mpv >=0.23.0.", True)
|
|
the_reactor.callFromThread(client.stop)
|
|
return
|
|
|
|
constants.MPV_OSC_VISIBILITY_CHANGE_VERSION = False if ver is None else int(ver.group(1)) > 0 or int(ver.group(2)) >= 28
|
|
if not constants.MPV_OSC_VISIBILITY_CHANGE_VERSION:
|
|
client.ui.showDebugMessage(
|
|
"This version of mpv is not known to be compatible with changing the OSC visibility. "
|
|
"Please use mpv >=0.28.0.")
|
|
return MpvPlayer(client, MpvPlayer.getExpandedPath(playerPath), filePath, args)
|
|
|
|
@staticmethod
|
|
def getStartupArgs(userArgs):
|
|
args = constants.MPV_ARGS
|
|
args["script"] = findResourcePath("syncplayintf.lua")
|
|
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.MPV_PATHS:
|
|
p = MpvPlayer.getExpandedPath(path)
|
|
if p:
|
|
l.append(p)
|
|
return l
|
|
|
|
@staticmethod
|
|
def isValidPlayerPath(path):
|
|
if "mpv" in path and "mpvnet" not in path and MpvPlayer.getExpandedPath(path):
|
|
return True
|
|
return False
|
|
|
|
@staticmethod
|
|
def getExpandedPath(playerPath):
|
|
if not os.path.isfile(playerPath):
|
|
if os.path.isfile(playerPath + "mpv.exe"):
|
|
playerPath += "mpv.exe"
|
|
return playerPath
|
|
elif os.path.isfile(playerPath + "\\mpv.exe"):
|
|
playerPath += "\\mpv.exe"
|
|
return 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.MPV_ICONPATH
|
|
|
|
@staticmethod
|
|
def getPlayerPathErrors(playerPath, filePath):
|
|
return None
|
|
|
|
def _setProperty(self, property_, value):
|
|
self._listener.sendLine(["set_property", property_, value])
|
|
|
|
def mpvErrorCheck(self, line):
|
|
if "Error parsing option" in line or "Error parsing commandline option" in line:
|
|
self.quitReason = getMessage("mpv-version-error")
|
|
|
|
elif "Could not open pipe at '/dev/stdin'" in line:
|
|
self.reactor.callFromThread(self._client.ui.showErrorMessage, getMessage("mpv-version-error"), True)
|
|
self.drop()
|
|
|
|
if constants and any(errormsg in line for errormsg in constants.MPV_ERROR_MESSAGES_TO_REPEAT):
|
|
self._client.ui.showErrorMessage(line)
|
|
|
|
def displayMessage(self, message, duration=(constants.OSD_DURATION * 1000), OSDType=constants.OSD_NOTIFICATION,
|
|
mood=constants.MESSAGE_NEUTRAL):
|
|
if not self._client._config["chatOutputEnabled"]:
|
|
messageString = self._sanitizeText(message.replace("\\n", "<NEWLINE>")).replace("<NEWLINE>", "\\n")
|
|
self._listener.mpvpipe.show_text(messageString, duration, constants.MPLAYER_OSD_LEVEL)
|
|
return
|
|
messageString = self._sanitizeText(message.replace("\\n", "<NEWLINE>")).replace(
|
|
"\\\\", constants.MPV_INPUT_BACKSLASH_SUBSTITUTE_CHARACTER).replace("<NEWLINE>", "\\n")
|
|
self._listener.sendLine(["script-message-to", "syncplayintf", "{}-osd-{}".format(OSDType, mood), messageString])
|
|
|
|
def displayChatMessage(self, username, message):
|
|
if not self._client._config["chatOutputEnabled"]:
|
|
messageString = "<{}> {}".format(username, message)
|
|
messageString = self._sanitizeText(messageString.replace("\\n", "<NEWLINE>")).replace("<NEWLINE>", "\\n")
|
|
duration = int(constants.OSD_DURATION * 1000)
|
|
self._listener.mpvpipe.show_text(messageString, duration, constants.MPLAYER_OSD_LEVEL)
|
|
return
|
|
username = self._sanitizeText(username.replace("\\", constants.MPV_INPUT_BACKSLASH_SUBSTITUTE_CHARACTER))
|
|
message = self._sanitizeText(message.replace("\\", constants.MPV_INPUT_BACKSLASH_SUBSTITUTE_CHARACTER))
|
|
messageString = "<{}> {}".format(username, message)
|
|
self._listener.sendLine(["script-message-to", "syncplayintf", "chat", messageString])
|
|
|
|
def setSpeed(self, value):
|
|
self._setProperty('speed', "{:.2f}".format(value))
|
|
|
|
def setPaused(self, value):
|
|
if self._paused == value:
|
|
self._client.ui.showDebugMessage("Not sending setPaused to mpv as state is already {}".format(value))
|
|
return
|
|
pauseValue = "yes" if value else "no"
|
|
self._setProperty("pause", pauseValue)
|
|
self._paused = value
|
|
if value == False:
|
|
self.lastMPVPositionUpdate = time.time()
|
|
|
|
def _getFilename(self):
|
|
self._getProperty('filename')
|
|
|
|
def _getLength(self):
|
|
self._getProperty('length')
|
|
|
|
def _getFilepath(self):
|
|
self._getProperty('path')
|
|
|
|
def _getProperty(self, property_):
|
|
floatProperties = ['time-pos']
|
|
if property_ in floatProperties:
|
|
propertyID = "={}".format(property_)
|
|
elif property_ == 'length':
|
|
propertyID = '=duration:${=length:0}'
|
|
else:
|
|
propertyID = property_
|
|
self._listener.sendLine(["print_text", '"ANS_{}=${{{}}}"'.format(property_, propertyID)])
|
|
|
|
def getCalculatedPosition(self):
|
|
if self.fileLoaded == False:
|
|
self._client.ui.showDebugMessage(
|
|
"File not loaded so using GlobalPosition for getCalculatedPosition({})".format(
|
|
self._client.getGlobalPosition()))
|
|
return self._client.getGlobalPosition()
|
|
|
|
if self.lastMPVPositionUpdate is None:
|
|
self._client.ui.showDebugMessage(
|
|
"MPV not updated position so using GlobalPosition for getCalculatedPosition ({})".format(
|
|
self._client.getGlobalPosition()))
|
|
return self._client.getGlobalPosition()
|
|
|
|
if self._recentlyReset():
|
|
self._client.ui.showDebugMessage(
|
|
"Recently reset so using self.position for getCalculatedPosition ({})".format(
|
|
self._position))
|
|
return self._position
|
|
|
|
diff = time.time() - self.lastMPVPositionUpdate
|
|
|
|
if diff > constants.MPV_UNRESPONSIVE_THRESHOLD:
|
|
self.reactor.callFromThread(
|
|
self._client.ui.showErrorMessage, getMessage("mpv-unresponsive-error").format(int(diff)), True)
|
|
self.drop()
|
|
if diff > constants.PLAYER_ASK_DELAY and not self._paused:
|
|
self._client.ui.showDebugMessage(
|
|
"mpv did not response in time, so assuming position is {} ({}+{})".format(
|
|
self._position + diff, self._position, diff))
|
|
return self._position + diff
|
|
else:
|
|
return self._position
|
|
|
|
def _storePosition(self, value):
|
|
if value is None:
|
|
self._client.ui.showDebugMessage("NONE TYPE POSITION!")
|
|
return
|
|
self.lastMPVPositionUpdate = time.time()
|
|
if self._recentlyReset():
|
|
self._client.ui.showDebugMessage("Recently reset, so storing position as 0")
|
|
self._position = 0
|
|
elif self._fileIsLoaded() or (value < constants.MPV_NEWFILE_IGNORE_TIME and self._fileIsLoaded(ignoreDelay=True)):
|
|
old_position = float(self._position)
|
|
self._position = max(value, 0)
|
|
#self._client.ui.showDebugMessage("Position changed from {} to {}".format(old_position, self._position))
|
|
else:
|
|
self._client.ui.showDebugMessage(
|
|
"No file loaded so storing position {} as GlobalPosition ({})".format(value, self._client.getGlobalPosition()))
|
|
self._position = self._client.getGlobalPosition()
|
|
|
|
def _storePauseState(self, value):
|
|
if value is None:
|
|
self._client.ui.showDebugMessage("NONE TYPE PAUSE STATE!")
|
|
return
|
|
if self._fileIsLoaded():
|
|
self._paused = value
|
|
#self._client.ui.showDebugMessage("PAUSE STATE STORED AS {}".format(self._paused))
|
|
else:
|
|
self._paused = self._client.getGlobalPaused()
|
|
#self._client.ui.showDebugMessage("STORING GLOBAL PAUSED AS FILE IS NOT LOADED")
|
|
|
|
|
|
def lineReceived(self, line):
|
|
if line:
|
|
self._client.ui.showDebugMessage("player << {}".format(line))
|
|
line = line.replace("[cplayer] ", "") # -v workaround
|
|
line = line.replace("[term-msg] ", "") # -v workaround
|
|
line = line.replace(" cplayer: ", "") # --msg-module workaround
|
|
line = line.replace(" term-msg: ", "")
|
|
if (
|
|
"Failed to get value of property" in line or
|
|
"=(unavailable)" in line or
|
|
line == "ANS_filename=" or
|
|
line == "ANS_length=" or
|
|
line == "ANS_path="
|
|
):
|
|
if "filename" in line:
|
|
self._getFilename()
|
|
elif "length" in line:
|
|
self._getLength()
|
|
elif "path" in line:
|
|
self._getFilepath()
|
|
return
|
|
match = self.RE_ANSWER.match(line)
|
|
if not match:
|
|
self._handleUnknownLine(line)
|
|
return
|
|
|
|
name, value = [m for m in match.groups() if m]
|
|
name = name.lower()
|
|
|
|
if name == self.POSITION_QUERY:
|
|
self._storePosition(float(value))
|
|
self._positionAsk.set()
|
|
elif name == "pause":
|
|
self._storePauseState(bool(value == 'yes'))
|
|
self._pausedAsk.set()
|
|
elif name == "length":
|
|
try:
|
|
self._duration = float(value)
|
|
except:
|
|
self._duration = 0
|
|
self._durationAsk.set()
|
|
elif name == "path":
|
|
self._filepath = value
|
|
self._pathAsk.set()
|
|
elif name == "filename":
|
|
self._filename = value
|
|
self._filenameAsk.set()
|
|
elif name == "exiting":
|
|
if value != 'Quit':
|
|
if self.quitReason is None:
|
|
self.quitReason = getMessage("media-player-error").format(value)
|
|
self.reactor.callFromThread(self._client.ui.showErrorMessage, self.quitReason, True)
|
|
self.drop()
|
|
|
|
def askForStatus(self):
|
|
self._positionAsk.clear()
|
|
self._pausedAsk.clear()
|
|
if not self._listener.isReadyForSend:
|
|
self._client.ui.showDebugMessage("mpv not ready for update")
|
|
return
|
|
|
|
self._getPausedAndPosition()
|
|
self._positionAsk.wait(constants.MPV_LOCK_WAIT_TIME)
|
|
self._pausedAsk.wait(constants.MPV_LOCK_WAIT_TIME)
|
|
self._client.updatePlayerStatus(
|
|
self._paused if self.fileLoaded else self._client.getGlobalPaused(), self.getCalculatedPosition())
|
|
|
|
def drop(self):
|
|
try:
|
|
self._listener.sendLine(['quit'])
|
|
except AttributeError as e:
|
|
self._client.ui.showDebugMessage("Could not send quit message: {}".format(str(e)))
|
|
self._takeLocksDown()
|
|
self.reactor.callFromThread(self._client.stop, False)
|
|
|
|
def _takeLocksDown(self):
|
|
try:
|
|
self._durationAsk.set()
|
|
self._filenameAsk.set()
|
|
self._pathAsk.set()
|
|
self._positionAsk.set()
|
|
self._pausedAsk.set()
|
|
except:
|
|
pass
|
|
|
|
|
|
def _getPausedAndPosition(self):
|
|
self._listener.sendLine(["script-message-to", "syncplayintf", "get_paused_and_position"])
|
|
|
|
def _getPaused(self):
|
|
self._getProperty('pause')
|
|
|
|
def _getPosition(self):
|
|
self._getProperty(self.POSITION_QUERY)
|
|
|
|
def _sanitizeText(self, text):
|
|
text = text.replace("\r", "")
|
|
text = text.replace("\n", "")
|
|
text = text.replace("\\\"", "<SYNCPLAY_QUOTE>")
|
|
text = text.replace("\"", "<SYNCPLAY_QUOTE>")
|
|
text = text.replace("%", "%%")
|
|
text = text.replace("\\", "\\\\")
|
|
text = text.replace("{", "\\\\{")
|
|
text = text.replace("}", "\\\\}")
|
|
text = text.replace("<SYNCPLAY_QUOTE>", "\\\"")
|
|
return text
|
|
|
|
def _quoteArg(self, arg):
|
|
arg = arg.replace('\\', '\\\\')
|
|
arg = arg.replace("'", "\\'")
|
|
arg = arg.replace('"', '\\"')
|
|
arg = arg.replace("\r", "")
|
|
arg = arg.replace("\n", "")
|
|
return '"{}"'.format(arg)
|
|
|
|
def _preparePlayer(self):
|
|
if self.delayedFilePath:
|
|
self.openFile(self.delayedFilePath)
|
|
self.setPaused(True)
|
|
self.reactor.callLater(0, self._client.initPlayer, self)
|
|
|
|
def _clearFileLoaded(self):
|
|
self.fileLoaded = False
|
|
self.lastLoadedTime = None
|
|
|
|
def _loadFile(self, filePath):
|
|
self._clearFileLoaded()
|
|
self._listener.sendLine(['loadfile', filePath], notReadyAfterThis=True)
|
|
|
|
def setFeatures(self, featureList):
|
|
self.sendMpvOptions()
|
|
|
|
def setPosition(self, value):
|
|
if value < constants.DO_NOT_RESET_POSITION_THRESHOLD and self._recentlyReset():
|
|
self._client.ui.showDebugMessage(
|
|
"Did not seek as recently reset and {} below 'do not reset position' threshold".format(value))
|
|
return
|
|
self._position = max(value, 0)
|
|
self._client.ui.showDebugMessage(
|
|
"Setting position to {}...".format(self._position))
|
|
self._setProperty(self.POSITION_QUERY, "{}".format(value))
|
|
time.sleep(0.03)
|
|
self.lastMPVPositionUpdate = time.time()
|
|
|
|
def openFile(self, filePath, resetPosition=False):
|
|
self._client.ui.showDebugMessage("openFile, resetPosition=={}".format(resetPosition))
|
|
if resetPosition:
|
|
self.lastResetTime = time.time()
|
|
if isURL(filePath):
|
|
self._client.ui.showDebugMessage("Setting additional lastResetTime due to stream")
|
|
self.lastResetTime += constants.STREAM_ADDITIONAL_IGNORE_TIME
|
|
self._loadFile(filePath)
|
|
if self._paused != self._client.getGlobalPaused():
|
|
self._client.ui.showDebugMessage("Want to set paused to {}".format(self._client.getGlobalPaused()))
|
|
else:
|
|
self._client.ui.showDebugMessage("Don't want to set paused to {}".format(self._client.getGlobalPaused()))
|
|
if resetPosition == False:
|
|
self._client.ui.showDebugMessage("OpenFile setting position to global position: {}".format(self._client.getGlobalPosition()))
|
|
self.setPosition(self._client.getGlobalPosition())
|
|
else:
|
|
self._storePosition(0)
|
|
# TO TRY: self._listener.setReadyToSend(False)
|
|
|
|
def sendMpvOptions(self):
|
|
options = []
|
|
for option in constants.MPV_SYNCPLAYINTF_OPTIONS_TO_SEND:
|
|
options.append("{}={}".format(option, self._client._config[option]))
|
|
for option in constants.MPV_SYNCPLAYINTF_CONSTANTS_TO_SEND:
|
|
options.append(option)
|
|
for option in constants.MPV_SYNCPLAYINTF_LANGUAGE_TO_SEND:
|
|
options.append("{}={}".format(option, getMessage(option)))
|
|
options.append("OscVisibilityChangeCompatible={}".format(constants.MPV_OSC_VISIBILITY_CHANGE_VERSION))
|
|
options_string = ", ".join(options)
|
|
self._listener.sendLine(["script-message-to", "syncplayintf", "set_syncplayintf_options", options_string])
|
|
self._setOSDPosition()
|
|
|
|
def _handleUnknownLine(self, line):
|
|
self.mpvErrorCheck(line)
|
|
|
|
if "<chat>" in line:
|
|
line = line.replace(constants.MPV_INPUT_BACKSLASH_SUBSTITUTE_CHARACTER, "\\")
|
|
self._listener.sendChat(line[6:-7])
|
|
|
|
if "<paused=" in line and ", pos=" in line:
|
|
update_string = line.replace(">", "<").replace("=", "<").replace(", ", "<").split("<")
|
|
paused_update = update_string[2]
|
|
position_update = update_string[4]
|
|
if paused_update == "nil":
|
|
self._storePauseState(float(self._client.getGlobalPaused()))
|
|
else:
|
|
self._storePauseState(bool(paused_update == 'true'))
|
|
self._pausedAsk.set()
|
|
if position_update == "nil":
|
|
self._storePosition(float(self._client.getGlobalPosition()))
|
|
else:
|
|
self._storePosition(float(position_update))
|
|
self._positionAsk.set()
|
|
#self._client.ui.showDebugMessage("{} = {} / {}".format(update_string, paused_update, position_update))
|
|
|
|
if "<get_syncplayintf_options>" in line:
|
|
self.sendMpvOptions()
|
|
|
|
if line == "<SyncplayUpdateFile>" or "Playing:" in line:
|
|
self._client.ui.showDebugMessage("Not ready to send due to <SyncplayUpdateFile>")
|
|
self._listener.setReadyToSend(False)
|
|
self._clearFileLoaded()
|
|
|
|
elif line == "</SyncplayUpdateFile>":
|
|
self._onFileUpdate()
|
|
self._listener.setReadyToSend(True)
|
|
self._client.ui.showDebugMessage("Ready to send due to </SyncplayUpdateFile>")
|
|
|
|
elif "Failed" in line or "failed" in line or "No video or audio streams selected" in line or "error" in line:
|
|
self._client.ui.showDebugMessage("Not ready to send due to error")
|
|
self._listener.setReadyToSend(True)
|
|
|
|
def _setOSDPosition(self):
|
|
if (
|
|
self._client._config['chatMoveOSD'] and (
|
|
self._client._config['chatOutputEnabled'] or (
|
|
self._client._config['chatInputEnabled'] and
|
|
self._client._config['chatInputPosition'] == constants.INPUT_POSITION_TOP
|
|
)
|
|
)
|
|
):
|
|
self._setProperty("osd-align-y", "bottom")
|
|
self._setProperty("osd-margin-y", int(self._client._config['chatOSDMargin']))
|
|
|
|
def _recentlyReset(self):
|
|
if not self.lastResetTime:
|
|
return False
|
|
elif time.time() < self.lastResetTime + constants.MPV_NEWFILE_IGNORE_TIME:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def _onFileUpdate(self):
|
|
self._client.ui.showDebugMessage("File update")
|
|
self.fileLoaded = True
|
|
self.lastLoadedTime = time.time()
|
|
self.reactor.callFromThread(self._client.updateFile, self._filename, self._duration, self._filepath)
|
|
if not (self._recentlyReset()):
|
|
self._client.ui.showDebugMessage("onFileUpdate setting position to global position: {}".format(self._client.getGlobalPosition()))
|
|
self.reactor.callFromThread(self.setPosition, self._client.getGlobalPosition())
|
|
if self._paused != self._client.getGlobalPaused():
|
|
self._client.ui.showDebugMessage("onFileUpdate setting position to global paused: {}".format(self._client.getGlobalPaused()))
|
|
self.reactor.callFromThread(self._client.getGlobalPaused)
|
|
|
|
def _fileIsLoaded(self, ignoreDelay=False):
|
|
if ignoreDelay:
|
|
self._client.ui.showDebugMessage("Ignoring _fileIsLoaded MPV_NEWFILE delay")
|
|
return bool(self.fileLoaded)
|
|
|
|
return (
|
|
self.fileLoaded and self.lastLoadedTime is not None and
|
|
time.time() > (self.lastLoadedTime + constants.MPV_NEWFILE_IGNORE_TIME)
|
|
)
|
|
|
|
|
|
def __init__(self, client, playerPath, filePath, args):
|
|
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
|
|
self._filename = None
|
|
self._filepath = None
|
|
self.quitReason = None
|
|
self.lastLoadedTime = None
|
|
self.fileLoaded = False
|
|
self.delayedFilePath = None
|
|
|
|
def _create_listener(self, playerPath, filePath, args):
|
|
try:
|
|
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"))
|
|
self.drop()
|
|
return
|
|
except AttributeError as e:
|
|
self._client.ui.showErrorMessage("Could not load mpv: " + str(e))
|
|
return
|
|
self._listener.setDaemon(True)
|
|
self._listener.start()
|
|
|
|
self._durationAsk = threading.Event()
|
|
self._filenameAsk = threading.Event()
|
|
self._pathAsk = threading.Event()
|
|
|
|
self._positionAsk = threading.Event()
|
|
self._pausedAsk = threading.Event()
|
|
|
|
self._preparePlayer()
|
|
|
|
def _fileUpdateClearEvents(self):
|
|
self._durationAsk.clear()
|
|
self._filenameAsk.clear()
|
|
self._pathAsk.clear()
|
|
|
|
def _fileUpdateWaitEvents(self):
|
|
self._durationAsk.wait()
|
|
self._filenameAsk.wait()
|
|
self._pathAsk.wait()
|
|
|
|
def mpv_log_handler(self, level, prefix, text):
|
|
self.lineReceived(text)
|
|
|
|
class __Listener(threading.Thread):
|
|
def __init__(self, playerController, playerIPCHandler, playerPath, filePath, args):
|
|
self.playerIPCHandler = playerIPCHandler
|
|
self.playerPath = playerPath
|
|
self.mpv_arguments = playerController.getStartupArgs(args)
|
|
self.mpv_running = True
|
|
self.sendQueue = []
|
|
self.readyToSend = True
|
|
self.lastSendTime = None
|
|
self.lastNotReadyTime = None
|
|
self.__playerController = playerController
|
|
if not self.__playerController._client._config["chatOutputEnabled"]:
|
|
self.__playerController.alertOSDSupported = False
|
|
self.__playerController.chatOSDSupported = False
|
|
if self.__playerController.getPlayerPathErrors(playerPath, filePath):
|
|
raise ValueError()
|
|
if filePath and '://' not in filePath:
|
|
if not os.path.isfile(filePath) and 'PWD' in os.environ:
|
|
filePath = os.environ['PWD'] + os.path.sep + filePath
|
|
filePath = os.path.realpath(filePath)
|
|
|
|
if filePath:
|
|
self.__playerController.delayedFilePath = filePath
|
|
|
|
# At least mpv may output escape sequences which result in syncplay
|
|
# trying to parse something like
|
|
# "\x1b[?1l\x1b>ANS_filename=blah.mkv". Work around this by
|
|
# unsetting TERM.
|
|
env = os.environ.copy()
|
|
if 'TERM' in env:
|
|
del env['TERM']
|
|
# On macOS, youtube-dl requires system python to run. Set the environment
|
|
# to allow that version of python to be executed in the mpv subprocess.
|
|
if isMacOS():
|
|
try:
|
|
pythonLibs = subprocess.check_output(['/usr/bin/python', '-E', '-c',
|
|
'import sys; print(sys.path)'],
|
|
text=True, env=dict())
|
|
pythonLibs = ast.literal_eval(pythonLibs)
|
|
pythonPath = ':'.join(pythonLibs[1:])
|
|
except:
|
|
pythonPath = None
|
|
if pythonPath is not None:
|
|
env['PATH'] = '/usr/bin:/usr/local/bin'
|
|
env['PYTHONPATH'] = pythonPath
|
|
try:
|
|
socket = self.mpv_arguments.get('input-ipc-server')
|
|
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, env=env, **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)
|
|
self.__playerController.drop()
|
|
self.__process = self.mpvpipe
|
|
#self.mpvpipe.show_text("HELLO WORLD!", 1000)
|
|
threading.Thread.__init__(self, name="MPV Listener")
|
|
|
|
def __getCwd(self, filePath, env):
|
|
if not filePath:
|
|
return None
|
|
if os.path.isfile(filePath):
|
|
cwd = os.path.dirname(filePath)
|
|
elif 'HOME' in env:
|
|
cwd = env['HOME']
|
|
elif 'APPDATA' in env:
|
|
cwd = env['APPDATA']
|
|
else:
|
|
cwd = None
|
|
return cwd
|
|
|
|
def run(self):
|
|
pass
|
|
|
|
def sendChat(self, message):
|
|
if message:
|
|
if message[:1] == "/" and message != "/":
|
|
command = message[1:]
|
|
if command and command[:1] == "/":
|
|
message = message[1:]
|
|
else:
|
|
self.__playerController.reactor.callFromThread(
|
|
self.__playerController._client.ui.executeCommand, command)
|
|
return
|
|
self.__playerController.reactor.callFromThread(self.__playerController._client.sendChat, message)
|
|
|
|
def isReadyForSend(self):
|
|
self.checkForReadinessOverride()
|
|
return self.readyToSend
|
|
|
|
def setReadyToSend(self, newReadyState):
|
|
oldState = self.readyToSend
|
|
self.readyToSend = newReadyState
|
|
self.lastNotReadyTime = time.time() if newReadyState == False else None
|
|
if self.readyToSend == True:
|
|
self.__playerController._client.ui.showDebugMessage("<mpv> Ready to send: True")
|
|
else:
|
|
self.__playerController._client.ui.showDebugMessage("<mpv> Ready to send: False")
|
|
if self.readyToSend == True and oldState == False:
|
|
self.processSendQueue()
|
|
|
|
def checkForReadinessOverride(self):
|
|
if self.lastNotReadyTime and time.time() - self.lastNotReadyTime > constants.MPV_MAX_NEWFILE_COOLDOWN_TIME:
|
|
self.setReadyToSend(True)
|
|
|
|
def sendLine(self, line, notReadyAfterThis=None):
|
|
self.checkForReadinessOverride()
|
|
try:
|
|
if self.sendQueue:
|
|
if constants.MPV_SUPERSEDE_IF_DUPLICATE_COMMANDS:
|
|
for command in constants.MPV_SUPERSEDE_IF_DUPLICATE_COMMANDS:
|
|
line_command = " ".join(line)
|
|
answer = line_command.startswith(command)
|
|
#self.__playerController._client.ui.showDebugMessage("Does line_command {} start with {}? {}".format(line_command, command, answer))
|
|
if line_command.startswith(command):
|
|
for itemID, deletionCandidate in enumerate(self.sendQueue):
|
|
if " ".join(deletionCandidate).startswith(command):
|
|
self.__playerController._client.ui.showDebugMessage(
|
|
"<mpv> Remove duplicate (supersede): {}".format(self.sendQueue[itemID]))
|
|
try:
|
|
self.sendQueue.remove(self.sendQueue[itemID])
|
|
except UnicodeWarning:
|
|
self.__playerController._client.ui.showDebugMessage(
|
|
"<mpv> Unicode mismatch occurred when trying to remove duplicate")
|
|
# TODO: Prevent this from being triggered
|
|
pass
|
|
break
|
|
break
|
|
if constants.MPV_REMOVE_BOTH_IF_DUPLICATE_COMMANDS:
|
|
for command in constants.MPV_REMOVE_BOTH_IF_DUPLICATE_COMMANDS:
|
|
if line == command:
|
|
for itemID, deletionCandidate in enumerate(self.sendQueue):
|
|
if deletionCandidate == command:
|
|
self.__playerController._client.ui.showDebugMessage(
|
|
"<mpv> Remove duplicate (delete both): {}".format(self.sendQueue[itemID]))
|
|
self.__playerController._client.ui.showDebugMessage(self.sendQueue[itemID])
|
|
return
|
|
except Exception as e:
|
|
self.__playerController._client.ui.showDebugMessage("<mpv> Problem removing duplicates, etc: {}".format(e))
|
|
self.sendQueue.append(line)
|
|
self.processSendQueue()
|
|
if notReadyAfterThis:
|
|
self.setReadyToSend(False)
|
|
|
|
def processSendQueue(self):
|
|
while self.sendQueue and self.readyToSend:
|
|
if self.lastSendTime and time.time() - self.lastSendTime < constants.MPV_SENDMESSAGE_COOLDOWN_TIME:
|
|
self.__playerController._client.ui.showDebugMessage(
|
|
"<mpv> Throttling message send, so sleeping for {}".format(
|
|
constants.MPV_SENDMESSAGE_COOLDOWN_TIME))
|
|
time.sleep(constants.MPV_SENDMESSAGE_COOLDOWN_TIME)
|
|
try:
|
|
lineToSend = self.sendQueue.pop()
|
|
if lineToSend:
|
|
self.lastSendTime = time.time()
|
|
self.actuallySendLine(lineToSend)
|
|
except IndexError:
|
|
pass
|
|
|
|
def stop_client(self):
|
|
self.readyToSend = False
|
|
try:
|
|
self.mpvpipe.terminate()
|
|
except: #When mpv is already closed
|
|
pass
|
|
self.__playerController._takeLocksDown()
|
|
self.__playerController.reactor.callFromThread(self.__playerController._client.stop, False)
|
|
|
|
def actuallySendLine(self, line):
|
|
try:
|
|
self.__playerController._client.ui.showDebugMessage("player >> {}".format(line))
|
|
try:
|
|
self.mpvpipe.command(*line)
|
|
except Exception as e:
|
|
self.__playerController._client.ui.showDebugMessage("CANNOT SEND {} DUE TO {}".format(line, e))
|
|
self.stop_client()
|
|
except IOError:
|
|
pass
|