FastLED 3.10.8
Loading...
Searching...
No Matches
pixel_stream.cpp.hpp
Go to the documentation of this file.
1
3#include "fl/log/log.h"
4#include "fl/stl/limits.h"
5#include "fl/stl/noexcept.h"
6#include "fl/stl/json.h"
7
8#define DBG FL_DBG
9
10namespace fl {
11namespace video {
12
13// FLED v1 header layout (12 bytes). See
14// https://github.com/zackees/ledmapper/blob/main/docs/fled-format.md
15namespace {
16constexpr fl::u8 kFledMagic[4] = {'F', 'L', 'E', 'D'};
17constexpr fl::u8 kFledVersionV1 = 1;
19// Defensive cap on the embedded JSON to bound the worst-case heap
20// allocation if a malformed file claims a giant json_length. Real
21// screenmaps run ~500 B – ~50 KB; 1 MiB is well above that.
22constexpr fl::size_t kFledMaxJsonBytes = 1u * 1024u * 1024u;
23
25 for (fl::size_t i = 0; i < size; ++i) {
26 if (bytes[i] != kFledMagic[i]) {
27 return false;
28 }
29 }
30 return true;
31}
32} // namespace
33
35 : mbytesPerFrame(bytes_per_frame), mBaseBytesPerFrame(bytes_per_frame),
36 mType(kFile) {}
37
39
41 close();
42 if (!h || mBaseBytesPerFrame <= 0) {
43 return false;
44 }
45 mHandle = h;
51 mHasVideoColor = false;
52 mHasFledContainer = false;
56 mStreamingRejected = false;
57 // Probe seekability: if seek-to-start succeeds, this is a seekable file.
59 if (mType == kFile) {
60 // A file without FLED magic remains a legacy headerless RGB stream.
61 // Once magic is recognized, though, every invalidity is a container
62 // error: falling back would render its header bytes as LEDs.
63 if (mHandle->size() >= 4) {
64 char hdr[kFledHeaderBytes];
65 fl::size_t got = mHandle->read(hdr, kFledHeaderBytes);
66 const bool hasFledMagic = got >= 4
67 && static_cast<fl::u8>(hdr[0]) == kFledMagic[0]
68 && static_cast<fl::u8>(hdr[1]) == kFledMagic[1]
69 && static_cast<fl::u8>(hdr[2]) == kFledMagic[2]
70 && static_cast<fl::u8>(hdr[3]) == kFledMagic[3];
71 if (hasFledMagic) {
72 const bool supportedFled = got == kFledHeaderBytes
73 && static_cast<fl::u8>(hdr[4]) == kFledVersionV1
74 && static_cast<fl::u8>(hdr[6]) == 0
75 && static_cast<fl::u8>(hdr[7]) == 0
76 && fled::bytesPerLed(static_cast<fl::u8>(hdr[5])) != 0;
77 if (!supportedFled) {
78 close();
79 return false;
80 }
81 const fl::u8 pixelFormat = static_cast<fl::u8>(hdr[5]);
82 const fl::u32 jsonLen =
83 static_cast<fl::u32>(static_cast<fl::u8>(hdr[8]))
84 | (static_cast<fl::u32>(static_cast<fl::u8>(hdr[9])) << 8)
85 | (static_cast<fl::u32>(static_cast<fl::u8>(hdr[10])) << 16)
86 | (static_cast<fl::u32>(static_cast<fl::u8>(hdr[11])) << 24);
87 // Cap the JSON length against a defensive maximum BEFORE
88 // doing any size arithmetic — guards against a malformed
89 // file declaring a multi-gigabyte json_length that would
90 // either overflow the offset calc or trigger a huge resize.
91 const fl::size_t jsonLenSz = static_cast<fl::size_t>(jsonLen);
92 const fl::size_t fileSize = mHandle->size();
93 const bool jsonInRange = jsonLenSz <= kFledMaxJsonBytes
94 && jsonLenSz <= fileSize - kFledHeaderBytes;
95 if (jsonInRange) {
96 mEmbeddedScreenMapJson.resize(static_cast<fl::size>(jsonLenSz));
97 fl::size_t jr = jsonLenSz > 0
98 ? mHandle->read(&mEmbeddedScreenMapJson[0], jsonLenSz)
99 : 0;
100 if (jr == jsonLenSz) {
101 mPayloadOffset = kFledHeaderBytes + jsonLenSz;
102 mFledPixelFormat = pixelFormat;
103 mFledBytesPerLed = fled::bytesPerLed(pixelFormat);
104 mHasFledContainer = true;
105 if (mbytesPerFrame % 3 != 0) {
106 close();
107 return false;
108 }
109 const fl::i32 ledCount = mbytesPerFrame / 3;
110 const fl::i32 bytesPerLed =
111 static_cast<fl::i32>(mFledBytesPerLed);
112 if (ledCount > (fl::numeric_limits<fl::i32>::max)() /
113 bytesPerLed) {
114 close();
115 return false;
116 }
117 mbytesPerFrame = ledCount * bytesPerLed;
118 if ((fileSize - mPayloadOffset) %
119 static_cast<fl::size_t>(mbytesPerFrame) != 0) {
120 close();
121 return false;
122 }
123 fl::json envelope;
124 if (!mEmbeddedScreenMapJson.empty()) {
126 }
127 const bool validEnvelope = mEmbeddedScreenMapJson.empty() ||
128 (envelope.has_value() && envelope.is_object());
129 if (!validEnvelope) {
130 close();
131 return false;
132 }
133 const bool colorOk =
135 &mVideoColor) ==
137 mHasVideoColor = colorOk;
138 if (!colorOk) {
139 fled::PixelStorage storage;
140 const bool advisoryRgb8 = pixelStorage(&storage) &&
142 if (!mBestEffortFled || !advisoryRgb8) {
143 close();
144 return false;
145 }
146 FL_WARN("FLED video.color rejected; using explicit best-effort RGB8 playback");
147 }
148 // Stream is now positioned at the first frame byte.
149 return mHandle->available();
150 }
152 }
153 // FLED magic was recognized, so malformed/truncated
154 // containers must not become headerless raw RGB.
155 close();
156 return false;
157 }
158 // No magic: rewind so subsequent reads see raw RGB triplets.
159 mHandle->seek(0, seek_dir::beg);
160 }
161 return mHandle->available();
162 }
163 // A partial FLED prefix stays buffered until enough bytes arrive to
164 // classify it. A raw mismatch is replayed through readBytes().
166 if (!probeStreamingMagic()) {
167 close();
168 return false;
169 }
170 return true;
171}
172
187
190 return !mStreamingRejected;
191 }
193 const fl::size_t want = 4 - mStreamingPrefixSize;
195 want);
196 if (got == 0) {
197 break;
198 }
200 if (!isFledMagicPrefix(mStreamingPrefix, mStreamingPrefixSize)) {
202 return true;
203 }
204 }
205 if (mStreamingPrefixSize == 4) {
207 mStreamingRejected = true;
208 return false;
209 }
210 return true;
211}
212
214
216 if (!mHandle || !dst) {
217 return false;
218 }
219 if (!isRgb8Playback()) {
220 return false;
221 }
222 return readBytes(&dst->r, 3) == 3;
223}
224
226 if (!mHandle || !out) {
227 return false;
228 }
229 fled::PixelStorage storage;
231 if (!pixelStorage(&storage) || !videoColor(&color) ||
232 storage.mFormat != fl::PixelFormat::Rgb16 ||
234 return false;
235 }
236 fl::u8 bytes[6];
237 if (readBytes(bytes, sizeof(bytes)) != sizeof(bytes)) {
238 return false;
239 }
240 out->mStorage = storage;
241 out->mColor = color;
242 for (fl::u8 i = 0; i < 3; ++i) {
243 const fl::size_t offset = static_cast<fl::size_t>(i) * 2;
244 out->mComponents[i] = static_cast<fl::u16>(bytes[offset]) |
245 (static_cast<fl::u16>(bytes[offset + 1]) << 8);
246 }
247 return true;
248}
249
251 if (!mHandle) {
252 return false;
253 }
254 if (mType == kStreaming) {
256 return false;
257 }
259 mHandle->available(mbytesPerFrame);
260 }
261 return mHandle->available();
262}
263
265 if (!mHandle) {
266 return true;
267 }
268 if (mType == kStreaming) {
269 return false;
270 }
271 return !mHandle->available();
272}
273
275 if (!mHandle || !frame) {
276 return false;
277 }
278 if (mType == kFile && !framesRemaining()) {
279 return false;
280 }
281 if (!isRgb8Playback()) {
282 return false;
283 }
284 size_t n = 0;
285 const fl::size_t pixels = static_cast<fl::size_t>(mbytesPerFrame) / 3;
286 fl::span<CRGB> dst = frame->rgb();
287 for (; n < pixels && n < dst.size() && readPixel(&dst[n]); ++n) {
288 }
289 if (mType == kFile) {
290 DBG("pos: " << mHandle->pos());
291 }
292 return n * 3 == size_t(mbytesPerFrame);
293}
294
295bool PixelStream::hasFrame(fl::u32 frameNumber) FL_NO_EXCEPT {
296 if (!mHandle) {
297 return false;
298 }
299 if (mType == kStreaming) {
300 // Streaming handle doesn't support seeking
301 DBG("Not implemented and therefore always returns true");
302 return true;
303 }
304 // Use size_t throughout so frameNumber * bytesPerFrame doesn't overflow
305 // u32 for high-LED-count grids past ~1M frames.
306 fl::size_t total_bytes = mHandle->size();
307 fl::size_t frameBytes = static_cast<fl::size_t>(frameNumber)
308 * static_cast<fl::size_t>(mbytesPerFrame);
309 fl::size_t target = mPayloadOffset + frameBytes;
310 return target < total_bytes;
311}
312
313bool PixelStream::readFrameAt(fl::u32 frameNumber, Frame *frame) FL_NO_EXCEPT {
314 if (!mHandle || !frame || mType == kStreaming) {
315 // Streaming handle doesn't support seeking
316 FL_DBG("Streaming handle doesn't support seeking");
317 return false;
318 }
319 if (!isRgb8Playback()) {
320 return false;
321 }
322 fl::size_t frameBytes = static_cast<fl::size_t>(frameNumber)
323 * static_cast<fl::size_t>(mbytesPerFrame);
324 mHandle->seek(mPayloadOffset + frameBytes);
325 if (mHandle->bytesLeft() == 0) {
326 return false;
327 }
328 size_t read =
329 mHandle->readRGB8(frame->rgb()) * 3;
330
331 bool ok = int(read) == mbytesPerFrame;
332 if (!ok) {
333 DBG("readFrameAt failed - read: "
334 << read << ", mbytesPerFrame: " << mbytesPerFrame << ", frame:"
335 << frameNumber << ", left: " << mHandle->bytesLeft());
336 }
337 return ok;
338}
339
341 if (mbytesPerFrame == 0)
342 return 0;
343 i32 bytes_left = bytesRemaining();
344 if (bytes_left <= 0) {
345 return 0;
346 }
347 return bytes_left / mbytesPerFrame;
348}
349
351 if (!mHandle) {
352 return 0;
353 }
354 if (mType == kStreaming) {
355 return -1;
356 }
357 if (mbytesPerFrame == 0) {
358 return 0;
359 }
360 fl::size_t pos = mHandle->pos();
361 if (pos < mPayloadOffset) return 0;
362 return static_cast<i32>((pos - mPayloadOffset) / mbytesPerFrame);
363}
364
366 if (!mHandle) {
367 return 0;
368 }
369 if (mType == kStreaming) {
370 // Use (max)() to prevent macro expansion by Arduino.h's max macro
372 }
373 return mHandle->bytesLeft();
374}
375
377 if (mbytesPerFrame == 0) {
378 return 0;
379 }
381}
382
384 if (!mHandle || mType == kStreaming) {
385 return false;
386 }
387 // Rewind to the start of the payload, not the start of the file —
388 // skips the FLED header on container-formatted streams.
389 mHandle->seek(mPayloadOffset);
390 return true;
391}
392
396
400
404
406 if (!out || !mHasFledContainer || !mHasVideoColor) {
407 return false;
408 }
409 *out = mVideoColor;
410 return true;
411}
412
417
422
423size_t PixelStream::readBytes(u8 *dst, size_t len) FL_NO_EXCEPT {
424 if (!mHandle || !dst) {
425 return 0;
426 }
427 if (mType == kStreaming &&
429 return 0;
430 }
431 size_t bytesRead = 0;
432 while (bytesRead < len && mStreamingPrefixPos < mStreamingPrefixSize) {
433 dst[bytesRead++] = mStreamingPrefix[mStreamingPrefixPos++];
434 }
435 if (mType == kStreaming) {
436 while (bytesRead < len && mHandle->available(len - bytesRead)) {
437 if (mHandle->read(dst + bytesRead, 1)) {
438 bytesRead++;
439 } else {
440 break;
441 }
442 }
443 } else {
444 while (bytesRead < len && mHandle->available()) {
445 if (mHandle->read(dst + bytesRead, 1)) {
446 bytesRead++;
447 } else {
448 break;
449 }
450 }
451 }
452 return bytesRead;
453}
454
455} // namespace video
456} // namespace fl
uint8_t pos
Definition Blur.ino:11
fl::Video video(NUM_LEDS, 2.0f)
bool is_object() const FL_NO_EXCEPT
Definition json.h:391
bool has_value() const FL_NO_EXCEPT
Definition json.h:819
static json parse(const fl::string &txt) FL_NO_EXCEPT
Definition json.h:842
constexpr fl::size size() const FL_NO_EXCEPT
Definition span.h:483
i32 bytesRemainingInFrame() const FL_NO_EXCEPT
bool readFrame(Frame *frame) FL_NO_EXCEPT
fl::filebuf_ptr mHandle
PixelStream(int bytes_per_frame) FL_NO_EXCEPT
i32 bytesPerFrame() FL_NO_EXCEPT
i32 framesRemaining() const FL_NO_EXCEPT
bool readSample(PixelSample *out) FL_NO_EXCEPT
bool hasEmbeddedScreenMap() const FL_NO_EXCEPT
const fl::i32 mBaseBytesPerFrame
bool probeStreamingMagic() const FL_NO_EXCEPT
bool begin(fl::filebuf_ptr h) FL_NO_EXCEPT
bool readPixel(CRGB *dst) FL_NO_EXCEPT
const fl::string & embeddedScreenMapJson() const FL_NO_EXCEPT
bool pixelStorage(fled::PixelStorage *out) const FL_NO_EXCEPT
fl::size_t mStreamingPrefixPos
i32 bytesRemaining() const FL_NO_EXCEPT
size_t readBytes(u8 *dst, size_t len) FL_NO_EXCEPT
void close() FL_NO_EXCEPT
virtual ~PixelStream() FL_NO_EXCEPT
bool available() const FL_NO_EXCEPT
bool hasFrame(fl::u32 frameNumber) FL_NO_EXCEPT
Type getType() const FL_NO_EXCEPT
fl::size_t mStreamingPrefixSize
bool atEnd() const FL_NO_EXCEPT
fled::VideoColor mVideoColor
bool videoColor(fled::VideoColor *out) const FL_NO_EXCEPT
bool readFrameAt(fl::u32 frameNumber, Frame *frame) FL_NO_EXCEPT
fl::string mEmbeddedScreenMapJson
i32 framesDisplayed() const FL_NO_EXCEPT
bool rewind() FL_NO_EXCEPT
bool isRgb8Playback() const FL_NO_EXCEPT
fl::UISlider offset("Offset", 0.0f, 0.0f, 1.0f, 0.01f)
FastLED's Elegant JSON Library: fl::json
#define FL_WARN(...)
Definition log.h:437
#define FL_DBG
Definition log.h:553
Centralized logging categories for FastLED hardware interfaces and subsystems.
__SIZE_TYPE__ size_t
Definition s16x16x4.h:16
fl::u8 bytesPerLed(fl::u8 pixelFormat) FL_NO_EXCEPT
ColorStatus resolveVideoColor(const fl::json &envelope, fl::u8 pixelFormat, VideoColor *out) FL_NO_EXCEPT
bool toPixelStorage(PixelFormat fledFormat, PixelStorage *out) FL_NO_EXCEPT
ComponentByteOrder mComponentByteOrder
fl::PixelFormat mFormat
bool isFledMagicPrefix(const fl::u8 *bytes, fl::size_t size) FL_NO_EXCEPT
unsigned char u8
Definition stdint.h:131
u8 u8 fract8 blur_amount FL_NO_EXCEPT
Definition blur.h:186
int read() FL_NO_EXCEPT
fl::size_t size_t
Definition s16x16x4.h:62
fl::shared_ptr< filebuf > filebuf_ptr
Definition idecoder.h:15
int available() FL_NO_EXCEPT
Base definition for an LED controller.
Definition crgb.hpp:179
Representation of an 8-bit RGB pixel (Red, Green, Blue)
Definition crgb.h:38
static constexpr T max() FL_NO_EXCEPT
Definition limits.h:108
#define DBG
Definition time.cpp.hpp:7