tequila init

This commit is contained in:
Uriziel 2012-10-12 18:37:12 +02:00
commit ac54d22fd0
24 changed files with 2445 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
*.py[co]
.*.sw[po]
venv
/SyncPlay.egg-info
/build
/dist
dist.7z
.*

27
Makefile Normal file
View File

@ -0,0 +1,27 @@
BIN_PATH = /usr/bin
LIB_PATH = /usr/lib
APP_SHORTCUT_PATH = /usr/share/applications
ICON_PATH = /usr/share/icons
install:
touch $(BIN_PATH)/syncplay
echo '#!/bin/sh\npython $(LIB_PATH)/syncplay/syncplayClient.py "$$@"' > $(BIN_PATH)/syncplay
chmod a+x $(BIN_PATH)/syncplay
touch $(BIN_PATH)/syncplay-server
echo '#!/bin/sh\npython $(LIB_PATH)/syncplay/syncplayServer.py "$$@"' > $(BIN_PATH)/syncplay-server
chmod a+x $(BIN_PATH)/syncplay-server
mkdir $(LIB_PATH)/syncplay/
cp syncplayClient.py $(LIB_PATH)/syncplay/
cp syncplayServer.py $(LIB_PATH)/syncplay/
cp -r syncplay $(LIB_PATH)/syncplay/
cp syncplay.desktop $(APP_SHORTCUT_PATH)/
cp syncplay-server.desktop $(APP_SHORTCUT_PATH)/
cp icon.ico $(ICON_PATH)/
uninstall:
rm $(BIN_PATH)/syncplay
rm $(BIN_PATH)/syncplay-server
rm -rf $(LIB_PATH)/syncplay
rm $(APP_SHORTCUT_PATH)/syncplay.desktop
rm $(APP_SHORTCUT_PATH)/syncplay-server.desktop
rm $(ICON_PATH)/icon.ico

171
README.md Normal file
View File

@ -0,0 +1,171 @@
# Syncplay
Solution to synchronize video playback across multiple instances of mplayer2 and/or Media Player Classic (MPC-HC) over the Internet.
## Notice
No official builds of Syncplay have been released yet. If you want the current beta then compile it yourself from the official branch or check out the #syncplay IRC channel on irc.rizon.net.
## What does it do
Syncplay synchronises the position and play state of multiple media players so that the viewers can watch the same thing at the same time.
This means that when one person pauses/unpauses playback or seeks (jumps position) within their media player then this will be replicated across all media players connected to the same server and in the same 'room' (viewing session).
When a new person joins they will also be synchronised.
## What it doesn't do
Syncplay does not use video streaming or file sharing so each user must have their own copy of the media to be played. Syncplay does not synchronise player configuration, audio or subtitle track choice, playback rate, volume or filters. Furthermore, users must manually choose what file to play as Syncplay does not synchronise which file is open. Finally, Syncplay does not provide a voice or text-based chat platform to allow for discussion during playback as Syncplay is intended to be used in conjunction with third-party communication solutions such as IRC and Mumble.
## Requirements
Frozen Windows executables are available on the download page - https://github.com/Uriziel/syncplay/downloads
* On Windows: `Media Player Classic - Home Cinema (MPC-HC)` >= `1.6.4`.
* On Linux: `mplayer2`. `MPlayer` >= `1.1` should be compatible, but is not supported.
### Python scripts (for those not using the frozen executable package)
If you are not using the frozen executable package then you will need the following to run Python scripts:
* `pygtk` >= `2.0.0` (unless started with `--no-gui`)
* `argparse` >= `1.1`
* `pywin32` >= `r217` (MPC-HC, Windows only)
* `twisted` >= `12.1.0`
If you are using the frozen executable package available from the download page then you will not need to be able to run Python scripts.
## Supported players
### mplayer2 on Linux
On Linux `syncplay` acts as a front-end for mplayer2.
To use it select "Open with..." in context menu and choose `Syncplay` or from command line: "syncplay video_filename". If you wish to pass more arguments to mplayer2 prepend them with -- argument, it's treated as the last argument for wrapper.
Default mplayer2 output is suppressed, but if mplayer2 quits with errors, those errors will be printed (at most 50 last lines).
### Media Player Classic - Home Cinema (MPC-HC) on Windows
On Windows simply running `syncplayClient.exe` opens a Syncplay command-line window for communication/control and a new instance of MPC-HC for synchronised video playback. This instance of MPC-HC is controlled by Syncplay through the associated command-line window, but other instances of MPC-HC will be unaffected.
## Using Syncplay
### Getting started with Syncplay on Windows
1. Ensure that you have the latest version of `Media Player Classic - Home Cinema (MPC-HC)` installed. The latest stable build is `1.6.4`.
2. Download Syncplay frozen executable package from https://github.com/Uriziel/syncplay/downloads and extract to a folder of your choosing.
3. If you are running your own server then open `syncplayServer.exe` (see "How to use the server", below).
4. Open `syncplayClient.exe` (or open the media file you wish to play with `syncplayClient.exe`, e.g. using "Open with").
5. Enter configuration settings (see "Configuration window", below). Ensure that you are on the same server and room as your fellow viewers.
6. If you don't have the file you want to play open then open it from within the MPC-HC instance initiated by Syncplay.
7. Playing, pausing and seeking from within the MPC-HC instance should now be synchronised with everyone else in the same 'room'.
### Getting started with Syncplay on Linux
1. Ensure that you have an up to date version of `mplayer2` installed.
2. Download Syncplay tarball from https://github.com/Uriziel/syncplay/downloads and run `make install`.
3. Open the media file you wish to play with `syncplay` (e.g. using "Open with...").
4. Enter configuration settings (see "Configuration window", below). Ensure that you are on the same server and room as your fellow viewers.
5. Playing, pausing and seeking from within the mplayer2 instance should now be synchronised with everyone else in the same 'room'.
### Opening a media file with Syncplay
Opening a file with `syncplayClient` (`Syncplay` on Linux) will automatically run Syncplay and load the file through MPC-HC on Windows or mplayer2 on Linux.
### Configuration window
The configuration window allows for various settings to be configured prior to Syncplay starting.
The window will appear if you:
1. Run `syncplayClient` without settings being configured, e.g. on first boot,
2. Run `syncplayClient` with the `--force-gui-prompt` or `-g` commandline switches, or
3. Run `syncplayClientForceConfiguration`.
The settings to be configured are as follows:
* `Host` - Address (hostname / IP) of server to connect to (optionally with port), e.g. `localhost:2734` or `192.168.0.1`. Default port is `8999`.
* `Username` - Name that the server and other viewers know you by.
* `Default room (optional)` - Room to join upon connection. You will only be synchronised with others in the same room on the same server. Default room is `default`.
* `Server password (optional)` - Password for server. Servers that are not password protected have a blank password.
* `Path to mpc-hc.exe [Windows only]` - Location of the MPC-HC executable (mpc-hc.exe or mpc-hc64.exe). If this is in a common location then it will be filled in by default. Users are advised to check that it points to their desired installation.
Pressing "Save" will save the settings and continue Syncplay start-up.
### Syncplay Commands
Within the Syncplay command-line you can enter the following commands (and then press enter) to access features:
* `help` - Displays list of commands and other information.
* `room [room]` - Leaves current room and joins specified room. You are only synchronised with others in the same room on the same server. If no room is specified then this command will use the filename of the currently open file, or alternatively will join the room `default`.
* `s [time]` - Seek (jump) to specified time. Can be `seconds`, `minutes:seconds` or `hours:minutes:seconds`.
* `s+ [time]` - Jumps [time] forward. Can be `seconds`, `minutes:seconds` or `hours:minutes:seconds`.
* `r` - Revert last seek. Seeks to where you were before the most recent seek.
* `p` - Toggle play/pause.
### Command-line switches
You can run `syncplayClient` with the following command-line switches to alter Syncplay settings or behaviour:
* `--no-gui` - Do not display graphical user interface (GUI)
* `--host [address]` - Specify address of server to connect to (can be `address:port`)
* `--name [name]` / `-n [name]` - Specify username to use
* `--debug` / `-d` - Enable debug mode
* `--force-gui-prompt` / `-g` - Force the configuration window to appear when Syncplay starts
* `--no-store` - Disable the storing of configuration data (in .syncplay file)
* `--room [room]` / `-r [room]` - Specify default room to join upon connection.
* `--password [password]` / `-p [password]` - Specify server password
* `[file]` - File to play upon start
* `--` - used as a last argument for syncplayClient, used to prepend arguments that are meant to be passed to player
### Notification messages
* `Rewinded due to time difference with [user]` - This means that your media player ended up too far in front of the specified user and has jumped back to keep you in sync. This is usually because someone's computer isn't powerful enough to play the file smoothly.
* `File you're playing is different from [user]'s` - This means that the filename, length and/or duration of the file that the user is playing is different from the file that you are playing. This is for information only and is not an error.
## How to use the server
Run `syncplayServer` to host a Syncplay server. If you have a public IP then you can try to launch the server on your computer
and give your friends your IP number so that they can connect to it. The server software will listen on port `8999` by default, but you can specify a different port. You might need to specifically allow connections to `syncplayServer` in your firewall/router. If that is the case then please consult your firewall/router instructions or contact your network administrator.
Pass the IP or hostname (and password / port if necessary) to people you want to watch with and you're ready to go. There are various online services that will tell you what your IP address is.
### Server command-line switches
* `--port [port]` - Use stated port instead of the default one.
* `--isolate-room` - If specified then 'room isolation' is enabled. This means that viewers will not be able to see information about users who are in rooms other than the one they are in. This feature is recommended for a public server, but not for a small private server.
* `--password [password]` - Restrict access to the Syncplay server to only those who use this password when they connect to the server. This feature is recommended for a private server but is not needed for a public server. By default the password is blank (i.e. there is no password restriction).
## Syncplay behaviour
The following information is sent from the client to the server:
* Public IP address of client and other necessary routing information (as per TCP/IP standards).
* Media position, play state, and any seek/pause/unpause commands (associated with the instance of the media player initiated by Syncplay).
* Size, length, and optionally filename of currently open media (associated with the instance of the media player initiated by Syncplay).
* Syncplay version, username, server password and current 'room'.
* Ping responses to assess latency.
Note: The current official build of the Syncplay server does not store any of this information. However, some of the information (not the IP address) is passed on to other users connected to the server (or just to those in the same room if 'isolation' mode is enabled).
The server has the ability to control the following aspects of the instance of the media player initiated by Syncplay:
* Current position (seek commands).
* Current play state (pause and unpause commands).
The client affects the following files:
* Modifying .syncplay file in %APPDATA% (or $HOME on Linux version) folder to store configuration information.
Note: This behaviour can be disabled by using the `--no-store` command-line switch (see "Command-line switches", above)
## How to report bugs
You can report bugs through https://github.com/Uriziel/syncplay/issues but first please check to see if your problem has already been reported.
You might also be able to discuss your problem through Internet Relay Chat (IRC). The #Syncplay channel is on the irc.rizon.net server.
### Known issues
1. Changing your system time while Syncplay is running confuses the sync. PROTIP: Don't do it.
2. Syncplay cannot properly handle a seek that is within 8 seconds of the current position. PROTIP: Don't do it.

