| 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 |