initial commit
This commit is contained in:
60
libraries/FastLED/src/fx/video/README.md
Normal file
60
libraries/FastLED/src/fx/video/README.md
Normal 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 wall‑clock 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 in‑between result. Supports non‑monotonic time (e.g., pause/rewind, audio sync).
|
||||
- **`VideoImpl` (`video_impl.h`)**: High‑level orchestrator. Owns a `PixelStream` and a `FrameInterpolator`, manages fade‑in/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 lower‑FPS content look smooth on higher‑FPS 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(); }
|
||||
```
|
||||
51
libraries/FastLED/src/fx/video/frame_interpolator.cpp
Normal file
51
libraries/FastLED/src/fx/video/frame_interpolator.cpp
Normal 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
|
||||
107
libraries/FastLED/src/fx/video/frame_interpolator.h
Normal file
107
libraries/FastLED/src/fx/video/frame_interpolator.h
Normal 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
|
||||
52
libraries/FastLED/src/fx/video/frame_tracker.cpp
Normal file
52
libraries/FastLED/src/fx/video/frame_tracker.cpp
Normal 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
|
||||
37
libraries/FastLED/src/fx/video/frame_tracker.h
Normal file
37
libraries/FastLED/src/fx/video/frame_tracker.h
Normal 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
|
||||
196
libraries/FastLED/src/fx/video/pixel_stream.cpp
Normal file
196
libraries/FastLED/src/fx/video/pixel_stream.cpp
Normal 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
|
||||
63
libraries/FastLED/src/fx/video/pixel_stream.h
Normal file
63
libraries/FastLED/src/fx/video/pixel_stream.h
Normal 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
|
||||
355
libraries/FastLED/src/fx/video/video_impl.cpp
Normal file
355
libraries/FastLED/src/fx/video/video_impl.cpp
Normal 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
|
||||
65
libraries/FastLED/src/fx/video/video_impl.h
Normal file
65
libraries/FastLED/src/fx/video/video_impl.h
Normal 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
|
||||
Reference in New Issue
Block a user