33
buildPy2exe.py Normal file
View File

@ -0,0 +1,33 @@
#!/usr/bin/env python
#coding:utf8
from setuptools import setup
import py2exe #@UnusedImport
from setuptools import find_packages
import syncplay
common_info = dict(
name = 'Syncplay',
version = syncplay.version,
author = 'Tomasz Kowalczyk, Uriziel',
author_email = 'code@fluxid.pl, urizieli@gmail.com',
description = 'Syncplay',
packages = find_packages(exclude=['venv']),
install_requires = ['Twisted>=11.1'],
)
info = dict(
common_info,
console = [{"script":"syncplayClient.py","icon_resources":[(1,"icon.ico")]}, {"script":"syncplayClientForceConfiguration.py","icon_resources":[(1,"icon.ico")]}, 'syncplayServer.py'],
options = {'py2exe': {
'includes': 'cairo, pango, pangocairo, atk, gobject',
'optimize': 2,
'compressed': 1
}
},
)
setup(**info)

BIN
icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

9
syncplay-server.desktop Normal file
View File

@ -0,0 +1,9 @@
[Desktop Entry]
Encoding=UTF-8
Name=Syncplay server
Comment=Synchronize video playback over network
Exec=/usr/bin/syncplay-server %u
Terminal=true
Type=Application
Icon=/usr/share/icons/icon.ico
Categories=AudioVideo;Audio;Video;

11
syncplay.desktop Normal file
View File

@ -0,0 +1,11 @@
[Desktop Entry]
Encoding=UTF-8
Name=Syncplay
Comment=Synchronize video playback over network
Exec=/usr/bin/syncplay %u
Terminal=true
Type=Application
Icon=/usr/share/icons/icon.ico
Categories=AudioVideo;Audio;Video;Player;Network;
MimeType=audio/ac3;audio/mp4;audio/mpeg;audio/vnd.rn-realaudio;audio/vorbis;audio/x-adpcm;audio/x-matroska;audio/x-mp2;audio/x-mp3;audio/x-ms-wma;audio/x-vorbis;audio/x-wav;audio/mpegurl;audio/x-mpegurl;audio/x-pn-realaudio;audio/x-scpls;video/avi;video/mp4;video/flv;video/mpeg;video/quicktime;video/vnd.rn-realvideo;video/x-matroska;video/x-ms-asf;video/x-msvideo;video/x-ms-wmv;video/x-ogm;video/x-theora;
NoDisplay=true

3
syncplay/__init__.py Normal file
View File

@ -0,0 +1,3 @@
version = '1.0.1'
milestone = 'Tequila'
projectURL = 'http://uriziel.github.com/syncplay/'

451
syncplay/client.py Normal file
View File

@ -0,0 +1,451 @@
#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
class SyncClientFactory(ClientFactory):
def __init__(self, client, retry = 10):
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('Connecting to {}:{}'.format(destination.host, destination.port))
def clientConnectionLost(self, connector, reason):
if self._timesTried < self.retry:
self._timesTried += 1
message = 'Connection lost, reconnecting'
self._client.ui.showMessage(message)
self.reconnecting = True
reactor.callLater(0.1*(2**self._timesTried), connector.connect)
else:
message = 'Disconnected'
self._client.ui.showMessage(message)
def clientConnectionFailed(self, connector, reason):
if not self.reconnecting:
message = 'Connection failed'
self._client.ui.showMessage(message)
self._client.stop()
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)
if(args.room == None or args.room == ''):
room = 'default'
self.defaultRoom = room
self.playerPositionBeforeLastSeek = 0.0
self.setUsername(args.name)
self.setRoom(room)
if(args.password):
args.password = hashlib.md5(args.password).hexdigest()
self._serverPassword = args.password
self._protocol = None
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._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=0.1):
self._askPlayerTimer = task.LoopingCall(self.askPlayer)
self._askPlayerTimer.start(when)
def askPlayer(self):
if(not self._running):
return
if(self._player):
self._player.askForStatus()
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 > 1 and _globalDiff > 1
return pauseChange, seeked
def updatePlayerStatus(self, paused, position):
pauseChange, seeked = self._determinePlayerStateChange(paused, position)
self._playerPosition = position
self._playerPaused = paused
if(self._lastGlobalUpdate):
self._lastPlayerUpdate = time.time()
if(pauseChange or seeked):
self.playerPositionBeforeLastSeek = self.getGlobalPosition()
if(self._protocol):
self._protocol.sendState(self.getPlayerPosition(), self.getPlayerPaused(), seeked, None, True)
def getLocalState(self):
paused = self.getPlayerPaused()
position = self.getPlayerPosition()
pauseChange, seeked = self._determinePlayerStateChange(paused, position)
if(self._lastGlobalUpdate):
return position, paused, seeked, pauseChange
else:
return None, None, None, None
def _initPlayerState(self, position, paused):
self._player.setPosition(position)
self._player.setPaused(paused)
madeChangeOnPlayer = True
return madeChangeOnPlayer
def _rewindPlayerDueToTimeDifference(self, position, setBy):
self._player.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._player.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._player.setPosition(position)
madeChangeOnPlayer = True
else:
madeChangeOnPlayer = False
message = '<{}> jumped from {} to {}'.format(setBy, self.ui.formatTime(self.playerPositionBeforeLastSeek), self.ui.formatTime(position))
self.ui.showMessage(message)
return madeChangeOnPlayer
def _slowDownToCoverTimeDifference(self, diff):
if(0.4 < diff < 4):
self._player.setSpeed(0.95)
self._speedChanged = True
elif(self._speedChanged):
self._player.setSpeed(1.00)
self._speedChanged = False
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)
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):
madeChangeOnPlayer = False
if(not paused):
position += latency
if(self._player):
madeChangeOnPlayer = self._changePlayerStateAccordingToGlobalState(position, paused, doSeek, setBy)
if(madeChangeOnPlayer):
self.askPlayer()
def getPlayerPosition(self):
if(not self._lastPlayerUpdate):
if(self._lastGlobalUpdate):
return self.getGlobalPosition()
else:
return 0.0
position = self._playerPosition
if(not self._playerPaused):
position += time.time() - self._lastPlayerUpdate
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
def getRoom(self):
return self.userlist.currentUser.room
def getPassword(self):
return self._serverPassword
def setPosition(self, position):
self._player.setPosition(position)
def setPaused(self, paused):
self._player.setPaused(paused)
def start(self, host, port):
if self._running:
return
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)
self._running = True
reactor.run()
def stop(self, promptForAction = True):
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):
self.username = username
self.room = room
self.file = file_
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 = self.file['duration'] == file_['duration']
return sameName and sameSize and sameDuration
def __lt__(self, other):
return self.username < other.username
class SyncplayUserlist(object):
def __init__(self, ui):
self.currentUser = SyncplayUser()
self._users = {}
self.ui = ui
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_):
duration = self.ui.formatTime(file_['duration'])
message = "<{}> is playing '{}' ({})".format(username, file_['name'], duration)
if(self.currentUser.room <> room):
message += " in room: '{}'".format(room)
self.ui.showMessage(message)
if(self.currentUser.file and not self.currentUser.isFileSame(file_)):
message = "File you are playing appears to be different from <{}>'s".format(username)
self.ui.showMessage(message)
def addUser(self, username, room, file_, noMessage = False):
if(username == self.currentUser.username):
return
user = SyncplayUser(username, room, file_)
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_, noMessage = False):
if(self._users.has_key(username)):
user = self._users[username]
if(not noMessage):
self.__displayModUserMessage(username, room, file_, user)
else:
self.addUser(username, room, file_)
def __addUserWithFileToList(self, rooms, user):
file_key = '\'{}\' ({})'.format(user.file['name'], self.ui.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):
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)
return rooms
def __addDifferentFileMessageIfNecessary(self, user, message):
if(self.currentUser.file):
fileHasSameSizeAsYour = user.file['size'] != self.currentUser.file['size']
differentFileMessage = " (but their file size is different from yours!)"
message += differentFileMessage if not fileHasSameSizeAsYour else ""
return message
def __displayFileWatchersInRoomList(self, key, users):
self.ui.showMessage("\t\tFile: {} is being played by:".format(key), True, True)
for user in sorted(users.itervalues()):
message = user.username
message = self.__addDifferentFileMessageIfNecessary(user, message)
self.ui.showMessage("\t\t\t<" + message + ">", True, True)
def __displayPeopleInRoomWithNoFile(self, noFileList):
if (noFileList):
self.ui.showMessage("\t\tPeople who are not playing any file:", True, True)
for user in sorted(noFileList.itervalues()):
self.ui.showMessage("\t\t\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)
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)
def formatTime(self, value):
weeks = value // 604800
days = (value % 604800) // 86400
hours = (value % 86400) // 3600
minutes = (value % 3600) // 60
seconds = value % 60
if(weeks > 0):
return '{0:.0f}w, {1:.0f}d, {2:02.0f}:{3:02.0f}:{4:02.0f}'.format(weeks, days, hours, minutes, seconds)
elif(days > 0):
return '{0:.0f}d, {1:02.0f}:{2:02.0f}:{3:02.0f}'.format(days, hours, minutes, seconds)
elif(hours > 0):
return '{0:02.0f}:{1:02.0f}:{2:02.0f}'.format(hours, minutes, seconds)
else:
return '{0:02.0f}:{1:02.0f}'.format(minutes, seconds)

