/** * @file * @brief Source file for VideoCacheThread class * @author Jonathan Thomas * * @ref License */ // Copyright (c) 2008-2025 OpenShot Studios, LLC // // SPDX-License-Identifier: LGPL-3.0-or-later #include "VideoCacheThread.h" #include "CacheBase.h" #include "Exceptions.h" #include "Frame.h" #include "Settings.h" #include "Timeline.h" #include #include #include namespace openshot { // Constructor VideoCacheThread::VideoCacheThread() : Thread("video-cache") , speed(0) , 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) , min_frames_ahead(4) , timeline_max_frame(0) , reader(nullptr) , force_directional_cache(false) , last_cached_index(0) { } // Destructor VideoCacheThread::~VideoCacheThread() { } // Is cache ready for playback (pre-roll) bool VideoCacheThread::isReady() { if (!reader) { return false; } const int64_t ready_min = min_frames_ahead.load(); if (ready_min < 0) { return true; } const int64_t cached_index = last_cached_index.load(); const int64_t playhead = requested_display_frame.load(); int dir = computeDirection(); // Near timeline boundaries, don't require more pre-roll than can exist. int64_t max_frame = reader->info.video_length; if (auto* timeline = dynamic_cast(reader)) { const int64_t timeline_max = timeline->GetMaxFrame(); if (timeline_max > 0) { max_frame = timeline_max; } } if (max_frame < 1) { return false; } int64_t required_ahead = min_frames_ahead; if (required_ahead < 0) { required_ahead = 0; } int64_t available_ahead = (dir > 0) ? std::max(0, max_frame - requested_display_frame) : std::max(0, requested_display_frame - 1); required_ahead = std::min(required_ahead, available_ahead); if (dir > 0) { return (cached_index >= playhead + required_ahead); } return (cached_index <= playhead - required_ahead); } void VideoCacheThread::setSpeed(int new_speed) { // Only update last_speed and last_dir when new_speed != 0 if (new_speed != 0) { last_speed.store(new_speed); last_dir.store(new_speed > 0 ? 1 : -1); } speed.store(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; } /// Start the cache thread at high priority, and return true if it’s actually running. bool VideoCacheThread::StartThread() { // JUCE’s startThread() returns void, so we launch it and then check if // the thread actually started: startThread(Priority::high); return isThreadRunning(); } /// Stop the cache thread, waiting up to timeoutMs ms. Returns true if it actually stopped. bool VideoCacheThread::StopThread(int timeoutMs) { stopThread(timeoutMs); return !isThreadRunning(); } void VideoCacheThread::Seek(int64_t new_position, bool start_preroll) { bool should_mark_seek = false; bool should_preroll = false; int64_t new_cached_count = cached_frame_count.load(); if (start_preroll) { should_mark_seek = true; CacheBase* cache = reader ? reader->GetCache() : nullptr; if (cache && !cache->Contains(new_position)) { // If user initiated seek, and current frame not found ( if (Timeline* timeline = dynamic_cast(reader)) { timeline->ClearAllCache(); } new_cached_count = 0; should_preroll = true; } else if (cache) { new_cached_count = cache->Count(); } } { std::lock_guard guard(seek_state_mutex); requested_display_frame.store(new_position); cached_frame_count.store(new_cached_count); if (start_preroll) { preroll_on_next_fill.store(should_preroll); userSeeked.store(should_mark_seek); } } } void VideoCacheThread::Seek(int64_t new_position) { Seek(new_position, false); } int VideoCacheThread::computeDirection() const { // If speed ≠ 0, use its sign; if speed==0, keep last_dir const int current_speed = speed.load(); if (current_speed != 0) { return (current_speed > 0 ? 1 : -1); } return last_dir.load(); } void VideoCacheThread::handleUserSeek(int64_t playhead, int dir) { // Place last_cached_index just “behind” playhead in the given dir last_cached_index.store(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.store(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) { if (paused && !cache->Contains(playhead)) { // If paused and playhead not in cache, clear everything if (Timeline* timeline = dynamic_cast(reader)) { timeline->ClearAllCache(); } cached_frame_count.store(0); 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.load() + 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.load()) { break; } if (!cache->Contains(next_frame)) { // Frame missing, fetch and add try { auto framePtr = reader->GetFrame(next_frame); cache->Add(framePtr); cached_frame_count.store(cache->Count()); } catch (const OutOfBoundsFrame&) { break; } window_full = false; } else { cache->Touch(next_frame); } last_cached_index.store(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, mark cache as ready and sleep briefly if (!settings->ENABLE_PLAYBACK_CACHING || !cache) { cached_frame_count.store(cache ? cache->Count() : 0); min_frames_ahead.store(-1); std::this_thread::sleep_for(double_micro_sec(50000)); continue; } // init local vars min_frames_ahead.store(settings->VIDEO_CACHE_MIN_PREROLL_FRAMES); Timeline* timeline = dynamic_cast(reader); if (!timeline) { std::this_thread::sleep_for(double_micro_sec(50000)); continue; } int64_t timeline_end = timeline->GetMaxFrame(); int64_t playhead = requested_display_frame.load(); bool paused = (speed.load() == 0); int64_t preroll_frames = computePrerollFrames(settings); cached_frame_count.store(cache->Count()); // Compute effective direction (±1) int dir = computeDirection(); if (speed.load() != 0) { last_dir.store(dir); } // Compute bytes_per_frame, max_bytes, and capacity once 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(); int64_t capacity = 0; if (max_bytes > 0 && bytes_per_frame > 0) { capacity = max_bytes / bytes_per_frame; if (capacity > settings->VIDEO_CACHE_MAX_FRAMES) { capacity = settings->VIDEO_CACHE_MAX_FRAMES; } } // Handle a user-initiated seek bool did_user_seek = false; bool use_preroll = false; { std::lock_guard guard(seek_state_mutex); playhead = requested_display_frame.load(); did_user_seek = userSeeked.load(); use_preroll = preroll_on_next_fill.load(); if (did_user_seek) { userSeeked.store(false); preroll_on_next_fill.store(false); } } if (did_user_seek) { if (use_preroll) { handleUserSeekWithPreroll(playhead, dir, timeline_end, preroll_frames); } else { handleUserSeek(playhead, dir); } } else if (!paused && capacity >= 1) { // In playback mode, check if last_cached_index drifted outside the new window int64_t base_ahead = static_cast(capacity * settings->VIDEO_CACHE_PERCENT_AHEAD); int64_t window_begin, window_end; computeWindowBounds( playhead, dir, base_ahead, timeline_end, window_begin, window_end ); bool outside_window = (dir > 0 && last_cached_index.load() > window_end) || (dir < 0 && last_cached_index.load() < window_begin); if (outside_window) { handleUserSeek(playhead, dir); } } // If capacity is insufficient, sleep and retry 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); int64_t window_size = ahead_count + 1; if (window_size < 1) { window_size = 1; } int64_t ready_target = window_size - 1; if (ready_target < 0) { ready_target = 0; } int64_t configured_min = settings->VIDEO_CACHE_MIN_PREROLL_FRAMES; min_frames_ahead.store(std::min(configured_min, ready_target)); // If paused and playhead is no longer in cache, clear everything bool did_clear = clearCacheIfPaused(playhead, paused, cache); if (did_clear) { handleUserSeekWithPreroll(playhead, dir, timeline_end, preroll_frames); } // 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