GCC Code Coverage Report


Directory: ./
File: playlist/PlaylistEntryDynamic.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 225 304 74.0%
Functions: 21 25 84.0%
Branches: 235 591 39.8%

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 <atomic>
20 #include <chrono>
21 #include <fstream>
22 #include <thread>
23
24 #include "../CurlManager.h"
25 #include "../common.h"
26 #include "../log.h"
27
28 #include "Playlist.h"
29 #include "PlaylistEntryBoth.h"
30 #include "PlaylistEntryCommand.h"
31 #include "PlaylistEntryDynamic.h"
32 #include "PlaylistEntryMedia.h"
33 #include "PlaylistEntryPause.h"
34 #include "PlaylistEntryRemap.h"
35 #include "PlaylistEntryScript.h"
36 #include "PlaylistEntrySequence.h"
37 #include "PlaylistEntryURL.h"
38
39 /*
40 *
41 */
42 15 PlaylistEntryDynamic::PlaylistEntryDynamic(Playlist* playlist, PlaylistEntryBase* parent) :
43 PlaylistEntryBase(playlist, parent),
44 15 m_drainQueue(0),
45 15 m_currentEntry(-1) {
46
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 LogDebug(VB_PLAYLIST, "PlaylistEntryDynamic::PlaylistEntryDynamic()\n");
47
48
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 m_type = "dynamic";
49
50 static std::atomic<uint64_t> s_dynamicCounter{ 0 };
51
2/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
15 m_curlToken = "PlaylistEntryDynamic-" + std::to_string(s_dynamicCounter.fetch_add(1));
52 15 }
53
54 /*
55 *
56 */
57 // Longest a plugin or URL source may take to answer before the entry gives up.
58 static constexpr long long DYNAMIC_FETCH_TIMEOUT_MS = 15000;
59
60 60 PlaylistEntryDynamic::~PlaylistEntryDynamic() {
61 // Ordering matters for UAF/lifecycle safety:
62 // 1) cancelRequests() removes this entry's in-flight handles from
63 // CurlManager and destroys their callbacks (each captures `this`)
64 // WITHOUT invoking them, so no callback can run against a half-torn-down
65 // object. Safe from the dtor because processCurls() and this dtor both
66 // run on the main loop, so no callback is executing right now.
67 // 2) only then release the cookie session: once the requests are gone, no
68 // easy handle still points CURLOPT_SHARE at the share, so
69 // curl_share_cleanup() is safe.
70 30 CurlManager::INSTANCE.cancelRequests(m_curlToken);
71 30 CurlManager::INSTANCE.releaseCookieSession(m_curlToken);
72
73 30 ClearPlaylistEntries();
74 60 }
75
76 /*
77 *
78 */
79 15 int PlaylistEntryDynamic::Init(Json::Value& config) {
80 15 LogDebug(VB_PLAYLIST, "PlaylistEntryDynamic::Init()\n");
81
82
2/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
15 m_subType = config["subType"].asString();
83
84 15 m_drainQueue = config["drainQueue"].asInt();
85
86
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 14 times.
15 if (config.isMember("pluginHost"))
87
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 m_pluginHost = config["pluginHost"].asString();
88
89
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 5 times.
15 if (m_subType == "file")
90
2/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
10 m_data = config["dataFile"].asString();
91
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4 times.
5 else if (m_subType == "plugin")
92
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 m_data = config["pluginName"].asString();
93
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 else if (m_subType == "url")
94
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 m_data = config["url"].asString();
95
96 // The plugin/url HTTP requests go through CurlManager (see PrepPlugin()/
97 // FetchURL()); there is no per-instance curl handle to set up.
98 // Cookie persistence across the plugin's prep/load/started calls is provided
99 // by CurlManager's cookie session keyed on m_curlToken.
100
101 15 return PlaylistEntryBase::Init(config);
102 }
103
104 /*
105 *
106 */
107 17 int PlaylistEntryDynamic::StartPlaying(void) {
108 17 LogDebug(VB_PLAYLIST, "PlaylistEntryDynamic::StartPlaying()\n");
109
110
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 17 times.
17 if (!CanPlay()) {
111 ✗ FinishPlay();
112 ✗ return 0;
113 }
114
115
6/6
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 10 times.
✓ Branch 4 taken 7 times.
✓ Branch 5 taken 10 times.
17 if ((m_subType == "plugin") || (m_subType == "url")) {
116
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 4 times.
7 int started = (m_subType == "plugin") ? PrepPlugin() : FetchURL();
117
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 7 times.
7 if (!started) {
118 ✗ FinishPlay();
119 ✗ return 0;
120 }
121 7 m_loading = true;
122 7 m_loadDeadline = GetMonotonicTimeMS() + DYNAMIC_FETCH_TIMEOUT_MS;
123 7 return PlaylistEntryBase::StartPlaying();
124 }
125
126 10 int res = 0;
127
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (m_subType == "command")
128 ✗ res = ReadFromCommand();
129
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 else if (m_subType == "file")
130 10 res = ReadFromFile();
131
132
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 4 times.
10 if (!BeginEntries(res)) {
133 6 FinishPlay();
134 6 return 0;
135 }
136
137 4 return PlaylistEntryBase::StartPlaying();
138 }
139
140 15 bool PlaylistEntryDynamic::BeginEntries(int loaded) {
141
3/4
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 7 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
15 if (!loaded || (m_currentEntry < 0))
142 7 return false;
143
144 8 m_isPrepped = 0;
145 8 m_playlistEntries[m_currentEntry]->StartPlayingOrFinish();
146 8 Started();
147 8 return true;
148 }
149
150 /*
151 *
152 */
153 437 int PlaylistEntryDynamic::Process(void) {
154
2/2
✓ Branch 0 taken 424 times.
✓ Branch 1 taken 13 times.
437 if (m_loading) {
155 PluginPrep state;
156 424 std::string item;
157 {
158
1/2
✓ Branch 0 taken 424 times.
✗ Branch 1 not taken.
424 std::lock_guard<std::mutex> g(m_pluginMutex);
159 424 state = m_pluginPrep;
160
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 419 times.
424 if (state == PluginPrep::Ready) {
161 5 item = std::move(m_pluginItem);
162 5 m_pluginItem.clear();
163 5 m_pluginPrep = PluginPrep::Idle; // consumed; the next start re-fetches
164 }
165 424 }
166
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 419 times.
424 if (state == PluginPrep::Ready) {
167 5 m_loading = false;
168
5/8
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 4 times.
5 if (!BeginEntries(ReadFromString(item)))
169
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 FinishPlay();
170
6/8
✓ Branch 0 taken 417 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 417 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 417 times.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 417 times.
419 } else if ((state == PluginPrep::Failed) || (GetMonotonicTimeMS() >= m_loadDeadline)) {
171 2 m_loading = false;
172
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (state != PluginPrep::Failed) {
173 ✗ LogErr(VB_PLAYLIST, "Dynamic playlist %s request did not complete in time\n", m_subType.c_str());
174 ✗ WarningHolder::AddWarningTimeout(60, 32, "Dynamic playlist " + m_subType + " request timed out");
175 }
176
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 FinishPlay();
177 }
178
1/2
✓ Branch 0 taken 424 times.
✗ Branch 1 not taken.
424 return PlaylistEntryBase::Process();
179 424 }
180
181
3/6
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
13 if ((m_currentEntry >= 0) && (m_playlistEntries[m_currentEntry])) {
182 // Only a child that is playing gets Process(), as in
183 // Playlist::Process(). One that finished inside StartPlaying() (a
184 // disabled entry, a failed start) was never set up for it: a disabled
185 // command entry has no result, and processing it dereferenced null.
186
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 1 times.
13 if (m_playlistEntries[m_currentEntry]->IsPlaying())
187 12 m_playlistEntries[m_currentEntry]->Process();
188
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 if (m_playlistEntries[m_currentEntry]->IsFinished()) {
189
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
26 if ((m_parentPlaylist->getPlaylistStatus() == FPP_STATUS_STOPPING_GRACEFULLY) ||
190
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
13 (m_parentPlaylist->getPlaylistStatus() == FPP_STATUS_STOPPING_GRACEFULLY_AFTER_LOOP)) {
191 ✗ FinishPlay();
192
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 8 times.
13 } else if (m_currentEntry < (m_playlistEntries.size() - 1)) {
193 5 m_currentEntry++;
194 5 m_playlistEntries[m_currentEntry]->StartPlayingOrFinish();
195
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 6 times.
8 } else if (m_drainQueue) {
196 // Check for more entries to play, if there are none,
197 // then StartPlaying() will call FinishPlay().
198 2 StartPlaying();
199 } else {
200 6 FinishPlay();
201 }
202 }
203
204 13 return 1;
205 }
206
207 ✗ return 0;
208 }
209
210 /*
211 *
212 */
213 ✗ int PlaylistEntryDynamic::Stop(void) {
214 ✗ LogDebug(VB_PLAYLIST, "PlaylistEntryDynamic::Stop()\n");
215 ✗ m_loading = false;
216
217 ✗ if ((m_currentEntry >= 0) && (m_playlistEntries[m_currentEntry])) {
218 ✗ m_playlistEntries[m_currentEntry]->Stop();
219 ✗ return 1;
220 }
221
222 ✗ return PlaylistEntryBase::Stop();
223 }
224
225 /*
226 *
227 */
228 ✗ void PlaylistEntryDynamic::Dump(void) {
229 ✗ LogDebug(VB_PLAYLIST, "SubType : %s\n", m_subType.c_str());
230 ✗ LogDebug(VB_PLAYLIST, "Data : %s\n", m_data.c_str());
231 ✗ LogDebug(VB_PLAYLIST, "Drain Queue: %d\n", m_drainQueue);
232 ✗ LogDebug(VB_PLAYLIST, "Plugin Host: %s\n", m_pluginHost.c_str());
233 ✗ }
234
235 /*
236 *
237 */
238 30 Json::Value PlaylistEntryDynamic::GetConfig(void) {
239 30 Json::Value result = PlaylistEntryBase::GetConfig();
240
241
2/4
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 result["subType"] = m_subType;
242
2/4
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 result["data"] = m_data;
243
244
3/4
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✓ Branch 3 taken 10 times.
30 if (m_subType == "file") {
245
2/4
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
20 result["dataFile"] = m_data;
246
3/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 8 times.
10 } else if (m_subType == "plugin") {
247
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 result["pluginName"] = m_data;
248
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 result["pluginHost"] = m_pluginHost;
249
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 } else if (m_subType == "url") {
250
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 result["url"] = m_data;
251 }
252
253
2/4
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 result["drainQueue"] = m_drainQueue;
254
5/6
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 20 times.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 10 times.
✓ Branch 5 taken 20 times.
30 if ((m_currentEntry >= 0) && (m_playlistEntries[m_currentEntry]))
255
2/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
10 result["dynamic"] = m_playlistEntries[m_currentEntry]->GetConfig();
256
257 30 return result;
258 ✗ }
259 378 PlaylistEntryBase* PlaylistEntryDynamic::GetCurrentEntry() {
260
6/8
✓ Branch 0 taken 337 times.
✓ Branch 1 taken 41 times.
✓ Branch 2 taken 337 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 337 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 337 times.
✓ Branch 7 taken 41 times.
378 if ((m_currentEntry >= 0) && (m_currentEntry < m_playlistEntries.size()) && (m_playlistEntries[m_currentEntry])) {
261 337 return m_playlistEntries[m_currentEntry];
262 }
263 41 return nullptr;
264 }
265
266 /*
267 *
268 */
269 ✗ int PlaylistEntryDynamic::ReadFromCommand(void) {
270 ✗ LogDebug(VB_PLAYLIST, "ReadFromCommand: %s\n", m_data.c_str());
271
272 // FIXME, implement this and change return to 1
273 ✗ return 0;
274 }
275
276 /*
277 *
278 */
279 10 int PlaylistEntryDynamic::ReadFromFile(void) {
280
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 LogDebug(VB_PLAYLIST, "ReadFromFile: %s\n", m_data.c_str());
281
282
3/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 9 times.
10 if (!FileExists(m_data.c_str())) {
283
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_PLAYLIST, "Filename %s does not exist\n", m_data.c_str());
284
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.
1 WarningHolder::AddWarningTimeout(60, 33, "Dynamic playlist source file does not exist: " + m_data);
285 1 return 0;
286 }
287
288
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 std::ifstream t(m_data.c_str());
289
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 std::stringstream buffer;
290
291
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 buffer << t.rdbuf();
292
293
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
9 return ReadFromString(buffer.str());
294 9 }
295
296 /*
297 * The "url" source: one async GET whose response is consumed by Process().
298 * rc==0 means the transfer never got an HTTP response (connect/timeout).
299 */
300 4 int PlaylistEntryDynamic::FetchURL(void) {
301
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 LogDebug(VB_PLAYLIST, "FetchURL: %s\n", m_data.c_str());
302 {
303
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 std::lock_guard<std::mutex> g(m_pluginMutex);
304
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (m_pluginPrep == PluginPrep::Running) {
305 ✗ return 1; // the previous start's request is still out; use it
306 }
307 4 m_pluginPrep = PluginPrep::Running;
308 4 m_pluginItem.clear();
309
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 }
310
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 std::string url = m_data;
311
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 CurlManager::INSTANCE.addGet(
312 url,
313
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
4 [this, url](int rc, const std::string& resp) {
314
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 std::lock_guard<std::mutex> g(m_pluginMutex);
315
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 if (rc == 0) {
316
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_PLAYLIST, "Dynamic playlist URL request failed (%s): %s\n", url.c_str(), resp.c_str());
317
5/10
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2 times.
✗ Branch 9 not taken.
2 WarningHolder::AddWarningTimeout(60, 32, "Dynamic playlist URL request failed (" + url + "): " + resp);
318 2 m_pluginPrep = PluginPrep::Failed;
319 } else {
320
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 m_pluginItem = resp;
321 2 m_pluginPrep = PluginPrep::Ready;
322 }
323 4 },
324
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 m_curlToken, m_curlToken);
325 4 return 1;
326 4 }
327
328 /*
329 *
330 */
331 14 int PlaylistEntryDynamic::ReadFromString(std::string jsonStr) {
332
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 Json::Value root;
333 14 PlaylistEntryBase* playlistEntry = NULL;
334
335
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 LogDebug(VB_PLAYLIST, "ReadFromString(): String:\n%s\n", jsonStr.c_str());
336
337
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 14 times.
14 if (jsonStr.empty()) {
338 ✗ LogDebug(VB_PLAYLIST, "Empty string in ReadFromString()\n");
339 ✗ return 0;
340 }
341
342
3/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 11 times.
14 if (!LoadJsonFromString(jsonStr, root, JsonRoot::Object)) {
343
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 LogErr(VB_PLAYLIST, "Error parsing JSON: %s\n", jsonStr.c_str());
344
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
9 WarningHolder::AddWarningTimeout(60, 33, "Dynamic playlist data is not valid JSON");
345 3 return 0;
346 }
347
348
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 ClearPlaylistEntries();
349
350
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 Json::Value entries;
351
3/6
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 11 times.
✗ Branch 5 not taken.
22 if (JsonHas(root, "mainPlaylist"))
352
2/4
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
11 entries = root["mainPlaylist"];
353 ✗ else if (JsonHas(root, "leadIn"))
354 ✗ entries = root["leadIn"];
355 ✗ else if (JsonHas(root, "leadOut"))
356 ✗ entries = root["leadOut"];
357 ✗ else if (JsonHas(root, "playlistEntries"))
358 ✗ entries = root["playlistEntries"];
359 else
360 ✗ return 0;
361
362
2/4
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 11 times.
11 if (!entries.isArray()) {
363 ✗ LogErr(VB_PLAYLIST, "Expected a JSON array of dynamic playlist entries, got something else\n");
364 ✗ WarningHolder::AddWarningTimeout(60, 33, "Dynamic playlist data is not valid JSON");
365 ✗ return 0;
366 }
367
368
3/4
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✓ Branch 3 taken 9 times.
26 for (int i = 0; i < entries.size(); i++) {
369
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
17 Json::Value pe = entries[i];
370
3/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 15 times.
17 if (!pe.isObject()) {
371
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_PLAYLIST, "Invalid dynamic playlist entry: expected a JSON object\n");
372
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
6 WarningHolder::AddWarningTimeout(60, 33, "Dynamic playlist data is not valid JSON");
373 2 continue;
374 }
375 15 playlistEntry = NULL;
376
377 // Plugins, URLs and files supply this data. jsoncpp throws on a
378 // wrong-typed value ("type" an object, "enabled":"1", ...) and nothing
379 // above StartPlaying() catches, so an escape terminated fppd. Treat a
380 // throw exactly like a failed Init().
381 try {
382
3/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 1 times.
15 std::string type = pe["type"].asString();
383
2/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 14 times.
14 if (type == "both")
384 ✗ playlistEntry = new PlaylistEntryBoth(m_parentPlaylist);
385
2/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
14 else if (type == "command")
386
3/8
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 14 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
14 playlistEntry = new PlaylistEntryCommand(m_parentPlaylist);
387 ✗ else if (type == "media")
388 ✗ playlistEntry = new PlaylistEntryMedia(m_parentPlaylist);
389 ✗ else if (type == "pause")
390 ✗ playlistEntry = new PlaylistEntryPause(m_parentPlaylist);
391 ✗ else if (type == "remap")
392 ✗ playlistEntry = new PlaylistEntryRemap(m_parentPlaylist);
393 ✗ else if (type == "script")
394 ✗ playlistEntry = new PlaylistEntryScript(m_parentPlaylist);
395 ✗ else if (type == "sequence")
396 ✗ playlistEntry = new PlaylistEntrySequence(m_parentPlaylist);
397 ✗ else if (type == "url")
398 ✗ playlistEntry = new PlaylistEntryURL(m_parentPlaylist);
399 else {
400 ✗ LogErr(VB_PLAYLIST, "Invalid Playlist Entry Type: %s\n", pe["type"].asString().c_str());
401 ✗ WarningHolder::AddWarningTimeout(60, 33, "Dynamic playlist contains an invalid entry type: " + pe["type"].asString());
402 ✗ ClearPlaylistEntries();
403 ✗ return 0;
404 }
405
406
3/4
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
14 if (!playlistEntry->Init(pe)) {
407 ✗ LogErr(VB_PLAYLIST, "Error initializing %s Playlist Entry\n", pe["type"].asString().c_str());
408 ✗ WarningHolder::AddWarningTimeout(60, 33, "Dynamic playlist entry failed to initialize: " + pe["type"].asString());
409 // Not yet pushed into m_playlistEntries (that happens only after a
410 // successful Init below), so ClearPlaylistEntries() won't free it.
411 ✗ delete playlistEntry;
412 ✗ ClearPlaylistEntries();
413 ✗ return 0;
414 }
415
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
16 } catch (const std::exception& ex) {
416
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_PLAYLIST, "Invalid dynamic playlist entry: %s\n", ex.what());
417
4/8
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
6 WarningHolder::AddWarningTimeout(60, 33, std::string("Dynamic playlist entry is invalid: ") + ex.what());
418
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 delete playlistEntry;
419
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 ClearPlaylistEntries();
420 2 return 0;
421 2 }
422
423
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 m_playlistEntries.push_back(playlistEntry);
424
3/3
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 2 times.
17 }
425
426
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 8 times.
9 if (!m_playlistEntries.size()) {
427
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_PLAYLIST, "Error, no valid playlistEntries in dynamic data!\n");
428
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, 33, "Dynamic playlist data contained no valid entries");
429 1 return 0;
430 }
431
432 8 m_currentEntry = 0;
433
434 8 return 1;
435 14 }
436
437 /*
438 *
439 */
440 8 int PlaylistEntryDynamic::Started(void) {
441 8 LogDebug(VB_PLAYLIST, "PlaylistEntryDynamic::Started()\n");
442
443 8 int res = 1;
444
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 6 times.
8 if (m_subType == "plugin")
445 2 res = StartedPlugin();
446
447
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (!res) {
448 ✗ return 0;
449 }
450
451 8 return res;
452 }
453
454 /*
455 *
456 */
457 2 int PlaylistEntryDynamic::StartedPlugin(void) {
458 // Fire-and-forget notification. The response is discarded and the entry does
459 // not wait on it, so it runs async through CurlManager. It uses the same
460 // cookie session (m_curlToken) so the plugin sees one PHP session, and it is
461 // ordered after loadNextItem because we only get here once the buffered item
462 // has been consumed and started. The callback captures nothing (no `this`),
463 // so it is safe even if this entry is destroyed before it completes; the
464 // owner tag still lets the dtor's cancelRequests() reclaim it.
465
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::string url = PluginCommandURL("startedNextItem");
466
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 CurlManager::INSTANCE.addGet(
467
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
2 url, [](int rc, const std::string& resp) {}, m_curlToken, m_curlToken);
468 2 return 1;
469 2 }
470
471 /*
472 *
473 */
474 ✗ int PlaylistEntryDynamic::Prep(void) {
475 ✗ LogDebug(VB_PLAYLIST, "PlaylistEntryDynamic::Prep()\n");
476
477 ✗ int res = 1;
478 ✗ if (m_subType == "plugin")
479 ✗ res = PrepPlugin();
480
481 ✗ if (!res) {
482 ✗ return 0;
483 }
484
485 ✗ return PlaylistEntryBase::Prep();
486 ;
487 }
488
489 /*
490 *
491 */
492 3 int PlaylistEntryDynamic::PrepPlugin(void) {
493 // Idempotent: if a prep/load chain is already running or has already buffered
494 // an item (an earlier Prep() primed it), leave it be. Only fire when
495 // Idle or after a prior Failed attempt (an inline-prep retry).
496 {
497
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 std::lock_guard<std::mutex> g(m_pluginMutex);
498
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
3 if (m_pluginPrep == PluginPrep::Running || m_pluginPrep == PluginPrep::Ready) {
499 ✗ return 1;
500 }
501 3 m_pluginPrep = PluginPrep::Running;
502 3 m_pluginItem.clear();
503
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 }
504
505
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 std::string url = PluginCommandURL("prepNextItem");
506
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 LogDebug(VB_PLAYLIST, "PrepPlugin URL: %s\n", url.c_str());
507
508 // prepNextItem's response is a notification (discarded). Its completion
509 // callback chains loadNextItem, guaranteeing the stateful prep->load order
510 // over the shared cookie session. rc==0 means the transfer never reached the
511 // host (connect/timeout) -> abort the chain and mark Failed.
512
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 CurlManager::INSTANCE.addGet(
513 url,
514 ✗ [this](int rc, const std::string& resp) {
515
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (rc == 0) {
516 ✗ std::lock_guard<std::mutex> g(m_pluginMutex);
517 ✗ m_pluginPrep = PluginPrep::Failed;
518 ✗ return;
519 ✗ }
520 3 FireLoadNextItem();
521 },
522
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
3 m_curlToken, m_curlToken);
523
524 3 return 1;
525 3 }
526
527 /*
528 *
529 */
530 3 void PlaylistEntryDynamic::FireLoadNextItem(void) {
531
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 std::string url = PluginCommandURL("loadNextItem");
532
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 LogDebug(VB_PLAYLIST, "FireLoadNextItem URL: %s\n", url.c_str());
533
534 // loadNextItem's response IS the next item's JSON; buffer it for
535 // Process() to consume. Runs on the main-loop thread (processCurls()),
536 // same as the consumer, so the lock only guards against the defensive case
537 // of a callback landing on another processCurls() call site.
538
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 CurlManager::INSTANCE.addGet(
539 url,
540 ✗ [this](int rc, const std::string& resp) {
541
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 std::lock_guard<std::mutex> g(m_pluginMutex);
542
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (rc == 0) {
543 ✗ m_pluginPrep = PluginPrep::Failed;
544 } else {
545
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 m_pluginItem = resp;
546 3 m_pluginPrep = PluginPrep::Ready;
547 }
548 3 },
549
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
3 m_curlToken, m_curlToken);
550 3 }
551
552 /*
553 *
554 */
555 8 std::string PlaylistEntryDynamic::PluginCommandURL(const char* command) const {
556
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 std::string url = "http://";
557
5/14
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 8 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 8 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
8 url += (m_pluginHost != "") ? m_pluginHost : "127.0.0.1";
558
3/6
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
8 url += "/plugin.php?plugin=" + m_data + "&page=playlistCallback.php&nopage=1&command=";
559
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 url += command;
560 8 return url;
561 ✗ }
562
563 /*
564 *
565 */
566 28 void PlaylistEntryDynamic::ClearPlaylistEntries(void) {
567
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 28 times.
41 while (!m_playlistEntries.empty()) {
568
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 delete m_playlistEntries.back();
569 13 m_playlistEntries.pop_back();
570 }
571
572 28 m_currentEntry = -1;
573 28 }
574