61
syncplay/clientManager.py Normal file
View File

@ -0,0 +1,61 @@
from syncplay.client import SyncplayClient
import sys
from syncplay.ui.ConfigurationGetter import ConfigurationGetter, InvalidConfigValue
from syncplay import ui
try:
from syncplay.players.mpc import MPCHCAPIPlayer
except ImportError:
MPCHCAPIPlayer = None
from syncplay.players.mplayer import MplayerPlayer
try:
from syncplay.ui.GuiConfiguration import GuiConfiguration
except ImportError:
GuiConfiguration = None
class SyncplayClientManager(object):
def __init__(self):
self._prepareArguments()
self.interface = ui.getUi(graphical = not self.args.no_gui)
self._checkAndSaveConfiguration()
syncplayClient = None
if(self.argsGetter.playerType == "mpc"):
syncplayClient = SyncplayClient(MPCHCAPIPlayer, self.interface, self.args)
elif(self.argsGetter.playerType == "mplayer"):
syncplayClient = SyncplayClient(MplayerPlayer, self.interface, self.args)
if(syncplayClient):
self.interface.addClient(syncplayClient)
syncplayClient.start(self.args.host, self.args.port)
else:
self.interface.showErrorMessage("Unable to start client")
def _checkAndSaveConfiguration(self):
try:
self._promptForMissingArguments()
self.argsGetter.saveValuesIntoConfigFile()
except InvalidConfigValue:
self._checkAndSaveConfiguration()
except Exception, e:
print e.message
sys.exit()
def _prepareArguments(self):
self.argsGetter = ConfigurationGetter()
self.args = self.argsGetter.getConfiguration()
def _guiPromptForMissingArguments(self):
if(GuiConfiguration):
self.args = GuiConfiguration(self.args, self.args.force_gui_prompt).getProcessedConfiguration()
def _promptForMissingArguments(self):
if(self.args.no_gui):
if (self.args.host == None):
self.args.host = self.interface.promptFor(prompt = "Hostname: ", message = "You must supply hostname on the first run, it's easier through command line arguments.")
if (self.args.name == None):
self.args.name = self.interface.promptFor(prompt = "Username: ", message = "You must supply username on the first run, it's easier through command line arguments.")
if (self.args.player_path == None):
self.args.player_path = self.interface.promptFor(prompt = "Player executable: ", message = "You must supply path to your player on the first run, it's easier through command line arguments.")
else:
self._guiPromptForMissingArguments()

View File

View File

@ -0,0 +1,22 @@
class BasePlayer(object):
def askForStatus(self):
raise NotImplementedError()
def displayMessage(self):
raise NotImplementedError()
def drop(self):
raise NotImplementedError()
@staticmethod
def run(client, playerPath, filePath, args):
raise NotImplementedError()
def setPaused(self):
raise NotImplementedError()
def setPosition(self):
raise NotImplementedError()
def setSpeed(self):
raise NotImplementedError()

439
syncplay/players/mpc.py Normal file
View File

