diff --git a/src/Qt/VideoCacheThread.cpp b/src/Qt/VideoCacheThread.cpp index 643ed0a6..80c4fa63 100644 --- a/src/Qt/VideoCacheThread.cpp +++ b/src/Qt/VideoCacheThread.cpp @@ -29,6 +29,7 @@ namespace openshot , last_speed(1) , last_dir(1) // assume forward (+1) on first launch , userSeeked(false) + , preroll_on_next_fill(false) , requested_display_frame(1) , current_display_frame(1) , cached_frame_count(0) @@ -56,7 +57,11 @@ namespace openshot return true; } - return (cached_frame_count > min_frames_ahead); + int dir = computeDirection(); + if (dir > 0) { + return (last_cached_index >= requested_display_frame + min_frames_ahead); + } + return (last_cached_index <= requested_display_frame - min_frames_ahead); } void VideoCacheThread::setSpeed(int new_speed) @@ -112,10 +117,15 @@ namespace openshot Timeline* timeline = static_cast(reader); timeline->ClearAllCache(); cached_frame_count = 0; + preroll_on_next_fill = true; } else if (cache) { cached_frame_count = cache->Count(); + preroll_on_next_fill = false; + } + else { + preroll_on_next_fill = false; } } requested_display_frame = new_position; @@ -138,6 +148,39 @@ namespace openshot last_cached_index = playhead - dir; } + void VideoCacheThread::handleUserSeekWithPreroll(int64_t playhead, + int dir, + int64_t timeline_end, + int64_t preroll_frames) + { + int64_t preroll_start = playhead; + if (preroll_frames > 0) { + if (dir > 0) { + preroll_start = std::max(1, playhead - preroll_frames); + } + else { + preroll_start = std::min(timeline_end, playhead + preroll_frames); + } + } + last_cached_index = preroll_start - dir; + } + + int64_t VideoCacheThread::computePrerollFrames(const Settings* settings) const + { + if (!settings) { + return 0; + } + int64_t min_frames = settings->VIDEO_CACHE_MIN_PREROLL_FRAMES; + int64_t max_frames = settings->VIDEO_CACHE_MAX_PREROLL_FRAMES; + if (min_frames < 0) { + return 0; + } + if (max_frames > 0 && min_frames > max_frames) { + min_frames = max_frames; + } + return min_frames; + } + bool VideoCacheThread::clearCacheIfPaused(int64_t playhead, bool paused, CacheBase* cache) @@ -242,6 +285,7 @@ namespace openshot int64_t timeline_end = timeline->GetMaxFrame(); int64_t playhead = requested_display_frame; bool paused = (speed == 0); + int64_t preroll_frames = computePrerollFrames(settings); cached_frame_count = cache->Count(); @@ -269,9 +313,16 @@ namespace openshot } // Handle a user-initiated seek + bool use_preroll = preroll_on_next_fill; if (userSeeked) { - handleUserSeek(playhead, dir); + if (use_preroll) { + handleUserSeekWithPreroll(playhead, dir, timeline_end, preroll_frames); + } + else { + handleUserSeek(playhead, dir); + } userSeeked = false; + preroll_on_next_fill = false; } else if (!paused && capacity >= 1) { // In playback mode, check if last_cached_index drifted outside the new window @@ -316,7 +367,7 @@ namespace openshot // If paused and playhead is no longer in cache, clear everything bool did_clear = clearCacheIfPaused(playhead, paused, cache); if (did_clear) { - handleUserSeek(playhead, dir); + handleUserSeekWithPreroll(playhead, dir, timeline_end, preroll_frames); } // Compute the current caching window diff --git a/src/Qt/VideoCacheThread.h b/src/Qt/VideoCacheThread.h index 72f502e0..121b596a 100644 --- a/src/Qt/VideoCacheThread.h +++ b/src/Qt/VideoCacheThread.h @@ -21,6 +21,7 @@ namespace openshot { + class Settings; using juce::Thread; /** @@ -107,6 +108,21 @@ namespace openshot */ void handleUserSeek(int64_t playhead, int dir); + /** + * @brief Reset last_cached_index to start caching with a directional preroll offset. + * @param playhead Current requested_display_frame + * @param dir Effective direction (±1) + * @param timeline_end Last valid frame index + * @param preroll_frames Number of frames to offset the cache start + */ + void handleUserSeekWithPreroll(int64_t playhead, + int dir, + int64_t timeline_end, + int64_t preroll_frames); + + /// @brief Compute preroll frame count from settings. + int64_t computePrerollFrames(const Settings* settings) const; + /** * @brief When paused and playhead is outside current cache, clear all frames. * @param playhead Current requested_display_frame @@ -163,6 +179,7 @@ namespace openshot int last_speed; ///< Last non-zero speed (for tracking). int last_dir; ///< Last direction sign (+1 forward, –1 backward). bool userSeeked; ///< True if Seek(..., true) was called (forces a cache reset). + bool preroll_on_next_fill; ///< True if next cache rebuild should include preroll offset. int64_t requested_display_frame; ///< Frame index the user requested. int64_t current_display_frame; ///< Currently displayed frame (unused here, reserved). diff --git a/src/Settings.h b/src/Settings.h index 0474e3d1..b91f733b 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -86,10 +86,10 @@ namespace openshot { float VIDEO_CACHE_PERCENT_AHEAD = 0.7; /// Minimum number of frames to cache before playback begins - int VIDEO_CACHE_MIN_PREROLL_FRAMES = 24; + int VIDEO_CACHE_MIN_PREROLL_FRAMES = 30; /// Max number of frames (ahead of playhead) to cache during playback - int VIDEO_CACHE_MAX_PREROLL_FRAMES = 48; + int VIDEO_CACHE_MAX_PREROLL_FRAMES = 60; /// Max number of frames (when paused) to cache for playback int VIDEO_CACHE_MAX_FRAMES = 30 * 10; diff --git a/tests/VideoCacheThread.cpp b/tests/VideoCacheThread.cpp index 74bd5c62..c918ea09 100644 --- a/tests/VideoCacheThread.cpp +++ b/tests/VideoCacheThread.cpp @@ -33,9 +33,13 @@ public: using VideoCacheThread::clearCacheIfPaused; using VideoCacheThread::prefetchWindow; using VideoCacheThread::handleUserSeek; + using VideoCacheThread::handleUserSeekWithPreroll; + using VideoCacheThread::computePrerollFrames; int64_t getLastCachedIndex() const { return last_cached_index; } void setLastCachedIndex(int64_t v) { last_cached_index = v; } + void setPlayhead(int64_t v) { requested_display_frame = v; } + void setMinFramesAhead(int64_t v) { min_frames_ahead = v; } void setLastDir(int d) { last_dir = d; } void forceUserSeekFlag() { userSeeked = true; } }; @@ -95,6 +99,37 @@ TEST_CASE("computeWindowBounds: forward and backward bounds, clamped", "[VideoCa CHECK(we == 3); } +TEST_CASE("isReady: requires cached frames ahead of playhead", "[VideoCacheThread]") { + TestableVideoCacheThread thread; + + Timeline timeline(/*width=*/1280, /*height=*/720, /*fps=*/Fraction(60,1), + /*sample_rate=*/48000, /*channels=*/2, ChannelLayout::LAYOUT_STEREO); + thread.Reader(&timeline); + + thread.setMinFramesAhead(30); + thread.setPlayhead(200); + thread.setSpeed(1); + + thread.setLastCachedIndex(200); + CHECK(!thread.isReady()); + + thread.setLastCachedIndex(229); + CHECK(!thread.isReady()); + + thread.setLastCachedIndex(230); + CHECK(thread.isReady()); + + thread.setSpeed(-1); + thread.setLastCachedIndex(200); + CHECK(!thread.isReady()); + + thread.setLastCachedIndex(171); + CHECK(!thread.isReady()); + + thread.setLastCachedIndex(170); + CHECK(thread.isReady()); +} + TEST_CASE("clearCacheIfPaused: clears only when paused and not in cache", "[VideoCacheThread]") { TestableVideoCacheThread thread; CacheMemory cache(/*max_bytes=*/100000000); @@ -139,6 +174,22 @@ TEST_CASE("handleUserSeek: sets last_cached_index to playhead - dir", "[VideoCac CHECK(thread.getLastCachedIndex() == 51); } +TEST_CASE("handleUserSeekWithPreroll: offsets start by preroll frames", "[VideoCacheThread]") { + TestableVideoCacheThread thread; + + thread.handleUserSeekWithPreroll(/*playhead=*/60, /*dir=*/1, /*timeline_end=*/200, /*preroll_frames=*/30); + CHECK(thread.getLastCachedIndex() == 29); + + thread.handleUserSeekWithPreroll(/*playhead=*/10, /*dir=*/1, /*timeline_end=*/200, /*preroll_frames=*/30); + CHECK(thread.getLastCachedIndex() == 0); + + thread.handleUserSeekWithPreroll(/*playhead=*/1, /*dir=*/1, /*timeline_end=*/200, /*preroll_frames=*/30); + CHECK(thread.getLastCachedIndex() == 0); + + thread.handleUserSeekWithPreroll(/*playhead=*/60, /*dir=*/-1, /*timeline_end=*/200, /*preroll_frames=*/30); + CHECK(thread.getLastCachedIndex() == 91); +} + TEST_CASE("prefetchWindow: forward caching with FFmpegReader & CacheMemory", "[VideoCacheThread]") { TestableVideoCacheThread thread; CacheMemory cache(/*max_bytes=*/100000000);