| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | /* | ||
| 3 | * This file is part of the Falcon Player (FPP) and is Copyright (C) | ||
| 4 | * 2013-2022 by the Falcon Player Developers. | ||
| 5 | * | ||
| 6 | * The Falcon Player (FPP) is free software, and is covered under | ||
| 7 | * multiple Open Source licenses. Please see the included 'LICENSES' | ||
| 8 | * file for descriptions of what files are covered by each license. | ||
| 9 | * | ||
| 10 | * This source file is covered under the LGPL v2.1 as described in the | ||
| 11 | * included LICENSE.LGPL file. | ||
| 12 | */ | ||
| 13 | |||
| 14 | #include <atomic> | ||
| 15 | #include "AtomicSharedPtr.h" | ||
| 16 | #include "fpp-json.h" | ||
| 17 | #include <list> | ||
| 18 | #include <memory> | ||
| 19 | #include <mutex> | ||
| 20 | #include <string> | ||
| 21 | #include <vector> | ||
| 22 | |||
| 23 | #include "PlaylistEntryBase.h" | ||
| 24 | |||
| 25 | enum PlaylistStatus { | ||
| 26 | FPP_STATUS_IDLE = 0, | ||
| 27 | FPP_STATUS_PLAYLIST_PLAYING, | ||
| 28 | FPP_STATUS_STOPPING_GRACEFULLY, | ||
| 29 | FPP_STATUS_STOPPING_GRACEFULLY_AFTER_LOOP, | ||
| 30 | FPP_STATUS_STOPPING_NOW, | ||
| 31 | FPP_STATUS_PLAYLIST_PAUSED | ||
| 32 | }; | ||
| 33 | |||
| 34 | // True while a PLAYLIST_STOPPED preset runs for an inserted playlist that has | ||
| 35 | // handed the player back to the one it interrupted (see Playlist.cpp). | ||
| 36 | bool InHandedBackStoppedPreset(); | ||
| 37 | |||
| 38 | // Owned through Player::CreatePlaylist(); weak_from_this() hands an inserted | ||
| 39 | // child an owning reference to the level it interrupted. | ||
| 40 | class Playlist : public std::enable_shared_from_this<Playlist> { | ||
| 41 | public: | ||
| 42 | Playlist(std::shared_ptr<Playlist> parent = nullptr); | ||
| 43 | ~Playlist(); | ||
| 44 | |||
| 45 | PlaylistStatus getPlaylistStatus(); | ||
| 46 | |||
| 47 | // New methods | ||
| 48 | Json::Value LoadJSON(const std::string& filename); | ||
| 49 | int Load(Json::Value& config); | ||
| 50 | int Load(const std::string& filename); | ||
| 51 | PlaylistEntryBase* LoadPlaylistEntry(Json::Value entry); | ||
| 52 | int LoadJSONIntoPlaylist(std::vector<PlaylistEntryBase*>& playlistPart, const Json::Value& entries, int startPos, int& maxEntries); | ||
| 53 | |||
| 54 | int Start(void); | ||
| 55 | int StopNow(int forceStop = 0); | ||
| 56 | int StopGracefully(int forceStop = 0, int afterCurrentLoop = 0); | ||
| 57 | void SetIdle(bool exit = true); | ||
| 58 | |||
| 59 | void Pause(); | ||
| 60 | void Resume(); | ||
| 61 | |||
| 62 | int IsPlaying(void); | ||
| 63 | |||
| 64 | int Process(void); | ||
| 65 | // Free retired playlists and entries. Main thread only; see Playlist.cpp. | ||
| 66 | static void ReleaseRetired(void); | ||
| 67 | void ProcessMedia(void); | ||
| 68 | int Cleanup(void); | ||
| 69 | // Cleanup(), but only if the playlist is idle when checked under | ||
| 70 | // m_playlistMutex. For callers outside a transition that decided to | ||
| 71 | // clean up from an unlocked status read: another thread may have started | ||
| 72 | // a new playlist in between, and Cleanup() would wipe it mid-play. | ||
| 73 | int CleanupIfIdle(void); | ||
| 74 | |||
| 75 | // Refresh the lock-free snapshot the crash handler reads (see | ||
| 76 | // PlaylistDumpCrashState). Called at the points where the section or | ||
| 77 | // position can change; cheap enough to not need gating. | ||
| 78 | void UpdateCrashSnapshot(void); | ||
| 79 | |||
| 80 | int Play(const std::string& filename, const int position = -1, const int repeat = -1, const int scheduleEntry = -1, const int endPosition = -1); | ||
| 81 | |||
| 82 | void InsertPlaylistAsNext(const std::string& filename, const int startPosition = -1, const int endPos = -1); | ||
| 83 | void InsertPlaylistImmediate(const std::string& filename, const int startPosition = -1, const int endPos = -1); | ||
| 84 | |||
| 85 | void SetPosition(int position); | ||
| 86 | void SetRepeat(int repeat); | ||
| 87 | |||
| 88 | void RandomizeMainPlaylist(); | ||
| 89 | int ShuffleMainPlaylistFrom(int position); | ||
| 90 | |||
| 91 | void Dump(void); | ||
| 92 | |||
| 93 | // Helper method for global pause between sequences | ||
| 94 | void StartPlayingWithGlobalPause(PlaylistEntryBase* entry); | ||
| 95 | |||
| 96 | void NextItem(void); | ||
| 97 | void RestartItem(void); | ||
| 98 | void PrevItem(void); | ||
| 99 | |||
| 100 | void GetCurrentStatus(Json::Value& result); | ||
| 101 | Json::Value GetCurrentEntry(void); | ||
| 102 | Json::Value GetInfo(void); | ||
| 103 | void GetInfo(Json::Value& v); | ||
| 104 | std::string GetPlaylistName(void); | ||
| 105 | // These getters are called from the scheduler, status, MQTT and API | ||
| 106 | // threads without m_playlistMutex, so the members behind them are atomic. | ||
| 107 | 580594 | int GetRepeat(void) { return m_repeat.load(std::memory_order_relaxed); } | |
| 108 | int GetPosition(void); | ||
| 109 | int GetResumePosition(void); | ||
| 110 | int GetSize(void); | ||
| 111 | 12 | int GetLoopNumber(void) { return (m_loop.load(std::memory_order_relaxed) + 1); } | |
| 112 | std::string GetConfigStr(void); | ||
| 113 | Json::Value GetConfig(void); | ||
| 114 | 50726 | uint64_t GetFileTime(void) { return (Json::UInt64)m_fileTime.load(std::memory_order_relaxed); } | |
| 115 | int GetForceStop(void) { return m_forceStop.load(std::memory_order_relaxed); } | ||
| 116 |
2/2✓ Branch 0 taken 34864 times.
✓ Branch 1 taken 480875 times.
|
515739 | int WasScheduled(void) { return (GetScheduleEntry() != -1) ? 1 : 0; } |
| 117 | 1036446 | int GetScheduleEntry(void) { return m_scheduleEntry.load(std::memory_order_relaxed); } | |
| 118 | |||
| 119 | // these Positions are 0 based and includes the leadIn entries | ||
| 120 | uint64_t GetCurrentPosInMS(); | ||
| 121 | uint64_t GetCurrentPosInMS(int& pos, uint64_t& posms, bool itemDefinedOnly = false); | ||
| 122 | uint64_t GetPosStartInMS(int pos); | ||
| 123 | int FindPosForMS(uint64_t& ms, bool itemDefinedOnly = false); // ms will be updated with how far into Pos it would be | ||
| 124 | void GetFilenamesForPos(int pos, std::string& seq, std::string& med); | ||
| 125 | |||
| 126 | int MQTTHandler(std::string topic, std::string msg); | ||
| 127 | |||
| 128 | int FileHasBeenModified(void); | ||
| 129 | std::string ReplaceMatches(std::string in); | ||
| 130 | Json::Value GetMqttStatusJSON(); // Returns Status as JSON | ||
| 131 | |||
| 132 | // Single source of truth for the human-facing status string. Derived from | ||
| 133 | // m_status so the string and the enum can never disagree: a paused playlist | ||
| 134 | // used to report "playing" because Pause() moved only the enum while the | ||
| 135 | // old m_currentState string mirror had no paused value. | ||
| 136 | static const char* PlaylistStatusToString(PlaylistStatus status); | ||
| 137 | |||
| 138 | // Number of playlists on the chain: 1 plus each one an insertion | ||
| 139 | // interrupted (the m_parent chain). | ||
| 140 | int GetLevelCount(); | ||
| 141 | |||
| 142 | private: | ||
| 143 | void GetParentPlaylistNames(std::list<std::string>& names); | ||
| 144 | int ReloadPlaylist(void); | ||
| 145 | void ReloadIfNeeded(void); | ||
| 146 | void SwitchToMainPlaylist(void); | ||
| 147 | void SwitchToLeadOut(void); | ||
| 148 | |||
| 149 | bool WillStopAfterCurrent(); | ||
| 150 | |||
| 151 | // LoadJSONIntoPlaylist(), recording where each of the section's rows (as | ||
| 152 | // the playlist file and editor number them) starts in the loaded entries. | ||
| 153 | int LoadSection(std::vector<PlaylistEntryBase*>& part, const Json::Value& entries, int startPos, | ||
| 154 | int& maxEntries, std::vector<int>* rowStart); | ||
| 155 | const std::vector<int>& CurrentSectionRows() const; | ||
| 156 | static int RowToSectionPosition(const std::vector<int>& rows, int row, int sectionSize); | ||
| 157 | static int SectionPositionToRow(const std::vector<int>& rows, int pos); | ||
| 158 | int FileRowToPosition(int row); | ||
| 159 | |||
| 160 | // A command reaches a playlist through a snapshot of the current one taken | ||
| 161 | // before it gets this playlist's lock. If the player switched playlists in | ||
| 162 | // between -- an inserted playlist started or finished -- the command is for | ||
| 163 | // whichever playlist is current now: a stop landing on the parent that was | ||
| 164 | // just paused, or a Next Item starting the parent's next entry alongside | ||
| 165 | // the inserted one. Returns that playlist, or null if it is this one. | ||
| 166 | // The current playlist only changes while the outgoing playlist's lock is | ||
| 167 | // held, so called under m_playlistMutex the answer can't go stale. Every | ||
| 168 | // public command method checks it first and forwards. | ||
| 169 | std::shared_ptr<Playlist> CurrentIfNotThis(void); | ||
| 170 | int StopGracefullyImpl(int forceStop, int afterCurrentLoop); | ||
| 171 | Playlist* SwitchToInsertedPlaylist(bool isStopping = false); | ||
| 172 | |||
| 173 | // The entry at the current section position, or nullptr if there is none. | ||
| 174 | // "None" is a normal state, not an error: m_sectionPosition is allowed to | ||
| 175 | // sit at exactly m_currentSection->size() while the playlist is between | ||
| 176 | // items, and m_currentSection is null until a playlist is loaded. Every | ||
| 177 | // access to the current entry goes through here so that single guard has | ||
| 178 | // one home. Caller must hold m_playlistMutex. | ||
| 179 | PlaylistEntryBase* CurrentEntry(); | ||
| 180 | |||
| 181 | // Written under m_playlistMutex, but read by unlocked getters from other | ||
| 182 | // threads (status paths, the scheduler) -- atomic so those reads are | ||
| 183 | // well-defined; volatile never made them so. | ||
| 184 | std::atomic<PlaylistStatus> m_status; | ||
| 185 | |||
| 186 | // The level this one interrupted, owned: a thread holding a snapshot of a | ||
| 187 | // level can walk to every parent it still links to (status breadcrumbs, | ||
| 188 | // SetIdle's hand-back, a graceful stop) while the main loop releases | ||
| 189 | // levels, and each parent stays alive until the last level linking to it | ||
| 190 | // lets go. A level is cut loose (nulled) when it is retired, so a retired | ||
| 191 | // level doesn't keep its parent alive either. | ||
| 192 | AtomicSharedPtr<Playlist> m_parent; | ||
| 193 | std::string m_filename; | ||
| 194 | std::string m_name; | ||
| 195 | std::string m_desc; | ||
| 196 | // True when this playlist was synthesized on the fly from a bare media | ||
| 197 | // file -- Playlist::Load(filename) with a .fseq/.mp3/... name builds a | ||
| 198 | // one-entry playlist NAMED after the file. Set under m_playlistMutex in | ||
| 199 | // Load(filename) and Cleanup(), alongside m_name; read under the same | ||
| 200 | // lock in GetCurrentStatus(), which publishes it as | ||
| 201 | // current_playlist.generated so the UI can tell "user pressed Play on a | ||
| 202 | // file" apart from "a saved playlist is playing". Never true for a | ||
| 203 | // playlist loaded from a .json file. | ||
| 204 | bool m_generatedOnTheFly = false; | ||
| 205 | // Atomic, read lock-free by the inline getters above. Each is the same | ||
| 206 | // size and in the same place as the plain field it replaces, so a plugin | ||
| 207 | // built against the old header still reads the right value. | ||
| 208 | std::atomic<int> m_repeat; | ||
| 209 | std::atomic<int> m_loop; | ||
| 210 | int m_loopCount; | ||
| 211 | int m_random; | ||
| 212 | int m_blankAtEnd; | ||
| 213 | long long m_startTime; | ||
| 214 | int m_subPlaylistDepth; | ||
| 215 | std::atomic<int> m_scheduleEntry; | ||
| 216 | std::atomic<int> m_forceStop; | ||
| 217 | int m_stopAtPos; | ||
| 218 | int m_loadStartPos; | ||
| 219 | int m_loadEndPos; | ||
| 220 | |||
| 221 | std::atomic<time_t> m_fileTime; | ||
| 222 | Json::Value m_config; | ||
| 223 | time_t m_configTime; | ||
| 224 | Json::Value m_playlistInfo; | ||
| 225 | |||
| 226 | std::string m_currentSectionStr; | ||
| 227 | int m_sectionPosition; | ||
| 228 | int m_startPosition; | ||
| 229 | |||
| 230 | // Global pause between sequences feature | ||
| 231 | int m_globalPauseBetweenSequencesMS; | ||
| 232 | bool m_isInGlobalPause; | ||
| 233 | bool m_shouldStartGlobalPause; | ||
| 234 | long long m_globalPauseStartTime; | ||
| 235 | |||
| 236 | // A repeating main playlist whose every entry finishes at once (command | ||
| 237 | // entries only) used to restart on every main loop pass, about a | ||
| 238 | // thousand times a second. A pass shorter than MIN_INSTANT_PASS_MS holds | ||
| 239 | // the next one until m_loopHoldUntilMS (see the repeat in Process()). | ||
| 240 | // A command entry that navigates its own playlist (Next Playlist Item | ||
| 241 | // alone in a repeating list) restarts through Start() instead, which this | ||
| 242 | // does not cover; the notification cascade limit bounds that one. | ||
| 243 | long long m_loopStartMS = 0; | ||
| 244 | long long m_loopHoldUntilMS = 0; | ||
| 245 | bool m_warnedInstantLoop = false; | ||
| 246 | |||
| 247 | std::string m_insertedPlaylist; | ||
| 248 | int m_insertedPlaylistPosition; | ||
| 249 | int m_insertedPlaylistEndPosition; | ||
| 250 | |||
| 251 | // NOTE: the old startNewPlaylist* members (deferred re-entrant start | ||
| 252 | // requests) moved to file-scope state in Playlist.cpp — the request | ||
| 253 | // belongs to the player, not to one Playlist instance, since the active | ||
| 254 | // instance can be swapped/queued for cleanup between defer and replay. | ||
| 255 | |||
| 256 | // The body of Play(); public Play() defers re-entrant calls, PlayImpl | ||
| 257 | // executes. Also called directly by SwitchToInsertedPlaylist(), which | ||
| 258 | // must start the inserted child inline within the parent's transition. | ||
| 259 | int PlayImpl(const std::string& filename, const int position, const int repeat, const int scheduleEntry, const int endPosition); | ||
| 260 | // The body of StopNow(); same public-defers / Impl-executes split. | ||
| 261 | int StopNowImpl(int forceStop = 0); | ||
| 262 | |||
| 263 | std::recursive_mutex m_playlistMutex; | ||
| 264 | |||
| 265 | std::vector<PlaylistEntryBase*> m_leadIn; | ||
| 266 | std::vector<PlaylistEntryBase*> m_mainPlaylist; | ||
| 267 | std::vector<PlaylistEntryBase*> m_leadOut; | ||
| 268 | std::vector<PlaylistEntryBase*>* m_currentSection; | ||
| 269 | |||
| 270 | // Members below are appended rather than grouped with their relatives so | ||
| 271 | // that the offsets of everything above stay put for already-built plugins. | ||
| 272 | |||
| 273 | // The status Pause() replaced, restored by Resume(). A playlist paused | ||
| 274 | // while stopping gracefully must keep stopping once resumed. | ||
| 275 | PlaylistStatus m_statusBeforePause = FPP_STATUS_PLAYLIST_PLAYING; | ||
| 276 | |||
| 277 | // m_name is written under m_playlistMutex (Load, Cleanup) but read by the | ||
| 278 | // scheduler and status threads through GetPlaylistName() without it -- | ||
| 279 | // a copy racing the assignment read a freed buffer. Writers hold both; | ||
| 280 | // GetPlaylistName() takes only this one, so it never waits on a playlist | ||
| 281 | // transition and never orders this lock against the scheduler's. | ||
| 282 | mutable std::mutex m_nameLock; | ||
| 283 | |||
| 284 | // For each section, the index in m_leadIn / m_mainPlaylist / m_leadOut | ||
| 285 | // where each row of the playlist file starts (-1 for rows not loaded). | ||
| 286 | // A sub-playlist row expands to several entries, so file rows -- what a | ||
| 287 | // Branch's item number, a resume position and Start At Item count -- and | ||
| 288 | // loaded positions differ once one appears. | ||
| 289 | std::vector<int> m_leadInRows; | ||
| 290 | std::vector<int> m_mainRows; | ||
| 291 | std::vector<int> m_leadOutRows; | ||
| 292 | }; | ||
| 293 | |||
| 294 | // Source-compatibility façade for the old `extern Playlist* playlist;` global. | ||
| 295 | // | ||
| 296 | // The player now owns its playlists through shared_ptr (see Player.h), so | ||
| 297 | // nothing in-tree touches this: FPP's own code calls Player::INSTANCE, which | ||
| 298 | // snapshots the current playlist per call. External plugins, however, were | ||
| 299 | // written against a raw global and dereference it as `playlist->Method(...)`. | ||
| 300 | // | ||
| 301 | // That expression still compiles, through two chained operator->s: PlaylistRef | ||
| 302 | // hands back a PlaylistHandle holding a snapshot, and the handle's operator-> | ||
| 303 | // yields the raw pointer. The handle is a temporary of the full expression, so | ||
| 304 | // the snapshot — and therefore the instance — outlives the call even if the | ||
| 305 | // main loop retires that playlist mid-call. That makes the old plugin | ||
| 306 | // expression lifetime-safe rather than merely still-compiling. | ||
| 307 | // | ||
| 308 | // ABI is NOT preserved (this used to be a pointer object). That was safe only | ||
| 309 | // because it landed before 10.0 shipped, inside the plugin API 6 development | ||
| 310 | // window, so no released API-6 plugin saw the old form. Plugins are not | ||
| 311 | // rebuilt after an FPPOS reflash, and prebuilt ones never are, so | ||
| 312 | // PlaylistHandle's layout (one shared_ptr, used inline by plugin code) is | ||
| 313 | // plugin ABI now: changing it needs an FPP_PLUGIN_API_VERSION bump | ||
| 314 | // (Plugin.h). See .claude/PLUGIN-ABI.md. | ||
| 315 | // | ||
| 316 | // Before Player::Init() the snapshot is empty and `playlist->` dereferences | ||
| 317 | // null, exactly as the old NULL-initialized global did. It is deliberately not | ||
| 318 | // papered over with a dummy instance: constructing a Playlist needs Events, | ||
| 319 | // PluginManager and CommandManager, so a plugin calling this from its own | ||
| 320 | // static initialization has a startup-order bug that a stand-in would hide. | ||
| 321 | class PlaylistHandle { | ||
| 322 | public: | ||
| 323 | Playlist* operator->() const { return p.get(); } | ||
| 324 | std::shared_ptr<Playlist> p; | ||
| 325 | }; | ||
| 326 | |||
| 327 | class PlaylistRef { | ||
| 328 | public: | ||
| 329 | PlaylistHandle operator->() const; | ||
| 330 | // So `if (playlist)` in external code still compiles and still means | ||
| 331 | // "is there a playlist to talk to". | ||
| 332 | explicit operator bool() const; | ||
| 333 | }; | ||
| 334 | |||
| 335 | extern PlaylistRef playlist; | ||
| 336 | |||
| 337 | // Write the last-known playing state to fd for a crash report. | ||
| 338 | // | ||
| 339 | // Reads a snapshot of plain scalars rather than the live Playlist, on purpose: | ||
| 340 | // the crash handler cannot take m_playlistMutex (the faulting thread may hold | ||
| 341 | // it, and blocking there hangs the process instead of producing a report), and | ||
| 342 | // it cannot walk a std::string or vector that another thread is mid-update. | ||
| 343 | // A field may be one update stale or torn; that is worth far more than nothing. | ||
| 344 | void PlaylistDumpCrashState(int fd); | ||
| 345 |