GCC Code Coverage Report


Directory: ./
File: playlist/PlaylistEntryMedia.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 353 419 84.2%
Functions: 22 24 91.7%
Branches: 374 758 49.3%

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 <filesystem>
20 #include <thread>
21 using namespace std::filesystem;
22
23 #include <sys/wait.h>
24
25 #include "../Events.h"
26 #include "../MultiSync.h"
27 #include "../commands/Commands.h"
28 #include "../Plugins.h"
29 #include "../channeloutput/ChannelOutputSetup.h"
30 #include "../common.h"
31 #include "../log.h"
32 #include "../mediadetails.h"
33
34 #include "Playlist.h"
35 #include "PlaylistEntryMedia.h"
36
37 int PlaylistEntryMedia::m_openStartDelay = -1;
38
39 /*
40 *
41 */
42 95 PlaylistEntryMedia::PlaylistEntryMedia(Playlist* playlist, PlaylistEntryBase* parent) :
43 PlaylistEntryBase(playlist, parent),
44 95 m_status(0),
45 95 m_mediaOutput(NULL),
46
1/2
✓ Branch 0 taken 95 times.
✗ Branch 1 not taken.
190 m_videoOutput("--Default--"),
47 95 m_streamSlot(1),
48 95 m_slotStatus(&mediaOutputStatus),
49 95 m_openTime(0),
50
1/2
✓ Branch 0 taken 95 times.
✗ Branch 1 not taken.
95 m_fileMode("single"),
51 95 m_duration(0),
52 285 m_pausedTime(-1) {
53
1/2
✓ Branch 0 taken 95 times.
✗ Branch 1 not taken.
95 LogDebug(VB_PLAYLIST, "PlaylistEntryMedia::PlaylistEntryMedia()\n");
54
2/2
✓ Branch 0 taken 57 times.
✓ Branch 1 taken 38 times.
95 if (m_openStartDelay == -1) {
55
1/2
✓ Branch 0 taken 57 times.
✗ Branch 1 not taken.
57 m_openStartDelay = getSettingInt("openStartDelay");
56 }
57
1/2
✓ Branch 0 taken 95 times.
✗ Branch 1 not taken.
95 m_type = "media";
58 95 m_fileSeed = (unsigned int)time(NULL);
59 95 }
60
61 /*
62 *
63 */
64 264 PlaylistEntryMedia::~PlaylistEntryMedia() {
65 // Companion streams belong to this entry; deleting a playing entry
66 // (Playlist::Cleanup() doesn't Stop() first) left them playing.
67 132 StopExtraMedia();
68
69 // Playlist::Cleanup() deletes entries without calling Stop() first — if
70 // media was still open the output object (and its live GStreamer
71 // pipeline) leaked. Swap under the lock, tear down outside it, same
72 // discipline as CloseMediaOutput().
73 132 MediaOutputBase* out = nullptr;
74 {
75 132 std::lock_guard<std::mutex> lock(m_mediaOutputLock);
76 132 out = m_mediaOutput;
77 132 m_mediaOutput = nullptr;
78 132 }
79
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 66 times.
132 delete out;
80 264 }
81
82 /*
83 *
84 */
85 95 int PlaylistEntryMedia::Init(Json::Value& config) {
86 95 LogDebug(VB_PLAYLIST, "PlaylistEntryMedia::Init()\n");
87
88
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 84 times.
95 if (config.isMember("fileMode")) {
89
2/4
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
11 m_fileMode = config["fileMode"].asString();
90 }
91
92
2/2
✓ Branch 0 taken 84 times.
✓ Branch 1 taken 11 times.
95 if (m_fileMode == "single") {
93
1/2
✓ Branch 0 taken 84 times.
✗ Branch 1 not taken.
84 if (config.isMember("mediaName")) {
94
2/4
✓ Branch 0 taken 84 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 84 times.
✗ Branch 3 not taken.
84 m_mediaFilename = config["mediaName"].asString();
95 } else {
96 ✗ LogErr(VB_PLAYLIST, "Missing mediaName entry\n");
97 ✗ return 0;
98 }
99 } else {
100
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 11 times.
11 if (config.isMember("mediaPrefix")) {
101 ✗ m_mediaPrefix = config["mediaPrefix"].asString();
102 }
103
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 if (GetFileList()) {
104
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 m_mediaFilename = GetNextRandomFile();
105 } else {
106 ✗ LogErr(VB_PLAYLIST, "Error, no files found when trying to play random file in %s mode\n",
107 m_fileMode.c_str());
108 ✗ WarningHolder::AddWarningTimeout(60, 30, "No media files found to play in '" + m_fileMode + "' mode");
109 ✗ return 0;
110 }
111 }
112
113
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 93 times.
95 if (config.isMember("videoOut")) {
114
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 m_videoOutput = config["videoOut"].asString();
115 }
116
117 // Companion streams to run alongside this entry (see ExtraMedia).
118 95 m_extraMedia.clear();
119
5/6
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 92 times.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✓ Branch 5 taken 92 times.
95 if (config.isMember("extraMedia") && config["extraMedia"].isArray()) {
120
8/14
✓ 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.
✓ Branch 6 taken 3 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 3 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 6 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 3 times.
✓ Branch 13 taken 3 times.
6 for (const auto& e : config["extraMedia"]) {
121 3 ExtraMedia em;
122
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.
3 em.mediaName = e.get("mediaName", "").asString();
123
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (em.mediaName.empty())
124 ✗ continue;
125
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.
3 em.slot = e.get("slot", 2).asInt();
126
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
3 if (em.slot < 2 || em.slot > StreamSlotManager::MAX_SLOTS) {
127 ✗ LogWarn(VB_PLAYLIST, "Media entry: extraMedia '%s' has slot %d; must be 2-%d (slot 1 is this entry's own media), skipping\n",
128 em.mediaName.c_str(), em.slot, StreamSlotManager::MAX_SLOTS);
129 ✗ continue;
130 }
131
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.
3 em.videoOut = e.get("videoOut", "").asString();
132
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.
3 em.sync = e.get("sync", false).asBool();
133
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 m_extraMedia.push_back(em);
134
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 }
135 }
136 // Flat single-companion form, which is what the playlist editor writes --
137 // the editor rebuilds an entry from the fields it knows about, so a
138 // hand-authored "extraMedia" array survives fppd but not a round trip
139 // through the UI. One companion covers both things this was asked for (a
140 // second audio feed on another device, a second video on another HDMI), so
141 // that case gets real fields and the array stays the advanced escape hatch.
142
10/16
✓ Branch 0 taken 95 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 93 times.
✓ 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.
✓ Branch 10 taken 2 times.
✓ Branch 11 taken 93 times.
✓ Branch 12 taken 2 times.
✓ Branch 13 taken 93 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
95 if (config.isMember("extraMediaName") && !config["extraMediaName"].asString().empty()) {
143 2 ExtraMedia em;
144
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 em.mediaName = config["extraMediaName"].asString();
145 // The slot is the output route -- which audio device a companion lands
146 // on is set per slot in the PipeWire audio groups, not per entry. The
147 // editor writes this as a string; hand-authored playlists may use an
148 // int, and asInt() on a string throws, so handle both.
149 2 em.slot = 2;
150
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 if (config.isMember("extraMediaSlot")) {
151
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 const Json::Value& sv = config["extraMediaSlot"];
152
6/16
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2 times.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✓ Branch 13 taken 2 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
2 int s = sv.isString() ? atoi(sv.asString().c_str()) : (sv.isIntegral() ? sv.asInt() : 0);
153
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 if (s >= 2 && s <= StreamSlotManager::MAX_SLOTS) {
154 2 em.slot = s;
155 ✗ } else if (s != 0) {
156 ✗ LogWarn(VB_PLAYLIST, "Media entry: extraMediaSlot %d out of range, using slot 2\n", s);
157 }
158 }
159
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.
2 em.videoOut = config.get("extraMediaOut", "").asString();
160 2 em.sync = true; // it belongs to the show; sync is only a repair path
161
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 m_extraMedia.push_back(em);
162 2 }
163
164 // Stream slot (1-5), default 1. Slot > 1 plays on a separate PipeWire stream.
165
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 95 times.
95 if (config.isMember("streamSlot")) {
166 ✗ m_streamSlot = config["streamSlot"].asInt();
167 ✗ if (m_streamSlot < 1) m_streamSlot = 1;
168 ✗ if (m_streamSlot > StreamSlotManager::MAX_SLOTS) m_streamSlot = StreamSlotManager::MAX_SLOTS;
169 }
170 95 m_slotStatus = StreamSlotManager::Instance().GetStatus(m_streamSlot);
171
172 95 m_pausedTime = -1;
173 95 return PlaylistEntryBase::Init(config);
174 }
175
176 115 int PlaylistEntryMedia::PreparePlay() {
177 115 LogDebug(VB_PLAYLIST, "PlaylistEntryMedia::PreparePlay()\n");
178
179
4/6
✓ Branch 0 taken 103 times.
✓ Branch 1 taken 12 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 103 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 115 times.
115 if (!m_resuming && !CanPlay()) {
180 ✗ FinishPlay();
181 ✗ return 0;
182 }
183 {
184 // mediaSeconds too: it still holds where the previous media on this
185 // slot ended, and StartPlaying() takes it as a lower bound for
186 // m_duration -- a longer clip before this one became this entry's
187 // length. A resume restores the whole slot status after this.
188
1/2
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
115 std::lock_guard<std::mutex> lock(MediaOutputStatus::FieldsLock());
189 115 m_slotStatus->minutesTotal = 0;
190 115 m_slotStatus->secondsTotal = 0;
191 115 m_slotStatus->secondsElapsed = 0;
192 115 m_slotStatus->subSecondsElapsed = 0;
193 115 m_slotStatus->mediaSeconds = 0.0;
194 115 }
195
196
6/6
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 101 times.
✓ Branch 2 taken 11 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 11 times.
✓ Branch 5 taken 104 times.
115 if ((m_fileMode != "single") && !m_resuming) {
197
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 10 times.
11 if (m_files.size()) {
198
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 m_mediaFilename = GetNextRandomFile();
199 } else {
200
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 1 times.
10 if (GetFileList()) {
201
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 m_mediaFilename = GetNextRandomFile();
202 } else {
203 1 LogErr(VB_PLAYLIST, "Error, no files found when trying to play random file in %s mode\n",
204 m_fileMode.c_str());
205
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, 30, "No media files found to play in '" + m_fileMode + "' mode");
206 1 return 0;
207 }
208 }
209 }
210
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 114 times.
114 if (!OpenMediaOutput()) {
211 ✗ FinishPlay();
212 ✗ return 0;
213 }
214
215
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 106 times.
114 if (multiSync->isMultiSyncEnabled()) {
216 8 multiSync->SendMediaOpenPacket(m_mediaFilename);
217 }
218
219 114 m_openTime = GetMonotonicTimeMS();
220
2/4
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 114 times.
✗ Branch 3 not taken.
304 Events::Publish("playlist/media/status", m_mediaFilename);
221
2/4
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 114 times.
✗ Branch 3 not taken.
228 Events::Publish("playlist/media/title", MediaDetails::INSTANCE.title);
222
2/4
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 114 times.
✗ Branch 3 not taken.
114 Events::Publish("playlist/media/artist", MediaDetails::INSTANCE.artist);
223 114 return 1;
224 }
225
226 /*
227 *
228 */
229 103 int PlaylistEntryMedia::StartPlaying(void) {
230
1/2
✓ Branch 0 taken 103 times.
✗ Branch 1 not taken.
103 LogDebug(VB_PLAYLIST, "PlaylistEntryMedia::StartPlaying()\n");
231
232
6/6
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 95 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 6 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 101 times.
103 if (multiSync->isMultiSyncEnabled() && m_openTime) {
233
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 long long st = GetMonotonicTimeMS() - m_openTime;
234
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (st < m_openStartDelay) {
235 ✗ std::this_thread::sleep_for(std::chrono::milliseconds(m_openStartDelay - st));
236 }
237 }
238
239
2/2
✓ Branch 0 taken 57 times.
✓ Branch 1 taken 46 times.
103 if (m_mediaOutput == nullptr) {
240
3/4
✓ Branch 0 taken 57 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 56 times.
57 if (PreparePlay() == 0) {
241 1 return 0;
242 }
243 }
244
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 102 times.
102 if (m_mediaOutput == nullptr) {
245 ✗ return 0;
246 }
247
248 102 float f = m_slotStatus->mediaSeconds * 1000;
249
1/2
✓ Branch 0 taken 102 times.
✗ Branch 1 not taken.
102 if (m_duration == 0) {
250 102 float f = m_slotStatus->minutesTotal * 60 + m_slotStatus->secondsTotal;
251 102 f *= 1000;
252 102 m_duration = f;
253 }
254
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 102 times.
102 if (f > m_duration) {
255 ✗ m_duration = f;
256 }
257
1/2
✓ Branch 0 taken 102 times.
✗ Branch 1 not taken.
102 std::unique_lock<std::mutex> lock(m_mediaOutputLock);
258
259
6/6
✓ Branch 0 taken 93 times.
✓ Branch 1 taken 9 times.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 86 times.
✓ Branch 4 taken 7 times.
✓ Branch 5 taken 95 times.
102 if (m_mediaOutput->getMediaOffsetMS() <= 0 && multiSync->isMultiSyncEnabled()) {
260
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 multiSync->SendMediaSyncStartPacket(m_mediaFilename);
261 }
262
263
1/2
✓ Branch 0 taken 102 times.
✗ Branch 1 not taken.
102 ResetChannelOutputFrameNumber();
264
3/4
✓ Branch 0 taken 102 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 100 times.
102 if (!m_mediaOutput->Start()) {
265
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_MEDIAOUT, "Could not start media %s\n", m_mediaOutput->m_mediaFilename.c_str());
266
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.
2 WarningHolder::AddWarningTimeout(60, 30, "Could not start media " + m_mediaOutput->m_mediaFilename);
267 // Swap-unlock-delete: the delete runs the full GStreamer teardown
268 // (sleeps plus a state change that can block indefinitely on a
269 // wedged decoder) — doing that under m_mediaOutputLock wedges any
270 // thread touching this entry, including HTTP status threads that
271 // are holding m_playlistMutex. Same rationale as CloseMediaOutput.
272 2 MediaOutputBase* failed = m_mediaOutput;
273 2 m_mediaOutput = 0;
274
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 lock.unlock();
275
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 delete failed;
276
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 FinishPlay();
277 2 return 0;
278 }
279
280
1/2
✓ Branch 0 taken 100 times.
✗ Branch 1 not taken.
100 lock.unlock();
281
1/2
✓ Branch 0 taken 100 times.
✗ Branch 1 not taken.
100 m_startTime = GetMonotonicTimeMS();
282 100 m_syncStartSent = false;
283
1/2
✓ Branch 0 taken 100 times.
✗ Branch 1 not taken.
100 StartExtraMedia();
284
1/2
✓ Branch 0 taken 100 times.
✗ Branch 1 not taken.
100 return PlaylistEntryBase::StartPlaying();
285 102 }
286
287 /*
288 *
289 */
290 145682 int PlaylistEntryMedia::Process(void) {
291
1/2
✓ Branch 0 taken 145682 times.
✗ Branch 1 not taken.
145682 std::unique_lock<std::mutex> lock(m_mediaOutputLock);
292
293
1/2
✓ Branch 0 taken 145682 times.
✗ Branch 1 not taken.
145682 if (m_mediaOutput) {
294
8/8
✓ Branch 0 taken 141253 times.
✓ Branch 1 taken 4429 times.
✓ Branch 2 taken 1376 times.
✓ Branch 3 taken 139877 times.
✓ Branch 4 taken 21 times.
✓ Branch 5 taken 1355 times.
✓ Branch 6 taken 21 times.
✓ Branch 7 taken 145661 times.
145682 if (!m_syncStartSent && m_mediaOutput->getMediaOffsetMS() > 0 && multiSync->isMultiSyncEnabled()) {
295
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 long long ct = GetMonotonicTimeMS() - m_startTime;
296
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 20 times.
21 if (ct > (m_mediaOutput->getMediaOffsetMS() - 20)) {
297
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 multiSync->SendMediaSyncStartPacket(m_mediaFilename);
298 1 m_syncStartSent = true;
299 }
300 }
301
302
1/2
✓ Branch 0 taken 145682 times.
✗ Branch 1 not taken.
145682 m_mediaOutput->Process();
303
3/4
✓ Branch 0 taken 145682 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53 times.
✓ Branch 3 taken 145629 times.
145682 if (!m_mediaOutput->IsPlaying()) {
304
1/2
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
53 FinishPlay();
305
1/2
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
53 lock.unlock();
306 // Companions are tied to this entry's lifetime, so they stop when
307 // the media ends naturally too -- not only on an explicit Stop().
308
1/2
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
53 StopExtraMedia();
309
1/2
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
53 CloseMediaOutput();
310
1/2
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
53 lock.lock();
311 }
312 } else {
313 ✗ FinishPlay();
314 }
315
316
1/2
✓ Branch 0 taken 145682 times.
✗ Branch 1 not taken.
145682 lock.unlock();
317
1/2
✓ Branch 0 taken 145682 times.
✗ Branch 1 not taken.
291364 return PlaylistEntryBase::Process();
318 145682 }
319
320 31 bool PlaylistEntryMedia::HasExtraAtEnd() {
321 // m_mediaOutput can be deleted concurrently by CloseMediaOutput() (e.g.
322 // the HTTP status thread calling this while the playlist thread tears
323 // down media), so the pointer must not be dereferenced without the lock.
324
1/2
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
31 std::lock_guard<std::mutex> lock(m_mediaOutputLock);
325 31 bool result = false;
326
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 15 times.
31 if (m_mediaOutput) {
327 16 result = m_mediaOutput->getMediaOffsetMS() < 0;
328 }
329 31 return result;
330 31 }
331 686 bool PlaylistEntryMedia::IsAudible() {
332
1/2
✓ Branch 0 taken 686 times.
✗ Branch 1 not taken.
686 std::lock_guard<std::mutex> lock(m_mediaOutputLock);
333
4/6
✓ Branch 0 taken 686 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 686 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 36 times.
✓ Branch 5 taken 650 times.
1372 return !m_mediaOutput || m_mediaOutput->IsAudible();
334 686 }
335 123565 int32_t PlaylistEntryMedia::GetMediaOffsetMS() {
336
1/2
✓ Branch 0 taken 123565 times.
✗ Branch 1 not taken.
123565 std::lock_guard<std::mutex> lock(m_mediaOutputLock);
337 123565 int32_t result = 0;
338
2/2
✓ Branch 0 taken 123550 times.
✓ Branch 1 taken 15 times.
123565 if (m_mediaOutput) {
339 123550 result = m_mediaOutput->getMediaOffsetMS();
340 }
341 123565 return result;
342 123565 }
343
344 27506 uint64_t PlaylistEntryMedia::GetLengthInMS() {
345
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 27494 times.
27506 if (m_duration == 0) {
346
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 MediaDetails details;
347
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 details.ParseMedia(m_mediaFilename.c_str());
348 12 m_duration = details.lengthMS;
349 12 }
350 // Always try to refine from the running media output status.
351 // Some backends (e.g. GStreamer) may not have the duration available
352 // immediately at Start(), so minutesTotal/secondsTotal get populated
353 // asynchronously. Re-checking here prevents GetElapsedMS()'s
354 // "if (f > m_duration)" guard from permanently caching a wrong value
355 // (i.e., the elapsed time) before the real duration was known.
356
4/4
✓ Branch 0 taken 26320 times.
✓ Branch 1 taken 1186 times.
✓ Branch 2 taken 26230 times.
✓ Branch 3 taken 90 times.
27506 if (m_slotStatus->minutesTotal > 0 || m_slotStatus->secondsTotal > 0) {
357 27416 uint64_t d = ((uint64_t)m_slotStatus->minutesTotal * 60 + m_slotStatus->secondsTotal) * 1000;
358
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 27415 times.
27416 if (d > m_duration) {
359 1 m_duration = d;
360 }
361 }
362 27506 return m_duration;
363 }
364 55024 uint64_t PlaylistEntryMedia::GetElapsedMS() {
365 // Before m_mediaOutput: Pause() closes the media output.
366
2/2
✓ Branch 0 taken 10626 times.
✓ Branch 1 taken 44398 times.
55024 if (IsPaused()) {
367 10626 return m_pausedTime;
368 }
369
1/2
✓ Branch 0 taken 44398 times.
✗ Branch 1 not taken.
44398 if (m_mediaOutput) {
370 44398 float f = m_slotStatus->secondsElapsed * 1000;
371 44398 f += m_slotStatus->subSecondsElapsed * 10; // subSec is in 1/100th second
372
2/2
✓ Branch 0 taken 46 times.
✓ Branch 1 taken 44352 times.
44398 if (m_duration == 0) {
373 46 float f = m_slotStatus->minutesTotal * 60 + m_slotStatus->secondsTotal;
374 46 f *= 1000;
375 46 m_duration = f;
376 }
377
2/2
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 44379 times.
44398 if (f > m_duration) {
378 19 m_duration = f;
379 }
380 44398 return f;
381 }
382 ✗ return 0;
383 }
384
385 /*
386 *
387 */
388 // Start/stop are delegated to the existing Play Media / Stop Media Slot
389 // commands rather than reimplementing slot setup here: those already handle
390 // registration, teardown and the sync flag, and reusing them keeps one code
391 // path for "play something on a slot".
392 112 void PlaylistEntryMedia::StartExtraMedia(int startMS) {
393
5/6
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 105 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 7 times.
✓ Branch 4 taken 105 times.
✓ Branch 5 taken 7 times.
112 if (m_extraMedia.empty() || m_extraMediaStarted)
394 105 return;
395 7 m_extraMediaStarted = true;
396
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 7 times.
14 for (const auto& em : m_extraMedia) {
397 std::vector<std::string> args = {
398 7 em.mediaName,
399 "1", // play once; it is stopped with the entry
400 "0", // no volume adjustment
401 7 std::to_string(em.slot),
402 7 em.videoOut,
403 7 em.sync ? "true" : "false"
404
3/6
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 42 times.
✓ Branch 3 taken 7 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
56 };
405
3/4
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
7 LogDebug(VB_PLAYLIST, "Media entry: starting extra media '%s' on slot %d (videoOut='%s'%s)\n",
406 em.mediaName.c_str(), em.slot, em.videoOut.c_str(),
407 em.sync ? ", synced" : "");
408
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
7 CommandManager::INSTANCE.run("Play Media", args);
409
410 // Resuming mid-entry: "Play Media" always starts at zero, so put the
411 // companion back on the entry's timeline explicitly. Waiting for the
412 // sync repair path is not enough -- it only acts past MAX_DRIFT_SEC,
413 // and only for companions that opted into sync at all.
414
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 6 times.
7 if (startMS > 0) {
415
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 StreamSlotManager::Instance().SeekSlot(em.slot, startMS / 1000.0f);
416 }
417 7 }
418
7/16
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 7 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 3 times.
✓ Branch 9 taken 4 times.
✓ Branch 10 taken 7 times.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
14 }
419
420 179 void PlaylistEntryMedia::StopExtraMedia(void) {
421
2/2
✓ Branch 0 taken 173 times.
✓ Branch 1 taken 6 times.
179 if (!m_extraMediaStarted)
422 173 return;
423 6 m_extraMediaStarted = false;
424
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 6 times.
12 for (const auto& em : m_extraMedia) {
425
3/6
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 6 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
18 std::vector<std::string> args = { std::to_string(em.slot) };
426
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 LogDebug(VB_COMMAND, "Media entry: stopping extra media '%s', running \"Stop Media Slot\" on slot %d\n", em.mediaName.c_str(), em.slot);
427
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 CommandManager::INSTANCE.run("Stop Media Slot", args);
428 6 }
429
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
6 }
430
431 48 int PlaylistEntryMedia::Stop(void) {
432 48 LogDebug(VB_PLAYLIST, "PlaylistEntryMedia::Stop()\n");
433
434 48 StopExtraMedia();
435 48 CloseMediaOutput();
436
437
3/6
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 48 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 48 times.
✗ Branch 5 not taken.
192 Events::Publish("playlist/media/status", "");
438
3/6
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 48 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 48 times.
✗ Branch 5 not taken.
192 Events::Publish("playlist/media/title", "");
439
3/6
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 48 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 48 times.
✗ Branch 5 not taken.
144 Events::Publish("playlist/media/artist", "");
440
441 {
442
1/2
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
48 std::lock_guard<std::mutex> lock(MediaOutputStatus::FieldsLock());
443 48 m_slotStatus->minutesTotal = 0;
444 48 m_slotStatus->secondsTotal = 0;
445 48 m_slotStatus->secondsElapsed = 0;
446 48 m_slotStatus->subSecondsElapsed = 0;
447 48 m_slotStatus->mediaSeconds = 0.0;
448 48 }
449
450 48 return PlaylistEntryBase::Stop();
451 }
452
453 /*
454 *
455 */
456 ✗ void PlaylistEntryMedia::Dump(void) {
457 ✗ PlaylistEntryBase::Dump();
458
459 ✗ LogDebug(VB_PLAYLIST, "Media Filename: %s\n", m_mediaFilename.c_str());
460 ✗ }
461
462 /*
463 *
464 */
465 114 int PlaylistEntryMedia::OpenMediaOutput(void) {
466
1/2
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
114 LogDebug(VB_PLAYLIST, "PlaylistEntryMedia::OpenMediaOutput() - Starting\n");
467
468 {
469
1/2
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
114 std::unique_lock<std::mutex> lock(m_mediaOutputLock);
470
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 114 times.
114 if (m_mediaOutput) {
471 ✗ lock.unlock();
472 ✗ CloseMediaOutput();
473 }
474 114 }
475
476
1/2
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
114 std::string tmpFile = m_mediaFilename;
477 114 std::size_t found = tmpFile.find_last_of(".");
478
479
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 114 times.
114 if (found == std::string::npos) {
480 ✗ LogWarn(VB_MEDIAOUT, "Unable to determine extension of media file %s\n",
481 m_mediaFilename.c_str());
482 ✗ return 0;
483 }
484
485
2/4
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 114 times.
✗ Branch 3 not taken.
114 std::string ext = toLowerCopy(tmpFile.substr(found + 1));
486
487
1/2
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
114 LogDebug(VB_PLAYLIST, "PlaylistEntryMedia - Starting %s\n", tmpFile.c_str());
488
489 // m_mediaOutputLock must NOT be held here: mediaCallback() re-enters this
490 // entry on the same thread via Playlist::GetInfo() -> GetCurrentEntry() ->
491 // GetConfig(), which takes m_mediaOutputLock. Holding it across the
492 // callback self-deadlocks the playlist thread (fppd hang on every media
493 // start). Nothing below touches m_mediaOutput until the assignment at
494 // the end, so the pointer needs no protection in between — only the
495 // final store is done under the lock.
496
1/2
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
114 MediaDetails::INSTANCE.ParseMedia(m_mediaFilename.c_str());
497
2/4
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 114 times.
✗ Branch 3 not taken.
114 PluginManager::INSTANCE.mediaCallback(m_parentPlaylist->GetInfo(), MediaDetails::INSTANCE);
498
499
1/2
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
114 std::string vOut = ResolveVideoOutput(m_videoOutput);
500
501
1/2
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
114 MediaOutputBase* out = CreateMediaOutput(tmpFile, vOut, m_streamSlot);
502
503
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 114 times.
114 if (!out) {
504 ✗ LogDebug(VB_MEDIAOUT, "No Media Output handler for %s\n", tmpFile.c_str());
505 ✗ return 0;
506 }
507
508 {
509
1/2
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
114 std::lock_guard<std::mutex> lock(m_mediaOutputLock);
510 114 m_mediaOutput = out;
511 114 }
512
513
1/2
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
114 LogDebug(VB_PLAYLIST, "PlaylistEntryMedia::OpenMediaOutput() - Complete\n");
514
515 114 return 1;
516 114 }
517
518 /*
519 *
520 */
521 113 int PlaylistEntryMedia::CloseMediaOutput(void) {
522
1/2
✓ Branch 0 taken 113 times.
✗ Branch 1 not taken.
113 LogDebug(VB_PLAYLIST, "PlaylistEntryMedia::CloseMediaOutput()\n");
523
524 113 m_slotStatus->status = MEDIAOUTPUTSTATUS_IDLE;
525
526
1/2
✓ Branch 0 taken 113 times.
✗ Branch 1 not taken.
113 std::unique_lock<std::mutex> lock(m_mediaOutputLock);
527
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 97 times.
113 if (!m_mediaOutput) {
528 16 return 0;
529 }
530
531 // Swap the member into a local and null it out while still holding the
532 // lock, then release the lock before the blocking teardown below.
533 // ~GStreamerOutput() (via delete) runs GStreamerOutput::Stop(), which
534 // has 250ms + 150ms sleeps and a gst_element_set_state(NULL) that can
535 // block indefinitely if the Pi's V4L2 decoder wedges (issue #2695).
536 // Holding m_mediaOutputLock across that would stall any other thread
537 // touching this entry (HTTP status thread, Process(), etc.) for the
538 // same duration.
539 97 MediaOutputBase* out = m_mediaOutput;
540 97 m_mediaOutput = NULL;
541
1/2
✓ Branch 0 taken 97 times.
✗ Branch 1 not taken.
97 lock.unlock();
542
543
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 89 times.
97 if (multiSync->isMultiSyncEnabled())
544
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 multiSync->SendMediaSyncStopPacket(m_mediaFilename);
545
546
1/2
✓ Branch 0 taken 97 times.
✗ Branch 1 not taken.
97 delete out;
547
548 97 return 1;
549 113 }
550
551 /*
552 *
553 */
554 21 int PlaylistEntryMedia::GetFileList(void) {
555 21 std::string dir;
556
557 21 m_files.clear();
558
559
2/4
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 21 times.
21 if (m_fileMode == "randomVideo")
560 ✗ dir = FPP_DIR_VIDEO("");
561
2/4
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21 times.
✗ Branch 3 not taken.
21 else if (m_fileMode == "randomAudio")
562
3/6
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 21 times.
✗ Branch 5 not taken.
42 dir = FPP_DIR_MUSIC("");
563
564 // Only media files of the kind this entry plays: without a prefix every
565 // directory entry went in, subdirectories included, and one picked as
566 // "random media" failed to play. Names are kept relative to the folder,
567 // as a single-file entry's are, rather than as this box's full path --
568 // they go out in MultiSync packets, and a remote looks them up in its own
569 // folder. A folder that can't be read is reported, not thrown.
570
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 bool video = (m_fileMode == "randomVideo");
571 21 std::error_code ec;
572
2/4
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21 times.
✗ Branch 3 not taken.
21 for (recursive_directory_iterator it(dir, directory_options::skip_permission_denied, ec), end;
573
6/8
✓ Branch 0 taken 157 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 178 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 157 times.
✓ Branch 5 taken 21 times.
✓ Branch 6 taken 157 times.
✓ Branch 7 taken 21 times.
178 !ec && it != end; it.increment(ec)) {
574 157 std::error_code fec;
575
2/2
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 121 times.
157 if (!it->is_regular_file(fec)) {
576 132 continue;
577 }
578
2/4
✓ Branch 0 taken 121 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 121 times.
✗ Branch 3 not taken.
121 std::string ext = it->path().extension().string();
579
1/2
✓ Branch 0 taken 121 times.
✗ Branch 1 not taken.
121 if (!ext.empty()) {
580
1/2
✓ Branch 0 taken 121 times.
✗ Branch 1 not taken.
121 ext.erase(0, 1);
581 }
582
4/8
✗ Branch 0 not taken.
✓ Branch 1 taken 121 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 121 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 96 times.
✓ Branch 7 taken 25 times.
121 if (video ? !IsExtensionVideo(ext) : !IsExtensionAudio(ext)) {
583 96 continue;
584 }
585
4/18
✗ Branch 0 not taken.
✓ Branch 1 taken 25 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 25 times.
✗ Branch 10 not taken.
✓ Branch 11 taken 25 times.
✗ Branch 12 not taken.
✓ Branch 13 taken 25 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
25 if (!m_mediaPrefix.empty() && it->path().stem().string().find(m_mediaPrefix) != 0) {
586 ✗ continue;
587 }
588
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 LogDebug(VB_PLAYLIST, "match: %s\n", it->path().c_str());
589
4/8
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 25 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 25 times.
✗ Branch 7 not taken.
25 m_files.push_back(it->path().lexically_relative(dir).string());
590
2/2
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 96 times.
142 }
591
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
21 if (ec) {
592 ✗ LogWarn(VB_PLAYLIST, "Could not list %s for random media: %s\n", dir.c_str(), ec.message().c_str());
593 }
594
595
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 LogDebug(VB_PLAYLIST, "%d files in %s directory\n", m_files.size(), dir.c_str());
596
597 42 return m_files.size();
598 21 }
599
600 /*
601 *
602 */
603 21 std::string PlaylistEntryMedia::GetNextRandomFile(void) {
604 21 std::string filename;
605
606
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
21 if (!m_files.size())
607 ✗ GetFileList();
608
609
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
21 if (!m_files.size()) {
610 ✗ LogWarn(VB_PLAYLIST, "No files found in GetNextRandomFile()\n");
611 ✗ return filename;
612 }
613
614 21 int i = rand_r(&m_fileSeed) % m_files.size();
615
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 filename = m_files[i];
616
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 m_files.erase(m_files.begin() + i);
617
618
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 LogDebug(VB_PLAYLIST, "GetNextRandomFile() = %s\n", filename.c_str());
619
620 21 return filename;
621 ✗ }
622
623 /*
624 *
625 */
626 300 Json::Value PlaylistEntryMedia::GetConfig(void) {
627
1/2
✓ Branch 0 taken 300 times.
✗ Branch 1 not taken.
300 Json::Value result = PlaylistEntryBase::GetConfig();
628 300 MediaOutputStatus status;
629 {
630
1/2
✓ Branch 0 taken 300 times.
✗ Branch 1 not taken.
300 std::lock_guard<std::mutex> lock(MediaOutputStatus::FieldsLock());
631
4/6
✓ Branch 0 taken 300 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✓ Branch 3 taken 288 times.
✓ Branch 4 taken 300 times.
✗ Branch 5 not taken.
300 status = IsPaused() ? m_pausedStatus : *m_slotStatus;
632 300 }
633
2/4
✓ Branch 0 taken 300 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 300 times.
✗ Branch 3 not taken.
300 result["mediaFilename"] = m_mediaFilename;
634
2/4
✓ Branch 0 taken 300 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 300 times.
✗ Branch 3 not taken.
300 result["streamSlot"] = m_streamSlot;
635
636 // Only need the pointer's non-null-ness here (the actual fields come
637 // from the already-copied "status" above), but the pointer itself can
638 // be deleted concurrently by CloseMediaOutput(), so check it under the
639 // lock rather than dereferencing/testing it unguarded.
640 bool hasMediaOutput;
641 {
642
1/2
✓ Branch 0 taken 300 times.
✗ Branch 1 not taken.
300 std::lock_guard<std::mutex> lock(m_mediaOutputLock);
643 300 hasMediaOutput = (m_mediaOutput != nullptr);
644 300 }
645
646
2/2
✓ Branch 0 taken 53 times.
✓ Branch 1 taken 247 times.
300 if (hasMediaOutput) {
647
2/4
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53 times.
✗ Branch 3 not taken.
53 result["status"] = status.status.load();
648
2/4
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53 times.
✗ Branch 3 not taken.
53 result["secondsElapsed"] = status.secondsElapsed;
649
2/4
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53 times.
✗ Branch 3 not taken.
53 result["subSecondsElapsed"] = status.subSecondsElapsed;
650
2/4
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53 times.
✗ Branch 3 not taken.
53 result["secondsRemaining"] = status.secondsRemaining;
651
2/4
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53 times.
✗ Branch 3 not taken.
53 result["subSecondsRemaining"] = status.subSecondsRemaining;
652
2/4
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53 times.
✗ Branch 3 not taken.
53 result["minutesTotal"] = status.minutesTotal;
653
2/4
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53 times.
✗ Branch 3 not taken.
53 result["secondsTotal"] = status.secondsTotal;
654
2/4
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53 times.
✗ Branch 3 not taken.
53 result["mediaSeconds"] = status.mediaSeconds;
655
2/4
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53 times.
✗ Branch 3 not taken.
53 result["mediaSeconds"] = status.mediaSeconds;
656
2/4
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53 times.
✗ Branch 3 not taken.
53 result["millisecondsElapsed"] = status.secondsElapsed * 1000 + status.subSecondsElapsed * 10;
657 }
658
659 300 return result;
660 300 }
661
662 ✗ Json::Value PlaylistEntryMedia::GetMqttStatus(void) {
663 ✗ Json::Value result = PlaylistEntryBase::GetMqttStatus();
664 ✗ MediaOutputStatus status;
665 {
666 ✗ std::lock_guard<std::mutex> lock(MediaOutputStatus::FieldsLock());
667 ✗ status = IsPaused() ? m_pausedStatus : *m_slotStatus;
668 ✗ }
669 ✗ result["secondsElapsed"] = status.secondsElapsed;
670 ✗ result["millisecondsElapsed"] = status.secondsElapsed * 1000 + status.subSecondsElapsed * 10;
671 ✗ result["secondsRemaining"] = status.secondsRemaining;
672 ✗ result["secondsTotal"] = status.minutesTotal * 60 + status.secondsTotal;
673 ✗ result["mediaName"] = m_mediaFilename;
674 ✗ result["mediaTitle"] = MediaDetails::INSTANCE.title;
675 ✗ result["mediaArtist"] = MediaDetails::INSTANCE.artist;
676
677 ✗ return result;
678 ✗ }
679
680 12 void PlaylistEntryMedia::Pause() {
681 {
682
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 std::lock_guard<std::mutex> lock(MediaOutputStatus::FieldsLock());
683
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 m_pausedStatus = *m_slotStatus;
684 12 }
685 12 m_pausedTime = GetElapsedMS();
686 // Pausing a media entry tears the output down rather than pausing the
687 // pipeline, so companions are stopped and rebuilt the same way; Resume()
688 // restarts them at m_pausedTime.
689 12 StopExtraMedia();
690 12 CloseMediaOutput();
691
3/6
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 12 times.
✗ Branch 5 not taken.
48 Events::Publish("playlist/media/status", "");
692
3/6
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 12 times.
✗ Branch 5 not taken.
48 Events::Publish("playlist/media/title", "");
693
3/6
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 12 times.
✗ Branch 5 not taken.
36 Events::Publish("playlist/media/artist", "");
694 12 }
695 347408 bool PlaylistEntryMedia::IsPaused() {
696 347408 return m_pausedTime >= 0;
697 }
698 12 void PlaylistEntryMedia::Resume() {
699
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 if (m_pausedTime >= 0) {
700 // Captured before the block below clears m_pausedTime, and used to put
701 // the companions back at the same offset as this entry's own media.
702 12 int resumeMS = m_pausedTime;
703 // PreparePlay()'s return is intentionally not treated as fatal here;
704 // if it failed, m_mediaOutput will be left null and is handled below
705 // instead of blindly dereferencing it.
706 12 m_resuming = true;
707 12 PreparePlay();
708 12 m_resuming = false;
709
710 // If Start() fails, the failed output is swapped out under the lock
711 // and deleted after it's released — its destructor runs the blocking
712 // GStreamer teardown (see CloseMediaOutput rationale).
713 12 MediaOutputBase* failed = nullptr;
714 {
715
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 std::lock_guard<std::mutex> lock(m_mediaOutputLock);
716
717
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 12 times.
12 if (!m_mediaOutput) {
718 // PreparePlay() couldn't (re)create the media output (e.g. file
719 // missing/unsupported). Nothing to resume -- still clear the
720 // paused state so the entry doesn't stay wedged reporting
721 // itself as paused forever.
722 ✗ LogErr(VB_MEDIAOUT, "Could not resume media %s, no media output available\n", m_mediaFilename.c_str());
723 ✗ m_pausedTime = -1;
724 {
725 ✗ std::lock_guard<std::mutex> lock(MediaOutputStatus::FieldsLock());
726 ✗ *m_slotStatus = m_pausedStatus;
727 ✗ }
728 ✗ return;
729 }
730
731
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 12 times.
12 if (multiSync->isMultiSyncEnabled())
732 ✗ multiSync->SendMediaSyncStartPacket(m_mediaFilename);
733 12 m_syncStartSent = true; // sent just above, offset or not
734
735
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 12 times.
12 if (!m_mediaOutput->Start(m_pausedTime)) {
736 ✗ LogErr(VB_MEDIAOUT, "Could not start media %s\n", m_mediaOutput->m_mediaFilename.c_str());
737 ✗ WarningHolder::AddWarningTimeout(60, 30, "Could not start media " + m_mediaOutput->m_mediaFilename);
738 ✗ failed = m_mediaOutput;
739 ✗ m_mediaOutput = nullptr;
740 }
741
742 // Whether Start() above succeeded or failed, the pause is over:
743 // clear m_pausedTime so the entry doesn't stay wedged reporting
744 // itself as paused, and restore the last known status snapshot.
745 // The lock (RAII) is released exactly once here on every path,
746 // when it goes out of scope.
747 12 m_pausedTime = -1;
748 {
749
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 std::lock_guard<std::mutex> lock(MediaOutputStatus::FieldsLock());
750
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 *m_slotStatus = m_pausedStatus;
751 12 }
752
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 }
753
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 12 times.
12 delete failed;
754
755 // Outside the media output lock: this dispatches commands, and holding
756 // the lock across that is how deadlocks get built.
757 12 StartExtraMedia(resumeMS);
758 }
759 }
760