much rewrite, very kane :)

This commit is contained in:
Stefan Kögl 2014-05-26 23:25:36 +02:00
parent 05745e3c52
commit 80d6aea666
7 changed files with 365 additions and 391 deletions

View File

@ -0,0 +1,9 @@
<!DOCTYPE HTML>
<html>
<head>
<link rel="icon" type="image/png" href="/icon.png" />
</head>
<body>
<img src="/texter_%d.mjpeg" alt="Smiley face" />
</body>
</html>

View File

@ -23,8 +23,9 @@ from __future__ import absolute_import
import os
import os.path
import re
import signal
import sys
from collections import deque
from datetime import datetime
from chaosc.argparser_groups import *
from chaosc.lib import logger, resolve_host
@ -33,7 +34,8 @@ from PyQt4.QtCore import QBuffer, QByteArray, QIODevice
from PyQt4.QtNetwork import QTcpServer, QTcpSocket, QUdpSocket, QHostAddress
from dump_grabber.dump_grabber_ui import Ui_MainWindow
from psylib.mjpeg_streaming_server import MjpegStreamingServer
from psylib.mjpeg_streaming_server import *
from psylib.psyqt_base import PsyQtChaoscClientBase
try:
from chaosc.c_osc_lib import OSCMessage, decode_osc
@ -42,62 +44,17 @@ except ImportError as e:
app = QtGui.QApplication([])
class TextStorage(object):
def __init__(self, columns):
super(TextStorage, self).__init__()
class ExclusiveTextStorage(object):
def __init__(self, columns, default_font, column_width, line_height, scene):
self.column_count = columns
self.colors = (QtCore.Qt.red, QtCore.Qt.green, QtGui.QColor(46, 100, 254))
def init_columns(self):
raise NotImplementedError()
def add_text(self, column, text):
raise NotImplementedError()
class ColumnTextStorage(TextStorage):
def __init__(self, columns, default_font, column_width, line_height, scene):
super(ColumnTextStorage, self).__init__(columns)
self.columns = list()
self.default_font = default_font
self.column_width = column_width
self.line_height = line_height
self.graphics_scene = scene
self.num_lines, self.offset = divmod(768, self.line_height)
def init_columns(self):
for x in range(self.column_count):
column = list()
color = self.colors[x]
for y in range(self.num_lines):
text_item = self.graphics_scene.addSimpleText("%d:%d" % (x, y), self.default_font)
text_item.setBrush(color)
text_item.setPos(x * self.column_width, y * self.line_height)
column.append(text_item)
self.columns.append(column)
def add_text(self, column, text):
text_item = self.graphics_scene.addSimpleText(text, self.default_font)
color = self.colors[column]
text_item.setBrush(color)
old_item = self.columns[column].pop(0)
self.graphics_scene.removeItem(old_item)
self.columns[column].append(text_item)
for iy, text_item in enumerate(self.columns[column]):
text_item.setPos(column * self.column_width, iy * self.line_height)
class ExclusiveTextStorage(TextStorage):
def __init__(self, columns, default_font, column_width, line_height, scene):
super(ExclusiveTextStorage, self).__init__(columns)
self.column_count = columns
self.lines = list()
self.lines = deque()
self.default_font = default_font
self.column_width = column_width
self.line_height = line_height
self.graphics_scene = scene
self.num_lines, self.offset = divmod(576, self.line_height)
self.data = deque()
def init_columns(self):
color = self.colors[0]
@ -107,26 +64,38 @@ class ExclusiveTextStorage(TextStorage):
text_item.setPos(0, y * self.line_height)
self.lines.append(text_item)
def add_text(self, column, text):
def __add_text(self, column, text):
text_item = self.graphics_scene.addSimpleText(text, self.default_font)
text_item.setX(column * self.column_width)
color = self.colors[column]
text_item.setBrush(color)
old_item = self.lines.pop(0)
old_item = self.lines.popleft()
self.graphics_scene.removeItem(old_item)
self.lines.append(text_item)
def finish(self):
while 1:
try:
column, text = self.data.popleft()
self.__add_text(column, text)
except IndexError, msg:
break
for iy, text_item in enumerate(self.lines):
text_item.setY(iy * self.line_height)
def add_text(self, column, text):
self.data.append((column, text))
class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
def __init__(self, args, parent=None, columns=3, column_exclusive=False):
super(MainWindow, self).__init__(parent)
class MainWindow(QtGui.QMainWindow, Ui_MainWindow, MjpegStreamingConsumerInterface, PsyQtChaoscClientBase):
def __init__(self, args, parent=None):
self.args = args
#PsyQtChaoscClientBase.__init__(self)
super(MainWindow, self).__init__()
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.setupUi(self)
self.http_server = MjpegStreamingServer((args.http_host, args.http_port), self)
@ -140,20 +109,16 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
self.graphics_view.setScene(self.graphics_scene)
self.default_font = QtGui.QFont("Monospace", 14)
self.default_font.setStyleHint(QtGui.QFont.Monospace)
self.default_font.setBold(True)
#self.default_font.setBold(True)
self.graphics_scene.setFont(self.default_font)
self.font_metrics = QtGui.QFontMetrics(self.default_font)
self.line_height = self.font_metrics.height()
columns = 3
self.column_width = 775 / columns
self.text_storage = ExclusiveTextStorage(columns, self.default_font, self.column_width, self.line_height, self.graphics_scene)
self.text_storage.init_columns()
self.osc_sock = QUdpSocket(self)
logger.info("osc bind localhost %d", args.client_port)
self.osc_sock.bind(QHostAddress("127.0.0.1"), args.client_port)
self.osc_sock.readyRead.connect(self.got_message)
self.osc_sock.error.connect(self.handle_osc_error)
msg = OSCMessage("/subscribe")
msg.appendTypedArg("localhost", "s")
msg.appendTypedArg(args.client_port, "i")
@ -165,6 +130,9 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
self.regex = re.compile("^/(uwe|merle|bjoern)/(.*?)$")
def pubdir(self):
return os.path.dirname(os.path.abspath(__file__))
def closeEvent(self, event):
msg = OSCMessage("/unsubscribe")
msg.appendTypedArg("localhost", "s")
@ -179,6 +147,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
self.text_storage.add_text(column, text)
def render_image(self):
self.text_storage.finish()
image = QtGui.QImage(768, 576, QtGui.QImage.Format_ARGB32_Premultiplied)
image.fill(QtCore.Qt.black)
painter = QtGui.QPainter(image)
@ -191,7 +160,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
painter.end()
buf = QBuffer()
buf.open(QIODevice.WriteOnly)
image.save(buf, "JPG", 80)
image.save(buf, "JPG", 85)
image_data = buf.data()
return image_data
@ -207,6 +176,8 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
except AttributeError:
pass
else:
if text == "temperatur":
text += "e"
if actor == "merle":
self.add_text(0, "%s = %s" % (text, ", ".join([str(i) for i in args])))
elif actor == "uwe":
@ -229,8 +200,11 @@ def main():
args = arg_parser.finalize()
http_host, http_port = resolve_host(args.http_host, args.http_port, args.address_family)
args.chaosc_host, args.chaosc_port = resolve_host(args.chaosc_host, args.chaosc_port, args.address_family)
window = MainWindow(args)
#window.show()
sys.excepthook = window.sigint_handler
signal.signal(signal.SIGTERM, window.sigterm_handler)
app.exec_()

