initial commit

This commit is contained in:
2026-02-12 00:45:31 -08:00
commit 5f168f370b
3024 changed files with 804889 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
# FastLED FX: Video Subsystem
This folder implements a simple video playback pipeline for LED arrays. It reads frames from a file or stream, buffers them, and interpolates between them to produce smooth animation at your desired output rate.
### Building blocks
- **`PixelStream` (`pixel_stream.h`)**: Reads pixel data as bytes from either a file (`FileHandle`) or a live `ByteStream`. Knows the `bytesPerFrame`, can read by pixel, by frame, or at an absolute frame number. Reports availability and end-of-stream.
- **`FrameTracker` (`frame_tracker.h`)**: Converts wallclock time to frame numbers (current and next) at a fixed FPS. Also exposes exact timestamps and frame interval in microseconds.
- **`FrameInterpolator` (`frame_interpolator.h`)**: Holds a small history of frames and, given the current time, blends the nearest two frames to produce an inbetween result. Supports nonmonotonic time (e.g., pause/rewind, audio sync).
- **`VideoImpl` (`video_impl.h`)**: Highlevel orchestrator. Owns a `PixelStream` and a `FrameInterpolator`, manages fadein/out, time scaling, pause/resume, and draws into either a `Frame` or your `CRGB*` buffer.
### Typical flow
1. Create `VideoImpl` with your `pixelsPerFrame` and the source FPS.
2. Call `begin(...)` with a `FileHandle` or `ByteStream`.
3. Each frame, call `video.draw(now, leds)`.
- Internally maintains a buffer of recent frames.
- Interpolates between frames to match your output timing.
4. Use `setFade(...)`, `pause(...)`, `resume(...)`, and `setTimeScale(...)` as needed.
5. Call `end()` or `rewind()` to manage lifecycle.
### Notes
- Interpolation makes lowerFPS content look smooth on higherFPS refresh loops.
- For streaming sources, some random access features (e.g., `rewind`) may be limited.
- `durationMicros()` reports the full duration for file sources, and `-1` for streams.
This subsystem is optional, intended for MCUs with adequate RAM and I/O throughput.
### Examples
- Memory stream video (from `examples/FxGfx2Video/FxGfx2Video.ino`):
```cpp
#include <FastLED.h>
#include "fl/bytestreammemory.h"
#include "fx/video.h"
using namespace fl;
#define W 22
#define H 22
#define NUM_LEDS (W*H)
CRGB leds[NUM_LEDS];
ByteStreamMemoryPtr stream = fl::make_shared<ByteStreamMemory>(3*NUM_LEDS*2);
Video video(NUM_LEDS, 2.0f); // 2 fps source
void setup(){ FastLED.addLeds<WS2811,2,GRB>(leds, NUM_LEDS); video.beginStream(stream); }
void loop(){ video.draw(millis(), leds); FastLED.show(); }
```
- SD card video (from `examples/FxSdCard/FxSdCard.ino`):
```cpp
#include <FastLED.h>
#include "fx/video.h"
#include "fl/file_system.h"
using namespace fl;
#define W 32
#define H 32
#define NUM_LEDS (W*H)
CRGB leds[NUM_LEDS];
FileSystem fs;
Video video;
void setup(){
FastLED.addLeds<WS2811,2,GRB>(leds, NUM_LEDS);
video = fs.openVideo("data/video.rgb", NUM_LEDS, 60, 2);
}
void loop(){ video.draw(millis(), leds); FastLED.show(); }
```

View File