@ -0,0 +1,439 @@
#coding:utf8
import time
import threading
import thread
import win32con, win32api, win32gui, ctypes, ctypes.wintypes #@UnresolvedImport @UnusedImport
from functools import wraps
from syncplay.players.basePlayer import BasePlayer
class MPCHCAPIPlayer(BasePlayer):
speedSupported = False
def __init__(self, client):
self.__client = client
self._mpcApi = MpcHcApi()
self._mpcApi.callbacks.onUpdateFilename = lambda _: self.__makePing()
self._mpcApi.callbacks.onMpcClosed = lambda _: self.__client.stop(False)
self._mpcApi.callbacks.onFileStateChange = lambda _: self.__lockAsking()
self._mpcApi.callbacks.onUpdatePlaystate = lambda _: self.__unlockAsking()
self._mpcApi.callbacks.onGetCurrentPosition = lambda _: self.__onGetPosition()
self._mpcApi.callbacks.onVersion = lambda _: self.__versionUpdate.set()
self._mpcApi.callbacks.onSeek = None
self._mpcApi.callbacks.onUpdatePath = None
self._mpcApi.callbacks.onUpdateFileDuration = None
self.__switchPauseCalls = False
self.__preventAsking = threading.Event()
self.__positionUpdate = threading.Event()
self.__versionUpdate = threading.Event()
self.__fileUpdate = threading.RLock()
self.__versionUpdate.clear()
def drop(self):
self.__preventAsking.set()
self.__positionUpdate.set()
self.__versionUpdate.set()
self._mpcApi.sendRawCommand(MpcHcApi.CMD_CLOSEAPP, "")
@staticmethod
def run(client, playerPath, filePath, args):
mpc = MPCHCAPIPlayer(client)
mpc._mpcApi.callbacks.onConnected = lambda: mpc.initPlayer(filePath if(filePath) else None)
mpc._mpcApi.startMpc(playerPath, args)
return mpc
def __lockAsking(self):
self.__preventAsking.clear()
def __unlockAsking(self):
self.__preventAsking.set()
def __onGetPosition(self):
self.__positionUpdate.set()
def setSpeed(self, value):
pass
def __dropIfNotSufficientVersion(self):
self._mpcApi.askForVersion()
if(not self.__versionUpdate.wait(0.1)):
self.__mpcError("MPC version not sufficient, please use `mpc-hc` >= `1.6.4`")
def __testMpcReady(self):
if(not self.__preventAsking.wait(10)):
raise Exception("Player failed opening file")
def __makePing(self):
try:
self.__testMpcReady()
self._mpcApi.callbacks.onUpdateFilename = lambda _: self.__handleUpdatedFilename()
self.__client.initPlayer(self)
self.__handleUpdatedFilename()
self.askForStatus()
except Exception, err:
self.__client.ui.showErrorMessage(err.message)
self.__client.stop()
def initPlayer(self, filePath):
self.__dropIfNotSufficientVersion()
self.__mpcVersion = self._mpcApi.version.split('.')
if(self.__mpcVersion[0:3] == ['1', '6', '4']):
self.__switchPauseCalls = True
if(filePath):
self._mpcApi.openFile(filePath)
def displayMessage(self, message):
self._mpcApi.sendOsd(message, 2, 3000)
def setPaused(self, value):
try:
if self.__switchPauseCalls:
value = not value
if value:
self._mpcApi.pause()
else:
self._mpcApi.unpause()
except MpcHcApi.PlayerNotReadyException:
self.setPaused(value)
def setPosition(self, value):
try:
self._mpcApi.seek(value)
except MpcHcApi.PlayerNotReadyException:
self.setPosition(value)
def __getPosition(self):
self.__positionUpdate.clear()
self._mpcApi.askForCurrentPosition()
self.__positionUpdate.wait()
return self._mpcApi.lastFilePosition
def askForStatus(self):
try:
if(self.__preventAsking.wait(0)):
position = self.__getPosition()
paused = self._mpcApi.isPaused()
position = float(position)
if(self.__fileUpdate.acquire(0)):
self.__client.updatePlayerStatus(paused, position)
self.__fileUpdate.release()
return
self.__echoGlobalStatus()
except MpcHcApi.PlayerNotReadyException:
self.askForStatus()
def __echoGlobalStatus(self):
self.__client.updatePlayerStatus(self.__client.getGlobalPaused(), self.__client.getGlobalPosition())
def __forcePause(self, paused):
i = 0
while(paused <> self._mpcApi.isPaused() and i < 15):
i+=1
self.setPaused(paused)
time.sleep(0.1)
time.sleep(0.1)
if(paused <> self._mpcApi.isPaused()):
self.__forcePause(paused)
def __setUpStateForNewlyOpenedFile(self):
try:
self.__forcePause(self.__client.getGlobalPaused())
self._mpcApi.seek(self.__client.getGlobalPosition())
time.sleep(0.1)
except MpcHcApi.PlayerNotReadyException:
time.sleep(0.1)
self.__setUpStateForNewlyOpenedFile()
def __handleUpdatedFilename(self):
with self.__fileUpdate:
self.__setUpStateForNewlyOpenedFile()
self.__client.updateFile(self._mpcApi.filePlaying, self._mpcApi.fileDuration, self._mpcApi.filePath)
def __mpcError(self, err=""):
self.__client.ui.showErrorMessage(err)
self.__client.stop()
class MpcHcApi:
def __init__(self):
self.callbacks = self.__Callbacks()
self.loadState = None
self.playState = None
self.filePlaying = None
self.fileDuration = None
self.filePath = None
self.lastFilePosition = None
self.version = None
self.__playpause_warden = False
self.__locks = self.__Locks()
self.__mpcExistenceChecking = threading.Thread(target = self.__mpcReadyInSlaveMode, name ="Check MPC window")
self.__mpcExistenceChecking.setDaemon(True)
self.__listener = self.__Listener(self, self.__locks)
self.__listener.setDaemon(True)
self.__listener.start()
self.__locks.listenerStart.wait()
def waitForFileStateReady(f): #@NoSelf
@wraps(f)
def wrapper(self, *args, **kwds):
if(not self.__locks.fileReady.wait(0.2)):
raise self.PlayerNotReadyException()
return f(self, *args, **kwds)
return wrapper
def startMpc(self, path, args = ()):
args = "%s /slave %s" % (" ".join(args), str(self.__listener.hwnd))
win32api.ShellExecute(0, "open", path, args, None, 1)
if(not self.__locks.mpcStart.wait(10)):
raise self.NoSlaveDetectedException("Unable to start MPC in slave mode!")
self.__mpcExistenceChecking.start()
def openFile(self, filePath):
self.__listener.SendCommand(self.CMD_OPENFILE, filePath)
def isPaused(self):
return (self.playState <> self.__MPC_PLAYSTATE.PS_PLAY and self.playState <> None)
def askForVersion(self):
self.__listener.SendCommand(self.CMD_GETVERSION)
@waitForFileStateReady
def pause(self):
self.__listener.SendCommand(self.CMD_PAUSE)
@waitForFileStateReady
def unpause(self):
self.__listener.SendCommand(self.CMD_PLAY)
@waitForFileStateReady
def askForCurrentPosition(self):
self.__listener.SendCommand(self.CMD_GETCURRENTPOSITION)
@waitForFileStateReady
def seek(self, position):
self.__listener.SendCommand(self.CMD_SETPOSITION, unicode(position))
def sendOsd(self, message, MsgPos = 2, DurationMs = 3000):
class __OSDDATASTRUCT(ctypes.Structure):
_fields_ = [
('nMsgPos', ctypes.c_int32),
('nDurationMS', ctypes.c_int32),
('strMsg', ctypes.c_wchar * (len(message)+1))
]
cmessage = __OSDDATASTRUCT()
cmessage.nMsgPos = MsgPos
cmessage.nDurationMS = DurationMs
cmessage.strMsg = message
self.__listener.SendCommand(self.CMD_OSDSHOWMESSAGE, cmessage)
def sendRawCommand(self, cmd, value):
self.__listener.SendCommand(cmd, value)
def handleCommand(self,cmd, value):
if (cmd == self.CMD_CONNECT):
self.__listener.mpcHandle = int(value)
self.__locks.mpcStart.set()
if(self.callbacks.onConnected):
thread.start_new_thread(self.callbacks.onConnected, ())
elif(cmd == self.CMD_STATE):
self.loadState = int(value)
fileNotReady = self.loadState == self.__MPC_LOADSTATE.MLS_CLOSING or self.loadState == self.__MPC_LOADSTATE.MLS_LOADING
if(fileNotReady):
self.playState = None
self.__locks.fileReady.clear()
else:
self.__locks.fileReady.set()
if(self.callbacks.onFileStateChange):
thread.start_new_thread(self.callbacks.onFileStateChange, (self.loadState,))
elif(cmd == self.CMD_PLAYMODE):
self.playState = int(value)
if(self.callbacks.onUpdatePlaystate):
thread.start_new_thread(self.callbacks.onUpdatePlaystate,(self.playState,))
elif(cmd == self.CMD_NOWPLAYING):
value = value.split('|')
self.filePath = value[3]
self.filePlaying = value[3].split('\\').pop()
self.fileDuration = float(value[4])
if(self.callbacks.onUpdatePath):
thread.start_new_thread(self.callbacks.onUpdatePath,(self.onUpdatePath,))
if(self.callbacks.onUpdateFilename):
thread.start_new_thread(self.callbacks.onUpdateFilename,(self.filePlaying,))
if(self.callbacks.onUpdateFileDuration):
thread.start_new_thread(self.callbacks.onUpdateFileDuration,(self.fileDuration,))
elif(cmd == self.CMD_CURRENTPOSITION):
self.lastFilePosition = float(value)
if(self.callbacks.onGetCurrentPosition):
thread.start_new_thread(self.callbacks.onGetCurrentPosition,(self.lastFilePosition,))
elif(cmd == self.CMD_NOTIFYSEEK):
if(self.lastFilePosition <> float(value)): #Notify seek is sometimes sent twice
self.lastFilePosition = float(value)
if(self.callbacks.onSeek):
thread.start_new_thread(self.callbacks.onSeek,(self.lastFilePosition,))
elif(cmd == self.CMD_DISCONNECT):
if(self.callbacks.onMpcClosed):
thread.start_new_thread(self.callbacks.onMpcClosed,(None,))
elif(cmd == self.CMD_VERSION):
if(self.callbacks.onVersion):
self.version = value
thread.start_new_thread(self.callbacks.onVersion,(value,))
class PlayerNotReadyException(Exception):
pass
class __Callbacks:
def __init__(self):
self.onConnected = None
self.onSeek = None
self.onUpdatePath = None
self.onUpdateFilename = None
self.onUpdateFileDuration = None
self.onGetCurrentPosition = None
self.onUpdatePlaystate = None
self.onFileStateChange = None
self.onMpcClosed = None
self.onVersion = None
class __Locks:
def __init__(self):
self.listenerStart = threading.Event()
self.mpcStart = threading.Event()
self.fileReady = threading.Event()
def __mpcReadyInSlaveMode(self):
while(True):
time.sleep(10)
if not win32gui.IsWindow(self.__listener.mpcHandle):
if(self.callbacks.onMpcClosed):
self.callbacks.onMpcClosed(None)
break
CMD_CONNECT = 0x50000000
CMD_STATE = 0x50000001
CMD_PLAYMODE = 0x50000002
CMD_NOWPLAYING = 0x50000003
CMD_LISTSUBTITLETRACKS = 0x50000004
CMD_LISTAUDIOTRACKS = 0x50000005
CMD_CURRENTPOSITION = 0x50000007
CMD_NOTIFYSEEK = 0x50000008
CMD_NOTIFYENDOFSTREAM = 0x50000009
CMD_PLAYLIST = 0x50000006
CMD_OPENFILE = 0xA0000000
CMD_STOP = 0xA0000001
CMD_CLOSEFILE = 0xA0000002
CMD_PLAYPAUSE = 0xA0000003
CMD_ADDTOPLAYLIST = 0xA0001000
CMD_CLEARPLAYLIST = 0xA0001001
CMD_STARTPLAYLIST = 0xA0001002
CMD_REMOVEFROMPLAYLIST = 0xA0001003 # TODO
CMD_SETPOSITION = 0xA0002000
CMD_SETAUDIODELAY = 0xA0002001
CMD_SETSUBTITLEDELAY = 0xA0002002
CMD_SETINDEXPLAYLIST = 0xA0002003 # DOESNT WORK
CMD_SETAUDIOTRACK = 0xA0002004
CMD_SETSUBTITLETRACK = 0xA0002005
CMD_GETSUBTITLETRACKS = 0xA0003000
CMD_GETCURRENTPOSITION = 0xA0003004
CMD_JUMPOFNSECONDS = 0xA0003005
CMD_GETAUDIOTRACKS = 0xA0003001
CMD_GETNOWPLAYING = 0xA0003002
CMD_GETPLAYLIST = 0xA0003003
CMD_TOGGLEFULLSCREEN = 0xA0004000
CMD_JUMPFORWARDMED = 0xA0004001
CMD_JUMPBACKWARDMED = 0xA0004002
CMD_INCREASEVOLUME = 0xA0004003
CMD_DECREASEVOLUME = 0xA0004004
CMD_SHADER_TOGGLE = 0xA0004005
CMD_CLOSEAPP = 0xA0004006
CMD_OSDSHOWMESSAGE = 0xA0005000
CMD_VERSION = 0x5000000A
CMD_DISCONNECT = 0x5000000B
CMD_PLAY = 0xA0000004
CMD_PAUSE = 0xA0000005
CMD_GETVERSION = 0xA0003006
class __MPC_LOADSTATE:
MLS_CLOSED = 0
MLS_LOADING = 1
MLS_LOADED = 2
MLS_CLOSING = 3
class __MPC_PLAYSTATE:
PS_PLAY = 0
PS_PAUSE = 1
PS_STOP = 2
PS_UNUSED = 3
class __Listener(threading.Thread):
def __init__(self, mpcApi, locks):
self.__mpcApi = mpcApi
self.locks = locks
self.mpcHandle = None
self.hwnd = None
self.__PCOPYDATASTRUCT = ctypes.POINTER(self.__COPYDATASTRUCT)
threading.Thread.__init__(self, name="MPC Listener")
def run(self):
message_map = {
win32con.WM_COPYDATA: self.OnCopyData
}
wc = win32gui.WNDCLASS()
wc.lpfnWndProc = message_map
wc.lpszClassName = 'MPCApiListener'
hinst = wc.hInstance = win32api.GetModuleHandle(None)
classAtom = win32gui.RegisterClass(wc)
self.hwnd = win32gui.CreateWindow (
classAtom,
"ListenerGUI",
0,
0,
0,
win32con.CW_USEDEFAULT,
win32con.CW_USEDEFAULT,
0,
0,
hinst,
None
)
self.locks.listenerStart.set()
win32gui.PumpMessages()
def OnCopyData(self, hwnd, msg, wparam, lparam):
pCDS = ctypes.cast(lparam, self.__PCOPYDATASTRUCT)
#print "API:\tin>\t 0x%X\t" % int(pCDS.contents.dwData), ctypes.wstring_at(pCDS.contents.lpData)
self.__mpcApi.handleCommand(pCDS.contents.dwData, ctypes.wstring_at(pCDS.contents.lpData))
def SendCommand(self, cmd, message = u''):
#print "API:\t<out\t 0x%X\t" % int(cmd), message
if not win32gui.IsWindow(self.mpcHandle):
if(self.__mpcApi.callbacks.onMpcClosed):
self.__mpcApi.callbacks.onMpcClosed(None)
cs = self.__COPYDATASTRUCT()
cs.dwData = cmd;
if(isinstance(message, (unicode, str))):
message = ctypes.create_unicode_buffer(message, len(message)+1)
elif(isinstance(message, ctypes.Structure)):
pass
else:
raise TypeError
cs.lpData = ctypes.addressof(message)
cs.cbData = ctypes.sizeof(message)
ptr= ctypes.addressof(cs)
win32api.SendMessage(self.mpcHandle, win32con.WM_COPYDATA, self.hwnd, ptr)
class __COPYDATASTRUCT(ctypes.Structure):
_fields_ = [
('dwData', ctypes.wintypes.LPARAM),
('cbData', ctypes.wintypes.DWORD),
('lpData', ctypes.c_void_p)
]

153
syncplay/players/mplayer.py Normal file
View File

