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