OSSIA
Open Scenario System for Interactive Application
Loading...
Searching...
No Matches
qml_ws_outbound_socket.hpp
1#pragma once
2#include <ossia/network/context.hpp>
3#include <ossia/network/sockets/udp_socket.hpp>
4#include <ossia/network/sockets/websocket_client.hpp>
5
6#include <ossia-qt/protocols/utils.hpp>
7
8#include <QJSValue>
9#include <QObject>
10#include <QQmlEngine>
11
12#include <nano_observer.hpp>
13
14#include <mutex>
15
16#include <verdigris>
17
18namespace ossia::qt
19{
20class qml_websocket_outbound_socket
21 : public QObject
22 , public protocols_sender
23{
24 W_OBJECT(qml_websocket_outbound_socket)
25public:
34 struct state : Nano::Observer
35 {
36 std::string url;
37 std::unique_ptr<ossia::net::websocket_client> client;
38 std::atomic_bool alive{true};
41 std::atomic_bool closed{false};
42
45 std::mutex qt_mutex;
46 qml_websocket_outbound_socket* self{};
47
48 void detach()
49 {
50 std::lock_guard g{qt_mutex};
51 self = nullptr;
52 }
53
55 void fire_open();
56 void fire_fail();
57 void fire_close();
58
60 void fire_message(websocketpp::frame::opcode::value opcode, std::string msg);
61 };
62
63 qml_websocket_outbound_socket() { }
64
65 ~qml_websocket_outbound_socket()
66 {
67 if(m_state)
68 {
69 m_state->alive = false;
70 // No websocketpp -> Qt call may start from here on. shutdown() then only
71 // touches the transport: the QQmlEngine that owns our QJSValues is being
72 // destroyed, so calling into the script here would be a use-after-free.
73 m_state->detach();
74 shutdown();
75 }
76 }
77
78 bool isOpen() const noexcept { return m_state && m_state->client; }
79
80 void open(
81 const ossia::net::outbound_socket_configuration& conf,
82 boost::asio::io_context& ctx)
83 {
84 m_state = std::make_shared<state>();
85 m_state->url = "ws://" + conf.host + ":" + std::to_string(conf.port); // FIXME wss
86 m_state->self = this;
87 auto st = m_state;
88 m_state->client = std::make_unique<ossia::net::websocket_client>(
89 ctx, [st](auto hdl, auto opcode, const std::string& msg) {
90 if(!st->alive)
91 return;
92 st->fire_message(opcode, msg);
93 });
94
95 if(onOpen.isCallable())
96 m_state->client->on_open.connect<&state::fire_open>(m_state.get());
97 if(onClose.isCallable())
98 m_state->client->on_close.connect<&state::fire_close>(m_state.get());
99 if(onError.isCallable())
100 m_state->client->on_fail.connect<&state::fire_fail>(m_state.get());
101
102 m_state->client->connect(m_state->url);
103 }
104
106 void deliver_message(
107 websocketpp::frame::opcode::value opcode, const std::string& msg)
108 {
109 if(opcode == websocketpp::frame::opcode::text && onTextMessage.isCallable())
110 {
111 onTextMessage.call({QString::fromStdString(msg)});
112 }
113 else if(opcode == websocketpp::frame::opcode::binary && onBinaryMessage.isCallable())
114 {
115 if(auto engine = qjsEngine(this))
116 onBinaryMessage.call({engine->toScriptValue(QByteArray(msg.data(), msg.size()))});
117 }
118 }
119
120 void on_open()
121 {
122 if(!m_state || !m_state->alive)
123 return;
124 ossia::qt::run_async(
125 this, [=, this] { onOpen.call({qjsEngine(this)->newQObject(this)}); },
126 Qt::AutoConnection);
127 }
128 void on_fail()
129 {
130 if(!m_state || !m_state->alive)
131 return;
132 ossia::qt::run_async(this, [=, this] { onError.call(); }, Qt::AutoConnection);
133 }
134 void on_close()
135 {
136 if(!m_state || !m_state->alive)
137 return;
138 if(m_state->closed.exchange(true))
139 return;
140 ossia::qt::run_async(this, [=, this] { onClose.call(); }, Qt::AutoConnection);
141 }
142
143 void write(QString message)
144 {
145 if(!m_state)
146 return;
147 auto st = m_state;
148 boost::asio::dispatch(
149 st->client->context(),
150 [st, msg = message.toStdString()] {
151 if(st->alive)
152 st->client->send_message(msg);
153 });
154 }
155 W_SLOT(write)
156
157 void writeBinary(QByteArray buffer)
158 {
159 if(!m_state)
160 return;
161 auto st = m_state;
162 boost::asio::dispatch(
163 st->client->context(),
164 [st, buf = std::string(buffer.data(), buffer.size())] {
165 if(st->alive)
166 st->client->send_binary_message(buf);
167 });
168 }
169 W_SLOT(writeBinary)
170
171 void close()
172 {
173 if(!m_state)
174 return;
175 shutdown();
176 // onClose reaches the script exactly once, whichever side closed first.
177 if(!m_state->closed.exchange(true) && onClose.isCallable())
178 onClose.call();
179 }
180 W_SLOT(close)
181
182 // FIXME
183 // void osc(QByteArray address, QJSValueList values) { this->send_osc(address, values); }
184 // W_SLOT(osc)
185
186 QJSValue onOpen;
187 QJSValue onClose;
188 QJSValue onError;
189 QJSValue onTextMessage;
190 QJSValue onBinaryMessage;
191
192private:
195 void shutdown()
196 {
197 if(m_state && m_state->client)
198 m_state->client->stop();
199 }
200
201 std::shared_ptr<state> m_state;
202};
203
204inline void qml_websocket_outbound_socket::state::fire_open()
205{
206 std::lock_guard g{qt_mutex};
207 if(self)
208 self->on_open();
209}
210
211inline void qml_websocket_outbound_socket::state::fire_fail()
212{
213 std::lock_guard g{qt_mutex};
214 if(self)
215 self->on_fail();
216}
217
218inline void qml_websocket_outbound_socket::state::fire_close()
219{
220 std::lock_guard g{qt_mutex};
221 if(self)
222 self->on_close();
223}
224
225inline void qml_websocket_outbound_socket::state::fire_message(
226 websocketpp::frame::opcode::value opcode, std::string msg)
227{
228 // The QJSValue callbacks must run on the Qt thread: nothing but plain data is
229 // touched here.
230 std::lock_guard g{qt_mutex};
231 if(!self)
232 return;
233 ossia::qt::run_async(
234 self,
235 [target = self, opcode, msg = std::move(msg)] {
236 target->deliver_message(opcode, msg);
237 },
238 Qt::AutoConnection);
239}
240
241}
Definition qml_device.cpp:43
Definition qml_ws_outbound_socket.hpp:35
std::mutex qt_mutex
Definition qml_ws_outbound_socket.hpp:45
void fire_open()
Nano slots, always on the websocketpp thread.
Definition qml_ws_outbound_socket.hpp:204
std::atomic_bool closed
Definition qml_ws_outbound_socket.hpp:41
void fire_message(websocketpp::frame::opcode::value opcode, std::string msg)
Message delivery, always on the websocketpp thread.
Definition qml_ws_outbound_socket.hpp:225