@ -0,0 +1,153 @@
import subprocess
import re
import threading
from syncplay.players.basePlayer import BasePlayer
class MplayerPlayer(BasePlayer):
speedSupported = True
RE_ANSWER = re.compile('^ANS_([a-zA-Z_]+)=(.+)$')
def __init__(self, client, playerPath, filePath):
self._client = client
self._paused = None
self._duration = None
self._filename = None
self._filepath = None
_process = subprocess.Popen([playerPath, filePath, '-slave', '-msglevel', 'all=1:global=4'], stdout=subprocess.PIPE, stdin=subprocess.PIPE)
self._listener = self.__Listener(self, _process)
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 _onFileUpdate(self):
self._fileUpdateClearEvents()
self._getFilename()
self._getLength()
self._getFilepath()
self._fileUpdateWaitEvents()
self._client.updateFile(self._filename, self._duration, self._filepath)
def _preparePlayer(self):
self.setPaused(self._client.getGlobalPaused())
self.setPosition(self._client.getGlobalPosition())
self._client.initPlayer(self)
self._onFileUpdate()
def askForStatus(self):
self._positionAsk.clear()
self._pausedAsk.clear()
self._getPaused()
self._getPosition()
self._positionAsk.wait()
self._pausedAsk.wait()
self._client.updatePlayerStatus(self._paused, self._position)
def _setProperty(self, property_, value):
self._listener.sendLine("set_property {} {}".format(property_, value))
def _getProperty(self, property_):
self._listener.sendLine("get_property {}".format(property_))
def displayMessage(self, message):
self._listener.sendLine('osd_show_text "{!s}" {} {}'.format(message, 3000, 1))
def setSpeed(self, value):
self._setProperty('speed', "{:.2}".format(value))
def setPosition(self, value):
self._position = value
self._setProperty('time_pos', "{:.2}".format(value))
def setPaused(self, value):
self._paused = value
self._setProperty('pause', 'yes' if value else 'no')
def _getFilename(self):
self._getProperty('filename')
def _getLength(self):
self._getProperty('length')
def _getFilepath(self):
self._getProperty('path')
def _getPaused(self):
self._getProperty('pause')
def _getPosition(self):
self._getProperty('time_pos')
def lineReceived(self, line):
match = self.RE_ANSWER.match(line)
if not match:
return
name, value = match.group(1).lower(), match.group(2)
if(name == "time_pos"):
self._position = float(value)
self._positionAsk.set()
elif(name == "pause"):
self._paused = bool(value == 'yes')
self._pausedAsk.set()
elif(name == "length"):
self._duration = float(value)
self._durationAsk.set()
elif(name == "path"):
self._filepath = value
self._pathAsk.set()
elif(name == "filename"):
self._filename = value
self._filenameAsk.set()
@staticmethod
def run(client, playerPath, filePath, args):
mplayer = MplayerPlayer(client, playerPath, filePath)
return mplayer
def drop(self):
self._listener.sendLine('quit')
self._durationAsk.set()
self._filenameAsk.set()
self._pathAsk.set()
self._positionAsk.set()
self._pausedAsk.set()
self._client.stop()
class __Listener(threading.Thread):
def __init__(self, playerController, playerProcess):
self.__playerController = playerController
self.__process = playerProcess
threading.Thread.__init__(self, name="MPlayer Listener")
def run(self):
while(self.__process.poll() is None):
line = self.__process.stdout.readline()
line = line.rstrip("\r\n")
self.__playerController.lineReceived(line)
self.__playerController.drop()
def sendLine(self, line):
try:
self.__process.stdin.write(line + "\n")
except IOError:
pass

389
syncplay/protocols.py Normal file
View File

@ -0,0 +1,389 @@
#coding:utf8
from twisted.protocols.basic import LineReceiver
import json
import syncplay
from functools import wraps
import time
class JSONCommandProtocol(LineReceiver):
def handleMessages(self, messages):
for message in messages.iteritems():
command = message[0]
if command == "Hello":
self.handleHello(message[1])
elif command == "Set":
self.handleSet(message[1])
elif command == "List":
self.handleList(message[1])
elif command == "State":
self.handleState(message[1])
elif command == "Error":
self.handleState(message[1])
else:
self.dropWithError("Unknown Command") #TODO: log, not drop
def printReceived(self, line): #TODO: remove
# print ">>i", line
pass
def printSent(self, line):
# print "o<<", line
pass
def lineReceived(self, line):
line = line.strip()
if not line:
return
self.printReceived(line)
try:
messages = json.loads(line)
except:
self.dropWithError("Not a json encoded string")
return
self.handleMessages(messages)
def sendMessage(self, dict_):
line = json.dumps(dict_)
self.printSent(line)
self.sendLine(line)
def drop(self):
self.transport.loseConnection()
def dropWithError(self, error):
raise NotImplementedError()
class SyncClientProtocol(JSONCommandProtocol):
def __init__(self, client):
self._client = client
self.clientIgnoringOnTheFly = 0
self.serverIgnoringOnTheFly = 0
self.logged = False
def connectionMade(self):
self._client.initProtocol(self)
self.sendHello()
def connectionLost(self, reason):
self._client.destroyProtocol()
def dropWithError(self, error):
self._client.ui.showErrorMessage(error)
self._client.protocolFactory.stopRetrying()
self.drop()
def _extractHelloArguments(self, hello):
username = hello["username"] if hello.has_key("username") else None
roomName = hello["room"]["name"] if hello.has_key("room") else None
version = hello["version"] if hello.has_key("version") else None
return username, roomName, version
def handleHello(self, hello):
username, roomName, version = self._extractHelloArguments(hello)
if(not username or not roomName or not version):
self.dropWithError("Not enough Hello arguments")
elif(version <> syncplay.version): #TODO: change to check only subversion
self.dropWithError("Mismatch between versions of client and server")
else:
self._client.setUsername(username)
self._client.setRoom(roomName)
message = "You have joined room {} as <{}>".format(roomName, username)
self._client.ui.showMessage(message)
self.logged = True
self._client.sendFile()
def sendHello(self):
hello = {}
hello["username"] = self._client.getUsername()
password = self._client.getPassword()
if(password): hello["password"] = password
room = self._client.getRoom()
if(room): hello["room"] = {"name" :room}
hello["version"] = syncplay.version
self.sendMessage({"Hello": hello})
def _SetUser(self, users):
for user in users.iteritems():
username = user[0]
settings = user[1]
room = settings["room"]["name"] if settings.has_key("room") else None
file_ = settings["file"] if settings.has_key("file") else None
if(settings.has_key("event")):
if(settings["event"].has_key("joined")):
self._client.userlist.addUser(username, room, file_)
elif(settings["event"].has_key("left")):
self._client.userlist.removeUser(username)
else:
self._client.userlist.modUser(username, room, file_)
def handleSet(self, settings):
for set_ in settings.iteritems():
command = set_[0]
if command == "room":
roomName = set_[1]["name"] if set_[1].has_key("name") else None
self._client.setRoom(roomName)
elif command == "user":
self._SetUser(set_[1])
def sendSet(self, setting):
self.sendMessage({"Set": setting})
def sendRoomSetting(self, roomName, password=None):
setting = {}
setting["name"] = roomName
if(password): setting["password"] = password
self.sendSet({"room": setting})
def sendFileSetting(self, file_):
self.sendSet({"file": file_})
def handleList(self, userList):
for room in userList.iteritems():
roomName = room[0]
for user in room[1].iteritems():
userName = user[0]
file_ = user[1] if user[1] <> {} else None
self._client.userlist.addUser(userName, roomName, file_, noMessage=True)
self._client.userlist.showUserList()
def sendList(self):
self.sendMessage({"List": None})
def _extractStatePlaystateArguments(self, state):
position = state["playstate"]["position"] if state["playstate"].has_key("position") else 0
paused = state["playstate"]["paused"] if state["playstate"].has_key("paused") else None
doSeek = state["playstate"]["doSeek"] if state["playstate"].has_key("doSeek") else None
setBy = state["playstate"]["setBy"] if state["playstate"].has_key("setBy") else None
return position, paused, doSeek, setBy
def _handleStatePing(self, state):
yourLatency = state["ping"]["yourLatency"] if state["ping"].has_key("yourLatency") else 0
senderLatency = state["ping"]["senderLatency"] if state["ping"].has_key("senderLatency") else 0
if (state["ping"].has_key("latencyCalculation")):
latencyCalculation = state["ping"]["latencyCalculation"]
return yourLatency, senderLatency, latencyCalculation
def handleState(self, state):
position, paused, doSeek, setBy = None, None, None, None
yourLatency, senderLatency = 0, 0
if(state.has_key("ignoringOnTheFly")):
ignore = state["ignoringOnTheFly"]
if(ignore.has_key("server")):
self.serverIgnoringOnTheFly = ignore["server"]
self.clientIgnoringOnTheFly = 0
elif(ignore.has_key("client")):
if(ignore['client']) == self.clientIgnoringOnTheFly:
self.clientIgnoringOnTheFly = 0
if(state.has_key("playstate")):
position, paused, doSeek, setBy = self._extractStatePlaystateArguments(state)
if(state.has_key("ping")):
yourLatency, senderLatency, latencyCalculation = self._handleStatePing(state)
if(position is not None and paused is not None and not self.clientIgnoringOnTheFly):
latency = yourLatency + senderLatency
self._client.updateGlobalState(position, paused, doSeek, setBy, latency)
position, paused, doSeek, stateChange = self._client.getLocalState()
self.sendState(position, paused, doSeek, latencyCalculation, stateChange)
def sendState(self, position, paused, doSeek, latencyCalculation, stateChange = False):
state = {}
positionAndPausedIsSet = position is not None and paused is not None
clientIgnoreIsNotSet = self.clientIgnoringOnTheFly == 0 or self.serverIgnoringOnTheFly != 0
if(clientIgnoreIsNotSet and positionAndPausedIsSet):
state["playstate"] = {}
state["playstate"]["position"] = position
state["playstate"]["paused"] = paused
if(doSeek): state["playstate"]["doSeek"] = doSeek
if(latencyCalculation):
state["ping"] = {"latencyCalculation": latencyCalculation}
if(stateChange):
self.clientIgnoringOnTheFly += 1
if(self.serverIgnoringOnTheFly or self.clientIgnoringOnTheFly):
state["ignoringOnTheFly"] = {}
if(self.serverIgnoringOnTheFly):
state["ignoringOnTheFly"]["server"] = self.serverIgnoringOnTheFly
self.serverIgnoringOnTheFly = 0
if(self.clientIgnoringOnTheFly):
state["ignoringOnTheFly"]["client"] = self.clientIgnoringOnTheFly
self.sendMessage({"State": state})
def handleError(self, error):
self.dropWithError(error["message"]) #TODO: more processing and fallbacking
def sendError(self, message):
self.sendMessage({"Error": {"message": message}})
class SyncServerProtocol(JSONCommandProtocol):
def __init__(self, factory):
self._factory = factory
self._logged = False
self.clientIgnoringOnTheFly = 0
self.serverIgnoringOnTheFly = 0
def __hash__(self):
return hash('|'.join((
self.transport.getPeer().host,
str(id(self)),
)))
def requireLogged(f): #@NoSelf
@wraps(f)
def wrapper(self, *args, **kwds):
if(not self._logged):
self.dropWithError("You must be known to server before sending this command")
return f(self, *args, **kwds)
return wrapper
def dropWithError(self, error):
print "Client drop: %s -- %s" % (self.transport.getPeer().host, error)
self.drop()
def connectionLost(self, reason):
self._factory.removeWatcher(self)
def _extractHelloArguments(self, hello):
roomName, roomPassword = None, None
username = hello["username"] if hello.has_key("username") else None
username = username.strip()
serverPassword = hello["password"] if hello.has_key("password") else None
room = hello["room"] if hello.has_key("room") else None
if(room):
roomName = room["name"] if room.has_key("name") else None
roomName = roomName.strip()
roomPassword = room["password"] if room.has_key("password") else None
version = hello["version"] if hello.has_key("version") else None
return username, serverPassword, roomName, roomPassword, version
def _checkPassword(self, serverPassword):
if(self._factory.password):
if(not serverPassword):
self.dropWithError("Password required")
return False
if(serverPassword != self._factory.password):
self.dropWithError("Wrong password supplied")
return False
return True
def handleHello(self, hello):
username, serverPassword, roomName, roomPassword, version = self._extractHelloArguments(hello)
if(not username or not roomName or not version):
self.dropWithError("Not enough Hello arguments")
elif(version <> syncplay.version): #TODO: change to check only major release
self.dropWithError("Mismatch between versions of client and server")
else:
if(not self._checkPassword(serverPassword)):
return
self._factory.addWatcher(self, username, roomName, roomPassword)
self._logged = True
self.sendHello()
self.sendList()
def sendHello(self):
hello = {}
hello["username"] = self._factory.watcherGetUsername(self)
room = self._factory.watcherGetRoom(self)
if(room): hello["room"] = {"name": room}
hello["version"] = syncplay.version
self.sendMessage({"Hello": hello})
@requireLogged
def handleSet(self, settings):
for set_ in settings.iteritems():
command = set_[0]
if command == "room":
roomName = set_[1]["name"] if set_[1].has_key("name") else None
self._factory.watcherSetRoom(self, roomName)
elif command == "file":
self._factory.watcherSetFile(self, set_[1])
def sendSet(self, setting):
self.sendMessage({"Set": setting})
def sendRoomSetting(self, roomName):
self.sendSet({"room": {"name": roomName}})
def sendUserSetting(self, username, roomName, file_, event):
room = {"name": roomName}
user = {}
user[username] = {}
user[username]["room"] = room
if(file_):
user[username]["file"] = file_
if(event):
user[username]["event"] = event
self.sendSet({"user": user})
def _addUserOnList(self, userlist, watcher):
if (not userlist.has_key(watcher.room)):
userlist[watcher.room] = {}
userlist[watcher.room][watcher.name] = watcher.file if watcher.file else {}
def sendList(self):
userlist = {}
watchers = self._factory.getAllWatchers(self)
for watcher in watchers.itervalues():
self._addUserOnList(userlist, watcher)
self.sendMessage({"List": userlist})
@requireLogged
def handleList(self, _):
self.sendList()
def sendState(self, position, paused, doSeek, setBy, senderLatency, watcherLatency, forced = False):
playstate = {
"position": position,
"paused": paused,
"doSeek": doSeek,
"setBy": setBy
}
ping = {
"yourLatency": watcherLatency,
"senderLatency": senderLatency,
"latencyCalculation": time.time()
}
state = {
"ping": ping,
"playstate": playstate,
}
if(forced):
self.serverIgnoringOnTheFly += 1
if(self.serverIgnoringOnTheFly or self.clientIgnoringOnTheFly):
state["ignoringOnTheFly"] = {}
if(self.serverIgnoringOnTheFly):
state["ignoringOnTheFly"]["server"] = self.serverIgnoringOnTheFly
if(self.clientIgnoringOnTheFly):
state["ignoringOnTheFly"]["client"] = self.clientIgnoringOnTheFly
self.clientIgnoringOnTheFly = 0
if(self.serverIgnoringOnTheFly == 0 or forced):
self.sendMessage({"State": state})
def _extractStatePlaystateArguments(self, state):
position = state["playstate"]["position"] if state["playstate"].has_key("position") else 0
paused = state["playstate"]["paused"] if state["playstate"].has_key("paused") else None
doSeek = state["playstate"]["doSeek"] if state["playstate"].has_key("doSeek") else None
return position, paused, doSeek
@requireLogged
def handleState(self, state):
position, paused, doSeek, latencyCalculation = None, None, None, None
if(state.has_key("ignoringOnTheFly")):
ignore = state["ignoringOnTheFly"]
if(ignore.has_key("server")):
if(self.serverIgnoringOnTheFly == ignore["server"]):
self.serverIgnoringOnTheFly = 0
if(ignore.has_key("client")):
self.clientIgnoringOnTheFly = ignore["client"]
if(state.has_key("playstate")):
position, paused, doSeek = self._extractStatePlaystateArguments(state)
if(state.has_key("ping")):
latencyCalculation = state["ping"]["latencyCalculation"] if state["ping"].has_key("latencyCalculation") else None
if(self.serverIgnoringOnTheFly == 0):
self._factory.updateWatcherState(self, position, paused, doSeek, latencyCalculation)
def handleError(self, error):
self.dropWithError(error["message"]) #TODO: more processing and fallbacking
def sendError(self, message):
self.sendMessage({"Error": {"message": message}})