@@ -0,0 +1,51 @@
#include "fx/video/frame_interpolator.h"
#include "fl/circular_buffer.h"
#include "fl/math_macros.h"
#include "fl/namespace.h"
#include "fx/video/pixel_stream.h"
#include "fl/dbg.h"
#include "fl/math_macros.h"
#include <math.h>
#define DBG FASTLED_DBG
namespace fl {
FrameInterpolator::FrameInterpolator(size_t nframes, float fps)
: mFrameTracker(fps) {
size_t capacity = MAX(1, nframes);
mFrames.setMaxSize(capacity);
}
bool FrameInterpolator::draw(fl::u32 now, Frame *dst) {
bool ok = draw(now, dst->rgb());
return ok;
}
bool FrameInterpolator::draw(fl::u32 now, CRGB *leds) {
fl::u32 frameNumber, nextFrameNumber;
uint8_t amountOfNextFrame;
// DBG("now: " << now);
mFrameTracker.get_interval_frames(now, &frameNumber, &nextFrameNumber,
&amountOfNextFrame);
if (!has(frameNumber)) {
return false;
}
if (has(frameNumber) && !has(nextFrameNumber)) {
// just paint the current frame
Frame *frame = get(frameNumber).get();
frame->draw(leds);
return true;
}
Frame *frame1 = get(frameNumber).get();
Frame *frame2 = get(nextFrameNumber).get();
Frame::interpolate(*frame1, *frame2, amountOfNextFrame, leds);
return true;
}
} // namespace fl

View File

@@ -0,0 +1,107 @@
#pragma once
#include "fl/map.h"
#include "fl/namespace.h"
#include "fx/frame.h"
#include "fx/video/frame_tracker.h"
#include "fx/video/pixel_stream.h"
namespace fl {
FASTLED_SMART_PTR(FrameInterpolator);
// Holds onto frames and allow interpolation. This allows
// effects to have high effective frame rate and also
// respond to things like sound which can modify the timing.
class FrameInterpolator {
public:
struct Less {
bool operator()(fl::u32 a, fl::u32 b) const { return a < b; }
};
typedef fl::SortedHeapMap<fl::u32, FramePtr, Less> FrameBuffer;
FrameInterpolator(size_t nframes, float fpsVideo);
// Will search through the array, select the two frames that are closest to
// the current time and then interpolate between them, storing the results
// in the provided frame. The destination frame will have "now" as the
// current timestamp if and only if there are two frames that can be
// interpolated. Else it's set to the timestamp of the frame that was
// selected. Returns true if the interpolation was successful, false
// otherwise. If false then the destination frame will not be modified. Note
// that this adjustable_time is allowed to go pause or go backward in time.
bool draw(fl::u32 adjustable_time, Frame *dst);
bool draw(fl::u32 adjustable_time, CRGB *leds);
bool insert(fl::u32 frameNumber, FramePtr frame) {
InsertResult result;
mFrames.insert(frameNumber, frame, &result);
return result != InsertResult::kMaxSize;
}
// Clear all frames
void clear() { mFrames.clear(); }
bool empty() const { return mFrames.empty(); }
bool has(fl::u32 frameNum) const { return mFrames.has(frameNum); }
FramePtr erase(fl::u32 frameNum) {
FramePtr out;
auto it = mFrames.find(frameNum);
if (it == mFrames.end()) {
return out;
}
out = it->second;
mFrames.erase(it);
return out;
}
FramePtr get(fl::u32 frameNum) const {
auto it = mFrames.find(frameNum);
if (it != mFrames.end()) {
return it->second;
}
return FramePtr();
}
bool full() const { return mFrames.full(); }
size_t capacity() const { return mFrames.capacity(); }
FrameBuffer *getFrames() { return &mFrames; }
bool needsFrame(fl::u32 now, fl::u32 *currentFrameNumber,
fl::u32 *nextFrameNumber) const {
mFrameTracker.get_interval_frames(now, currentFrameNumber,
nextFrameNumber);
return !has(*currentFrameNumber) || !has(*nextFrameNumber);
}
bool get_newest_frame_number(fl::u32 *frameNumber) const {
if (mFrames.empty()) {
return false;
}
auto &front = mFrames.back();
*frameNumber = front.first;
return true;
}
bool get_oldest_frame_number(fl::u32 *frameNumber) const {
if (mFrames.empty()) {
return false;
}
auto &front = mFrames.front();
*frameNumber = front.first;
return true;
}
fl::u32 get_exact_timestamp_ms(fl::u32 frameNumber) const {
return mFrameTracker.get_exact_timestamp_ms(frameNumber);
}
FrameTracker &getFrameTracker() { return mFrameTracker; }
private:
FrameBuffer mFrames;
FrameTracker mFrameTracker;
};
} // namespace fl

