v2.0.0
Loading...
Searching...
No Matches
lsl_stream_outlet.cpp
Go to the documentation of this file.
1//=============================================================================================================
30
31//=============================================================================================================
32// INCLUDES
33//=============================================================================================================
34
35#include "lsl_stream_outlet.h"
36
37//=============================================================================================================
38// QT INCLUDES
39//=============================================================================================================
40
41#include <QTcpServer>
42#include <QTcpSocket>
43#include <QUdpSocket>
44#include <QHostAddress>
45#include <QDebug>
46
47//=============================================================================================================
48// STL INCLUDES
49//=============================================================================================================
50
51#include <thread>
52#include <mutex>
53#include <atomic>
54#include <cstring>
55#include <queue>
56#include <chrono>
57
58//=============================================================================================================
59// USED NAMESPACES
60//=============================================================================================================
61
62using namespace LSLLIB;
63
64//=============================================================================================================
65// CONSTANTS
66//=============================================================================================================
67
68namespace {
69 const QHostAddress DISCOVERY_MULTICAST_GROUP("239.255.172.215");
70 const quint16 DISCOVERY_PORT = 16571;
71 const int BROADCAST_INTERVAL_MS = 500;
72}
73
74//=============================================================================================================
75// PRIVATE IMPLEMENTATION
76//=============================================================================================================
77
88{
89public:
91 : m_info(info)
92 , m_bRunning(false)
93 {
94 }
95
97 {
98 stop();
99 }
100
101 //=========================================================================================================
105 void start()
106 {
107 m_bRunning = true;
108 m_bgThread = std::thread(&StreamOutletPrivate::run, this);
109 }
110
111 //=========================================================================================================
115 void stop()
116 {
117 m_bRunning = false;
118 if (m_bgThread.joinable()) {
119 m_bgThread.join();
120 }
121 }
122
123 //=========================================================================================================
127 void enqueueSample(const std::vector<float>& sample)
128 {
129 std::lock_guard<std::mutex> lock(m_queueMutex);
130 m_sampleQueue.push(sample);
131 }
132
133 //=========================================================================================================
137 void enqueueChunk(const std::vector<std::vector<float>>& chunk)
138 {
139 std::lock_guard<std::mutex> lock(m_queueMutex);
140 for (const auto& sample : chunk) {
141 m_sampleQueue.push(sample);
142 }
143 }
144
145 //=========================================================================================================
150 {
151 return m_info;
152 }
153
154 //=========================================================================================================
158 bool haveConsumers() const
159 {
160 return m_nClients.load() > 0;
161 }
162
163private:
164 //=========================================================================================================
173 void run()
174 {
175 // --- Set up TCP server ---
176 QTcpServer tcpServer;
177 if (!tcpServer.listen(QHostAddress::Any, 0)) {
178 qDebug() << "[lsl::stream_outlet] Failed to start TCP server:" << tcpServer.errorString();
179 return;
180 }
181
182 // Record the assigned port into stream_info
183 m_info.set_data_port(tcpServer.serverPort());
184
185 // --- Set up UDP socket for multicast discovery ---
186 QUdpSocket udpSocket;
187#ifndef Q_OS_WASM
188 udpSocket.setSocketOption(QAbstractSocket::MulticastTtlOption, 1);
189#endif
190
191 // --- Prepare the handshake header ---
192 // "LSL1" (4 bytes) + channel_count (4 bytes, little-endian int)
193 QByteArray handshake("LSL1", 4);
194 int ch = m_info.channel_count();
195 handshake.append(reinterpret_cast<const char*>(&ch), sizeof(int));
196
197 // Prepare discovery datagram (re-created each loop to include up-to-date port)
198 std::string discoveryPayload = m_info.to_string();
199
200 // Track connected client sockets (owned by this thread)
201 std::vector<QTcpSocket*> clients;
202
203 auto lastBroadcast = std::chrono::steady_clock::now() - std::chrono::seconds(10); // send immediately
204
205 // --- Main loop ---
206 while (m_bRunning) {
207 // 1. Accept new connections
208 while (tcpServer.waitForNewConnection(0)) {
209 QTcpSocket* client = tcpServer.nextPendingConnection();
210 if (client) {
211 // Send handshake to the new client
212 client->write(handshake);
213 client->flush();
214 clients.push_back(client);
215 m_nClients.store(static_cast<int>(clients.size()));
216 }
217 }
218
219 // 2. Send UDP discovery broadcast (every BROADCAST_INTERVAL_MS)
220 auto now = std::chrono::steady_clock::now();
221 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - lastBroadcast).count();
222 if (elapsed >= BROADCAST_INTERVAL_MS) {
223 QByteArray datagram(discoveryPayload.c_str(), static_cast<int>(discoveryPayload.size()));
224 udpSocket.writeDatagram(datagram, DISCOVERY_MULTICAST_GROUP, DISCOVERY_PORT);
225 lastBroadcast = now;
226 }
227
228 // 3. Drain sample queue and write to all clients
229 {
230 std::lock_guard<std::mutex> lock(m_queueMutex);
231 while (!m_sampleQueue.empty()) {
232 const std::vector<float>& sample = m_sampleQueue.front();
233 QByteArray sampleData(reinterpret_cast<const char*>(sample.data()),
234 static_cast<int>(sample.size() * sizeof(float)));
235
236 // Write to all clients, remove disconnected ones
237 auto it = clients.begin();
238 while (it != clients.end()) {
239 QTcpSocket* client = *it;
240 if (client->state() != QAbstractSocket::ConnectedState) {
241 delete client;
242 it = clients.erase(it);
243 continue;
244 }
245 client->write(sampleData);
246 ++it;
247 }
248
249 m_sampleQueue.pop();
250 }
251 }
252
253 // Flush all clients
254 for (QTcpSocket* client : clients) {
255 client->flush();
256 }
257
258 m_nClients.store(static_cast<int>(clients.size()));
259
260 // Sleep briefly to avoid busy-waiting
261 std::this_thread::sleep_for(std::chrono::milliseconds(1));
262 }
263
264 // --- Cleanup ---
265 for (QTcpSocket* client : clients) {
266 client->disconnectFromHost();
267 delete client;
268 }
269 clients.clear();
270 m_nClients.store(0);
271
272 tcpServer.close();
273 udpSocket.close();
274 }
275
276 stream_info m_info;
277 std::atomic<bool> m_bRunning;
278 std::thread m_bgThread;
279
280 std::mutex m_queueMutex;
281 std::queue<std::vector<float>> m_sampleQueue;
282 std::atomic<int> m_nClients{0};
283};
284
285//=============================================================================================================
286// DEFINE MEMBER METHODS
287//=============================================================================================================
288
290: m_pImpl(new StreamOutletPrivate(info))
291{
292 m_pImpl->start();
293}
294
295//=============================================================================================================
296
298{
299 // unique_ptr destructor handles cleanup via StreamOutletPrivate destructor
300}
301
302//=============================================================================================================
303
304void stream_outlet::push_sample(const std::vector<float>& sample)
305{
306 m_pImpl->enqueueSample(sample);
307}
308
309//=============================================================================================================
310
311void stream_outlet::push_chunk(const std::vector<std::vector<float>>& chunk)
312{
313 m_pImpl->enqueueChunk(chunk);
314}
315
316//=============================================================================================================
317
319{
320 return m_pImpl->info();
321}
322
323//=============================================================================================================
324
326{
327 return m_pImpl->haveConsumers();
328}
Declares stream_outlet, the server side of an LSL stream that publishes samples over TCP and advertis...
Lab Streaming Layer (LSL) integration for real-time data exchange.
Value-type descriptor of a single LSL stream: semantic metadata plus transport endpoint,...
int channel_count() const noexcept
Number of channels.
std::string to_string() const
Serialize stream_info into a string for network transport.
void set_data_port(int port)
Set the TCP data port (used internally during discovery / outlet creation).
StreamOutletPrivate(const stream_info &info)
void enqueueSample(const std::vector< float > &sample)
void enqueueChunk(const std::vector< std::vector< float > > &chunk)
void push_sample(const std::vector< float > &sample)
stream_outlet(const stream_info &info)
void push_chunk(const std::vector< std::vector< float > > &chunk)
stream_info info() const