Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 32 additions & 8 deletions syncplay/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ def __init__(self, playerClass, ui, config):
self._lastGlobalUpdate = None
self._globalPosition = 0.0
self._globalPaused = 0.0
self._globalSpeed = constants.DEFAULT_PLAYBACK_SPEED
self._userOffset = 0.0
self._speedChanged = False
self.behindFirstDetected = None
Expand Down Expand Up @@ -400,18 +401,18 @@ def _slowDownToCoverTimeDifference(self, diff, setBy):
if self.getUsername() == setBy:
self.ui.showDebugMessage("Caught attempt to slow down due to time difference with self")
else:
self._player.setSpeed(constants.SLOWDOWN_RATE)
self._player.setSpeed(self._globalSpeed * constants.SLOWDOWN_RATE)
self._speedChanged = True
self.ui.showMessage(getMessage("slowdown-notification").format(setBy), hideFromOSD)
madeChangeOnPlayer = True
elif self._speedChanged and diff < constants.SLOWDOWN_RESET_THRESHOLD:
self._player.setSpeed(1.00)
self._player.setSpeed(self._globalSpeed)
self._speedChanged = False
self.ui.showMessage(getMessage("revert-notification"), hideFromOSD)
madeChangeOnPlayer = True
return madeChangeOnPlayer

def _changePlayerStateAccordingToGlobalState(self, position, paused, doSeek, setBy):
def _changePlayerStateAccordingToGlobalState(self, position, paused, doSeek, setBy, speed=None):
madeChangeOnPlayer = False
pauseChanged = paused != self.getGlobalPaused() or paused != self.getPlayerPaused()
diff = self.getPlayerPosition() - position
Expand All @@ -420,6 +421,14 @@ def _changePlayerStateAccordingToGlobalState(self, position, paused, doSeek, set
self._globalPaused = paused
self._globalPosition = position
self._lastGlobalUpdate = time.time()
if speed is not None and abs(speed - self._globalSpeed) > constants.SPEED_TOLERANCE:
self._globalSpeed = speed
if self._player.speedSupported and not self._speedChanged:
self._player.setSpeed(speed)
madeChangeOnPlayer = True
hideFromOSD = not constants.SHOW_SAME_ROOM_OSD
self.ui.showMessage(getMessage("speed-change-notification").format(setBy, speed), hideFromOSD)
self.ui.updateSpeed(speed)
if doSeek:
madeChangeOnPlayer = self._serverSeeked(position, setBy)
if diff > self._config['rewindThreshold'] and not doSeek and not self._config['rewindOnDesync'] == False:
Expand Down Expand Up @@ -449,15 +458,15 @@ def _executePlaystateHooks(self, position, paused, doSeek, setBy, messageAge):
self._warnings.checkWarnings()
self.userlist.roomStateConfirmed()

def updateGlobalState(self, position, paused, doSeek, setBy, messageAge):
def updateGlobalState(self, position, paused, doSeek, setBy, messageAge, speed=None):
if self.__getUserlistOnLogon:
self.__getUserlistOnLogon = False
self.getUserList()
madeChangeOnPlayer = False
if not paused:
position += messageAge
if self._player:
madeChangeOnPlayer = self._changePlayerStateAccordingToGlobalState(position, paused, doSeek, setBy)
madeChangeOnPlayer = self._changePlayerStateAccordingToGlobalState(position, paused, doSeek, setBy, speed)
if madeChangeOnPlayer:
self.askPlayer()
self._executePlaystateHooks(position, paused, doSeek, setBy, messageAge)
Expand Down Expand Up @@ -489,7 +498,7 @@ def getPlayerPosition(self):
position = self._playerPosition
if not self._playerPaused:
diff = time.time() - self._lastPlayerUpdate
position += diff
position += diff * self._globalSpeed
return position

def getStoredPlayerPosition(self):
Expand All @@ -508,14 +517,25 @@ def getGlobalPosition(self):
return 0.0
position = self._globalPosition
if not self._globalPaused:
position += time.time() - self._lastGlobalUpdate
position += (time.time() - self._lastGlobalUpdate) * self._globalSpeed
return position

def getGlobalPaused(self):
if not self._lastGlobalUpdate:
return True
return self._globalPaused

def getGlobalSpeed(self):
return self._globalSpeed

def setSpeed(self, speed):
self._globalSpeed = speed
self.ui.updateSpeed(speed)
if self._protocol and self._protocol.logged:
if not self.serverFeatures.get("speedSync"):
return
self._protocol.sendState(self.getPlayerPosition(), self.getPlayerPaused(), False, None, True)

def eofReportedByPlayer(self):
if self.playlist.notJustChangedPlaylist() and self.userlist.currentUser.file:
self.ui.showDebugMessage("Fixing file duration to allow for playlist advancement")
Expand Down Expand Up @@ -671,14 +691,17 @@ def checkForFeatureSupport(self, featureList):
"maxUsernameLength": constants.FALLBACK_MAX_USERNAME_LENGTH,
"maxRoomNameLength": constants.FALLBACK_MAX_ROOM_NAME_LENGTH,
"maxFilenameLength": constants.FALLBACK_MAX_FILENAME_LENGTH,
"setOthersReadiness": utils.meetsMinVersion(self.serverVersion, constants.SET_OTHERS_READINESS_MIN_VERSION)
"setOthersReadiness": utils.meetsMinVersion(self.serverVersion, constants.SET_OTHERS_READINESS_MIN_VERSION),
"speedSync": utils.meetsMinVersion(self.serverVersion, constants.SPEED_SYNC_MIN_VERSION)
}
if featureList:
self.serverFeatures.update(featureList)
if not utils.meetsMinVersion(self.serverVersion, constants.SHARED_PLAYLIST_MIN_VERSION):
self.ui.showErrorMessage(getMessage("shared-playlists-not-supported-by-server-error").format(constants.SHARED_PLAYLIST_MIN_VERSION, self.serverVersion))
elif not self.serverFeatures["sharedPlaylists"]:
self.ui.showErrorMessage(getMessage("shared-playlists-disabled-by-server-error"))
if not self.serverFeatures["speedSync"]:
self.ui.showDebugMessage(getMessage("speed-sync-not-supported-by-server-error").format(constants.SPEED_SYNC_MIN_VERSION, self.serverVersion))
# TODO: Have messages for all unsupported & disabled features
if self.serverFeatures["maxChatMessageLength"] is not None:
constants.MAX_CHAT_MESSAGE_LENGTH = self.serverFeatures["maxChatMessageLength"]
Expand Down Expand Up @@ -744,6 +767,7 @@ def getFeatures(self):
features["managedRooms"] = True
features["persistentRooms"] = True
features["setOthersReadiness"] = True
features["speedSync"] = True

