2015-06-01 00:20:14 -07:00
|
|
|
|
/**
|
|
|
|
|
|
* @file
|
|
|
|
|
|
* @brief Source file for VideoCacheThread class
|
|
|
|
|
|
* @author Jonathan Thomas <jonathan@openshot.org>
|
|
|
|
|
|
*
|
2019-06-09 08:31:04 -04:00
|
|
|
|
* @ref License
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2025-06-03 16:23:17 -05:00
|
|
|
|
// Copyright (c) 2008-2025 OpenShot Studios, LLC
|
2021-10-16 01:26:26 -04:00
|
|
|
|
//
|
|
|
|
|
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
2015-06-01 00:20:14 -07:00
|
|
|
|
|
2020-10-18 07:43:37 -04:00
|
|
|
|
#include "VideoCacheThread.h"
|
2021-10-27 14:34:05 -04:00
|
|
|
|
#include "CacheBase.h"
|
2021-01-26 10:52:04 -05:00
|
|
|
|
#include "Exceptions.h"
|
2021-10-27 14:34:05 -04:00
|
|
|
|
#include "Frame.h"
|
2022-09-15 18:33:06 -05:00
|
|
|
|
#include "Settings.h"
|
|
|
|
|
|
#include "Timeline.h"
|
2025-06-03 16:23:17 -05:00
|
|
|
|
#include <thread>
|
|
|
|
|
|
#include <chrono>
|
2025-06-03 17:49:56 -05:00
|
|
|
|
#include <algorithm>
|
2020-09-02 02:07:54 -04:00
|
|
|
|
|
2015-06-01 00:20:14 -07:00
|
|
|
|
namespace openshot
|
|
|
|
|
|
{
|
2025-06-03 16:23:17 -05:00
|
|
|
|
// Constructor
|
|
|
|
|
|
VideoCacheThread::VideoCacheThread()
|
|
|
|
|
|
: Thread("video-cache")
|
|
|
|
|
|
, speed(0)
|
|
|
|
|
|
, last_speed(1)
|
2025-06-03 19:36:05 -05:00
|
|
|
|
, last_dir(1) // assume forward (+1) on first launch
|
2025-06-03 16:23:17 -05:00
|
|
|
|
, userSeeked(false)
|
2026-02-05 12:39:06 -06:00
|
|
|
|
, preroll_on_next_fill(false)
|
2025-06-03 16:23:17 -05:00
|
|
|
|
, 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)
|
2025-06-03 19:36:05 -05:00
|
|
|
|
, last_cached_index(0)
|
2015-06-01 00:20:14 -07:00
|
|
|
|
{
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Destructor
|
2025-06-03 16:23:17 -05:00
|
|
|
|
VideoCacheThread::~VideoCacheThread()
|
2015-06-01 00:20:14 -07:00
|
|
|
|
{
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-03 16:23:17 -05:00
|
|
|
|
// Is cache ready for playback (pre-roll)
|
|
|
|
|
|
bool VideoCacheThread::isReady()
|
|
|
|
|
|
{
|
2025-09-15 18:20:05 -05:00
|
|
|
|
if (!reader) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 20:11:47 -06:00
|
|
|
|
const int64_t ready_min = min_frames_ahead.load();
|
|
|
|
|
|
if (ready_min < 0) {
|
2025-09-15 18:20:05 -05:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 20:11:47 -06:00
|
|
|
|
const int64_t cached_index = last_cached_index.load();
|
|
|
|
|
|
const int64_t playhead = requested_display_frame.load();
|
2026-02-05 12:39:06 -06:00
|
|
|
|
int dir = computeDirection();
|
|
|
|
|
|
if (dir > 0) {
|
2026-02-11 20:11:47 -06:00
|
|
|
|
return (cached_index >= playhead + ready_min);
|
2026-02-05 12:39:06 -06:00
|
|
|
|
}
|
2026-02-11 20:11:47 -06:00
|
|
|
|
return (cached_index <= playhead - ready_min);
|
2025-06-03 16:23:17 -05:00
|
|
|
|
}
|
2022-01-26 17:56:33 -06:00
|
|
|
|
|
2025-06-03 19:36:05 -05:00
|
|
|
|
void VideoCacheThread::setSpeed(int new_speed)
|
2015-06-01 00:20:14 -07:00
|
|
|
|
{
|
2025-06-03 19:36:05 -05:00
|
|
|
|
// Only update last_speed and last_dir when new_speed != 0
|
|
|
|
|
|
if (new_speed != 0) {
|
2026-02-11 20:11:47 -06:00
|
|
|
|
last_speed.store(new_speed);
|
|
|
|
|
|
last_dir.store(new_speed > 0 ? 1 : -1);
|
2025-06-03 16:23:17 -05:00
|
|
|
|
}
|
2026-02-11 20:11:47 -06:00
|
|
|
|
speed.store(new_speed);
|
2025-06-03 19:36:05 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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<int64_t>(width) * height * sizeof(char) * 4;
|
|
|
|
|
|
// Approximate audio: (sample_rate * channels)/fps samples per frame
|
|
|
|
|
|
bytes += ((sample_rate * channels) / fps) * sizeof(float);
|
|
|
|
|
|
return bytes;
|
2015-06-01 00:20:14 -07:00
|
|
|
|
}
|
2025-06-03 16:23:17 -05:00
|
|
|
|
|
2025-06-06 15:25:46 -05:00
|
|
|
|
/// 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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-03 16:23:17 -05:00
|
|
|
|
void VideoCacheThread::Seek(int64_t new_position, bool start_preroll)
|
|
|
|
|
|
{
|
2026-02-11 20:11:47 -06:00
|
|
|
|
bool should_mark_seek = false;
|
|
|
|
|
|
bool should_preroll = false;
|
|
|
|
|
|
int64_t new_cached_count = cached_frame_count.load();
|
2025-06-07 16:05:19 -05:00
|
|
|
|
|
2026-02-11 20:11:47 -06:00
|
|
|
|
if (start_preroll) {
|
|
|
|
|
|
should_mark_seek = true;
|
2025-09-15 18:20:05 -05:00
|
|
|
|
CacheBase* cache = reader ? reader->GetCache() : nullptr;
|
|
|
|
|
|
|
|
|
|
|
|
if (cache && !cache->Contains(new_position))
|
2025-06-07 16:05:19 -05:00
|
|
|
|
{
|
|
|
|
|
|
// If user initiated seek, and current frame not found (
|
2026-02-11 20:11:47 -06:00
|
|
|
|
if (Timeline* timeline = dynamic_cast<Timeline*>(reader)) {
|
|
|
|
|
|
timeline->ClearAllCache();
|
|
|
|
|
|
}
|
|
|
|
|
|
new_cached_count = 0;
|
|
|
|
|
|
should_preroll = true;
|
2025-09-15 18:20:05 -05:00
|
|
|
|
}
|
|
|
|
|
|
else if (cache)
|
|
|
|
|
|
{
|
2026-02-11 20:11:47 -06:00
|
|
|
|
new_cached_count = cache->Count();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
std::lock_guard<std::mutex> 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);
|
2025-06-07 16:05:19 -05:00
|
|
|
|
}
|
2025-06-03 16:23:17 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void VideoCacheThread::Seek(int64_t new_position)
|
|
|
|
|
|
{
|
|
|
|
|
|
Seek(new_position, false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-03 19:36:05 -05:00
|
|
|
|
int VideoCacheThread::computeDirection() const
|
|
|
|
|
|
{
|
|
|
|
|
|
// If speed ≠ 0, use its sign; if speed==0, keep last_dir
|
2026-02-11 20:11:47 -06:00
|
|
|
|
const int current_speed = speed.load();
|
|
|
|
|
|
if (current_speed != 0) {
|
|
|
|
|
|
return (current_speed > 0 ? 1 : -1);
|
|
|
|
|
|
}
|
|
|
|
|
|
return last_dir.load();
|
2025-06-03 19:36:05 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void VideoCacheThread::handleUserSeek(int64_t playhead, int dir)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Place last_cached_index just “behind” playhead in the given dir
|
2026-02-11 20:11:47 -06:00
|
|
|
|
last_cached_index.store(playhead - dir);
|
2025-06-03 19:36:05 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 12:39:06 -06:00
|
|
|
|
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<int64_t>(1, playhead - preroll_frames);
|
|
|
|
|
|
}
|
|
|
|
|
|
else {
|
|
|
|
|
|
preroll_start = std::min<int64_t>(timeline_end, playhead + preroll_frames);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-11 20:11:47 -06:00
|
|
|
|
last_cached_index.store(preroll_start - dir);
|
2026-02-05 12:39:06 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-03 19:36:05 -05:00
|
|
|
|
bool VideoCacheThread::clearCacheIfPaused(int64_t playhead,
|
|
|
|
|
|
bool paused,
|
|
|
|
|
|
CacheBase* cache)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (paused && !cache->Contains(playhead)) {
|
|
|
|
|
|
// If paused and playhead not in cache, clear everything
|
2026-02-11 20:11:47 -06:00
|
|
|
|
if (Timeline* timeline = dynamic_cast<Timeline*>(reader)) {
|
|
|
|
|
|
timeline->ClearAllCache();
|
|
|
|
|
|
}
|
|
|
|
|
|
cached_frame_count.store(0);
|
2025-06-03 19:36:05 -05:00
|
|
|
|
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<int64_t>(window_begin, 1);
|
|
|
|
|
|
window_end = std::min<int64_t>(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;
|
2026-02-11 20:11:47 -06:00
|
|
|
|
int64_t next_frame = last_cached_index.load() + dir;
|
2025-06-03 19:36:05 -05:00
|
|
|
|
|
|
|
|
|
|
// 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
|
2026-02-11 20:11:47 -06:00
|
|
|
|
if (userSeeked.load()) {
|
2025-06-03 19:36:05 -05:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!cache->Contains(next_frame)) {
|
|
|
|
|
|
// Frame missing, fetch and add
|
|
|
|
|
|
try {
|
|
|
|
|
|
auto framePtr = reader->GetFrame(next_frame);
|
|
|
|
|
|
cache->Add(framePtr);
|
2026-02-11 20:11:47 -06:00
|
|
|
|
cached_frame_count.store(cache->Count());
|
2025-06-03 19:36:05 -05:00
|
|
|
|
}
|
|
|
|
|
|
catch (const OutOfBoundsFrame&) {
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
window_full = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
else {
|
|
|
|
|
|
cache->Touch(next_frame);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 20:11:47 -06:00
|
|
|
|
last_cached_index.store(next_frame);
|
2025-06-03 19:36:05 -05:00
|
|
|
|
next_frame += dir;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return window_full;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void VideoCacheThread::run()
|
|
|
|
|
|
{
|
|
|
|
|
|
using micro_sec = std::chrono::microseconds;
|
|
|
|
|
|
using double_micro_sec = std::chrono::duration<double, micro_sec::period>;
|
|
|
|
|
|
|
|
|
|
|
|
while (!threadShouldExit()) {
|
|
|
|
|
|
Settings* settings = Settings::Instance();
|
|
|
|
|
|
CacheBase* cache = reader ? reader->GetCache() : nullptr;
|
|
|
|
|
|
|
2025-09-15 18:20:05 -05:00
|
|
|
|
// If caching disabled or no reader, mark cache as ready and sleep briefly
|
2025-06-07 16:05:19 -05:00
|
|
|
|
if (!settings->ENABLE_PLAYBACK_CACHING || !cache) {
|
2026-02-11 20:11:47 -06:00
|
|
|
|
cached_frame_count.store(cache ? cache->Count() : 0);
|
|
|
|
|
|
min_frames_ahead.store(-1);
|
2025-06-03 19:36:05 -05:00
|
|
|
|
std::this_thread::sleep_for(double_micro_sec(50000));
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-06 15:25:46 -05:00
|
|
|
|
// init local vars
|
2026-02-11 20:11:47 -06:00
|
|
|
|
min_frames_ahead.store(settings->VIDEO_CACHE_MIN_PREROLL_FRAMES);
|
2025-06-06 15:25:46 -05:00
|
|
|
|
|
2026-02-11 20:11:47 -06:00
|
|
|
|
Timeline* timeline = dynamic_cast<Timeline*>(reader);
|
|
|
|
|
|
if (!timeline) {
|
|
|
|
|
|
std::this_thread::sleep_for(double_micro_sec(50000));
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2025-06-03 19:36:05 -05:00
|
|
|
|
int64_t timeline_end = timeline->GetMaxFrame();
|
2026-02-11 20:11:47 -06:00
|
|
|
|
int64_t playhead = requested_display_frame.load();
|
|
|
|
|
|
bool paused = (speed.load() == 0);
|
2026-02-05 12:39:06 -06:00
|
|
|
|
int64_t preroll_frames = computePrerollFrames(settings);
|
2025-06-03 19:36:05 -05:00
|
|
|
|
|
2026-02-11 20:11:47 -06:00
|
|
|
|
cached_frame_count.store(cache->Count());
|
2025-09-15 18:20:05 -05:00
|
|
|
|
|
2025-06-03 19:36:05 -05:00
|
|
|
|
// Compute effective direction (±1)
|
|
|
|
|
|
int dir = computeDirection();
|
2026-02-11 20:11:47 -06:00
|
|
|
|
if (speed.load() != 0) {
|
|
|
|
|
|
last_dir.store(dir);
|
2025-06-03 19:36:05 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-06 15:25:46 -05:00
|
|
|
|
// Compute bytes_per_frame, max_bytes, and capacity once
|
2025-06-03 19:36:05 -05:00
|
|
|
|
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();
|
2025-06-06 15:25:46 -05:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2025-06-03 19:36:05 -05:00
|
|
|
|
}
|
2025-06-06 15:25:46 -05:00
|
|
|
|
|
|
|
|
|
|
// Handle a user-initiated seek
|
2026-02-11 20:11:47 -06:00
|
|
|
|
bool did_user_seek = false;
|
|
|
|
|
|
bool use_preroll = false;
|
|
|
|
|
|
{
|
|
|
|
|
|
std::lock_guard<std::mutex> 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) {
|
2026-02-05 12:39:06 -06:00
|
|
|
|
if (use_preroll) {
|
|
|
|
|
|
handleUserSeekWithPreroll(playhead, dir, timeline_end, preroll_frames);
|
|
|
|
|
|
}
|
|
|
|
|
|
else {
|
|
|
|
|
|
handleUserSeek(playhead, dir);
|
|
|
|
|
|
}
|
2025-06-06 15:25:46 -05:00
|
|
|
|
}
|
|
|
|
|
|
else if (!paused && capacity >= 1) {
|
|
|
|
|
|
// In playback mode, check if last_cached_index drifted outside the new window
|
|
|
|
|
|
int64_t base_ahead = static_cast<int64_t>(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 =
|
2026-02-11 20:11:47 -06:00
|
|
|
|
(dir > 0 && last_cached_index.load() > window_end) ||
|
|
|
|
|
|
(dir < 0 && last_cached_index.load() < window_begin);
|
2025-06-06 15:25:46 -05:00
|
|
|
|
if (outside_window) {
|
|
|
|
|
|
handleUserSeek(playhead, dir);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If capacity is insufficient, sleep and retry
|
2025-06-03 19:36:05 -05:00
|
|
|
|
if (capacity < 1) {
|
|
|
|
|
|
std::this_thread::sleep_for(double_micro_sec(50000));
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
int64_t ahead_count = static_cast<int64_t>(capacity *
|
|
|
|
|
|
settings->VIDEO_CACHE_PERCENT_AHEAD);
|
2025-09-15 18:20:05 -05:00
|
|
|
|
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;
|
2026-02-11 20:11:47 -06:00
|
|
|
|
min_frames_ahead.store(std::min<int64_t>(configured_min, ready_target));
|
2025-06-03 19:36:05 -05:00
|
|
|
|
|
|
|
|
|
|
// If paused and playhead is no longer in cache, clear everything
|
|
|
|
|
|
bool did_clear = clearCacheIfPaused(playhead, paused, cache);
|
|
|
|
|
|
if (did_clear) {
|
2026-02-05 12:39:06 -06:00
|
|
|
|
handleUserSeekWithPreroll(playhead, dir, timeline_end, preroll_frames);
|
2025-06-03 19:36:05 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
2025-06-06 15:25:46 -05:00
|
|
|
|
bool window_full = prefetchWindow(cache, window_begin, window_end, dir, reader);
|
2025-06-03 19:36:05 -05:00
|
|
|
|
|
|
|
|
|
|
// 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<int64_t>(
|
|
|
|
|
|
1000000.0 / reader->info.fps.ToFloat() / 4.0
|
|
|
|
|
|
);
|
|
|
|
|
|
std::this_thread::sleep_for(double_micro_sec(sleep_us));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-03 16:23:17 -05:00
|
|
|
|
} // namespace openshot
|