Merge 094a204d9e2d240dd601447862b8d2fcab64b469 into dd6edb7a61f5e60976766ed784ce3b6488b5c603

This commit is contained in:
Peter Marheine 2016-01-21 18:22:33 +00:00
commit 4849b2ba65
10 changed files with 93 additions and 27 deletions

View File

@ -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:

View File

@ -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)

View File

@ -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",

View File

@ -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

View File

@ -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):

View File

@ -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))

View File

@ -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

View File

@ -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:

View File

@ -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")

View File

@ -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)