diff --git a/src/Qt/VideoCacheThread.cpp b/src/Qt/VideoCacheThread.cpp index e850b340..4697ffe8 100644 --- a/src/Qt/VideoCacheThread.cpp +++ b/src/Qt/VideoCacheThread.cpp @@ -27,7 +27,7 @@ namespace openshot : Thread("video-cache") , speed(0) , last_speed(1) - , last_dir(1) // Assume forward (+1) on first launch + , last_dir(1) // assume forward (+1) on first launch , is_playing(false) , userSeeked(false) , requested_display_frame(1) @@ -36,10 +36,9 @@ namespace openshot , min_frames_ahead(4) , max_frames_ahead(8) , timeline_max_frame(0) - , should_pause_cache(false) - , should_break(false) , reader(nullptr) , force_directional_cache(false) + , last_cached_index(0) { } @@ -48,37 +47,13 @@ namespace openshot { } - // Seek the reader to a particular frame number - void VideoCacheThread::setSpeed(int new_speed) - { - // Only update last_speed and last_dir when new_speed is non-zero. - if (new_speed != 0) { - last_speed = new_speed; - last_dir = (new_speed > 0 ? 1 : -1); - } - speed = new_speed; - } - - // Get the size in bytes of a frame (rough estimate) - int64_t VideoCacheThread::getBytes(int width, int height, int sample_rate, int channels, float fps) - { - // Estimate memory for RGBA video frame - int64_t bytes = static_cast(width) * height * sizeof(char) * 4; - - // Approximate audio memory: (sample_rate * channels) samples per second, - // divided across fps frames, each sample is sizeof(float). - bytes += ((sample_rate * channels) / fps) * sizeof(float); - - return bytes; - } - // Play the video void VideoCacheThread::Play() { is_playing = true; } - // Stop the audio + // Stop the video void VideoCacheThread::Stop() { is_playing = false; @@ -87,146 +62,31 @@ namespace openshot // Is cache ready for playback (pre-roll) bool VideoCacheThread::isReady() { - // Return true when pre-roll has cached at least min_frames_ahead frames. return (cached_frame_count > min_frames_ahead); } - // Start the thread - void VideoCacheThread::run() + void VideoCacheThread::setSpeed(int new_speed) { - using micro_sec = std::chrono::microseconds; - using double_micro_sec = std::chrono::duration; - - // Index of the most recently cached frame; starts “behind” the playhead. - int64_t last_cached_index = 0; - - while (!threadShouldExit()) { - Settings* settings = Settings::Instance(); - CacheBase* cache = reader ? reader->GetCache() : nullptr; - - // If caching is disabled or no reader is assigned, wait briefly and retry - if (!settings->ENABLE_PLAYBACK_CACHING || !cache) { - std::this_thread::sleep_for(double_micro_sec(50000)); - continue; - } - - Timeline* timeline = static_cast(reader); - int64_t timeline_end = timeline->GetMaxFrame(); - int64_t playhead = requested_display_frame; - bool paused = (speed == 0); - - // Determine effective direction: use speed if non-zero, otherwise keep last_dir. - int dir = (speed != 0 ? (speed > 0 ? 1 : -1) : last_dir); - - // On any non-seek iteration, update last_dir if speed changed from zero - if (speed != 0) { - last_dir = dir; - } - - // Handle user-initiated seek: reset last_cached_index to just behind playhead. - if (userSeeked) { - last_cached_index = playhead - dir; - userSeeked = false; - } - - // Determine how many frames ahead/behind to cache based on settings & memory - int64_t bytes_per_frame = getBytes( - (timeline->preview_width ? timeline->preview_width : reader->info.width), - (timeline->preview_height ? timeline->preview_height : reader->info.height), - reader->info.sample_rate, - reader->info.channels, - reader->info.fps.ToFloat() - ); - - int64_t max_bytes = cache->GetMaxBytes(); - if (max_bytes <= 0 || bytes_per_frame <= 0) { - std::this_thread::sleep_for(double_micro_sec(50000)); - continue; - } - - int64_t capacity = max_bytes / bytes_per_frame; - if (capacity < 1) { - std::this_thread::sleep_for(double_micro_sec(50000)); - continue; - } - - // Number of frames to keep ahead (or behind) based on settings - int64_t ahead_count = static_cast(capacity * settings->VIDEO_CACHE_PERCENT_AHEAD); - - // Compute window bounds around playhead each iteration: - // - If moving forward (dir > 0): [playhead ... playhead + ahead_count] - // - If moving backward (dir < 0): [playhead - ahead_count ... playhead] - int64_t window_begin = (dir > 0) - ? playhead - : (playhead - ahead_count); - int64_t window_end = (dir > 0) - ? (playhead + ahead_count) - : playhead; - - // Clamp to valid timeline range - window_begin = std::max(window_begin, 1); - window_end = std::min(window_end, timeline_end); - - // If we're paused and the playhead moves outside cache, clear & rebuild - if (paused && !cache->Contains(playhead)) { - timeline->ClearAllCache(); - last_cached_index = playhead - dir; - } - - // If playing, ensure last_cached_index is within one step of window - // If it's already beyond the window, reset so caching continues from playhead - bool outside_window = (dir > 0 && last_cached_index > window_end) || - (dir < 0 && last_cached_index < window_begin); - if (!paused && outside_window) { - last_cached_index = playhead - dir; - } - - // Prefetch frames from last_cached_index + dir up to window_end (or down to window_begin) - int64_t next_frame = last_cached_index + dir; - bool window_full = true; - - while ((dir > 0 && next_frame <= window_end) || - (dir < 0 && next_frame >= window_begin)) - { - if (threadShouldExit()) { - break; - } - // If a new seek arrives mid-cache, break and start over next loop - if (userSeeked) { - break; - } - - if (!cache->Contains(next_frame)) { - // Missing frame: fetch and add to cache - try { - auto framePtr = reader->GetFrame(next_frame); - cache->Add(framePtr); - ++cached_frame_count; - } - catch (const OutOfBoundsFrame&) { - break; - } - window_full = false; - } - else { - cache->Touch(next_frame); - } - - last_cached_index = next_frame; - next_frame += dir; - } - - // If paused and the window was already filled, just touch playhead to keep it fresh - if (paused && window_full) { - cache->Touch(playhead); - } - - // Short sleep to throttle CPU (quarter-frame interval) - int64_t sleep_us = static_cast( - 1000000.0 / reader->info.fps.ToFloat() / 4.0 - ); - std::this_thread::sleep_for(double_micro_sec(sleep_us)); + // Only update last_speed and last_dir when new_speed != 0 + if (new_speed != 0) { + last_speed = new_speed; + last_dir = (new_speed > 0 ? 1 : -1); } + speed = new_speed; + } + + // Get the size in bytes of a frame (rough estimate) + int64_t VideoCacheThread::getBytes(int width, + int height, + int sample_rate, + int channels, + float fps) + { + // RGBA video frame + int64_t bytes = static_cast(width) * height * sizeof(char) * 4; + // Approximate audio: (sample_rate * channels)/fps samples per frame + bytes += ((sample_rate * channels) / fps) * sizeof(float); + return bytes; } void VideoCacheThread::Seek(int64_t new_position, bool start_preroll) @@ -242,4 +102,215 @@ namespace openshot Seek(new_position, false); } + int VideoCacheThread::computeDirection() const + { + // If speed ≠ 0, use its sign; if speed==0, keep last_dir + return (speed != 0 ? (speed > 0 ? 1 : -1) : last_dir); + } + + void VideoCacheThread::handleUserSeek(int64_t playhead, int dir) + { + // Place last_cached_index just “behind” playhead in the given dir + last_cached_index = playhead - dir; + } + + bool VideoCacheThread::clearCacheIfPaused(int64_t playhead, + bool paused, + CacheBase* cache) + { + if (paused && !cache->Contains(playhead)) { + // If paused and playhead not in cache, clear everything + Timeline* timeline = static_cast(reader); + timeline->ClearAllCache(); + return true; + } + return false; + } + + void VideoCacheThread::computeWindowBounds(int64_t playhead, + int dir, + int64_t ahead_count, + int64_t timeline_end, + int64_t& window_begin, + int64_t& window_end) const + { + if (dir > 0) { + // Forward window: [playhead ... playhead + ahead_count] + window_begin = playhead; + window_end = playhead + ahead_count; + } + else { + // Backward window: [playhead - ahead_count ... playhead] + window_begin = playhead - ahead_count; + window_end = playhead; + } + // Clamp to [1 ... timeline_end] + window_begin = std::max(window_begin, 1); + window_end = std::min(window_end, timeline_end); + } + + bool VideoCacheThread::prefetchWindow(CacheBase* cache, + int64_t window_begin, + int64_t window_end, + int dir, + ReaderBase* reader) + { + bool window_full = true; + int64_t next_frame = last_cached_index + dir; + + // Advance from last_cached_index toward window boundary + while ((dir > 0 && next_frame <= window_end) || + (dir < 0 && next_frame >= window_begin)) + { + if (threadShouldExit()) { + break; + } + // If a Seek was requested mid-caching, bail out immediately + if (userSeeked) { + break; + } + + if (!cache->Contains(next_frame)) { + // Frame missing, fetch and add + try { + auto framePtr = reader->GetFrame(next_frame); + cache->Add(framePtr); + ++cached_frame_count; + } + catch (const OutOfBoundsFrame&) { + break; + } + window_full = false; + } + else { + cache->Touch(next_frame); + } + + last_cached_index = next_frame; + next_frame += dir; + } + + return window_full; + } + + void VideoCacheThread::run() + { + using micro_sec = std::chrono::microseconds; + using double_micro_sec = std::chrono::duration; + + while (!threadShouldExit()) { + Settings* settings = Settings::Instance(); + CacheBase* cache = reader ? reader->GetCache() : nullptr; + + // If caching disabled or no reader, sleep briefly + if (!settings->ENABLE_PLAYBACK_CACHING || !cache) { + std::this_thread::sleep_for(double_micro_sec(50000)); + continue; + } + + Timeline* timeline = static_cast(reader); + int64_t timeline_end = timeline->GetMaxFrame(); + int64_t playhead = requested_display_frame; + bool paused = (speed == 0); + + // Compute effective direction (±1) + int dir = computeDirection(); + if (speed != 0) { + last_dir = dir; + } + + // If a seek was requested, reset last_cached_index + if (userSeeked) { + handleUserSeek(playhead, dir); + userSeeked = false; + } + else if (!paused) { + // Check if last_cached_index drifted outside the new window; if so, reset it + int64_t bytes_per_frame = getBytes( + (timeline->preview_width ? timeline->preview_width : reader->info.width), + (timeline->preview_height ? timeline->preview_height : reader->info.height), + reader->info.sample_rate, + reader->info.channels, + reader->info.fps.ToFloat() + ); + int64_t max_bytes = cache->GetMaxBytes(); + if (max_bytes > 0 && bytes_per_frame > 0) { + int64_t capacity = max_bytes / bytes_per_frame; + if (capacity >= 1) { + int64_t ahead_count = static_cast(capacity * + settings->VIDEO_CACHE_PERCENT_AHEAD); + int64_t window_begin, window_end; + computeWindowBounds(playhead, + dir, + ahead_count, + timeline_end, + window_begin, + window_end); + + bool outside_window = + (dir > 0 && last_cached_index > window_end) || + (dir < 0 && last_cached_index < window_begin); + if (outside_window) { + handleUserSeek(playhead, dir); + } + } + } + } + + // Recompute capacity & ahead_count now that we’ve possibly updated last_cached_index + int64_t bytes_per_frame = getBytes( + (timeline->preview_width ? timeline->preview_width : reader->info.width), + (timeline->preview_height ? timeline->preview_height : reader->info.height), + reader->info.sample_rate, + reader->info.channels, + reader->info.fps.ToFloat() + ); + int64_t max_bytes = cache->GetMaxBytes(); + if (max_bytes <= 0 || bytes_per_frame <= 0) { + std::this_thread::sleep_for(double_micro_sec(50000)); + continue; + } + int64_t capacity = max_bytes / bytes_per_frame; + if (capacity < 1) { + std::this_thread::sleep_for(double_micro_sec(50000)); + continue; + } + int64_t ahead_count = static_cast(capacity * + settings->VIDEO_CACHE_PERCENT_AHEAD); + + // If paused and playhead is no longer in cache, clear everything + bool did_clear = clearCacheIfPaused(playhead, paused, cache); + if (did_clear) { + handleUserSeek(playhead, dir); + } + + // Compute the current caching window + int64_t window_begin, window_end; + computeWindowBounds(playhead, + dir, + ahead_count, + timeline_end, + window_begin, + window_end); + + // Attempt to fill any missing frames in that window + bool window_full = prefetchWindow(cache, + window_begin, + window_end, + dir, + reader); + + // If paused and window was already full, keep playhead fresh + if (paused && window_full) { + cache->Touch(playhead); + } + + // Sleep a short fraction of a frame interval + int64_t sleep_us = static_cast( + 1000000.0 / reader->info.fps.ToFloat() / 4.0 + ); + std::this_thread::sleep_for(double_micro_sec(sleep_us)); + } + } + } // namespace openshot diff --git a/src/Qt/VideoCacheThread.h b/src/Qt/VideoCacheThread.h index 6c40659a..fc2c944f 100644 --- a/src/Qt/VideoCacheThread.h +++ b/src/Qt/VideoCacheThread.h @@ -17,123 +17,161 @@ #include #include +#include namespace openshot { using juce::Thread; - using juce::WaitableEvent; /** * @brief Handles prefetching and caching of video/audio frames for smooth playback. * - * This thread maintains a rolling cache window in the current playback direction (forward or backward). - * When paused, it continues to build the cache in the last known direction. On seek, it resets the window. + * This thread continuously maintains a “window” of cached frames in the current playback + * direction (forward or backward). When paused, it continues to fill that same window; + * when seeking, it resets to cache around the new position. */ class VideoCacheThread : public Thread { public: /// Constructor: initializes member variables and assumes forward direction on first launch. VideoCacheThread(); - - /// Destructor. ~VideoCacheThread() override; - /// Returns true if enough frames are cached for playback (pre-roll completed). + /// @return True if at least min_frames_ahead frames have been cached. bool isReady(); - /// Starts playback by setting is_playing to true. + /// Set is_playing = true, so run() will begin caching/playback. void Play(); - /// Stops playback by setting is_playing to false. + /// Set is_playing = false, effectively pausing playback (caching still runs). void Stop(); /** - * @brief Set the playback speed and direction. - * @param new_speed Positive values for forward play, negative for rewind, zero to pause. + * @brief Set playback speed/direction. Positive = forward, negative = rewind, zero = pause. + * @param new_speed * - * When new_speed != 0, last_speed and last_dir are updated. If new_speed is zero, - * last_dir remains unchanged so that pausing does not flip the cache direction. + * If new_speed != 0, last_speed and last_dir are updated. + * If new_speed == 0, last_dir is left unchanged so that pausing does not flip direction. */ void setSpeed(int new_speed); - /// Returns the current speed setting (1=normal, 2=fast, -1=rewind, etc.). + /// @return The current speed (1=normal, 2=fast, –1=rewind, etc.) int getSpeed() const { return speed; } - /** - * @brief Seek to a specific frame without pre-roll. - * @param new_position Frame index to seek to. - */ + /// Seek to a specific frame (no preroll). void Seek(int64_t new_position); /** - * @brief Seek to a specific frame and optionally start pre-roll. - * @param new_position Frame index to seek to. - * @param start_preroll If true, signals the thread to rebuild cache from new_position. + * @brief Seek to a specific frame and optionally start a preroll (cache reset). + * @param new_position Frame index to jump to. + * @param start_preroll If true, forces cache to rebuild around new_position. */ void Seek(int64_t new_position, bool start_preroll); /** - * @brief Assigns the ReaderBase source and begins caching. - * @param new_reader Pointer to the ReaderBase instance to cache from. + * @brief Attach a ReaderBase (e.g. Timeline, FFmpegReader) and begin caching. + * @param new_reader */ void Reader(ReaderBase* new_reader) { reader = new_reader; Play(); } - // Friend classes that may access protected members directly. - friend class PlayerPrivate; - friend class QtPlayer; - protected: - /** - * @brief Thread entry point: maintains and updates the cache window. - * - * This method runs continuously until threadShouldExit() returns true. It: - * 1. Computes effective playback direction (dir) based on speed or last_dir. - * 2. On seek or direction change, resets cache_start and last_cached. - * 3. When paused (speed == 0), continues caching in dir without advancing playhead. - * 4. When playing, caches in the direction of playback around the current playhead. - * 5. Sleeps for short intervals to throttle CPU usage. - */ + /// Thread entry point: loops until threadShouldExit() is true. void run() override; /** - * @brief Estimate memory footprint of a single frame (video + audio). - * @param width Frame width in pixels. - * @param height Frame height in pixels. - * @param sample_rate Audio sample rate (e.g., 48000). - * @param channels Number of audio channels. - * @param fps Frames per second. - * @return Approximate size in bytes for storing one frame. + * @brief Estimate memory usage for a single frame (video + audio). + * @param width Frame width (pixels) + * @param height Frame height (pixels) + * @param sample_rate Audio sample rate (e.g. 48000) + * @param channels Number of audio channels + * @param fps Frames per second + * @return Approximate size in bytes for one frame */ int64_t getBytes(int width, int height, int sample_rate, int channels, float fps); - //------------------------------------------------------------------------ - // Member variables - //------------------------------------------------------------------------ + //---------- Helper functions, broken out for clarity & unit testing ---------- + + /// @return Effective playback direction (+1 forward, –1 backward), preserving last_dir if speed == 0. + int computeDirection() const; + + /** + * @brief If userSeeked is true, reset last_cached_index just behind the playhead. + * @param playhead Current requested_display_frame + * @param dir Effective direction (±1) + */ + void handleUserSeek(int64_t playhead, int dir); + + /** + * @brief When paused and playhead is outside current cache, clear all frames. + * @param playhead Current requested_display_frame + * @param paused True if speed == 0 + * @param cache Pointer to CacheBase + * @return True if ClearAllCache() was invoked. + */ + bool clearCacheIfPaused(int64_t playhead, bool paused, CacheBase* cache); + + /** + * @brief Compute the “window” of frames to cache around playhead. + * @param playhead Current requested_display_frame + * @param dir Effective direction (±1) + * @param ahead_count Number of frames ahead/back to cache + * @param timeline_end Last valid frame index + * @param[out] window_begin Lower bound (inclusive) of caching window + * @param[out] window_end Upper bound (inclusive) of caching window + * + * If dir > 0: window = [playhead ... playhead + ahead_count] + * If dir < 0: window = [playhead – ahead_count ... playhead] + * Always clamps to [1 ... timeline_end]. + */ + void computeWindowBounds(int64_t playhead, + int dir, + int64_t ahead_count, + int64_t timeline_end, + int64_t& window_begin, + int64_t& window_end) const; + + /** + * @brief Prefetch all missing frames in [window_begin ... window_end] or [window_end ... window_begin]. + * @param cache Pointer to CacheBase + * @param window_begin Inclusive lower bound of the window + * @param window_end Inclusive upper bound of the window + * @param dir Effective direction (±1) + * @param reader Pointer to ReaderBase to call GetFrame() + * @return True if the window was already full (no new frames added) + * + * Internally, this method iterates from last_cached_index + dir toward window_end (or window_begin) + * and calls GetFrame()/Add() for each missing frame until hitting the window boundary or an OOB. + * It also breaks early if threadShouldExit() or userSeeked becomes true. + */ + bool prefetchWindow(CacheBase* cache, + int64_t window_begin, + int64_t window_end, + int dir, + ReaderBase* reader); + + //---------- Internal state ---------- std::shared_ptr last_cached_frame; ///< Last frame pointer added to cache. - int speed; ///< Current playback speed (0=paused, >0=forward, <0=backward). - int last_speed; ///< Last non-zero speed value (used to compute direction). - int last_dir; ///< Last playback direction: +1 for forward, -1 for backward. + int speed; ///< Current playback speed (0=paused, >0 forward, <0 backward). + int last_speed; ///< Last non-zero speed (for tracking). + int last_dir; ///< Last direction sign (+1 forward, –1 backward). - bool is_playing; ///< True when playback is running, false when stopped. - bool userSeeked; ///< True if a seek was requested (triggers cache reset). + bool is_playing; ///< True if playback is “running” (affects thread loop, not caching). + bool userSeeked; ///< True if Seek(..., true) was called (forces a cache reset). - int64_t requested_display_frame; ///< Frame index requested by the user. - int64_t current_display_frame; ///< Currently displaying frame index (not used in caching). - int64_t cached_frame_count; ///< Number of frames currently in cache. + int64_t requested_display_frame; ///< Frame index the user requested. + int64_t current_display_frame; ///< Currently displayed frame (unused here, reserved). + int64_t cached_frame_count; ///< Count of frames currently added to cache. - int64_t min_frames_ahead; ///< Minimum number of frames to keep ahead in cache. - int64_t max_frames_ahead; ///< Maximum number of frames to keep ahead in cache. + int64_t min_frames_ahead; ///< Minimum number of frames considered “ready” (pre-roll). + int64_t max_frames_ahead; ///< Maximum frames to attempt to cache (mem capped). int64_t timeline_max_frame; ///< Highest valid frame index in the timeline. - bool should_pause_cache; ///< Flag to pause cache updates (not currently used). - bool should_break; ///< Internal flag to break out of loops (not currently used). + ReaderBase* reader; ///< The source reader (e.g., Timeline, FFmpegReader). + bool force_directional_cache; ///< (Reserved for future use). - ReaderBase* reader; ///< Pointer to the video/audio source (ReaderBase). - - /// Forces caching in a fixed direction when seeking into an uncached frame. - bool force_directional_cache; + int64_t last_cached_index; ///< Index of the most recently cached frame. }; } // namespace openshot