return features

Expand Down
5 changes: 5 additions & 0 deletions syncplay/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,12 @@ def getValueForOS(constantDict):
FASTFORWARD_BEHIND_THRESHOLD = 1.75
SEEK_THRESHOLD = 1
SLOWDOWN_RATE = 0.95
DEFAULT_PLAYBACK_SPEED = 1.0
DEFAULT_SLOWDOWN_KICKIN_THRESHOLD = 1.5
MINIMUM_SLOWDOWN_THRESHOLD = 1.3
SLOWDOWN_RESET_THRESHOLD = 0.1
SPEED_TOLERANCE = 0.01
SPEED_SET_GRACE_PERIOD = 0.5
DIFFERENT_DURATION_THRESHOLD = 2.5
PROTOCOL_TIMEOUT = 12.5
RECONNECT_RETRIES = 999
Expand Down Expand Up @@ -134,6 +137,7 @@ def getValueForOS(constantDict):
COMMANDS_NEXT = ["next", "qn"]
COMMANDS_SETREADY = ['setready', 'sr']
COMMANDS_SETNOTREADY = ['setready', 'snr']
COMMANDS_SPEED = ['sp', 'speed']
MPC_MIN_VER = "1.6.4"
MPC_BE_MIN_VER = "1.5.2.3123"
VLC_MIN_VERSION = "2.2.1"
Expand All @@ -146,6 +150,7 @@ def getValueForOS(constantDict):
CHAT_MIN_VERSION = "1.5.0"
FEATURE_LIST_MIN_VERSION = "1.5.0"
SET_OTHERS_READINESS_MIN_VERSION = "1.7.2"
SPEED_SYNC_MIN_VERSION = "1.7.5"