View File

@@ -0,0 +1,52 @@
#include "frame_tracker.h"
#include "fl/int.h"
namespace fl {
namespace { // anonymous namespace
long linear_map(long x, long in_min, long in_max, long out_min, long out_max) {
const long run = in_max - in_min;
if (run == 0) {
return 0; // AVR returns -1, SAM returns 0
}
const long rise = out_max - out_min;
const long delta = x - in_min;
return (delta * rise) / run + out_min;
}
} // anonymous namespace
FrameTracker::FrameTracker(float fps) {
// Convert fps to microseconds per frame interval
mMicrosSecondsPerInterval = static_cast<fl::u32>(1000000.0f / fps + .5f);
}
void FrameTracker::get_interval_frames(fl::u32 now, fl::u32 *frameNumber,
fl::u32 *nextFrameNumber,
uint8_t *amountOfNextFrame) const {
// Account for any pause time
fl::u32 effectiveTime = now;
// Convert milliseconds to microseconds for precise calculation
fl::u64 microseconds = static_cast<fl::u64>(effectiveTime) * 1000ULL;
// Calculate frame number with proper rounding
*frameNumber = microseconds / mMicrosSecondsPerInterval;
*nextFrameNumber = *frameNumber + 1;
// Calculate interpolation amount if requested
if (amountOfNextFrame != nullptr) {
fl::u64 frame1_start = (*frameNumber * mMicrosSecondsPerInterval);
fl::u64 frame2_start = (*nextFrameNumber * mMicrosSecondsPerInterval);
fl::u32 rel_time = microseconds - frame1_start;
fl::u32 frame_duration = frame2_start - frame1_start;
uint8_t progress = uint8_t(linear_map(rel_time, 0, frame_duration, 0, 255));
*amountOfNextFrame = progress;
}
}
fl::u32 FrameTracker::get_exact_timestamp_ms(fl::u32 frameNumber) const {
fl::u64 microseconds = frameNumber * mMicrosSecondsPerInterval;
return static_cast<fl::u32>(microseconds / 1000) + mStartTime;
}
} // namespace fl

View File

@@ -0,0 +1,37 @@
#pragma once
#include "fl/stdint.h"
#include "fl/namespace.h"
#include "fl/int.h"
// #include <iostream>
// using namespace std;
namespace fl {
// Tracks the current frame number based on the time elapsed since the start of
// the animation.
class FrameTracker {
public:
FrameTracker(float fps);
// Gets the current frame and the next frame number based on the current
// time.
void get_interval_frames(fl::u32 now, fl::u32 *frameNumber,
fl::u32 *nextFrameNumber,
uint8_t *amountOfNextFrame = nullptr) const;
// Given a frame number, returns the exact timestamp in milliseconds that
// the frame should be displayed.
fl::u32 get_exact_timestamp_ms(fl::u32 frameNumber) const;
fl::u32 microsecondsPerFrame() const { return mMicrosSecondsPerInterval; }
private:
fl::u32 mMicrosSecondsPerInterval;
fl::u32 mStartTime = 0;
};
} // namespace fl

View File

