From: Steinar H. Gunderson Date: Sun, 25 Feb 2018 23:54:59 +0000 (+0100) Subject: Change to communicating over WebSockets instead of over ACMP. X-Git-Url: https://git.sesse.net/?p=ultimatescore;a=commitdiff_plain;h=17ab0d7fb9504ab8f1d4ffdf27580b86fbdd2336 Change to communicating over WebSockets instead of over ACMP. --- diff --git a/client/Makefile b/client/Makefile index e17047e..2d0b7db 100644 --- a/client/Makefile +++ b/client/Makefile @@ -2,13 +2,13 @@ CXX=g++ PROTOC=protoc INSTALL=install EMBEDDED_BMUSB=no -PKG_MODULES := Qt5Core Qt5Gui Qt5Widgets +PKG_MODULES := Qt5Core Qt5Gui Qt5Widgets Qt5WebSockets CXXFLAGS ?= -O2 -g -Wall # Will be overridden by environment. CXXFLAGS += -std=gnu++11 -fPIC $(shell pkg-config --cflags $(PKG_MODULES)) -pthread LDLIBS=$(shell pkg-config --libs $(PKG_MODULES)) -pthread -OBJS_WITH_MOC = mainwindow.o -OBJS += $(OBJS_WITH_MOC) main.o acmp_client.o event_device.o +OBJS_WITH_MOC = mainwindow.o ws_server.o +OBJS += $(OBJS_WITH_MOC) main.o event_device.o OBJS += $(OBJS_WITH_MOC:.o=.moc.o) %.o: %.cpp diff --git a/client/acmp_client.cpp b/client/acmp_client.cpp deleted file mode 100644 index bb1e56c..0000000 --- a/client/acmp_client.cpp +++ /dev/null @@ -1,213 +0,0 @@ -#include "acmp_client.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace std; - -ACMPClient::ACMPClient(const string &host, int port) - : host(host), port(port) {} - -void ACMPClient::add_init_command(const string &cmd) -{ - init_commands.push_back(cmd + "\r\n"); -} - -void ACMPClient::start() -{ - t = thread(&ACMPClient::thread_func, this); -} - -void ACMPClient::end() -{ - t.join(); -} - -void ACMPClient::send_command(const string &cmd) -{ - lock_guard lock(mu); - queued_commands.push_back(cmd + "\r\n"); -} - -void ACMPClient::change_server(const string &host, int port) -{ - lock_guard lock(mu); - queued_commands.push_back(""); // Marker for disconnect. - this->host = host; - this->port = port; -} - -void ACMPClient::set_connection_callback(const std::function &callback) -{ - connection_callback = callback; -} - -namespace { - -int lookup_and_connect(const char *host, int port) -{ - addrinfo hints; - memset(&hints, 0, sizeof(hints)); - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - - addrinfo *res; - char portstr[16]; - snprintf(portstr, sizeof(portstr), "%d", port); - int err = getaddrinfo(host, portstr, &hints, &res); - if (err != 0) { - fprintf(stderr, "Lookup of %s:%d failed: %s\n", host, port, strerror(errno)); - return -1; - } - - for (addrinfo *p = res; p != NULL; p = p->ai_next) { - int sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol); - if (sock == -1) { - perror("socket"); - continue; - } - - if (connect(sock, p->ai_addr, p->ai_addrlen) == -1) { - perror("connect"); - close(sock); - continue; - } - - // Success! - freeaddrinfo(res); - return sock; - } - - freeaddrinfo(res); - return -1; -} - -} // namespace - -void ACMPClient::thread_func() -{ - if (connection_callback) { - connection_callback(false); - } - for ( ;; ) { - int sock, port_copy; - string host_copy; - { - lock_guard lock(mu); - host_copy = host; - port_copy = port; - } - sock = lookup_and_connect(host_copy.c_str(), port_copy); - if (sock == -1) { - sleep(1); - continue; - } - - int one = 1; - if (ioctl(sock, FIONBIO, &one) == 1) { - perror("ioctl(FIONBIO)"); - close(sock); - sleep(1); - continue; - } - - printf("Connected to CasparCG.\n"); - if (connection_callback) { - connection_callback(true); - } - - bool first = true; - - for ( ;; ) { - vector commands; - if (first) { - commands = init_commands; - first = false; - } else { - lock_guard lock(mu); - swap(commands, queued_commands); - } - - bool broken = false; - string buf; - for (const string &cmd : commands) { - buf += cmd; - if (cmd.empty()) { - printf("Closing CasparCG socket for reconnection.\n"); - broken = true; - break; - } - } - - if (broken) { - break; - } - if (!buf.empty()) { - printf("Writing: '%s'\n", buf.c_str()); - } - - size_t pos = 0; - do { - // Consume until there is no more. - char junk[1024]; - int err = read(sock, junk, sizeof(junk)); - if (err == -1) { - if (err == EAGAIN) { - perror("read"); - broken = true; - break; - } - } - if (err == 0) { - // Closed. - printf("Server closed connection.\n"); - broken = true; - break; - } - if (err > 0) { - // Try again. - junk[err] = 0; - printf("From server: '%s'\n", junk); - continue; - } - - if (pos < buf.size()) { - // Now write as much as we can. - err = write(sock, buf.data() + pos, buf.size() - pos); - if (err == -1) { - perror("write"); - broken = true; - break; - } - if (err == 0) { - // Uh-oh. Buffer full for some reason? - usleep(10000); - } - pos += err; - } - } while (pos < buf.size()); - - if (broken) { - break; - } - if (buf.empty()) { - usleep(100000); - continue; - } - } - - close(sock); - if (connection_callback) { - connection_callback(false); - } - sleep(1); - } -} diff --git a/client/acmp_client.h b/client/acmp_client.h deleted file mode 100644 index a386b27..0000000 --- a/client/acmp_client.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef _ACMP_CLIENT_H -#define _ACMP_CLIENT_H 1 - -#include -#include -#include -#include -#include - -class ACMPClient { -public: - ACMPClient(const std::string &host, int port); - - void add_init_command(const std::string &cmd); - void start(); - void end(); - void send_command(const std::string &cmd); // Thread-safe. - void change_server(const std::string &host, int port); // Thread-safe. - void set_connection_callback(const std::function &callback); - -private: - void thread_func(); - - std::thread t; - std::vector init_commands; - std::function connection_callback; - - std::mutex mu; - std::string host; // Protected by mu. - int port; // Protected by mu. - std::vector queued_commands; // Protected by mu. -}; - -#endif // !defined(_ACMP_CLIENT_H) diff --git a/client/mainwindow.cpp b/client/mainwindow.cpp index b47a9e6..6b8f713 100644 --- a/client/mainwindow.cpp +++ b/client/mainwindow.cpp @@ -52,7 +52,7 @@ string escape_quotes(const string &str) { string s = ""; for (char ch : str) { - if (ch == '"' || ch == '\\') { + if (ch == '"') { s += '\\'; } s += ch; @@ -84,19 +84,15 @@ MainWindow::MainWindow(QWidget *parent) : ui(new Ui::MainWindow) { ui->setupUi(this); - acmp = new ACMPClient("127.0.0.1", 5250); - acmp->add_init_command("MIXER 1 STRAIGHT_ALPHA_OUTPUT 1"); - acmp->add_init_command("CG 1 ADD 0 \"score\" 1 \"\""); - acmp->set_connection_callback([this](bool connected) { + ws = new WSServer("127.0.0.1", 5250); + ws->set_connection_callback([this](bool connected) { string msg = connected ? "Connected" : "Not connected"; post_to_main_thread([this, msg]() { - ui->casparcg_connected_label->setText(QString::fromStdString(msg)); + ui->ws_connected_label->setText(QString::fromStdString(msg)); }); }); - acmp->start(); - - connect(ui->casparcg_reconnect_btn, &QPushButton::clicked, this, &MainWindow::casparcg_reconnect_clicked); + connect(ui->ws_disconnect_btn, &QPushButton::clicked, this, &MainWindow::ws_disconnect_clicked); connect(ui->set_initials_btn, &QPushButton::clicked, this, &MainWindow::set_initials_clicked); connect(ui->set_color_btn, &QPushButton::clicked, this, &MainWindow::set_color_clicked); connect(ui->set_score_btn, &QPushButton::clicked, this, &MainWindow::set_score_clicked); @@ -143,14 +139,12 @@ MainWindow::MainWindow(QWidget *parent) : MainWindow::~MainWindow() { - acmp->end(); delete ui; } -void MainWindow::casparcg_reconnect_clicked() +void MainWindow::ws_disconnect_clicked() { - acmp->change_server(ui->casparcg_host_box->text().toStdString(), - stoi(ui->casparcg_port_box->text().toStdString())); + ws->change_port(stoi(ui->ws_port_box->text().toStdString())); } void MainWindow::set_initials_clicked() @@ -158,8 +152,8 @@ void MainWindow::set_initials_clicked() map param; param["team1"] = escape_html(ui->initials_1_edit->text().toStdString()); param["team2"] = escape_html(ui->initials_2_edit->text().toStdString()); - acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\""); - acmp->send_command("cg 1 invoke 1 setteams"); + ws->send_command("update " + serialize_as_json(param)); + ws->send_command("eval setteams()"); } void MainWindow::set_color_clicked() @@ -167,8 +161,8 @@ void MainWindow::set_color_clicked() map param; param["team1color"] = ui->color_1_edit->text().toStdString(); // Should maybe be escaped, but meh. param["team2color"] = ui->color_2_edit->text().toStdString(); - acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\""); - acmp->send_command("cg 1 invoke 1 setcolors"); + ws->send_command("update " + serialize_as_json(param)); + ws->send_command("eval setcolors()"); } void MainWindow::set_score_clicked() @@ -176,8 +170,8 @@ void MainWindow::set_score_clicked() map param; param["score1"] = to_string(ui->score_1_box->value()); param["score2"] = to_string(ui->score_2_box->value()); - acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\""); - acmp->send_command("cg 1 invoke 1 setscore"); + ws->send_command("update " + serialize_as_json(param)); + ws->send_command("eval setscore()"); autocomment_update(); } @@ -199,8 +193,8 @@ void MainWindow::set_clock_clicked() map param; param["clock_min"] = to_string(ui->clock_min_box->value()); param["clock_sec"] = to_string(ui->clock_sec_box->value()); - acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\""); - acmp->send_command("cg 1 invoke 1 setclockfromstate"); + ws->send_command("update " + serialize_as_json(param)); + ws->send_command("eval setclockfromstate()"); } void MainWindow::set_clock_limit_clicked() @@ -208,42 +202,42 @@ void MainWindow::set_clock_limit_clicked() map param; param["clock_limit_min"] = to_string(ui->clock_limit_min_box->value()); param["clock_limit_sec"] = to_string(ui->clock_limit_sec_box->value()); - acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\""); - acmp->send_command("cg 1 invoke 1 setclocklimitfromstate"); + ws->send_command("update " + serialize_as_json(param)); + ws->send_command("eval setclocklimitfromstate()"); } void MainWindow::start_and_show_clock_clicked() { - acmp->send_command("cg 1 invoke 1 startclock"); // Also shows. + ws->send_command("eval startclock()"); // Also shows. } void MainWindow::stop_clock_clicked() { - acmp->send_command("cg 1 invoke 1 stopclock"); + ws->send_command("eval stopclock()"); } void MainWindow::show_clock_clicked() { - acmp->send_command("cg 1 invoke 1 showclock"); + ws->send_command("eval showclock()"); } void MainWindow::hide_clock_clicked() { - acmp->send_command("cg 1 invoke 1 hideclock"); + ws->send_command("eval hideclock()"); } void MainWindow::set_comment_clicked() { map param; param["comment"] = ui->comment_edit->text().toStdString(); - acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\""); - acmp->send_command("cg 1 invoke 1 setcomment"); + ws->send_command("update " + serialize_as_json(param)); + ws->send_command("eval setcomment()"); } void MainWindow::set_and_show_comment_clicked() { set_comment_clicked(); - acmp->send_command("cg 1 invoke 1 showcomment"); + ws->send_command("eval showcomment()"); } void MainWindow::set_and_show_autocomment_clicked() @@ -254,7 +248,7 @@ void MainWindow::set_and_show_autocomment_clicked() void MainWindow::hide_comment_clicked() { - acmp->send_command("cg 1 invoke 1 hidecomment"); + ws->send_command("eval hidecomment()"); } void MainWindow::show_lower_third_clicked() @@ -262,13 +256,13 @@ void MainWindow::show_lower_third_clicked() map param; param["text1"] = ui->lowerthird_heading_edit->text().toStdString(); param["text2"] = ui->lowerthird_subheading_edit->text().toStdString(); - acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\""); - acmp->send_command("cg 1 invoke 1 setandshowlowerthird"); + ws->send_command("update " + serialize_as_json(param)); + ws->send_command("eval setandshowlowerthird()"); } void MainWindow::hide_lower_third_clicked() { - acmp->send_command("cg 1 invoke 1 hidelowerthird"); + ws->send_command("eval hidelowerthird()"); } void MainWindow::quick_lower_third_activate() @@ -279,12 +273,12 @@ void MainWindow::quick_lower_third_activate() } else if (code == "B") { add_goal(ui->score_2_box, 1); } else if (code == "C") { - acmp->send_command("cg 1 invoke 1 hidelowerthird"); + ws->send_command("eval hidelowerthird()"); } else { map param; param["code"] = code; - acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\""); - acmp->send_command("cg 1 invoke 1 quicklowerthird"); + ws->send_command("update " + serialize_as_json(param)); + ws->send_command("eval quicklowerthird()"); } ui->quick_lower_third_edit->clear(); } @@ -313,39 +307,39 @@ void MainWindow::autocomment_update() void MainWindow::show_scorebug_clicked() { - acmp->send_command("cg 1 invoke 1 stopcarousel"); - acmp->send_command("cg 1 invoke 1 hidetable"); - acmp->send_command("cg 1 invoke 1 showscorebug"); + ws->send_command("eval stopcarousel()"); + ws->send_command("eval hidetable()"); + ws->send_command("eval showscorebug()"); } void MainWindow::show_group_clicked(const std::string &group_name) { map param; param["group_name"] = group_name; - acmp->send_command("cg 1 invoke 1 stopcarousel"); - acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\""); - acmp->send_command("cg 1 invoke 1 showgroup_from_state"); + ws->send_command("eval stopcarousel()"); + ws->send_command("update " + serialize_as_json(param)); + ws->send_command("eval showgroup_from_state()"); } void MainWindow::show_roster_clicked(const std::string &team_code) { map param; param["team_code"] = team_code; - acmp->send_command("cg 1 invoke 1 stopcarousel"); - acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\""); - acmp->send_command("cg 1 invoke 1 showroster_from_state"); + ws->send_command("eval stopcarousel()"); + ws->send_command("update " + serialize_as_json(param)); + ws->send_command("eval showroster_from_state()"); } void MainWindow::show_schedule_clicked() { - acmp->send_command("cg 1 invoke 1 stopcarousel"); - acmp->send_command("cg 1 invoke 1 showschedule"); + ws->send_command("eval stopcarousel()"); + ws->send_command("eval showschedule()"); } void MainWindow::show_carousel_clicked() { - acmp->send_command("cg 1 invoke 1 stopcarousel"); - acmp->send_command("cg 1 invoke 1 showcarousel"); + ws->send_command("eval stopcarousel()"); + ws->send_command("eval showcarousel()"); } void MainWindow::show_roster_carousel_clicked() @@ -353,14 +347,14 @@ void MainWindow::show_roster_carousel_clicked() map param; param["team1"] = escape_html(ui->initials_1_edit->text().toStdString()); param["team2"] = escape_html(ui->initials_2_edit->text().toStdString()); - acmp->send_command("cg 1 invoke 1 stopcarousel"); - acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\""); - acmp->send_command("cg 1 invoke 1 showrostercarousel_from_state"); + ws->send_command("eval stopcarousel()"); + ws->send_command("update " + serialize_as_json(param)); + ws->send_command("eval showrostercarousel_from_state()"); } void MainWindow::show_nothing_clicked() { - acmp->send_command("cg 1 invoke 1 hidescorebug"); - acmp->send_command("cg 1 invoke 1 stopcarousel"); - acmp->send_command("cg 1 invoke 1 hidetable"); + ws->send_command("eval hidescorebug()"); + ws->send_command("eval stopcarousel()"); + ws->send_command("eval hidetable()"); } diff --git a/client/mainwindow.h b/client/mainwindow.h index ae675ca..1d7fe88 100644 --- a/client/mainwindow.h +++ b/client/mainwindow.h @@ -3,7 +3,7 @@ #include -#include "acmp_client.h" +#include "ws_server.h" #include "event_device.h" namespace Ui { @@ -21,7 +21,7 @@ public: ~MainWindow(); private: - void casparcg_reconnect_clicked(); + void ws_disconnect_clicked(); void set_initials_clicked(); void set_color_clicked(); void set_score_clicked(); @@ -50,7 +50,7 @@ private: void show_nothing_clicked(); Ui::MainWindow *ui; - ACMPClient *acmp; + WSServer *ws; EventDevice *event_device; }; diff --git a/client/mainwindow.ui b/client/mainwindow.ui index 28a01a5..cfc8cc4 100644 --- a/client/mainwindow.ui +++ b/client/mainwindow.ui @@ -34,19 +34,12 @@ - CasparCG server: + WebSocket server port: - - - localhost - - - - - + 50 @@ -59,14 +52,14 @@ - + - Reconnect + Change port and disconnect all - + 100 @@ -609,9 +602,8 @@ - casparcg_host_box - casparcg_port_box - casparcg_reconnect_btn + ws_port_box + ws_disconnect_btn initials_1_edit initials_2_edit set_initials_btn diff --git a/client/ws_server.cpp b/client/ws_server.cpp new file mode 100644 index 0000000..914da38 --- /dev/null +++ b/client/ws_server.cpp @@ -0,0 +1,74 @@ +#include "ws_server.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "QtWebSockets/qwebsocketserver.h" +#include "QtWebSockets/qwebsocket.h" + +using namespace std; + +WSServer::WSServer(const string &host, int port) + : ws_server(new QWebSocketServer("ACMP client", QWebSocketServer::NonSecureMode, this)) +{ + if (ws_server->listen(QHostAddress::Any, port)) { + connect(ws_server, &QWebSocketServer::newConnection, this, &WSServer::on_new_connection); + } +} + +void WSServer::change_port(int port) +{ + unordered_set old_clients = move(clients); + for (QWebSocket *sock : old_clients) { + delete sock; + } + ws_server->close(); + ws_server->listen(QHostAddress::Any, port); +} + +void WSServer::add_init_command(const string &cmd) +{ + init_commands.push_back(cmd + "\r\n"); +} + +void WSServer::set_connection_callback(const std::function &callback) +{ + connection_callback = callback; +} + +void WSServer::send_command(const string &cmd) +{ + for (QWebSocket *sock : clients) { + sock->sendTextMessage(QString::fromStdString(cmd)); + } +} + +void WSServer::on_new_connection() +{ + QWebSocket *sock = ws_server->nextPendingConnection(); + connect(sock, &QWebSocket::disconnected, this, &WSServer::disconnected); + + for (const string &cmd : init_commands) { + sock->sendTextMessage(QString::fromStdString(cmd)); + } + clients.insert(sock); + connection_callback(true); +} + +void WSServer::disconnected() +{ + QWebSocket *sock = qobject_cast(sender()); + if (sock) { + clients.erase(sock); + sock->deleteLater(); + connection_callback(!clients.empty()); + } +} diff --git a/client/ws_server.h b/client/ws_server.h new file mode 100644 index 0000000..17d1309 --- /dev/null +++ b/client/ws_server.h @@ -0,0 +1,41 @@ +#ifndef _WS_SERVER_H +#define _WS_SERVER_H 1 + +#include +#include +#include +#include +#include +#include + +#include + +class QWebSocket; +class QWebSocketServer; + +class WSServer : public QObject { + Q_OBJECT + +public: + WSServer(const std::string &host, int port); + + void add_init_command(const std::string &cmd); + void set_connection_callback(const std::function &callback); + void send_command(const std::string &cmd); + void change_port(int port); + +private slots: + void on_new_connection(); + void disconnected(); + +private: + void thread_func(); + + std::vector init_commands; + std::function connection_callback; + + QWebSocketServer *ws_server; + std::unordered_set clients; +}; + +#endif // !defined(_WS_SERVER_H) diff --git a/score.js b/score.js index deae9d8..85ab488 100644 --- a/score.js +++ b/score.js @@ -241,3 +241,40 @@ update_score(); //play(); //startclock(); + +let websocket = null; + +function open_ws() +{ + console.log("Connecting..."); + try { + if (websocket) + websocket.close(); + websocket = new WebSocket("ws://127.0.0.1:5250/"); + websocket.onopen = function(evt) { + console.log("Connected to client."); + }; + websocket.onclose = function(evt) { + console.log("Disconnected from client."); + setTimeout(open_ws, 100); + }; + websocket.onmessage = function(evt) { + let msg = evt.data; + let m = msg.match(/^update (.*)/); + if (m !== null) { + update(m[1]); + } + m = msg.match(/^eval (.*)/); + if (m !== null) { + eval(m[1]); + } + }; + websocket.onerror = function(evt) { + console.log('Error: ' + evt.data); + }; + } catch (exception) { + console.log('Error: ' + exception); + setTimeout(open_ws, 100); + } +}; +open_ws();