GCC Code Coverage Report


Directory: ./
File: playlist/PlaylistEntryImage.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 226 274 82.5%
Functions: 19 21 90.5%
Branches: 204 429 47.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 LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include "fpp-json.h"
16
17 #include "Warnings.h" // WarningHolder -- needed directly for NOPCH builds
18
19 #include "../common.h"
20 #include "../log.h"
21 #include "../settings.h"
22
23 #include "PlaylistEntryImage.h"
24 #include <sys/stat.h>
25
26 #include <cstdio>
27 #include <filesystem>
28 #include <functional>
29 using namespace std::filesystem;
30
31 #include <Magick++.h>
32 using namespace Magick;
33
34 #include "overlays/PixelOverlay.h"
35 #include "overlays/PixelOverlayEffects.h" // ScaleOverlayImage - shared sizing
36
37 void StartPrepLoopThread(PlaylistEntryImage* fb);
38
39 /*
40 *
41 */
42 342 PlaylistEntryImage::PlaylistEntryImage(Playlist* playlist, PlaylistEntryBase* parent) :
43 PlaylistEntryBase(playlist, parent),
44 342 m_width(800),
45 342 m_height(600),
46 342 m_buffer(NULL),
47 342 m_bufferSize(0) {
48
1/2
✓ Branch 0 taken 342 times.
✗ Branch 1 not taken.
342 LogDebug(VB_PLAYLIST, "PlaylistEntryImage::PlaylistEntryImage()\n");
49
50
1/2
✓ Branch 0 taken 342 times.
✗ Branch 1 not taken.
342 m_type = "image";
51
52 342 m_fileSeed = (unsigned int)(time(NULL) + ((long long)this & 0xFFFFFFFF));
53
2/4
✓ Branch 0 taken 342 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 342 times.
✗ Branch 3 not taken.
342 m_cacheDir = FPP_DIR_MEDIA("/cache");
54 342 m_cacheEntries = 200;
55 342 m_cacheSize = 1024; // MB
56 342 m_freeSpace = 2048; // MB
57 342 }
58
59 /*
60 *
61 */
62 1336 PlaylistEntryImage::~PlaylistEntryImage() {
63 {
64 // Under the lock: PrepLoop() checks m_runLoop and then waits, and a
65 // write between the two was a lost wakeup -- the prep thread slept on
66 // and the join() below hung whichever thread was deleting the entry.
67 668 std::lock_guard<std::mutex> lock(m_bufferLock);
68 668 m_runLoop = false;
69 668 }
70 668 m_prepSignal.notify_all();
71
2/2
✓ Branch 0 taken 333 times.
✓ Branch 1 taken 1 times.
668 if (m_prepThread) {
72 666 m_prepThread->join();
73
1/2
✓ Branch 0 taken 333 times.
✗ Branch 1 not taken.
666 delete m_prepThread;
74 }
75
76 // Freed only after the prep thread is joined above, so no PrepImage() write
77 // to m_buffer can race this. m_buffer is NULL until Init() allocates it, so
78 // delete[] is a safe no-op on an entry that failed (or never ran) Init.
79
2/2
✓ Branch 0 taken 333 times.
✓ Branch 1 taken 1 times.
668 delete[] m_buffer;
80
81
2/2
✓ Branch 0 taken 47 times.
✓ Branch 1 taken 287 times.
668 if (m_modelOrigState == PixelOverlayState::PixelState::Disabled) {
82 94 m_model = PixelOverlayManager::INSTANCE.getModelShared(m_modelName);
83
84
1/2
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
94 if (m_model) {
85 94 m_model->clear();
86 94 m_model->setState(PixelOverlayState(PixelOverlayState::PixelState::Disabled));
87 }
88 }
89 1336 }
90
91 /*
92 *
93 */
94 342 int PlaylistEntryImage::Init(Json::Value& config) {
95
1/2
✓ Branch 0 taken 342 times.
✗ Branch 1 not taken.
342 if (config.isMember("modelName"))
96
2/4
✓ Branch 0 taken 342 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 342 times.
✗ Branch 3 not taken.
342 m_modelName = config["modelName"].asString();
97
98
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 342 times.
342 if (m_modelName == "") {
99 ✗ LogErr(VB_PLAYLIST, "Empty Pixel Overlay Model name\n");
100 ✗ WarningHolder::AddWarningTimeout(60, 35, "Playlist image entry has an empty Pixel Overlay Model name");
101 ✗ return 0;
102 }
103
104
1/2
✓ Branch 0 taken 342 times.
✗ Branch 1 not taken.
342 m_model = PixelOverlayManager::INSTANCE.getModelShared(m_modelName);
105
106
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 342 times.
342 if (!m_model) {
107 ✗ LogErr(VB_PLAYLIST, "Invalid Pixel Overlay Model: '%s'\n", m_modelName.c_str());
108 ✗ WarningHolder::AddWarningTimeout(60, 35, "Invalid Pixel Overlay Model: '" + m_modelName + "'");
109 ✗ return 0;
110 }
111 342 m_width = m_model->getWidth();
112 342 m_height = m_model->getHeight();
113
1/2
✓ Branch 0 taken 342 times.
✗ Branch 1 not taken.
342 m_modelOrigState = m_model->getState().getState();
114
115
2/2
✓ Branch 0 taken 54 times.
✓ Branch 1 taken 288 times.
342 if (m_modelOrigState == PixelOverlayState::PixelState::Disabled)
116
1/2
✓ Branch 0 taken 54 times.
✗ Branch 1 not taken.
54 m_model->setState(PixelOverlayState(PixelOverlayState::PixelState::Enabled));
117
118
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 341 times.
342 if (!config.isMember("imagePath")) {
119 1 LogErr(VB_PLAYLIST, "Missing imagePath\n");
120
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
3 WarningHolder::AddWarningTimeout(60, 34, "Playlist image entry is missing the image path");
121 1 return 0;
122 }
123
124
2/4
✓ Branch 0 taken 341 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 341 times.
✗ Branch 3 not taken.
341 m_imagePath = config["imagePath"].asString();
125
3/6
✓ Branch 0 taken 341 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 341 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 341 times.
682 if (startsWith(m_imagePath, "/")) {
126 ✗ m_imageFullPath = m_imagePath;
127 } else {
128
4/8
✓ Branch 0 taken 341 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 341 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 341 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 341 times.
✗ Branch 7 not taken.
1023 m_imageFullPath = FPP_DIR_IMAGE("/" + m_imagePath);
129 }
130
131 341 SetFileList();
132 341 CleanupCache();
133
134 341 m_bufferSize = m_width * m_height * 3; // RGB
135 341 m_buffer = new unsigned char[m_bufferSize];
136
137 341 m_runLoop = true;
138 341 m_imagePrepped = false;
139 341 m_imageDrawn = false;
140
2/6
✓ Branch 0 taken 341 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 341 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
341 m_prepThread = new std::thread(StartPrepLoopThread, this);
141
142 341 return PlaylistEntryBase::Init(config);
143 }
144
145 /*
146 *
147 */
148 43 int PlaylistEntryImage::StartPlaying(void) {
149
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 43 times.
43 if (!CanPlay()) {
150 ✗ FinishPlay();
151 ✗ return 0;
152 }
153
154 // Each pass draws afresh. m_imageDrawn was only ever set, so on a repeat
155 // whose image wasn't ready yet the entry "finished" without drawing.
156 43 m_imageDrawn = false;
157
158
2/2
✓ Branch 0 taken 34 times.
✓ Branch 1 taken 9 times.
43 if (m_imagePrepped)
159 34 Draw();
160 else
161 9 m_prepSignal.notify_all();
162
163 43 return PlaylistEntryBase::StartPlaying();
164 }
165
166 /*
167 *
168 */
169 42 int PlaylistEntryImage::Process(void) {
170
5/6
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 34 times.
✓ Branch 4 taken 8 times.
✓ Branch 5 taken 34 times.
42 if (m_imagePrepped && !m_imageDrawn)
171 8 Draw();
172
173
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 if (m_imageDrawn)
174 42 FinishPlay();
175
176 42 return PlaylistEntryBase::Process();
177 }
178
179 /*
180 *
181 */
182 1 int PlaylistEntryImage::Stop(void) {
183 1 FinishPlay();
184
185 1 return PlaylistEntryBase::Stop();
186 }
187
188 /*
189 *
190 */
191 ✗ void PlaylistEntryImage::Dump(void) {
192 ✗ PlaylistEntryBase::Dump();
193
194 ✗ LogDebug(VB_PLAYLIST, "Image Path : %s\n", m_imagePath.c_str());
195 ✗ LogDebug(VB_PLAYLIST, "Image Full Path: %s\n", m_imageFullPath.c_str());
196 ✗ LogDebug(VB_PLAYLIST, "Model Name : %s\n", m_modelName.c_str());
197 ✗ }
198
199 /*
200 *
201 */
202 85 Json::Value PlaylistEntryImage::GetConfig(void) {
203 85 Json::Value result = PlaylistEntryBase::GetConfig();
204
205
2/4
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 85 times.
✗ Branch 3 not taken.
85 result["imagePath"] = m_imagePath;
206
2/4
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 85 times.
✗ Branch 3 not taken.
85 result["modelName"] = m_modelName;
207
208 85 std::size_t found = m_curFileName.find_last_of("/");
209
1/2
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
85 if (found > 0)
210
3/6
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 85 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 85 times.
✗ Branch 5 not taken.
85 result["imageFilename"] = m_curFileName.substr(found + 1);
211 else
212 ✗ result["imageFilename"] = m_curFileName;
213
214 85 return result;
215 ✗ }
216
217 /*
218 *
219 */
220 376 void PlaylistEntryImage::SetFileList(void) {
221 376 LogDebug(VB_PLAYLIST, "PlaylistEntryImage::SetFileList()\n");
222
223 376 m_files.clear();
224
225
4/6
✓ Branch 0 taken 376 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 376 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 362 times.
✓ Branch 5 taken 14 times.
376 if (is_regular_file(m_imageFullPath)) {
226 362 m_files.push_back(m_imageFullPath);
227 362 return;
228 }
229
230
4/6
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 11 times.
✓ Branch 5 taken 3 times.
14 if (is_directory(m_imageFullPath)) {
231 // Only the images: the walk also yields the folders inside this one
232 // and whatever else is kept there, and one of those picked at random
233 // failed to read, so that pass of the entry drew nothing.
234 11 std::error_code ec;
235
2/4
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
11 for (auto it = recursive_directory_iterator(m_imageFullPath, ec);
236
7/10
✓ Branch 0 taken 43 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 54 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 43 times.
✓ Branch 5 taken 11 times.
✓ Branch 6 taken 54 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 43 times.
✓ Branch 9 taken 11 times.
54 !ec && it != recursive_directory_iterator(); it.increment(ec)) {
237
1/2
✓ Branch 0 taken 43 times.
✗ Branch 1 not taken.
43 std::string entry = it->path().string();
238
3/4
✓ Branch 0 taken 43 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✓ Branch 3 taken 27 times.
43 if (IsOverlayImageFile(entry)) {
239
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 m_files.push_back(entry);
240 }
241 54 }
242
243
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 LogDebug(VB_PLAYLIST, "%d images in directory\n", m_files.size());
244
245 11 return;
246 }
247
248 // FIXME, handle file glob
249 }
250
251 /*
252 *
253 */
254 382 const std::string PlaylistEntryImage::GetNextFile(void) {
255 // At most one refill per call. This used to recurse once per missing
256 // file and refill whenever the list ran dry, so a directory whose files
257 // had all gone (broken links) recursed until the stack overflowed.
258 382 bool refilled = false;
259 while (true) {
260
2/2
✓ Branch 0 taken 35 times.
✓ Branch 1 taken 347 times.
382 if (!m_files.size()) {
261
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 35 times.
35 if (refilled)
262 ✗ break;
263 35 SetFileList();
264 35 refilled = true;
265
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 31 times.
35 if (!m_files.size())
266 4 break;
267 }
268
269 378 int i = rand_r(&m_fileSeed) % m_files.size();
270
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
378 std::string result = m_files[i];
271
1/2
✓ Branch 0 taken 378 times.
✗ Branch 1 not taken.
378 m_files.erase(m_files.begin() + i);
272
273
2/4
✓ Branch 0 taken 378 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 378 times.
✗ Branch 3 not taken.
378 if (FileExists(result)) {
274
1/2
✓ Branch 0 taken 378 times.
✗ Branch 1 not taken.
378 LogDebug(VB_PLAYLIST, "GetNextFile() = %s\n", result.c_str());
275 378 return result;
276 }
277 ✗ }
278
279 4 LogWarn(VB_PLAYLIST, "No files found in GetNextFile()\n");
280
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
8 return "";
281 }
282
283 /*
284 *
285 */
286 382 void PlaylistEntryImage::PrepImage(void) {
287
1/2
✓ Branch 0 taken 382 times.
✗ Branch 1 not taken.
382 Image image;
288
1/2
✓ Branch 0 taken 382 times.
✗ Branch 1 not taken.
382 Blob blob;
289 382 bool needToCache = true;
290
1/2
✓ Branch 0 taken 382 times.
✗ Branch 1 not taken.
382 std::string nextFile = GetNextFile();
291
292
3/4
✓ Branch 0 taken 382 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 378 times.
382 if (nextFile == "") {
293 4 m_noImage = true;
294 4 m_imagePrepped = true;
295 4 return;
296 }
297 378 m_noImage = false;
298
299
2/2
✓ Branch 0 taken 31 times.
✓ Branch 1 taken 347 times.
378 if (nextFile == m_nextFileName) {
300 31 m_imagePrepped = true;
301 31 return;
302 }
303
304 // The GraphicsMagick decode/resize/cache work below only touches locals
305 // (image, blob) and the on-disk cache — m_bufferLock is needed solely for
306 // the m_buffer/m_nextFileName updates at the end. Decoding outside the
307 // lock keeps Draw() (playlist thread) from blocking behind a slow disk
308 // read or resize.
309 347 bool decoded = false;
310 try {
311 347 int cols = 0;
312 347 int rows = 0;
313
314 347 needToCache = false;
315
1/2
✓ Branch 0 taken 347 times.
✗ Branch 1 not taken.
347 image.quiet(true); // Squelch warning exceptions
316
317
3/6
✓ Branch 0 taken 347 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 347 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 347 times.
347 if (GetImageFromCache(nextFile, image)) {
318 ✗ cols = image.columns();
319 ✗ rows = image.rows();
320 }
321
322
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 347 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
347 if ((cols != m_width) || (rows != m_height)) {
323
2/4
✓ Branch 0 taken 347 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 347 times.
✗ Branch 3 not taken.
347 image.read(nextFile.c_str());
324
1/2
✓ Branch 0 taken 347 times.
✗ Branch 1 not taken.
347 image.autoOrient();
325
1/2
✓ Branch 0 taken 347 times.
✗ Branch 1 not taken.
347 cols = image.columns();
326
1/2
✓ Branch 0 taken 347 times.
✗ Branch 1 not taken.
347 rows = image.rows();
327 }
328
329 // Scale when the image differs from the model in EITHER dimension: the
330 // RGB blob must be exactly m_width*m_height*3 to fill m_buffer, and
331 // ScaleOverlayImage() crops to precisely m_width x m_height. An image
332 // matching one axis but not the other still needs scaling; with && it
333 // was left at the wrong size and the guarded memcpy below truncated it,
334 // rendering garbled. Matching both axes still skips (||-of-two-falses).
335
4/4
✓ Branch 0 taken 344 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 343 times.
347 if ((cols != m_width) || (rows != m_height)) {
336 4 needToCache = true;
337
338 // Shared with the Image overlay effect so a given image lands on a
339 // model identically whether it arrives via a playlist entry or a
340 // command. See ScaleOverlayImage() in PixelOverlayEffects.cpp.
341
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
8 ScaleOverlayImage(image, m_width, m_height, "Scale to Fit");
342 }
343
344
1/2
✓ Branch 0 taken 347 times.
✗ Branch 1 not taken.
347 image.type(TrueColorType);
345
346
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 343 times.
347 if (needToCache) {
347
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 CacheImage(nextFile, image);
348 }
349
350
2/4
✓ Branch 0 taken 347 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 347 times.
✗ Branch 3 not taken.
347 image.magick("RGB");
351
1/2
✓ Branch 0 taken 347 times.
✗ Branch 1 not taken.
347 image.write(&blob);
352
353 347 decoded = true;
354 ✗ } catch (Exception& error_) {
355 ✗ LogErr(VB_PLAYLIST, "GraphicsMagick exception reading %s: %s\n",
356 nextFile.c_str(), error_.what());
357 ✗ WarningHolder::AddWarningTimeout(60, 34, "Could not read playlist image file: " + nextFile);
358 ✗ } catch (const std::exception& ex) {
359 // Not everything that can throw here is a Magick::Exception
360 // (std::bad_alloc, filesystem errors). The old code held
361 // m_bufferLock via raw lock()/unlock() with only the Magick catch,
362 // so any other exception leaked the mutex and wedged both Draw()
363 // and this prep thread permanently.
364 ✗ LogErr(VB_PLAYLIST, "Exception preparing image %s: %s\n",
365 nextFile.c_str(), ex.what());
366 ✗ WarningHolder::AddWarningTimeout(60, 34, "Could not read playlist image file: " + nextFile);
367 ✗ }
368
369 {
370
1/2
✓ Branch 0 taken 347 times.
✗ Branch 1 not taken.
347 std::lock_guard<std::mutex> lock(m_bufferLock);
371
1/2
✓ Branch 0 taken 347 times.
✗ Branch 1 not taken.
347 m_nextFileName = nextFile;
372 347 memset(m_buffer, 0, m_bufferSize);
373
1/2
✓ Branch 0 taken 347 times.
✗ Branch 1 not taken.
347 if (decoded) {
374 // Guard the copy length — a short blob previously read past the
375 // end of the blob's allocation.
376
2/6
✓ Branch 0 taken 347 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 347 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
347 size_t len = blob.length() < (size_t)m_bufferSize ? blob.length() : (size_t)m_bufferSize;
377
1/2
✓ Branch 0 taken 347 times.
✗ Branch 1 not taken.
347 memcpy(m_buffer, blob.data(), len);
378 }
379 347 m_imagePrepped = true;
380 347 }
381
6/6
✓ Branch 0 taken 347 times.
✓ Branch 1 taken 35 times.
✓ Branch 2 taken 347 times.
✓ Branch 3 taken 35 times.
✓ Branch 4 taken 347 times.
✓ Branch 5 taken 35 times.
452 }
382
383 /*
384 *
385 */
386 42 void PlaylistEntryImage::Draw(void) {
387 {
388 // RAII guard: the old raw lock()/unlock() code returned from the
389 // !m_model branch with m_bufferLock still held; m_imageDrawn stayed
390 // false, so the next Process() tick called Draw() again and the
391 // playlist thread re-locked a mutex it already owned — a permanent
392 // wedge whenever the overlay model disappeared mid-playlist.
393
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 std::lock_guard<std::mutex> lock(m_bufferLock);
394
395 // We need to look this up again since the model may have disappeared
396
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 m_model = PixelOverlayManager::INSTANCE.getModelShared(m_modelName);
397
398
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 41 times.
42 if (!m_model) {
399 // The model went away mid-playlist. Finish the entry rather than
400 // retrying (and re-raising the warning) on every tick forever.
401
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_PLAYLIST, "Invalid Pixel Overlay Model: '%s'\n", m_modelName.c_str());
402
4/8
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
1 WarningHolder::AddWarningTimeout(60, 35, "Invalid Pixel Overlay Model: '" + m_modelName + "'");
403 1 m_imageDrawn = true;
404 1 return;
405 }
406
407
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 39 times.
41 if (m_noImage) {
408 // Nothing to show; count it as drawn so the entry finishes.
409
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogWarn(VB_PLAYLIST, "No image found for '%s', skipping\n", m_imagePath.c_str());
410 } else {
411 // m_buffer is m_width x m_height RGB. setData() reads the model's
412 // own pixel size -- 4 bytes on an RGBW model, a third past the end
413 // of this buffer -- and assumes the model is still the size it was
414 // at Init(). blitData() converts to the model's format and clips.
415
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
78 m_model->blitData(m_buffer, m_width, m_height, 3, 0, 0,
416 39 PixelOverlayState(PixelOverlayState::Enabled));
417 // Reported as imageFilename in status; it was never set.
418
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 m_curFileName = m_nextFileName;
419 }
420
421 // Under the lock for the same lost-wakeup reason as the destructor.
422 41 m_imageDrawn = true;
423 41 m_imagePrepped = false;
424
2/2
✓ Branch 0 taken 41 times.
✓ Branch 1 taken 1 times.
42 }
425
426 41 m_prepSignal.notify_all();
427 }
428
429 /*
430 *
431 */
432 341 void StartPrepLoopThread(PlaylistEntryImage* pe) {
433 341 pe->PrepLoop();
434 333 }
435
436 341 void PlaylistEntryImage::PrepLoop(void) {
437
2/4
✓ Branch 0 taken 341 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 341 times.
✗ Branch 3 not taken.
341 SetThreadName("FPP-ImageLoop");
438
1/2
✓ Branch 0 taken 341 times.
✗ Branch 1 not taken.
341 std::unique_lock<std::mutex> lock(m_bufferLock);
439
2/2
✓ Branch 0 taken 382 times.
✓ Branch 1 taken 333 times.
715 while (m_runLoop) {
440
1/2
✓ Branch 0 taken 382 times.
✗ Branch 1 not taken.
382 if (!m_imagePrepped) {
441
1/2
✓ Branch 0 taken 382 times.
✗ Branch 1 not taken.
382 lock.unlock();
442
443
1/2
✓ Branch 0 taken 382 times.
✗ Branch 1 not taken.
382 PrepImage();
444
1/2
✓ Branch 0 taken 382 times.
✗ Branch 1 not taken.
382 lock.lock();
445 }
446
447 // The predicate guards against a lost wakeup: the destructor sets
448 // m_runLoop=false and notifies while this thread is inside
449 // PrepImage() (lock released above); a bare wait() would then sleep
450 // forever and the destructor's join() would hang whichever thread
451 // is deleting this entry.
452
5/6
✓ Branch 0 taken 374 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 423 times.
✓ Branch 3 taken 333 times.
✓ Branch 4 taken 41 times.
✓ Branch 5 taken 382 times.
1138 m_prepSignal.wait(lock, [this] { return !m_runLoop || !m_imagePrepped; });
453 }
454 333 }
455
456 /*
457 *
458 */
459 351 std::string PlaylistEntryImage::GetCacheFileName(std::string fileName) {
460 351 std::size_t found = fileName.find_last_of("/");
461
1/2
✓ Branch 0 taken 351 times.
✗ Branch 1 not taken.
351 std::string baseFile = fileName.substr(found + 1);
462
1/2
✓ Branch 0 taken 351 times.
✗ Branch 1 not taken.
351 std::string result = m_cacheDir;
463
464 // Keyed by the full path and the model size, not just the file name: two
465 // images with the same name in different folders used to share a cache
466 // file and show each other. The base name stays in it for readability;
467 // the "pei-" prefix and ".png" suffix are what CleanupCache() matches.
468 char key[64];
469 351 snprintf(key, sizeof(key), "-%016zx-%dx%d", std::hash<std::string>{}(fileName), m_width, m_height);
470
471
1/2
✓ Branch 0 taken 351 times.
✗ Branch 1 not taken.
351 result += "/pei-";
472
1/2
✓ Branch 0 taken 351 times.
✗ Branch 1 not taken.
351 result += baseFile;
473
1/2
✓ Branch 0 taken 351 times.
✗ Branch 1 not taken.
351 result += key;
474
1/2
✓ Branch 0 taken 351 times.
✗ Branch 1 not taken.
351 result += ".png";
475
476 351 return result;
477 351 }
478
479 /*
480 *
481 */
482 347 bool PlaylistEntryImage::GetImageFromCache(std::string fileName, Image& image) {
483
2/4
✓ Branch 0 taken 347 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 347 times.
✗ Branch 3 not taken.
347 std::string cacheFile = GetCacheFileName(fileName);
484 struct stat os;
485 struct stat cs;
486
487
2/4
✓ Branch 0 taken 347 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 347 times.
347 if (FileExists(cacheFile)) {
488 // Either stat failing leaves its st_mtime uninitialized; treat that as
489 // a cache miss rather than comparing garbage.
490 ✗ if ((stat(fileName.c_str(), &os) != 0) || (stat(cacheFile.c_str(), &cs) != 0))
491 ✗ return false;
492
493 ✗ if (os.st_mtime > cs.st_mtime)
494 ✗ return false;
495
496 try {
497 ✗ image.read(cacheFile.c_str());
498 ✗ } catch (Exception& error_) {
499 ✗ LogErr(VB_PLAYLIST, "GraphicsMagick exception reading %s: %s\n",
500 cacheFile.c_str(), error_.what());
501 ✗ }
502
503 ✗ return true;
504 }
505
506 347 return false;
507 347 }
508
509 /*
510 *
511 */
512 4 void PlaylistEntryImage::CacheImage(std::string fileName, Image& image) {
513 // The cache only saves a later pass the decode and scale, so nothing here
514 // may stop this one: a write that threw (no cache folder, a full disk)
515 // used to skip the conversion after it, and the entry drew nothing.
516 try {
517 4 std::error_code ec;
518
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 std::filesystem::create_directories(m_cacheDir, ec);
519
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 std::string cacheFile = GetCacheFileName(fileName);
520
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 image.write(cacheFile.c_str());
521
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
4 } catch (const std::exception& ex) {
522 ✗ LogWarn(VB_PLAYLIST, "Could not cache the scaled %s: %s\n", fileName.c_str(), ex.what());
523 ✗ return;
524 ✗ }
525
526 4 CleanupCache();
527 }
528
529 /*
530 *
531 */
532 345 void PlaylistEntryImage::CleanupCache(void) {
533 try {
534
4/6
✓ Branch 0 taken 345 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 345 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 339 times.
✓ Branch 5 taken 6 times.
345 if (!exists(m_cacheDir)) {
535 339 return;
536 }
537
538 // Collect all cache files with their metadata
539 6 std::vector<std::pair<std::string, std::filesystem::file_time_type>> cacheFiles;
540 6 uintmax_t totalSize = 0;
541
542
5/8
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 6 times.
✓ Branch 7 taken 6 times.
12 for (const auto& entry : directory_iterator(m_cacheDir)) {
543
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 if (entry.is_regular_file()) {
544
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 std::string filename = entry.path().filename().string();
545 // Only process our cache files
546
3/6
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
6 if (filename.find("pei-") == 0 && filename.find(".png") != std::string::npos) {
547
3/6
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
6 cacheFiles.push_back({entry.path().string(), last_write_time(entry)});
548
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 totalSize += file_size(entry);
549 }
550 6 }
551 6 }
552
553 // Sort by modification time (oldest first)
554
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 std::sort(cacheFiles.begin(), cacheFiles.end(),
555 1 [](const auto& a, const auto& b) { return a.second < b.second; });
556
557 // Remove files if we exceed entry count or size limit
558 6 size_t maxEntries = m_cacheEntries;
559 6 uintmax_t maxSize = static_cast<uintmax_t>(m_cacheSize) * 1024 * 1024; // Convert MB to bytes
560
561
4/6
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 6 times.
16 while ((!cacheFiles.empty()) &&
562
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
10 ((cacheFiles.size() > maxEntries) || (totalSize > maxSize))) {
563 // Remove the oldest file
564 ✗ const auto& oldest = cacheFiles.front();
565 ✗ uintmax_t fileSize = file_size(oldest.first);
566
567 ✗ remove(oldest.first);
568 ✗ LogDebug(VB_PLAYLIST, "Removed cache file: %s (size: %lu bytes)\n",
569 oldest.first.c_str(), fileSize);
570
571 ✗ totalSize -= fileSize;
572 ✗ cacheFiles.erase(cacheFiles.begin());
573 }
574
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
6 } catch (std::filesystem::filesystem_error& e) {
575 ✗ LogErr(VB_PLAYLIST, "Error cleaning up cache: %s\n", e.what());
576 ✗ }
577 }
578