commit
da94041f2d
@ -0,0 +1,694 @@
|
||||
'''
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2013 Dave P.
|
||||
'''
|
||||
import sys
|
||||
VER = sys.version_info[0]
|
||||
if VER >= 3:
|
||||
import socketserver
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
from io import StringIO, BytesIO
|
||||
else:
|
||||
import SocketServer
|
||||
from BaseHTTPServer import BaseHTTPRequestHandler
|
||||
from StringIO import StringIO
|
||||
|
||||
import hashlib
|
||||
import base64
|
||||
import socket
|
||||
import struct
|
||||
import ssl
|
||||
import errno
|
||||
import codecs
|
||||
from collections import deque
|
||||
from select import select
|
||||
|
||||
__all__ = ['WebSocket',
|
||||
'SimpleWebSocketServer',
|
||||
'SimpleSSLWebSocketServer']
|
||||
|
||||
def _check_unicode(val):
|
||||
if VER >= 3:
|
||||
return isinstance(val, str)
|
||||
else:
|
||||
return isinstance(val, unicode)
|
||||
|
||||
class HTTPRequest(BaseHTTPRequestHandler):
|
||||
def __init__(self, request_text):
|
||||
if VER >= 3:
|
||||
self.rfile = BytesIO(request_text)
|
||||
else:
|
||||
self.rfile = StringIO(request_text)
|
||||
self.raw_requestline = self.rfile.readline()
|
||||
self.error_code = self.error_message = None
|
||||
self.parse_request()
|
||||
|
||||
_VALID_STATUS_CODES = [1000, 1001, 1002, 1003, 1007, 1008,
|
||||
1009, 1010, 1011, 3000, 3999, 4000, 4999]
|
||||
|
||||
HANDSHAKE_STR = (
|
||||
"HTTP/1.1 101 Switching Protocols\r\n"
|
||||
"Upgrade: WebSocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Accept: %(acceptstr)s\r\n\r\n"
|
||||
)
|
||||
|
||||
GUID_STR = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
||||
|
||||
STREAM = 0x0
|
||||
TEXT = 0x1
|
||||
BINARY = 0x2
|
||||
CLOSE = 0x8
|
||||
PING = 0x9
|
||||
PONG = 0xA
|
||||
|
||||
HEADERB1 = 1
|
||||
HEADERB2 = 3
|
||||
LENGTHSHORT = 4
|
||||
LENGTHLONG = 5
|
||||
MASK = 6
|
||||
PAYLOAD = 7
|
||||
|
||||
MAXHEADER = 65536
|
||||
MAXPAYLOAD = 33554432
|
||||
|
||||
class WebSocket(object):
|
||||
|
||||
def __init__(self, server, sock, address):
|
||||
self.server = server
|
||||
self.client = sock
|
||||
self.address = address
|
||||
|
||||
self.handshaked = False
|
||||
self.headerbuffer = bytearray()
|
||||
self.headertoread = 2048
|
||||
|
||||
self.fin = 0
|
||||
self.data = bytearray()
|
||||
self.opcode = 0
|
||||
self.hasmask = 0
|
||||
self.maskarray = None
|
||||
self.length = 0
|
||||
self.lengtharray = None
|
||||
self.index = 0
|
||||
self.request = None
|
||||
self.usingssl = False
|
||||
|
||||
self.frag_start = False
|
||||
self.frag_type = BINARY
|
||||
self.frag_buffer = None
|
||||
self.frag_decoder = codecs.getincrementaldecoder('utf-8')(errors='strict')
|
||||
self.closed = False
|
||||
self.sendq = deque()
|
||||
|
||||
self.state = HEADERB1
|
||||
|
||||
# restrict the size of header and payload for security reasons
|
||||
self.maxheader = MAXHEADER
|
||||
self.maxpayload = MAXPAYLOAD
|
||||
|
||||
def handleMessage(self):
|
||||
"""
|
||||
Called when websocket frame is received.
|
||||
To access the frame data call self.data.
|
||||
|
||||
If the frame is Text then self.data is a unicode object.
|
||||
If the frame is Binary then self.data is a bytearray object.
|
||||
"""
|
||||
pass
|
||||
|
||||
def handleConnected(self):
|
||||
"""
|
||||
Called when a websocket client connects to the server.
|
||||
"""
|
||||
pass
|
||||
|
||||
def handleClose(self):
|
||||
"""
|
||||
Called when a websocket server gets a Close frame from a client.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _handlePacket(self):
|
||||
if self.opcode == CLOSE:
|
||||
pass
|
||||
elif self.opcode == STREAM:
|
||||
pass
|
||||
elif self.opcode == TEXT:
|
||||
pass
|
||||
elif self.opcode == BINARY:
|
||||
pass
|
||||
elif self.opcode == PONG or self.opcode == PING:
|
||||
if len(self.data) > 125:
|
||||
raise Exception('control frame length can not be > 125')
|
||||
else:
|
||||
# unknown or reserved opcode so just close
|
||||
raise Exception('unknown opcode')
|
||||
|
||||
if self.opcode == CLOSE:
|
||||
status = 1000
|
||||
reason = u''
|
||||
length = len(self.data)
|
||||
|
||||
if length == 0:
|
||||
pass
|
||||
elif length >= 2:
|
||||
status = struct.unpack_from('!H', self.data[:2])[0]
|
||||
reason = self.data[2:]
|
||||
|
||||
if status not in _VALID_STATUS_CODES:
|
||||
status = 1002
|
||||
|
||||
if len(reason) > 0:
|
||||
try:
|
||||
reason = reason.decode('utf8', errors='strict')
|
||||
except:
|
||||
status = 1002
|
||||
else:
|
||||
status = 1002
|
||||
|
||||
self.close(status, reason)
|
||||
return
|
||||
|
||||
elif self.fin == 0:
|
||||
if self.opcode != STREAM:
|
||||
if self.opcode == PING or self.opcode == PONG:
|
||||
raise Exception('control messages can not be fragmented')
|
||||
|
||||
self.frag_type = self.opcode
|
||||
self.frag_start = True
|
||||
self.frag_decoder.reset()
|
||||
|
||||
if self.frag_type == TEXT:
|
||||
self.frag_buffer = []
|
||||
utf_str = self.frag_decoder.decode(self.data, final = False)
|
||||
if utf_str:
|
||||
self.frag_buffer.append(utf_str)
|
||||
else:
|
||||
self.frag_buffer = bytearray()
|
||||
self.frag_buffer.extend(self.data)
|
||||
|
||||
else:
|
||||
if self.frag_start is False:
|
||||
raise Exception('fragmentation protocol error')
|
||||
|
||||
if self.frag_type == TEXT:
|
||||
utf_str = self.frag_decoder.decode(self.data, final = False)
|
||||
if utf_str:
|
||||
self.frag_buffer.append(utf_str)
|
||||
else:
|
||||
self.frag_buffer.extend(self.data)
|
||||
|
||||
else:
|
||||
if self.opcode == STREAM:
|
||||
if self.frag_start is False:
|
||||
raise Exception('fragmentation protocol error')
|
||||
|
||||
if self.frag_type == TEXT:
|
||||
utf_str = self.frag_decoder.decode(self.data, final = True)
|
||||
self.frag_buffer.append(utf_str)
|
||||
self.data = u''.join(self.frag_buffer)
|
||||
else:
|
||||
self.frag_buffer.extend(self.data)
|
||||
self.data = self.frag_buffer
|
||||
|
||||
self.handleMessage()
|
||||
|
||||
self.frag_decoder.reset()
|
||||
self.frag_type = BINARY
|
||||
self.frag_start = False
|
||||
self.frag_buffer = None
|
||||
|
||||
elif self.opcode == PING:
|
||||
self._sendMessage(False, PONG, self.data)
|
||||
|
||||
elif self.opcode == PONG:
|
||||
pass
|
||||
|
||||
else:
|
||||
if self.frag_start is True:
|
||||
raise Exception('fragmentation protocol error')
|
||||
|
||||
if self.opcode == TEXT:
|
||||
try:
|
||||
self.data = self.data.decode('utf8', errors='strict')
|
||||
except Exception as exp:
|
||||
raise Exception('invalid utf-8 payload')
|
||||
|
||||
self.handleMessage()
|
||||
|
||||
|
||||
def _handleData(self):
|
||||
# do the HTTP header and handshake
|
||||
if self.handshaked is False:
|
||||
|
||||
data = self.client.recv(self.headertoread)
|
||||
if not data:
|
||||
raise Exception('remote socket closed')
|
||||
|
||||
else:
|
||||
# accumulate
|
||||
self.headerbuffer.extend(data)
|
||||
|
||||
if len(self.headerbuffer) >= self.maxheader:
|
||||
raise Exception('header exceeded allowable size')
|
||||
|
||||
# indicates end of HTTP header
|
||||
if b'\r\n\r\n' in self.headerbuffer:
|
||||
self.request = HTTPRequest(self.headerbuffer)
|
||||
|
||||
# handshake rfc 6455
|
||||
try:
|
||||
key = self.request.headers['Sec-WebSocket-Key']
|
||||
k = key.encode('ascii') + GUID_STR.encode('ascii')
|
||||
k_s = base64.b64encode(hashlib.sha1(k).digest()).decode('ascii')
|
||||
hStr = HANDSHAKE_STR % {'acceptstr': k_s}
|
||||
self.sendq.append((BINARY, hStr.encode('ascii')))
|
||||
self.handshaked = True
|
||||
self.handleConnected()
|
||||
except Exception as e:
|
||||
raise Exception('handshake failed: %s', str(e))
|
||||
|
||||
# else do normal data
|
||||
else:
|
||||
data = self.client.recv(8192)
|
||||
if not data:
|
||||
raise Exception("remote socket closed")
|
||||
|
||||
if VER >= 3:
|
||||
for d in data:
|
||||
self._parseMessage(d)
|
||||
else:
|
||||
for d in data:
|
||||
self._parseMessage(ord(d))
|
||||
|
||||
def close(self, status = 1000, reason = u''):
|
||||
"""
|
||||
Send Close frame to the client. The underlying socket is only closed
|
||||
when the client acknowledges the Close frame.
|
||||
|
||||
status is the closing identifier.
|
||||
reason is the reason for the close.
|
||||
"""
|
||||
try:
|
||||
if self.closed is False:
|
||||
close_msg = bytearray()
|
||||
close_msg.extend(struct.pack("!H", status))
|
||||
if _check_unicode(reason):
|
||||
close_msg.extend(reason.encode('utf-8'))
|
||||
else:
|
||||
close_msg.extend(reason)
|
||||
|
||||
self._sendMessage(False, CLOSE, close_msg)
|
||||
|
||||
finally:
|
||||
self.closed = True
|
||||
|
||||
|
||||
def _sendBuffer(self, buff):
|
||||
size = len(buff)
|
||||
tosend = size
|
||||
already_sent = 0
|
||||
|
||||
while tosend > 0:
|
||||
try:
|
||||
# i should be able to send a bytearray
|
||||
sent = self.client.send(buff[already_sent:])
|
||||
if sent == 0:
|
||||
raise RuntimeError('socket connection broken')
|
||||
|
||||
already_sent += sent
|
||||
tosend -= sent
|
||||
|
||||
except socket.error as e:
|
||||
# if we have full buffers then wait for them to drain and try again
|
||||
if e.errno in [errno.EAGAIN, errno.EWOULDBLOCK]:
|
||||
return buff[already_sent:]
|
||||
else:
|
||||
raise e
|
||||
|
||||
return None
|
||||
|
||||
def sendFragmentStart(self, data):
|
||||
"""
|
||||
Send the start of a data fragment stream to a websocket client.
|
||||
Subsequent data should be sent using sendFragment().
|
||||
A fragment stream is completed when sendFragmentEnd() is called.
|
||||
|
||||
If data is a unicode object then the frame is sent as Text.
|
||||
If the data is a bytearray object then the frame is sent as Binary.
|
||||
"""
|
||||
opcode = BINARY
|
||||
if _check_unicode(data):
|
||||
opcode = TEXT
|
||||
self._sendMessage(True, opcode, data)
|
||||
|
||||
def sendFragment(self, data):
|
||||
"""
|
||||
see sendFragmentStart()
|
||||
|
||||
If data is a unicode object then the frame is sent as Text.
|
||||
If the data is a bytearray object then the frame is sent as Binary.
|
||||
"""
|
||||
self._sendMessage(True, STREAM, data)
|
||||
|
||||
def sendFragmentEnd(self, data):
|
||||
"""
|
||||
see sendFragmentEnd()
|
||||
|
||||
If data is a unicode object then the frame is sent as Text.
|
||||
If the data is a bytearray object then the frame is sent as Binary.
|
||||
"""
|
||||
self._sendMessage(False, STREAM, data)
|
||||
|
||||
def sendMessage(self, data):
|
||||
"""
|
||||
Send websocket data frame to the client.
|
||||
|
||||
If data is a unicode object then the frame is sent as Text.
|
||||
If the data is a bytearray object then the frame is sent as Binary.
|
||||
"""
|
||||
opcode = BINARY
|
||||
if _check_unicode(data):
|
||||
opcode = TEXT
|
||||
self._sendMessage(False, opcode, data)
|
||||
|
||||
|
||||
def _sendMessage(self, fin, opcode, data):
|
||||
|
||||
payload = bytearray()
|
||||
|
||||
b1 = 0
|
||||
b2 = 0
|
||||
if fin is False:
|
||||
b1 |= 0x80
|
||||
b1 |= opcode
|
||||
|
||||
if _check_unicode(data):
|
||||
data = data.encode('utf-8')
|
||||
|
||||
length = len(data)
|
||||
payload.append(b1)
|
||||
|
||||
if length <= 125:
|
||||
b2 |= length
|
||||
payload.append(b2)
|
||||
|
||||
elif length >= 126 and length <= 65535:
|
||||
b2 |= 126
|
||||
payload.append(b2)
|
||||
payload.extend(struct.pack("!H", length))
|
||||
|
||||
else:
|
||||
b2 |= 127
|
||||
payload.append(b2)
|
||||
payload.extend(struct.pack("!Q", length))
|
||||
|
||||
if length > 0:
|
||||
payload.extend(data)
|
||||
|
||||
self.sendq.append((opcode, payload))
|
||||
|
||||
|
||||
def _parseMessage(self, byte):
|
||||
# read in the header
|
||||
if self.state == HEADERB1:
|
||||
|
||||
self.fin = byte & 0x80
|
||||
self.opcode = byte & 0x0F
|
||||
self.state = HEADERB2
|
||||
|
||||
self.index = 0
|
||||
self.length = 0
|
||||
self.lengtharray = bytearray()
|
||||
self.data = bytearray()
|
||||
|
||||
rsv = byte & 0x70
|
||||
if rsv != 0:
|
||||
raise Exception('RSV bit must be 0')
|
||||
|
||||
elif self.state == HEADERB2:
|
||||
mask = byte & 0x80
|
||||
length = byte & 0x7F
|
||||
|
||||
if self.opcode == PING and length > 125:
|
||||
raise Exception('ping packet is too large')
|
||||
|
||||
if mask == 128:
|
||||
self.hasmask = True
|
||||
else:
|
||||
self.hasmask = False
|
||||
|
||||
if length <= 125:
|
||||
self.length = length
|
||||
|
||||
# if we have a mask we must read it
|
||||
if self.hasmask is True:
|
||||
self.maskarray = bytearray()
|
||||
self.state = MASK
|
||||
else:
|
||||
# if there is no mask and no payload we are done
|
||||
if self.length <= 0:
|
||||
try:
|
||||
self._handlePacket()
|
||||
finally:
|
||||
self.state = self.HEADERB1
|
||||
self.data = bytearray()
|
||||
|
||||
# we have no mask and some payload
|
||||
else:
|
||||
#self.index = 0
|
||||
self.data = bytearray()
|
||||
self.state = PAYLOAD
|
||||
|
||||
elif length == 126:
|
||||
self.lengtharray = bytearray()
|
||||
self.state = LENGTHSHORT
|
||||
|
||||
elif length == 127:
|
||||
self.lengtharray = bytearray()
|
||||
self.state = LENGTHLONG
|
||||
|
||||
|
||||
elif self.state == LENGTHSHORT:
|
||||
self.lengtharray.append(byte)
|
||||
|
||||
if len(self.lengtharray) > 2:
|
||||
raise Exception('short length exceeded allowable size')
|
||||
|
||||
if len(self.lengtharray) == 2:
|
||||
self.length = struct.unpack_from('!H', self.lengtharray)[0]
|
||||
|
||||
if self.hasmask is True:
|
||||
self.maskarray = bytearray()
|
||||
self.state = MASK
|
||||
else:
|
||||
# if there is no mask and no payload we are done
|
||||
if self.length <= 0:
|
||||
try:
|
||||
self._handlePacket()
|
||||
finally:
|
||||
self.state = HEADERB1
|
||||
self.data = bytearray()
|
||||
|
||||
# we have no mask and some payload
|
||||
else:
|
||||
#self.index = 0
|
||||
self.data = bytearray()
|
||||
self.state = PAYLOAD
|
||||
|
||||
elif self.state == LENGTHLONG:
|
||||
|
||||
self.lengtharray.append(byte)
|
||||
|
||||
if len(self.lengtharray) > 8:
|
||||
raise Exception('long length exceeded allowable size')
|
||||
|
||||
if len(self.lengtharray) == 8:
|
||||
self.length = struct.unpack_from('!Q', self.lengtharray)[0]
|
||||
|
||||
if self.hasmask is True:
|
||||
self.maskarray = bytearray()
|
||||
self.state = MASK
|
||||
else:
|
||||
# if there is no mask and no payload we are done
|
||||
if self.length <= 0:
|
||||
try:
|
||||
self._handlePacket()
|
||||
finally:
|
||||
self.state = HEADERB1
|
||||
self.data = bytearray()
|
||||
|
||||
# we have no mask and some payload
|
||||
else:
|
||||
#self.index = 0
|
||||
self.data = bytearray()
|
||||
self.state = PAYLOAD
|
||||
|
||||
# MASK STATE
|
||||
elif self.state == MASK:
|
||||
self.maskarray.append(byte)
|
||||
|
||||
if len(self.maskarray) > 4:
|
||||
raise Exception('mask exceeded allowable size')
|
||||
|
||||
if len(self.maskarray) == 4:
|
||||
# if there is no mask and no payload we are done
|
||||
if self.length <= 0:
|
||||
try:
|
||||
self._handlePacket()
|
||||
finally:
|
||||
self.state = HEADERB1
|
||||
self.data = bytearray()
|
||||
|
||||
# we have no mask and some payload
|
||||
else:
|
||||
#self.index = 0
|
||||
self.data = bytearray()
|
||||
self.state = PAYLOAD
|
||||
|
||||
# PAYLOAD STATE
|
||||
elif self.state == PAYLOAD:
|
||||
if self.hasmask is True:
|
||||
self.data.append( byte ^ self.maskarray[self.index % 4] )
|
||||
else:
|
||||
self.data.append( byte )
|
||||
|
||||
# if length exceeds allowable size then we except and remove the connection
|
||||
if len(self.data) >= self.maxpayload:
|
||||
raise Exception('payload exceeded allowable size')
|
||||
|
||||
# check if we have processed length bytes; if so we are done
|
||||
if (self.index+1) == self.length:
|
||||
try:
|
||||
self._handlePacket()
|
||||
finally:
|
||||
#self.index = 0
|
||||
self.state = HEADERB1
|
||||
self.data = bytearray()
|
||||
else:
|
||||
self.index += 1
|
||||
|
||||
|
||||
class SimpleWebSocketServer(object):
|
||||
def __init__(self, host, port, websocketclass, selectInterval = 0.1):
|
||||
self.websocketclass = websocketclass
|
||||
self.serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.serversocket.bind((host, port))
|
||||
self.serversocket.listen(5)
|
||||
self.selectInterval = selectInterval
|
||||
self.connections = {}
|
||||
self.listeners = [self.serversocket]
|
||||
|
||||
def _decorateSocket(self, sock):
|
||||
return sock
|
||||
|
||||
def _constructWebSocket(self, sock, address):
|
||||
return self.websocketclass(self, sock, address)
|
||||
|
||||
def close(self):
|
||||
self.serversocket.close()
|
||||
|
||||
for desc, conn in self.connections.items():
|
||||
conn.close()
|
||||
conn.handleClose()
|
||||
|
||||
|
||||
def serveforever(self):
|
||||
while True:
|
||||
writers = []
|
||||
for fileno in self.listeners:
|
||||
if fileno == self.serversocket:
|
||||
continue
|
||||
client = self.connections[fileno]
|
||||
if client.sendq:
|
||||
writers.append(fileno)
|
||||
|
||||
if self.selectInterval:
|
||||
rList, wList, xList = select(self.listeners, writers, self.listeners, self.selectInterval)
|
||||
else:
|
||||
rList, wList, xList = select(self.listeners, writers, self.listeners)
|
||||
|
||||
for ready in wList:
|
||||
client = self.connections[ready]
|
||||
try:
|
||||
while client.sendq:
|
||||
opcode, payload = client.sendq.popleft()
|
||||
remaining = client._sendBuffer(payload)
|
||||
if remaining is not None:
|
||||
client.sendq.appendleft((opcode, remaining))
|
||||
break
|
||||
else:
|
||||
if opcode == CLOSE:
|
||||
raise Exception('received client close')
|
||||
|
||||
except Exception as n:
|
||||
client.client.close()
|
||||
client.handleClose()
|
||||
del self.connections[ready]
|
||||
self.listeners.remove(ready)
|
||||
|
||||
for ready in rList:
|
||||
if ready == self.serversocket:
|
||||
try:
|
||||
sock, address = self.serversocket.accept()
|
||||
newsock = self._decorateSocket(sock)
|
||||
newsock.setblocking(0)
|
||||
fileno = newsock.fileno()
|
||||
self.connections[fileno] = self._constructWebSocket(newsock, address)
|
||||
self.listeners.append(fileno)
|
||||
except Exception as n:
|
||||
if sock is not None:
|
||||
sock.close()
|
||||
else:
|
||||
if ready not in self.connections:
|
||||
continue
|
||||
client = self.connections[ready]
|
||||
try:
|
||||
client._handleData()
|
||||
except Exception as n:
|
||||
client.client.close()
|
||||
client.handleClose()
|
||||
del self.connections[ready]
|
||||
self.listeners.remove(ready)
|
||||
|
||||
for failed in xList:
|
||||
if failed == self.serversocket:
|
||||
self.close()
|
||||
raise Exception('server socket failed')
|
||||
else:
|
||||
if failed not in self.connections:
|
||||
continue
|
||||
client = self.connections[failed]
|
||||
client.client.close()
|
||||
client.handleClose()
|
||||
del self.connections[failed]
|
||||
self.listeners.remove(failed)
|
||||
|
||||
|
||||
class SimpleSSLWebSocketServer(SimpleWebSocketServer):
|
||||
|
||||
def __init__(self, host, port, websocketclass, certfile,
|
||||
keyfile, version = ssl.PROTOCOL_TLSv1, selectInterval = 0.1):
|
||||
|
||||
SimpleWebSocketServer.__init__(self, host, port,
|
||||
websocketclass, selectInterval)
|
||||
|
||||
self.context = ssl.SSLContext(version)
|
||||
self.context.load_cert_chain(certfile, keyfile)
|
||||
|
||||
def close(self):
|
||||
super(SimpleSSLWebSocketServer, self).close()
|
||||
|
||||
def _decorateSocket(self, sock):
|
||||
sslsock = self.context.wrap_socket(sock, server_side=True)
|
||||
return sslsock
|
||||
|
||||
def _constructWebSocket(self, sock, address):
|
||||
ws = self.websocketclass(self, sock, address)
|
||||
ws.usingssl = True
|
||||
return ws
|
||||
|
||||
def serveforever(self):
|
||||
super(SimpleSSLWebSocketServer, self).serveforever()
|
||||
Binary file not shown.
@ -1,25 +0,0 @@
|
||||
{
|
||||
"name": "mpvremote-server",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"repository": "indefero@ghostdub.de:mpvremote.git",
|
||||
"licenses": [
|
||||
{
|
||||
"type": "gpl"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"bunyan": "^1.8.0",
|
||||
"every-moment": "0.0.1",
|
||||
"nesh": "1.6.0",
|
||||
"nodemon": "1.9.1",
|
||||
"q": "1.4.1",
|
||||
"q-io": "1.13.2",
|
||||
"restify": "4.0.4",
|
||||
"restify-plugins": "1.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "./node_modules/.bin/nodemon server.js | ./node_modules/.bin/bunyan"
|
||||
}
|
||||
}
|
||||
@ -1,253 +0,0 @@
|
||||
var restify = require('restify');
|
||||
var bunyan = require('bunyan');
|
||||
var Q = require("q");
|
||||
var FS = require("q-io/fs");
|
||||
const net = require('net');
|
||||
var util = require("util");
|
||||
|
||||
String.prototype.format = function() {
|
||||
var formatted = this;
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
var regexp = new RegExp('\\{'+i+'\\}', 'gi');
|
||||
formatted = formatted.replace(regexp, arguments[i]);
|
||||
}
|
||||
return formatted;
|
||||
};
|
||||
|
||||
// CONNECTIONS AND STUF
|
||||
var socketPath = '/home/daddel9/.mpv-sock';
|
||||
var log = bunyan.createLogger({name: 'MpvRemote'});
|
||||
|
||||
// SETUP
|
||||
var server = restify.createServer();
|
||||
server.pre(restify.pre.sanitizePath());
|
||||
|
||||
|
||||
// Serve static files
|
||||
server.get(/\/client\/?.*/, restify.serveStatic({
|
||||
directory: __dirname
|
||||
}));
|
||||
|
||||
|
||||
// Cödê
|
||||
|
||||
|
||||
global.mpvstate = {
|
||||
"percent-pos":null,
|
||||
"time-pos":null,
|
||||
"time-remaining":null,
|
||||
"chapter":null,
|
||||
"chapter-list/count":null,
|
||||
"pause":null,
|
||||
"volume":null,
|
||||
"mute":null,
|
||||
};
|
||||
global.mpvisWaitingFor = null;
|
||||
|
||||
global.mpvsocket = null;
|
||||
global.mpvisConnected = false;
|
||||
global.mpvconnect = function() {
|
||||
defer = Q.defer();
|
||||
|
||||
log.info("socket is {0} isConnected is {1}".format(global.mpvsocket, global.mpvisConnected));
|
||||
if(global.mpvisConnected) {
|
||||
defer.resolve(global.mpvsocket);
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
sock = net.connect(socketPath);
|
||||
|
||||
sock.on("connect", function() {
|
||||
log.info("socket had connect event");
|
||||
global.mpvsocket = sock;
|
||||
global.mpvisConnected=true;
|
||||
defer.resolve(sock)
|
||||
});
|
||||
sock.on("error", function(err) {
|
||||
global.mpvisConnected=false;
|
||||
defer.reject(err)
|
||||
});
|
||||
sock.on("end", function() {
|
||||
global.mpvisConnected=false;
|
||||
defer.reject(new Error("socket ended"))
|
||||
});
|
||||
sock.on("data", function(dta) {
|
||||
log.info("had data '{0}' while waitingfor {1}".format(dta, global.mpvisWaitingFor));
|
||||
if(global.mpvisWaitingFor) {
|
||||
dta = JSON.parse(dta);
|
||||
var property = global.mpvisWaitingFor[0];
|
||||
var defer = global.mpvisWaitingFor[1];
|
||||
log.info("was waitingfor {0}, had data {1}, resolving now".format(property, dta['data']));
|
||||
global.mpvstate[property] = dta['data'];
|
||||
global.mpvisWaitingFor = null;
|
||||
defer.resolve();
|
||||
}
|
||||
});
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
|
||||
function buildCommand(cmd, params) {
|
||||
paramsStr="";
|
||||
params.forEach(function(val, idx, arr) {
|
||||
paramsStr = paramsStr+', "{0}"'.format(val);
|
||||
})
|
||||
ret = '{ "command": [ "{0}"{1} ] }\n'.format(cmd, paramsStr);
|
||||
return ret;
|
||||
}
|
||||
|
||||
function writeCommand(cmd, params) {
|
||||
defer = Q.defer();
|
||||
global.mpvconnect()
|
||||
.then( function(sock) {
|
||||
log.info("can write in "+sock);
|
||||
try {
|
||||
cmdStr = buildCommand(cmd, params);
|
||||
log.info("trying to write cmd '{0}' to sock".format(cmdStr));
|
||||
sock.write(cmdStr);
|
||||
defer.resolve();
|
||||
} catch(e) {
|
||||
log.error("got error "+e);
|
||||
defer.reject(e);
|
||||
}
|
||||
}, function(reason) {
|
||||
defer.reject(reason);
|
||||
});
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
|
||||
function updateProperty(propertyIdx) {
|
||||
var defer = Q.defer();
|
||||
var keys = Object.keys(global.mpvstate);
|
||||
var property = keys[propertyIdx];
|
||||
log.info("updating property at idx {0} which is {1}".format(propertyIdx, property));
|
||||
|
||||
writeCommand("get_property", [property]).then(function(){
|
||||
global.mpvisWaitingFor=[property,defer];
|
||||
|
||||
if(propertyIdx >= keys.length-1) {
|
||||
log.info("idx too large, returning");
|
||||
return;
|
||||
} else {
|
||||
log.info("deferring updateProperty({0})".format(propertyIdx+1));
|
||||
defer.promise.then(function() { updateProperty(propertyIdx+1) } );
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function stateUpdate() {
|
||||
log.info("updating state");
|
||||
updateProperty(0);
|
||||
}
|
||||
|
||||
var every = require('every-moment');
|
||||
var timer = every(10, 'second', function() {
|
||||
stateUpdate();
|
||||
});
|
||||
|
||||
var timer = every(12, 'second', function() {
|
||||
log.info("my current state is: "+util.inspect(global.mpvstate));
|
||||
});
|
||||
|
||||
// /seekTime/:time — seeks relative
|
||||
// /seekPercent/:percent — seeks percent absolute
|
||||
// /seekChapter/:where — seeks to prev (where=-x) or next (where=+x) chapter
|
||||
// /volume/:amount — increases (amount=+x) or decreases (amount=-x) volume
|
||||
// /progress — show osd progress
|
||||
// /playpause — toggles playing
|
||||
// /muteunmute — toggles muting
|
||||
// /stateUpdate — update state
|
||||
// /stateVar/:name — get value of tracked property with name
|
||||
|
||||
server.get('/seek/:time', function (req, res, next) {
|
||||
log.info("seek "+req.params.time);
|
||||
writeCommand("seek", [req.params.time])
|
||||
.then(function() {
|
||||
res.send({"success":true});
|
||||
return next(false);
|
||||
}, function(reason) {
|
||||
res.send({"success":false, "reason": reason});
|
||||
return next(false);
|
||||
});
|
||||
});
|
||||
|
||||
server.get('/seekPercent/:percent', function (req, res, next) {
|
||||
log.info("seekpercent "+req.params.percent);
|
||||
writeCommand("seek", [req.params.percent, "absolute-percent"])
|
||||
.then(function() {
|
||||
res.send({"success":true});
|
||||
return next(false);
|
||||
}, function(reason) {
|
||||
res.send({"success":false, "reason": reason});
|
||||
return next(false);
|
||||
});
|
||||
});
|
||||
|
||||
server.get('/seekChapter/:where', function (req, res, next) {
|
||||
log.info("seekChapter "+req.params.where);
|
||||
writeCommand("add", ["chapter", req.params.where])
|
||||
.then(function() {
|
||||
res.send({"success":true});
|
||||
return next(false);
|
||||
}, function(reason) {
|
||||
res.send({"success":false, "reason": reason});
|
||||
return next(false);
|
||||
});
|
||||
});server.get('/volume/:amount', function (req, res, next) {
|
||||
log.info("volume "+req.params.amount);
|
||||
writeCommand("add", ["volume", req.params.amount])
|
||||
.then(function() {
|
||||
res.send({"success":true});
|
||||
return next(false);
|
||||
}, function(reason) {
|
||||
res.send({"success":false, "reason": reason});
|
||||
return next(false);
|
||||
});
|
||||
});
|
||||
|
||||
server.get('/volume/:amount', function (req, res, next) {
|
||||
log.info("volume "+req.params.amount);
|
||||
writeCommand("add", ["volume", req.params.amount])
|
||||
.then(function() {
|
||||
res.send({"success":true});
|
||||
return next(false);
|
||||
}, function(reason) {
|
||||
res.send({"success":false, "reason": reason});
|
||||
return next(false);
|
||||
});
|
||||
});
|
||||
|
||||
server.get('/playpause', function (req, res, next) {
|
||||
log.info("playpause ");
|
||||
writeCommand("cycle", ["pause"])
|
||||
.then(function() {
|
||||
res.send({"success":true});
|
||||
return next(false);
|
||||
}, function(reason) {
|
||||
res.send({"success":false, "reason": reason});
|
||||
return next(false);
|
||||
});
|
||||
});
|
||||
|
||||
server.get('/muteunmute', function (req, res, next) {
|
||||
log.info("muteunmute ");
|
||||
writeCommand("cycle", ["mute"])
|
||||
.then(function() {
|
||||
res.send({"success":true});
|
||||
return next(false);
|
||||
}, function(reason) {
|
||||
res.send({"success":false, "reason": reason});
|
||||
return next(false);
|
||||
});
|
||||
});
|
||||
|
||||
server.get('/stateUpdate', function (req, res, next) {
|
||||
log.info("stateUpdate ");
|
||||
stateUpdate();
|
||||
});
|
||||
|
||||
server.listen(8080, function() {
|
||||
console.log('%s listening at %s', server.name, server.url);
|
||||
});
|
||||
@ -0,0 +1,154 @@
|
||||
from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket
|
||||
|
||||
import json
|
||||
import socket
|
||||
import os
|
||||
import threading
|
||||
import select
|
||||
import random
|
||||
|
||||
SOCKPATH='/home/dario/.mpv.sock'
|
||||
global mpvParser
|
||||
mpvParser = None
|
||||
|
||||
class SocketPoller(threading.Thread):
|
||||
def __init__(self, sockParser):
|
||||
super(SocketPoller, self).__init__()
|
||||
self.sockParser = sockParser
|
||||
self.stop=False
|
||||
|
||||
if not os.path.exists(SOCKPATH):
|
||||
raise Exception("Could not find server socket ~/.mpv.sock, exiting")
|
||||
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self.sock.connect(SOCKPATH)
|
||||
self.buf=""
|
||||
|
||||
def run(self):
|
||||
while not self.stop:
|
||||
if len( select.select([self.sock], [], [])[0] ) > 0:
|
||||
self.buf += self.sock.recv(1)
|
||||
if '\n' in self.buf:
|
||||
self.sockParser.newData(self.buf)
|
||||
self.buf=""
|
||||
|
||||
def sendData(self, data):
|
||||
print "sending data '%s'"%data
|
||||
if not '\n' in data:
|
||||
data+='\n'
|
||||
self.sock.sendall(data)
|
||||
|
||||
class MpvSockParser(object):
|
||||
def __init__(self):
|
||||
self.listeners = []
|
||||
self.playerState = {
|
||||
"totalDuration": 42,
|
||||
"currentDuration": 23,
|
||||
"volume": 66,
|
||||
"fileName": "No Data Yet",
|
||||
"isPlaying": False
|
||||
}
|
||||
self.poller = None
|
||||
self.waitingForCommand = None
|
||||
|
||||
def registerListener(self, listener):
|
||||
self.listeners.append(listener)
|
||||
def unregisterListener(self, listener):
|
||||
self.listeners.remove(listener)
|
||||
def notifyListeners(self):
|
||||
for l in self.listeners:
|
||||
l.dataNotify(self.playerState)
|
||||
|
||||
def startPoll(self):
|
||||
self.poller = SocketPoller(self)
|
||||
|
||||
self.poller.sendData('{"command":["observe_property",1,"time-pos"]}')
|
||||
self.poller.sendData('{"command":["observe_property",2,"volume"]}')
|
||||
self.poller.sendData('{"command":["observe_property",3,"filename"]}')
|
||||
self.poller.sendData('{"command":["observe_property",4,"duration"]}')
|
||||
self.poller.sendData('{"command":["observe_property",4,"duration"]}')
|
||||
self.poller.sendData('{ "command": ["get_property", "pause"], "request_id":10}')
|
||||
|
||||
self.poller.start()
|
||||
|
||||
def newData(self, data):
|
||||
data = json.loads(data)
|
||||
# mpv-generated event
|
||||
if "event" in data:
|
||||
self.processEvent(data)
|
||||
# answer to our request
|
||||
if 'error' in data:
|
||||
self.processCommand(data)
|
||||
|
||||
def processEvent(self, data):
|
||||
sendNotify = True
|
||||
if "data" in data and data["data"]==None:
|
||||
return
|
||||
|
||||
if data['event'] == 'property-change' and data['name'] == 'time-pos':
|
||||
oldCur = self.playerState['currentDuration']
|
||||
curCur = int(data['data'])
|
||||
if curCur - oldCur < 1:
|
||||
sendNotify = False
|
||||
self.playerState['currentDuration'] = curCur
|
||||
if data['event'] == 'property-change' and data['name'] == 'volume':
|
||||
self.playerState['volume'] = int(data['data'])
|
||||
if data['event'] == 'property-change' and data['name'] == 'filename':
|
||||
self.playerState['fileName'] = data['data']
|
||||
if data['event'] == 'property-change' and data['name'] == 'duration':
|
||||
self.playerState['totalDuration'] = int(data['data'])
|
||||
if data['event'] == 'pause':
|
||||
self.playerState['isPlaying'] = False
|
||||
if data['event'] == 'unpause':
|
||||
self.playerState['isPlaying'] = True
|
||||
|
||||
if sendNotify:
|
||||
print self.playerState
|
||||
self.notifyListeners()
|
||||
|
||||
def processCommand(self, data):
|
||||
if "request_id" in data and data['request_id'] == 10:
|
||||
print 'synced pause state'
|
||||
self.playerState['isPlaying'] = not data["data"]
|
||||
|
||||
def sendCommand(self, command):
|
||||
if command["command"] == "play":
|
||||
self.poller.sendData('{ "command": ["set_property", "pause", false] }')
|
||||
if command["command"] == "pause":
|
||||
self.poller.sendData('{ "command": ["set_property", "pause", true] }')
|
||||
if command["command"] == "seek":
|
||||
self.poller.sendData('{ "command": ["seek", "%s", "absolute"] }'%command['seekValue'])
|
||||
if command["command"] == "volume":
|
||||
self.poller.sendData('{ "command": ["set", "volume", %s] }'%command['volume'])
|
||||
if command["command"] == "seekChapter":
|
||||
if command['direction'] == 'forward':
|
||||
direction = +1
|
||||
elif command['direction'] == 'backward':
|
||||
direction = -1
|
||||
self.poller.sendData('{ "command": ["add", "chapter", %s] }'%direction)
|
||||
|
||||
class WSHandler(WebSocket):
|
||||
def handleMessage(self):
|
||||
global mpvParser
|
||||
command = json.loads(self.data)
|
||||
print 'got commadn %s'%command
|
||||
mpvParser.sendCommand(command)
|
||||
def handleConnected(self):
|
||||
global mpvParser
|
||||
mpvParser.registerListener(self)
|
||||
print self.address, 'connected'
|
||||
def handleClose(self):
|
||||
global mpvParser
|
||||
print self.address, 'closed'
|
||||
mpvParser.unregisterListener(self)
|
||||
def dataNotify(self, data):
|
||||
data['type'] = 'status'
|
||||
self.sendMessage(unicode( json.dumps(data) ))
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
global mpvParser
|
||||
mpvParser = MpvSockParser()
|
||||
mpvParser.startPoll()
|
||||
|
||||
server = SimpleWebSocketServer('', 8000, WSHandler)
|
||||
server.serveforever()
|
||||
Loading…
Reference in new issue