From d7f3a7daea89b867da0a1710a2435e08983cd014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sat, 10 May 2014 14:52:21 +0200 Subject: [PATCH] changed from ffmpeg grabbing to internal grabbing and added a async http server --- texter/texter/main.py | 372 +++++++++++++++++++++++++++++------------- 1 file changed, 256 insertions(+), 116 deletions(-) diff --git a/texter/texter/main.py b/texter/texter/main.py index a17c363..84ab185 100644 --- a/texter/texter/main.py +++ b/texter/texter/main.py @@ -1,38 +1,49 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +# This file is part of texter package +# +# texter 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. +# +# texter 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 texter. If not, see . +# +# Copyright (C) 2014 Stefan Kögl + +from __future__ import absolute_import + + import cPickle import os.path import re -import subprocess -import sys -from math import pow - -from operator import itemgetter from PyQt4 import QtCore, QtGui +from PyQt4.QtCore import QBuffer, QByteArray, QIODevice +from PyQt4.QtGui import QPixmap -from PyKDE4.kdecore import ki18n, KCmdLineArgs, KAboutData -from PyKDE4.kdeui import KDialog, KActionCollection, KRichTextWidget, KComboBox, KPushButton, KRichTextWidget, KMainWindow, KToolBar, KApplication, KAction, KToolBarSpacerAction, KSelectAction, KToggleAction, KShortcut +from PyKDE4.kdeui import (KDialog, KActionCollection, KRichTextWidget, + KRichTextWidget, KMainWindow, KToolBar, KAction, KToolBarSpacerAction, + KSelectAction, KToggleAction, KShortcut) -from texter_ui import Ui_MainWindow, _fromUtf8 -from text_sorter_ui import Ui_TextSorterDialog -from text_model import TextModel +from PyQt4.QtNetwork import QTcpServer, QTcpSocket -appName = "texter" -catalog = "448texter" -programName = ki18n("4.48 Psychose Texter") -version = "0.1" +from chaosc.argparser_groups import ArgParser +from chaosc.lib import resolve_host -aboutData = KAboutData(appName, catalog, programName, version) +from texter.texter_ui import Ui_MainWindow, _fromUtf8 +from texter.text_sorter_ui import Ui_TextSorterDialog +from texter.text_model import TextModel -KCmdLineArgs.init (sys.argv, aboutData) - -app = KApplication() - -for path in QtGui.QIcon.themeSearchPaths(): - print "%s/%s" % (path, QtGui.QIcon.themeName()) +app = QtGui.QApplication([]) # NOTE: if the QIcon.fromTheme method does not find any icons, you can use @@ -40,21 +51,155 @@ for path in QtGui.QIcon.themeSearchPaths(): # in your local icon directory: # ln -s /your/icon/theme/directory $HOME/.icons/hicolor +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 + 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): + sock = self.sender() + sock_id = id(sock) + print "handle_request", sock + if sock.state() in (QTcpSocket.UnconnectedState, QTcpSocket.ClosingState): + print "connection closed" + self.sockets.remove(sock) + sock.deleteLater() + return + + client_data = str(sock.readAll()) + print "request", repr(client_data) + line = client_data.split("\r\n")[0] + print "first line", repr(line) + try: + resource, ext, http_version = self.regex.match(line).groups() + print "resource, ext, http_version", resource, ext, http_version + except AttributeError: + print "regex not matched" + 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)) + if 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: + print "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: + print "not found/handled" + sock.write("HTTP/1.1 404 Not Found\r\n") + self.sockets.remove(sock) + sock.close() + + def remove_stream_client(self): + sock = self.sender() + sock_id = id(sock) + print "remove_stream_client", sock, sock_id + if sock.state() == QTcpSocket.UnconnectedState: + self.sockets.remove(sock) + print "removed sock", sock + sock.close() + try: + self.stream_clients.remove(sock) + except ValueError, error: + print "sock was not in stream_clients", error + + try: + stream_client = self.html_map.pop(sock_id) + except KeyError, error: + print "socket has no child socket" + else: + print "html socket has linked stream socket to remove", stream_client, id(stream_client) + stream_client.close() + try: + self.stream_clients.remove(stream_client) + except ValueError, error: + print "error", error + + try: + self.sockets.remove(stream_client) + except ValueError, error: + print "error", error + + def render_image(self): + if not self.stream_clients: + return + + pixmap = QPixmap.grabWidget(self.widget.live_text, *self.coords) + buf = QBuffer() + buf.open(QIODevice.WriteOnly) + pixmap.save(buf, "JPG", 25) + 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() + 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 TextSorterDialog(QtGui.QWidget, Ui_TextSorterDialog): - def __init__(self, parent = None): + def __init__(self, parent=None): super(TextSorterDialog, self).__init__(parent) self.setupUi(self) self.fill_list() self.text_list.clicked.connect(self.slot_show_text) - self.remove_button.clicked.connect(self.slot_removeItem) + self.remove_button.clicked.connect(self.slot_remove_item) self.move_up_button.clicked.connect(self.slot_text_up) self.move_down_button.clicked.connect(self.slot_text_down) self.text_list.clicked.connect(self.slot_toggle_buttons) self.move_up_button.setEnabled(False) self.move_down_button.setEnabled(False) - + self.model = None def slot_toggle_buttons(self, index): row = index.row() @@ -71,9 +216,9 @@ class TextSorterDialog(QtGui.QWidget, Ui_TextSorterDialog): def fill_list(self): self.model = self.parent().parent().model self.text_list.setModel(self.model) - ix = self.parent().parent().current_index - index = self.model.index(ix, 0) - self.text_list.setCurrentIndex(index) + index = self.parent().parent().current_index + model_index = self.model.index(index, 0) + self.text_list.setCurrentIndex(model_index) def slot_text_up(self): @@ -107,8 +252,7 @@ class TextSorterDialog(QtGui.QWidget, Ui_TextSorterDialog): except IndexError: pass - - def slot_removeItem(self): + def slot_remove_item(self): index = self.text_list.currentIndex().row() self.model.removeRows(index, 1) index = self.model.index(0, 0) @@ -128,7 +272,6 @@ class FadeAnimation(QtCore.QObject): self.current_alpha = 255 self.timer = None - def start_animation(self): print "start_animation" self.animation_started.emit() @@ -141,13 +284,12 @@ class FadeAnimation(QtCore.QObject): self.timer.timeout.connect(self.slot_animate) self.timer.start(100) - def slot_animate(self): print "slot_animate" print "current_alpha", self.current_alpha if self.fade_delta > 0: if self.current_alpha > 0: - self.live_text.setStyleSheet("color:%d, %d, %d;" % (self.current_alpha, self.current_alpha,self.current_alpha)) + self.live_text.setStyleSheet("color:%d, %d, %d;" % (self.current_alpha, self.current_alpha, self.current_alpha)) self.current_alpha -= self.fade_delta else: self.live_text.setStyleSheet("color:black;") @@ -160,7 +302,7 @@ class FadeAnimation(QtCore.QObject): print "animation_finished" else: if self.current_alpha < 255: - self.live_text.setStyleSheet("color:%d,%d, %d;" % (self.current_alpha, self.current_alpha,self.current_alpha)) + self.live_text.setStyleSheet("color:%d,%d, %d;" % (self.current_alpha, self.current_alpha, self.current_alpha)) self.current_alpha -= self.fade_delta else: self.live_text.setStyleSheet("color:white") @@ -214,7 +356,6 @@ class TextAnimation(QtCore.QObject): def slot_animate(self): self.animation_started.emit() - parent = self.parent() if self.it is None: src_root_frame = self.src_document.rootFrame() @@ -272,12 +413,11 @@ class TextAnimation(QtCore.QObject): class MainWindow(KMainWindow, Ui_MainWindow): - def __init__(self, parent=None): + def __init__(self, args, parent=None): super(MainWindow, self).__init__(parent) - + self.args = args self.is_streaming = False - self.ffserver = None - self.ffmpeg = None + self.http_server = MjpegStreamingServer((args.http_host, args.http_port), self) self.live_center_action = None self.preview_center_action = None self.live_size_action = None @@ -307,8 +447,22 @@ class MainWindow(KMainWindow, Ui_MainWindow): self.font = QtGui.QFont("monospace", self.default_size) self.font.setStyleHint(QtGui.QFont.TypeWriter) - - self.create_toolbar() + self.previous_action = None + self.next_action = None + self.publish_action = None + self.auto_publish_action = None + self.save_live_action = None + self.save_preview_action = None + self.save_action = None + self.dialog_widget = None + self.action_collection = None + self.streaming_action = None + self.text_combo = None + self.clear_live_action = None + self.clear_preview_action = None + self.toolbar = None + self.typer_animation_action = None + self.text_editor_action = None #self.preview_text.document().setDefaultFont(self.font) self.preview_text.setFont(self.font) @@ -322,48 +476,28 @@ class MainWindow(KMainWindow, Ui_MainWindow): self.live_editor_collection = KActionCollection(self) self.live_text.createActions(self.live_editor_collection) self.filter_editor_actions() + self.create_toolbar() self.slot_load() + + app.focusChanged.connect(self.focusChanged) + self.start_streaming() + self.get_live_coords() + self.show() - self.save_action.triggered.connect(self.slot_save) - - self.publish_action.triggered.connect(self.slot_publish) - self.clear_live_action.triggered.connect(self.slot_clear_live) - self.clear_preview_action.triggered.connect(self.slot_clear_preview) - self.text_combo.triggered[int].connect(self.slot_load_preview_text) - - app.focusChanged.connect(self.focusChanged) - self.text_editor_action.triggered.connect(self.slot_open_dialog) - self.save_live_action.triggered.connect(self.slot_save_live_text) - self.save_preview_action.triggered.connect(self.slot_save_preview_text) - self.streaming_action.triggered.connect(self.slot_toggle_streaming) - self.auto_publish_action.toggled.connect(self.slot_auto_publish) - self.typer_animation_action.toggled.connect(self.slot_toggle_animation) - self.preview_size_action.triggered[QtGui.QAction].connect(self.slot_preview_font_size) - self.live_size_action.triggered[QtGui.QAction].connect(self.slot_live_font_size) - - #self.fade_action.triggered.connect(self.slot_fade) - self.next_action.triggered.connect(self.slot_next_item) - self.previous_action.triggered.connect(self.slot_previous_item) - - self.getLiveCoords() - print "desktop", app.desktop().availableGeometry() - - - def getLiveCoords(self): + def get_live_coords(self): public_rect = self.live_text.geometry() global_rect = QtCore.QRect(self.mapToGlobal(public_rect.topLeft()), self.mapToGlobal(public_rect.bottomRight())) x = global_rect.x() y = global_rect.y() self.statusBar().showMessage("live text editor dimensions: x=%r, y=%r, width=%r, height=%r" % (x, y, global_rect.width(), global_rect.height())) - def getPreviewCoords(self): + def get_preview_coords(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 filter_editor_actions(self): disabled_action_names = [ @@ -391,7 +525,7 @@ class MainWindow(KMainWindow, Ui_MainWindow): for action in self.live_editor_collection.actions(): text = str(action.objectName()) - print "text", text + #print "text", text if text in disabled_action_names: action.setVisible(False) @@ -417,7 +551,6 @@ class MainWindow(KMainWindow, Ui_MainWindow): self.slot_set_preview_defaults() self.slot_set_live_defaults() - def create_toolbar(self): self.toolbar = KToolBar(self, True, True) @@ -534,9 +667,27 @@ class MainWindow(KMainWindow, Ui_MainWindow): self.toolbar.addSeparator() + self.save_action.triggered.connect(self.slot_save) + + self.publish_action.triggered.connect(self.slot_publish) + self.clear_live_action.triggered.connect(self.slot_clear_live) + self.clear_preview_action.triggered.connect(self.slot_clear_preview) + self.text_combo.triggered[int].connect(self.slot_load_preview_text) + self.text_editor_action.triggered.connect(self.slot_open_dialog) + self.save_live_action.triggered.connect(self.slot_save_live_text) + self.save_preview_action.triggered.connect(self.slot_save_preview_text) + self.streaming_action.triggered.connect(self.slot_toggle_streaming) + self.auto_publish_action.toggled.connect(self.slot_auto_publish) + self.typer_animation_action.toggled.connect(self.slot_toggle_animation) + self.preview_size_action.triggered[QtGui.QAction].connect(self.slot_preview_font_size) + self.live_size_action.triggered[QtGui.QAction].connect(self.slot_live_font_size) + + #self.fade_action.triggered.connect(self.slot_fade) + self.next_action.triggered.connect(self.slot_next_item) + self.previous_action.triggered.connect(self.slot_previous_item) + self.streaming_action.setChecked(True) def closeEvent(self, event): - self.stop_streaming() if self.db_dirty: self.dialog = KDialog(self) self.dialog.setCaption("4.48 texter - text db not saved") @@ -546,20 +697,15 @@ class MainWindow(KMainWindow, Ui_MainWindow): self.dialog.okClicked.connect(self.slot_save) self.dialog.exec_() + def live_text_rect(self): + return 3, 3, 768, 576 + def stop_streaming(self): self.is_streaming = False - if self.ffmpeg is not None: - self.ffmpeg.kill() - self.ffmpeg = None - if self.ffserver is not None: - self.ffserver.kill() - self.ffserver = None + self.http_server.stop() def start_streaming(self): - public_rect = self.live_text.geometry() - global_rect = QtCore.QRect(self.mapToGlobal(public_rect.topLeft()), self.mapToGlobal(public_rect.bottomRight())) - self.ffserver = subprocess.Popen("ffserver -f /etc/ffserver.conf", shell=True, close_fds=True) - self.ffmpeg = subprocess.Popen("ffmpeg -f x11grab -show_region 1 -s 768x576 -r 30 -i :0.0+%d,%d -vcodec mjpeg -pix_fmt yuvj444p -r 30 -aspect 4:3 http://localhost:8090/webcam.ffm" % (global_rect.x()+3, global_rect.y()+3), shell=True, close_fds=True) + self.http_server.listen(port=9009) self.is_streaming = True def focusChanged(self, old, new): @@ -572,16 +718,11 @@ class MainWindow(KMainWindow, Ui_MainWindow): def custom_clear(self, cursor): cursor.beginEditBlock() - cursor.movePosition(QtGui.QTextCursor.Start); - cursor.movePosition(QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor); + cursor.movePosition(QtGui.QTextCursor.Start) + cursor.movePosition(QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor) cursor.removeSelectedText() cursor.endEditBlock() - - def get_preview_text(self, text): - return re.sub(" +", " ", text.replace("\n", " ")).strip()[:20] - - def slot_auto_publish(self, state): self.is_auto_publish = bool(state) @@ -589,11 +730,10 @@ class MainWindow(KMainWindow, Ui_MainWindow): self.is_animate = bool(state) def slot_toggle_streaming(self): - if self.ffserver is None: - self.start_streaming() - else: + if self.is_streaming: self.stop_streaming() - + else: + self.start_streaming() def slot_next_item(self): try: @@ -603,7 +743,6 @@ class MainWindow(KMainWindow, Ui_MainWindow): except ZeroDivisionError: pass - def slot_previous_item(self): try: self.current = (self.text_combo.currentItem() - 1) % len(self.model.text_db) @@ -612,26 +751,22 @@ class MainWindow(KMainWindow, Ui_MainWindow): except ZeroDivisionError: pass - def slot_publish(self): if self.is_animate: self.animation.start_animation(self.preview_text, self.live_text, 0) else: self.live_text.setTextOrHtml(self.preview_text.textOrHtml()) - def slot_live_font_size(self, action): self.default_size = self.live_size_action.fontSize() self.slot_set_preview_defaults() self.slot_set_live_defaults() - def slot_preview_font_size(self, action): self.default_size = self.preview_size_action.fontSize() self.slot_set_live_defaults() self.slot_set_preview_defaults() - def slot_toggle_publish(self, state=None): if state: @@ -639,7 +774,6 @@ class MainWindow(KMainWindow, Ui_MainWindow): else: self.slot_clear_live() - def slot_set_preview_defaults(self): self.preview_center_action.setChecked(True) self.preview_text.alignCenter() @@ -675,11 +809,11 @@ class MainWindow(KMainWindow, Ui_MainWindow): self.text_combo.clear() current_row = -1 - for ix, list_obj in enumerate(self.model.text_db): + 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 = ix + current_row = index if current_row == -1: current_row = self.current_index @@ -697,7 +831,7 @@ class MainWindow(KMainWindow, Ui_MainWindow): def slot_save_live_text(self): text = self.live_text.toHtml() - preview = self.get_preview_text(unicode(self.live_text.toPlainText())) + preview = get_preview_text(unicode(self.live_text.toPlainText())) if not preview: return old_item = self.model.text_by_preview(preview) @@ -720,7 +854,7 @@ class MainWindow(KMainWindow, Ui_MainWindow): def slot_save_preview_text(self): text = self.preview_text.toHtml() - preview = self.get_preview_text(unicode(self.preview_text.toPlainText())) + preview = get_preview_text(unicode(self.preview_text.toPlainText())) if not preview: return @@ -747,7 +881,6 @@ class MainWindow(KMainWindow, Ui_MainWindow): cPickle.dump(self.model.text_db, f, cPickle.HIGHEST_PROTOCOL) self.db_dirty = False - def slot_open_dialog(self): self.current_index = self.text_combo.currentItem() self.current_object = self.model.text_db[self.current_index] @@ -758,13 +891,8 @@ class MainWindow(KMainWindow, Ui_MainWindow): self.dialog = KDialog(self) self.dialog_widget = TextSorterDialog(self.dialog) self.dialog.setMainWidget(self.dialog_widget) - pos_x, pos_y = self.getPreviewCoords() + pos_x, pos_y = self.get_preview_coords() self.dialog.move(pos_x, 0) - rect = app.desktop().availableGeometry() - global_width = rect.width() - global_height = rect.height() - x = global_width - pos_x - 10 - #self.dialog.setFixedSize(x, global_height-40) self.dialog.okClicked.connect(self.fill_combo_box) self.dialog.exec_() @@ -773,25 +901,37 @@ class MainWindow(KMainWindow, Ui_MainWindow): if not os.path.isdir(path): os.mkdir(path) try: - f = open(os.path.join(path, "texter.db")) + db_file = open(os.path.join(path, "texter.db")) except IOError: return try: - self.model.text_db = [list(i) for i in cPickle.load(f)] - except Exception, e: - print e + self.model.text_db = [list(i) for i in cPickle.load(db_file)] + except ValueError, error: + print error self.fill_combo_box() self.text_combo.setCurrentItem(0) self.slot_load_preview_text(0) - def main(): - window = MainWindow() + arg_parser = ArgParser("dump_grabber") + 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=9001, + type=int, help='my port, defaults to 9001') + 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) + + window = MainWindow(args) app.exec_() -if ( __name__ == '__main__' ): +if __name__ == '__main__': main()