diff --git a/server/SimpleWebSocketServer.py b/server/SimpleWebSocketServer.py new file mode 100644 index 0000000..c505e60 --- /dev/null +++ b/server/SimpleWebSocketServer.py @@ -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() diff --git a/server/SimpleWebSocketServer.pyc b/server/SimpleWebSocketServer.pyc new file mode 100644 index 0000000..63c958b Binary files /dev/null and b/server/SimpleWebSocketServer.pyc differ diff --git a/server/package.json b/server/package.json deleted file mode 100644 index e6329c3..0000000 --- a/server/package.json +++ /dev/null @@ -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" - } -} diff --git a/server/server.js b/server/server.js deleted file mode 100644 index 5dcf4e3..0000000 --- a/server/server.js +++ /dev/null @@ -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); -}); diff --git a/server/server.py b/server/server.py new file mode 100644 index 0000000..8c9a4f6 --- /dev/null +++ b/server/server.py @@ -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()