IINA_PATHS = ['/Applications/IINA.app/Contents/MacOS/IINA']
MPC_PATHS = [
Expand Down
8 changes: 8 additions & 0 deletions syncplay/messages_en.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
"fastforward-notification": "Fast-forwarded due to time difference with {}", # User
"slowdown-notification": "Slowing down due to time difference with {}", # User
"revert-notification": "Reverting speed back to normal",
"speed-change-notification": "{} changed playback speed to {}x", # Username, Speed
"current-speed-notification": "Current speed: {}x",

"pause-notification": "{} paused at {}", # User, Time
"unpause-notification": "{} unpaused", # User
Expand Down Expand Up @@ -110,6 +112,7 @@
"commandlist-notification/create": "\tc [name] - create managed room using name of current room",
"commandlist-notification/auth": "\ta [password] - authenticate as room operator with operator password",
"commandlist-notification/chat": "\tch [message] - send a chat message in a room",
"commandlist-notification/speed": "\tsp [speed] - set playback speed (e.g. sp 1.5). Without argument, shows current speed",
"commandList-notification/queue": "\tqa [file/url] - add file or url to bottom of playlist",
"commandList-notification/queueandselect": "\tqas [file/url] - add file or url to bottom of playlist and select it",
"commandList-notification/playlist": "\tql - show the current playlist",
Expand Down Expand Up @@ -168,13 +171,16 @@
"feature-readiness": "readiness", # used for not-supported-by-server-error
"feature-managedRooms": "managed rooms", # used for not-supported-by-server-error
"feature-setOthersReadiness": "readiness override", # used for not-supported-by-server-error
"feature-speedSync": "speed sync", # used for not-supported-by-server-error

"not-supported-by-server-error": "The {} feature is not supported by this server..", # feature
"shared-playlists-not-supported-by-server-error": "The shared playlists feature may not be supported by the server. To ensure that it works correctly requires a server running Syncplay {}+, but the server is running Syncplay {}.", # minVersion, serverVersion
"shared-playlists-disabled-by-server-error": "The shared playlist feature has been disabled in the server configuration. To use this feature you will need to connect to a different server.",
"speed-sync-not-supported-by-server-error": "The speed sync feature may not be supported by the server. To ensure that it works correctly requires a server running Syncplay {}+, but the server is running Syncplay {}.", # minVersion, serverVersion

"invalid-seek-value": "Invalid seek value",
"invalid-offset-value": "Invalid offset value",
"invalid-speed-value": "Invalid speed value",

"switch-file-not-found-error": "Could not switch to file '{0}'. Syncplay looks in specified media directories.", # File not found
"folder-search-timeout-error": "The search for media in media directories was aborted as it took too long to search through '{}' after having processed the first {:,} files. This will occur if you select a folder with too many sub-folders in your list of media folders to search through or if there are too many files to process. For automatic file switching to work again please select File->Set Media Directories in the menu bar and remove this directory or replace it with an appropriate sub-folder. If the folder is actually fine then you can re-enable it by selecting File->Set Media Directories and pressing 'OK'.", # Folder, Files processed. Note: {:,} is {} but with added commas seprators.
Expand Down Expand Up @@ -311,6 +317,8 @@
"joinroom-menu-label": "Join room {}",
"seektime-menu-label": "Seek to time",
"undoseek-menu-label": "Undo seek",
"setspeed-menu-label": "Set speed",
"setspeed-msgbox-label": "Set playback speed (e.g. 1.0, 1.5, 0.75).",
"play-menu-label": "Play",
"pause-menu-label": "Pause",
"playbackbuttons-menu-label": "Show playback buttons",
Expand Down
3 changes: 2 additions & 1 deletion syncplay/players/mpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ class __COPYDATASTRUCT(ctypes.Structure):


class MPCHCAPIPlayer(BasePlayer):
speedSupported = False
speedSupported = True
alertOSDSupported = False
customOpenDialog = False
chatOSDSupported = False
Expand Down Expand Up @@ -370,6 +370,7 @@ def __onGetPosition(self):
self.__positionUpdate.set()

def setSpeed(self, value):
self._lastSpeedSetTime = time.time()
try:
self._mpcApi.setSpeed(value)
except MpcHcApi.PlayerNotReadyException:
Expand Down
16 changes: 16 additions & 0 deletions syncplay/players/mplayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def __init__(self, client, playerPath, filePath, args):
self._duration = None
self._filename = None
self._filepath = None
self._detectedSpeed = None
self.quitReason = None
self.lastLoadedTime = None
self.fileLoaded = False
Expand Down Expand Up @@ -80,13 +81,22 @@ def _preparePlayer(self):
self.reactor.callLater(0, self._client.initPlayer, self)
self._onFileUpdate()

def _getSpeed(self):
self._getProperty('speed')

def askForStatus(self):
self._positionAsk.clear()
self._pausedAsk.clear()
self._getPaused()
self._getPosition()
self._getSpeed()
self._positionAsk.wait()
self._pausedAsk.wait()
if self._detectedSpeed is not None and not self._client._speedChanged:
detectedSpeed = round(self._detectedSpeed, 2)
gracePeriodActive = time.time() - getattr(self, '_lastSpeedSetTime', 0) < constants.SPEED_SET_GRACE_PERIOD
if not gracePeriodActive and abs(detectedSpeed - self._client.getGlobalSpeed()) > constants.SPEED_TOLERANCE:
self._client.setSpeed(detectedSpeed)
self._client.updatePlayerStatus(self._paused, self._position)

def _setProperty(self, property_, value):
Expand All @@ -111,6 +121,7 @@ def displayChatMessage(self, username, message):
self.OSD_QUERY, messageString, duration, constants.MPLAYER_OSD_LEVEL))

