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