View File

@ -1,7 +1,7 @@
<HTML>
<BODY>
<img src="/camera.mjpeg" alt="Smiley face">
<img src="/camera_%d.mjpeg" alt="Smiley face">
</BODY>
</HTML>

View File

@ -24,79 +24,82 @@
from __future__ import absolute_import
from chaosc.argparser_groups import *
from chaosc.lib import logger, resolve_host
from datetime import datetime
from operator import attrgetter
from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import QBuffer, QByteArray, QIODevice
from PyQt4.QtNetwork import QTcpServer, QTcpSocket, QUdpSocket, QHostAddress
from PyQt4.QtGui import QPixmap
import logging
import numpy as np
import atexit
import random
import os.path
import re
import signal
import sys
import exceptions
from PyQt4 import QtCore, QtGui
from PyQt4.QtCore import QBuffer, QByteArray, QIODevice
from PyQt4.QtNetwork import QUdpSocket, QHostAddress
import numpy as np
import pyqtgraph as pg
from pyqtgraph.widgets.PlotWidget import PlotWidget
import Queue
import re
import select
import socket
import threading
import time
from psylib.mjpeg_streaming_server import MjpegStreamingServer
from chaosc.argparser_groups import ArgParser
from chaosc.lib import logger, resolve_host
from psylib.mjpeg_streaming_server import *
from psylib.psyqt_base import PsyQtChaoscClientBase
try:
from chaosc.c_osc_lib import OSCMessage, decode_osc
except ImportError as e:
logging.exception(e)
from chaosc.osc_lib import OSCMessage, decode_osc
qtapp = QtGui.QApplication([])
def get_steps(pulse_rate, rate):
beat_length = 60. / pulse_rate
steps_pre = int(beat_length / rate) + 1
def get_steps(pulse, delta_ms):
beat_length = 60000. / pulse
steps_pre = int(beat_length / delta_ms) + 1
used_sleep_time = beat_length / steps_pre
steps = int(beat_length / used_sleep_time)
return steps, used_sleep_time
class Generator(object):
def __init__(self, pulse=92, delta=0.08):
def __init__(self, pulse=92, delta=80):
self.count = 0
self.pulse = 92
self.pulse = random.randint(85, 105)
self.delta = delta
self.steps = get_steps(self.pulse, delta / 2)
self.multiplier = 4
self.steps, _ = get_steps(self.pulse, delta / self.multiplier)
def __call__(self):
while 1:
value = random.randint(0, steps)
if self.count < int(steps / 100. * 20):
value = random.randint(0,20)
elif self.count < int(steps / 100. * 30):
value = random.randint(20, 30)
elif self.count < int(steps / 100. * 40):
value = random.randint(30,100)
elif self.count < int(steps / 2.):
value = random.randint(100,200)
elif self.count == int(steps / 2.):
if self.count < int(self.steps / 100. * 30):
value = random.randint(30, 35)
elif self.count == int(self.steps / 100. * 30):
value = random.randint(random.randint(50,60), random.randint(60, 70))
elif self.count < int(self.steps / 100. * 45):
value = random.randint(30, 35)
elif self.count < int(self.steps / 2.):
value = random.randint(0, 15)
elif self.count == int(self.steps / 2.):
value = 255
elif self.count < int(steps / 100. * 60):
value = random.randint(100, 200)
elif self.count < int(steps / 100. * 70):
value = random.randint(50, 100)
elif self.count < int(steps / 100. * 80):
value = random.randint(20, 50)
elif self.count <= steps:
value = random.randint(0,20)
elif self.count >= steps:
elif self.count < int(self.steps / 100. * 60):
value = random.randint(random.randint(25,30), random.randint(30, 35))
elif self.count < int(self.steps / 100. * 70):
value = random.randint(random.randint(10,25), random.randint(25, 30))
elif self.count < self.steps:
value = random.randint(random.randint(15,25), random.randint(25, 30))
else:
self.count = 0
value = 30
self.count += 1
yield value
def set_pulse(self, pulse):
self.pulse = pulse
self.steps, _ = get_steps(pulse, self.delta / self.multiplier)
def retrigger(self):
self.count = self.steps / 2
@ -115,11 +118,14 @@ class Actor(object):
self.data = np.array([self.offset] * num_data)
self.head = 0
self.pre_head = 0
self.plotItem = pg.PlotCurveItem(pen=pg.mkPen(color, width=3), name=name)
self.plotItem = pg.PlotCurveItem(pen=pg.mkPen(color, width=3), width=4, name=name)
#self.plotItem.setShadowPen(pg.mkPen("w", width=5))
self.plotPoint = pg.ScatterPlotItem(pen=pg.mkPen("w", width=5), brush=pg.mkBrush(color), size=5)
self.osci = None
self.osci_obj = None
def __str__(self):
return "<Actor name:%r, active=%r, position=%r>" % (self.name, self.active, self.head)
return "<Actor name:%r, position=%r>" % (self.name, self.head)
__repr__ = __str__
@ -143,29 +149,20 @@ class Actor(object):
def render(self):
self.plotItem.setData(y=self.data, clear=True)
self.plotPoint.setData(x=[self.pre_head], y = [self.data[self.pre_head]])
self.plotPoint.setData(x=[self.pre_head], y=[self.data[self.pre_head]])
class EkgPlotWidget(PlotWidget):
class EkgPlotWidget(PlotWidget, MjpegStreamingConsumerInterface, PsyQtChaoscClientBase):
def __init__(self, args, parent=None):
super(EkgPlotWidget, self).__init__(parent)
self.args = args
PsyQtChaoscClientBase.__init__(self)
super(EkgPlotWidget, self).__init__()
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.http_server = MjpegStreamingServer((args.http_host, args.http_port), self)
self.fps = 12.5
self.http_server = MjpegStreamingServer((args.http_host, args.http_port), self, self.fps)
self.http_server.listen(port=args.http_port)
self.osc_sock = QUdpSocket(self)
logger.info("osc bind localhost %d", args.client_port)
self.osc_sock.bind(QHostAddress("127.0.0.1"), args.client_port)
self.osc_sock.readyRead.connect(self.got_message)
self.osc_sock.error.connect(self.handle_osc_error)
msg = OSCMessage("/subscribe")
msg.appendTypedArg("localhost", "s")
msg.appendTypedArg(args.client_port, "i")
msg.appendTypedArg(self.args.authenticate, "s")
if self.args.subscriber_label is not None:
msg.appendTypedArg(self.args.subscriber_label, "s")
self.osc_sock.writeDatagram(QByteArray(msg.encode_osc()), QHostAddress("127.0.0.1"), 7110)
self.num_data = 100
self.hide()
@ -173,8 +170,6 @@ class EkgPlotWidget(PlotWidget):
self.setYRange(0, 255)
self.setXRange(0, self.num_data)
self.resize(768, 576)
colors = ["r", "g", "b"]
ba = self.getAxis("bottom")
@ -186,10 +181,9 @@ class EkgPlotWidget(PlotWidget):
self.active_actors = list()
self.actors = dict()
self.lengths1 = [0]
self.max_value = 255
actor_names = ["merle", "uwe", "bjoern" ]
actor_names = ["merle", "uwe", "bjoern"]
self.max_actors = len(actor_names)
self.actor_height = self.max_value / self.max_actors
@ -197,11 +191,10 @@ class EkgPlotWidget(PlotWidget):
self.add_actor(actor_name, self.num_data, color, ix, self.max_actors, self.actor_height)
self.set_positions()
self.heartbeat_regex = re.compile("^/(.*?)/heartbeat$")
self.ekg_regex = re.compile("^/(.*?)/ekg$")
self.ctl_regex = re.compile("^/plot/(.*?)$")
self.updated_actors = set()
self.new_round()
def pubdir(self):
return os.path.dirname(os.path.abspath(__file__))
def add_actor(self, actor_name, num_data, color, ix, max_actors, actor_height):
@ -210,7 +203,8 @@ class EkgPlotWidget(PlotWidget):
self.addItem(actor_obj.plotItem)
self.addItem(actor_obj.plotPoint)
self.active_actors.append(actor_obj)
actor_obj.osci_obj = Generator(delta=self.http_server.timer_delta)
actor_obj.osci = actor_obj.osci_obj()
def set_positions(self):
for ix, actor_obj in enumerate(self.active_actors):
@ -220,51 +214,24 @@ class EkgPlotWidget(PlotWidget):
def active_actor_count(self):
return self.max_actors
def new_round(self):
for ix, actor in enumerate(self.active_actors):
actor.updated = 0
def update(self, osc_address, args):
def update_missing_actors(self):
liste = sorted(self.active_actors, key=attrgetter("updated"))
max_values = liste[-1].updated
if max_values == 0:
# handling no signal
for actor in self.active_actors:
actor.add_value(0)
return
for ix, actor in enumerate(self.active_actors):
diff = max_values - actor.updated
if diff > 0:
for i in range(diff):
actor.add_value(0)
def update(self, osc_address, value):
res = self.ekg_regex.match(osc_address)
res = self.heartbeat_regex.match(osc_address)
if res:
actor_name = res.group(1)
actor_obj = self.actors[actor_name]
actor_obj.add_value(value)
#logger.info("actor: %r, %r", actor_name, args)
if args[0] == 1:
actor_obj.osci_obj.retrigger()
actor_obj.osci_obj.set_pulse(args[1])
def render(self):
for ix, actor in enumerate(self.active_actors):
actor.render()
def closeEvent(self, event):
msg = OSCMessage("/unsubscribe")
msg.appendTypedArg("localhost", "s")
msg.appendTypedArg(self.args.client_port, "i")
msg.appendTypedArg(self.args.authenticate, "s")
self.osc_sock.writeDatagram(QByteArray(msg.encode_osc()), QHostAddress("127.0.0.1"), 7110)
def handle_osc_error(self, error):
logger.info("osc socket error %d", error)
def render_image(self):
self.update_missing_actors()
self.render()
for actor_obj in self.active_actors:
osc = actor_obj.osci
for i in range(actor_obj.osci_obj.multiplier):
actor_obj.add_value(osc.next())
actor_obj.render()
exporter = pg.exporters.ImageExporter.ImageExporter(self.plotItem)
exporter.parameters()['width'] = 768
img = exporter.export(toBytes=True)
@ -272,7 +239,6 @@ class EkgPlotWidget(PlotWidget):
buf.open(QIODevice.WriteOnly)
img.save(buf, "JPG", 75)
JpegData = buf.data()
self.new_round()
return JpegData
def got_message(self):
@ -282,10 +248,8 @@ class EkgPlotWidget(PlotWidget):
osc_address, typetags, args = decode_osc(data, 0, len(data))
except Exception, e:
logger.exception(e)
return
else:
self.update(osc_address, args[0])
return True
self.update(osc_address, args)
@ -293,18 +257,20 @@ def main():
arg_parser = ArgParser("ekgplotter")
arg_parser.add_global_group()
client_group = arg_parser.add_client_group()
arg_parser.add_argument(client_group, '-x', "--http_host", default="::",
help='my host, defaults to "::"')
arg_parser.add_argument(client_group, '-X', "--http_port", default=9000,
type=int, help='my port, defaults to 9000')
arg_parser.add_argument(client_group, '-x', "--http_host", default='::',
help='my host, defaults to "::"')
arg_parser.add_argument(client_group, '-X', '--http_port', default=9000,
type=int, help='my port, defaults to 9000')
arg_parser.add_chaosc_group()
arg_parser.add_subscriber_group()
args = arg_parser.finalize()
args.http_host, args.http_port = resolve_host(args.http_host, args.http_port, args.address_family)
args.chaosc_host, args.chaosc_port = resolve_host(args.chaosc_host, args.chaosc_port, args.address_family)
qtapp = QtGui.QApplication([])
widget = EkgPlotWidget(args)
window = EkgPlotWidget(args)
sys.excepthook = window.sigint_handler
signal.signal(signal.SIGTERM, window.sigterm_handler)
qtapp.exec_()