@@ -0,0 +1,196 @@
#include "fx/video/pixel_stream.h"
#include "fl/dbg.h"
#include "fl/namespace.h"
#ifndef INT32_MAX
#define INT32_MAX 0x7fffffff
#endif
#define DBG FASTLED_DBG
using fl::ByteStreamPtr;
using fl::FileHandlePtr;
namespace fl {
PixelStream::PixelStream(int bytes_per_frame)
: mbytesPerFrame(bytes_per_frame), mUsingByteStream(false) {}
PixelStream::~PixelStream() { close(); }
bool PixelStream::begin(FileHandlePtr h) {
close();
mFileHandle = h;
mUsingByteStream = false;
return mFileHandle->available();
}
bool PixelStream::beginStream(ByteStreamPtr s) {
close();
mByteStream = s;
mUsingByteStream = true;
return mByteStream->available(mbytesPerFrame);
}
void PixelStream::close() {
if (!mUsingByteStream && mFileHandle) {
mFileHandle.reset();
}
mByteStream.reset();
mFileHandle.reset();
}
int32_t PixelStream::bytesPerFrame() { return mbytesPerFrame; }
bool PixelStream::readPixel(CRGB *dst) {
if (mUsingByteStream) {
return mByteStream->read(&dst->r, 1) && mByteStream->read(&dst->g, 1) &&
mByteStream->read(&dst->b, 1);
} else {
return mFileHandle->read(&dst->r, 1) && mFileHandle->read(&dst->g, 1) &&
mFileHandle->read(&dst->b, 1);
}
}
bool PixelStream::available() const {
if (mUsingByteStream) {
return mByteStream->available(mbytesPerFrame);
} else {
return mFileHandle->available();
}
}
bool PixelStream::atEnd() const {
if (mUsingByteStream) {
return false;
} else {
return !mFileHandle->available();
}
}
bool PixelStream::readFrame(Frame *frame) {
if (!frame) {
return false;
}
if (!mUsingByteStream) {
if (!framesRemaining()) {
return false;
}
size_t n = mFileHandle->readCRGB(frame->rgb(), mbytesPerFrame / 3);
DBG("pos: " << mFileHandle->pos());
return n * 3 == size_t(mbytesPerFrame);
}
size_t n = mByteStream->readCRGB(frame->rgb(), mbytesPerFrame / 3);
return n * 3 == size_t(mbytesPerFrame);
}
bool PixelStream::hasFrame(fl::u32 frameNumber) {
if (mUsingByteStream) {
// ByteStream doesn't support seeking
DBG("Not implemented and therefore always returns true");
return true;
} else {
size_t total_bytes = mFileHandle->size();
return frameNumber * mbytesPerFrame < total_bytes;
}
}
bool PixelStream::readFrameAt(fl::u32 frameNumber, Frame *frame) {
// DBG("read frame at " << frameNumber);
if (mUsingByteStream) {
// ByteStream doesn't support seeking
FASTLED_DBG("ByteStream doesn't support seeking");
return false;
} else {
// DBG("mbytesPerFrame: " << mbytesPerFrame);
mFileHandle->seek(frameNumber * mbytesPerFrame);
if (mFileHandle->bytesLeft() == 0) {
return false;
}
size_t read =
mFileHandle->readCRGB(frame->rgb(), mbytesPerFrame / 3) * 3;
// DBG("read: " << read);
// DBG("pos: " << mFileHandle->Position());
bool ok = int(read) == mbytesPerFrame;
if (!ok) {
DBG("readFrameAt failed - read: "
<< read << ", mbytesPerFrame: " << mbytesPerFrame << ", frame:"
<< frameNumber << ", left: " << mFileHandle->bytesLeft());
}
return ok;
}
}
int32_t PixelStream::framesRemaining() const {
if (mbytesPerFrame == 0)
return 0;
int32_t bytes_left = bytesRemaining();
if (bytes_left <= 0) {
return 0;
}
return bytes_left / mbytesPerFrame;
}
int32_t PixelStream::framesDisplayed() const {
if (mUsingByteStream) {
// ByteStream doesn't have a concept of total size, so we can't
// calculate this
return -1;
} else {
int32_t bytes_played = mFileHandle->pos();
return bytes_played / mbytesPerFrame;
}
}
int32_t PixelStream::bytesRemaining() const {
if (mUsingByteStream) {
return INT32_MAX;
} else {
return mFileHandle->bytesLeft();
}
}
int32_t PixelStream::bytesRemainingInFrame() const {
return bytesRemaining() % mbytesPerFrame;
}
bool PixelStream::rewind() {
if (mUsingByteStream) {
// ByteStream doesn't support rewinding
return false;
} else {
mFileHandle->seek(0);
return true;
}
}
PixelStream::Type PixelStream::getType() const {
return mUsingByteStream ? Type::kStreaming : Type::kFile;
}
size_t PixelStream::readBytes(uint8_t *dst, size_t len) {
uint16_t bytesRead = 0;
if (mUsingByteStream) {
while (bytesRead < len && mByteStream->available(len)) {
// use pop_front()
if (mByteStream->read(dst + bytesRead, 1)) {
bytesRead++;
} else {
break;
}
}
} else {
while (bytesRead < len && mFileHandle->available()) {
if (mFileHandle->read(dst + bytesRead, 1)) {
bytesRead++;
} else {
break;
}
}
}
return bytesRead;
}
} // namespace fl

