From 4fda98492b34c8351b71bce0989439557d96c6b0 Mon Sep 17 00:00:00 2001 From: Uriziel Date: Fri, 29 Jun 2012 21:59:32 +0200 Subject: [PATCH] Fixes on mpc-api --- sync_mpc_api.py | 11 +- syncplay/client.py | 1 + syncplay/mpc_api.py | 266 +++++++++++------------------- syncplay/players/mpc_using_api.py | 86 +++++++--- 4 files changed, 165 insertions(+), 199 deletions(-) diff --git a/sync_mpc_api.py b/sync_mpc_api.py index e8dc19a..468d320 100644 --- a/sync_mpc_api.py +++ b/sync_mpc_api.py @@ -15,7 +15,10 @@ def prepare_args(args): args.args.extend(['/open', '/new']) if __name__ == '__main__': - args = utils.get_configuration() - prepare_args(args) - manager = client.Manager(args.host, args.port, args.name, lambda m: mpc_using_api.run_mpc(m, args.mpc_path, args.file, args.args)) - manager.start() + try: + args = utils.get_configuration() + prepare_args(args) + manager = client.Manager(args.host, args.port, args.name, lambda m: mpc_using_api.run_mpc(m, args.mpc_path, args.file, args.args)) + manager.start() + finally: + manager.stop() diff --git a/syncplay/client.py b/syncplay/client.py index 92b111b..e5d041b 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -212,6 +212,7 @@ class Manager(object): command_handler = threading.Thread(target = utils.stdin_thread, args = (self,), name = "Command handler") command_handler.setDaemon(True) command_handler.start() + def start(self): if self.running: return diff --git a/syncplay/mpc_api.py b/syncplay/mpc_api.py index def1f86..b0274af 100644 --- a/syncplay/mpc_api.py +++ b/syncplay/mpc_api.py @@ -1,29 +1,22 @@ #coding:utf8 +import threading,time,thread import win32con, win32api, win32gui, ctypes, ctypes.wintypes -import os, thread class MPC_API: def __init__(self, enforce_custom_handler = False): - self.enforce_custom_handler = enforce_custom_handler - self.__listener = None - thread.start_new_thread(self.__Listener, (self,)) - while(self.__listener == None): continue - self.__position_request_warden = False - self.__seek_warden = False - self.__playpause_warden = False ''' List of callbacks that can be set - Each callback by default receives a tuple argument, can be empty though - on_connected - on_seek - on_update_filename - on_update_file_duration - on_update_position - on_update_playstate - on_file_ready - custom_handler + on_connected (0 args) + on_seek (0 args) + on_update_filename (filename) + on_update_file_duration (duration) + on_update_position (positon) + on_update_playstate (playstate) + on_file_ready (filename) + custom_handler (cmd, value) ''' + self.enforce_custom_handler = enforce_custom_handler self.callbacks = self.__CALLBACKS() self.loadstate = None self.playstate = None @@ -33,30 +26,33 @@ class MPC_API: Most likely won't be up to date unless you ask API to refresh it ''' self.lastfileposition = None + self.__playpause_warden = False + self.__locks = self.__LOCKS() + self.__mpc_ready_checking = threading.Thread(target = self.__mpc_ready_in_slave_mode, name ="Check MPC window") + self.__mpc_ready_checking.setDaemon(True) + self.__listener = self.__Listener(self, self.__locks) + self.__listener.setDaemon(True) + self.__listener.start() + self.__locks.listner_start.wait() + + - ''' - This is called from another thread and if it could it would be a private method - Developers should not bother with it - ''' - def register_listener(self, listener): - self.__listener = listener - - ''' Given a path fo mpc-hc.exe and optional additional arguments in tuple will start mpc-hc in a slave mode ''' def start_mpc(self, path, args = ()): - is_starting = os.spawnl(os.P_NOWAIT, path, ' ' + ' '.join(args),'/slave %s ' % str(self.__listener.hwnd)) #can be switched with win32api.ShellExecute - while(self.__listener.mpc_handle == None and is_starting): continue - + args = "%s /slave %s" % (" ".join(args), str(self.__listener.hwnd)) + win32api.ShellExecute(0, "open", path, args, None, 1) + if(not self.__locks.mpc_start.wait(10)): + raise MPC_API.NoSlaveDetectedException("Unable to start MPC in slave mode!") + self.__mpc_ready_checking.start() ''' Checks if api is ready to receive commands Throws MPC_API.NoSlaveDetectedException if mpc window is not found ''' def is_api_ready(self): - self.__mpc_ready_in_slave_mode() file_state_ok = self.loadstate == self.__MPC_LOADSTATE.MLS_CLOSED or self.loadstate == self.__MPC_LOADSTATE.MLS_LOADED or self.loadstate == None listener_ok = self.__listener <> None and self.__listener.mpc_handle <> None return (file_state_ok and listener_ok) @@ -66,7 +62,6 @@ class MPC_API: Throws MPC_API.NoSlaveDetectedException if mpc window is not found ''' def is_file_ready(self): - self.__mpc_ready_in_slave_mode() return (self.loadstate == self.__MPC_LOADSTATE.MLS_LOADED and self.fileplaying and self.playstate <> None) ''' @@ -74,7 +69,6 @@ class MPC_API: Throws MPC_API.NoSlaveDetectedException if mpc window is not found ''' def open_file(self, file_path): - self.__mpc_ready_in_slave_mode() self.__listener.SendCommand(MPC_API_COMMANDS.CMD_OPENFILE, file_path) ''' @@ -82,7 +76,6 @@ class MPC_API: Throws MPC_API.NoSlaveDetectedException if mpc window is not found ''' def is_paused(self): - self.__mpc_ready_in_slave_mode() return (self.playstate <> self.__MPC_PLAYSTATE.PS_PLAY and self.playstate <> None) ''' @@ -90,39 +83,31 @@ class MPC_API: Throws MPC_API.NoSlaveDetectedException if mpc window is not found ''' def pause(self): - self.__mpc_ready_in_slave_mode() - while(self.playstate == None):self.__mpc_ready_in_slave_mode() - if(not self.is_paused() and self.__playpause_warden == False): - self.__playpause_warden = True - self.__listener.SendCommand(MPC_API_COMMANDS.CMD_PLAYPAUSE) - while(not self.is_paused()): self.__mpc_ready_in_slave_mode() - self.__playpause_warden = False - + if(not self.is_file_ready()): raise MPC_API.PlayerNotReadyException("Playstate change on no file") + if(not self.is_paused()): + self.playpause() ''' Play paused file Throws MPC_API.NoSlaveDetectedException if mpc window is not found ''' def unpause(self): - self.__mpc_ready_in_slave_mode() - while(self.playstate == None):self.__mpc_ready_in_slave_mode() - if(self.is_paused() and self.__playpause_warden == False): - self.__playpause_warden = True - self.__listener.SendCommand(MPC_API_COMMANDS.CMD_PLAYPAUSE) - while(self.is_paused()): self.__mpc_ready_in_slave_mode() - self.__playpause_warden = False + if(not self.is_file_ready()): raise MPC_API.PlayerNotReadyException("Playstate change on no file") + if(self.is_paused()): + self.playpause() ''' Toggle play/pause Throws MPC_API.NoSlaveDetectedException if mpc window is not found ''' def playpause(self): - self.__mpc_ready_in_slave_mode() + if(not self.is_file_ready()): raise MPC_API.PlayerNotReadyException("Playstate change on no file") tmp = self.playstate - while(self.playstate == None):self.__mpc_ready_in_slave_mode() if(self.__playpause_warden == False): self.__playpause_warden = True self.__listener.SendCommand(MPC_API_COMMANDS.CMD_PLAYPAUSE) - while(tmp == self.playstate): self.__mpc_ready_in_slave_mode() - self.__playpause_warden = False + while(tmp == self.playstate and self.__playpause_warden): continue + self.__playpause_warden = False + if(tmp == self.playstate): self.playpause() #playstate changed manualy after issuing a command + ''' Asks mpc for it's current file position, if on_update_position callback is set @@ -130,22 +115,23 @@ class MPC_API: Throws MPC_API.NoSlaveDetectedException if mpc window is not found ''' def ask_for_current_position(self): - self.__mpc_ready_in_slave_mode() - if(not self.__position_request_warden): - self.__position_request_warden = True - self.__listener.SendCommand(MPC_API_COMMANDS.CMD_GETCURRENTPOSITION) - while(self.__position_request_warden and not self.callbacks.on_update_position): self.__mpc_ready_in_slave_mode() - return self.lastfileposition + if(not self.is_file_ready()): + raise MPC_API.PlayerNotReadyException("File not yet ready") + self.__locks.positionget.clear() + self.__listener.SendCommand(MPC_API_COMMANDS.CMD_GETCURRENTPOSITION) + if(not self.callbacks.on_update_position): + if(not self.__locks.positionget.wait(0.2)): + raise MPC_API.PlayerNotReadyException("Position get fail") + return self.lastfileposition ''' Given a position in seconds will ask client to seek there Throws MPC_API.NoSlaveDetectedException if mpc window is not found ''' def seek(self, position): - self.__mpc_ready_in_slave_mode() - self.__seek_warden = True + self.__locks.seek.clear() self.__listener.SendCommand(MPC_API_COMMANDS.CMD_SETPOSITION, unicode(position)) - while(self.__seek_warden): self.__mpc_ready_in_slave_mode() - + if(not self.__locks.seek.wait(0.2)): + raise MPC_API.PlayerNotReadyException("Seek fail") ''' @param message: unicode string to display in player @param MsgPos: Either 1, left top corner or 2, right top corner, defaults to 2 @@ -153,7 +139,6 @@ class MPC_API: Throws MPC_API.NoSlaveDetectedException if mpc window is not found ''' def send_osd(self, message, MsgPos = 2, DurationMs = 3000): - self.__mpc_ready_in_slave_mode() class __OSDDATASTRUCT(ctypes.Structure): _fields_ = [ ('nMsgPos', ctypes.c_int32), @@ -173,7 +158,6 @@ class MPC_API: Throws MPC_API.NoSlaveDetectedException if mpc window is not found ''' def send_raw_command(self, cmd, value): - self.__mpc_ready_in_slave_mode() self.__listener.SendCommand(cmd, value) ''' @@ -184,44 +168,49 @@ class MPC_API: ''' def handle_command(self,cmd, value, enforce_custom_handler = False): if((self.enforce_custom_handler or enforce_custom_handler) and self.callbacks.custom_handler <> None): - self.callbacks.custom_handler((cmd, value,)) + thread.start_new_thread(self.callbacks.custom_handler,(cmd, value,)) else: if (cmd == MPC_API_COMMANDS.CMD_CONNECT): self.__listener.mpc_handle = int(value) + self.__locks.mpc_start.set() if(self.callbacks.on_connected): - self.callbacks.on_connected(()) + thread.start_new_thread(self.callbacks.on_connected, ()) elif(cmd == MPC_API_COMMANDS.CMD_STATE): self.loadstate = int(value) elif(cmd == MPC_API_COMMANDS.CMD_PLAYMODE): self.playstate = int(value) - if(self.callbacks.on_update_playstate): self.callbacks.on_update_playstate((self.playstate,)) + if(self.callbacks.on_update_playstate): thread.start_new_thread(self.callbacks.on_update_playstate,(self.playstate,)) elif(cmd == MPC_API_COMMANDS.CMD_NOWPLAYING): - if(self.callbacks.on_file_ready): self.callbacks.on_file_ready(()) self.fileplaying = value.split('|')[3].split('\\').pop() - if(self.callbacks.on_update_filename): self.callbacks.on_update_filename((self.fileplaying,)) + if(self.callbacks.on_update_filename): thread.start_new_thread(self.callbacks.on_update_filename,(self.fileplaying,)) self.fileduration = int(value.split('|')[4]) - if(self.callbacks.on_update_file_duration): self.callbacks.on_update_file_duration((self.fileplaying,)) - + if(self.callbacks.on_update_file_duration): thread.start_new_thread(self.callbacks.on_update_file_duration,(self.fileplaying,)) + if(self.callbacks.on_file_ready): thread.start_new_thread(self.callbacks.on_file_ready, ()) + elif(cmd == MPC_API_COMMANDS.CMD_CURRENTPOSITION): self.lastfileposition = float(value) - self.__position_request_warden = False - if(self.callbacks.on_update_position): self.callbacks.on_update_position((self.lastfileposition,)) + self.__locks.positionget.set() + if(self.callbacks.on_update_position): thread.start_new_thread(self.callbacks.on_update_position,(self.lastfileposition,)) elif(cmd == MPC_API_COMMANDS.CMD_NOTIFYSEEK): - self.__seek_warden = False + self.__locks.seek.set() if(self.lastfileposition <> float(value)): #Notify seek is sometimes sent twice self.lastfileposition = float(value) - if(self.callbacks.on_seek): self.callbacks.on_seek((self.lastfileposition,)) + if(self.callbacks.on_seek): thread.start_new_thread(self.callbacks.on_seek,(self.lastfileposition,)) else: if(self.callbacks.custom_handler <> None): - self.callbacks.custom_handler((cmd, value,)) + thread.start_new_thread(self.callbacks.custom_handler,(cmd, value,)) class NoSlaveDetectedException(Exception): def __init__(self, message): Exception.__init__(self, message) + class PlayerNotReadyException(Exception): + def __init__(self, message): + Exception.__init__(self, message) + class __CALLBACKS: def __init__(self): @@ -233,12 +222,22 @@ class MPC_API: self.on_update_playstate = None self.on_file_ready = None self.custom_handler = None + self.on_mpc_closed = None + + class __LOCKS: + def __init__(self): + self.listner_start = threading.Event() + self.mpc_start = threading.Event() + self.positionget = threading.Event() + self.seek = threading.Event() - def __mpc_ready_in_slave_mode(self): - if not win32gui.IsWindow(self.__listener.mpc_handle): - raise MPC_API.NoSlaveDetectedException("MPC Slave Window not detected") - + while(True): + time.sleep(1) + if not win32gui.IsWindow(self.__listener.mpc_handle): + self.callbacks.on_mpc_closed() + break + class __MPC_LOADSTATE: MLS_CLOSED = 0 MLS_LOADING = 1 @@ -259,9 +258,16 @@ class MPC_API: ''' - class __Listener: - def __init__(self, mpc_api): + class __Listener(threading.Thread): + def __init__(self, mpc_api, locks): + self.__mpc_api = mpc_api + self.locks = locks self.mpc_handle = 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 } @@ -283,11 +289,9 @@ class MPC_API: hinst, None ) - self.__PCOPYDATASTRUCT = ctypes.POINTER(self.__COPYDATASTRUCT) - self.__mpc_api = mpc_api - - mpc_api.register_listener(self) + self.locks.listner_start.set() win32gui.PumpMessages() + def OnCopyData(self, hwnd, msg, wparam, lparam): pCDS = ctypes.cast(lparam, self.__PCOPYDATASTRUCT) @@ -321,130 +325,44 @@ class MPC_API: ] - class MPC_API_COMMANDS(): - # Send after connection - # Par 1 : MPC window handle (command should be send to this HWnd) CMD_CONNECT = 0x50000000 - # Send when opening or closing file - # Par 1 : current state (see MPC_LOADSTATE enum) CMD_STATE = 0x50000001 - # Send when playing, pausing or closing file - # Par 1 : current play mode (see MPC_PLAYSTATE enum) CMD_PLAYMODE = 0x50000002 - # Send after opening a new file - # Par 1 : title - # Par 2 : author - # Par 3 : description - # Par 4 : complete filename (path included) - # Par 5 : duration in seconds CMD_NOWPLAYING = 0x50000003 - # List of subtitle tracks - # Par 1 : Subtitle track name 0 - # Par 2 : Subtitle track name 1 - # ... - # Par n : Active subtitle track, -1 if subtitles disabled - # - # if no subtitle track present, returns -1 - # if no file loaded, returns -2 CMD_LISTSUBTITLETRACKS = 0x50000004 - # List of audio tracks - # Par 1 : Audio track name 0 - # Par 2 : Audio track name 1 - # ... - # Par n : Active audio track - # - # if no audio track present, returns -1 - # if no file loaded, returns -2 CMD_LISTAUDIOTRACKS = 0x50000005 - # Send current playback position in responce - # of CMD_GETCURRENTPOSITION. - # Par 1 : current position in seconds CMD_CURRENTPOSITION = 0x50000007 - # Send the current playback position after a jump. - # (Automatically sent after a seek event). - # Par 1 : new playback position (in seconds). CMD_NOTIFYSEEK = 0x50000008 - # Notify the end of current playback - # (Automatically sent). - # Par 1 : none. CMD_NOTIFYENDOFSTREAM = 0x50000009 - # List of files in the playlist - # Par 1 : file path 0 - # Par 2 : file path 1 - # ... - # Par n : active file, -1 if no active file CMD_PLAYLIST = 0x50000006 - # ==== Commands from host to MPC - # Open new file - # Par 1 : file path CMD_OPENFILE = 0xA0000000 - # Stop playback, but keep file / playlist CMD_STOP = 0xA0000001 - # Stop playback and close file / playlist CMD_CLOSEFILE = 0xA0000002 - # Pause or restart playback CMD_PLAYPAUSE = 0xA0000003 - # Add a new file to playlist (did not start playing) - # Par 1 : file path CMD_ADDTOPLAYLIST = 0xA0001000 - # Remove all files from playlist CMD_CLEARPLAYLIST = 0xA0001001 - # Start playing playlist CMD_STARTPLAYLIST = 0xA0001002 CMD_REMOVEFROMPLAYLIST = 0xA0001003 # TODO - # Cue current file to specific position - # Par 1 : new position in seconds CMD_SETPOSITION = 0xA0002000 - # Set the audio delay - # Par 1 : new audio delay in ms CMD_SETAUDIODELAY = 0xA0002001 - # Set the subtitle delay - # Par 1 : new subtitle delay in ms CMD_SETSUBTITLEDELAY = 0xA0002002 - # Set the active file in the playlist - # Par 1 : index of the active file, -1 for no file selected - # DOESNT WORK - CMD_SETINDEXPLAYLIST = 0xA0002003 - # Set the audio track - # Par 1 : index of the audio track + CMD_SETINDEXPLAYLIST = 0xA0002003 # DOESNT WORK CMD_SETAUDIOTRACK = 0xA0002004 - # Set the subtitle track - # Par 1 : index of the subtitle track, -1 for disabling subtitles CMD_SETSUBTITLETRACK = 0xA0002005 - # Ask for a list of the subtitles tracks of the file - # return a CMD_LISTSUBTITLETRACKS CMD_GETSUBTITLETRACKS = 0xA0003000 - # Ask for the current playback position, - # see CMD_CURRENTPOSITION. - # Par 1 : current position in seconds CMD_GETCURRENTPOSITION = 0xA0003004 - # Jump forward/backward of N seconds, - # Par 1 : seconds (negative values for backward) CMD_JUMPOFNSECONDS = 0xA0003005 - # Ask for a list of the audio tracks of the file - # return a CMD_LISTAUDIOTRACKS CMD_GETAUDIOTRACKS = 0xA0003001 - # Ask for the properties of the current loaded file - # return a CMD_NOWPLAYING CMD_GETNOWPLAYING = 0xA0003002 - # Ask for the current playlist - # return a CMD_PLAYLIST CMD_GETPLAYLIST = 0xA0003003 - # Toggle FullScreen CMD_TOGGLEFULLSCREEN = 0xA0004000 - # Jump forward(medium) CMD_JUMPFORWARDMED = 0xA0004001 - # Jump backward(medium) CMD_JUMPBACKWARDMED = 0xA0004002 - # Increase Volume CMD_INCREASEVOLUME = 0xA0004003 - # Decrease volume CMD_DECREASEVOLUME = 0xA0004004 - # Shader toggle CMD_SHADER_TOGGLE = 0xA0004005 - # Close App CMD_CLOSEAPP = 0xA0004006 - # show host defined OSD message string CMD_OSDSHOWMESSAGE = 0xA0005000 - \ No newline at end of file + + \ No newline at end of file diff --git a/syncplay/players/mpc_using_api.py b/syncplay/players/mpc_using_api.py index 0ae58e1..7ec86fd 100644 --- a/syncplay/players/mpc_using_api.py +++ b/syncplay/players/mpc_using_api.py @@ -1,6 +1,7 @@ #coding:utf8 + from ..mpc_api import MPC_API -from twisted.internet import reactor +import time class MPCHCAPIPlayer(object): def __init__(self, manager): @@ -11,24 +12,37 @@ class MPCHCAPIPlayer(object): self.tmp_filename = None self.tmp_position = None - self.mpc_api.callbacks.on_file_ready = lambda _: reactor.callLater(0.7,self.make_ping) + self.mpc_api.callbacks.on_file_ready = lambda: self.make_ping() + self.mpc_api.callbacks.on_mpc_closed = lambda: self.mpc_error("MPC closed") + + self.semaphore_filename = False + def drop(self): pass def set_speed(self, value): pass + def test_mpc_ready(self): + try: + self.mpc_api.ask_for_current_position() + except MPC_API.PlayerNotReadyException: + time.sleep(0.1) + self.test_mpc_ready() + return + def make_ping(self): + self.mpc_api.callbacks.on_file_ready = None + self.test_mpc_ready() self.manager.init_player(self) self.pinged = True self.ask_for_status() - self.mpc_api.callbacks.on_file_ready = None def display_message(self, message): try: self.mpc_api.send_osd(message) - except: - self.mpc_error() + except Exception, err: + self.mpc_error(err) def set_paused(self, value): try: @@ -36,49 +50,79 @@ class MPCHCAPIPlayer(object): self.mpc_api.pause() else: self.mpc_api.unpause() - except: - self.mpc_error() + except MPC_API.PlayerNotReadyException: + time.sleep(0.2) + self.set_paused(value) + return + except Exception, err: + self.mpc_error(err) def set_position(self, value): try: self.mpc_api.seek(value) - except: - self.mpc_error() + except MPC_API.PlayerNotReadyException: + self.set_position(value) + return + except Exception, err: + self.mpc_error(err) def ask_for_status(self): position = self.tmp_position if self.tmp_position else 0 paused = None try: - if(self.mpc_api.is_file_ready()): + if(self.mpc_api.is_file_ready() and not self.semaphore_filename): + try: + position = self.mpc_api.ask_for_current_position() + except MPC_API.PlayerNotReadyException: + time.sleep(0.1) + self.ask_for_status() + return if(self.tmp_filename <> self.mpc_api.fileplaying): self.handle_updated_filename(self.mpc_api.fileplaying) return - position = self.mpc_api.ask_for_current_position() paused = self.mpc_api.is_paused() position = float(position) self.tmp_position = position self.manager.update_player_status(paused, position) else: self.manager.update_player_status(True, self.manager.get_global_position()) - except MPC_API.NoSlaveDetectedException: - self.mpc_error() - - def handle_updated_filename(self,filename): + except Exception, err: + self.mpc_error(err) + + def __force_pause(self, filename, position): + self.set_paused(True) + time.sleep(0.1) + if (not self.mpc_api.is_paused()): + self.__set_up_newly_opened_file(filename, position) + + def __set_up_newly_opened_file(self, filename, position): + self.test_mpc_ready() + try: + self.mpc_api.seek(position) + except MPC_API.PlayerNotReadyException: + time.sleep(0.1) + self.__set_up_newly_opened_file(filename, position) + self.__force_pause(filename, position) + + def handle_updated_filename(self, filename): position = self.manager.get_global_position() + if(self.semaphore_filename): + self.manager.update_player_status(True, position) + return + self.semaphore_filename = True + self.__set_up_newly_opened_file(filename, position) self.tmp_filename = filename self.manager.update_filename(str(self.tmp_filename)) - self.mpc_api.seek(position) - reactor.callLater(0.7, self.set_paused, True) self.manager.update_player_status(True, position) + self.semaphore_filename = False - def mpc_error(self): + def mpc_error(self, err=""): + print "ERROR:", str(err) + ',', "desu" if self.manager.running: print 'Failed to connect to MPC-HC API!' self.manager.stop() - def run_mpc(manager, mpc_path, file_path, args): mpc = MPCHCAPIPlayer(manager) - mpc.mpc_api.callbacks.on_connected = lambda _: mpc.mpc_api.open_file(file_path) if(file_path) else None + mpc.mpc_api.callbacks.on_connected = lambda: mpc.mpc_api.open_file(file_path) if(file_path) else None mpc.mpc_api.start_mpc(mpc_path, args) -