Merge 094a204d9e2d240dd601447862b8d2fcab64b469 into dd6edb7a61f5e60976766ed784ce3b6488b5c603
This commit is contained in:
commit
4849b2ba65
@ -110,6 +110,8 @@ class SyncplayClient(object):
|
||||
self.autoPlay = False
|
||||
self.autoPlayThreshold = None
|
||||
|
||||
self._lastPlayerCommand = time.time()
|
||||
|
||||
self.autoplayTimer = task.LoopingCall(self.autoplayCountdown)
|
||||
self.autoplayTimeLeft = constants.AUTOPLAY_DELAY
|
||||
|
||||
@ -137,6 +139,16 @@ class SyncplayClient(object):
|
||||
constants.OSD_WARNING_MESSAGE_DURATION = constants.NO_SECONDARY_OSD_WARNING_DURATION
|
||||
self.scheduleAskPlayer()
|
||||
|
||||
def _playerRequest(self, f, *args, **kwargs):
|
||||
"""Send a request with cookie to the player."""
|
||||
kwargs['cookie'] = time.time()
|
||||
f(*args, **kwargs)
|
||||
|
||||
def _playerCommand(self, f, *args, **kwargs):
|
||||
"""Send a command to the player, affecting cookie freshness."""
|
||||
self._lastPlayerCommand = time.time()
|
||||
f(*args, **kwargs)
|
||||
|
||||
def scheduleAskPlayer(self, when=constants.PLAYER_ASK_DELAY):
|
||||
self._askPlayerTimer = task.LoopingCall(self.askPlayer)
|
||||
self._askPlayerTimer.start(when)
|
||||
@ -145,7 +157,7 @@ class SyncplayClient(object):
|
||||
if not self._running:
|
||||
return
|
||||
if self._player:
|
||||
self._player.askForStatus()
|
||||
self._playerRequest(self._player.askForStatus)
|
||||
self.checkIfConnected()
|
||||
|
||||
def checkIfConnected(self):
|
||||
@ -163,14 +175,24 @@ class SyncplayClient(object):
|
||||
seeked = _playerDiff > constants.SEEK_THRESHOLD and _globalDiff > constants.SEEK_THRESHOLD
|
||||
return pauseChange, seeked
|
||||
|
||||
def updatePlayerStatus(self, paused, position):
|
||||
def _ignoringPlayerStatus(self, cookie=None):
|
||||
if cookie is None:
|
||||
cookie = time.time()
|
||||
return cookie < self._lastPlayerCommand + self._config['playerCommandDelay']
|
||||
|
||||
def updatePlayerStatus(self, paused, position, cookie=None):
|
||||
# Ignore status report if the cookie is stale
|
||||
if self._ignoringPlayerStatus(cookie):
|
||||
self.ui.showDebugMessage('Ignoring stale player status with cookie {}'.format(cookie))
|
||||
return
|
||||
|
||||
position -= self.getUserOffset()
|
||||
pauseChange, seeked = self._determinePlayerStateChange(paused, position)
|
||||
self._playerPosition = position
|
||||
self._playerPaused = paused
|
||||
if pauseChange and utils.meetsMinVersion(self.serverVersion, constants.USER_READY_MIN_VERSION):
|
||||
if not self.userlist.currentUser.canControl():
|
||||
self._player.setPaused(self._globalPaused)
|
||||
self._playerCommand(self._player.setPaused, self._globalPaused)
|
||||
self.toggleReady(manuallyInitiated=True)
|
||||
self._playerPaused = self._globalPaused
|
||||
pauseChange = False
|
||||
@ -180,7 +202,7 @@ class SyncplayClient(object):
|
||||
self.ui.showMessage(getMessage("set-as-not-ready-notification"))
|
||||
elif not paused and not self.instaplayConditionsMet():
|
||||
paused = True
|
||||
self._player.setPaused(paused)
|
||||
self._playerCommand(self._player.setPaused, paused)
|
||||
self._playerPaused = paused
|
||||
self.changeReadyState(True, manuallyInitiated=True)
|
||||
pauseChange = False
|
||||
@ -213,7 +235,7 @@ class SyncplayClient(object):
|
||||
def _initPlayerState(self, position, paused):
|
||||
if self.userlist.currentUser.file:
|
||||
self.setPosition(position)
|
||||
self._player.setPaused(paused)
|
||||
self._playerCommand(self._player.setPaused, paused)
|
||||
madeChangeOnPlayer = True
|
||||
return madeChangeOnPlayer
|
||||
|
||||
@ -241,16 +263,21 @@ class SyncplayClient(object):
|
||||
|
||||
def _serverUnpaused(self, setBy):
|
||||
hideFromOSD = not constants.SHOW_SAME_ROOM_OSD
|
||||
self._player.setPaused(False)
|
||||
# In high-player-latency situations we might report our state back to
|
||||
# the server before any player status is accepted as fresh. Override
|
||||
# the locally-stored playback state.
|
||||
self._playerPaused = False
|
||||
self._playerCommand(self._player.setPaused, False)
|
||||
madeChangeOnPlayer = True
|
||||
self.ui.showMessage(getMessage("unpause-notification").format(setBy), hideFromOSD)
|
||||
return madeChangeOnPlayer
|
||||
|
||||
def _serverPaused(self, setBy):
|
||||
hideFromOSD = not constants.SHOW_SAME_ROOM_OSD
|
||||
self._playerPaused = True
|
||||
if constants.SYNC_ON_PAUSE and self.getUsername() <> setBy:
|
||||
self.setPosition(self.getGlobalPosition())
|
||||
self._player.setPaused(True)
|
||||
self._playerCommand(self._player.setPaused, True)
|
||||
madeChangeOnPlayer = True
|
||||
if (self.lastLeftTime < time.time() - constants.OSD_DURATION) or (hideFromOSD == True):
|
||||
self.ui.showMessage(getMessage("pause-notification").format(setBy), hideFromOSD)
|
||||
@ -493,18 +520,19 @@ class SyncplayClient(object):
|
||||
def setPosition(self, position):
|
||||
if self._lastPlayerUpdate:
|
||||
self._lastPlayerUpdate = time.time()
|
||||
self._playerPosition = position
|
||||
position += self.getUserOffset()
|
||||
if self._player and self.userlist.currentUser.file:
|
||||
if position < 0:
|
||||
position = 0
|
||||
self._protocol.sendState(self.getPlayerPosition(), self.getPlayerPaused(), True, None, True)
|
||||
self._player.setPosition(position)
|
||||
self._playerCommand(self._player.setPosition, position)
|
||||
|
||||
def setPaused(self, paused):
|
||||
if self._player and self.userlist.currentUser.file:
|
||||
if self._lastPlayerUpdate and not paused:
|
||||
self._lastPlayerUpdate = time.time()
|
||||
self._player.setPaused(paused)
|
||||
self._playerCommand(self._player.setPaused, paused)
|
||||
|
||||
def start(self, host, port):
|
||||
if self._running:
|
||||
|
||||
@ -49,6 +49,7 @@ SERVER_STATE_INTERVAL = 1
|
||||
WARNING_OSD_MESSAGES_LOOP_INTERVAL = 1
|
||||
AUTOPLAY_DELAY = 3.0
|
||||
SYNC_ON_PAUSE = True # Client seek to global position - subtitles may disappear on some media players
|
||||
DEFAULT_PLAYER_COMMAND_DELAY = 0.05
|
||||
|
||||
# Options for the File Switch feature:
|
||||
FOLDER_SEARCH_TIMEOUT = 60.0 # Secs - How long to wait until searches in folder to update cache are aborted (may be longer than this if hard drive needs to spin up)
|
||||
|
||||
@ -194,6 +194,9 @@ en = {
|
||||
"forceguiprompt-label" : "Don't always show the Syncplay configuration window", # (Inverted)
|
||||
"nostore-label" : "Don't store this configuration", # (Inverted)
|
||||
"showosd-label" : "Enable OSD Messages",
|
||||
"playercommanddelay-title" : "Player latency compensation",
|
||||
"playercommanddelay-label" : "Seconds to ignore player status after commands",
|
||||
"playercommanddelay-tooltip" : "Larger values are less likely to spuriously (un)pause but tend to sync less accurately.",
|
||||
|
||||
"showosdwarnings-label" : "Include warnings (e.g. when files are different, users not ready)",
|
||||
"showsameroomosd-label" : "Include events in your room",
|
||||
|
||||
@ -6,7 +6,7 @@ class BasePlayer(object):
|
||||
execute updatePlayerStatus(paused, position) on client
|
||||
Given the arguments: boolean paused and float position in seconds
|
||||
'''
|
||||
def askForStatus(self):
|
||||
def askForStatus(self, cookie=None):
|
||||
raise NotImplementedError()
|
||||
|
||||
'''
|
||||
@ -121,4 +121,4 @@ class DummyPlayer(BasePlayer):
|
||||
|
||||
@staticmethod
|
||||
def getPlayerPathErrors(playerPath, filePath):
|
||||
return None
|
||||
return None
|
||||
|
||||
@ -420,20 +420,22 @@ class MPCHCAPIPlayer(BasePlayer):
|
||||
return self._mpcApi.lastFilePosition
|
||||
|
||||
@retry(MpcHcApi.PlayerNotReadyException, constants.MPC_MAX_RETRIES, constants.MPC_RETRY_WAIT_TIME, 1)
|
||||
def askForStatus(self):
|
||||
def askForStatus(self, cookie=None):
|
||||
if self._mpcApi.filePlaying and self.__preventAsking.wait(0) and self.__fileUpdate.acquire(0):
|
||||
self.__fileUpdate.release()
|
||||
position = self.__getPosition()
|
||||
paused = self._mpcApi.isPaused()
|
||||
position = float(position)
|
||||
if self.__preventAsking.wait(0) and self.__fileUpdate.acquire(0):
|
||||
self.__client.updatePlayerStatus(paused, position)
|
||||
self.__client.updatePlayerStatus(paused, position, cookie=cookie)
|
||||
self.__fileUpdate.release()
|
||||
return
|
||||
self.__echoGlobalStatus()
|
||||
self.__echoGlobalStatus(cookie)
|
||||
|
||||
def __echoGlobalStatus(self):
|
||||
self.__client.updatePlayerStatus(self.__client.getGlobalPaused(), self.__client.getGlobalPosition())
|
||||
def __echoGlobalStatus(self, cookie):
|
||||
self.__client.updatePlayerStatus(self.__client.getGlobalPaused(),
|
||||
self.__client.getGlobalPosition(),
|
||||
cookie=cookie)
|
||||
|
||||
def __forcePause(self):
|
||||
for _ in xrange(constants.MPC_MAX_RETRIES):
|
||||
|
||||
@ -72,14 +72,14 @@ class MplayerPlayer(BasePlayer):
|
||||
self.reactor.callLater(0, self._client.initPlayer, self)
|
||||
self._onFileUpdate()
|
||||
|
||||
def askForStatus(self):
|
||||
def askForStatus(self, cookie=None):
|
||||
self._positionAsk.clear()
|
||||
self._pausedAsk.clear()
|
||||
self._getPaused()
|
||||
self._getPosition()
|
||||
self._positionAsk.wait()
|
||||
self._pausedAsk.wait()
|
||||
self._client.updatePlayerStatus(self._paused, self._position)
|
||||
self._client.updatePlayerStatus(self._paused, self._position, cookie=cookie)
|
||||
|
||||
def _setProperty(self, property_, value):
|
||||
self._listener.sendLine("set_property {} {}".format(property_, value))
|
||||
|
||||
@ -154,14 +154,16 @@ class NewMpvPlayer(OldMpvPlayer):
|
||||
else:
|
||||
self._paused = self._client.getGlobalPaused()
|
||||
|
||||
def askForStatus(self):
|
||||
def askForStatus(self, cookie=None):
|
||||
self._positionAsk.clear()
|
||||
self._pausedAsk.clear()
|
||||
self._getPaused()
|
||||
self._getPosition()
|
||||
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())
|
||||
self._client.updatePlayerStatus(self._paused if self.fileLoaded else self._client.getGlobalPaused(),
|
||||
self.getCalculatedPosition(),
|
||||
cookie=cookie)
|
||||
|
||||
def _preparePlayer(self):
|
||||
if self.delayedFilePath:
|
||||
@ -219,4 +221,4 @@ class NewMpvPlayer(OldMpvPlayer):
|
||||
if self.fileLoaded == True and self.lastLoadedTime != None and time.time() > (self.lastLoadedTime + constants.MPV_NEWFILE_IGNORE_TIME):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return False
|
||||
|
||||
@ -84,16 +84,20 @@ class VlcPlayer(BasePlayer):
|
||||
self.setPaused(self._client.getGlobalPaused())
|
||||
self.setPosition(self._client.getGlobalPosition())
|
||||
|
||||
def askForStatus(self):
|
||||
def askForStatus(self, cookie=None):
|
||||
self._filechanged = False
|
||||
self._positionAsk.clear()
|
||||
self._pausedAsk.clear()
|
||||
self._listener.sendLine(".")
|
||||
if self._filename and not self._filechanged:
|
||||
self._positionAsk.wait(constants.PLAYER_ASK_DELAY)
|
||||
self._client.updatePlayerStatus(self._paused, self.getCalculatedPosition())
|
||||
self._client.updatePlayerStatus(self._paused,
|
||||
self.getCalculatedPosition(),
|
||||
cookie=cookie)
|
||||
else:
|
||||
self._client.updatePlayerStatus(self._client.getGlobalPaused(), self._client.getGlobalPosition())
|
||||
self._client.updatePlayerStatus(self._client.getGlobalPaused(),
|
||||
self._client.getGlobalPosition(),
|
||||
cookie=cookie)
|
||||
|
||||
def getCalculatedPosition(self):
|
||||
if self._lastVLCPositionUpdate is None:
|
||||
|
||||
@ -63,7 +63,8 @@ class ConfigurationGetter(object):
|
||||
"showSameRoomOSD" : True,
|
||||
"showNonControllerOSD" : False,
|
||||
"showContactInfo" : True,
|
||||
"showDurationNotification" : True
|
||||
"showDurationNotification" : True,
|
||||
"playerCommandDelay": constants.DEFAULT_PLAYER_COMMAND_DELAY
|
||||
}
|
||||
|
||||
self._defaultConfig = self._config.copy()
|
||||
@ -117,11 +118,21 @@ class ConfigurationGetter(object):
|
||||
"rewindThreshold",
|
||||
"fastforwardThreshold",
|
||||
"autoplayMinUsers",
|
||||
"playerCommandDelay",
|
||||
]
|
||||
|
||||
self._iniStructure = {
|
||||
"server_data": ["host", "port", "password"],
|
||||
"client_settings": ["name", "room", "playerPath", "perPlayerArguments", "slowdownThreshold", "rewindThreshold", "fastforwardThreshold", "slowOnDesync", "rewindOnDesync", "fastforwardOnDesync", "dontSlowDownWithMe", "forceGuiPrompt", "filenamePrivacyMode", "filesizePrivacyMode", "unpauseAction", "pauseOnLeave", "readyAtStart", "autoplayMinUsers", "autoplayInitialState", "mediaSearchDirectories"],
|
||||
"client_settings": ["name", "room", "playerPath",
|
||||
"perPlayerArguments", "slowdownThreshold",
|
||||
"rewindThreshold", "fastforwardThreshold",
|
||||
"slowOnDesync", "rewindOnDesync",
|
||||
"fastforwardOnDesync", "dontSlowDownWithMe",
|
||||
"forceGuiPrompt", "filenamePrivacyMode",
|
||||
"filesizePrivacyMode", "unpauseAction",
|
||||
"pauseOnLeave", "readyAtStart", "autoplayMinUsers",
|
||||
"autoplayInitialState", "mediaSearchDirectories",
|
||||
"playerCommandDelay"],
|
||||
"gui": ["showOSD", "showOSDWarnings", "showSlowdownOSD", "showDifferentRoomOSD", "showSameRoomOSD", "showNonControllerOSD", "showDurationNotification"],
|
||||
"general": ["language", "checkForUpdatesAutomatically", "lastCheckedForUpdates"]
|
||||
}
|
||||
@ -425,4 +436,4 @@ class SafeConfigParserUnicode(SafeConfigParser):
|
||||
if (value is not None) or (self._optcre == self.OPTCRE):
|
||||
key = " = ".join((key, unicode(value).replace('\n', '\n\t')))
|
||||
fp.write("%s\n" % key)
|
||||
fp.write("\n")
|
||||
fp.write("\n")
|
||||
|
||||
@ -449,6 +449,8 @@ class ConfigDialog(QtGui.QDialog):
|
||||
widget.setChecked(True)
|
||||
elif isinstance(widget, QLineEdit):
|
||||
widget.setText(self.config[valueName])
|
||||
elif isinstance(widget, QDoubleSpinBox):
|
||||
widget.setValue(self.config[valueName])
|
||||
|
||||
def saveValues(self, widget):
|
||||
valueName = str(widget.objectName())
|
||||
@ -471,6 +473,8 @@ class ConfigDialog(QtGui.QDialog):
|
||||
self.config[radioName] = radioValue
|
||||
elif isinstance(widget, QLineEdit):
|
||||
self.config[valueName] = widget.text()
|
||||
elif isinstance(widget, QDoubleSpinBox):
|
||||
self.config[valueName] = widget.value()
|
||||
|
||||
def connectChildren(self, widget):
|
||||
widgetName = str(widget.objectName())
|
||||
@ -766,6 +770,10 @@ class ConfigDialog(QtGui.QDialog):
|
||||
self.rewindCheckbox.setObjectName("rewindOnDesync")
|
||||
self.fastforwardCheckbox = QCheckBox(getMessage("fastforwardondesync-label"))
|
||||
self.fastforwardCheckbox.setObjectName("fastforwardOnDesync")
|
||||
self.commandDelaySpinbox = QDoubleSpinBox()
|
||||
self.commandDelaySpinbox.setObjectName("playerCommandDelay")
|
||||
self.commandDelaySpinbox.setMaximum(10)
|
||||
self.commandDelaySpinbox.setSingleStep(.1)
|
||||
|
||||
self.desyncSettingsLayout = QtGui.QGridLayout()
|
||||
self.desyncSettingsLayout.setSpacing(2)
|
||||
@ -794,10 +802,17 @@ class ConfigDialog(QtGui.QDialog):
|
||||
self.othersyncSettingsLayout.setAlignment(Qt.AlignLeft)
|
||||
self.othersyncSettingsLayout.addWidget(self.fastforwardCheckbox, 3, 0,1,2, Qt.AlignLeft)
|
||||
|
||||
self.playerLatencyGroup = QtGui.QGroupBox(getMessage("playercommanddelay-title"))
|
||||
self.playerLatencyLayout = QtGui.QHBoxLayout()
|
||||
self.playerLatencyGroup.setLayout(self.playerLatencyLayout)
|
||||
self.playerLatencyLayout.addWidget(self.commandDelaySpinbox)
|
||||
self.playerLatencyLayout.addWidget(QLabel(getMessage("playercommanddelay-label")))
|
||||
|
||||
self.othersyncSettingsGroup.setLayout(self.othersyncSettingsLayout)
|
||||
self.othersyncSettingsGroup.setMaximumHeight(self.othersyncSettingsGroup.minimumSizeHint().height())
|
||||
self.syncSettingsLayout.addWidget(self.othersyncSettingsGroup)
|
||||
self.syncSettingsLayout.addWidget(self.desyncSettingsGroup)
|
||||
self.syncSettingsLayout.addWidget(self.playerLatencyGroup)
|
||||
self.syncSettingsFrame.setLayout(self.syncSettingsLayout)
|
||||
self.desyncSettingsGroup.setMaximumHeight(self.desyncSettingsGroup.minimumSizeHint().height())
|
||||
self.syncSettingsLayout.setAlignment(Qt.AlignTop)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user