GCC Code Coverage Report


Directory: ./
File: mediaoutput/StreamSlotManager.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 213 230 92.6%
Functions: 24 25 96.0%
Branches: 229 382 59.9%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2026 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 #include "log.h"
17 #include "common_mini.h"
18 #include "Timers.h"
19
20 #include "StreamSlotManager.h"
21 #include "GStreamerOut.h"
22 #include "mediaoutput.h"
23 #include "common.h"
24 #include "settings.h"
25
26 // The volume the user last set for a slot, from pipewire-stream-slots.json.
27 // Slot volumes apply to a stream that only exists while that slot is playing,
28 // so they cannot be restored at boot the way group volumes are -- they have to
29 // be reapplied each time the slot starts instead.
30 //
31 // All five slots are treated alike. Slot 1 used to be excluded because its
32 // fader was wired to the global master rather than to the stream, which made it
33 // a duplicate master control instead of a slot of its own.
34 //
35 // KEEP IN SYNC with SaveStreamSlotVolume()/GetStreamSlotVolumes() in
36 // www/api/controllers/pipewire.php.
37 1854 static int savedStreamSlotVolume(int slot) {
38
2/4
✓ Branch 0 taken 1854 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1854 times.
1854 if (slot < 1 || slot > 5) {
39 ✗ return -1;
40 }
41 // The file only exists once a slot fader has been moved, so its absence is
42 // the normal case -- don't let LoadJsonFromFile() log it as an error.
43
3/6
✓ Branch 0 taken 1854 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1854 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1854 times.
✗ Branch 5 not taken.
1854 std::string file = FPP_DIR_CONFIG("/pipewire-stream-slots.json");
44
1/2
✓ Branch 0 taken 1854 times.
✗ Branch 1 not taken.
1854 Json::Value root;
45 // Shape-checked, not trusted: a list root, a non-object "slots" or a
46 // volume saved as a string threw from here on every stream start.
47
8/10
✓ Branch 0 taken 1854 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 1851 times.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 1 times.
✓ Branch 8 taken 1852 times.
✓ Branch 9 taken 2 times.
1856 if (!FileExists(file) || !LoadJsonFromFile(file, root, JsonRoot::Object) ||
48
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 2 times.
2 !root["slots"].isObject()) {
49 1852 return -1;
50 }
51 2 std::string key = std::to_string(slot);
52
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 const Json::Value& vol = root["slots"][key];
53
3/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1 times.
2 if (!vol.isIntegral()) {
54 1 return -1;
55 }
56
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 int v = vol.asInt();
57
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 if (v < 0 || v > 100) {
58 ✗ return -1;
59 }
60 1 return v;
61 1854 }
62
63 1860 StreamSlotManager::StreamSlotManager() {
64
2/2
✓ Branch 0 taken 9300 times.
✓ Branch 1 taken 1860 times.
11160 for (int i = 0; i < MAX_SLOTS; i++) {
65
1/2
✓ Branch 0 taken 9300 times.
✗ Branch 1 not taken.
9300 m_slots[i].status = {};
66 9300 m_slots[i].status.status = MEDIAOUTPUTSTATUS_IDLE;
67 9300 m_slots[i].activeOutput = nullptr;
68 9300 m_slots[i].isBackground = false;
69 }
70 1860 }
71
72 662899 StreamSlotManager& StreamSlotManager::Instance() {
73
4/8
✓ Branch 0 taken 1860 times.
✓ Branch 1 taken 661039 times.
✓ Branch 2 taken 1860 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1860 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
662899 static StreamSlotManager instance;
74 662899 return instance;
75 }
76
77 2460 MediaOutputStatus* StreamSlotManager::GetStatus(int slot) {
78
2/4
✓ Branch 0 taken 2460 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2460 times.
2460 if (slot < 1 || slot > MAX_SLOTS) {
79 ✗ LogWarn(VB_MEDIAOUT, "StreamSlotManager::GetStatus: invalid slot %d\n", slot);
80 ✗ return &mediaOutputStatus; // fallback to global
81 }
82
2/2
✓ Branch 0 taken 783 times.
✓ Branch 1 taken 1677 times.
2460 if (slot == 1) {
83 // Slot 1 uses the global mediaOutputStatus for backward compatibility
84 783 return &mediaOutputStatus;
85 }
86 1677 return &m_slots[slot - 1].status;
87 }
88
89 1966 void StreamSlotManager::SetActiveOutput(int slot, GStreamerOutput* output) {
90
2/4
✓ Branch 0 taken 1966 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1966 times.
1966 if (slot < 1 || slot > MAX_SLOTS) return;
91
1/2
✓ Branch 0 taken 1966 times.
✗ Branch 1 not taken.
1966 std::lock_guard<std::recursive_mutex> lock(m_mutex);
92 1966 m_slots[slot - 1].activeOutput = output;
93 // Started again after its Start() detached it (DetachOutput()): it is no
94 // longer closing.
95
3/6
✓ Branch 0 taken 1966 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1966 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 1966 times.
1966 if (output && m_slots[slot - 1].closingOutput == output) {
96 ✗ m_slots[slot - 1].closingOutput = nullptr;
97 }
98 #ifdef HAS_GSTREAMER
99
6/8
✓ Branch 0 taken 1966 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1966 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1835 times.
✓ Branch 5 taken 131 times.
✓ Branch 6 taken 1835 times.
✓ Branch 7 taken 131 times.
1966 if (output && output->NeedsSlotPump()) {
100
1/2
✓ Branch 0 taken 1835 times.
✗ Branch 1 not taken.
1835 StartPumpTimer();
101 }
102 #endif
103 // Every start (including a resume) re-arms the settle window.
104
1/2
✓ Branch 0 taken 1966 times.
✗ Branch 1 not taken.
1966 m_slots[slot - 1].syncSettleMS = GetMonotonicTimeMS() + SYNC_SETTLE_MS;
105 #ifdef HAS_GSTREAMER
106
1/2
✓ Branch 0 taken 1966 times.
✗ Branch 1 not taken.
1966 if (output) {
107
1/2
✓ Branch 0 taken 1966 times.
✗ Branch 1 not taken.
1966 m_slots[slot - 1].mediaFilename = output->m_mediaFilename;
108
1/2
✓ Branch 0 taken 1966 times.
✗ Branch 1 not taken.
1966 LogDebug(VB_MEDIAOUT, "StreamSlotManager: slot %d active (%s)\n", slot,
109 output->m_mediaFilename.c_str());
110 // A volume set while the slot was idle is newer than the saved one
111 // (and is all there is when it came from a command, not the mixer).
112 1966 int saved = m_slots[slot - 1].pendingVolume;
113 1966 m_slots[slot - 1].pendingVolume = -1;
114
2/2
✓ Branch 0 taken 1854 times.
✓ Branch 1 taken 112 times.
1966 if (saved < 0) {
115
1/2
✓ Branch 0 taken 1854 times.
✗ Branch 1 not taken.
1854 saved = savedStreamSlotVolume(slot);
116 }
117
2/2
✓ Branch 0 taken 113 times.
✓ Branch 1 taken 1853 times.
1966 if (saved >= 0) {
118
1/2
✓ Branch 0 taken 113 times.
✗ Branch 1 not taken.
113 output->SetVolume(saved);
119
1/2
✓ Branch 0 taken 113 times.
✗ Branch 1 not taken.
113 LogDebug(VB_MEDIAOUT, "StreamSlotManager: restored slot %d volume to %d%%\n", slot, saved);
120 }
121 }
122 #endif
123 1966 }
124
125 5668 void StreamSlotManager::DetachOutput(int slot, GStreamerOutput* owner) {
126
3/6
✓ Branch 0 taken 5668 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5668 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 5668 times.
5668 if (slot < 1 || slot > MAX_SLOTS || !owner) return;
127
1/2
✓ Branch 0 taken 5668 times.
✗ Branch 1 not taken.
5668 std::lock_guard<std::recursive_mutex> lock(m_mutex);
128
2/2
✓ Branch 0 taken 1217 times.
✓ Branch 1 taken 4451 times.
5668 if (m_slots[slot - 1].activeOutput == owner) {
129 1217 m_slots[slot - 1].activeOutput = nullptr;
130 1217 m_slots[slot - 1].closingOutput = owner;
131 }
132 5668 }
133
134 3960 bool StreamSlotManager::RequestStop(int slot) {
135 #ifdef HAS_GSTREAMER
136
2/4
✓ Branch 0 taken 3960 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3960 times.
3960 if (slot < 1 || slot > MAX_SLOTS) return false;
137
1/2
✓ Branch 0 taken 3960 times.
✗ Branch 1 not taken.
3960 std::lock_guard<std::recursive_mutex> lock(m_mutex);
138
2/2
✓ Branch 0 taken 49 times.
✓ Branch 1 taken 3911 times.
3960 if (m_slots[slot - 1].activeOutput) {
139
1/2
✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
49 m_slots[slot - 1].activeOutput->RequestStop();
140 49 return true;
141 }
142 #endif
143 3911 return false;
144 3960 }
145
146 10333 GStreamerOutput* StreamSlotManager::GetActiveOutput(int slot) {
147
2/4
✓ Branch 0 taken 10333 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 10333 times.
10333 if (slot < 1 || slot > MAX_SLOTS) return nullptr;
148
1/2
✓ Branch 0 taken 10333 times.
✗ Branch 1 not taken.
10333 std::lock_guard<std::recursive_mutex> lock(m_mutex);
149 10333 return m_slots[slot - 1].activeOutput;
150 10333 }
151
152 1970 void StreamSlotManager::ClaimSlot(int slot, GStreamerOutput* output) {
153
2/4
✓ Branch 0 taken 1970 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1970 times.
1970 if (slot < 1 || slot > MAX_SLOTS) return;
154
1/2
✓ Branch 0 taken 1970 times.
✗ Branch 1 not taken.
1970 std::lock_guard<std::recursive_mutex> lock(m_mutex);
155 1970 m_slots[slot - 1].claimedBy = output;
156 1970 }
157
158 3698 void StreamSlotManager::ClearSlot(int slot, GStreamerOutput* owner) {
159
2/4
✓ Branch 0 taken 3698 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3698 times.
6268 if (slot < 1 || slot > MAX_SLOTS) return;
160
1/2
✓ Branch 0 taken 3698 times.
✗ Branch 1 not taken.
3698 std::lock_guard<std::recursive_mutex> lock(m_mutex);
161
3/4
✓ Branch 0 taken 3698 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1217 times.
✓ Branch 3 taken 2481 times.
3698 bool closing = owner && m_slots[slot - 1].closingOutput == owner;
162
2/2
✓ Branch 0 taken 1217 times.
✓ Branch 1 taken 2481 times.
3698 if (closing) {
163 1217 m_slots[slot - 1].closingOutput = nullptr;
164 }
165 3698 GStreamerOutput* claimedBy = m_slots[slot - 1].claimedBy;
166
5/6
✓ Branch 0 taken 3698 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2685 times.
✓ Branch 3 taken 1013 times.
✓ Branch 4 taken 1553 times.
✓ Branch 5 taken 1132 times.
3698 if (owner && claimedBy && claimedBy != owner) {
167 // A newer stream has started on this slot (ClaimSlot) and may already
168 // have written its status -- its duration, say -- though it has not
169 // registered yet: leave the slot to it. Only our own registration,
170 // if it is somehow still there, goes.
171
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1553 times.
1553 if (m_slots[slot - 1].activeOutput == owner) {
172 ✗ m_slots[slot - 1].activeOutput = nullptr;
173 }
174
1/2
✓ Branch 0 taken 1553 times.
✗ Branch 1 not taken.
1553 LogDebug(VB_MEDIAOUT, "StreamSlotManager: slot %d clear skipped, claimed by a newer stream\n", slot);
175 1553 return;
176 }
177
3/4
✓ Branch 0 taken 2145 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1132 times.
✓ Branch 3 taken 1013 times.
2145 if (!owner || claimedBy == owner) {
178 1132 m_slots[slot - 1].claimedBy = nullptr;
179 }
180
4/6
✓ Branch 0 taken 2145 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2145 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1017 times.
✓ Branch 5 taken 1128 times.
4290 if (owner && m_slots[slot - 1].activeOutput != owner &&
181
3/4
✓ Branch 0 taken 1128 times.
✓ Branch 1 taken 1017 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1128 times.
2145 !(closing && !m_slots[slot - 1].activeOutput)) {
182 // Another (newer) stream has already claimed this slot -- our
183 // teardown is stale, don't blank out its registration.
184
1/2
✓ Branch 0 taken 1017 times.
✗ Branch 1 not taken.
1017 LogDebug(VB_MEDIAOUT, "StreamSlotManager: slot %d clear skipped, already reclaimed by a newer stream\n", slot);
185 1017 return;
186 }
187 1128 m_slots[slot - 1].activeOutput = nullptr;
188 1128 m_slots[slot - 1].mediaFilename.clear();
189 1128 m_slots[slot - 1].isBackground = false;
190 1128 m_slots[slot - 1].syncToMaster = false;
191 1128 m_slots[slot - 1].lastSyncMS = 0;
192 1128 m_slots[slot - 1].syncSettleMS = 0;
193 // Zero the timing fields, not just the state: GetAllSlotsStatus() reports
194 // them for any slot holding an active output, so leaving the last track's
195 // numbers behind makes a freshly started stream read as though it were
196 // already minutes in until its first Process() lands. Slot 1 aliases the
197 // global mediaOutputStatus, which is not ours to wipe.
198
1/2
✓ Branch 0 taken 1128 times.
✗ Branch 1 not taken.
1128 std::lock_guard<std::mutex> fieldsLock(MediaOutputStatus::FieldsLock());
199
2/2
✓ Branch 0 taken 573 times.
✓ Branch 1 taken 555 times.
1128 if (slot > 1) {
200
1/2
✓ Branch 0 taken 573 times.
✗ Branch 1 not taken.
573 m_slots[slot - 1].status = {};
201 }
202 1128 m_slots[slot - 1].status.status = MEDIAOUTPUTSTATUS_IDLE;
203 // #2713: release the KMS connector gate (mediaOutputStatus.output) when the
204 // stream tears down. Otherwise it stays set to the video's connector (e.g.
205 // "DSI-1") after playback ends and the KMSFrameBuffer overlay path stays
206 // gated off, so images never scan out until fppd is restarted. Use
207 // GetStatus(slot) so slot 1's global mediaOutputStatus is the object cleared.
208
2/4
✓ Branch 0 taken 1128 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1128 times.
✗ Branch 3 not taken.
1128 GetStatus(slot)->output = "";
209
1/2
✓ Branch 0 taken 1128 times.
✗ Branch 1 not taken.
1128 LogDebug(VB_MEDIAOUT, "StreamSlotManager: slot %d cleared\n", slot);
210
2/2
✓ Branch 0 taken 1128 times.
✓ Branch 1 taken 2570 times.
3698 }
211
212 624082 std::string StreamSlotManager::GetNodeName(int slot) {
213
1/2
✓ Branch 0 taken 624082 times.
✗ Branch 1 not taken.
624082 return "fppd_stream_" + std::to_string(slot);
214 }
215
216 623826 std::string StreamSlotManager::GetNodeDescription(int slot) {
217
1/2
✓ Branch 0 taken 623826 times.
✗ Branch 1 not taken.
623826 return "FPP Media Stream " + std::to_string(slot);
218 }
219
220 124 std::string StreamSlotManager::GetVideoNodeName(int slot) {
221
1/2
✓ Branch 0 taken 124 times.
✗ Branch 1 not taken.
124 return "fppd_video_stream_" + std::to_string(slot);
222 }
223
224 44 std::string StreamSlotManager::GetVideoNodeDescription(int slot) {
225
1/2
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
44 return "FPP Video Stream " + std::to_string(slot);
226 }
227
228 70906 bool StreamSlotManager::SetSlotVolume(int slot, int volume, bool* deferred) {
229 #ifdef HAS_GSTREAMER
230
2/4
✓ Branch 0 taken 70906 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 70906 times.
70906 if (slot < 1 || slot > MAX_SLOTS) return false;
231
1/2
✓ Branch 0 taken 70906 times.
✗ Branch 1 not taken.
70906 std::lock_guard<std::recursive_mutex> lock(m_mutex);
232 70906 GStreamerOutput* output = m_slots[slot - 1].activeOutput;
233 // An output being stopped stays registered until its Close() but no
234 // longer takes a volume, and one playing media with no sound has nothing
235 // to take it with: either way the slot's next stream starts at it.
236
5/6
✓ Branch 0 taken 63042 times.
✓ Branch 1 taken 7864 times.
✓ Branch 2 taken 63042 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 63013 times.
✓ Branch 5 taken 29 times.
70906 bool applied = output && output->TrySetVolume(volume);
237
2/2
✓ Branch 0 taken 7893 times.
✓ Branch 1 taken 63013 times.
70906 if (!applied) {
238 // Nothing to apply it to yet: the slot's next stream starts at it,
239 // as it does at the level the mixer saves.
240 7893 m_slots[slot - 1].pendingVolume = volume;
241 }
242
1/2
✓ Branch 0 taken 70906 times.
✗ Branch 1 not taken.
70906 if (deferred) {
243 70906 *deferred = !applied;
244 }
245 70906 return true;
246 #else
247 return false;
248 #endif
249 70906 }
250
251 1 bool StreamSlotManager::SeekSlot(int slot, float seconds) {
252 #ifdef HAS_GSTREAMER
253
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 if (slot < 1 || slot > MAX_SLOTS) return false;
254
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::lock_guard<std::recursive_mutex> lock(m_mutex);
255 1 GStreamerOutput* output = m_slots[slot - 1].activeOutput;
256
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (output) {
257
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 return output->SeekTo(seconds);
258 }
259 #endif
260 ✗ return false;
261 1 }
262
263 124331 Json::Value StreamSlotManager::GetAllSlotsStatus() {
264
1/2
✓ Branch 0 taken 124331 times.
✗ Branch 1 not taken.
124331 Json::Value result(Json::arrayValue);
265
1/2
✓ Branch 0 taken 124331 times.
✗ Branch 1 not taken.
124331 std::lock_guard<std::recursive_mutex> lock(m_mutex);
266
2/2
✓ Branch 0 taken 621655 times.
✓ Branch 1 taken 124331 times.
745986 for (int i = 0; i < MAX_SLOTS; i++) {
267
1/2
✓ Branch 0 taken 621655 times.
✗ Branch 1 not taken.
621655 Json::Value slotJson;
268
2/4
✓ Branch 0 taken 621655 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 621655 times.
✗ Branch 3 not taken.
621655 slotJson["slot"] = i + 1;
269
3/6
✓ Branch 0 taken 621655 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 621655 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 621655 times.
✗ Branch 5 not taken.
621655 slotJson["nodeName"] = GetNodeName(i + 1);
270
3/6
✓ Branch 0 taken 621655 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 621655 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 621655 times.
✗ Branch 5 not taken.
621655 slotJson["nodeDescription"] = GetNodeDescription(i + 1);
271
272 // The output's own status, which is not always the slot's: Play Media
273 // on slot 1 keeps a private one so it doesn't overwrite the playlist's
274 // (the global mediaOutputStatus), and reading the global reported 0
275 // for its whole track. Close() takes an output out of the table
276 // before freeing it, so under m_mutex the pointer is live.
277 621655 GStreamerOutput* out = m_slots[i].activeOutput;
278
3/4
✓ Branch 0 taken 28892 times.
✓ Branch 1 taken 592763 times.
✓ Branch 2 taken 28892 times.
✗ Branch 3 not taken.
1214418 MediaOutputStatus* st = (out && out->m_mediaOutputStatus) ? out->m_mediaOutputStatus
279
2/2
✓ Branch 0 taken 496925 times.
✓ Branch 1 taken 95838 times.
1089688 : (i == 0) ? &mediaOutputStatus
280 496925 : &m_slots[i].status;
281
2/2
✓ Branch 0 taken 28892 times.
✓ Branch 1 taken 592763 times.
621655 if (out) {
282
1/2
✓ Branch 0 taken 28892 times.
✗ Branch 1 not taken.
28892 std::lock_guard<std::mutex> fieldsLock(MediaOutputStatus::FieldsLock());
283
2/4
✓ Branch 0 taken 28892 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28892 times.
✗ Branch 3 not taken.
28892 slotJson["status"] = "playing";
284
2/4
✓ Branch 0 taken 28892 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28892 times.
✗ Branch 3 not taken.
28892 slotJson["mediaFilename"] = m_slots[i].mediaFilename;
285
2/4
✓ Branch 0 taken 28892 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28892 times.
✗ Branch 3 not taken.
28892 slotJson["secondsElapsed"] = st->secondsElapsed;
286
2/4
✓ Branch 0 taken 28892 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28892 times.
✗ Branch 3 not taken.
28892 slotJson["subSecondsElapsed"] = st->subSecondsElapsed;
287
2/4
✓ Branch 0 taken 28892 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28892 times.
✗ Branch 3 not taken.
28892 slotJson["secondsRemaining"] = st->secondsRemaining;
288
2/4
✓ Branch 0 taken 28892 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28892 times.
✗ Branch 3 not taken.
28892 slotJson["subSecondsRemaining"] = st->subSecondsRemaining;
289
2/4
✓ Branch 0 taken 28892 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28892 times.
✗ Branch 3 not taken.
28892 slotJson["minutesTotal"] = st->minutesTotal;
290
2/4
✓ Branch 0 taken 28892 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28892 times.
✗ Branch 3 not taken.
28892 slotJson["secondsTotal"] = st->secondsTotal;
291
2/4
✓ Branch 0 taken 28892 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28892 times.
✗ Branch 3 not taken.
28892 slotJson["isBackground"] = m_slots[i].isBackground;
292 28892 } else {
293
2/4
✓ Branch 0 taken 592763 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 592763 times.
✗ Branch 3 not taken.
592763 slotJson["status"] = "idle";
294
2/4
✓ Branch 0 taken 592763 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 592763 times.
✗ Branch 3 not taken.
592763 slotJson["mediaFilename"] = "";
295
2/4
✓ Branch 0 taken 592763 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 592763 times.
✗ Branch 3 not taken.
592763 slotJson["isBackground"] = false;
296 }
297
1/2
✓ Branch 0 taken 621655 times.
✗ Branch 1 not taken.
621655 result.append(slotJson);
298 621655 }
299 124331 return result;
300 124331 }
301
302 ✗ int StreamSlotManager::ActiveSlotCount() {
303 ✗ std::lock_guard<std::recursive_mutex> lock(m_mutex);
304 ✗ int count = 0;
305 ✗ for (int i = 0; i < MAX_SLOTS; i++) {
306 ✗ if (m_slots[i].activeOutput) count++;
307 }
308 ✗ return count;
309 ✗ }
310
311 873 void StreamSlotManager::StopBackgroundSlots() {
312 #ifdef HAS_GSTREAMER
313 // These outputs belong to someone else -- Play Media's registry, or a
314 // playlist entry playing on a background slot -- which stops, closes and
315 // frees them on its own thread. Stopping and closing them here, from
316 // whatever thread is stopping the playlist, raced the owner: a second
317 // Close() alongside the deferred delete's, and Stop() under this lock
318 // while the main loop waited on it. Each output is asked to stop, and
319 // its own Close() clears the slot.
320
1/2
✓ Branch 0 taken 873 times.
✗ Branch 1 not taken.
873 std::lock_guard<std::recursive_mutex> lock(m_mutex);
321
2/2
✓ Branch 0 taken 3492 times.
✓ Branch 1 taken 873 times.
4365 for (int i = 1; i < MAX_SLOTS; i++) { // skip slot 1 (managed by playlist)
322
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 3487 times.
3492 if (m_slots[i].activeOutput) {
323
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 LogInfo(VB_MEDIAOUT, "StreamSlotManager: stopping slot %d\n", i + 1);
324
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 m_slots[i].activeOutput->RequestStop();
325 }
326 }
327 #endif
328 873 }
329
330 3 void StreamSlotManager::SetSyncToMaster(int slot, bool sync) {
331
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
3 if (slot < 1 || slot > MAX_SLOTS)
332 ✗ return;
333
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 std::lock_guard<std::recursive_mutex> lock(m_mutex);
334 3 m_slots[slot - 1].syncToMaster = sync;
335
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (sync) {
336
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 LogInfo(VB_MEDIAOUT, "StreamSlotManager: slot %d will follow the primary media clock\n", slot);
337 }
338 3 }
339
340 139345 void StreamSlotManager::SyncSlotsToMaster(float masterPos) {
341 #ifdef HAS_GSTREAMER
342
1/2
✓ Branch 0 taken 139345 times.
✗ Branch 1 not taken.
139345 std::lock_guard<std::recursive_mutex> lock(m_mutex);
343
1/2
✓ Branch 0 taken 139345 times.
✗ Branch 1 not taken.
139345 long long now = GetMonotonicTimeMS();
344 // Slot 1 IS the master (it is the playlist's own media), so start at 2.
345
2/2
✓ Branch 0 taken 557380 times.
✓ Branch 1 taken 139345 times.
696725 for (int i = 1; i < MAX_SLOTS; i++) {
346
6/6
✓ Branch 0 taken 2621 times.
✓ Branch 1 taken 554759 times.
✓ Branch 2 taken 1494 times.
✓ Branch 3 taken 1127 times.
✓ Branch 4 taken 556253 times.
✓ Branch 5 taken 1127 times.
557380 if (!m_slots[i].activeOutput || !m_slots[i].syncToMaster)
347 556253 continue;
348
349 // Process() first: nothing else drives a non-background slot's status,
350 // so without this its mediaSeconds stays 0 and AdjustSpeed bails out
351 // ("can't adjust speed if not playing yet") having never seen the slot
352 // actually move.
353
1/2
✓ Branch 0 taken 1127 times.
✗ Branch 1 not taken.
1127 m_slots[i].activeOutput->Process();
354
355
2/2
✓ Branch 0 taken 1103 times.
✓ Branch 1 taken 24 times.
1127 if ((now - m_slots[i].lastSyncMS) < SYNC_INTERVAL_MS)
356 1103 continue;
357 24 m_slots[i].lastSyncMS = now;
358
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 8 times.
24 if (now < m_slots[i].syncSettleMS)
359 16 continue;
360
361 // Slots started together on this box share the pipeline clock and stay
362 // sample-locked by themselves -- measured 0.000s offset across a whole
363 // track with no correction at all. So this is a repair path, not a
364 // control loop: it only fires when something has genuinely knocked a
365 // slot off the show timeline (the primary was seeked or restarted, or
366 // MultiSync is rate-adjusting the primary on a remote).
367 //
368 // Correcting by rate is not an option. These pipelines refuse
369 // GST_SEEK_FLAG_INSTANT_RATE_CHANGE, so ApplyRate() falls back to a
370 // flush seek, which moves the position enough to feed the next
371 // correction -- measured 0.9-2.5s oscillating, never converging, on
372 // streams that were exactly locked before the "sync" was applied.
373 float slotPos;
374 {
375
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 std::lock_guard<std::mutex> fieldsLock(MediaOutputStatus::FieldsLock());
376 8 slotPos = m_slots[i].status.mediaSeconds;
377 8 }
378 8 float drift = slotPos - masterPos;
379
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 if (drift > -MAX_DRIFT_SEC && drift < MAX_DRIFT_SEC)
380 8 continue;
381 ✗ LogInfo(VB_MEDIAOUT, "StreamSlotManager: slot %d drifted %0.3fs from the primary, seeking to %0.3f\n",
382 i + 1, drift, masterPos);
383 ✗ m_slots[i].activeOutput->SeekTo(masterPos);
384 }
385 #endif
386 139345 }
387
388 298513 void StreamSlotManager::ProcessBackgroundSlots() {
389 298513 PumpSlotOutputs();
390 298513 }
391
392 // Calls Process() for every output whose owner doesn't (see
393 // GStreamerOutput::NeedsSlotPump()) -- Play Media's, on any slot. Without it
394 // their status never advances, so the UI and status API report a frozen
395 // position, and the preroll and stall watchdogs never run. This used to
396 // happen only from a playing playlist (ProcessBackgroundSlots()), and only for
397 // slots 2-5: Play Media with the player idle, or on slot 1, got nothing. The
398 // pump timer covers those; the playlist's tick still pumps while it runs, and
399 // the two share one cadence.
400 //
401 // Teardown is deliberately not done here. Media started by the "Play Media"
402 // command is owned by runningCommandMedia and freed off the main loop when
403 // Stopped() fires, so deleting it here would be a double free.
404 // Close()/ClearSlot() on that path is what clears the slot.
405 313868 void StreamSlotManager::PumpSlotOutputs() {
406 #ifdef HAS_GSTREAMER
407
1/2
✓ Branch 0 taken 313868 times.
✗ Branch 1 not taken.
313868 std::lock_guard<std::recursive_mutex> lock(m_mutex);
408
1/2
✓ Branch 0 taken 313868 times.
✗ Branch 1 not taken.
313868 long long now = GetMonotonicTimeMS();
409
2/2
✓ Branch 0 taken 200091 times.
✓ Branch 1 taken 113777 times.
313868 if (now - m_lastPumpMS < PUMP_INTERVAL_MS / 2) {
410 200091 return;
411 }
412 113777 m_lastPumpMS = now;
413
2/2
✓ Branch 0 taken 568885 times.
✓ Branch 1 taken 113777 times.
682662 for (int i = 0; i < MAX_SLOTS; i++) {
414
7/8
✓ Branch 0 taken 59657 times.
✓ Branch 1 taken 509228 times.
✓ Branch 2 taken 59657 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 11112 times.
✓ Branch 5 taken 48545 times.
✓ Branch 6 taken 11112 times.
✓ Branch 7 taken 557773 times.
568885 if (m_slots[i].activeOutput && m_slots[i].activeOutput->NeedsSlotPump()) {
415
1/2
✓ Branch 0 taken 11112 times.
✗ Branch 1 not taken.
11112 m_slots[i].activeOutput->Process();
416 }
417 }
418 #endif
419
2/2
✓ Branch 0 taken 113777 times.
✓ Branch 1 taken 200091 times.
313868 }
420
421 1835 void StreamSlotManager::StartPumpTimer() {
422
2/2
✓ Branch 0 taken 1721 times.
✓ Branch 1 taken 114 times.
1835 if (m_pumpTimerStarted.exchange(true)) {
423 1721 return;
424 }
425
2/4
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 114 times.
✗ Branch 3 not taken.
342 Timers::INSTANCE.addPeriodicTimer("StreamSlotPump", PUMP_INTERVAL_MS, [this]() {
426 15355 PumpSlotOutputs();
427 15355 });
428 }
429