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