234
syncplay/server.py Normal file
View File

@ -0,0 +1,234 @@
#coding:utf8
#TODO: #12, #13, #8;
import hashlib
from twisted.internet import task, reactor
from twisted.internet.protocol import Factory
import syncplay
from syncplay.protocols import SyncServerProtocol
import time
import threading
class SyncFactory(Factory):
def __init__(self, password = '', banlist = None , isolateRooms = False):
print "Welcome to Syncplay server, ver. {0}".format(syncplay.version)
if(password):
password = hashlib.md5(password).hexdigest()
self.password = password
self._rooms = {}
self._roomStates = {}
self.isolateRooms = isolateRooms
self.roomUpdateLock = threading.RLock()
self.roomUsersLock = threading.RLock()
def buildProtocol(self, addr):
return SyncServerProtocol(self)
def _createRoomIfDoesntExist(self, roomName):
if (not self._rooms.has_key(roomName)):
self._rooms[roomName] = {}
self._roomStates[roomName] = {
"position": 0.0,
"paused": True,
"setBy": None,
"lastUpdate": time.time()
}
def addWatcher(self, watcherProtocol, username, roomName, roomPassword):
allnames = []
for room in self._rooms.itervalues():
for watcher in room.itervalues():
allnames.append(watcher.name.lower())
while username.lower() in allnames:
username += '_'
self._createRoomIfDoesntExist(roomName)
watcher = Watcher(self, watcherProtocol, username, roomName)
self._rooms[roomName][watcherProtocol] = watcher
print "{0}({2}) connected to room '{1}'".format(username, roomName, watcherProtocol.transport.getPeer().host)
reactor.callLater(0.1, watcher.scheduleSendState)
l = lambda w: w.sendUserSetting(username, roomName, None, {"joined": True})
self.broadcast(watcherProtocol, l)
def getWatcher(self, watcherProtocol):
with self.roomUsersLock:
for room in self._rooms.itervalues():
if(room.has_key(watcherProtocol)):
return room[watcherProtocol]
def getAllWatchers(self, watcherProtocol):
with self.roomUsersLock:
if(self.isolateRooms):
room = self.getWatcher(watcherProtocol).room
return self._rooms[room]
else:
watchers = {}
for room in self._rooms.itervalues():
for watcher in room.itervalues():
watchers[watcher.watcherProtocol] = watcher
return watchers
def _removeWatcherFromTheRoom(self, watcherProtocol):
with self.roomUsersLock:
for room in self._rooms.itervalues():
watcher = room.pop(watcherProtocol, None)
return watcher
def _deleteRoomIfEmpty(self, room):
with self.roomUpdateLock:
with self.roomUsersLock:
if (self._rooms[room] == {}):
self._rooms.pop(room)
self._roomStates.pop(room)
def sendState(self, watcherProtocol, doSeek = False, senderLatency = 0, forcedUpdate = False):
watcher = self.getWatcher(watcherProtocol)
if(not watcher):
return
room = watcher.room
position = self._roomStates[room]["position"]
paused = self._roomStates[room]["paused"]
if(not paused):
timePassedSinceSet = time.time() - self._roomStates[room]["lastUpdate"]
position += timePassedSinceSet
setBy = self._roomStates[room]["setBy"]
watcher.paused = paused
watcher.position = position
watcherProtocol.sendState(position, paused, doSeek, setBy, senderLatency, watcher.latency, forcedUpdate)
if(time.time() - watcher.lastUpdate > 4.1):
watcherProtocol.drop()
def __updateWatcherPing(self, latencyCalculation, watcher):
if (latencyCalculation):
ping = (time.time() - latencyCalculation) / 2
if (watcher.latency):
watcher.latency = watcher.latency * (0.85) + ping * (0.15) #Exponential moving average
else:
watcher.latency = ping
def __shouldServerForceUpdateOnRoom(self, pauseChanged, doSeek):
return doSeek or pauseChanged
def __updatePausedState(self, paused, watcher):
watcher.paused = paused
if(self._roomStates[watcher.room]["paused"] <> paused):
self._roomStates[watcher.room]["setBy"] = watcher.name
self._roomStates[watcher.room]["paused"] = paused
self._roomStates[watcher.room]["lastUpdate"] = time.time()
return True
def __updatePositionState(self, position, doSeek, watcher):
watcher.position = position
if (doSeek):
self._roomStates[watcher.room]["position"] = position
self._roomStates[watcher.room]["setBy"] = watcher.name
self._roomStates[watcher.room]["lastUpdate"] = time.time()
else:
setter = min(self._rooms[watcher.room].values())
self._roomStates[watcher.room]["position"] = setter.position
self._roomStates[watcher.room]["setBy"] = setter.name
self._roomStates[watcher.room]["lastUpdate"] = setter.lastUpdate
def updateWatcherState(self, watcherProtocol, position, paused, doSeek, latencyCalculation):
watcher = self.getWatcher(watcherProtocol)
self.__updateWatcherPing(latencyCalculation, watcher)
watcher.lastUpdate = time.time()
with self.roomUpdateLock:
if(watcher.file):
if(position is not None):
self.__updatePositionState(position, doSeek, watcher)
pauseChanged = False
if(paused is not None):
pauseChanged = self.__updatePausedState(paused, watcher)
forceUpdate = self.__shouldServerForceUpdateOnRoom(pauseChanged, doSeek)
if(forceUpdate):
l = lambda w: self.sendState(w, doSeek, watcher.latency, forceUpdate)
self.broadcastRoom(watcher, l)
def removeWatcher(self, watcherProtocol):
with self.roomUsersLock:
watcher = self._removeWatcherFromTheRoom(watcherProtocol)
if(not watcher):
return
watcher.deactivate()
print "{0} left server".format(watcher.name)
self._deleteRoomIfEmpty(watcher.room)
l = lambda w: w.sendUserSetting(watcher.name, watcher.room, None, {"left": True})
self.broadcast(watcherProtocol, l)
def watcherGetUsername(self, watcherProtocol):
return self.getWatcher(watcherProtocol).name
def watcherGetRoom(self, watcherProtocol):
return self.getWatcher(watcherProtocol).room
def watcherSetRoom(self, watcherProtocol, room):
with self.roomUsersLock:
watcher = self._removeWatcherFromTheRoom(watcherProtocol)
if(not watcher):
return
watcher.resetStateTimer()
oldRoom = watcher.room
self._createRoomIfDoesntExist(room)
self._rooms[room][watcherProtocol] = watcher
self._deleteRoomIfEmpty(oldRoom)
if(self.isolateRooms): #this is trick to inform old room about leaving
l = lambda w: w.sendUserSetting(watcher.name, room, watcher.file, None)
self.broadcast(watcherProtocol, l)
watcher.room = room
l = lambda w: w.sendUserSetting(watcher.name, watcher.room, watcher.file, None)
self.broadcast(watcherProtocol, l)
def watcherSetFile(self, watcherProtocol, file_):
watcher = self.getWatcher(watcherProtocol)
watcher.file = file_
l = lambda w: w.sendUserSetting(watcher.name, watcher.room, watcher.file, None)
self.broadcast(watcherProtocol, l)
def broadcastRoom(self, sender, what):
room = self._rooms[self.watcherGetRoom(sender.watcherProtocol)]
if(room):
for receiver in room:
what(receiver)
def broadcast(self, sender, what):
if(self.isolateRooms):
self.broadcastRoom(sender, what)
for room in self._rooms.itervalues():
for receiver in room:
what(receiver)
class Watcher(object):
def __init__(self, factory, watcherProtocol, name, room):
self.factory = factory
self.watcherProtocol = watcherProtocol
self.name = name
self.room = room
self.file = None
self._sendStateTimer = None
self.position = None
self.latency = 0
self.lastUpdate = time.time()
def __lt__(self, b):
if(self.position is None):
return False
elif(b.position is None):
return True
else:
return self.position < b.position
def scheduleSendState(self, when=1):
self._sendStateTimer = task.LoopingCall(self.sendState)
self._sendStateTimer.start(when, True)
def sendState(self):
self.factory.sendState(self.watcherProtocol)
def resetStateTimer(self):
if(self._sendStateTimer):
self._sendStateTimer.reset()
def deactivate(self):
if(self._sendStateTimer):
self._sendStateTimer.stop()

