Commits

Mathias Panzenböck committed 51b8fc1

started with websocket based api

Comments (0)

Files changed (1)

contents/code/websocketapi.py

+# -*- coding: utf-8 -*-
+from hashlib import sha1
+from base64 import b64encode
+from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
+
+import struct
+
+from PyQt4.QtCore import QSocketNotifier
+
+from mediaplayer import MediaPlayerBase, \
+	CAN_GO_NEXT, CAN_GO_PREV, CAN_PAUSE, CAN_PLAY, CAN_SEEK, CAN_PROVIDE_METADATA, CAN_HAS_TRACKLIST, \
+	STATUS_PLAYING, STATUS_PAUSED, STATUS_STOPPED, STATUS_MAP
+
+try:
+	import json
+except ImportError:
+	import simplejson as json
+
+CAPABILITIY_MAP = {
+	'go_next': CAN_GO_NEXT,
+	'go_prev': CAN_GO_PREV,
+	'play':    CAN_PLAY,
+	'pause':   CAN_PAUSE,
+	'seek':    CAN_SEEK,
+	'provide_metadata': CAN_PROVIDE_METADATA,
+	'has_tracklist':    CAN_HAS_TRACKLIST
+}
+
+class WebSocketRequestHandler(BaseHTTPRequestHandler):
+    TEXT = 0x01
+    BINARY = 0x02
+
+	protocol_version = 'HTTP/1.1'
+
+	def __init__(self, request, client_address, sever):
+		BaseHTTPRequestHandler.__init__(self, request, client_address, sever)
+		self.__wsconnected = False
+		
+	wsconnected = property(lambda self: self.__wsconnected)
+
+	def send_wsupgrade(self,protocol=None,key=None):
+		if key is None:
+			key = self.headers['Sec-WebSocket-Key']
+
+		accept = b64encode(sha1(key+"258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest())
+
+		self.send_response(101,'Switching Protocols')
+		self.send_header('Upgrade','websocket')
+		self.send_header('Connection','Upgrade')
+		self.send_header('Sec-WebSocket-Accept',accept)
+		if protocol:
+			self.send_header('Sec-WebSocket-Protocol',protocol)
+		self.end_headers()
+
+		self.__wsconnected = True
+	
+	def parse_wsframe(self):
+		# basically from http://www.cs.rpi.edu/~goldsd/docs/spring2012-csci4220/websocket-py.txt
+		
+		buf = self.rfile.read(2)
+		b   = ord(buf[0])
+#		fin = b & 0x80 # 1st bit XXX: unused?
+		# next 3 bits reserved
+		opcode = b & 0x0f # low 4 bits
+		b2 = ord(buf[1])
+		mask   = b2 & 0x80 # high bit of the second byte
+		length = b2 & 0x7f # low 7 bits of the second byte
+
+		if length == 126:
+			length, = struct.unpack(">H", self.rfile.read(2))
+		elif length == 127:
+			length, = struct.unpack(">I", self.rfile.read(4))
+		
+		if mask:
+			mask_bytes = bytearray(self.rfile.read(4))
+
+		payload = self.rfile.read(length)
+
+		if mask:
+			payload = bytearray(mask_bytes[i % 4] ^ ord(b) for b, i in enumerate(payload))
+
+		if opcode == WebSocketRequestHandler.TEXT:
+			payload = payload.decode("UTF-8")
+
+		return payload
+
+	def send_wsframe(self,data):
+		# basically from http://www.cs.rpi.edu/~goldsd/docs/spring2012-csci4220/websocket-py.txt
+
+		b1 = 0x80
+		if isinstance(data, unicode):
+			b1 |= WebSocketRequestHandler.TEXT
+			payload = data.encode("UTF-8")
+		elif isinstance(data, (str,bytearray)):
+			b1 |= WebSocketRequestHandler.BINARY
+			payload = data
+		else:
+			raise TypeError, data
+
+		buf = bytearray()
+		buf.append(b1)
+
+		# never mask frames from the server to the client
+		length = len(payload)
+		if length < 126:
+			buf.append(length)
+		elif length < (2 ** 16) - 1:
+			buf.append(126)
+			buf.extend(struct.pack(">H", length))
+		else:
+			buf.append(127)
+			buf.extend(struct.pack(">Q", length))
+
+		buf.extend(payload)
+		self.wfile.write(buf)
+
+	def finish(self):
+		if not self.wfile.closed:
+			self.wfile.flush()
+		if not self.__wsconnected:
+			self.wfile.close()
+			self.rfile.close()
+
+	def wsfinish(self):
+		if not self.wfile.closed:
+			self.wfile.flush()
+		self.wfile.close()
+		self.rfile.close()
+
+class PlayerAPIRequestHandler(WebSocketRequestHandler):
+	__slots__ = 'notifier', 'player'
+
+	def __init__(self, request, client_address, sever):
+		WebSocketRequestHandler.__init__(self, request, client_address, sever)
+		self.notifier = None
+		self.player   = None
+
+	def do_GET(self):
+		if self.path == "/connect":
+			self.send_wsupgrade('mediaplayer-control')
+
+			self.notifier = QSocketNotifier(self.rfile.fileno(), QSocketNotifier.Read)
+			self.notifier.activated.connect(self.incoming)
+		else:
+			self.send_error(404)
+
+	def incoming(self,fd):
+		self.notifier.setEnabled(False)
+		try:
+			data = self.parse_wsframe()
+			if isinstance(data,bytearray):
+				data = str(data)
+			event = json.loads(data)
+			cmd = event['type']
+			action = getattr(self,'_do_player_'+cmd,None)
+			if action is not None:
+				action(event)
+
+		finally:
+			self.notifier.setEnabled(True)
+	
+	def _do_player_connect(self,event):
+		name = 'ws://'+':'.join(self.client_address)
+		identity = event['player']
+		icon = event.get('icon',None)
+		caps = event.get('capabilities',None)
+		if caps is None:
+			capabilities = CAN_PLAY | CAN_PAUSE
+		else:
+			capabilities = 0
+			for cap in caps:
+				capabilities |= CAPABILITIY_MAP.get(cap.lower(),0)
+		if self.player:
+			self.do_player_disconnect({'type':'disconnect'})
+		self.player = PlayerAPIMediaPlayer(self,name,identity,icon,capabilities)
+		self.do_player_connect(self.player)
+		
+	def _do_player_disconnect(self,event):
+		try:
+			self.do_player_disconnect(self.player)
+		finally:
+			self.player = None
+		
+	def _do_player_statuschange(self,event):
+		status = STATUS_MAP.get(event['status'].lower(),None)
+		if status is not None and self.player:
+			self.player.setStatus(status)
+			self.do_player_statuschange(status)
+	
+	def do_player_connect(self,player):
+		pass
+		
+	def do_player_disconnect(self,player):
+		pass
+		
+	def do_player_statuschange(self,status):
+		pass
+
+class PlayerAPIServer(HTTPServer):
+	__slots__ = 'notifier',
+
+	def __init__(self,address,handler):
+		HTTPServer.__init__(self,address,handler)
+		self.notifier = QSocketNotifier(self.fileno(), QSocketNotifier.Read)
+		self.notifier.activated.connect(incoming)
+	
+	def incoming(self,fd):
+		self.notifier.setEnabled(False)
+		try:
+			self.handle_request()
+		finally:
+			self.notifier.setEnabled(True)
+
+class PlayerAPIMediaPlayer(MediaPlayerBase):
+	__slots__ = 'request', '__identity', '__icon', '__capabilities', '__status'
+
+	def __init__(self,request,name,identity,icon,capabilities):
+		MediaPlayerBase.__init__(self,None,name,self)
+		self.request = request
+		self.__identity = identity
+		self.__icon     = icon
+		self.__capabilities = capabilities
+		self.__status   = STATUS_STOPPED
+
+	def setStatus(self,status):
+		self.__status = status
+	
+	def icon(self,identity=None):
+		return self.__icon
+	
+	def identity(self):
+		return self.__identity
+	
+	def capabilities(self):
+		return self.__capabilities
+	
+	def status(self):
+		return self.__status
+
+	def playing(self):
+		return self.__status == STATUS_PLAYING
+	
+	def _call(self,name):
+		self.request.send_wsframe(json.dumps({'type':name}))
+
+	playPause = lambda self: self._call('playPause')
+	play      = lambda self: self._call('play')
+	pause     = lambda self: self._call('pause')
+	quit      = lambda self: self._call('quit')
+	next      = lambda self: self._call('next')
+	prev      = lambda self: self._call('prev')
+	backward  = lambda self: self._call('backward')
+	forward   = lambda self: self._call('forward')
+