syncplay/syncplay/client.py
2012-12-29 12:37:54 +01:00

516 lines
20 KiB
Python

#coding:utf8
import hashlib
import os.path
import time
from twisted.internet.protocol import ClientFactory
from twisted.internet import reactor, task
from syncplay.protocols import SyncClientProtocol
from syncplay import utils, constants
class SyncClientFactory(ClientFactory):
def __init__(self, client, retry = constants.RECONNECT_RETRIES):
self._client = client
self.retry = retry
self._timesTried = 0
self.reconnecting = False
def buildProtocol(self, addr):
return SyncClientProtocol(self._client)
def startedConnecting(self, connector):
destination = connector.getDestination()
self._client.ui.showMessage('Attempting to connect to {}:{}'.format(destination.host, destination.port))
def clientConnectionLost(self, connector, reason):
if self._timesTried < self.retry:
self._timesTried += 1
message = 'Connection with server lost, attempting to reconnect'
self._client.ui.showMessage(message)
self.reconnecting = True
reactor.callLater(0.1*(2**self._timesTried), connector.connect)
else:
message = 'Disconnected from server'
self._client.ui.showMessage(message)
def clientConnectionFailed(self, connector, reason):
if not self.reconnecting:
message = 'Connection with server failed'
self._client.ui.showMessage(message)
self._client.stop(True)
else:
self.clientConnectionLost(connector, reason)
def resetRetrying(self):
self._timesTried = 0
def stopRetrying(self):
self._timesTried = self.retry
class SyncplayClient(object):
def __init__(self, playerClass, ui, args):
self.protocolFactory = SyncClientFactory(self)
self.ui = UiManager(self, ui)
self.userlist = SyncplayUserlist(self.ui, self)
self._protocol = None
if(args.room == None or args.room == ''):
args.room = constants.DEFAULT_ROOM
self.defaultRoom = args.room
self.playerPositionBeforeLastSeek = 0.0
self.setUsername(args.name)
self.setRoom(args.room)
if(args.password):
args.password = hashlib.md5(args.password).hexdigest()
self._serverPassword = args.password
if(not args.file):
self.__getUserlistOnLogon = True
else:
self.__getUserlistOnLogon = False
self._player = None
self._playerClass = playerClass
self._startupArgs = args
self._running = False
self._askPlayerTimer = None
self._lastPlayerUpdate = None
self._playerPosition = 0.0
self._playerPaused = True
self._lastGlobalUpdate = None
self._globalPosition = 0.0
self._globalPaused = 0.0
self._userOffset = 0.0
self._speedChanged = False
def initProtocol(self, protocol):
self._protocol = protocol
def destroyProtocol(self):
if(self._protocol):
self._protocol.drop()
self._protocol = None
def initPlayer(self, player):
self._player = player
self.scheduleAskPlayer()
def scheduleAskPlayer(self, when=constants.PLAYER_ASK_DELAY):
self._askPlayerTimer = task.LoopingCall(self.askPlayer)
self._askPlayerTimer.start(when)
def askPlayer(self):
if(not self._running):
return
if(self._player):
self._player.askForStatus()
self.checkIfConnected()
def checkIfConnected(self):
if(self._lastGlobalUpdate and self._protocol and time.time() - self._lastGlobalUpdate > constants.PROTOCOL_TIMEOUT):
self._lastGlobalUpdate = None
self.ui.showErrorMessage("Connection with server timed out")
self._protocol.drop()
return False
return True
def _determinePlayerStateChange(self, paused, position):
pauseChange = self.getPlayerPaused() != paused and self.getGlobalPaused() != paused
_playerDiff = abs(self.getPlayerPosition() - position)
_globalDiff = abs(self.getGlobalPosition() - position)
seeked = _playerDiff > constants.SEEK_BOUNDARY and _globalDiff > constants.SEEK_BOUNDARY
return pauseChange, seeked
def updatePlayerStatus(self, paused, position):
position -= self.getUserOffset()
pauseChange, seeked = self._determinePlayerStateChange(paused, position)
self._playerPosition = position
self._playerPaused = paused
if(self._lastGlobalUpdate):
self._lastPlayerUpdate = time.time()
if((pauseChange or seeked) and self._protocol):
if(seeked):
self.playerPositionBeforeLastSeek = self.getGlobalPosition()
self._protocol.sendState(self.getPlayerPosition(), self.getPlayerPaused(), seeked, None, True)
def getLocalState(self):
paused = self.getPlayerPaused()
position = self.getPlayerPosition()
pauseChange, _ = self._determinePlayerStateChange(paused, position)
if(self._lastGlobalUpdate):
return position, paused, _, pauseChange
else:
return None, None, None, None
def _initPlayerState(self, position, paused):
self.setPosition(position)
self._player.setPaused(paused)
madeChangeOnPlayer = True
return madeChangeOnPlayer
def _rewindPlayerDueToTimeDifference(self, position, setBy):
self.setPosition(position)
message = "Rewinded due to time difference with <{}>".format(setBy)
self.ui.showMessage(message)
madeChangeOnPlayer = True
return madeChangeOnPlayer
def _serverUnpaused(self, setBy):
self._player.setPaused(False)
madeChangeOnPlayer = True
message = '<{}> unpaused'.format(setBy)
self.ui.showMessage(message)
return madeChangeOnPlayer
def _serverPaused(self, setBy, diff):
if (diff > 0):
self.setPosition(self.getGlobalPosition())
self._player.setPaused(True)
madeChangeOnPlayer = True
message = '<{}> paused'.format(setBy)
self.ui.showMessage(message)
return madeChangeOnPlayer
def _serverSeeked(self, position, setBy):
if(self.getUsername() <> setBy):
self.playerPositionBeforeLastSeek = self.getPlayerPosition()
self.setPosition(position)
madeChangeOnPlayer = True
else:
madeChangeOnPlayer = False
message = '<{}> jumped from {} to {}'.format(setBy, utils.formatTime(self.playerPositionBeforeLastSeek), utils.formatTime(position))
self.ui.showMessage(message)
return madeChangeOnPlayer
def _slowDownToCoverTimeDifference(self, diff, setBy):
if(constants.SLOWDOWN_KICKIN_BOUNDARY < diff and not self._speedChanged):
self._player.setSpeed(constants.SLOWDOWN_RATE)
self._speedChanged = True
message = "Slowing down due to time difference with <{}>".format(setBy)
self.ui.showMessage(message)
elif(self._speedChanged and diff < constants.SLOWDOWN_RESET_BOUNDARY):
self._player.setSpeed(1.00)
self._speedChanged = False
message = "Reverting speed back to normal"
self.ui.showMessage(message)
madeChangeOnPlayer = True
return madeChangeOnPlayer
def _changePlayerStateAccordingToGlobalState(self, position, paused, doSeek, setBy):
madeChangeOnPlayer = False
pauseChanged = paused != self.getGlobalPaused()
diff = self.getPlayerPosition() - position
if(self._lastGlobalUpdate is None):
madeChangeOnPlayer = self._initPlayerState(position, paused)
self._globalPaused = paused
self._globalPosition = position
self._lastGlobalUpdate = time.time()
if (doSeek):
madeChangeOnPlayer = self._serverSeeked(position, setBy)
if (diff > 4 and not doSeek):
madeChangeOnPlayer = self._rewindPlayerDueToTimeDifference(position, setBy)
if (self._player.speedSupported and not doSeek and not paused):
madeChangeOnPlayer = self._slowDownToCoverTimeDifference(diff, setBy)
if (paused == False and pauseChanged):
madeChangeOnPlayer = self._serverUnpaused(setBy)
elif (paused == True and pauseChanged):
madeChangeOnPlayer = self._serverPaused(setBy, diff)
return madeChangeOnPlayer
def updateGlobalState(self, position, paused, doSeek, setBy, latency):
if(self.__getUserlistOnLogon):
self.__getUserlistOnLogon = False
self.getUserList()
madeChangeOnPlayer = False
if(not paused):
position += latency
if(self._player):
madeChangeOnPlayer = self._changePlayerStateAccordingToGlobalState(position, paused, doSeek, setBy)
if(madeChangeOnPlayer):
self.askPlayer()
def getUserOffset(self):
return self._userOffset
def setUserOffset(self, time):
self._userOffset = time
message = "Current offset: {} seconds".format(self._userOffset)
self.setPosition(self.getGlobalPosition())
self.ui.showMessage(message)
def getPlayerPosition(self):
if(not self._lastPlayerUpdate):
if(self._lastGlobalUpdate):
return self.getGlobalPosition()
else:
return 0.0
position = self._playerPosition
if(not self._playerPaused):
diff = time.time() - self._lastPlayerUpdate
if diff < 0.5:
position += diff
return position
def getPlayerPaused(self):
if(not self._lastPlayerUpdate):
if(self._lastGlobalUpdate):
return self.getGlobalPaused()
else:
return True
return self._playerPaused
def getGlobalPosition(self):
if not self._lastGlobalUpdate:
return 0.0
position = self._globalPosition
if not self._globalPaused:
position += time.time() - self._lastGlobalUpdate
return position
def getGlobalPaused(self):
if(not self._lastGlobalUpdate):
return True
return self._globalPaused
def updateFile(self, filename, duration, path):
size = os.path.getsize(path)
self.userlist.currentUser.setFile(filename, duration, size)
self.sendFile()
def sendFile(self):
file_ = self.userlist.currentUser.file
if(self._protocol and self._protocol.logged and file_):
self._protocol.sendFileSetting(file_)
def setUsername(self, username):
self.userlist.currentUser.username = username
def getUsername(self):
return self.userlist.currentUser.username
def setRoom(self, roomName):
self.userlist.currentUser.room = roomName
self.getUserList()
def sendRoom(self):
room = self.userlist.currentUser.room
if(self._protocol and self._protocol.logged and room):
self._protocol.sendRoomSetting(room)
def getRoom(self):
return self.userlist.currentUser.room
def getUserList(self):
if(self._protocol and self._protocol.logged):
self._protocol.sendList()
def getPassword(self):
return self._serverPassword
def setPosition(self, position):
position += self.getUserOffset()
if(self._player):
if(position < 0):
position = 0
self._protocol.sendState(self.getPlayerPosition(), self.getPlayerPaused(), True, None, True)
self._player.setPosition(position)
def setPaused(self, paused):
if(self._player):
self._player.setPaused(paused)
def start(self, host, port):
if self._running:
return
self._running = True
if self._playerClass:
self._playerClass.run(self, self._startupArgs.player_path, self._startupArgs.file, self._startupArgs._args)
self._playerClass = None
self.protocolFactory = SyncClientFactory(self)
reactor.connectTCP(host, port, self.protocolFactory)
reactor.run()
def stop(self, promptForAction = False):
if not self._running:
return
self._running = False
if self.protocolFactory:
self.protocolFactory.stopRetrying()
self.destroyProtocol()
if self._player:
self._player.drop()
reactor.callLater(0.1, reactor.stop)
if(promptForAction):
self.ui.promptFor("Press enter to exit\n")
class SyncplayUser(object):
def __init__(self, username = None, room = None, file_ = None, position = 0):
self.username = username
self.room = room
self.file = file_
self.lastPosition = position
def setFile(self, filename, duration, size):
file_ = {
"name": filename,
"duration": duration,
"size":size
}
self.file = file_
def isFileSame(self, file_):
if(not self.file):
return False
sameName = self.file['name'] == file_['name']
sameSize = self.file['size'] == file_['size']
sameDuration = int(self.file['duration']) - int(file_['duration']) < constants.DIFFFERENT_DURATION_BOUNDARY
return sameName and sameSize and sameDuration
def __lt__(self, other):
return self.username < other.username
class SyncplayUserlist(object):
def __init__(self, ui, client):
self.currentUser = SyncplayUser()
self._users = {}
self.ui = ui
self._client = client
def __showUserChangeMessage(self, username, room, file_):
if (room and not file_):
message = "<{}> has joined the room: '{}'".format(username, room)
self.ui.showMessage(message)
elif (room and file_ and username != self.currentUser.username):
duration = utils.formatTime(file_['duration'])
message = "<{}> is playing '{}' ({})".format(username, file_['name'], duration)
if(self.currentUser.room <> room or self.currentUser.username == username):
message += " in room: '{}'".format(room)
self.ui.showMessage(message)
if(self.currentUser.file and not self.currentUser.isFileSame(file_) and self.currentUser.room == room):
message = "File you are playing appears to be different from <{}>'s".format(username)
self.ui.showMessage(message)
differences = []
if(self.currentUser.file['name'] <> file_['name']):
differences.append("filename")
if(self.currentUser.file['size'] <> file_['size']):
differences.append("size")
if(self.currentUser.file['duration'] <> file_['duration']):
differences.append("duration")
message = "Your file differs in the following way(s): " + ", ".join(differences)
self.ui.showMessage(message)
def addUser(self, username, room, file_, position = 0, noMessage = False):
if(username == self.currentUser.username):
self.currentUser.lastPosition = position
return
user = SyncplayUser(username, room, file_, position)
self._users[username] = user
if(not noMessage):
self.__showUserChangeMessage(username, room, file_)
def removeUser(self, username):
if(self._users.has_key(username)):
self._users.pop(username)
message = "<{}> has left".format(username)
self.ui.showMessage(message)
def __displayModUserMessage(self, username, room, file_, user):
if (file_ and not user.isFileSame(file_)):
self.__showUserChangeMessage(username, room, file_)
elif (room and room != user.room):
self.__showUserChangeMessage(username, room, None)
def modUser(self, username, room, file_):
if(self._users.has_key(username)):
user = self._users[username]
self.__displayModUserMessage(username, room, file_, user)
user.room = room
user.file = file_
elif(username == self.currentUser.username):
self.__showUserChangeMessage(username, room, file_)
else:
self.addUser(username, room, file_)
def __addUserWithFileToList(self, rooms, user):
currentPosition = utils.formatTime(user.lastPosition)
file_key = '\'{}\' ({}/{})'.format(user.file['name'], currentPosition, utils.formatTime(user.file['duration']))
if (not rooms[user.room].has_key(file_key)):
rooms[user.room][file_key] = {}
rooms[user.room][file_key][user.username] = user
def __addUserWithoutFileToList(self, rooms, user):
if (not rooms[user.room].has_key("__noFile__")):
rooms[user.room]["__noFile__"] = {}
rooms[user.room]["__noFile__"][user.username] = user
def __createListOfPeople(self, rooms):
if(not rooms.has_key(self.currentUser.room)):
rooms[self.currentUser.room] = {}
for user in self._users.itervalues():
if (not rooms.has_key(user.room)):
rooms[user.room] = {}
if(user.file):
self.__addUserWithFileToList(rooms, user)
else:
self.__addUserWithoutFileToList(rooms, user)
if(self.currentUser.file):
self.__addUserWithFileToList(rooms, self.currentUser)
else:
self.__addUserWithoutFileToList(rooms, self.currentUser)
return rooms
def __addDifferentFileMessageIfNecessary(self, user, message):
if(self.currentUser.file):
fileHasSameSizeAsYour = user.file['size'] == self.currentUser.file['size']
fileHasSameNameYour = user.file['name'] == self.currentUser.file['name']
differentFileMessage = " (their file size is different from yours!)"
message += differentFileMessage if not fileHasSameSizeAsYour and fileHasSameNameYour else ""
return message
def __displayFileWatchersInRoomList(self, key, users):
self.ui.showMessage("File: {} is being played by:".format(key), True, True)
for user in sorted(users.itervalues()):
message = "<"+user.username+">"
if(self.currentUser.username == user.username):
message = "*" + message + "*"
message = self.__addDifferentFileMessageIfNecessary(user, message)
self.ui.showMessage("\t" + message, True, True)
def __displayPeopleInRoomWithNoFile(self, noFileList):
if (noFileList):
self.ui.showMessage("People who are not playing any file:", True, True)
for user in sorted(noFileList.itervalues()):
self.ui.showMessage("\t<" + user.username + ">", True, True)
def __displayListOfPeople(self, rooms):
for roomName in sorted(rooms.iterkeys()):
self.ui.showMessage("In room '{}':".format(roomName), True, False)
noFileList = rooms[roomName].pop("__noFile__") if (rooms[roomName].has_key("__noFile__")) else None
for key in sorted(rooms[roomName].iterkeys()):
self.__displayFileWatchersInRoomList(key, rooms[roomName][key])
self.__displayPeopleInRoomWithNoFile(noFileList)
def showUserList(self):
rooms = {}
self.__createListOfPeople(rooms)
self.__displayListOfPeople(rooms)
def clearList(self):
self._users = {}
class UiManager(object):
def __init__(self, client, ui):
self._client = client
self.__ui = ui
def showMessage(self, message, noPlayer = False, noTimestamp = False):
if(self._client._player and not noPlayer): self._client._player.displayMessage(message)
self.__ui.showMessage(message, noTimestamp)
def showErrorMessage(self, message):
self.__ui.showErrorMessage(message)
def promptFor(self, prompt):
return self.__ui.promptFor(prompt)