View File

@@ -0,0 +1,63 @@
#pragma once
#include "crgb.h"
#include "fl/bytestream.h"
#include "fl/file_system.h"
#include "fl/namespace.h"
#include "fl/memory.h"
#include "fx/frame.h"
#include "fl/int.h"
namespace fl {
FASTLED_SMART_PTR(FileHandle);
FASTLED_SMART_PTR(ByteStream);
} // namespace fl
namespace fl {
FASTLED_SMART_PTR(PixelStream);
// PixelStream takes either a file handle or a byte stream
// and reads frames from it in order to serve data to the
// video system.
class PixelStream {
public:
enum Type {
kStreaming,
kFile,
};
explicit PixelStream(int bytes_per_frame);
bool begin(fl::FileHandlePtr h);
bool beginStream(fl::ByteStreamPtr s);
void close();
int32_t bytesPerFrame();
bool readPixel(CRGB *dst); // Convenience function to read a pixel
size_t readBytes(uint8_t *dst, size_t len);
bool readFrame(Frame *frame);
bool readFrameAt(fl::u32 frameNumber, Frame *frame);
bool hasFrame(fl::u32 frameNumber);
int32_t framesRemaining() const; // -1 if this is a stream.
int32_t framesDisplayed() const;
bool available() const;
bool atEnd() const;
int32_t bytesRemaining() const;
int32_t bytesRemainingInFrame() const;
bool
rewind(); // Returns false on failure, which can happen for streaming mode.
Type getType()
const; // Returns the type of the video stream (kStreaming or kFile)
private:
fl::i32 mbytesPerFrame;
fl::FileHandlePtr mFileHandle;
fl::ByteStreamPtr mByteStream;
bool mUsingByteStream;
public:
virtual ~PixelStream();
};
} // namespace fl

View File

