From 43f9ed89aeb2ed9c76055293d7fa5aa2a191b5d5 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Thu, 5 Feb 2026 12:39:06 -0600 Subject: [PATCH 1/7] VideoCacheThread: add directional preroll - start cache rebuilds with a preroll offset (settings-based) - tighten isReady to require frames ahead/behind playhead add unit tests for preroll clamping and readiness --- src/Qt/VideoCacheThread.cpp | 57 +++++++++++++++++++++++++++++++++++-- src/Qt/VideoCacheThread.h | 17 +++++++++++ src/Settings.h | 4 +-- tests/VideoCacheThread.cpp | 51 +++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 5 deletions(-) diff --git a/src/Qt/VideoCacheThread.cpp b/src/Qt/VideoCacheThread.cpp index 643ed0a64..80c4fa639 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 72f502e04..121b596a5 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 0474e3d1c..b91f733b5 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 74bd5c621..c918ea09a 100644 --- a/tests/VideoCacheThread.cpp +++ b/tests/VideoCacheThread.cpp @@ -33,9 +33,13 @@ class TestableVideoCacheThread : public VideoCacheThread { 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); From de012ac6c847b3b07c5b7b88000289930d58602e Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 6 Feb 2026 16:46:40 -0600 Subject: [PATCH 2/7] Improve seek retry fallback and clean up hw decode logging - add adaptive seek fallback and switch fprintf to ZmqLogger - improves seeking on certain files by up to 30% (faster) - avoids situations where seeking too far would sometimes freeze on certain videos --- examples/Example.cpp | 171 ++++++++++++++++++++++++++----------------- src/FFmpegReader.cpp | 109 +++++++++++++++++++-------- src/FFmpegReader.h | 4 +- 3 files changed, 185 insertions(+), 99 deletions(-) diff --git a/examples/Example.cpp b/examples/Example.cpp index 2067fb569..0e143c3b6 100644 --- a/examples/Example.cpp +++ b/examples/Example.cpp @@ -13,83 +13,116 @@ #include #include #include +#include +#include "Clip.h" #include "Frame.h" #include "FFmpegReader.h" -#include "FFmpegWriter.h" +#include "Settings.h" #include "Timeline.h" -#include "Qt/VideoCacheThread.h" // <— your new header using namespace openshot; int main(int argc, char* argv[]) { - - - // 1) Open the FFmpegReader as usual - const char* input_path = "/home/jonathan/Downloads/openshot-testing/sintel_trailer-720p.mp4"; - FFmpegReader reader(input_path); - reader.Open(); - - const int64_t total_frames = reader.info.video_length; - std::cout << "Total frames: " << total_frames << "\n"; - - - - Timeline timeline(reader.info.width, reader.info.height, reader.info.fps, reader.info.sample_rate, reader.info.channels, reader.info.channel_layout); - Clip c1(&reader); - timeline.AddClip(&c1); - timeline.Open(); - timeline.DisplayInfo(); - - - // 2) Construct a VideoCacheThread around 'reader' and start its background loop - // (VideoCacheThread inherits juce::Thread) - std::shared_ptr cache = std::make_shared(); - cache->Reader(&timeline); // attaches the FFmpegReader and internally calls Play() - cache->StartThread(); // juce::Thread method, begins run() - - // 3) Set up the writer exactly as before - FFmpegWriter writer("/home/jonathan/Downloads/performance‐cachetest.mp4"); - writer.SetAudioOptions("aac", 48000, 192000); - writer.SetVideoOptions("libx264", 1280, 720, Fraction(30, 1), 5000000); - writer.Open(); - - // 4) Forward pass: for each frame 1…N, tell the cache thread to seek to that frame, - // then immediately call cache->GetFrame(frame), which will block only if that frame - // hasn’t been decoded into the cache yet. - auto t0 = std::chrono::high_resolution_clock::now(); - cache->setSpeed(1); - for (int64_t f = 1; f <= total_frames; ++f) { - float pct = (float(f) / total_frames) * 100.0f; - std::cout << "Forward: requesting frame " << f << " (" << pct << "%)\n"; - - cache->Seek(f); // signal “I need frame f now (and please prefetch f+1, f+2, …)” - std::shared_ptr framePtr = timeline.GetFrame(f); - writer.WriteFrame(framePtr); - } - auto t1 = std::chrono::high_resolution_clock::now(); - auto forward_ms = std::chrono::duration_cast(t1 - t0).count(); - - // 5) Backward pass: same idea in reverse - auto t2 = std::chrono::high_resolution_clock::now(); - cache->setSpeed(-1); - for (int64_t f = total_frames; f >= 1; --f) { - float pct = (float(total_frames - f + 1) / total_frames) * 100.0f; - std::cout << "Backward: requesting frame " << f << " (" << pct << "%)\n"; - - cache->Seek(f); - std::shared_ptr framePtr = timeline.GetFrame(f); - writer.WriteFrame(framePtr); + using clock = std::chrono::high_resolution_clock; + auto total_start = clock::now(); + + const std::string output_dir = "/home/jonathan/Downloads"; + const std::string input_paths[] = { + "/home/jonathan/Videos/3.4 Release/Screencasts/Timing.mp4", + "/home/jonathan/Downloads/openshot-testing/sintel_trailer-720p.mp4" + }; + const int64_t frames_to_fetch[] = {175, 225, 240, 500, 1000}; + const bool use_hw_decode = false; + + std::cout << "Hardware decode: " << (use_hw_decode ? "ON" : "OFF") << "\n"; + openshot::Settings::Instance()->HARDWARE_DECODER = use_hw_decode ? 1 : 0; + + for (const std::string& input_path : input_paths) { + auto file_start = clock::now(); + std::string base = input_path; + size_t slash = base.find_last_of('/'); + if (slash != std::string::npos) { + base = base.substr(slash + 1); + } + + std::cout << "\n=== File: " << base << " ===\n"; + + auto t0 = clock::now(); + FFmpegReader reader(input_path.c_str()); + auto t1 = clock::now(); + std::cout << "FFmpegReader ctor: " + << std::chrono::duration_cast(t1 - t0).count() + << " ms\n"; + + auto t2 = clock::now(); + reader.Open(); + auto t3 = clock::now(); + std::cout << "FFmpegReader Open(): " + << std::chrono::duration_cast(t3 - t2).count() + << " ms\n"; + + auto t4 = clock::now(); + Timeline timeline(1920, 1080, Fraction(30, 1), reader.info.sample_rate, reader.info.channels, reader.info.channel_layout); + timeline.SetMaxSize(640, 480); + auto t5 = clock::now(); + std::cout << "Timeline ctor (1080p30): " + << std::chrono::duration_cast(t5 - t4).count() + << " ms\n"; + + auto t6 = clock::now(); + Clip c1(&reader); + auto t7 = clock::now(); + std::cout << "Clip ctor: " + << std::chrono::duration_cast(t7 - t6).count() + << " ms\n"; + + timeline.AddClip(&c1); + + auto t8 = clock::now(); + timeline.Open(); + auto t9 = clock::now(); + std::cout << "Timeline Open(): " + << std::chrono::duration_cast(t9 - t8).count() + << " ms\n"; + + for (int64_t frame_number : frames_to_fetch) { + auto loop_start = clock::now(); + std::cout << "Requesting frame " << frame_number << "...\n"; + + auto t10 = clock::now(); + std::shared_ptr frame = timeline.GetFrame(frame_number); + auto t11 = clock::now(); + std::cout << "Timeline GetFrame(" << frame_number << "): " + << std::chrono::duration_cast(t11 - t10).count() + << " ms\n"; + + std::string out_path = output_dir + "/frame-" + base + "-" + std::to_string(frame_number) + ".jpg"; + + auto t12 = clock::now(); + frame->Thumbnail(out_path, 200, 80, "", "", "#000000", false, "JPEG", 95, 0.0f); + auto t13 = clock::now(); + std::cout << "Frame Thumbnail() JPEG (" << frame_number << "): " + << std::chrono::duration_cast(t13 - t12).count() + << " ms\n"; + + auto loop_end = clock::now(); + std::cout << "Frame loop total (" << frame_number << "): " + << std::chrono::duration_cast(loop_end - loop_start).count() + << " ms\n"; + } + + reader.Close(); + timeline.Close(); + + auto file_end = clock::now(); + std::cout << "File total (" << base << "): " + << std::chrono::duration_cast(file_end - file_start).count() + << " ms\n"; } - auto t3 = std::chrono::high_resolution_clock::now(); - auto backward_ms = std::chrono::duration_cast(t3 - t2).count(); - - std::cout << "\nForward pass elapsed: " << forward_ms << " ms\n"; - std::cout << "Backward pass elapsed: " << backward_ms << " ms\n"; - // 6) Shut down the cache thread, close everything - cache->StopThread(10000); // politely tells run() to exit, waits up to 10s - reader.Close(); - writer.Close(); - timeline.Close(); + auto total_end = clock::now(); + std::cout << "Total elapsed: " + << std::chrono::duration_cast(total_end - total_start).count() + << " ms\n"; return 0; } diff --git a/src/FFmpegReader.cpp b/src/FFmpegReader.cpp index 82c595f31..e0d1b5bc3 100644 --- a/src/FFmpegReader.cpp +++ b/src/FFmpegReader.cpp @@ -79,7 +79,9 @@ FFmpegReader::FFmpegReader(const std::string &path, bool inspect_reader) FFmpegReader::FFmpegReader(const std::string &path, DurationStrategy duration_strategy, bool inspect_reader) : last_frame(0), is_seeking(0), seeking_pts(0), seeking_frame(0), seek_count(0), NO_PTS_OFFSET(-99999), path(path), is_video_seek(true), check_interlace(false), check_fps(false), enable_seek(true), is_open(false), - seek_audio_frame_found(0), seek_video_frame_found(0),is_duration_known(false), largest_frame_processed(0), + seek_audio_frame_found(0), seek_video_frame_found(0), + last_seek_max_frame(-1), seek_stagnant_count(0), + is_duration_known(false), largest_frame_processed(0), current_video_frame(0), packet(NULL), duration_strategy(duration_strategy), audio_pts(0), video_pts(0), pFormatCtx(NULL), videoStream(-1), audioStream(-1), pCodecCtx(NULL), aCodecCtx(NULL), pStream(NULL), aStream(NULL), pFrame(NULL), previous_packet_location{-1,0}, @@ -136,52 +138,69 @@ static enum AVPixelFormat get_hw_dec_format(AVCodecContext *ctx, const enum AVPi { const enum AVPixelFormat *p; + // Prefer only the format matching the selected hardware decoder + int selected = openshot::Settings::Instance()->HARDWARE_DECODER; + for (p = pix_fmts; *p != AV_PIX_FMT_NONE; p++) { switch (*p) { #if defined(__linux__) // Linux pix formats case AV_PIX_FMT_VAAPI: - hw_de_av_pix_fmt_global = AV_PIX_FMT_VAAPI; - hw_de_av_device_type_global = AV_HWDEVICE_TYPE_VAAPI; - return *p; + if (selected == 1) { + hw_de_av_pix_fmt_global = AV_PIX_FMT_VAAPI; + hw_de_av_device_type_global = AV_HWDEVICE_TYPE_VAAPI; + return *p; + } break; case AV_PIX_FMT_VDPAU: - hw_de_av_pix_fmt_global = AV_PIX_FMT_VDPAU; - hw_de_av_device_type_global = AV_HWDEVICE_TYPE_VDPAU; - return *p; + if (selected == 6) { + hw_de_av_pix_fmt_global = AV_PIX_FMT_VDPAU; + hw_de_av_device_type_global = AV_HWDEVICE_TYPE_VDPAU; + return *p; + } break; #endif #if defined(_WIN32) // Windows pix formats case AV_PIX_FMT_DXVA2_VLD: - hw_de_av_pix_fmt_global = AV_PIX_FMT_DXVA2_VLD; - hw_de_av_device_type_global = AV_HWDEVICE_TYPE_DXVA2; - return *p; + if (selected == 3) { + hw_de_av_pix_fmt_global = AV_PIX_FMT_DXVA2_VLD; + hw_de_av_device_type_global = AV_HWDEVICE_TYPE_DXVA2; + return *p; + } break; case AV_PIX_FMT_D3D11: - hw_de_av_pix_fmt_global = AV_PIX_FMT_D3D11; - hw_de_av_device_type_global = AV_HWDEVICE_TYPE_D3D11VA; - return *p; + if (selected == 4) { + hw_de_av_pix_fmt_global = AV_PIX_FMT_D3D11; + hw_de_av_device_type_global = AV_HWDEVICE_TYPE_D3D11VA; + return *p; + } break; #endif #if defined(__APPLE__) // Apple pix formats case AV_PIX_FMT_VIDEOTOOLBOX: - hw_de_av_pix_fmt_global = AV_PIX_FMT_VIDEOTOOLBOX; - hw_de_av_device_type_global = AV_HWDEVICE_TYPE_VIDEOTOOLBOX; - return *p; + if (selected == 5) { + hw_de_av_pix_fmt_global = AV_PIX_FMT_VIDEOTOOLBOX; + hw_de_av_device_type_global = AV_HWDEVICE_TYPE_VIDEOTOOLBOX; + return *p; + } break; #endif // Cross-platform pix formats case AV_PIX_FMT_CUDA: - hw_de_av_pix_fmt_global = AV_PIX_FMT_CUDA; - hw_de_av_device_type_global = AV_HWDEVICE_TYPE_CUDA; - return *p; + if (selected == 2) { + hw_de_av_pix_fmt_global = AV_PIX_FMT_CUDA; + hw_de_av_device_type_global = AV_HWDEVICE_TYPE_CUDA; + return *p; + } break; case AV_PIX_FMT_QSV: - hw_de_av_pix_fmt_global = AV_PIX_FMT_QSV; - hw_de_av_device_type_global = AV_HWDEVICE_TYPE_QSV; - return *p; + if (selected == 7) { + hw_de_av_pix_fmt_global = AV_PIX_FMT_QSV; + hw_de_av_device_type_global = AV_HWDEVICE_TYPE_QSV; + return *p; + } break; default: // This is only here to silence unused-enum warnings @@ -302,7 +321,7 @@ void FFmpegReader::Open() { char *adapter_ptr = NULL; int adapter_num; adapter_num = openshot::Settings::Instance()->HW_DE_DEVICE_SET; - fprintf(stderr, "Hardware decoding device number: %d\n", adapter_num); + ZmqLogger::Instance()->AppendDebugMethod("Hardware decoding device number", "adapter_num", adapter_num); // Set hardware pix format (callback) pCodecCtx->get_format = get_hw_dec_format; @@ -388,6 +407,10 @@ void FFmpegReader::Open() { hw_device_ctx = NULL; // Here the first hardware initialisations are made if (av_hwdevice_ctx_create(&hw_device_ctx, hw_de_av_device_type, adapter_ptr, NULL, 0) >= 0) { + const char* hw_name = av_hwdevice_get_type_name(hw_de_av_device_type); + std::string hw_msg = "HW decode active: "; + hw_msg += (hw_name ? hw_name : "unknown"); + ZmqLogger::Instance()->Log(hw_msg); if (!(pCodecCtx->hw_device_ctx = av_buffer_ref(hw_device_ctx))) { throw InvalidCodec("Hardware device reference create failed.", path); } @@ -420,7 +443,8 @@ void FFmpegReader::Open() { */ } else { - throw InvalidCodec("Hardware device create failed.", path); + ZmqLogger::Instance()->Log("HW decode active: no (falling back to software)"); + throw InvalidCodec("Hardware device create failed.", path); } } #endif // USE_HW_ACCEL @@ -1035,6 +1059,8 @@ bool FFmpegReader::GetIsDurationKnown() { } std::shared_ptr FFmpegReader::GetFrame(int64_t requested_frame) { + last_seek_max_frame = -1; + seek_stagnant_count = 0; // Check for open reader (or throw exception) if (!is_open) throw ReaderClosed("The FFmpegReader is closed. Call Open() before calling this method.", path); @@ -1137,7 +1163,7 @@ std::shared_ptr FFmpegReader::ReadStream(int64_t requested_frame) { // Check the status of a seek (if any) if (is_seeking) { - check_seek = CheckSeek(false); + check_seek = CheckSeek(); } else { check_seek = false; } @@ -1446,9 +1472,12 @@ bool FFmpegReader::GetAVFrame() { } // Check the current seek position and determine if we need to seek again -bool FFmpegReader::CheckSeek(bool is_video) { +bool FFmpegReader::CheckSeek() { // Are we seeking for a specific frame? if (is_seeking) { + const int64_t kSeekRetryMax = 5; + const int kSeekStagnantMax = 2; + // Determine if both an audio and video packet have been decoded since the seek happened. // If not, allow the ReadStream method to keep looping if ((is_video_seek && !seek_video_frame_found) || (!is_video_seek && !seek_audio_frame_found)) @@ -1460,6 +1489,13 @@ bool FFmpegReader::CheckSeek(bool is_video) { // Determine max seeked frame int64_t max_seeked_frame = std::max(seek_audio_frame_found, seek_video_frame_found); + // Track stagnant seek results (no progress between retries) + if (max_seeked_frame == last_seek_max_frame) { + seek_stagnant_count++; + } else { + last_seek_max_frame = max_seeked_frame; + seek_stagnant_count = 0; + } // determine if we are "before" the requested frame if (max_seeked_frame >= seeking_frame) { @@ -1473,7 +1509,22 @@ bool FFmpegReader::CheckSeek(bool is_video) { "seek_audio_frame_found", seek_audio_frame_found); // Seek again... to the nearest Keyframe - Seek(seeking_frame - (10 * seek_count * seek_count)); + if (seek_count < kSeekRetryMax) { + Seek(seeking_frame - (10 * seek_count * seek_count)); + } else { + if (seek_stagnant_count >= kSeekStagnantMax) { + // Overshot and no progress: restart from the beginning and walk forward + Seek(1); + is_seeking = false; + seeking_frame = 0; + seeking_pts = -1; + } else { + // Give up retrying and walk forward + is_seeking = false; + seeking_frame = 0; + seeking_pts = -1; + } + } } else { // SEEK WORKED ZmqLogger::Instance()->AppendDebugMethod("FFmpegReader::CheckSeek (Successful)", @@ -1980,7 +2031,7 @@ void FFmpegReader::Seek(int64_t requested_frame) { if (!seek_worked && info.has_video && !HasAlbumArt()) { seek_target = ConvertFrameToVideoPTS(requested_frame - buffer_amount); if (av_seek_frame(pFormatCtx, info.video_stream_index, seek_target, AVSEEK_FLAG_BACKWARD) < 0) { - fprintf(stderr, "%s: error while seeking video stream\n", pFormatCtx->AV_FILENAME); + ZmqLogger::Instance()->Log(std::string(pFormatCtx->AV_FILENAME) + ": error while seeking video stream"); } else { // VIDEO SEEK is_video_seek = true; @@ -1992,7 +2043,7 @@ void FFmpegReader::Seek(int64_t requested_frame) { if (!seek_worked && info.has_audio) { seek_target = ConvertFrameToAudioPTS(requested_frame - buffer_amount); if (av_seek_frame(pFormatCtx, info.audio_stream_index, seek_target, AVSEEK_FLAG_BACKWARD) < 0) { - fprintf(stderr, "%s: error while seeking audio stream\n", pFormatCtx->AV_FILENAME); + ZmqLogger::Instance()->Log(std::string(pFormatCtx->AV_FILENAME) + ": error while seeking audio stream"); } else { // AUDIO SEEK is_video_seek = false; diff --git a/src/FFmpegReader.h b/src/FFmpegReader.h index f264754c3..23ce70e07 100644 --- a/src/FFmpegReader.h +++ b/src/FFmpegReader.h @@ -136,6 +136,8 @@ namespace openshot { int seek_count; int64_t seek_audio_frame_found; int64_t seek_video_frame_found; + int64_t last_seek_max_frame; + int seek_stagnant_count; int64_t last_frame; int64_t largest_frame_processed; @@ -172,7 +174,7 @@ namespace openshot { void CheckFPS(); /// Check the current seek position and determine if we need to seek again - bool CheckSeek(bool is_video); + bool CheckSeek(); /// Check the working queue, and move finished frames to the finished queue void CheckWorkingFrames(int64_t requested_frame); From f3c45046625e54c340352b1b5f9d68b2e8dc7a23 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 6 Feb 2026 23:11:00 -0600 Subject: [PATCH 3/7] Improve benchmark regression caused by aggressive memory trimming, and caches that were too small. A new debounce strategy for memory trimming (1 time max per 30 seconds). --- src/CacheMemory.cpp | 27 ++++----------------------- src/CacheMemory.h | 2 -- src/FFmpegReader.cpp | 11 +++++------ src/FrameMapper.cpp | 5 +---- src/MemoryTrim.cpp | 11 ++++++----- src/MemoryTrim.h | 2 +- src/Timeline.cpp | 4 ++-- 7 files changed, 19 insertions(+), 43 deletions(-) diff --git a/src/CacheMemory.cpp b/src/CacheMemory.cpp index bd9a6c2c2..563570384 100644 --- a/src/CacheMemory.cpp +++ b/src/CacheMemory.cpp @@ -19,7 +19,7 @@ using namespace std; using namespace openshot; // Default constructor, no max bytes -CacheMemory::CacheMemory() : CacheBase(0), bytes_freed_since_trim(0) { +CacheMemory::CacheMemory() : CacheBase(0) { // Set cache type name cache_type = "CacheMemory"; range_version = 0; @@ -27,7 +27,7 @@ CacheMemory::CacheMemory() : CacheBase(0), bytes_freed_since_trim(0) { } // Constructor that sets the max bytes to cache -CacheMemory::CacheMemory(int64_t max_bytes) : CacheBase(max_bytes), bytes_freed_since_trim(0) { +CacheMemory::CacheMemory(int64_t max_bytes) : CacheBase(max_bytes) { // Set cache type name cache_type = "CacheMemory"; range_version = 0; @@ -162,8 +162,6 @@ void CacheMemory::Remove(int64_t start_frame_number, int64_t end_frame_number) { // Create a scoped lock, to protect the cache from multiple threads const std::lock_guard lock(*cacheMutex); - int64_t removed_bytes = 0; - // Loop through frame numbers std::deque::iterator itr; for(itr = frame_numbers.begin(); itr != frame_numbers.end();) @@ -182,10 +180,6 @@ void CacheMemory::Remove(int64_t start_frame_number, int64_t end_frame_number) { if (*itr_ordered >= start_frame_number && *itr_ordered <= end_frame_number) { - // Count bytes freed before erasing the frame - if (frames.count(*itr_ordered)) - removed_bytes += frames[*itr_ordered]->GetBytes(); - // erase frame number frames.erase(*itr_ordered); itr_ordered = ordered_frame_numbers.erase(itr_ordered); @@ -193,17 +187,6 @@ void CacheMemory::Remove(int64_t start_frame_number, int64_t end_frame_number) itr_ordered++; } - if (removed_bytes > 0) - { - bytes_freed_since_trim += removed_bytes; - if (bytes_freed_since_trim >= TRIM_THRESHOLD_BYTES) - { - // Periodically return freed arenas to the OS - if (TrimMemoryToOS()) - bytes_freed_since_trim = 0; - } - } - // Needs range processing (since cache has changed) needs_range_processing = true; } @@ -246,10 +229,8 @@ void CacheMemory::Clear() ordered_frame_numbers.clear(); ordered_frame_numbers.shrink_to_fit(); needs_range_processing = true; - bytes_freed_since_trim = 0; - - // Trim freed arenas back to OS after large clears - TrimMemoryToOS(true); + // Trim freed arenas back to OS after large clears (debounced) + TrimMemoryToOS(); } // Count the frames in the queue diff --git a/src/CacheMemory.h b/src/CacheMemory.h index 9972b1025..e35fdb11c 100644 --- a/src/CacheMemory.h +++ b/src/CacheMemory.h @@ -28,10 +28,8 @@ namespace openshot { */ class CacheMemory : public CacheBase { private: - static constexpr int64_t TRIM_THRESHOLD_BYTES = 1024LL * 1024 * 1024; ///< Release memory after freeing this much memory std::map > frames; ///< This map holds the frame number and Frame objects std::deque frame_numbers; ///< This queue holds a sequential list of cached Frame numbers - int64_t bytes_freed_since_trim; ///< Tracks bytes freed to trigger a heap trim /// Clean up cached frames that exceed the max number of bytes void CleanUp(); diff --git a/src/FFmpegReader.cpp b/src/FFmpegReader.cpp index e0d1b5bc3..361832f8d 100644 --- a/src/FFmpegReader.cpp +++ b/src/FFmpegReader.cpp @@ -97,8 +97,8 @@ FFmpegReader::FFmpegReader(const std::string &path, DurationStrategy duration_st audio_pts_seconds = NO_PTS_OFFSET; // Init cache - working_cache.SetMaxBytesFromInfo(info.fps.ToDouble() * 2, info.width, info.height, info.sample_rate, info.channels); - final_cache.SetMaxBytesFromInfo(24, info.width, info.height, info.sample_rate, info.channels); + working_cache.SetMaxBytesFromInfo(OPEN_MP_NUM_PROCESSORS * 4, info.width, info.height, info.sample_rate, info.channels); + final_cache.SetMaxBytesFromInfo(OPEN_MP_NUM_PROCESSORS * 4, info.width, info.height, info.sample_rate, info.channels); // Open and Close the reader, to populate its attributes (such as height, width, etc...) if (inspect_reader) { @@ -637,8 +637,8 @@ void FFmpegReader::Open() { previous_packet_location.sample_start = 0; // Adjust cache size based on size of frame and audio - working_cache.SetMaxBytesFromInfo(info.fps.ToDouble() * 2, info.width, info.height, info.sample_rate, info.channels); - final_cache.SetMaxBytesFromInfo(24, info.width, info.height, info.sample_rate, info.channels); + working_cache.SetMaxBytesFromInfo(OPEN_MP_NUM_PROCESSORS * info.fps.ToDouble() * 2, info.width, info.height, info.sample_rate, info.channels); + final_cache.SetMaxBytesFromInfo(OPEN_MP_NUM_PROCESSORS * 2, info.width, info.height, info.sample_rate, info.channels); // Scan PTS for any offsets (i.e. non-zero starting streams). At least 1 stream must start at zero timestamp. // This method allows us to shift timestamps to ensure at least 1 stream is starting at zero. @@ -740,8 +740,7 @@ void FFmpegReader::Close() { avformat_close_input(&pFormatCtx); av_freep(&pFormatCtx); - // Release free’d arenas back to OS after heavy teardown - TrimMemoryToOS(true); + // Do not trim here; trimming is handled on explicit cache clears // Reset some variables last_frame = 0; diff --git a/src/FrameMapper.cpp b/src/FrameMapper.cpp index 531cd571c..a2522e271 100644 --- a/src/FrameMapper.cpp +++ b/src/FrameMapper.cpp @@ -746,9 +746,6 @@ void FrameMapper::Close() SWR_FREE(&avr); avr = NULL; } - - // Release free’d arenas back to OS after heavy teardown - TrimMemoryToOS(true); } @@ -845,7 +842,7 @@ void FrameMapper::ChangeMapping(Fraction target_fps, PulldownType target_pulldow final_cache.Clear(); // Adjust cache size based on size of frame and audio - final_cache.SetMaxBytesFromInfo(24, info.width, info.height, info.sample_rate, info.channels); + final_cache.SetMaxBytesFromInfo(OPEN_MP_NUM_PROCESSORS * 4, info.width, info.height, info.sample_rate, info.channels); // Deallocate resample buffer if (avr) { diff --git a/src/MemoryTrim.cpp b/src/MemoryTrim.cpp index 8a73ec501..50aae55c5 100644 --- a/src/MemoryTrim.cpp +++ b/src/MemoryTrim.cpp @@ -25,7 +25,7 @@ namespace { // Limit trim attempts to once per interval to avoid spamming platform calls -constexpr uint64_t kMinTrimIntervalMs = 1000; // 1s debounce +constexpr uint64_t kMinTrimIntervalMs = 30000; // 30s debounce std::atomic g_last_trim_ms{0}; std::atomic g_trim_in_progress{false}; @@ -37,12 +37,12 @@ uint64_t NowMs() { namespace openshot { -bool TrimMemoryToOS(bool force) noexcept { +bool TrimMemoryToOS() noexcept { const uint64_t now_ms = NowMs(); const uint64_t last_ms = g_last_trim_ms.load(std::memory_order_relaxed); - // Skip if we recently trimmed (unless forced) - if (!force && now_ms - last_ms < kMinTrimIntervalMs) + // Skip if we recently trimmed + if (now_ms - last_ms < kMinTrimIntervalMs) return false; // Only one trim attempt runs at a time @@ -70,8 +70,9 @@ bool TrimMemoryToOS(bool force) noexcept { did_trim = false; #endif - if (did_trim) + if (did_trim) { g_last_trim_ms.store(now_ms, std::memory_order_relaxed); + } g_trim_in_progress.store(false, std::memory_order_release); return did_trim; diff --git a/src/MemoryTrim.h b/src/MemoryTrim.h index 943fa0ae6..aa99d969f 100644 --- a/src/MemoryTrim.h +++ b/src/MemoryTrim.h @@ -25,6 +25,6 @@ namespace openshot { * @param force If true, bypass the debounce interval (useful for teardown). * @return true if a platform-specific trim call was made, false otherwise. */ -bool TrimMemoryToOS(bool force = false) noexcept; +bool TrimMemoryToOS() noexcept; } // namespace openshot diff --git a/src/Timeline.cpp b/src/Timeline.cpp index 26fa96bd1..03568f3b7 100644 --- a/src/Timeline.cpp +++ b/src/Timeline.cpp @@ -69,7 +69,7 @@ Timeline::Timeline(int width, int height, Fraction fps, int sample_rate, int cha // Init cache final_cache = new CacheMemory(); - final_cache->SetMaxBytesFromInfo(24, info.width, info.height, info.sample_rate, info.channels); + final_cache->SetMaxBytesFromInfo(OPEN_MP_NUM_PROCESSORS * 4, info.width, info.height, info.sample_rate, info.channels); } // Delegating constructor that copies parameters from a provided ReaderInfo @@ -201,7 +201,7 @@ Timeline::Timeline(const std::string& projectPath, bool convert_absolute_paths) // Init cache final_cache = new CacheMemory(); - final_cache->SetMaxBytesFromInfo(24, info.width, info.height, info.sample_rate, info.channels); + final_cache->SetMaxBytesFromInfo(OPEN_MP_NUM_PROCESSORS * 4, info.width, info.height, info.sample_rate, info.channels); } Timeline::~Timeline() { From 367d21affe4da5a8c6abdb189fd3ec4f700e5854 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Mon, 9 Feb 2026 20:51:24 -0600 Subject: [PATCH 4/7] Fix FFmpeg seek fallback to avoid overshoot frame mismatches. Harden FFmpegReader::CheckSeek() when seek retries are exhausted. --- src/FFmpegReader.cpp | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/FFmpegReader.cpp b/src/FFmpegReader.cpp index 361832f8d..4df68e57d 100644 --- a/src/FFmpegReader.cpp +++ b/src/FFmpegReader.cpp @@ -1511,18 +1511,12 @@ bool FFmpegReader::CheckSeek() { if (seek_count < kSeekRetryMax) { Seek(seeking_frame - (10 * seek_count * seek_count)); } else { - if (seek_stagnant_count >= kSeekStagnantMax) { - // Overshot and no progress: restart from the beginning and walk forward - Seek(1); - is_seeking = false; - seeking_frame = 0; - seeking_pts = -1; - } else { - // Give up retrying and walk forward - is_seeking = false; - seeking_frame = 0; - seeking_pts = -1; - } + // Retry budget exhausted: always restart from frame 1 and walk forward. + // This avoids returning frames from an overshot seek position. + Seek(1); + is_seeking = false; + seeking_frame = 0; + seeking_pts = -1; } } else { // SEEK WORKED From d6b54091d7002526ac019d792895d2f878e28286 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Mon, 9 Feb 2026 21:14:45 -0600 Subject: [PATCH 5/7] Adding minimum cache size floor, for systems with very few cores. This is required to fix a failure running unit tests on our GitLab runners. --- src/Timeline.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Timeline.cpp b/src/Timeline.cpp index 03568f3b7..8b3606a7e 100644 --- a/src/Timeline.cpp +++ b/src/Timeline.cpp @@ -19,6 +19,7 @@ #include "FrameMapper.h" #include "Exceptions.h" +#include #include #include #include @@ -69,7 +70,8 @@ Timeline::Timeline(int width, int height, Fraction fps, int sample_rate, int cha // Init cache final_cache = new CacheMemory(); - final_cache->SetMaxBytesFromInfo(OPEN_MP_NUM_PROCESSORS * 4, info.width, info.height, info.sample_rate, info.channels); + const int cache_frames = std::max(24, OPEN_MP_NUM_PROCESSORS * 4); + final_cache->SetMaxBytesFromInfo(cache_frames, info.width, info.height, info.sample_rate, info.channels); } // Delegating constructor that copies parameters from a provided ReaderInfo @@ -201,7 +203,8 @@ Timeline::Timeline(const std::string& projectPath, bool convert_absolute_paths) // Init cache final_cache = new CacheMemory(); - final_cache->SetMaxBytesFromInfo(OPEN_MP_NUM_PROCESSORS * 4, info.width, info.height, info.sample_rate, info.channels); + const int cache_frames = std::max(24, OPEN_MP_NUM_PROCESSORS * 4); + final_cache->SetMaxBytesFromInfo(cache_frames, info.width, info.height, info.sample_rate, info.channels); } Timeline::~Timeline() { From 6f63254c089331ac5ce78e9361a54d0da1bf3590 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Mon, 9 Feb 2026 21:35:06 -0600 Subject: [PATCH 6/7] More changes to seek logic to pass unit tests on Windows. --- src/FFmpegReader.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/FFmpegReader.cpp b/src/FFmpegReader.cpp index 4df68e57d..2844e3859 100644 --- a/src/FFmpegReader.cpp +++ b/src/FFmpegReader.cpp @@ -1510,13 +1510,12 @@ bool FFmpegReader::CheckSeek() { // Seek again... to the nearest Keyframe if (seek_count < kSeekRetryMax) { Seek(seeking_frame - (10 * seek_count * seek_count)); + } else if (seek_stagnant_count >= kSeekStagnantMax) { + // Stagnant seek: force a much earlier target and keep seeking. + Seek(seeking_frame - (10 * kSeekRetryMax * kSeekRetryMax)); } else { - // Retry budget exhausted: always restart from frame 1 and walk forward. - // This avoids returning frames from an overshot seek position. - Seek(1); - is_seeking = false; - seeking_frame = 0; - seeking_pts = -1; + // Retry budget exhausted: keep seeking from a conservative offset. + Seek(seeking_frame - (10 * seek_count * seek_count)); } } else { // SEEK WORKED From 2e4e12d2c7fcce27a8d9a7303ac6a21ec6843a82 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Mon, 9 Feb 2026 22:10:32 -0600 Subject: [PATCH 7/7] Setting a minimum # of frames (floor) in Timeline, FFmpegReader, and FrameMapper - so low cpu/thread systems don't end up with a tiny # that makes passing our unit tests difficult. --- src/FFmpegReader.cpp | 12 ++++++++---- src/FrameMapper.cpp | 7 +++++-- src/Settings.h | 11 +++++++---- src/Timeline.cpp | 4 ++-- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/FFmpegReader.cpp b/src/FFmpegReader.cpp index 2844e3859..e70d5540e 100644 --- a/src/FFmpegReader.cpp +++ b/src/FFmpegReader.cpp @@ -97,8 +97,10 @@ FFmpegReader::FFmpegReader(const std::string &path, DurationStrategy duration_st audio_pts_seconds = NO_PTS_OFFSET; // Init cache - working_cache.SetMaxBytesFromInfo(OPEN_MP_NUM_PROCESSORS * 4, info.width, info.height, info.sample_rate, info.channels); - final_cache.SetMaxBytesFromInfo(OPEN_MP_NUM_PROCESSORS * 4, info.width, info.height, info.sample_rate, info.channels); + const int init_working_cache_frames = std::max(Settings::Instance()->CACHE_MIN_FRAMES, OPEN_MP_NUM_PROCESSORS * 4); + const int init_final_cache_frames = std::max(Settings::Instance()->CACHE_MIN_FRAMES, OPEN_MP_NUM_PROCESSORS * 4); + working_cache.SetMaxBytesFromInfo(init_working_cache_frames, info.width, info.height, info.sample_rate, info.channels); + final_cache.SetMaxBytesFromInfo(init_final_cache_frames, info.width, info.height, info.sample_rate, info.channels); // Open and Close the reader, to populate its attributes (such as height, width, etc...) if (inspect_reader) { @@ -637,8 +639,10 @@ void FFmpegReader::Open() { previous_packet_location.sample_start = 0; // Adjust cache size based on size of frame and audio - working_cache.SetMaxBytesFromInfo(OPEN_MP_NUM_PROCESSORS * info.fps.ToDouble() * 2, info.width, info.height, info.sample_rate, info.channels); - final_cache.SetMaxBytesFromInfo(OPEN_MP_NUM_PROCESSORS * 2, info.width, info.height, info.sample_rate, info.channels); + const int working_cache_frames = std::max(Settings::Instance()->CACHE_MIN_FRAMES, int(OPEN_MP_NUM_PROCESSORS * info.fps.ToDouble() * 2)); + const int final_cache_frames = std::max(Settings::Instance()->CACHE_MIN_FRAMES, OPEN_MP_NUM_PROCESSORS * 2); + working_cache.SetMaxBytesFromInfo(working_cache_frames, info.width, info.height, info.sample_rate, info.channels); + final_cache.SetMaxBytesFromInfo(final_cache_frames, info.width, info.height, info.sample_rate, info.channels); // Scan PTS for any offsets (i.e. non-zero starting streams). At least 1 stream must start at zero timestamp. // This method allows us to shift timestamps to ensure at least 1 stream is starting at zero. diff --git a/src/FrameMapper.cpp b/src/FrameMapper.cpp index a2522e271..5e678b5b2 100644 --- a/src/FrameMapper.cpp +++ b/src/FrameMapper.cpp @@ -11,6 +11,7 @@ // SPDX-License-Identifier: LGPL-3.0-or-later #include +#include #include #include @@ -49,7 +50,8 @@ FrameMapper::FrameMapper(ReaderBase *reader, Fraction target, PulldownType targe field_toggle = true; // Adjust cache size based on size of frame and audio - final_cache.SetMaxBytesFromInfo(OPEN_MP_NUM_PROCESSORS, info.width, info.height, info.sample_rate, info.channels); + const int initial_cache_frames = std::max(Settings::Instance()->CACHE_MIN_FRAMES, OPEN_MP_NUM_PROCESSORS); + final_cache.SetMaxBytesFromInfo(initial_cache_frames, info.width, info.height, info.sample_rate, info.channels); } // Destructor @@ -842,7 +844,8 @@ void FrameMapper::ChangeMapping(Fraction target_fps, PulldownType target_pulldow final_cache.Clear(); // Adjust cache size based on size of frame and audio - final_cache.SetMaxBytesFromInfo(OPEN_MP_NUM_PROCESSORS * 4, info.width, info.height, info.sample_rate, info.channels); + const int reset_cache_frames = std::max(Settings::Instance()->CACHE_MIN_FRAMES, OPEN_MP_NUM_PROCESSORS * 4); + final_cache.SetMaxBytesFromInfo(reset_cache_frames, info.width, info.height, info.sample_rate, info.channels); // Deallocate resample buffer if (avr) { diff --git a/src/Settings.h b/src/Settings.h index b91f733b5..7ad8eb96a 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -67,11 +67,14 @@ namespace openshot { /// Number of threads of OpenMP int OMP_THREADS = 16; - /// Number of threads that ffmpeg uses - int FF_THREADS = 16; + /// Number of threads that ffmpeg uses + int FF_THREADS = 16; - /// Maximum rows that hardware decode can handle - int DE_LIMIT_HEIGHT_MAX = 1100; + /// Minimum number of frames for frame-count-based caches + int CACHE_MIN_FRAMES = 24; + + /// Maximum rows that hardware decode can handle + int DE_LIMIT_HEIGHT_MAX = 1100; /// Maximum columns that hardware decode can handle int DE_LIMIT_WIDTH_MAX = 1950; diff --git a/src/Timeline.cpp b/src/Timeline.cpp index 8b3606a7e..c32c19dff 100644 --- a/src/Timeline.cpp +++ b/src/Timeline.cpp @@ -70,7 +70,7 @@ Timeline::Timeline(int width, int height, Fraction fps, int sample_rate, int cha // Init cache final_cache = new CacheMemory(); - const int cache_frames = std::max(24, OPEN_MP_NUM_PROCESSORS * 4); + const int cache_frames = std::max(Settings::Instance()->CACHE_MIN_FRAMES, OPEN_MP_NUM_PROCESSORS * 4); final_cache->SetMaxBytesFromInfo(cache_frames, info.width, info.height, info.sample_rate, info.channels); } @@ -203,7 +203,7 @@ Timeline::Timeline(const std::string& projectPath, bool convert_absolute_paths) // Init cache final_cache = new CacheMemory(); - const int cache_frames = std::max(24, OPEN_MP_NUM_PROCESSORS * 4); + const int cache_frames = std::max(Settings::Instance()->CACHE_MIN_FRAMES, OPEN_MP_NUM_PROCESSORS * 4); final_cache->SetMaxBytesFromInfo(cache_frames, info.width, info.height, info.sample_rate, info.channels); }