View File

@ -0,0 +1,182 @@
import ConfigParser
import argparse
import os
import sys
class InvalidConfigValue(Exception):
def __init__(self, message):
Exception.__init__(self, message)
class ConfigurationGetter(object):
def __init__(self):
self._config = None
self._args = None
self._syncplayClient = None
self._configFile = None
self._parser = None
self._configName = ".syncplay"
self.playerType = None
def _findWorkingDir(self):
frozen = getattr(sys, 'frozen', '')
if not frozen:
path = os.path.dirname(os.path.dirname(__file__))
elif frozen in ('dll', 'console_exe', 'windows_exe'):
path = os.path.dirname(os.path.dirname(__file__))
else:
path = ""
return path
def _checkForPortableFile(self):
path = self._findWorkingDir()
if(os.path.isfile(os.path.join(path, self._configName))):
return os.path.join(path, self._configName)
def _getConfigurationFilePath(self):
self._configFile = self._checkForPortableFile()
if(not self._configFile):
if(os.name <> 'nt'):
self._configFile = os.path.join(os.getenv('HOME', '.'), self._configName)
else:
self._configFile = os.path.join(os.getenv('APPDATA', '.'), self._configName)
def _prepareArgParser(self):
self._parser = argparse.ArgumentParser(description='Solution to synchronize playback of multiple MPlayer and MPC-HC instances over the network.',
epilog='If no options supplied values from .syncplay file will be used')
self._parser.add_argument('--no-gui', action='store_true', help='show no GUI')
self._parser.add_argument('-a', '--host', metavar='hostname', type=str, help='server\'s address')
self._parser.add_argument('-n', '--name', metavar='username', type=str, help='desired username')
self._parser.add_argument('-d','--debug', action='store_true', help='debug mode')
self._parser.add_argument('-g','--force-gui-prompt', action='store_true', help='make configuration prompt appear')
self._parser.add_argument('--no-store', action='store_true', help='don\'t store values in syncplay.ini')
self._parser.add_argument('-r', '--room', metavar='room', type=str, nargs='?', help='default room')
self._parser.add_argument('-p', '--password', metavar='password', type=str, nargs='?', help='server password')
self._parser.add_argument('--player-path', metavar='path', type=str, help='path to your player executable')
self._parser.add_argument('file', metavar='file', type=str, nargs='?', help='file to play')
self._parser.add_argument('_args', metavar='options', type=str, nargs='*', help='player options, if you need to pass options starting with - prepend them with single \'--\' argument')
def _openConfigFile(self):
if(not self._config):
self._config = ConfigParser.RawConfigParser(allow_no_value=True)
self._config.read(self._configFile)
def _getSectionName(self):
return 'sync' if not self._args.debug else 'debug'
def saveValuesIntoConfigFile(self):
self._splitPortAndHost()
self._openConfigFile()
section_name = self._getSectionName()
self._validateArguments()
if(not self._args.no_store):
with open(self._configFile, 'wb') as configfile:
if(not self._config.has_section(section_name)):
self._config.add_section(section_name)
self._config.set(section_name, 'host', self._args.host)
self._config.set(section_name, 'name', self._args.name)
self._config.set(section_name, 'room', self._args.room)
self._config.set(section_name, 'password', self._args.password)
self._config.set(section_name, 'player_path', self._args.player_path)
self._config.write(configfile)
def _validateArguments(self):
if(not (self._args.host <> "" and self._args.host is not None)):
self._args.host = None
raise InvalidConfigValue("Hostname can't be empty")
if(not (self._args.name <> "" and self._args.name is not None)):
self._args.name = None
raise InvalidConfigValue("Username can't be empty")
if(self._isPlayerMPCAndValid()):
self._addSpecialMPCFlags()
self.playerType = "mpc"
elif(self._isMplayerPathValid()):
self.playerType = "mplayer"
else:
self._args.player_path = None
raise InvalidConfigValue('Path to player is not valid')
def _readConfigValue(self, section_name, name):
try:
return self._config.get(section_name, name)
except ConfigParser.NoOptionError:
return None
def _readMissingValuesFromConfigFile(self):
self._openConfigFile()
section_name = self._getSectionName()
try:
self._valuesToReadFromConfig(section_name)
except ConfigParser.NoSectionError:
pass
def _isPlayerMPCAndValid(self):
if(os.path.isfile(self._args.player_path)):
if(self._args.player_path[-10:] == 'mpc-hc.exe' or self._args.player_path[-12:] == 'mpc-hc64.exe'):
return True
if(os.path.isfile(self._args.player_path + "\\mpc-hc.exe")):
self._args.player_path += "\\mpc-hc.exe"
return True
if(os.path.isfile(self._args.player_path + "\\mpc-hc64.exe")):
self._args.player_path += "\\mpc-hc64.exe"
return True
return False
def _addSpecialMPCFlags(self):
self._args._args.extend(['/open', '/new'])
def _isMplayerPathValid(self):
if("mplayer" in self._args.player_path):
if os.access(self._args.player_path, os.X_OK):
return True
for path in os.environ['PATH'].split(':'):
path = os.path.join(os.path.realpath(path), self._args.player_path)
if os.access(path, os.X_OK):
self._args.player_path = path
return True
return False
def _valuesToReadFromConfig(self, section_name):
if (self._args.host == None):
self._args.host = self._readConfigValue(section_name, 'host')
if (self._args.name == None):
self._args.name = self._readConfigValue(section_name, 'name')
if (self._args.room == None):
self._args.room = self._readConfigValue(section_name, 'room')
if (self._args.password == None):
self._args.password = self._readConfigValue(section_name, 'password')
if (self._args.player_path == None):
self._args.player_path = self._readConfigValue(section_name, 'player_path')
def _splitPortAndHost(self):
if(self._args.host):
if ':' in self._args.host:
self._args.host, port = self._args.host.split(':', 1)
self._args.port = int(port)
else:
self._args.port = 8999
def setConfiguration(self, args):
self._args = args
def getConfiguration(self):
self._getConfigurationFilePath()
self._prepareArgParser()
self._args = self._parser.parse_args()
self._readMissingValuesFromConfigFile()
self._splitPortAndHost()
return self._args
class ServerConfigurationGetter(ConfigurationGetter):
def getConfiguration(self):
self._prepareArgParser()
self._args = self._parser.parse_args()
if(self._args.port == None):
self._args.port = 8999
return self._args
def _prepareArgParser(self):
self._parser = argparse.ArgumentParser(description='Solution to synchronize playback of multiple MPlayer and MPC-HC instances over the network. Server instance',
epilog='If no options supplied _config values will be used')
self._parser.add_argument('--port', metavar='port', type=str, nargs='?', help='server TCP port')
self._parser.add_argument('--password', metavar='password', type=str, nargs='?', help='server password')
self._parser.add_argument('--isolate-rooms', action='store_true', help='should rooms be isolated?')