@@ -0,0 +1,355 @@
#include "video_impl.h"
#include "fl/assert.h"
#include "fl/math_macros.h"
#include "fl/namespace.h"
#include "fl/warn.h"
namespace fl {
VideoImpl::VideoImpl(size_t pixelsPerFrame, float fpsVideo,
size_t nFramesInBuffer)
: mPixelsPerFrame(pixelsPerFrame),
mFrameInterpolator(
fl::make_shared<FrameInterpolator>(MAX(1, nFramesInBuffer), fpsVideo)) {}
void VideoImpl::pause(fl::u32 now) {
if (!mTime) {
mTime = fl::make_shared<TimeWarp>(now);
}
mTime->pause(now);
}
void VideoImpl::resume(fl::u32 now) {
if (!mTime) {
mTime = fl::make_shared<TimeWarp>(now);
}
mTime->resume(now);
}
void VideoImpl::setTimeScale(float timeScale) {
mTimeScale = timeScale;
if (mTime) {
mTime->setSpeed(timeScale);
}
}
void VideoImpl::setFade(fl::u32 fadeInTime, fl::u32 fadeOutTime) {
mFadeInTime = fadeInTime;
mFadeOutTime = fadeOutTime;
}
bool VideoImpl::needsFrame(fl::u32 now) const {
fl::u32 f1, f2;
bool out = mFrameInterpolator->needsFrame(now, &f1, &f2);
return out;
}
VideoImpl::~VideoImpl() { end(); }
void VideoImpl::begin(FileHandlePtr h) {
end();
// Removed setStartTime call
mStream = fl::make_shared<PixelStream>(mPixelsPerFrame * kSizeRGB8);
mStream->begin(h);
mPrevNow = 0;
}
void VideoImpl::beginStream(ByteStreamPtr bs) {
end();
mStream = fl::make_shared<PixelStream>(mPixelsPerFrame * kSizeRGB8);
// Removed setStartTime call
mStream->beginStream(bs);
mPrevNow = 0;
}
void VideoImpl::end() {
mFrameInterpolator->clear();
// Removed resetFrameCounter and setStartTime calls
mStream.reset();
}
bool VideoImpl::full() const { return mFrameInterpolator->getFrames()->full(); }
bool VideoImpl::draw(fl::u32 now, Frame *frame) {
return draw(now, frame->rgb());
}
int32_t VideoImpl::durationMicros() const {
if (!mStream) {
return -1;
}
int32_t frames = mStream->framesRemaining();
if (frames < 0) {
return -1; // Stream case, duration unknown
}
fl::u32 micros_per_frame =
mFrameInterpolator->getFrameTracker().microsecondsPerFrame();
return (frames * micros_per_frame); // Convert to milliseconds
}
bool VideoImpl::draw(fl::u32 now, CRGB *leds) {
if (!mTime) {
mTime = fl::make_shared<TimeWarp>(now);
mTime->setSpeed(mTimeScale);
mTime->reset(now);
}
now = mTime->update(now);
if (!mStream) {
FASTLED_WARN("no stream");
return false;
}
bool ok = updateBufferIfNecessary(mPrevNow, now);
mPrevNow = now;
if (!ok) {
FASTLED_WARN("updateBufferIfNecessary failed");
return false;
}
mFrameInterpolator->draw(now, leds);
fl::u32 time = mTime->time();
fl::u32 brightness = 255;
// Compute fade in/out brightness.
if (mFadeInTime || mFadeOutTime) {
brightness = 255;
if (time <= mFadeInTime) {
if (mFadeInTime == 0) {
brightness = 255;
} else {
brightness = time * 255 / mFadeInTime;
}
} else if (mFadeOutTime) {
int32_t frames_remaining = mStream->framesRemaining();
if (frames_remaining < 0) {
// -1 means this is a stream.
brightness = 255;
} else {
FrameTracker &frame_tracker =
mFrameInterpolator->getFrameTracker();
fl::u32 micros_per_frame =
frame_tracker.microsecondsPerFrame();
fl::u32 millis_left =
(frames_remaining * micros_per_frame) / 1000;
if (millis_left < mFadeOutTime) {
brightness = millis_left * 255 / mFadeOutTime;
}
}
}
}
if (brightness < 255) {
if (brightness == 0) {
for (size_t i = 0; i < mPixelsPerFrame; ++i) {
leds[i] = CRGB::Black;
}
} else {
for (size_t i = 0; i < mPixelsPerFrame; ++i) {
leds[i].nscale8(brightness);
}
}
}
return true;
}
bool VideoImpl::updateBufferFromStream(fl::u32 now) {
FASTLED_ASSERT(mTime, "mTime is null");
if (!mStream) {
FASTLED_WARN("no stream");
return false;
}
if (mStream->atEnd()) {
return false;
}
fl::u32 currFrameNumber = 0;
fl::u32 nextFrameNumber = 0;
bool needs_frame =
mFrameInterpolator->needsFrame(now, &currFrameNumber, &nextFrameNumber);
if (!needs_frame) {
return true;
}
if (mFrameInterpolator->capacity() == 0) {
FASTLED_WARN("capacity == 0");
return false;
}
const bool has_current_frame = mFrameInterpolator->has(currFrameNumber);
const bool has_next_frame = mFrameInterpolator->has(nextFrameNumber);
fl::FixedVector<fl::u32, 2> frame_numbers;
if (!has_current_frame) {
frame_numbers.push_back(currFrameNumber);
}
size_t capacity = mFrameInterpolator->capacity();
if (capacity > 1 && !has_next_frame) {
frame_numbers.push_back(nextFrameNumber);
}
for (size_t i = 0; i < frame_numbers.size(); ++i) {
FramePtr recycled_frame;
if (mFrameInterpolator->full()) {
fl::u32 frame_to_erase = 0;
bool ok =
mFrameInterpolator->get_oldest_frame_number(&frame_to_erase);
if (!ok) {
FASTLED_WARN("get_oldest_frame_number failed");
return false;
}
recycled_frame = mFrameInterpolator->erase(frame_to_erase);
if (!recycled_frame) {
FASTLED_WARN("erase failed for frame: " << frame_to_erase);
return false;
}
}
fl::u32 frame_to_fetch = frame_numbers[i];
if (!recycled_frame) {
// Happens when we are not full and we need to allocate a new frame.
recycled_frame = fl::make_shared<Frame>(mPixelsPerFrame);
}
if (!mStream->readFrame(recycled_frame.get())) {
if (mStream->atEnd()) {
if (!mStream->rewind()) {
FASTLED_WARN("rewind failed");
return false;
}
mTime->reset(now);
frame_to_fetch = 0;
if (!mStream->readFrameAt(frame_to_fetch,
recycled_frame.get())) {
FASTLED_WARN("readFrameAt failed");
return false;
}
} else {
FASTLED_WARN("We failed for some other reason");
return false;
}
}
bool ok = mFrameInterpolator->insert(frame_to_fetch, recycled_frame);
if (!ok) {
FASTLED_WARN("insert failed");
return false;
}
}
return true;
}
bool VideoImpl::updateBufferFromFile(fl::u32 now, bool forward) {
fl::u32 currFrameNumber = 0;
fl::u32 nextFrameNumber = 0;
bool needs_frame =
mFrameInterpolator->needsFrame(now, &currFrameNumber, &nextFrameNumber);
if (!needs_frame) {
return true;
}
bool has_curr_frame = mFrameInterpolator->has(currFrameNumber);
bool has_next_frame = mFrameInterpolator->has(nextFrameNumber);
if (has_curr_frame && has_next_frame) {
return true;
}
if (mFrameInterpolator->capacity() == 0) {
FASTLED_WARN("capacity == 0");
return false;
}
fl::FixedVector<fl::u32, 2> frame_numbers;
if (!mFrameInterpolator->has(currFrameNumber)) {
frame_numbers.push_back(currFrameNumber);
}
if (mFrameInterpolator->capacity() > 1 &&
!mFrameInterpolator->has(nextFrameNumber)) {
frame_numbers.push_back(nextFrameNumber);
}
for (size_t i = 0; i < frame_numbers.size(); ++i) {
FramePtr recycled_frame;
if (mFrameInterpolator->full()) {
fl::u32 frame_to_erase = 0;
bool ok = false;
if (forward) {
ok = mFrameInterpolator->get_oldest_frame_number(
&frame_to_erase);
if (!ok) {
FASTLED_WARN("get_oldest_frame_number failed");
return false;
}
} else {
ok = mFrameInterpolator->get_newest_frame_number(
&frame_to_erase);
if (!ok) {
FASTLED_WARN("get_newest_frame_number failed");
return false;
}
}
recycled_frame = mFrameInterpolator->erase(frame_to_erase);
if (!recycled_frame) {
FASTLED_WARN("erase failed for frame: " << frame_to_erase);
return false;
}
}
fl::u32 frame_to_fetch = frame_numbers[i];
if (!recycled_frame) {
// Happens when we are not full and we need to allocate a new frame.
recycled_frame = fl::make_shared<Frame>(mPixelsPerFrame);
}
do { // only to use break
if (!mStream->readFrameAt(frame_to_fetch, recycled_frame.get())) {
if (!forward) {
// nothing more we can do, we can't go negative.
return false;
}
if (mStream->atEnd()) {
if (!mStream->rewind()) { // Is this still
FASTLED_WARN("rewind failed");
return false;
}
mTime->reset(now);
frame_to_fetch = 0;
if (!mStream->readFrameAt(frame_to_fetch,
recycled_frame.get())) {
FASTLED_WARN("readFrameAt failed");
return false;
}
break; // we have the frame, so we can break out of the loop
}
FASTLED_WARN("We failed for some other reason");
return false;
}
break;
} while (false);
bool ok = mFrameInterpolator->insert(frame_to_fetch, recycled_frame);
if (!ok) {
FASTLED_WARN("insert failed");
return false;
}
}
return true;
}
bool VideoImpl::updateBufferIfNecessary(fl::u32 prev, fl::u32 now) {
const bool forward = now >= prev;
PixelStream::Type type = mStream->getType();
switch (type) {
case PixelStream::kFile:
return updateBufferFromFile(now, forward);
case PixelStream::kStreaming:
return updateBufferFromStream(now);
default:
FASTLED_WARN("Unknown type: " << fl::u32(type));
return false;
}
}
bool VideoImpl::rewind() {
if (!mStream || !mStream->rewind()) {
return false;
}
mFrameInterpolator->clear();
return true;
}
} // namespace fl

