GCC Code Coverage Report


Directory: ./
File: channeloutput/HTTPVirtualDisplay3D.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 282 332 84.9%
Functions: 14 15 93.3%
Branches: 229 462 49.6%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2022 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the GPL v2 as described in the
10 * included LICENSE.GPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include "fpp-json.h"
16
17
18 #include <netinet/in.h>
19 #include <poll.h>
20 #include <sys/socket.h>
21 #include <fcntl.h>
22 #include <unistd.h>
23 #include <unordered_map>
24
25 #include <ctime>
26 #include <iomanip>
27
28 #include "../fppversion.h"
29 #include "../log.h"
30 #include "../common.h"
31
32 #include "HTTPVirtualDisplay3D.h"
33 #include "ChannelOutputSetup.h"
34
35 #include "Plugin.h"
36 #include "../SysSocket.h"
37
38 class HTTPVirtualDisplay3DPlugin : public FPPPlugins::Plugin, public FPPPlugins::ChannelOutputPlugin {
39 public:
40 5 HTTPVirtualDisplay3DPlugin() :
41
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
15 FPPPlugins::Plugin("HTTPVirtualDisplay3D") {
42 5 }
43 8 virtual ChannelOutput* createChannelOutput(unsigned int startChannel, unsigned int channelCount) override {
44
2/6
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
16 return new HTTPVirtualDisplay3DOutput(startChannel, channelCount);
45 }
46 };
47
48 extern "C" {
49 5 FPPPlugins::Plugin* createPlugin() {
50
2/6
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
10 return new HTTPVirtualDisplay3DPlugin();
51 }
52 }
53 /////////////////////////////////////////////////////////////////////////////
54
55 /*
56 *
57 */
58 8 HTTPVirtualDisplay3DOutput::HTTPVirtualDisplay3DOutput(unsigned int startChannel,
59 8 unsigned int channelCount) :
60 VirtualDisplayBaseOutput(startChannel, channelCount),
61 8 m_port(HTTPVIRTUALDISPLAY3DPORT),
62 8 m_screenSize(0),
63 8 m_updateInterval(25), // Default: 25ms (40fps) to match typical sequence frame rate
64 8 m_nextSendMS(0),
65 8 m_socket(-1),
66 8 m_running(true),
67 8 m_connListChanged(true),
68 8 m_connThread(nullptr),
69 16 m_selectThread(nullptr) {
70
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 LogDebug(VB_CHANNELOUT, "HTTPVirtualDisplay3DOutput::HTTPVirtualDisplay3DOutput(%u, %u)\n",
71 startChannel, channelCount);
72
73 8 m_bytesPerPixel = 3;
74 8 m_bpp = 24;
75 8 }
76
77 /*
78 *
79 */
80 32 HTTPVirtualDisplay3DOutput::~HTTPVirtualDisplay3DOutput() {
81 16 LogDebug(VB_CHANNELOUT, "HTTPVirtualDisplay3DOutput::~HTTPVirtualDisplay3DOutput()\n");
82
83 16 Close();
84
85 16 m_running = false;
86
87
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
16 if (m_connThread) {
88 16 m_connThread->join();
89
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
16 delete m_connThread;
90 }
91
92
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
16 if (m_selectThread) {
93 16 m_selectThread->join();
94
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
16 delete m_selectThread;
95 }
96
97 // Also reached when Init() failed part way. Without this every reload
98 // left the old listener open on the port.
99
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
16 if (m_socket >= 0) {
100 16 close(m_socket);
101 16 m_socket = -1;
102 }
103
104
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 6 times.
16 if (m_connList.size()) {
105
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 2 times.
10 for (int i = m_connList.size() - 1; i >= 0; i--) {
106 6 close(m_connList[i]);
107 6 m_connList.erase(m_connList.begin() + i);
108 }
109 }
110 32 }
111
112 /*
113 *
114 */
115 8 void RunConnectionThread3D(HTTPVirtualDisplay3DOutput* VirtualDisplay) {
116 8 LogDebug(VB_CHANNELOUT, "Started 3D ConnectionThread()\n");
117 8 VirtualDisplay->ConnectionThread();
118 8 }
119
120 /*
121 *
122 */
123 8 void RunSelectThread3D(HTTPVirtualDisplay3DOutput* VirtualDisplay) {
124 8 LogDebug(VB_CHANNELOUT, "Started 3D SelectThread()\n");
125 8 VirtualDisplay->SelectThread();
126 8 }
127
128 /*
129 *
130 */
131 8 int HTTPVirtualDisplay3DOutput::Init(Json::Value config) {
132
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 LogDebug(VB_CHANNELOUT, "HTTPVirtualDisplay3DOutput::Init()\n");
133
134
3/6
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 8 times.
8 if (!VirtualDisplayBaseOutput::Init(config))
135 ✗ return 0;
136
137
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 if (config.isMember("port"))
138
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 m_port = config["port"].asInt();
139
140
3/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 7 times.
8 if (config.isMember("updateInterval"))
141
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 m_updateInterval = config["updateInterval"].asInt();
142
143 // Clamp to reasonable values (10-100ms between updates)
144 // Typical: 25ms for 40fps, 16ms for 60fps, 50ms for 20fps
145
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (m_updateInterval < 10) m_updateInterval = 10;
146
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (m_updateInterval > 100) m_updateInterval = 100;
147
148 // Enable duplicate pixels for 3D mode - we can have multiple physical lights at same coordinates
149 8 m_allowDuplicatePixels = true;
150
151 // Signal base class to allocate m_virtualDisplay buffer
152 8 m_width = -1;
153 8 m_height = -1;
154
155
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
8 if (!InitializePixelMap()) {
156 ✗ LogErr(VB_CHANNELOUT, "Error, unable to initialize pixel map\n");
157 ✗ AddOutputConfigWarning(37, "3D Virtual Display preview: could not initialize pixel map");
158 ✗ return 0;
159 }
160
161 // An empty/headerless map parses "successfully" but allocates nothing (and
162 // absurd map dimensions fail the allocation inside InitializePixelMap the
163 // same way): refuse to run with no buffer or dimensions that cannot form
164 // one, instead of bzero()'ing a null display below. Mirrors the 2D guard.
165
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
16 if (m_virtualDisplay == nullptr ||
166
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
8 virtualDisplayBufferBytes(m_width, m_height, m_bytesPerPixel) == 0) {
167 ✗ LogErr(VB_CHANNELOUT, "Error, virtual display has no usable buffer (%dx%d)\n",
168 m_width, m_height);
169 ✗ AddOutputConfigWarning(37, "3D Virtual Display preview: could not initialize pixel map");
170 ✗ return 0;
171 }
172
173 // Auto-calculate channel range from virtualdisplaymap
174
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 if (!m_pixels.empty()) {
175 8 unsigned int minChannel = m_pixels[0].ch;
176 8 unsigned int maxChannel = m_pixels[0].ch;
177
178
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 8 times.
22 for (const auto& pixel : m_pixels) {
179
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 14 times.
14 if (pixel.ch < minChannel) minChannel = pixel.ch;
180 // Each pixel uses cpp channels (typically 3 for RGB)
181 14 unsigned int pixelMaxCh = pixel.ch + pixel.cpp - 1;
182
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 if (pixelMaxCh > maxChannel) maxChannel = pixelMaxCh;
183 }
184
185 // Update our channel range to match what's in the map
186 8 m_startChannel = minChannel;
187 8 m_channelCount = (maxChannel - minChannel) + 1;
188
189
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 LogInfo(VB_CHANNELOUT, "HTTPVirtualDisplay3D auto-detected channel range: %u - %u (%u channels, %zu pixels)\n",
190 m_startChannel, maxChannel, m_channelCount, m_pixels.size());
191
192 // pixel.ch stays absolute: PrepData() is handed the whole sequence
193 // buffer, not one offset to this output, and GetRequiredChannelRanges()
194 // reports the pixels' own channels. Rebasing them by the lowest
195 // channel showed channel ch - min for every pixel whenever the map
196 // did not start at channel 1, and requested the wrong range.
197 }
198
199
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 m_screenSize = (int)virtualDisplayBufferBytes(m_width, m_height, m_bytesPerPixel);
200
201 8 bzero(m_virtualDisplay, m_screenSize);
202
203 // Bind a dual-stack socket (IPv6 with V6ONLY off) so the SSE feed
204 // is reachable on both IPv4 and IPv6 clients without two listeners.
205 8 m_socket = OpenSocket(AF_INET6, SOCK_STREAM, 0);
206
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (m_socket < 0) {
207 ✗ LogErr(VB_CHANNELOUT, "Could not create socket: %s\n", FPPstrerror(errno));
208 ✗ AddOutputConfigWarning(37, "3D Virtual Display preview: could not create socket");
209 ✗ return 0;
210 }
211
212
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 fcntl(m_socket, F_SETFL, O_NONBLOCK);
213
214 // SO_REUSEADDR so a reload can rebind while the previous instance's
215 // connections sit in TIME_WAIT. Not SO_REUSEPORT: that lets a listener
216 // that was never closed keep taking a share of the new connections.
217 8 int optval = 1;
218
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (setsockopt(m_socket, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0) {
219 ✗ LogErr(VB_CHANNELOUT, "Error turning on SO_REUSEADDR; %s\n", FPPstrerror(errno));
220 ✗ return 0;
221 }
222 8 int v6only = 0;
223 8 setsockopt(m_socket, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only));
224
225 struct sockaddr_in6 addr;
226 8 memset(&addr, 0, sizeof(addr));
227 8 addr.sin6_family = AF_INET6;
228 8 addr.sin6_addr = in6addr_any;
229 8 addr.sin6_port = htons(m_port);
230
231 8 int rc = bind(m_socket, (struct sockaddr*)&addr, sizeof(addr));
232
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (rc < 0) {
233 ✗ LogErr(VB_CHANNELOUT, "Could not bind socket: %s\n", FPPstrerror(errno));
234 ✗ AddOutputConfigWarning(37, "3D Virtual Display preview: could not bind to its port (already in use?)");
235 ✗ return 0;
236 }
237
238 // Increase listen backlog to handle rapid reconnections (hard refresh)
239 8 rc = listen(m_socket, 32);
240
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (rc < 0) {
241 ✗ LogErr(VB_CHANNELOUT, "Could not listen on socket: %s\n", FPPstrerror(errno));
242 ✗ AddOutputConfigWarning(37, "3D Virtual Display preview: could not listen on its socket");
243 ✗ return 0;
244 }
245
246
3/8
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 8 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
8 m_connThread = new std::thread(RunConnectionThread3D, this);
247
3/8
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 8 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
8 m_selectThread = new std::thread(RunSelectThread3D, this);
248
249
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 LogInfo(VB_CHANNELOUT, "HTTPVirtualDisplay3D listening on port %d\n", m_port);
250 8 return 1;
251 }
252
253 /*
254 *
255 */
256 16 int HTTPVirtualDisplay3DOutput::Close(void) {
257 16 LogDebug(VB_CHANNELOUT, "HTTPVirtualDisplay3DOutput::Close()\n");
258
259 16 return VirtualDisplayBaseOutput::Close();
260 }
261
262 /*
263 *
264 */
265 8 void HTTPVirtualDisplay3DOutput::ConnectionThread(void) {
266
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 SetThreadName("FPP-HTTPVD3D-Con");
267 int client;
268
269
2/2
✓ Branch 0 taken 190 times.
✓ Branch 1 taken 8 times.
198 while (m_running) {
270
1/2
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
190 client = AcceptSocket(m_socket, NULL, NULL);
271
272
2/2
✓ Branch 0 taken 35 times.
✓ Branch 1 taken 155 times.
190 if (client >= 0) {
273 // Enable TCP keepalive to detect dead connections
274 35 int keepalive = 1;
275 35 setsockopt(client, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive));
276
277 // Set socket to non-blocking to avoid write() blocking on dead connections
278
1/2
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
35 int flags = fcntl(client, F_GETFL, 0);
279
1/2
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
35 fcntl(client, F_SETFL, (flags < 0 ? 0 : flags) | O_NONBLOCK);
280
281 35 auto t = std::time(nullptr);
282 struct tm tm;
283 35 localtime_r(&t, &tm);
284
1/2
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
35 std::stringstream sstr;
285
1/2
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
35 sstr << std::put_time(&tm, "%a %b %d %H:%M:%S %Z %Y");
286
287 35 std::string sseResp;
288 sseResp =
289 "HTTP/1.1 200 OK\r\n"
290 "Content-Type: text/event-stream;charset=UTF-8\r\n"
291 "Transfer-Encoding: chunked\r\n"
292 "Connection: close\r\n"
293
1/2
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
35 "Date: ";
294
2/4
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35 times.
✗ Branch 3 not taken.
35 sseResp += sstr.str();
295 sseResp +=
296 "\r\n"
297 "Server: fppd\r\n"
298
1/2
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
35 "X-Powered-By: FPP/";
299
2/4
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35 times.
✗ Branch 3 not taken.
35 sseResp += getFPPVersion();
300 sseResp += "\r\n"
301 "Cache-Control: no-cache, private\r\n"
302 "Access-Control-Allow-Origin: *\r\n"
303 "Access-Control-Allow-Credentials: true\r\n"
304
1/2
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
35 "\r\n";
305
306 // Reset our display cache so we draw everything needed
307 35 bzero(m_virtualDisplay, m_screenSize);
308
309
1/2
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
35 std::unique_lock<std::mutex> lock(m_connListLock);
310
1/2
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
35 m_connList.push_back(client);
311
312
1/2
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
35 write(client, sseResp.c_str(), sseResp.length());
313
314 35 m_connListChanged = true;
315
316
1/2
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
35 LogDebug(VB_CHANNELOUT, "3D Virtual Display client connected\n");
317 35 }
318
319
1/2
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
190 usleep(100000);
320 }
321 8 }
322
323 /*
324 *
325 */
326 8 void HTTPVirtualDisplay3DOutput::SelectThread(void) {
327
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 SetThreadName("FPP-HTTPVD3D-Sel");
328 fd_set active_fd_set;
329 fd_set read_fd_set;
330 int selectResult;
331 struct timeval timeout;
332 char buf[1024];
333 int bytesRead;
334
335
2/2
✓ Branch 0 taken 128 times.
✓ Branch 1 taken 8 times.
136 FD_ZERO(&active_fd_set);
336
337
2/2
✓ Branch 0 taken 31 times.
✓ Branch 1 taken 8 times.
39 while (m_running) {
338
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 7 times.
31 if (m_connListChanged) {
339
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 LogDebug(VB_CHANNELOUT, "3D Connection list changed, rebuilding active_fd-set\n");
340
2/2
✓ Branch 0 taken 384 times.
✓ Branch 1 taken 24 times.
408 FD_ZERO(&active_fd_set);
341
342
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 std::unique_lock<std::mutex> lock(m_connListLock);
343
344
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 24 times.
32 for (int i = 0; i < m_connList.size(); i++) {
345 8 FD_SET(m_connList[i], &active_fd_set);
346 }
347
348 24 m_connListChanged = false;
349 24 }
350
351 31 timeout.tv_sec = 1;
352 31 timeout.tv_usec = 0;
353
354 31 read_fd_set = active_fd_set;
355
356
1/2
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
31 selectResult = select(FD_SETSIZE, &read_fd_set, NULL, NULL, &timeout);
357
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 31 times.
31 if (selectResult < 0) {
358 ✗ if (errno == EINTR) {
359 23 continue;
360 } else {
361 ✗ LogErr(VB_CHANNELOUT, "Main select() failed\n");
362 }
363
2/2
✓ Branch 0 taken 23 times.
✓ Branch 1 taken 8 times.
31 } else if (selectResult == 0) {
364 // Nothing to do
365 23 continue;
366 }
367
368
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 std::unique_lock<std::mutex> lock(m_connListLock);
369
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 8 times.
16 for (int i = m_connList.size() - 1; i >= 0; i--) {
370
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 if (FD_ISSET(m_connList[i], &read_fd_set)) {
371
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 bytesRead = recv(m_connList[i], buf, sizeof(buf), 0);
372
373
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (bytesRead > 0) {
374 ✗ LogDebug(VB_CHANNELOUT, "Data read from 3D socket %d, connection %d \n", m_connList[i], i);
375
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 } else if (bytesRead == 0) {
376
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 LogDebug(VB_CHANNELOUT, "Closing 3D socket %d, connection %d\n", m_connList[i], i);
377
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 close(m_connList[i]);
378
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 m_connList.erase(m_connList.begin() + i);
379 8 m_connListChanged = true;
380 } else {
381 ✗ LogErr(VB_CHANNELOUT, "Read failed for 3D socket %d, connection %d. Closing connection.\n", m_connList[i], i);
382 ✗ close(m_connList[i]);
383 ✗ m_connList.erase(m_connList.begin() + i);
384 ✗ m_connListChanged = true;
385 }
386 }
387 }
388 8 }
389 8 }
390
391 /*
392 *
393 */
394 // The output thread's whole budget for preview writes in one frame, and how
395 // many frames in a row a client may refuse before it is dropped.
396 static constexpr uint64_t SSE_FRAME_BUDGET_MS = 20;
397 static constexpr int SSE_MAX_DROPPED_FRAMES = 200;
398
399 325 int HTTPVirtualDisplay3DOutput::WriteSSEPacket(int fd, const std::string& data, uint64_t deadlineMS) {
400 325 int len = data.size();
401 325 std::string sendData;
402
403 // Pre-allocate to avoid reallocations
404
1/2
✓ Branch 0 taken 325 times.
✗ Branch 1 not taken.
325 sendData.reserve(20 + len); // hex length + delimiters + data
405
406 // Convert length to hex directly into string
407 char hexBuf[16];
408 325 snprintf(hexBuf, sizeof(hexBuf), "%x", len);
409
410
1/2
✓ Branch 0 taken 325 times.
✗ Branch 1 not taken.
325 sendData = hexBuf;
411
1/2
✓ Branch 0 taken 325 times.
✗ Branch 1 not taken.
325 sendData += "\r\n";
412
1/2
✓ Branch 0 taken 325 times.
✗ Branch 1 not taken.
325 sendData += data;
413
1/2
✓ Branch 0 taken 325 times.
✗ Branch 1 not taken.
325 sendData += "\r\n";
414
415 325 size_t total = sendData.size();
416 325 size_t sent = 0;
417
418
2/2
✓ Branch 0 taken 325 times.
✓ Branch 1 taken 301 times.
626 while (sent < total) {
419
1/2
✓ Branch 0 taken 325 times.
✗ Branch 1 not taken.
325 ssize_t written = write(fd, sendData.c_str() + sent, total - sent);
420
421
2/2
✓ Branch 0 taken 301 times.
✓ Branch 1 taken 24 times.
325 if (written > 0) {
422 301 sent += written;
423 301 continue;
424 }
425
426
2/4
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 24 times.
24 if (written < 0 && errno == EINTR) {
427 ✗ continue;
428 }
429
430
3/6
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 24 times.
24 if (written < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
431 ✗ if (sent == 0) {
432 // The client isn't draining fast enough and the send buffer is
433 // full. Drop this frame rather than the connection - nothing is
434 // on the wire yet, and every 3D frame is a full refresh so the
435 // next one brings the client straight back up to date.
436 ✗ LogExcess(VB_CHANNELOUT, "3D SSE send buffer full, dropping frame\n");
437 ✗ return 0;
438 }
439
440 // We're part way through a chunk. We can't abandon it - the client
441 // is counting out the bytes we promised in the chunk header, so a
442 // truncated chunk desyncs the stream permanently. Wait (briefly)
443 // for room and finish it.
444 struct pollfd pfd;
445 ✗ pfd.fd = fd;
446 ✗ pfd.events = POLLOUT;
447 // Bounded by the frame's budget, not per client per stall: a
448 // client trickling a few bytes at a time otherwise held the
449 // output thread (and every output) for as long as it liked.
450 ✗ long long left = (long long)deadlineMS - (long long)GetMonotonicTimeMS();
451 ✗ if (left <= 0) {
452 ✗ LogDebug(VB_CHANNELOUT, "SSE client too slow for this frame (%zu/%zu bytes), closing connection\n", sent, total);
453 ✗ return -1;
454 }
455 ✗ int pr = poll(&pfd, 1, (int)std::min<long long>(left, 250));
456 ✗ if (pr > 0) {
457 ✗ continue;
458 }
459 ✗ if (pr < 0 && errno == EINTR) {
460 ✗ continue;
461 }
462
463 ✗ LogDebug(VB_CHANNELOUT, "3D SSE stalled part way through a chunk (%zu/%zu bytes), closing connection\n",
464 sent, total);
465 ✗ return -1;
466 }
467
468 // EPIPE/ECONNRESET/ENOTCONN and anything else: the connection is gone
469
2/4
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
24 LogDebug(VB_CHANNELOUT, "3D SSE write failed (connection closed): %s\n", FPPstrerror(errno));
470 24 return -1;
471 }
472
473 301 return 1;
474 325 }
475
476 /*
477 * PrepData for 3D - sends pixel index instead of 2D coordinates
478 * Format: RGB_COLOR:PIXEL_INDEX;PIXEL_INDEX;PIXEL_INDEX|...
479 */
480 386 void HTTPVirtualDisplay3DOutput::PrepData(unsigned char* channelData) {
481 static int id = 0;
482 static bool lastFrameWasBlack = false;
483
484
1/2
✓ Branch 0 taken 386 times.
✗ Branch 1 not taken.
386 LogExcess(VB_CHANNELOUT, "HTTPVirtualDisplay3DOutput::PrepData(%p)\n",
485 channelData);
486
487 {
488 // Short circuit if no current connections
489
1/2
✓ Branch 0 taken 386 times.
✗ Branch 1 not taken.
386 std::unique_lock<std::mutex> lock(m_connListLock);
490
2/2
✓ Branch 0 taken 73 times.
✓ Branch 1 taken 313 times.
386 if (!m_connList.size()) {
491 73 m_sseData.clear();
492 73 return;
493 }
494
2/2
✓ Branch 0 taken 313 times.
✓ Branch 1 taken 73 times.
386 }
495
496 // We run at the sequence frame rate, which can now be well above anything a
497 // browser can render (or even parse) - every 3D frame is a full dump of all
498 // lit pixels, not a delta. Sending faster than the client drains just piles
499 // frames up in the socket and proxy buffers, so the preview falls further and
500 // further behind playback. Rate limit to m_updateInterval instead.
501 //
502 // The deadline is advanced by whole intervals rather than reset to "now" so
503 // that an update interval which isn't a multiple of the frame time still
504 // averages out to the requested rate instead of aliasing down to the next
505 // frame boundary (eg. 25ms against a 20ms sequence gives 40fps, not 25fps).
506
1/2
✓ Branch 0 taken 313 times.
✗ Branch 1 not taken.
313 long long now = GetMonotonicTimeMS();
507
2/2
✓ Branch 0 taken 31 times.
✓ Branch 1 taken 282 times.
313 if (now < m_nextSendMS) {
508 31 m_sseData.clear();
509 31 return;
510 }
511 282 m_nextSendMS += m_updateInterval;
512
2/2
✓ Branch 0 taken 251 times.
✓ Branch 1 taken 31 times.
282 if (m_nextSendMS <= now) {
513 // First frame, or we've been idle/stalled long enough that the deadline
514 // is stale - resync rather than sending a burst to catch up.
515 251 m_nextSendMS = now + m_updateInterval;
516 }
517
518 // Fast black detection - check if all channel data is zero (sequence ended/stopped)
519 static int blackCheckCounter = 0;
520 282 bool allBlack = false;
521 282 bool forceBlackUpdate = false;
522
523
6/6
✓ Branch 0 taken 226 times.
✓ Branch 1 taken 56 times.
✓ Branch 2 taken 21 times.
✓ Branch 3 taken 205 times.
✓ Branch 4 taken 77 times.
✓ Branch 5 taken 205 times.
282 if (lastFrameWasBlack || (++blackCheckCounter >= 10)) {
524 77 blackCheckCounter = 0;
525 77 allBlack = true;
526
6/6
✓ Branch 0 taken 202 times.
✓ Branch 1 taken 66 times.
✓ Branch 2 taken 191 times.
✓ Branch 3 taken 11 times.
✓ Branch 4 taken 191 times.
✓ Branch 5 taken 77 times.
268 for (int i = 0; i < m_pixels.size() && allBlack; i++) {
527 unsigned char r, g, b;
528
1/2
✓ Branch 0 taken 191 times.
✗ Branch 1 not taken.
191 GetPixelRGB(m_pixels[i], channelData, r, g, b);
529
4/6
✓ Branch 0 taken 171 times.
✓ Branch 1 taken 20 times.
✓ Branch 2 taken 171 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 171 times.
191 if (r != 0 || g != 0 || b != 0) {
530 20 allBlack = false;
531 }
532 }
533
534
4/4
✓ Branch 0 taken 57 times.
✓ Branch 1 taken 20 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 56 times.
77 forceBlackUpdate = (allBlack && !lastFrameWasBlack);
535 77 lastFrameWasBlack = allBlack;
536 }
537
538 282 int pixelsChanged = 0;
539 unsigned char r, g, b;
540
541 // Use unordered_map for O(1) lookup instead of std::map O(log n)
542 // Key is 24-bit color packed into uint32_t for fast hashing
543
3/4
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 277 times.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
282 static std::unordered_map<uint32_t, std::string> colors;
544 282 colors.clear();
545
546 char pixelIdx[16];
547 static const char* const base64 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/";
548
549 // Pre-build color string lookup for all 64x64x64 = 262144 possible RGB666 values
550 // This avoids snprintf in the hot loop
551 static bool colorLookupInitialized = false;
552 static char colorLookup[64][64][64][4];
553
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 277 times.
282 if (!colorLookupInitialized) {
554
2/2
✓ Branch 0 taken 320 times.
✓ Branch 1 taken 5 times.
325 for (int ri = 0; ri < 64; ri++) {
555
2/2
✓ Branch 0 taken 20480 times.
✓ Branch 1 taken 320 times.
20800 for (int gi = 0; gi < 64; gi++) {
556
2/2
✓ Branch 0 taken 1310720 times.
✓ Branch 1 taken 20480 times.
1331200 for (int bi = 0; bi < 64; bi++) {
557 1310720 colorLookup[ri][gi][bi][0] = base64[ri];
558 1310720 colorLookup[ri][gi][bi][1] = base64[gi];
559 1310720 colorLookup[ri][gi][bi][2] = base64[bi];
560 1310720 colorLookup[ri][gi][bi][3] = '\0';
561 }
562 }
563 }
564 5 colorLookupInitialized = true;
565 }
566
567
2/2
✓ Branch 0 taken 650 times.
✓ Branch 1 taken 282 times.
932 for (int i = 0; i < m_pixels.size(); i++) {
568
1/2
✓ Branch 0 taken 650 times.
✗ Branch 1 not taken.
650 GetPixelRGB(m_pixels[i], channelData, r, g, b);
569
570 // 18-bit RGB666 pixel data passed over Event Stream in 3 bytes
571 650 r = r >> 2;
572 650 g = g >> 2;
573 650 b = b >> 2;
574
575 // For 3D mode: Send ALL non-black pixels every frame
576 // 3D sends pixel indexes, not buffer positions, and never reads
577 // m_virtualDisplay back, so there is nothing to cache here.
578
6/8
✓ Branch 0 taken 230 times.
✓ Branch 1 taken 420 times.
✓ Branch 2 taken 230 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 230 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3 times.
✓ Branch 7 taken 227 times.
650 if (r != 0 || g != 0 || b != 0 || forceBlackUpdate) {
579 // Pack RGB into 32-bit key for fast hash lookup
580 423 uint32_t colorKey = (r << 16) | (g << 8) | b;
581
582 // Use integer to string conversion (faster than snprintf)
583 423 int len = 0;
584 423 int temp = i;
585 do {
586 423 pixelIdx[len++] = '0' + (temp % 10);
587 423 temp /= 10;
588
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 423 times.
423 } while (temp > 0);
589 // Reverse the digits
590
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 423 times.
423 for (int j = 0; j < len / 2; j++) {
591 ✗ char t = pixelIdx[j];
592 ✗ pixelIdx[j] = pixelIdx[len - 1 - j];
593 ✗ pixelIdx[len - 1 - j] = t;
594 }
595 423 pixelIdx[len] = '\0';
596
597
1/2
✓ Branch 0 taken 423 times.
✗ Branch 1 not taken.
423 auto colorIt = colors.find(colorKey);
598
2/2
✓ Branch 0 taken 158 times.
✓ Branch 1 taken 265 times.
423 if (colorIt != colors.end()) {
599
1/2
✓ Branch 0 taken 158 times.
✗ Branch 1 not taken.
158 colorIt->second += ';';
600
1/2
✓ Branch 0 taken 158 times.
✗ Branch 1 not taken.
158 colorIt->second += pixelIdx;
601 } else {
602 // First pixel with this color - create entry with "RGB:index"
603 265 std::string entry;
604
1/2
✓ Branch 0 taken 265 times.
✗ Branch 1 not taken.
265 entry.reserve(24);
605
1/2
✓ Branch 0 taken 265 times.
✗ Branch 1 not taken.
265 entry = colorLookup[r][g][b];
606
1/2
✓ Branch 0 taken 265 times.
✗ Branch 1 not taken.
265 entry += ':';
607
1/2
✓ Branch 0 taken 265 times.
✗ Branch 1 not taken.
265 entry += pixelIdx;
608
1/2
✓ Branch 0 taken 265 times.
✗ Branch 1 not taken.
265 colors[colorKey] = std::move(entry);
609 265 }
610
611 423 pixelsChanged++;
612 }
613 }
614
615
2/2
✓ Branch 0 taken 221 times.
✓ Branch 1 taken 61 times.
282 if (colors.size()) {
616 // Pre-allocate to avoid reallocations during string building
617 221 m_sseData.clear();
618
1/2
✓ Branch 0 taken 221 times.
✗ Branch 1 not taken.
221 m_sseData.reserve(128 + colors.size() * 16);
619
620
1/2
✓ Branch 0 taken 221 times.
✗ Branch 1 not taken.
221 m_sseData = "id: ";
621
2/4
✓ Branch 0 taken 221 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 221 times.
✗ Branch 3 not taken.
221 m_sseData += std::to_string(id) + "\r\n";
622
1/2
✓ Branch 0 taken 221 times.
✗ Branch 1 not taken.
221 m_sseData += "event: message\r\n";
623
1/2
✓ Branch 0 taken 221 times.
✗ Branch 1 not taken.
221 m_sseData += "data: ";
624
625 221 std::string data2;
626
1/2
✓ Branch 0 taken 221 times.
✗ Branch 1 not taken.
221 data2.reserve(colors.size() * 16);
627
628 221 bool first = true;
629
2/2
✓ Branch 0 taken 265 times.
✓ Branch 1 taken 221 times.
486 for (const auto& pair : colors) {
630
2/2
✓ Branch 0 taken 44 times.
✓ Branch 1 taken 221 times.
265 if (!first)
631
1/2
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
44 data2 += "|";
632 else
633 221 first = false;
634
635
1/2
✓ Branch 0 taken 265 times.
✗ Branch 1 not taken.
265 data2 += pair.second;
636 }
637
2/4
✓ Branch 0 taken 221 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 221 times.
✗ Branch 3 not taken.
221 m_sseData += data2 + "\r\n\r\n";
638
639
1/2
✓ Branch 0 taken 221 times.
✗ Branch 1 not taken.
221 LogExcess(VB_CHANNELOUT, "3D PixelsChanged: %d, Colors: %d, Data Size: %d\n",
640 pixelsChanged, colors.size(), m_sseData.size());
641 221 } else {
642 // Send empty update to keep connection alive
643
1/2
✓ Branch 0 taken 61 times.
✗ Branch 1 not taken.
61 m_sseData = "id: ";
644
2/4
✓ Branch 0 taken 61 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 61 times.
✗ Branch 3 not taken.
61 m_sseData += std::to_string(id) + "\r\n";
645
1/2
✓ Branch 0 taken 61 times.
✗ Branch 1 not taken.
61 m_sseData += "event: ping\r\n";
646
1/2
✓ Branch 0 taken 61 times.
✗ Branch 1 not taken.
61 m_sseData += "data: \r\n\r\n";
647 }
648
649 282 id++;
650 }
651
652 /*
653 *
654 */
655 376 int HTTPVirtualDisplay3DOutput::SendData(unsigned char* channelData) {
656
2/2
✓ Branch 0 taken 278 times.
✓ Branch 1 taken 98 times.
376 if (m_sseData != "") {
657
1/2
✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
278 std::unique_lock<std::mutex> lock(m_connListLock);
658
1/2
✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
278 const uint64_t deadline = GetMonotonicTimeMS() + SSE_FRAME_BUDGET_MS;
659 // Iterate backwards so we can safely remove elements
660
2/2
✓ Branch 0 taken 325 times.
✓ Branch 1 taken 278 times.
603 for (int i = m_connList.size() - 1; i >= 0; i--) {
661
1/2
✓ Branch 0 taken 325 times.
✗ Branch 1 not taken.
325 int result = WriteSSEPacket(m_connList[i], m_sseData, deadline);
662
2/8
✗ Branch 0 not taken.
✓ Branch 1 taken 325 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 325 times.
325 if (result == 0 && ++m_dropCounts[m_connList[i]] > SSE_MAX_DROPPED_FRAMES) {
663 // A client that has taken nothing for this long is gone or
664 // stuck; dropping its frames forever also cost every frame a
665 // resync of the pixels it never received.
666 ✗ LogDebug(VB_CHANNELOUT, "SSE client %d took no frames for %d frames, closing it\n", m_connList[i], SSE_MAX_DROPPED_FRAMES);
667 ✗ result = -1;
668
2/2
✓ Branch 0 taken 301 times.
✓ Branch 1 taken 24 times.
325 } else if (result > 0) {
669
1/2
✓ Branch 0 taken 301 times.
✗ Branch 1 not taken.
301 m_dropCounts.erase(m_connList[i]);
670 }
671
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 301 times.
325 if (result < 0) {
672 // Connection is dead, remove it
673
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 LogDebug(VB_CHANNELOUT, "Removing dead 3D SSE connection %d\n", m_connList[i]);
674
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 close(m_connList[i]);
675
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 m_connList.erase(m_connList.begin() + i);
676 24 m_connListChanged = true;
677 }
678 }
679 // forget counts for connections the select thread has since closed
680
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 278 times.
278 for (auto it = m_dropCounts.begin(); it != m_dropCounts.end();) {
681 ✗ if (std::find(m_connList.begin(), m_connList.end(), it->first) == m_connList.end()) {
682 ✗ it = m_dropCounts.erase(it);
683 } else {
684 ✗ ++it;
685 }
686 }
687 278 }
688
689 376 return m_channelCount;
690 }
691