def setSpeed(self, value):
self._lastSpeedSetTime = time.time()
self._setProperty('speed', "{:.2f}".format(value))

def _loadFile(self, filePath):
Expand Down Expand Up @@ -219,6 +230,11 @@ def lineReceived(self, line):
elif name == "pause":
self._storePauseState(bool(value == 'yes'))
self._pausedAsk.set()
elif name == "speed":
try:
self._detectedSpeed = float(value)
except (ValueError, TypeError):
pass
elif name == "length":
try:
self._duration = float(value)
Expand Down
15 changes: 14 additions & 1 deletion syncplay/players/mpv.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ def displayChatMessage(self, username, message):
self._listener.sendLine(["script-message-to", "syncplayintf", "chat", messageString])

def setSpeed(self, value):
self._lastSpeedSetTime = time.time()
self._setProperty('speed', "{:.2f}".format(value))

def setPaused(self, value):
Expand Down Expand Up @@ -314,6 +315,11 @@ def askForStatus(self):
self._getPausedAndPosition()
self._positionAsk.wait(constants.MPV_LOCK_WAIT_TIME)
self._pausedAsk.wait(constants.MPV_LOCK_WAIT_TIME)
if self._detectedSpeed is not None and not self._client._speedChanged:
detectedSpeed = round(self._detectedSpeed, 2)
gracePeriodActive = time.time() - getattr(self, '_lastSpeedSetTime', 0) < constants.SPEED_SET_GRACE_PERIOD
if not gracePeriodActive and abs(detectedSpeed - self._client.getGlobalSpeed()) > constants.SPEED_TOLERANCE:
self._client.setSpeed(detectedSpeed)
self._client.updatePlayerStatus(
self._paused if self.fileLoaded else self._client.getGlobalPaused(), self.getCalculatedPosition())

Expand Down Expand Up @@ -452,7 +458,13 @@ def _handleUnknownLine(self, line):
else:
self._storePosition(float(position_update))
self._positionAsk.set()
#self._client.ui.showDebugMessage("{} = {} / {}".format(update_string, paused_update, position_update))
if len(update_string) > 6 and update_string[5] == "speed":
speed_update = update_string[6]
if speed_update != "nil":
try:
self._detectedSpeed = float(speed_update)
except ValueError:
pass

if "<get_syncplayintf_options>" in line:
self._sendMpvOptions()
Expand Down Expand Up @@ -529,6 +541,7 @@ def _set_defaults(self):
self._duration = None
self._filename = None
self._filepath = None
self._detectedSpeed = None
self.quitReason = None
self.lastLoadedTime = None
self.fileLoaded = False
Expand Down
15 changes: 14 additions & 1 deletion syncplay/players/vlc.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ def __init__(self, client, playerPath, filePath, args):
self._filename = None
self._filepath = None
self._filechanged = False
self._detectedSpeed = None
self._lastVLCPositionUpdate = None
self.shownVLCLatencyError = False
self._previousPreviousPosition = -2
Expand Down Expand Up @@ -192,6 +193,11 @@ def askForStatus(self):
self._listener.sendLine(".")
if self._filename and not self._filechanged:
self._positionAsk.wait(constants.PLAYER_ASK_DELAY)
if self._detectedSpeed is not None and not self._client._speedChanged:
detectedSpeed = round(self._detectedSpeed, 2)
gracePeriodActive = time.time() - getattr(self, '_lastSpeedSetTime', 0) < constants.SPEED_SET_GRACE_PERIOD
if not gracePeriodActive and abs(detectedSpeed - self._client.getGlobalSpeed()) > constants.SPEED_TOLERANCE:
self._client.setSpeed(detectedSpeed)
self._client.updatePlayerStatus(self._paused, self.getCalculatedPosition())
else:
self._client.updatePlayerStatus(self._client.getGlobalPaused(), self._client.getGlobalPosition())
Expand Down Expand Up @@ -222,7 +228,8 @@ def displayMessage(
self._listener.sendLine('display-secondary-osd: {}, {}, {}'.format('center', duration, message))

def setSpeed(self, value):
self._listener.sendLine("set-rate: {:.2n}".format(value))
self._lastSpeedSetTime = time.time()
self._listener.sendLine("set-rate: {:.2f}".format(value))

def setFeatures(self, featureList):
pass
Expand Down Expand Up @@ -343,6 +350,12 @@ def lineReceived(self, line):
self.drop(getMessage("vlc-failed-versioncheck"))
self._lastVLCPositionUpdate = time.time()
self._positionAsk.set()
elif name == "rate":
if value != "no-input":
try:
self._detectedSpeed = float(value.replace(",", "."))
except (ValueError, AttributeError):
pass
elif name == "filename":
self._filechanged = True
self._filename = value
Expand Down
Loading