From 05745e3c52819f4638d6f38ce9b38735529d41f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sun, 25 May 2014 15:53:50 +0200 Subject: [PATCH] consolidate mjpeg streaming server using qtnetwork into a reusable class --- psylib/psylib/__init__.py | 0 psylib/psylib/mjpeg_streaming_server.py | 169 ++++++++++++++++++++++++ psylib/psylib/other.py | 164 +++++++++++++++++++++++ psylib/setup.py | 43 ++++++ 4 files changed, 376 insertions(+) create mode 100644 psylib/psylib/__init__.py create mode 100644 psylib/psylib/mjpeg_streaming_server.py create mode 100644 psylib/psylib/other.py create mode 100644 psylib/setup.py diff --git a/psylib/psylib/__init__.py b/psylib/psylib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/psylib/psylib/mjpeg_streaming_server.py b/psylib/psylib/mjpeg_streaming_server.py new file mode 100644 index 0000000..c36a9df --- /dev/null +++ b/psylib/psylib/mjpeg_streaming_server.py @@ -0,0 +1,169 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of chaosc and psychosis +# +# chaosc/psychosis is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# chaosc/psychosis is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with chaosc/psychosis. If not, see . +# +# Copyright (C) 2014 Stefan Kögl + +from __future__ import absolute_import + +import os +import os.path +import re +import sys + +from datetime import datetime +from chaosc.argparser_groups import * +from chaosc.lib import logger, resolve_host +from PyQt4 import QtCore, QtGui +from PyQt4.QtCore import QBuffer, QByteArray, QIODevice +from PyQt4.QtNetwork import QTcpServer, QTcpSocket + +class MjpegStreamingServer(QTcpServer): + + def __init__(self, server_address, parent=None): + super(MjpegStreamingServer, self).__init__(parent) + self.server_address = server_address + self.newConnection.connect(self.new_connection) + self.widget = parent + self.sockets = list() + self.img_data = None + self.timer = QtCore.QTimer() + self.timer.timeout.connect(self.send_image) + self.timer.start(80) + self.stream_clients = list() + self.get_regex = re.compile("^GET /(\w+?)\.(\w+?) HTTP/(\d+\.\d+)$") + self.host_regex = re.compile("^Host: (\w+?):(\d+)$") + self.html_map = dict() + + def handle_request(self): + sock = self.sender() + logger.info("handle_request: %s %d", sock.peerAddress(), sock.peerPort()) + sock_id = id(sock) + if sock.state() in (QTcpSocket.UnconnectedState, QTcpSocket.ClosingState): + logger.info("connection closed") + self.sockets.remove(sock) + sock.deleteLater() + return + + client_data = str(sock.readAll()) + logger.info("request %r", client_data) + line = client_data.split("\r\n")[0] + logger.info("first line: %r", line) + try: + resource, ext, http_version = self.get_regex.match(line).groups() + logger.info("resource=%r, ext=%r, http_version=%r", resource, ext, http_version) + except AttributeError: + try: + host, port = self.host_regex.match(line).groups() + logger.info("found host header %r %r", host, port) + #return + #sock.write("HTTP/1.1 501 Not Implemented\r\n") + return + except AttributeError: + logger.info("no matching request - sending 404 not found") + sock.write("HTTP/1.1 404 Not Found\r\n") + return + else: + if ext == "ico": + directory = os.path.dirname(os.path.abspath(__file__)) + data = open(os.path.join(directory, "favicon.ico"), "rb").read() + sock.write(QByteArray('HTTP/1.1 200 Ok\r\nContent-Type: image/x-ico\r\n\r\n%s' % data)) + elif ext == "html": + directory = os.path.dirname(os.path.abspath(__file__)) + data = open(os.path.join(directory, "index.html"), "rb").read() % sock_id + self.html_map[sock_id] = None + sock.write(QByteArray('HTTP/1.1 200 Ok\r\nContent-Type: text/html;encoding: utf-8\r\n\r\n%s' % data)) + elif ext == "mjpeg": + try: + _, html_sock_id = resource.split("_", 1) + html_sock_id = int(html_sock_id) + except ValueError: + html_sock_id = None + + if sock not in self.stream_clients: + logger.info("starting streaming...") + if html_sock_id is not None: + self.html_map[html_sock_id] = sock + self.stream_clients.append(sock) + sock.write(QByteArray('HTTP/1.1 200 Ok\r\nContent-Type: multipart/x-mixed-replace; boundary=--2342\r\n\r\n')) + else: + logger.error("request not found/handled - sending 404 not found") + sock.write("HTTP/1.1 404 Not Found\r\n") + + def slot_remove_connection(self): + try: + sock = self.sender() + except RuntimeError: + return + if sock.state() == QTcpSocket.UnconnectedState: + self.__remove_connection(sock) + + def __remove_connection(self, sock): + sock_id = id(sock) + sock.disconnected.disconnect(self.slot_remove_connection) + sock.close() + sock.deleteLater() + self.sockets.remove(sock) + logger.info("connection removed: sock=%r, sock_id=%r", sock, sock_id) + + try: + self.stream_clients.remove(sock) + except ValueError: + pass + + try: + stream_client = self.html_map.pop(sock_id) + except KeyError: + logger.info("socket has no child socket") + else: + stream_client.close() + try: + self.stream_clients.remove(stream_client) + logger.info("removed stream_client=%r", id(stream_client)) + except ValueError: + pass + + try: + self.sockets.remove(stream_client) + logger.info("removed child sock_id=%r", id(stream_client)) + except ValueError: + pass + + + def send_image(self): + if not self.stream_clients: + return + + img_data = self.widget.render_image() + len_data = len(img_data) + array = QByteArray("--2342\r\nContent-Type: image/jpeg\r\nContent-length: %d\r\n\r\n%s\r\n\r\n\r\n" % (len_data, img_data)) + for sock in self.stream_clients: + sock.write(array) + + def new_connection(self): + while self.hasPendingConnections(): + sock = self.nextPendingConnection() + logger.info("new connection=%r", id(sock)) + sock.readyRead.connect(self.handle_request) + sock.disconnected.connect(self.slot_remove_connection) + self.sockets.append(sock) + + def stop(self): + self.stream_clients = list() + self.sockets = list() + self.html_map = dict() + self.close() diff --git a/psylib/psylib/other.py b/psylib/psylib/other.py new file mode 100644 index 0000000..3140be1 --- /dev/null +++ b/psylib/psylib/other.py @@ -0,0 +1,164 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of chaosc and psychosis +# +# chaosc/psychosis is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# chaosc/psychosis is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with chaosc/psychosis. If not, see . +# +# Copyright (C) 2014 Stefan Kögl + +from __future__ import absolute_import + +import os +import os.path +import re +import sys + +from datetime import datetime +from chaosc.argparser_groups import * +from chaosc.lib import logger, resolve_host +from PyQt4 import QtCore, QtGui +from PyQt4.QtCore import QBuffer, QByteArray, QIODevice +from PyQt4.QtNetwork import QTcpServer + +class MjpegStreamingServer(QTcpServer): + + def __init__(self, server_address, parent=None): + super(MjpegStreamingServer, self).__init__(parent) + self.server_address = server_address + self.newConnection.connect(self.new_connection) + self.widget = parent + self.win_id = self.widget.winId() + self.sockets = list() + self.img_data = None + self.timer = QtCore.QTimer() + self.timer.timeout.connect(self.render_image) + self.timer.start(80) + self.stream_clients = list() + self.get_regex = re.compile("^GET /(\w+?)\.(\w+?) HTTP/(\d+\.\d+)$") + self.host_regex = re.compile("^Host: (\w+?):(\d+)$") + self.html_map = dict() + + def handle_request(self): + sock = self.sender() + logger.info("handle_request: %s %d", sock.peerAddress(), sock.peerPort()) + sock_id = id(sock) + if sock.state() in (QTcpSocket.UnconnectedState, QTcpSocket.ClosingState): + logger.info("connection closed") + self.sockets.remove(sock) + sock.deleteLater() + return + + client_data = str(sock.readAll()) + logger.info("request %r", client_data) + line = client_data.split("\r\n")[0] + logger.info("first line: %r", line) + try: + resource, ext, http_version = self.get_regex.match(line).groups() + logger.info("resource=%r, ext=%r, http_version=%r", resource, ext, http_version) + except AttributeError: + try: + host, port = self.host_regex.match(line).groups() + print "found host header", host, port + return + #sock.write("HTTP/1.1 501 Not Implemented\r\n") + except AttributeError: + logger.info("no matching request - sending 404 not found") + sock.write("HTTP/1.1 404 Not Found\r\n") + return + else: + if ext == "ico": + directory = os.path.dirname(os.path.abspath(__file__)) + data = open(os.path.join(directory, "favicon.ico"), "rb").read() + sock.write(QByteArray('HTTP/1.1 200 Ok\r\nContent-Type: image/x-ico\r\n\r\n%s' % data)) + elif ext == "html": + directory = os.path.dirname(os.path.abspath(__file__)) + data = open(os.path.join(directory, "index.html"), "rb").read() % sock_id + self.html_map[sock_id] = None + sock.write(QByteArray('HTTP/1.1 200 Ok\r\nContent-Type: text/html;encoding: utf-8\r\n\r\n%s' % data)) + elif ext == "mjpeg": + try: + _, html_sock_id = resource.split("_", 1) + html_sock_id = int(html_sock_id) + except ValueError: + html_sock_id = None + + if sock not in self.stream_clients: + logger.info("starting streaming...") + if html_sock_id is not None: + self.html_map[html_sock_id] = sock + self.stream_clients.append(sock) + sock.write(QByteArray('HTTP/1.1 200 Ok\r\nContent-Type: multipart/x-mixed-replace; boundary=--2342\r\n\r\n')) + else: + logger.error("request not found/handled - sending 404 not found") + sock.write("HTTP/1.1 404 Not Found\r\n") + + def remove_connection(self): + try: + sock = self.sender() + except RuntimeError: + return + sock_id = id(sock) + logger.info("remove_connection: sock=%r, sock_id=%r", sock, sock_id) + if sock.state() == QTcpSocket.UnconnectedState: + sock.disconnected.disconnect(self.remove_connection) + self.sockets.remove(sock) + logger.info("removed sock_id=%r", sock_id) + sock.close() + try: + self.stream_clients.remove(sock) + except ValueError: + pass + + try: + stream_client = self.html_map.pop(sock_id) + except KeyError: + logger.info("socket has no child socket") + else: + stream_client.close() + try: + self.stream_clients.remove(stream_client) + logger.info("removed stream_client=%r", id(stream_client)) + except ValueError: + pass + + try: + self.sockets.remove(stream_client) + logger.info("removed child sock_id=%r", id(stream_client)) + except ValueError: + pass + + def render_image(self): + if not self.stream_clients: + return + + img_data = self.widget.render_image() + len_data = len(img_data) + array = QByteArray("--2342\r\nContent-Type: image/jpeg\r\nContent-length: %d\r\n\r\n%s\r\n\r\n\r\n" % (len_data, img_data)) + for sock in self.stream_clients: + sock.write(array) + + def new_connection(self): + while self.hasPendingConnections(): + sock = self.nextPendingConnection() + logger.info("new connection=%r", id(sock)) + sock.readyRead.connect(self.handle_request) + sock.disconnected.connect(self.remove_connection) + self.sockets.append(sock) + + def stop(self): + self.stream_clients = list() + self.sockets = list() + self.html_map = dict() + self.close() diff --git a/psylib/setup.py b/psylib/setup.py new file mode 100644 index 0000000..40332a1 --- /dev/null +++ b/psylib/setup.py @@ -0,0 +1,43 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +from distribute_setup import use_setuptools +use_setuptools() + +import sys +from setuptools import find_packages, setup + +if sys.version_info >= (3,): + extras['use_2to3'] = True + +setup( + name='psylib', + version="0.1", + packages=find_packages(exclude=["scripts",]), + + include_package_data = True, + + exclude_package_data = {'': ['.gitignore']}, + + zip_safe = False, + + # pypi metadata + author = "Stefan Kögl", + + # FIXME: add author email + author_email = "hotte@ctdo.de", + description = "library for psychosis", + + # FIXME: add long_description + long_description = """ + """, + + # FIXME: add license + license = "GPL", + + # FIXME: add keywords + keywords = "", + + # FIXME: add download url + url = "", +)