GCC Code Coverage Report


Directory: ./
File: playlist/Playlist.h
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 5 5 100.0%
Functions: 5 5 100.0%
Branches: 2 2 100.0%

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