| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | |||
| 3 | // PLUGIN ABI: native plugins compile against this header and are loaded with | ||
| 4 | // dlopen(), so parts of it are baked into binaries FPP did not build. | ||
| 5 | // - FSEQFile, V1FSEQFile, V2FSEQFile and FrameData vtables: plugins call | ||
| 6 | // their virtuals (a mid-vtable virtual added here once only escaped | ||
| 7 | // because an unrelated bump landed a week later). | ||
| 8 | // - Their data members: plugins use inline setters that write protected | ||
| 9 | // fields, and read V2FSEQFile::m_sparseRanges. | ||
| 10 | // - VariableHeader, which plugins construct and push by value. | ||
| 11 | // Any such change must bump FPP_PLUGIN_API_VERSION in Plugin.h. Without the | ||
| 12 | // bump, a plugin built against the previous header still loads and silently | ||
| 13 | // uses the old layout - FPP 10.2 shipped exactly that and crash-looped fppd. | ||
| 14 | // See .claude/PLUGIN-ABI.md for which plugins use what. | ||
| 15 | |||
| 16 | #include <stdio.h> | ||
| 17 | #include <cstdint> | ||
| 18 | #include <string> | ||
| 19 | #include <vector> | ||
| 20 | |||
| 21 | class FSEQFile { | ||
| 22 | public: | ||
| 23 | class VariableHeader { | ||
| 24 | public: | ||
| 25 | 4 | VariableHeader() : | |
| 26 | 4 | fseqFile(nullptr) { | |
| 27 | 4 | code[0] = code[1] = 0; | |
| 28 | 4 | offset = 0; | |
| 29 | 4 | length = 0; | |
| 30 | 4 | extendedData = false; | |
| 31 | 4 | } | |
| 32 | 111 | VariableHeader(FSEQFile* fseq) : | |
| 33 | 111 | fseqFile(fseq), | |
| 34 | 111 | offset(0), | |
| 35 | 111 | length(0) { | |
| 36 | 111 | code[0] = code[1] = 0; | |
| 37 | 111 | extendedData = false; | |
| 38 | 111 | } | |
| 39 | 153 | VariableHeader(const VariableHeader& cp) : | |
| 40 | 153 | data(cp.data) { | |
| 41 | 153 | code[0] = cp.code[0]; | |
| 42 | 153 | code[1] = cp.code[1]; | |
| 43 | 153 | extendedData = cp.extendedData; | |
| 44 | 153 | offset = cp.offset; | |
| 45 | 153 | length = cp.length; | |
| 46 | 153 | fseqFile = cp.fseqFile; | |
| 47 | 153 | } | |
| 48 | 295 | ~VariableHeader() {} | |
| 49 | |||
| 50 | uint8_t code[2]; | ||
| 51 | bool extendedData = false; | ||
| 52 | |||
| 53 | 105 | void resizeData(size_t sz) { | |
| 54 | 105 | data.resize(sz); | |
| 55 | 105 | length = 0; | |
| 56 | 105 | offset = 0; | |
| 57 | 105 | } | |
| 58 | 197 | const uint32_t getDataLength() const { | |
| 59 |
1/2✓ Branch 0 taken 197 times.
✗ Branch 1 not taken.
|
197 | if (!data.empty()) { |
| 60 | 197 | return (uint32_t)data.size(); | |
| 61 | } | ||
| 62 | ✗ | return length; | |
| 63 | } | ||
| 64 | 8 | void setDataLocation(uint64_t off, uint32_t len) { | |
| 65 | 8 | offset = off; | |
| 66 | 8 | length = len; | |
| 67 | 8 | } | |
| 68 | 29 | const std::vector<uint8_t>& getData() const { | |
| 69 | 29 | loadData(); | |
| 70 | 29 | return data; | |
| 71 | } | ||
| 72 | 161 | std::vector<uint8_t>& getData() { | |
| 73 | 161 | loadData(); | |
| 74 | 161 | return data; | |
| 75 | } | ||
| 76 | |||
| 77 |
2/2✓ Branch 0 taken 13 times.
✓ Branch 1 taken 101 times.
|
114 | uint64_t getExtDataOffset() const { return extendedData ? offset : 0; } |
| 78 | |||
| 79 | private: | ||
| 80 | void loadData() const; | ||
| 81 | mutable std::vector<uint8_t> data; | ||
| 82 | uint64_t offset; | ||
| 83 | uint32_t length; | ||
| 84 | FSEQFile* fseqFile; | ||
| 85 | }; | ||
| 86 | |||
| 87 | class FrameData { | ||
| 88 | public: | ||
| 89 | 125137 | FrameData(uint32_t f) : | |
| 90 | 125137 | frame(f){}; | |
| 91 | 250274 | virtual ~FrameData(){}; | |
| 92 | |||
| 93 | virtual bool readFrame(uint8_t* data, uint32_t maxChannels) = 0; | ||
| 94 | |||
| 95 | uint32_t frame; | ||
| 96 | }; | ||
| 97 | |||
| 98 | enum CompressionType { | ||
| 99 | none, | ||
| 100 | zstd, | ||
| 101 | zlib | ||
| 102 | }; | ||
| 103 | constexpr static const char* CompressionTypeStrings[] = { "none", "zstd", "zlib" }; | ||
| 104 | |||
| 105 | protected: | ||
| 106 | // open file for reading | ||
| 107 | FSEQFile(const std::string& fn, FILE* file, const std::vector<uint8_t>& header); | ||
| 108 | // open file for writing | ||
| 109 | FSEQFile(const std::string& fn); | ||
| 110 | |||
| 111 | public: | ||
| 112 | virtual ~FSEQFile(); | ||
| 113 | |||
| 114 | static FSEQFile* openFSEQFile(const std::string& fn); | ||
| 115 | |||
| 116 | static FSEQFile* createFSEQFile(const std::string& fn, | ||
| 117 | int version, | ||
| 118 | CompressionType ct = CompressionType::zstd, | ||
| 119 | int level = -99); | ||
| 120 | // utility methods | ||
| 121 | static std::string getMediaFilename(const std::string& fn); | ||
| 122 | std::string getMediaFilename() const; | ||
| 123 | ✗ | uint32_t getTotalTimeMS() const { return m_seqNumFrames * m_seqStepTime; } | |
| 124 | |||
| 125 | void parseVariableHeaders(const std::vector<uint8_t>& header, int start); | ||
| 126 | |||
| 127 | // How the caller intends to read this file. This is about the access | ||
| 128 | // pattern, not the hardware: it says what the reader is allowed to do ahead | ||
| 129 | // of the frame being asked for. | ||
| 130 | enum class ReadPattern { | ||
| 131 | // Serve one frame at a time with the smallest possible latency and | ||
| 132 | // memory footprint - what playback needs, especially on a small single | ||
| 133 | // core device that cannot hold the sequence. The default. | ||
| 134 | Streaming, | ||
| 135 | // Every frame will be read in order, front to back, as fast as | ||
| 136 | // possible, and the caller has already allocated room for what it | ||
| 137 | // needs. The reader may then decompress whole blocks ahead of the | ||
| 138 | // requested frame, in parallel, at the cost of a bounded amount of | ||
| 139 | // extra memory. | ||
| 140 | Bulk | ||
| 141 | }; | ||
| 142 | // Must be set before prepareRead()/getFrame() to have any effect. | ||
| 143 | 38 | virtual void setReadPattern(ReadPattern p) { m_readPattern = p; } | |
| 144 | 27 | ReadPattern getReadPattern() const { return m_readPattern; } | |
| 145 | |||
| 146 | // prepare to start reading. The ranges will be the list of channel ranges that | ||
| 147 | // are actually needed for each frame. The reader can optimize to only | ||
| 148 | // read those frames. | ||
| 149 | ✗ | virtual void prepareRead(const std::vector<std::pair<uint32_t, uint32_t>>& ranges, uint32_t startFrame = 0) {} | |
| 150 | |||
| 151 | // For reading data from the fseq file, returns an object can | ||
| 152 | // provide the necessary data in a timely fashion for the given frame | ||
| 153 | // It may not be used right away and will be deleted at some point in the future | ||
| 154 | virtual FrameData* getFrame(uint32_t frame) = 0; | ||
| 155 | |||
| 156 | // For writing to the fseq file | ||
| 157 | 2 | virtual void enableMinorVersionFeatures(uint8_t ver) {} | |
| 158 | virtual void initializeFromFSEQ(const FSEQFile& fseq); | ||
| 159 | virtual void writeHeader() = 0; | ||
| 160 | virtual void addFrame(uint32_t frame, | ||
| 161 | const uint8_t* data) = 0; | ||
| 162 | virtual void finalize(); | ||
| 163 | |||
| 164 | virtual void dumpInfo(bool indent = false); | ||
| 165 | |||
| 166 | 124545 | uint32_t getNumFrames() const { return m_seqNumFrames; } | |
| 167 | 2551 | int getStepTime() const { return m_seqStepTime; } | |
| 168 | 533382 | uint32_t getChannelCount() const { return m_seqChannelCount; } | |
| 169 | 65 | int getVersionMajor() const { return m_seqVersionMajor; } | |
| 170 | 18 | int getVersionMinor() const { return m_seqVersionMinor; } | |
| 171 | 18 | uint64_t getUniqueId() const { return m_uniqueId; } | |
| 172 | 1048 | const std::string& getFilename() const { return m_filename; } | |
| 173 | |||
| 174 | virtual uint32_t getMaxChannel() const = 0; | ||
| 175 | 1722 | const std::vector<VariableHeader>& getVariableHeaders() const { return m_variableHeaders; } | |
| 176 | |||
| 177 | void setNumFrames(uint32_t f) { m_seqNumFrames = f; } | ||
| 178 | void setStepTime(int st) { m_seqStepTime = st; } | ||
| 179 | void setChannelCount(int cc) { m_seqChannelCount = cc; } | ||
| 180 | void addVariableHeader(const VariableHeader& header) { m_variableHeaders.push_back(header); } | ||
| 181 | |||
| 182 | ✗ | const std::vector<uint8_t>& getMemoryBuffer() const { return m_memoryBuffer; } | |
| 183 | uint64_t getMemoryBufferPos() const { return m_memoryBufferPos; } | ||
| 184 | |||
| 185 | protected: | ||
| 186 | std::string m_filename; | ||
| 187 | uint64_t m_uniqueId; | ||
| 188 | uint32_t m_seqNumFrames; | ||
| 189 | uint32_t m_seqChannelCount; | ||
| 190 | int m_seqStepTime; | ||
| 191 | int m_seqVersionMajor; | ||
| 192 | int m_seqVersionMinor; | ||
| 193 | ReadPattern m_readPattern = ReadPattern::Streaming; | ||
| 194 | |||
| 195 | std::vector<VariableHeader> m_variableHeaders; | ||
| 196 | |||
| 197 | protected: | ||
| 198 | uint64_t m_seqFileSize; | ||
| 199 | uint64_t m_seqChanDataOffset; | ||
| 200 | |||
| 201 | int seek(uint64_t location, int origin); | ||
| 202 | uint64_t tell(); | ||
| 203 | uint64_t write(const void* ptr, uint64_t size); | ||
| 204 | uint64_t read(void* ptr, uint64_t size); | ||
| 205 | void preload(uint64_t pos, uint64_t size); | ||
| 206 | |||
| 207 | private: | ||
| 208 | FILE* volatile m_seqFile; | ||
| 209 | std::vector<uint8_t> m_memoryBuffer; | ||
| 210 | uint64_t m_memoryBufferPos; | ||
| 211 | }; | ||
| 212 | |||
| 213 | class V1FSEQFile : public FSEQFile { | ||
| 214 | public: | ||
| 215 | V1FSEQFile(const std::string& fn, FILE* file, const std::vector<uint8_t>& header); | ||
| 216 | V1FSEQFile(const std::string& fn); | ||
| 217 | |||
| 218 | virtual ~V1FSEQFile(); | ||
| 219 | |||
| 220 | virtual void prepareRead(const std::vector<std::pair<uint32_t, uint32_t>>& ranges, uint32_t startFrame = 0) override; | ||
| 221 | virtual FrameData* getFrame(uint32_t frame) override; | ||
| 222 | |||
| 223 | virtual void writeHeader() override; | ||
| 224 | virtual void addFrame(uint32_t frame, | ||
| 225 | const uint8_t* data) override; | ||
| 226 | virtual void finalize() override; | ||
| 227 | |||
| 228 | virtual uint32_t getMaxChannel() const override; | ||
| 229 | |||
| 230 | // The ranges to read and the data size needed to read the ranges | ||
| 231 | std::vector<std::pair<uint32_t, uint32_t>> m_rangesToRead; | ||
| 232 | uint32_t m_dataBlockSize; | ||
| 233 | }; | ||
| 234 | |||
| 235 | class V2Handler; | ||
| 236 | |||
| 237 | class V2FSEQFile : public FSEQFile { | ||
| 238 | public: | ||
| 239 | V2FSEQFile(const std::string& fn, FILE* file, const std::vector<uint8_t>& header); | ||
| 240 | V2FSEQFile(const std::string& fn, CompressionType ct, int cl); | ||
| 241 | |||
| 242 | virtual ~V2FSEQFile(); | ||
| 243 | |||
| 244 | virtual void prepareRead(const std::vector<std::pair<uint32_t, uint32_t>>& ranges, uint32_t startFrame = 0) override; | ||
| 245 | virtual FrameData* getFrame(uint32_t frame) override; | ||
| 246 | |||
| 247 | virtual void writeHeader() override; | ||
| 248 | virtual void addFrame(uint32_t frame, | ||
| 249 | const uint8_t* data) override; | ||
| 250 | virtual void finalize() override; | ||
| 251 | |||
| 252 | virtual void dumpInfo(bool indent = false) override; | ||
| 253 | |||
| 254 | virtual uint32_t getMaxChannel() const override; | ||
| 255 | |||
| 256 | 36 | virtual void enableMinorVersionFeatures(uint8_t ver) override { | |
| 257 | 36 | m_seqVersionMinor = ver; | |
| 258 |
2/2✓ Branch 0 taken 27 times.
✓ Branch 1 taken 9 times.
|
36 | if (ver == 0) { |
| 259 | 27 | m_allowExtendedBlocks = false; | |
| 260 | } | ||
| 261 |
2/2✓ Branch 0 taken 9 times.
✓ Branch 1 taken 27 times.
|
36 | if (ver >= 1) { |
| 262 | 9 | m_allowExtendedBlocks = true; | |
| 263 | } | ||
| 264 | 36 | } | |
| 265 | |||
| 266 | // The fixed header holds at most 255 sparse ranges. From minor version 3 a longer | ||
| 267 | // table also goes in an 'SR' extended data block; below that, the ranges are | ||
| 268 | // merged across their smallest gaps until they fit. | ||
| 269 | static void fitSparseRangesToHeader(std::vector<std::pair<uint32_t, uint32_t>>& ranges); | ||
| 270 | |||
| 271 | 17 | [[nodiscard]] std::string CompressionTypeString() const { | |
| 272 |
1/2✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
|
34 | return CompressionTypeStrings[(int)m_compressionType]; |
| 273 | } | ||
| 274 | |||
| 275 | CompressionType m_compressionType; | ||
| 276 | int m_compressionLevel; | ||
| 277 | std::vector<std::pair<uint32_t, uint32_t>> m_sparseRanges; | ||
| 278 | std::vector<std::pair<uint32_t, uint32_t>> m_rangesToRead; | ||
| 279 | std::vector<std::pair<uint32_t, uint64_t>> m_frameOffsets; | ||
| 280 | uint32_t m_dataBlockSize; | ||
| 281 | bool m_allowExtendedBlocks; | ||
| 282 | |||
| 283 | private: | ||
| 284 | void createHandler(); | ||
| 285 | |||
| 286 | V2Handler* m_handler; | ||
| 287 | friend class V2Handler; | ||
| 288 | }; | ||
| 289 |