View File

@ -0,0 +1,109 @@
import pygtk
import os
pygtk.require('2.0')
import gtk
gtk.set_interactive(False)
import cairo, gio, pango, atk, pangocairo, gobject #@UnusedImport
class GuiConfiguration:
def __init__(self, args, force = False):
self.args = args
self.closedAndNotSaved = False
if(args.player_path == None or args.host == None or args.name == None or force):
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
self.window.set_title("Syncplay Configuration")
self.window.connect("delete_event", lambda w, e: self._windowClosed())
vbox = gtk.VBox(False, 0)
self.window.add(vbox)
vbox.show()
self._addLabeledEntries(args, vbox)
self.hostEntry.select_region(0, len(self.hostEntry.get_text()))
button = gtk.Button(stock=gtk.STOCK_SAVE)
button.connect("clicked", lambda w: self._saveDataAndLeave())
vbox.pack_start(button, True, True, 0)
button.set_flags(gtk.CAN_DEFAULT)
button.grab_default()
button.show()
self.window.show()
gtk.main()
def _windowClosed(self):
self.window.destroy()
gtk.main_quit()
self.closedAndNotSaved = True
def _addLabeledEntries(self, args, vbox):
self.hostEntry = self._addLabeledEntryToVbox('Host: ', args.host, vbox, self._focusNext)
self.userEntry = self._addLabeledEntryToVbox('Username: ', args.name, vbox, self._focusNext)
self.roomEntry = self._addLabeledEntryToVbox('Default room (optional): ', args.room, vbox, self._focusNext)
self.passEntry = self._addLabeledEntryToVbox('Server password (optional): ', args.password, vbox, self._focusNext)
self._tryToFillUpMpcPath()
self.mpcEntry = self._addLabeledEntryToVbox('Path to player executable: ', self.args.player_path, vbox, self._focusNext)
def _tryToFillUpMpcPath(self):
if(self.args.player_path == None):
paths = ["C:\Program Files (x86)\MPC-HC\mpc-hc.exe",
"C:\Program Files\MPC-HC\mpc-hc.exe",
"C:\Program Files\MPC-HC\mpc-hc64.exe",
"C:\Program Files\Media Player Classic - Home Cinema\mpc-hc.exe",
"C:\Program Files\Media Player Classic - Home Cinema\mpc-hc64.exe",
"C:\Program Files (x86)\Media Player Classic - Home Cinema\mpc-hc.exe",
"C:\Program Files (x86)\K-Lite Codec Pack\Media Player Classic\mpc-hc.exe",
"C:\Program Files\K-Lite Codec Pack\Media Player Classic\mpc-hc.exe",
"C:\Program Files (x86)\Combined Community Codec Pack\MPC\mpc-hc.exe",
"C:\Program Files\MPC HomeCinema (x64)\mpc-hc64.exe",
]
for path in paths:
if(os.path.isfile(path)):
self.args.player_path = path
return
def getProcessedConfiguration(self):
if(self.closedAndNotSaved):
raise self.WindowClosed
return self.args
def _saveDataAndLeave(self):
self.args.host = self.hostEntry.get_text()
self.args.name = self.userEntry.get_text()
self.args.room = self.roomEntry.get_text()
self.args.password = self.passEntry.get_text()
self.args.player_path = self.mpcEntry.get_text()
self.window.destroy()
gtk.main_quit()
def _focusNext(self, widget, entry):
self.window.get_toplevel().child_focus(gtk.DIR_TAB_FORWARD)
def _addLabeledEntryToVbox(self, label, initialEntryValue, vbox, callback):
hbox = gtk.HBox(False, 0)
hbox.set_border_width(3)
vbox.pack_start(hbox, False, False, 0)
hbox.show()
label_ = gtk.Label()
label_.set_text(label)
label_.set_alignment(xalign=0, yalign=0.5)
hbox.pack_start(label_, False, False, 0)
label_.show()
entry = gtk.Entry()
entry.connect("activate", callback, entry)
if(initialEntryValue == None):
initialEntryValue = ""
entry.set_text(initialEntryValue)
hbox.pack_end(entry, False, False, 0)
entry.set_usize(200, -1)
entry.show()
hbox = gtk.HBox(False, 0)
vbox.add(hbox)
hbox.show()
return entry
class WindowClosed(Exception):
def __init__(self):
Exception.__init__(self)

11
syncplay/ui/__init__.py Normal file
View File

@ -0,0 +1,11 @@
from syncplay.ui.gui import GraphicalUI
from syncplay.ui.consoleUI import ConsoleUI
def getUi(graphical = True):
if(False): #graphical): #TODO: Add graphical ui
ui = GraphicalUI()
else:
ui = ConsoleUI()
ui.setDaemon(True)
ui.start()
return ui

97
syncplay/ui/consoleUI.py Normal file
View File

@ -0,0 +1,97 @@
from __future__ import print_function
import threading
import re
import time
import syncplay
class ConsoleUI(threading.Thread):
def __init__(self):
self.promptMode = threading.Event()
self.PromptResult = ""
self.promptMode.set()
self._syncplayClient = None
threading.Thread.__init__(self, name="ConsoleUI")
def addClient(self, client):
self._syncplayClient = client
def run(self):
try:
while True:
data = raw_input()
data = data.rstrip('\n\r')
if(not self.promptMode.isSet()):
self.PromptResult = data
self.promptMode.set()
elif(self._syncplayClient):
self._executeCommand(data)
except:
self._syncplayClient.protocolFactory.stopRetrying()
pass
def promptFor(self, prompt = ">", message = ""):
if message <> "":
print(message)
self.promptMode.clear()
print(prompt, end='')
self.promptMode.wait()
return self.PromptResult
def showMessage(self, message, noTimestamp):
if(noTimestamp):
print(message)
else:
print(time.strftime("[%X] ", time.localtime()) + message)
def showDebugMessage(self, message):
print(message)
def showErrorMessage(self, message):
print("ERROR:\t" + message)
def __exectueSeekCmd(self, seek_type, minutes, seconds):
self._syncplayClient.playerPositionBeforeLastSeek = self._syncplayClient.player_position
if seek_type == 's':
seconds = int(seconds) if seconds <> None else 0
seconds += int(minutes) * 60 if minutes <> None else 0
self._syncplayClient.setPosition(seconds)
else: #seek_type s+
seconds = int(seconds) if seconds <> None and minutes <> None else 20
seconds += int(minutes) * 60 if minutes <> None else 60
self._syncplayClient.setPosition(self.player_position+seconds)
def _executeCommand(self, data):
RE_SEEK = re.compile("^([\+\-s]+) ?(-?\d+)?([^0-9](\d+))?$")
RE_ROOM = re.compile("^room( (\w+))?")
matched_seek = RE_SEEK.match(data)
matched_room = RE_ROOM.match(data)
if matched_seek :
self.__exectueSeekCmd(matched_seek.group(1), matched_seek.group(2), matched_seek.group(4))
elif matched_room:
room = matched_room.group(2)
if room == None:
if self._syncplayClient.users.currentUser._filename <> None:
room = self._syncplayClient.users.currentUser._filename
else:
room = self._syncplayClient.defaultRoom
self._syncplayClient.protocol.sendRoomSetting(room)
elif data == "r":
tmp_pos = self._syncplayClient.getPlayerPosition()
self._syncplayClient.setPosition(self._syncplayClient.playerPositionBeforeLastSeek)
self._syncplayClient.playerPositionBeforeLastSeek = tmp_pos
elif data == "p":
self._syncplayClient.setPaused(not self._syncplayClient.getPlayerPaused())
elif data == "k": #TODO: remove?
self._syncplayClient.stop()
elif data == 'help':
self.showMessage( "Available commands:" )
self.showMessage( "\thelp - this help" )
self.showMessage( "\ts [time] - seek" )
self.showMessage( "\ts+ [time] - seek to: current position += time" )
self.showMessage( "\tr - revert last seek" )
self.showMessage( "\tp - toggle pause" )
self.showMessage( "\troom [name] - change room" )
self.showMessage("Syncplay version: %s" % syncplay.version)
self.showMessage("More info available at: %s" % syncplay.projectURL)
else:
self.showMessage( "Unrecognized command, type 'help' for list of available commands" )

9
syncplay/ui/gui.py Normal file
View File

@ -0,0 +1,9 @@
'''
Created on 05-07-2012
@author: Uriziel
'''
class GraphicalUI(object):
def __init__(self):
pass

5
syncplayClient.py Normal file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env python
from syncplay.clientManager import SyncplayClientManager
if(__name__ == '__main__'):
SyncplayClientManager()

View File

@ -0,0 +1,9 @@
#!/usr/bin/env python
from syncplay.clientManager import SyncplayClientManager
import sys
if(__name__ == '__main__'):
if not '-g' in sys.argv:
if(not '--force-gui-prompt' in sys.argv):
sys.argv.extend(("-g",))
SyncplayClientManager()

11
syncplayServer.py Normal file
View File

@ -0,0 +1,11 @@
#coding:utf8
from twisted.internet import reactor
from syncplay.server import SyncFactory
from syncplay.ui.ConfigurationGetter import ServerConfigurationGetter
argsGetter = ServerConfigurationGetter()
args = argsGetter.getConfiguration()
reactor.listenTCP(args.port, SyncFactory(args.password, args.isolate_rooms))
reactor.run()