GCC Code Coverage Report


Directory: ./
File: channeloutput/HTTPVirtualDisplay.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 305 333 91.6%
Functions: 14 15 93.3%
Branches: 260 460 56.5%

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
24 #include <ctime>
25 #include <iomanip>
26
27 #include "../fppversion.h"
28 #include "../log.h"
29 #include "../common.h"
30
31 #include "HTTPVirtualDisplay.h"
32 #include "ChannelOutputSetup.h"
33
34 #include "Plugin.h"
35 #include "../SysSocket.h"
36
37 class HTTPVirtualDisplayPlugin : public FPPPlugins::Plugin, public FPPPlugins::ChannelOutputPlugin {
38 public:
39 17 HTTPVirtualDisplayPlugin() :
40
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
51 FPPPlugins::Plugin("HTTPVirtualDisplay") {
41 17 }
42 26 virtual ChannelOutput* createChannelOutput(unsigned int startChannel, unsigned int channelCount) override {
43
2/6
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 26 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
52 return new HTTPVirtualDisplayOutput(startChannel, channelCount);
44 }
45 };
46
47 extern "C" {
48 17 FPPPlugins::Plugin* createPlugin() {
49
2/6
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 17 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
34 return new HTTPVirtualDisplayPlugin();
50 }
51 }
52 /////////////////////////////////////////////////////////////////////////////
53
54 /*
55 *
56 */
57 26 HTTPVirtualDisplayOutput::HTTPVirtualDisplayOutput(unsigned int startChannel,
58 26 unsigned int channelCount) :
59 VirtualDisplayBaseOutput(startChannel, channelCount),
60 26 m_port(HTTPVIRTUALDISPLAYPORT),
61 26 m_screenSize(0),
62 26 m_updateInterval(25), // Default: 25ms (40fps), about all a browser can render
63 26 m_nextSendMS(0),
64 26 m_socket(-1),
65 26 m_running(true),
66 26 m_connListChanged(true),
67 26 m_resetCache(false),
68 26 m_connThread(nullptr),
69 52 m_selectThread(nullptr) {
70
1/2
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
26 LogDebug(VB_CHANNELOUT, "HTTPVirtualDisplayOutput::HTTPVirtualDisplayOutput(%u, %u)\n",
71 startChannel, channelCount);
72
73 26 m_bytesPerPixel = 3;
74 26 m_bpp = 24;
75 26 }
76
77 /*
78 *
79 */
80 104 HTTPVirtualDisplayOutput::~HTTPVirtualDisplayOutput() {
81 52 LogDebug(VB_CHANNELOUT, "HTTPVirtualDisplayOutput::~HTTPVirtualDisplayOutput()\n");
82
83 52 m_running = false;
84
85
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 2 times.
52 if (m_connThread) {
86 48 m_connThread->join();
87
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
48 delete m_connThread;
88 48 m_connThread = nullptr;
89 }
90
91
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 2 times.
52 if (m_selectThread) {
92 48 m_selectThread->join();
93
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
48 delete m_selectThread;
94 48 m_selectThread = nullptr;
95 }
96
97
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 2 times.
52 if (m_socket >= 0) {
98 48 close(m_socket);
99 48 m_socket = -1;
100 }
101
102
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 25 times.
52 if (m_connList.size()) {
103
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 1 times.
14 for (int i = m_connList.size() - 1; i >= 0; i--) {
104 12 close(m_connList[i]);
105 12 m_connList.erase(m_connList.begin() + i);
106 }
107 }
108 104 }
109
110 /*
111 *
112 */
113 24 void RunConnectionThread(HTTPVirtualDisplayOutput* VirtualDisplay) {
114 24 LogDebug(VB_CHANNELOUT, "Started ConnectionThread()\n");
115 24 VirtualDisplay->ConnectionThread();
116 24 }
117
118 /*
119 *
120 */
121 24 void RunSelectThread(HTTPVirtualDisplayOutput* VirtualDisplay) {
122 24 LogDebug(VB_CHANNELOUT, "Started SelectThread()\n");
123 24 VirtualDisplay->SelectThread();
124 24 }
125
126 /*
127 *
128 */
129 26 int HTTPVirtualDisplayOutput::Init(Json::Value config) {
130
1/2
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
26 LogDebug(VB_CHANNELOUT, "HTTPVirtualDisplayOutput::Init()\n");
131
132
3/6
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 26 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 26 times.
26 if (!VirtualDisplayBaseOutput::Init(config))
133 ✗ return 0;
134
135
2/4
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 26 times.
✗ Branch 3 not taken.
26 if (config.isMember("port"))
136
2/4
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 26 times.
✗ Branch 3 not taken.
26 m_port = config["port"].asInt();
137
138
2/4
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 26 times.
26 if (config.isMember("updateInterval"))
139 ✗ m_updateInterval = config["updateInterval"].asInt();
140
141 // Clamp to reasonable values (10-100ms between updates)
142 // Typical: 25ms for 40fps, 16ms for 60fps, 50ms for 20fps
143
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 26 times.
26 if (m_updateInterval < 10) m_updateInterval = 10;
144
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 26 times.
26 if (m_updateInterval > 100) m_updateInterval = 100;
145
146 // Signal base class to allocate m_virtualDisplay buffer
147 26 m_width = -1;
148 26 m_height = -1;
149
150
3/4
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 25 times.
26 if (!InitializePixelMap()) {
151
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_CHANNELOUT, "Error, unable to initialize pixel map\n");
152
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 AddOutputConfigWarning(37, "Virtual Display preview: could not initialize pixel map");
153 1 return 0;
154 }
155
156 // An empty map parses "successfully" but allocates nothing (and absurd map
157 // dimensions fail the allocation above the same way): refuse to run with
158 // no buffer or dimensions that cannot form one, instead of crashing on
159 // the first draw/status poll.
160
4/4
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 24 times.
49 if (m_virtualDisplay == nullptr ||
161
2/4
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 24 times.
24 virtualDisplayBufferBytes(m_width, m_height, m_bytesPerPixel) == 0) {
162
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_CHANNELOUT, "Error, virtual display has no usable buffer (%dx%d)\n",
163 m_width, m_height);
164
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 AddOutputConfigWarning(37, "Virtual Display preview: could not initialize pixel map");
165 1 return 0;
166 }
167
168
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 m_screenSize = (int)virtualDisplayBufferBytes(m_width, m_height, m_bytesPerPixel);
169
170 // Worst case a frame touches every pixel; size this once so the hot loop
171 // never reallocates
172
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 m_cacheRollback.reserve(m_pixels.size() * 3);
173
174 24 bzero(m_virtualDisplay, m_screenSize);
175
176 // Bind a dual-stack socket (IPv6 with V6ONLY off) so the SSE feed
177 // is reachable on both IPv4 and IPv6 clients without needing two
178 // listeners. Apache's mod_proxy talks to us on localhost; both
179 // 127.0.0.1 and ::1 land here.
180 24 m_socket = OpenSocket(AF_INET6, SOCK_STREAM, 0);
181
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 24 times.
24 if (m_socket < 0) {
182 ✗ LogErr(VB_CHANNELOUT, "Could not create socket: %s\n", FPPstrerror(errno));
183 ✗ AddOutputConfigWarning(37, "Virtual Display preview: could not create socket");
184 ✗ return 0;
185 }
186
187
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 fcntl(m_socket, F_SETFL, O_NONBLOCK);
188
189 // SO_REUSEADDR so a reload can rebind while the previous instance's
190 // connections sit in TIME_WAIT. Not SO_REUSEPORT: that lets a listener
191 // that was never closed keep taking a share of the new connections.
192 24 int optval = 1;
193
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 24 times.
24 if (setsockopt(m_socket, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0) {
194 ✗ LogErr(VB_CHANNELOUT, "Error turning on SO_REUSEADDR; %s\n", FPPstrerror(errno));
195 ✗ return 0;
196 }
197 24 int v6only = 0;
198 24 setsockopt(m_socket, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only));
199
200 struct sockaddr_in6 addr;
201 24 memset(&addr, 0, sizeof(addr));
202 24 addr.sin6_family = AF_INET6;
203 24 addr.sin6_addr = in6addr_any;
204 24 addr.sin6_port = htons(m_port);
205
206 24 int rc = bind(m_socket, (struct sockaddr*)&addr, sizeof(addr));
207
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 24 times.
24 if (rc < 0) {
208 ✗ LogErr(VB_CHANNELOUT, "Could not bind socket: %s\n", FPPstrerror(errno));
209 ✗ AddOutputConfigWarning(37, "Virtual Display preview: could not bind to its port (already in use?)");
210 ✗ return 0;
211 }
212
213 24 rc = listen(m_socket, 5);
214
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 24 times.
24 if (rc < 0) {
215 ✗ LogErr(VB_CHANNELOUT, "Could not listen on socket: %s\n", FPPstrerror(errno));
216 ✗ AddOutputConfigWarning(37, "Virtual Display preview: could not listen on its socket");
217 ✗ return 0;
218 }
219
220
3/8
✓ 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.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
24 m_connThread = new std::thread(RunConnectionThread, this);
221
3/8
✓ 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.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
24 m_selectThread = new std::thread(RunSelectThread, this);
222 24 return 1;
223 }
224
225 /*
226 *
227 */
228 24 int HTTPVirtualDisplayOutput::Close(void) {
229 24 LogDebug(VB_CHANNELOUT, "HTTPVirtualDisplayOutput::Close()\n");
230
231 24 return VirtualDisplayBaseOutput::Close();
232 }
233
234 /*
235 *
236 */
237 24 void HTTPVirtualDisplayOutput::ConnectionThread(void) {
238
2/4
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
24 SetThreadName("FPP-HTTPVD-Con");
239 int client;
240
241
2/2
✓ Branch 0 taken 793 times.
✓ Branch 1 taken 24 times.
817 while (m_running) {
242
1/2
✓ Branch 0 taken 793 times.
✗ Branch 1 not taken.
793 client = AcceptSocket(m_socket, NULL, NULL);
243
244
2/2
✓ Branch 0 taken 89 times.
✓ Branch 1 taken 704 times.
793 if (client >= 0) {
245 // Enable TCP keepalive to detect dead connections
246 89 int keepalive = 1;
247 89 setsockopt(client, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive));
248
249 // Accepted sockets don't inherit O_NONBLOCK from the listener, so
250 // set it explicitly - otherwise a browser that stops draining its
251 // socket blocks write() on the channel output thread, which stalls
252 // the actual light output, not just this preview.
253
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 int flags = fcntl(client, F_GETFL, 0);
254
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 fcntl(client, F_SETFL, (flags < 0 ? 0 : flags) | O_NONBLOCK);
255
256 89 auto t = std::time(nullptr);
257 struct tm tm;
258 89 localtime_r(&t, &tm);
259
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 std::stringstream sstr;
260
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 sstr << std::put_time(&tm, "%a %b %d %H:%M:%S %Z %Y");
261
262 89 std::string sseResp;
263 sseResp =
264 "HTTP/1.1 200 OK\r\n"
265 "Content-Type: text/event-stream;charset=UTF-8\r\n"
266 "Transfer-Encoding: chunked\r\n"
267 "Connection: close\r\n"
268
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 "Date: ";
269
2/4
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 89 times.
✗ Branch 3 not taken.
89 sseResp += sstr.str();
270 sseResp +=
271 "\r\n"
272 "Server: fppd\r\n"
273
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 "X-Powered-By: FPP/";
274
2/4
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 89 times.
✗ Branch 3 not taken.
89 sseResp += getFPPVersion();
275 sseResp += "\r\n"
276 "Cache-Control: no-cache, private\r\n"
277 "Access-Control-Allow-Origin: *\r\n"
278 "Access-Control-Allow-Credentials: true\r\n"
279
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 "\r\n";
280
281 // Have the output thread reset its display cache so the next frame
282 // draws everything; set before the client is listed, so a frame that
283 // sees the client also sees the reset.
284
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 std::unique_lock<std::mutex> lock(m_connListLock);
285 89 m_resetCache = true;
286
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 m_connList.push_back(client);
287
288
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 write(client, sseResp.c_str(), sseResp.length());
289
290 89 m_connListChanged = true;
291 89 }
292
293
1/2
✓ Branch 0 taken 793 times.
✗ Branch 1 not taken.
793 usleep(100000);
294 }
295 24 }
296
297 /*
298 *
299 */
300 24 void HTTPVirtualDisplayOutput::SelectThread(void) {
301
2/4
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
24 SetThreadName("FPP-HTTPVD-Sel");
302 fd_set active_fd_set;
303 fd_set read_fd_set;
304 int selectResult;
305 struct timeval timeout;
306 char buf[1024];
307 int bytesRead;
308
309
2/2
✓ Branch 0 taken 384 times.
✓ Branch 1 taken 24 times.
408 FD_ZERO(&active_fd_set);
310
311
2/2
✓ Branch 0 taken 114 times.
✓ Branch 1 taken 24 times.
138 while (m_running) {
312
2/2
✓ Branch 0 taken 74 times.
✓ Branch 1 taken 40 times.
114 if (m_connListChanged) {
313
1/2
✓ Branch 0 taken 74 times.
✗ Branch 1 not taken.
74 LogDebug(VB_CHANNELOUT, "Connection list changed, rebuilding active_fd-set\n");
314
2/2
✓ Branch 0 taken 1184 times.
✓ Branch 1 taken 74 times.
1258 FD_ZERO(&active_fd_set);
315
316
1/2
✓ Branch 0 taken 74 times.
✗ Branch 1 not taken.
74 std::unique_lock<std::mutex> lock(m_connListLock);
317
318
2/2
✓ Branch 0 taken 41 times.
✓ Branch 1 taken 74 times.
115 for (int i = 0; i < m_connList.size(); i++) {
319 41 FD_SET(m_connList[i], &active_fd_set);
320 }
321
322 74 m_connListChanged = false;
323 74 }
324
325 114 timeout.tv_sec = 1;
326 114 timeout.tv_usec = 0;
327
328 114 read_fd_set = active_fd_set;
329
330
1/2
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
114 selectResult = select(FD_SETSIZE, &read_fd_set, NULL, NULL, &timeout);
331
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 114 times.
114 if (selectResult < 0) {
332 ✗ if (errno == EINTR) {
333 90 continue;
334 } else {
335 ✗ LogErr(VB_CHANNELOUT, "Main select() failed\n");
336 }
337
2/2
✓ Branch 0 taken 90 times.
✓ Branch 1 taken 24 times.
114 } else if (selectResult == 0) {
338 // Nothing to do
339 90 continue;
340 }
341
342
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 std::unique_lock<std::mutex> lock(m_connListLock);
343
2/2
✓ Branch 0 taken 39 times.
✓ Branch 1 taken 24 times.
63 for (int i = m_connList.size() - 1; i >= 0; i--) {
344
2/2
✓ Branch 0 taken 35 times.
✓ Branch 1 taken 4 times.
39 if (FD_ISSET(m_connList[i], &read_fd_set)) {
345
1/2
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
35 bytesRead = recv(m_connList[i], buf, sizeof(buf), 0);
346
347
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 35 times.
35 if (bytesRead > 0) {
348 ✗ LogDebug(VB_CHANNELOUT, "Data read from socket %d, connection %d \n", m_connList[i], i);
349
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 20 times.
35 } else if (bytesRead == 0) {
350
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 LogDebug(VB_CHANNELOUT, "Closing socket %d, connection %d\n", m_connList[i], i);
351
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 close(m_connList[i]);
352
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 m_connList.erase(m_connList.begin() + i);
353 15 m_connListChanged = true;
354 } else {
355
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 LogErr(VB_CHANNELOUT, "Read failed for socket %d, connection %d. Closing connection.\n", m_connList[i], i);
356
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 close(m_connList[i]);
357
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 m_connList.erase(m_connList.begin() + i);
358 20 m_connListChanged = true;
359 }
360 }
361 }
362 24 }
363 24 }
364
365 /*
366 *
367 */
368 // The output thread's whole budget for preview writes in one frame, and how
369 // many frames in a row a client may refuse before it is dropped.
370 static constexpr uint64_t SSE_FRAME_BUDGET_MS = 20;
371 static constexpr int SSE_MAX_DROPPED_FRAMES = 200;
372
373 1034 int HTTPVirtualDisplayOutput::WriteSSEPacket(int fd, const std::string& data, uint64_t deadlineMS) {
374 1034 int len = data.size();
375 1034 std::string sendData;
376
377 // Pre-allocate to avoid reallocations
378
1/2
✓ Branch 0 taken 1034 times.
✗ Branch 1 not taken.
1034 sendData.reserve(20 + len); // hex length + delimiters + data
379
380 // Convert length to hex directly into string
381 char hexBuf[16];
382 1034 snprintf(hexBuf, sizeof(hexBuf), "%x", len);
383
384
1/2
✓ Branch 0 taken 1034 times.
✗ Branch 1 not taken.
1034 sendData = hexBuf;
385
1/2
✓ Branch 0 taken 1034 times.
✗ Branch 1 not taken.
1034 sendData += "\r\n";
386
1/2
✓ Branch 0 taken 1034 times.
✗ Branch 1 not taken.
1034 sendData += data;
387
1/2
✓ Branch 0 taken 1034 times.
✗ Branch 1 not taken.
1034 sendData += "\r\n";
388
389 1034 size_t total = sendData.size();
390 1034 size_t sent = 0;
391
392
2/2
✓ Branch 0 taken 1038 times.
✓ Branch 1 taken 769 times.
1807 while (sent < total) {
393
1/2
✓ Branch 0 taken 1038 times.
✗ Branch 1 not taken.
1038 ssize_t written = write(fd, sendData.c_str() + sent, total - sent);
394
395
2/2
✓ Branch 0 taken 772 times.
✓ Branch 1 taken 266 times.
1038 if (written > 0) {
396 772 sent += written;
397 772 continue;
398 }
399
400
2/4
✓ Branch 0 taken 266 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 266 times.
266 if (written < 0 && errno == EINTR) {
401 ✗ continue;
402 }
403
404
4/6
✓ Branch 0 taken 266 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 45 times.
✓ Branch 3 taken 221 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 45 times.
266 if (written < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
405
2/2
✓ Branch 0 taken 218 times.
✓ Branch 1 taken 3 times.
221 if (sent == 0) {
406 // The client isn't draining fast enough and the send buffer is
407 // full. Drop this frame rather than the connection - nothing is
408 // on the wire yet. The caller resyncs the client afterwards,
409 // since the pixel changes in this payload are only sent once.
410
1/2
✓ Branch 0 taken 218 times.
✗ Branch 1 not taken.
218 LogExcess(VB_CHANNELOUT, "SSE send buffer full, dropping frame\n");
411 220 return 0;
412 }
413
414 // We're part way through a chunk. We can't abandon it - the client
415 // is counting out the bytes we promised in the chunk header, so a
416 // truncated chunk desyncs the stream permanently. Wait (briefly)
417 // for room and finish it.
418 struct pollfd pfd;
419 3 pfd.fd = fd;
420 3 pfd.events = POLLOUT;
421 // Bounded by the frame's budget, not per client per stall: a
422 // client trickling a few bytes at a time otherwise held the
423 // output thread (and every output) for as long as it liked.
424
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 long long left = (long long)deadlineMS - (long long)GetMonotonicTimeMS();
425
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
3 if (left <= 0) {
426
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogDebug(VB_CHANNELOUT, "SSE client too slow for this frame (%zu/%zu bytes), closing connection\n", sent, total);
427 1 return -1;
428 }
429
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 int pr = poll(&pfd, 1, (int)std::min<long long>(left, 250));
430
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 if (pr > 0) {
431 1 continue;
432 }
433
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1 if (pr < 0 && errno == EINTR) {
434 ✗ continue;
435 }
436
437
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogDebug(VB_CHANNELOUT, "SSE stalled part way through a chunk (%zu/%zu bytes), closing connection\n",
438 sent, total);
439 1 return -1;
440 }
441
442 // EPIPE/ECONNRESET/ENOTCONN and anything else: the connection is gone
443
2/4
✓ Branch 0 taken 45 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 45 times.
✗ Branch 3 not taken.
45 LogDebug(VB_CHANNELOUT, "SSE write failed (connection closed): %s\n", FPPstrerror(errno));
444 45 return -1;
445 }
446
447 769 return 1;
448 1034 }
449
450 /*
451 *
452 */
453 1406 void HTTPVirtualDisplayOutput::PrepData(unsigned char* channelData) {
454 static int id = 0;
455 static bool lastFrameWasBlack = false;
456
457
1/2
✓ Branch 0 taken 1406 times.
✗ Branch 1 not taken.
1406 LogExcess(VB_CHANNELOUT, "HTTPVirtualDisplayOutput::PrepData(%p)\n",
458 channelData);
459
460 {
461 // Short circuit if no current connections
462
1/2
✓ Branch 0 taken 1406 times.
✗ Branch 1 not taken.
1406 std::unique_lock<std::mutex> lock(m_connListLock);
463
2/2
✓ Branch 0 taken 453 times.
✓ Branch 1 taken 953 times.
1406 if (!m_connList.size()) {
464 453 m_sseData.clear();
465 453 return;
466 }
467
2/2
✓ Branch 0 taken 953 times.
✓ Branch 1 taken 453 times.
1406 }
468
469 // We run at the sequence frame rate, which can now be well above anything a
470 // browser can render. Sending faster than the client drains just piles
471 // frames up in the socket and proxy buffers, so the preview falls further
472 // and further behind playback. Rate limit to m_updateInterval instead.
473 //
474 // Skipping a frame here is safe even though we send deltas: m_virtualDisplay
475 // is only updated for pixels we actually emit, so it always holds what the
476 // client was last sent and the next diff simply spans a longer gap.
477 //
478 // The deadline is advanced by whole intervals rather than reset to "now" so
479 // that an update interval which isn't a multiple of the frame time still
480 // averages out to the requested rate instead of aliasing down to the next
481 // frame boundary (eg. 25ms against a 20ms sequence gives 40fps, not 25fps).
482
1/2
✓ Branch 0 taken 953 times.
✗ Branch 1 not taken.
953 long long now = GetMonotonicTimeMS();
483
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 953 times.
953 if (now < m_nextSendMS) {
484 ✗ m_sseData.clear();
485 ✗ return;
486 }
487 953 m_nextSendMS += m_updateInterval;
488
2/2
✓ Branch 0 taken 949 times.
✓ Branch 1 taken 4 times.
953 if (m_nextSendMS <= now) {
489 // First frame, or we've been idle/stalled long enough that the deadline
490 // is stale - resync rather than sending a burst to catch up.
491 949 m_nextSendMS = now + m_updateInterval;
492 }
493
494 // Start a fresh rollback record for this frame's cache writes
495 953 m_cacheRollback.clear();
496
497 // A client joined: it has nothing yet, so diff this frame against black.
498
2/2
✓ Branch 0 taken 66 times.
✓ Branch 1 taken 887 times.
953 if (m_resetCache.exchange(false)) {
499 66 bzero(m_virtualDisplay, m_screenSize);
500 }
501
502 // Fast black detection - check if all channel data is zero (sequence ended/stopped)
503 // Only do this check occasionally to reduce CPU overhead
504 static int blackCheckCounter = 0;
505 953 bool allBlack = false;
506 953 bool forceBlackUpdate = false;
507
508 // Check for black every 10th frame to reduce CPU, but always check if last frame was black
509 // to catch the transition back to color quickly
510
5/6
✓ Branch 0 taken 953 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 91 times.
✓ Branch 3 taken 862 times.
✓ Branch 4 taken 91 times.
✓ Branch 5 taken 862 times.
953 if (lastFrameWasBlack || (++blackCheckCounter >= 10)) {
511 91 blackCheckCounter = 0;
512 91 allBlack = true;
513
6/6
✓ Branch 0 taken 128 times.
✓ Branch 1 taken 54 times.
✓ Branch 2 taken 91 times.
✓ Branch 3 taken 37 times.
✓ Branch 4 taken 91 times.
✓ Branch 5 taken 91 times.
182 for (int i = 0; i < m_pixels.size() && allBlack; i++) {
514 unsigned char r, g, b;
515
1/2
✓ Branch 0 taken 91 times.
✗ Branch 1 not taken.
91 GetPixelRGB(m_pixels[i], channelData, r, g, b);
516
5/6
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 66 times.
✓ Branch 2 taken 12 times.
✓ Branch 3 taken 13 times.
✓ Branch 4 taken 12 times.
✗ Branch 5 not taken.
91 if (r != 0 || g != 0 || b != 0) {
517 91 allBlack = false;
518 }
519 }
520
521 // If transitioning to black, force update of all pixels
522
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 91 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
91 forceBlackUpdate = (allBlack && !lastFrameWasBlack);
523 91 lastFrameWasBlack = allBlack;
524 }
525
526 953 std::string data;
527 953 int pixelsChanged = 0;
528 unsigned char r, g, b;
529 953 std::map<std::string, std::string> colors;
530 953 std::map<std::string, std::string>::iterator colorIt;
531 char color[4];
532 953 std::string colorStr;
533 char loc[7];
534 int y;
535 VirtualDisplayPixel pixel;
536 static const char* const base64 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/";
537
538
2/2
✓ Branch 0 taken 1534459 times.
✓ Branch 1 taken 953 times.
1535412 for (int i = 0; i < m_pixels.size(); i++) {
539
1/2
✓ Branch 0 taken 1534459 times.
✗ Branch 1 not taken.
1534459 GetPixelRGB(m_pixels[i], channelData, r, g, b);
540
541 // 18-bit RGB666 pixel data passed over Event Stream in 3 bytes
542 // using Base64 allowed characters since SSE is non-binary
543 1534459 r = r >> 2;
544 1534459 g = g >> 2;
545 1534459 b = b >> 2;
546
547 // Send pixels that changed OR when forcing black update (sequence ended)
548 3068918 if (forceBlackUpdate ||
549
2/2
✓ Branch 0 taken 1150261 times.
✓ Branch 1 taken 384198 times.
1534459 (m_virtualDisplay[m_pixels[i].r] != r) ||
550
5/6
✓ Branch 0 taken 1534459 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 935961 times.
✓ Branch 3 taken 214300 times.
✓ Branch 4 taken 702398 times.
✓ Branch 5 taken 832061 times.
4004879 (m_virtualDisplay[m_pixels[i].g] != g) ||
551
2/2
✓ Branch 0 taken 103900 times.
✓ Branch 1 taken 832061 times.
935961 (m_virtualDisplay[m_pixels[i].b] != b)) {
552 // Remember the pre-send values so SendData can undo them if this
553 // frame never makes it to the client
554
1/2
✓ Branch 0 taken 702398 times.
✗ Branch 1 not taken.
702398 m_cacheRollback.emplace_back(m_pixels[i].r, m_virtualDisplay[m_pixels[i].r]);
555
1/2
✓ Branch 0 taken 702398 times.
✗ Branch 1 not taken.
702398 m_cacheRollback.emplace_back(m_pixels[i].g, m_virtualDisplay[m_pixels[i].g]);
556
1/2
✓ Branch 0 taken 702398 times.
✗ Branch 1 not taken.
702398 m_cacheRollback.emplace_back(m_pixels[i].b, m_virtualDisplay[m_pixels[i].b]);
557
558 702398 m_virtualDisplay[m_pixels[i].r] = r;
559 702398 m_virtualDisplay[m_pixels[i].g] = g;
560 702398 m_virtualDisplay[m_pixels[i].b] = b;
561
562 702398 snprintf(color, sizeof(color), "%c%c%c", base64[r], base64[g], base64[b]);
563
1/2
✓ Branch 0 taken 702398 times.
✗ Branch 1 not taken.
702398 colorStr = color;
564
565 702398 y = m_previewHeight - m_pixels[i].y;
566 // must pick the same form as the page's scaleMap keys
567 // (virtualdisplaybody.php), or the browser finds no entry for the
568 // pixel and drops the rest of the frame
569
3/6
✓ Branch 0 taken 702398 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 702398 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 702398 times.
702398 if ((m_pixels[i].x >= 4096) || (y >= 4096))
570 ✗ snprintf(loc, sizeof(loc), "%c%c%c%c%c%c",
571 ✗ base64[(m_pixels[i].x >> 12) & 0x3f],
572 ✗ base64[(m_pixels[i].x >> 6) & 0x3f],
573 ✗ base64[(m_pixels[i].x) & 0x3f],
574 ✗ base64[(y >> 12) & 0x3f],
575 ✗ base64[(y >> 6) & 0x3f],
576 ✗ base64[(y)&0x3f]);
577 else
578 2107194 snprintf(loc, sizeof(loc), "%c%c%c%c",
579 702398 base64[(m_pixels[i].x >> 6) & 0x3f],
580 702398 base64[(m_pixels[i].x) & 0x3f],
581 702398 base64[(y >> 6) & 0x3f],
582 702398 base64[(y)&0x3f]);
583
584
1/2
✓ Branch 0 taken 702398 times.
✗ Branch 1 not taken.
702398 colorIt = colors.find(colorStr);
585
2/2
✓ Branch 0 taken 702005 times.
✓ Branch 1 taken 393 times.
702398 if (colorIt != colors.end())
586
3/6
✓ Branch 0 taken 702005 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 702005 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 702005 times.
✗ Branch 5 not taken.
1404010 colorIt->second += std::string(";") + loc;
587 else
588
3/6
✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 393 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 393 times.
✗ Branch 5 not taken.
393 colors[colorStr] = colorStr + ":" + loc;
589
590 702398 pixelsChanged++;
591 }
592 }
593
594
2/2
✓ Branch 0 taken 393 times.
✓ Branch 1 taken 560 times.
953 if (colors.size()) {
595 // Pre-allocate to avoid reallocations during string building
596 393 m_sseData.clear();
597
1/2
✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
393 m_sseData.reserve(128 + colors.size() * 16); // Estimate: header + avg color data
598
599
1/2
✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
393 m_sseData = "id: ";
600
2/4
✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 393 times.
✗ Branch 3 not taken.
393 m_sseData += std::to_string(id) + "\r\n";
601
1/2
✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
393 m_sseData += "event: message\r\n";
602
1/2
✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
393 m_sseData += "data: ";
603
604 393 std::string data2;
605
1/2
✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
393 data2.reserve(colors.size() * 16); // Pre-allocate for color data
606
607 393 bool first = true;
608
2/2
✓ Branch 0 taken 393 times.
✓ Branch 1 taken 393 times.
786 for (const auto& pair : colors) {
609
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 393 times.
393 if (!first)
610 ✗ data2 += "|";
611 else
612 393 first = false;
613
614
1/2
✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
393 data2 += pair.second;
615 }
616
2/4
✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 393 times.
✗ Branch 3 not taken.
393 m_sseData += data2 + "\r\n\r\n";
617
618
1/2
✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
393 LogExcess(VB_CHANNELOUT, "PixelsChanged: %d, Colors: %d, Data Size: %d\n",
619 pixelsChanged, colors.size(), m_sseData.size());
620 393 } else {
621 // Send empty update to keep connection alive and maintain frame timing
622
1/2
✓ Branch 0 taken 560 times.
✗ Branch 1 not taken.
560 m_sseData = "id: ";
623
2/4
✓ Branch 0 taken 560 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 560 times.
✗ Branch 3 not taken.
560 m_sseData += std::to_string(id) + "\r\n";
624
1/2
✓ Branch 0 taken 560 times.
✗ Branch 1 not taken.
560 m_sseData += "event: ping\r\n";
625
1/2
✓ Branch 0 taken 560 times.
✗ Branch 1 not taken.
560 m_sseData += "data: \r\n\r\n";
626 }
627
628 953 id++;
629 953 }
630
631 /*
632 *
633 */
634 1375 int HTTPVirtualDisplayOutput::SendData(unsigned char* channelData) {
635 1375 bool dropped = false;
636
637
2/2
✓ Branch 0 taken 946 times.
✓ Branch 1 taken 429 times.
1375 if (m_sseData != "") {
638
1/2
✓ Branch 0 taken 946 times.
✗ Branch 1 not taken.
946 std::unique_lock<std::mutex> lock(m_connListLock);
639
1/2
✓ Branch 0 taken 946 times.
✗ Branch 1 not taken.
946 const uint64_t deadline = GetMonotonicTimeMS() + SSE_FRAME_BUDGET_MS;
640 // Iterate backwards so we can safely remove elements
641
2/2
✓ Branch 0 taken 1034 times.
✓ Branch 1 taken 946 times.
1980 for (int i = m_connList.size() - 1; i >= 0; i--) {
642
1/2
✓ Branch 0 taken 1034 times.
✗ Branch 1 not taken.
1034 int result = WriteSSEPacket(m_connList[i], m_sseData, deadline);
643
7/8
✓ Branch 0 taken 218 times.
✓ Branch 1 taken 816 times.
✓ Branch 2 taken 218 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 217 times.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 1033 times.
1034 if (result == 0 && ++m_dropCounts[m_connList[i]] > SSE_MAX_DROPPED_FRAMES) {
644 // A client that has taken nothing for this long is gone or
645 // stuck; dropping its frames forever also cost every frame a
646 // resync of the pixels it never received.
647
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogDebug(VB_CHANNELOUT, "SSE client %d took no frames for %d frames, closing it\n", m_connList[i], SSE_MAX_DROPPED_FRAMES);
648 1 result = -1;
649
2/2
✓ Branch 0 taken 769 times.
✓ Branch 1 taken 264 times.
1033 } else if (result > 0) {
650
1/2
✓ Branch 0 taken 769 times.
✗ Branch 1 not taken.
769 m_dropCounts.erase(m_connList[i]);
651 }
652
2/2
✓ Branch 0 taken 48 times.
✓ Branch 1 taken 986 times.
1034 if (result < 0) {
653 // Connection is dead, remove it
654
1/2
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
48 LogDebug(VB_CHANNELOUT, "Removing dead SSE connection %d\n", m_connList[i]);
655
1/2
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
48 close(m_connList[i]);
656
1/2
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
48 m_connList.erase(m_connList.begin() + i);
657 48 m_connListChanged = true;
658
2/2
✓ Branch 0 taken 217 times.
✓ Branch 1 taken 769 times.
986 } else if (result == 0) {
659 217 dropped = true;
660 }
661 }
662 // forget counts for connections the select thread has since closed
663
2/2
✓ Branch 0 taken 221 times.
✓ Branch 1 taken 946 times.
1167 for (auto it = m_dropCounts.begin(); it != m_dropCounts.end();) {
664
3/4
✓ Branch 0 taken 221 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 217 times.
221 if (std::find(m_connList.begin(), m_connList.end(), it->first) == m_connList.end()) {
665
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 it = m_dropCounts.erase(it);
666 } else {
667 217 ++it;
668 }
669 }
670 946 }
671
672
2/2
✓ Branch 0 taken 203 times.
✓ Branch 1 taken 1172 times.
1375 if (dropped) {
673 // The payload held the only copy of this frame's pixel changes, so put
674 // the cache back the way it was and let the next frame re-send them.
675
2/2
✓ Branch 0 taken 30450 times.
✓ Branch 1 taken 203 times.
30653 for (auto it = m_cacheRollback.rbegin(); it != m_cacheRollback.rend(); ++it) {
676
2/4
✓ Branch 0 taken 30450 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30450 times.
✗ Branch 3 not taken.
30450 m_virtualDisplay[it->first] = it->second;
677 }
678 203 m_cacheRollback.clear();
679 }
680
681 1375 return m_channelCount;
682 }
683