View File

@ -1,20 +1,20 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# This file is part of chaosc and psychosis
# This file is part of chaosc/psylib package
#
# chaosc/psychosis is free software: you can redistribute it and/or modify
# chaosc/psylib 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,
# chaosc/psylib 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 <http://www.gnu.org/licenses/>.
# along with chaosc/psylib. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2014 Stefan Kögl
@ -23,27 +23,47 @@ 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 chaosc.lib import logger
from PyQt4 import QtCore
from PyQt4.QtCore import QByteArray
from PyQt4.QtNetwork import QTcpServer, QTcpSocket
__all__ = ["MjpegStreamingConsumerInterface", "MjpegStreamingServer"]
class MjpegStreamingConsumerInterface(object):
def pubdir(self):
""" returns the directory, from where your static files should be served
fast and dirty implementation e.g:
return os.path.dirname(os.path.abspath(__file__))
"""
raise NotImplementedError()
def render_image(self):
"""returns a QByteArray with the binary date of a jpg image
this method should implement the actual window/widget grabbing"""
raise NotImplementedError()
class MjpegStreamingServer(QTcpServer):
def __init__(self, server_address, parent=None):
def __init__(self, server_address, parent=None, fps=12.5):
super(MjpegStreamingServer, self).__init__(parent)
self.server_address = server_address
self.newConnection.connect(self.new_connection)
assert isinstance(parent, MjpegStreamingConsumerInterface)
self.widget = parent
self.sockets = list()
self.img_data = None
self.fps = fps
self.timer_delta = 1000 / fps
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.send_image)
self.timer.start(80)
self.timer.start(self.timer_delta)
self.stream_clients = list()
self.get_regex = re.compile("^GET /(\w+?)\.(\w+?) HTTP/(\d+\.\d+)$")
self.host_regex = re.compile("^Host: (\w+?):(\d+)$")
@ -51,8 +71,8 @@ class MjpegStreamingServer(QTcpServer):
def handle_request(self):
sock = self.sender()
logger.info("handle_request: %s %d", sock.peerAddress(), sock.peerPort())
sock_id = id(sock)
logger.info("handle_request: sock_id=%r", sock_id)
if sock.state() in (QTcpSocket.UnconnectedState, QTcpSocket.ClosingState):
logger.info("connection closed")
self.sockets.remove(sock)
@ -79,14 +99,26 @@ class MjpegStreamingServer(QTcpServer):
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))
directory = self.widget.pubdir()
try:
data = open(os.path.join(directory, "favicon.ico"), "rb").read()
except IOError:
logger.error("request not found/handled - sending 404 not found")
sock.write("HTTP/1.1 404 Not Found\r\n")
return
else:
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))
directory = self.widget.pubdir()
try:
data = open(os.path.join(directory, "index.html"), "rb").read() % sock_id
self.html_map[sock_id] = None
except IOError:
logger.error("request not found/handled - sending 404 not found")
sock.write("HTTP/1.1 404 Not Found\r\n")
return
else:
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)
@ -117,31 +149,40 @@ class MjpegStreamingServer(QTcpServer):
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.sockets.remove(sock)
logger.info("connection %r removed", sock_id)
except ValueError, msg:
logger.info("connection %r was not stored?", sock_id)
try:
self.stream_clients.remove(sock)
except ValueError:
pass
logger.info("connection %r was not streaming", sock_id)
try:
stream_client = self.html_map.pop(sock_id)
except KeyError:
logger.info("socket has no child socket")
logger.info("socket %r has no child socket", sock_id)
else:
stream_client.close()
try:
self.stream_clients.remove(stream_client)
logger.info("removed stream_client=%r", id(stream_client))
except ValueError:
pass
stream_client.close()
except AttributeError, msg:
logger.info("no stream client")
else:
try:
self.stream_clients.remove(stream_client)
logger.info("child connection %r removed from streaming", 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
try:
self.sockets.remove(stream_client)
logger.info("child connection %r removed from storage", id(stream_client))
except ValueError:
pass
def send_image(self):

105
psylib/psylib/psyqt_base.py Normal file
View File

@ -0,0 +1,105 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# This file is part of chaosc/psylib package
#
# chaosc/psylib 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/psylib 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/psylib. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2014 Stefan Kögl
from __future__ import absolute_import
import sys
from PyQt4 import QtCore, QtGui
from PyQt4.QtCore import QBuffer, QByteArray
from PyQt4.QtNetwork import QUdpSocket, QHostAddress
from chaosc.lib import logger
try:
from chaosc.c_osc_lib import OSCMessage, decode_osc
except ImportError as e:
from chaosc.osc_lib import OSCMessage, decode_osc
class PsyQtClientBase(QtCore.QObject):
def __init__(self):
super(PsyQtClientBase, self).__init__()
# periodically trap into python interpreter domain to catch signals etc
timer = QtCore.QTimer()
timer.start(2000)
timer.timeout.connect(lambda: None)
def sigint_handler(self, ex_cls, ex, traceback):
"""Handler for the SIGINT signal."""
if ex_cls == KeyboardInterrupt:
logger.info("found KeyboardInterrupt")
QtGui.QApplication.exit()
else:
logger.critical(''.join(traceback.format_tb(tb)))
logger.critical('{0}: {1}'.format(ex_cls, ex))
class PsyQtChaoscClientBase(PsyQtClientBase):
def __init__(self):
super(PsyQtChaoscClientBase, self).__init__()
self.osc_sock = QUdpSocket(self)
logger.info("osc bind localhost %d", self.args.client_port)
self.osc_sock.bind(QHostAddress(self.args.client_host), self.args.client_port)
self.osc_sock.readyRead.connect(self.got_message)
self.osc_sock.error.connect(self.handle_osc_error)
self.subscribe()
def sigint_handler(self, ex_cls, ex, traceback):
"""Handler for the SIGINT signal."""
if ex_cls == KeyboardInterrupt:
logger.info("found KeyboardInterrupt")
self.unsubscribe()
QtGui.QApplication.exit()
else:
logger.critical(''.join(traceback.format_tb(traceback)))
logger.critical('{0}: {1}'.format(ex_cls, ex))
def sigterm_handler(self, *args):
self.unsubscribe()
QtGui.QApplication.exit()
def subscribe(self):
logger.info("subscribe")
msg = OSCMessage("/subscribe")
msg.appendTypedArg("localhost", "s")
msg.appendTypedArg(self.args.client_port, "i")
msg.appendTypedArg(self.args.authenticate, "s")
if self.args.subscriber_label is not None:
msg.appendTypedArg(self.args.subscriber_label, "s")
self.osc_sock.writeDatagram(QByteArray(msg.encode_osc()), QHostAddress(self.args.chaosc_host), self.args.chaosc_port)
def unsubscribe(self):
logger.info("unsubscribe")
msg = OSCMessage("/unsubscribe")
msg.appendTypedArg("localhost", "s")
msg.appendTypedArg(self.args.client_port, "i")
msg.appendTypedArg(self.args.authenticate, "s")
self.osc_sock.writeDatagram(QByteArray(msg.encode_osc()), QHostAddress(self.args.chaosc_host), self.args.chaosc_port)
def handle_osc_error(self, error):
logger.info("osc socket error %d", error)
def closeEvent(self, event):
logger.info("closeEvent", event)
self.unsubscribe()
event.accept()

View File

@ -24,6 +24,7 @@ from __future__ import absolute_import
import cPickle
import os.path
import re
import sys
from PyQt4 import QtCore, QtGui
@ -39,11 +40,14 @@ from PyQt4.QtNetwork import QTcpServer, QTcpSocket
from chaosc.argparser_groups import ArgParser
from chaosc.lib import resolve_host, logger
from psylib.mjpeg_streaming_server import *
from psylib.psyqt_base import PsyQtClientBase
from texter.texter_ui import Ui_MainWindow, _fromUtf8
from texter.edit_dialog_ui import Ui_EditDialog
from texter.text_model import TextModel
app = QtGui.QApplication([])
qtapp = QtGui.QApplication([])
# NOTE: if the QIcon.fromTheme method does not find any icons, you can use
@ -54,144 +58,6 @@ app = QtGui.QApplication([])
def get_preview_text(text):
return re.sub(" +", " ", text.replace("\n", " ")).strip()[:20]
class MjpegStreamingServer(QTcpServer):
def __init__(self, server_address, parent=None):
super(MjpegStreamingServer, self).__init__(parent)
self.server_address = server_address
self.newConnection.connect(self.start_streaming)
self.widget = parent.live_text
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.regex = re.compile("^GET /(\w+?)\.(\w+?) HTTP/(\d+\.\d+)$")
self.html_map = dict()
self.coords = parent.live_text_rect()
def handle_request(self):
print "foo"
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.regex.match(line).groups()
logger.info("resource=%r, ext=%r, http_version=%r", resource, ext, http_version)
except AttributeError:
logger.info("no matching request - sending 404 not found")
sock.write("HTTP/1.1 404 Not Found\r\n")
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))
#sock.close()
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_stream_client(self):
try:
sock = self.sender()
except RuntimeError:
return
sock_id = id(sock)
logger.info("remove_stream_client: sock=%r, sock_id=%r", sock, sock_id)
if sock.state() == QTcpSocket.UnconnectedState:
sock.disconnected.disconnect(self.remove_stream_client)
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
#pixmap = QPixmap.grabWidget(self.widget, QtCore.QRect(10, 10, 768, 576))
pixmap = QPixmap.grabWindow(self.win_id, 5, 5, 768, 576)
buf = QBuffer()
buf.open(QIODevice.WriteOnly)
pixmap.save(buf, "JPG", 30)
self.img_data = buf.data()
len_data = len(self.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, self.img_data))
for sock in self.stream_clients:
sock.write(array)
def start_streaming(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_stream_client)
self.sockets.append(sock)
def stop(self):
for sock in self.sockets:
sock.close()
sock.deleteLater()
for sock in self.stream_clients:
sock.close()
sock.deleteLater()
self.stream_clients = list()
self.sockets = list()
self.html_map = dict()
self.close()
class EditDialog(QtGui.QWidget, Ui_EditDialog):
def __init__(self, parent=None):
@ -366,10 +232,10 @@ class TextAnimation(QtCore.QObject):
self.count += 1
class MainWindow(KMainWindow, Ui_MainWindow):
class MainWindow(KMainWindow, Ui_MainWindow, MjpegStreamingConsumerInterface, PsyQtClientBase):
def __init__(self, args, parent=None):
super(MainWindow, self).__init__(parent)
self.args = args
super(MainWindow, self).__init__()
self.is_streaming = False
self.live_center_action = None
@ -391,11 +257,15 @@ class MainWindow(KMainWindow, Ui_MainWindow):
self.dialog = None
self.current_object = None
self.current_index = -1
self.win_id = self.winId()
self.is_auto_publish = False
self.setupUi(self)
self.http_server = MjpegStreamingServer((args.http_host, args.http_port), self)
self.coords = self.live_text_rect()
self.fps = 12.5
self.http_server = MjpegStreamingServer((args.http_host, args.http_port), self, self.fps)
self.live_text.setLineWrapMode(QtGui.QTextEdit.LineWrapMode(QtGui.QTextEdit.FixedPixelWidth))
self.live_text.setLineWrapColumnOrWidth(768)
@ -433,17 +303,29 @@ class MainWindow(KMainWindow, Ui_MainWindow):
self.create_toolbar()
self.slot_load()
app.focusChanged.connect(self.focusChanged)
qtapp.focusChanged.connect(self.focusChanged)
self.start_streaming()
self.show()
def pubdir(self):
return os.path.dirname(os.path.abspath(__file__))
def getPreviewCoords(self):
public_rect = self.preview_text.geometry()
global_rect = QtCore.QRect(self.mapToGlobal(public_rect.topLeft()), self.mapToGlobal(public_rect.bottomRight()))
return global_rect.x(), global_rect.y()
def render_image(self):
pixmap = QPixmap.grabWindow(self.win_id, self.coords.x() + 10, self.coords.y() + 10, 768, 576)
buf = QBuffer()
buf.open(QIODevice.WriteOnly)
pixmap.save(buf, "JPG", 75)
return buf.data()
def filter_editor_actions(self):
disabled_action_names = [
"action_to_plain_text",
@ -631,9 +513,10 @@ class MainWindow(KMainWindow, Ui_MainWindow):
self.dialog.setButtons(KDialog.ButtonCodes(KDialog.Ok | KDialog.Cancel))
self.dialog.okClicked.connect(self.slot_save)
self.dialog.exec_()
super(self, MainWindow).closeEvent(event)
def live_text_rect(self):
return 5, 5, 768, 576
return self.live_text.geometry()
def stop_streaming(self):
self.is_streaming = False
@ -643,6 +526,24 @@ class MainWindow(KMainWindow, Ui_MainWindow):
self.http_server.listen(port=self.args.http_port)
self.is_streaming = True
def fill_combo_box(self):
if self.dialog is not None:
self.dialog.deleteLater()
self.dialog = None
self.text_combo.clear()
current_row = -1
for index, list_obj in enumerate(self.model.text_db):
preview, text = list_obj
self.text_combo.addAction(preview)
if list_obj == self.current_object:
current_row = index
if current_row == -1:
current_row = self.current_index
self.slot_load_preview_text(current_row)
self.text_combo.setCurrentItem(current_row)
def focusChanged(self, old, new):
if new == self.preview_text:
self.live_editor_collection.clearAssociatedWidgets()
@ -651,13 +552,6 @@ class MainWindow(KMainWindow, Ui_MainWindow):
self.preview_editor_collection.clearAssociatedWidgets()
self.live_editor_collection.addAssociatedWidget(self.toolbar)
def custom_clear(self, cursor):
cursor.beginEditBlock()
cursor.movePosition(QtGui.QTextCursor.Start)
cursor.movePosition(QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor)
cursor.removeSelectedText()
cursor.endEditBlock()
def slot_auto_publish(self, state):
self.is_auto_publish = bool(state)
@ -737,23 +631,6 @@ class MainWindow(KMainWindow, Ui_MainWindow):
if self.fade_animation.timer is None:
self.fade_animation.start_animation()
def fill_combo_box(self):
if self.dialog is not None:
self.dialog.deleteLater()
self.dialog = None
self.text_combo.clear()
current_row = -1
for index, list_obj in enumerate(self.model.text_db):
preview, text = list_obj
self.text_combo.addAction(preview)
if list_obj == self.current_object:
current_row = index
if current_row == -1:
current_row = self.current_index
self.slot_load_preview_text(current_row)
self.text_combo.setCurrentItem(current_row)
def slot_load_preview_text(self, index):
try:
@ -864,9 +741,11 @@ def main():
args = arg_parser.finalize()
args.http_host, args.http_port = resolve_host(args.http_host, args.http_port, args.address_family)
args.chaosc_host, args.chaosc_port = resolve_host(args.chaosc_host, args.chaosc_port, args.address_family)
window = MainWindow(args)
app.exec_()
window = MainWindow(args, None)
sys.excepthook = window.sigint_handler
qtapp.exec_()
if __name__ == '__main__':