| 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 | // PLUGIN API: native plugins compile against this header and are loaded with | ||
| 15 | // dlopen(). | ||
| 16 | // - Plugins use the global `sequence` and call its methods by symbol | ||
| 17 | // (SendBlankingData, ...), and rely on FPPD_MAX_CHANNELS. | ||
| 18 | // Data layout here is NOT plugin ABI, so members can change freely. Changing | ||
| 19 | // or removing a signature a plugin calls is: keep the old one as an overload | ||
| 20 | // (as Timers::addPeriodicTimer does) or bump FPP_PLUGIN_API_VERSION in | ||
| 21 | // Plugin.h. See .claude/PLUGIN-ABI.md for which plugins use what. | ||
| 22 | |||
| 23 | #include <stdio.h> | ||
| 24 | #include <string> | ||
| 25 | |||
| 26 | #include <atomic> | ||
| 27 | #include <condition_variable> | ||
| 28 | #include <list> | ||
| 29 | #include <map> | ||
| 30 | #include <memory> | ||
| 31 | #include <mutex> | ||
| 32 | #include <thread> | ||
| 33 | #include <vector> | ||
| 34 | |||
| 35 | #include "AtomicSharedPtr.h" | ||
| 36 | #include "fseq/FSEQFile.h" | ||
| 37 | |||
| 38 | #define FPPD_MAX_CHANNELS (8192 * 1024) | ||
| 39 | #define DATA_DUMP_SIZE 28 | ||
| 40 | |||
| 41 | // reserve 4 channels of 0 and 4 channels of 0xFF for indexes | ||
| 42 | // that require one or the other | ||
| 43 | #define FPPD_OFF_CHANNEL FPPD_MAX_CHANNELS | ||
| 44 | #define FPPD_WHITE_CHANNEL (FPPD_MAX_CHANNELS + 4) | ||
| 45 | #define FPPD_MAX_CHANNEL_NUM (FPPD_WHITE_CHANNEL + 4) | ||
| 46 | |||
| 47 | class Sequence { | ||
| 48 | public: | ||
| 49 | Sequence(); | ||
| 50 | ~Sequence(); | ||
| 51 | |||
| 52 | int IsSequenceRunning(void); | ||
| 53 | int IsSequenceRunning(const std::string& filename); | ||
| 54 | int OpenSequenceFile(const std::string& filename, int startFrame = 0, int startSecond = -1); | ||
| 55 | void StartSequence(const std::string& filename, int startFrame); | ||
| 56 | void StartSequence(); | ||
| 57 | void ProcessSequenceData(int ms); | ||
| 58 | void SeekSequenceFile(int frameNumber); | ||
| 59 | // Takes the channel output thread's pending frame skip and seeks that | ||
| 60 | // many frames on from the frame being output. Under m_sequenceLock, which | ||
| 61 | // every open holds while it cancels the pending skip, so a skip counted | ||
| 62 | // from one open's position is never applied to the next: a remote's stale | ||
| 63 | // sync packet from the last play-through of the same file used to jump the | ||
| 64 | // next one to its end. | ||
| 65 | void SkipFrames(std::atomic_int& skip); | ||
| 66 | void ReadSequenceData(bool forceFirstFrame = false); | ||
| 67 | void SendSequenceData(void); | ||
| 68 | void SendBlankingData(void); | ||
| 69 | void CloseIfOpen(const std::string& filename); | ||
| 70 | void CloseSequenceFile(void); | ||
| 71 | void ToggleSequencePause(void); | ||
| 72 | void SingleStepSequence(void); | ||
| 73 | void SingleStepSequenceBack(void); | ||
| 74 | int SequenceIsPaused(void); | ||
| 75 | |||
| 76 | bool hasBridgeData(); | ||
| 77 | 130913 | bool isDataProcessed() const { return m_dataProcessed.load(std::memory_order_acquire); } | |
| 78 | 17715 | void setDataNotProcessed() { m_dataProcessed = false; } | |
| 79 | |||
| 80 | // Advanced by the output thread every frame and read by status, the | ||
| 81 | // playlist and the crash handler from other threads. | ||
| 82 | std::atomic_int m_seqMSDuration; | ||
| 83 | std::atomic_int m_seqMSElapsed; | ||
| 84 | std::atomic_int m_seqMSRemaining; | ||
| 85 | char* m_seqData; | ||
| 86 | std::string m_seqFilename; | ||
| 87 | |||
| 88 | // Every read of m_seqFilename from a thread that is not holding | ||
| 89 | // m_sequenceLock must go through this. The open/close paths reassign the | ||
| 90 | // string, and a reallocating assignment hands a concurrent raw reader a | ||
| 91 | // freed buffer; copying under m_seqFilenameLock is the only safe read. | ||
| 92 | std::string GetSeqFilenameCopy() const; | ||
| 93 | |||
| 94 | // Run every queued sequence preset now, on the calling thread. Normally | ||
| 95 | // driven by the main-loop timer queuePreset() arms; called directly once | ||
| 96 | // during shutdown, because a preset queued in the last loop iteration | ||
| 97 | // (a sequence that ended, or a stop command, right as the loop exits) has | ||
| 98 | // no later tick to fire on and would otherwise be dropped. MUST be called | ||
| 99 | // with no sequence lock held -- presets run arbitrary commands. | ||
| 100 | void drainPendingPresets(); | ||
| 101 | |||
| 102 | 117503 | int GetSeqStepTime() const { return m_seqStepTime; } | |
| 103 | void BlankSequenceData(bool clearBridge = false); | ||
| 104 | |||
| 105 | // expireMS is on the GetMonotonicTimeMS() clock. | ||
| 106 | void SetBridgeData(uint8_t* data, int startChannel, int len, uint64_t expireMS); | ||
| 107 | |||
| 108 | // Bridge data arrives in bursts (one recvmmsg drain can be hundreds of | ||
| 109 | // packets). WriteBridgeData copies one packet's channels into the bridge | ||
| 110 | // buffer without touching the range bookkeeping; CommitBridgeRanges then | ||
| 111 | // records all of a burst's ranges under a single lock acquisition. | ||
| 112 | // SetBridgeData above is the two combined, for single-packet callers. | ||
| 113 | bool WriteBridgeData(uint8_t* data, int startChannel, int len); | ||
| 114 | void CommitBridgeRanges(const std::vector<std::pair<uint32_t, uint32_t>>& ranges, uint64_t expireMS); | ||
| 115 | |||
| 116 | private: | ||
| 117 | void ProcessVariableHeaders(); | ||
| 118 | void SetLastFrameData(FSEQFile::FrameData* data); | ||
| 119 | |||
| 120 | // A sequence file we are asked to play that isn't present here. The | ||
| 121 | // "does not exist" warning carries a timeout so it clears itself once the | ||
| 122 | // player moves on, but a remote gets no second open attempt after the | ||
| 123 | // first failure (m_seqFilename is left set, so StartSequence short-circuits), | ||
| 124 | // so the warning has to be re-armed from the sync path or it expires while | ||
| 125 | // the sequence is still playing on the player. | ||
| 126 | void SetMissingSequenceWarning(const std::string& filename, const std::string& warning); | ||
| 127 | void RefreshMissingSequenceWarning(); | ||
| 128 | void ClearMissingSequenceWarning(); | ||
| 129 | |||
| 130 | void setBridgePrioritySetting(const std::string& value); | ||
| 131 | bool m_prioritize_sequence_over_bridge; | ||
| 132 | bool m_warn_if_bridging = false; | ||
| 133 | |||
| 134 | class BridgeRangeData { | ||
| 135 | public: | ||
| 136 | 3055 | BridgeRangeData() : | |
| 137 | 3055 | startChannel(0) {} | |
| 138 | |||
| 139 | uint32_t startChannel; | ||
| 140 | // map of len -> ms when it expires, in MOST cases, this will be a single len (like 512 for e1.31) | ||
| 141 | std::map<uint32_t, uint64_t> expires; | ||
| 142 | }; | ||
| 143 | std::map<uint64_t, BridgeRangeData> m_bridgeRanges; | ||
| 144 | std::mutex m_bridgeRangesLock; | ||
| 145 | uint8_t* m_bridgeData; | ||
| 146 | // Mirrors !m_bridgeRanges.empty(); updated wherever the map is modified | ||
| 147 | // (under m_bridgeRangesLock) so hasBridgeData() - called several times a | ||
| 148 | // frame from both the main loop and the output thread - needs no lock. | ||
| 149 | std::atomic<bool> m_hasBridgeRanges{ false }; | ||
| 150 | // Per-frame scratch for the merge in ProcessSequenceData, kept as a | ||
| 151 | // member so its allocation is reused frame to frame. | ||
| 152 | std::vector<std::pair<uint32_t, uint32_t>> m_bridgeRangeScratch; | ||
| 153 | |||
| 154 | AtomicSharedPtr<FSEQFile> m_seqFile; | ||
| 155 | |||
| 156 | // 2 while a file opens, 1 once its header is read, 0 when it plays. Set | ||
| 157 | // by whichever thread opens or starts the sequence; read by the | ||
| 158 | // read-ahead thread, the output thread and status. | ||
| 159 | std::atomic_int m_seqStarting; | ||
| 160 | int m_seqPaused; | ||
| 161 | int m_seqStepTime; | ||
| 162 | int m_seqSingleStep; | ||
| 163 | int m_seqSingleStepBack; | ||
| 164 | float m_seqRefreshRate; | ||
| 165 | int m_remoteBlankCount; | ||
| 166 | // Set by whichever thread processed the frame (normally the output | ||
| 167 | // thread) and cleared by the bridge receive path and sequence loads. | ||
| 168 | std::atomic<bool> m_dataProcessed; | ||
| 169 | int m_numSeek; | ||
| 170 | |||
| 171 | int m_blankBetweenSequences; | ||
| 172 | |||
| 173 | // A sequence that played to its end, until the next open. Guarded by | ||
| 174 | // m_sequenceLock. A remote whose copy is shorter than the master's | ||
| 175 | // finishes first, and every later sync packet for it used to reopen the | ||
| 176 | // file at a frame past its end -- several reopens a second, each firing | ||
| 177 | // SEQUENCE_STARTED/STOPPED, until the master stopped. | ||
| 178 | std::string m_endedSeqFilename; | ||
| 179 | // Sync packets for the ended sequence are ignored only from this frame | ||
| 180 | // on: the last two seconds of our copy, or past its end. One asking for | ||
| 181 | // an earlier frame means we ended early (a stale sync packet from the | ||
| 182 | // previous play-through of the same file jumped us to the end), and it | ||
| 183 | // reopens the file there, as remotes did before m_endedSeqFilename. | ||
| 184 | int m_endedSeqIgnoreFrom = 0; | ||
| 185 | |||
| 186 | // The name the sequence was opened as -- the master's name for it -- even | ||
| 187 | // when a remote is playing fallback.fseq in its place. m_seqFilename names | ||
| 188 | // the file actually playing; sync packets and stop packets carry this one. | ||
| 189 | // Matching sync against m_seqFilename alone meant a fallback never took the | ||
| 190 | // master's position (so the output thread gave up after two seconds) and | ||
| 191 | // was never stopped by the master's stop. Guarded by m_sequenceLock. | ||
| 192 | std::string m_seqSyncName; | ||
| 193 | |||
| 194 | // Bumped by every OpenSequenceFile() under m_sequenceLock. The open drops | ||
| 195 | // that lock while it sends the MultiSync open packet; if another open ran | ||
| 196 | // in that window, the older one stands down instead of replacing the newer | ||
| 197 | // file with its own (leaving the newer one's caches and triggers behind). | ||
| 198 | uint64_t m_openGeneration = 0; | ||
| 199 | |||
| 200 | // Both guarded by m_sequenceLock. | ||
| 201 | std::string m_missingSeqFilename; | ||
| 202 | std::string m_missingSeqWarning; | ||
| 203 | |||
| 204 | std::recursive_mutex m_sequenceLock; | ||
| 205 | // Covers m_seqFilename only, and is always the innermost lock taken. The | ||
| 206 | // three assignments to m_seqFilename all happen under m_sequenceLock, so | ||
| 207 | // readers that hold m_sequenceLock need nothing more; this exists for the | ||
| 208 | // readers that don't (the status producers, the output thread, MultiSync). | ||
| 209 | mutable std::mutex m_seqFilenameLock; | ||
| 210 | |||
| 211 | std::atomic_int m_lastFrameRead; | ||
| 212 | // Read by the read-ahead thread outside frameCacheLock (under readFileLock | ||
| 213 | // only), written by CloseSequenceFile() under frameCacheLock. | ||
| 214 | std::atomic<bool> m_doneRead; | ||
| 215 | // Set by the destructor while the read-ahead thread is still checking it. | ||
| 216 | std::atomic<bool> m_shuttingDown; | ||
| 217 | std::thread* m_readThread; | ||
| 218 | std::list<FSEQFile::FrameData*> frameCache; | ||
| 219 | std::list<FSEQFile::FrameData*> pastFrameCache; | ||
| 220 | FSEQFile::FrameData* m_lastFrameData; | ||
| 221 | void clearCaches(); | ||
| 222 | std::mutex frameCacheLock; | ||
| 223 | std::mutex readFileLock; // lock for just the stuff needed to read from the file (m_seqFile variable) | ||
| 224 | std::condition_variable frameLoadSignal; | ||
| 225 | std::condition_variable frameLoadedSignal; | ||
| 226 | |||
| 227 | // Frame-triggered presets and effects from the file's FC/FE variable | ||
| 228 | // headers. Built whole by ProcessVariableHeaders() and published with the | ||
| 229 | // file; the output thread reads a snapshot. They used to be plain maps | ||
| 230 | // that open/close cleared and refilled while the output thread was | ||
| 231 | // looking things up in them. | ||
| 232 | // | ||
| 233 | // Each command/effect name is stored once in `names` and the frame maps | ||
| 234 | // hold indexes into it. An FC/FE block gives a name followed by its frame | ||
| 235 | // ranges, and copying the name into every range let one long name with a | ||
| 236 | // few million ranges expand a small header into hundreds of MB. | ||
| 237 | struct Triggers { | ||
| 238 | std::vector<std::string> names; | ||
| 239 | std::map<uint32_t, std::vector<uint32_t>> commandPresets; | ||
| 240 | std::map<uint32_t, std::vector<uint32_t>> effectsOn; | ||
| 241 | std::map<uint32_t, std::vector<uint32_t>> effectsOff; | ||
| 242 | }; | ||
| 243 | AtomicSharedPtr<Triggers> m_triggers; | ||
| 244 | // Frame number of m_lastFrameData (-1 for none), set with it under | ||
| 245 | // frameCacheLock, so the output thread can read it without the lock -- | ||
| 246 | // and without touching a frame another thread may be freeing. | ||
| 247 | std::atomic<int64_t> m_lastFrameNumber{ -1 }; | ||
| 248 | // Last frame whose triggers have fired; reset when a file opens. | ||
| 249 | std::atomic<int64_t> m_lastTriggeredFrame{ -1 }; | ||
| 250 | // FE-started effects still running. They loop, so they are stopped when | ||
| 251 | // the sequence closes rather than left running after it. | ||
| 252 | std::mutex m_triggerEffectsLock; | ||
| 253 | std::vector<std::string> m_triggerEffects; | ||
| 254 | // The trigger table m_triggerEffects belongs to, or null once it has been | ||
| 255 | // forgotten (closed). Guarded by m_triggerEffectsLock. The output thread | ||
| 256 | // can be inside FireFrameTriggers() with a snapshot of the old table while | ||
| 257 | // the close swaps m_triggerEffects out; an effect it starts after that | ||
| 258 | // swap would be recorded in a list nobody stops. It checks this under the | ||
| 259 | // same lock and stops such an effect itself. Compared only against a | ||
| 260 | // table the caller still holds a reference to, so the address cannot have | ||
| 261 | // been reused. | ||
| 262 | const Triggers* m_triggerEffectsOwner = nullptr; | ||
| 263 | void FireFrameTriggers(); | ||
| 264 | void ForgetTriggers(); | ||
| 265 | |||
| 266 | // Sequence-fired presets (SEQUENCE_STARTED/SEQUENCE_STOPPED and the FSEQ | ||
| 267 | // frame-triggered commands) must never run at the call site: the open/close | ||
| 268 | // paths hold m_sequenceLock (often recursively, from callers that also hold | ||
| 269 | // it) and SendSequenceData runs on the channel output thread under | ||
| 270 | // outputThreadLock, so a preset that touches the playlist deadlocks against | ||
| 271 | // a playlist thread opening a sequence (m_playlistMutex -> m_sequenceLock | ||
| 272 | // vs m_sequenceLock -> m_playlistMutex). They are queued here and drained | ||
| 273 | // in FIFO order by a main-loop timer with no sequence locks held. | ||
| 274 | std::mutex m_pendingPresetsLock; | ||
| 275 | std::list<std::pair<std::string, std::map<std::string, std::string>>> m_pendingPresets; | ||
| 276 | // Effects to stop, drained with the presets for the same reason. | ||
| 277 | std::list<std::string> m_pendingEffectStops; | ||
| 278 | |||
| 279 | public: | ||
| 280 | // Also used by outputs that fire presets from the output thread (the | ||
| 281 | // control channel output), for the same reason. | ||
| 282 | void queuePreset(const std::string& preset, std::map<std::string, std::string>&& keywords); | ||
| 283 | |||
| 284 | void ReadFramesLoop(); | ||
| 285 | |||
| 286 | private: | ||
| 287 | // Whether m_seqFile and m_triggers are set, so the per-frame checks | ||
| 288 | // (IsSequenceRunning(), FireFrameTriggers()) need no lock: an | ||
| 289 | // AtomicSharedPtr load is a mutex and a refcount round trip, about 0.5 us | ||
| 290 | // on an AM335x, and the output thread asks several times a frame. Both | ||
| 291 | // pointers are stored only through setSeqFile()/setTriggers(), which keep | ||
| 292 | // these in step. Declared last so no existing member moves. | ||
| 293 | std::atomic<bool> m_haveSeqFile{ false }; | ||
| 294 | std::atomic<bool> m_haveTriggers{ false }; | ||
| 295 | void setSeqFile(std::shared_ptr<FSEQFile> f); | ||
| 296 | void setTriggers(std::shared_ptr<Triggers> t); | ||
| 297 | }; | ||
| 298 | |||
| 299 | extern Sequence* sequence; | ||
| 300 |