/** * @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 namespace openshot { // Constructor VideoCacheThread::VideoCacheThread() : Thread("video-cache") , speed(0) , last_speed(1) , last_dir(1) // Assume forward (+1) on first launch , is_playing(false) , userSeeked(false) , requested_display_frame(1) , current_display_frame(1) , cached_frame_count(0) , 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) { } // Destructor VideoCacheThread::~VideoCacheThread() { } // 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 void VideoCacheThread::Stop() { is_playing = false; } // 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() { using micro_sec = std::chrono::microseconds; using double_micro_sec = std::chrono::duration; int64_t last_cached_index = 0; int64_t cache_start_index = 0; bool last_paused = true; 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 the effective direction: // If speed != 0, dir = sign(speed). // Otherwise (speed == 0), use last_dir to continue caching in the same direction. int dir = (speed != 0 ? (speed > 0 ? 1 : -1) : last_dir); // On transition from paused (speed==0) to playing (speed!=0), reset window base. if (!paused && last_paused) { cache_start_index = playhead; last_cached_index = playhead - dir; } last_paused = paused; // Calculate bytes needed for one frame in cache 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; } // Determine how many frames ahead to cache based on settings int64_t ahead_count = static_cast(capacity * settings->VIDEO_CACHE_PERCENT_AHEAD); // Handle user-initiated seek: reset window base if requested bool user_seek = userSeeked; if (user_seek) { cache_start_index = playhead; last_cached_index = playhead - dir; userSeeked = false; } // -------------------------------------------------------------------- // PAUSED BRANCH: Continue caching in 'dir' without advancing playhead // -------------------------------------------------------------------- if (paused) { // If the playhead is not currently in cache, clear cache and restart if (!cache->Contains(playhead)) { timeline->ClearAllCache(); cache_start_index = playhead; last_cached_index = playhead - dir; } // Build the cache window in the effective direction if (dir > 0) { // Forward: [cache_start_index ... cache_start_index + ahead_count] int64_t window_end = cache_start_index + ahead_count; window_end = std::min(window_end, timeline_end); // If all frames in this forward window are already cached, touch playhead and sleep bool window_full = true; for (int64_t frame = playhead; frame <= window_end; ++frame) { if (!cache->Contains(frame)) { window_full = false; break; } } if (window_full) { cache->Touch(playhead); std::this_thread::sleep_for(double_micro_sec(50000)); continue; } // Prefetch missing frames forward int64_t start_index = std::max(last_cached_index + 1, cache_start_index); for (int64_t frame = start_index; frame <= window_end; ++frame) { if (threadShouldExit()) { break; } if (!cache->Contains(frame)) { try { auto framePtr = reader->GetFrame(frame); cache->Add(framePtr); ++cached_frame_count; } catch (const OutOfBoundsFrame&) { break; } } else { cache->Touch(frame); } last_cached_index = frame; } } else { // Backward: [cache_start_index - ahead_count ... cache_start_index] int64_t window_begin = cache_start_index - ahead_count; window_begin = std::max(window_begin, 1); // If all frames in this backward window are cached, touch playhead and sleep bool window_full = true; for (int64_t frame = playhead; frame >= window_begin; --frame) { if (!cache->Contains(frame)) { window_full = false; break; } } if (window_full) { cache->Touch(playhead); std::this_thread::sleep_for(double_micro_sec(50000)); continue; } // Prefetch missing frames backward int64_t start_index = std::min(last_cached_index - 1, cache_start_index); for (int64_t frame = start_index; frame >= window_begin; --frame) { if (threadShouldExit()) { break; } if (!cache->Contains(frame)) { try { auto framePtr = reader->GetFrame(frame); cache->Add(framePtr); ++cached_frame_count; } catch (const OutOfBoundsFrame&) { break; } } else { cache->Touch(frame); } last_cached_index = frame; } } // Sleep for a fraction of a frame interval to throttle CPU usage int64_t pause_sleep = static_cast( 1000000.0 / reader->info.fps.ToFloat() / 4.0 ); std::this_thread::sleep_for(double_micro_sec(pause_sleep)); continue; } // -------------------------------------------------------------------- // PLAYING BRANCH: Cache around the playhead in the playback direction // -------------------------------------------------------------------- if (dir > 0 && playhead > last_cached_index) { // Forward playback has moved beyond the last cached frame: reset window cache_start_index = playhead; last_cached_index = playhead - 1; } else if (dir < 0 && playhead < last_cached_index) { // Backward playback has moved before the last cached frame: reset window cache_start_index = playhead; last_cached_index = playhead + 1; } if (dir >= 0) { // Forward caching: [playhead ... playhead + ahead_count] int64_t window_end = playhead + ahead_count; window_end = std::min(window_end, timeline_end); int64_t start_index = std::max(last_cached_index + 1, playhead); for (int64_t frame = start_index; frame <= window_end; ++frame) { if (threadShouldExit() || (userSeeked && !paused)) { if (userSeeked) { last_cached_index = playhead - 1; userSeeked = false; } break; } if (!cache->Contains(frame)) { try { auto framePtr = reader->GetFrame(frame); cache->Add(framePtr); ++cached_frame_count; } catch (const OutOfBoundsFrame&) { break; } } else { cache->Touch(frame); } last_cached_index = frame; } } else { // Backward caching: [playhead - ahead_count ... playhead] int64_t window_begin = playhead - ahead_count; window_begin = std::max(window_begin, 1); int64_t start_index = std::min(last_cached_index - 1, playhead); for (int64_t frame = start_index; frame >= window_begin; --frame) { if (threadShouldExit() || (userSeeked && !paused)) { if (userSeeked) { last_cached_index = playhead + 1; userSeeked = false; } break; } if (!cache->Contains(frame)) { try { auto framePtr = reader->GetFrame(frame); cache->Add(framePtr); ++cached_frame_count; } catch (const OutOfBoundsFrame&) { break; } } else { cache->Touch(frame); } last_cached_index = frame; } } // Sleep for a fraction of a frame interval int64_t quarter_us = static_cast( 1000000.0 / reader->info.fps.ToFloat() / 4.0 ); std::this_thread::sleep_for(double_micro_sec(quarter_us)); } } void VideoCacheThread::Seek(int64_t new_position, bool start_preroll) { if (start_preroll) { userSeeked = true; } requested_display_frame = new_position; } void VideoCacheThread::Seek(int64_t new_position) { Seek(new_position, false); } } // namespace openshot