View File

@@ -0,0 +1,65 @@
#pragma once
#include "fl/bytestream.h"
#include "fl/file_system.h"
#include "fx/video/frame_interpolator.h"
#include "fx/video/pixel_stream.h"
#include "fl/stdint.h"
#include "fl/namespace.h"
namespace fl {
FASTLED_SMART_PTR(FileHandle);
FASTLED_SMART_PTR(ByteStream);
} // namespace fl
namespace fl {
FASTLED_SMART_PTR(VideoImpl);
FASTLED_SMART_PTR(FrameInterpolator);
FASTLED_SMART_PTR(PixelStream)
class VideoImpl {
public:
enum {
kSizeRGB8 = 3,
};
// frameHistoryCount is the number of frames to keep in the buffer after
// draw. This allows for time based effects like syncing video speed to
// audio triggers.
VideoImpl(size_t pixelsPerFrame, float fpsVideo,
size_t frameHistoryCount = 0);
~VideoImpl();
// Api
void begin(fl::FileHandlePtr h);
void beginStream(fl::ByteStreamPtr s);
void setFade(fl::u32 fadeInTime, fl::u32 fadeOutTime);
bool draw(fl::u32 now, CRGB *leds);
void end();
bool rewind();
// internal use
bool draw(fl::u32 now, Frame *frame);
bool full() const;
void setTimeScale(float timeScale);
float timeScale() const { return mTimeScale; }
size_t pixelsPerFrame() const { return mPixelsPerFrame; }
void pause(fl::u32 now);
void resume(fl::u32 now);
bool needsFrame(fl::u32 now) const;
int32_t durationMicros() const; // -1 if this is a stream.
private:
bool updateBufferIfNecessary(fl::u32 prev, fl::u32 now);
bool updateBufferFromFile(fl::u32 now, bool forward);
bool updateBufferFromStream(fl::u32 now);
fl::u32 mPixelsPerFrame = 0;
PixelStreamPtr mStream;
fl::u32 mPrevNow = 0;
FrameInterpolatorPtr mFrameInterpolator;
TimeWarpPtr mTime;
fl::u32 mFadeInTime = 1000;
fl::u32 mFadeOutTime = 1000;
float mTimeScale = 1.0f;
};
} // namespace fl