OpenShot Library | libopenshot  0.5.0
VideoCacheThread.cpp
Go to the documentation of this file.
1 
9 // Copyright (c) 2008-2025 OpenShot Studios, LLC
10 //
11 // SPDX-License-Identifier: LGPL-3.0-or-later
12 
13 #include "VideoCacheThread.h"
14 #include "CacheBase.h"
15 #include "Exceptions.h"
16 #include "Frame.h"
17 #include "Settings.h"
18 #include "Timeline.h"
19 #include <thread>
20 #include <chrono>
21 #include <algorithm>
22 
23 namespace openshot
24 {
25  // Constructor
27  : Thread("video-cache")
28  , speed(0)
29  , last_speed(1)
30  , last_dir(1) // assume forward (+1) on first launch
31  , userSeeked(false)
32  , preroll_on_next_fill(false)
33  , requested_display_frame(1)
34  , current_display_frame(1)
35  , cached_frame_count(0)
36  , min_frames_ahead(4)
37  , timeline_max_frame(0)
38  , reader(nullptr)
39  , force_directional_cache(false)
40  , last_cached_index(0)
41  {
42  }
43 
44  // Destructor
46  {
47  }
48 
49  // Is cache ready for playback (pre-roll)
51  {
52  if (!reader) {
53  return false;
54  }
55 
56  const int64_t ready_min = min_frames_ahead.load();
57  if (ready_min < 0) {
58  return true;
59  }
60 
61  const int64_t cached_index = last_cached_index.load();
62  const int64_t playhead = requested_display_frame.load();
63  int dir = computeDirection();
64 
65  // Near timeline boundaries, don't require more pre-roll than can exist.
66  int64_t max_frame = reader->info.video_length;
67  if (auto* timeline = dynamic_cast<Timeline*>(reader)) {
68  const int64_t timeline_max = timeline->GetMaxFrame();
69  if (timeline_max > 0) {
70  max_frame = timeline_max;
71  }
72  }
73  if (max_frame < 1) {
74  return false;
75  }
76 
77  int64_t required_ahead = ready_min;
78  int64_t available_ahead = (dir > 0)
79  ? std::max<int64_t>(0, max_frame - playhead)
80  : std::max<int64_t>(0, playhead - 1);
81  required_ahead = std::min(required_ahead, available_ahead);
82 
83  if (dir > 0) {
84  return (cached_index >= playhead + required_ahead);
85  }
86  return (cached_index <= playhead - required_ahead);
87  }
88 
89  void VideoCacheThread::setSpeed(int new_speed)
90  {
91  // Only update last_speed and last_dir when new_speed != 0
92  if (new_speed != 0) {
93  last_speed.store(new_speed);
94  last_dir.store(new_speed > 0 ? 1 : -1);
95  }
96  speed.store(new_speed);
97  }
98 
99  // Get the size in bytes of a frame (rough estimate)
100  int64_t VideoCacheThread::getBytes(int width,
101  int height,
102  int sample_rate,
103  int channels,
104  float fps)
105  {
106  // RGBA video frame
107  int64_t bytes = static_cast<int64_t>(width) * height * sizeof(char) * 4;
108  // Approximate audio: (sample_rate * channels)/fps samples per frame
109  bytes += ((sample_rate * channels) / fps) * sizeof(float);
110  return bytes;
111  }
112 
115  {
116  // JUCE’s startThread() returns void, so we launch it and then check if
117  // the thread actually started:
118  startThread(Priority::high);
119  return isThreadRunning();
120  }
121 
123  bool VideoCacheThread::StopThread(int timeoutMs)
124  {
125  stopThread(timeoutMs);
126  return !isThreadRunning();
127  }
128 
129  void VideoCacheThread::Seek(int64_t new_position, bool start_preroll)
130  {
131  bool should_mark_seek = false;
132  bool should_preroll = false;
133  int64_t new_cached_count = cached_frame_count.load();
134 
135  if (start_preroll) {
136  should_mark_seek = true;
137  CacheBase* cache = reader ? reader->GetCache() : nullptr;
138 
139  if (cache && !cache->Contains(new_position))
140  {
141  // If user initiated seek, and current frame not found (
142  if (Timeline* timeline = dynamic_cast<Timeline*>(reader)) {
143  timeline->ClearAllCache();
144  }
145  new_cached_count = 0;
146  should_preroll = true;
147  }
148  else if (cache)
149  {
150  new_cached_count = cache->Count();
151  }
152  }
153 
154  {
155  std::lock_guard<std::mutex> guard(seek_state_mutex);
156  requested_display_frame.store(new_position);
157  cached_frame_count.store(new_cached_count);
158  if (start_preroll) {
159  preroll_on_next_fill.store(should_preroll);
160  userSeeked.store(should_mark_seek);
161  }
162  }
163  }
164 
165  void VideoCacheThread::Seek(int64_t new_position)
166  {
167  Seek(new_position, false);
168  }
169 
171  {
172  // If speed ≠ 0, use its sign; if speed==0, keep last_dir
173  const int current_speed = speed.load();
174  if (current_speed != 0) {
175  return (current_speed > 0 ? 1 : -1);
176  }
177  return last_dir.load();
178  }
179 
180  void VideoCacheThread::handleUserSeek(int64_t playhead, int dir)
181  {
182  // Place last_cached_index just “behind” playhead in the given dir
183  last_cached_index.store(playhead - dir);
184  }
185 
187  int dir,
188  int64_t timeline_end,
189  int64_t preroll_frames)
190  {
191  int64_t preroll_start = playhead;
192  if (preroll_frames > 0) {
193  if (dir > 0) {
194  preroll_start = std::max<int64_t>(1, playhead - preroll_frames);
195  }
196  else {
197  preroll_start = std::min<int64_t>(timeline_end, playhead + preroll_frames);
198  }
199  }
200  last_cached_index.store(preroll_start - dir);
201  }
202 
203  int64_t VideoCacheThread::computePrerollFrames(const Settings* settings) const
204  {
205  if (!settings) {
206  return 0;
207  }
208  int64_t min_frames = settings->VIDEO_CACHE_MIN_PREROLL_FRAMES;
209  int64_t max_frames = settings->VIDEO_CACHE_MAX_PREROLL_FRAMES;
210  if (min_frames < 0) {
211  return 0;
212  }
213  if (max_frames > 0 && min_frames > max_frames) {
214  min_frames = max_frames;
215  }
216  return min_frames;
217  }
218 
220  bool paused,
221  CacheBase* cache)
222  {
223  if (paused && !cache->Contains(playhead)) {
224  // If paused and playhead not in cache, clear everything
225  if (Timeline* timeline = dynamic_cast<Timeline*>(reader)) {
226  timeline->ClearAllCache();
227  }
228  cached_frame_count.store(0);
229  return true;
230  }
231  return false;
232  }
233 
235  int dir,
236  int64_t ahead_count,
237  int64_t timeline_end,
238  int64_t& window_begin,
239  int64_t& window_end) const
240  {
241  if (dir > 0) {
242  // Forward window: [playhead ... playhead + ahead_count]
243  window_begin = playhead;
244  window_end = playhead + ahead_count;
245  }
246  else {
247  // Backward window: [playhead - ahead_count ... playhead]
248  window_begin = playhead - ahead_count;
249  window_end = playhead;
250  }
251  // Clamp to [1 ... timeline_end]
252  window_begin = std::max<int64_t>(window_begin, 1);
253  window_end = std::min<int64_t>(window_end, timeline_end);
254  }
255 
257  int64_t window_begin,
258  int64_t window_end,
259  int dir,
260  ReaderBase* reader)
261  {
262  bool window_full = true;
263  int64_t next_frame = last_cached_index.load() + dir;
264 
265  // Advance from last_cached_index toward window boundary
266  while ((dir > 0 && next_frame <= window_end) ||
267  (dir < 0 && next_frame >= window_begin))
268  {
269  if (threadShouldExit()) {
270  break;
271  }
272  // If a Seek was requested mid-caching, bail out immediately
273  if (userSeeked.load()) {
274  break;
275  }
276 
277  if (!cache->Contains(next_frame)) {
278  // Frame missing, fetch and add
279  try {
280  auto framePtr = reader->GetFrame(next_frame);
281  cache->Add(framePtr);
282  cached_frame_count.store(cache->Count());
283  }
284  catch (const OutOfBoundsFrame&) {
285  break;
286  }
287  window_full = false;
288  }
289  else {
290  cache->Touch(next_frame);
291  }
292 
293  last_cached_index.store(next_frame);
294  next_frame += dir;
295  }
296 
297  return window_full;
298  }
299 
301  {
302  using micro_sec = std::chrono::microseconds;
303  using double_micro_sec = std::chrono::duration<double, micro_sec::period>;
304 
305  while (!threadShouldExit()) {
306  Settings* settings = Settings::Instance();
307  CacheBase* cache = reader ? reader->GetCache() : nullptr;
308 
309  // If caching disabled or no reader, mark cache as ready and sleep briefly
310  if (!settings->ENABLE_PLAYBACK_CACHING || !cache) {
311  cached_frame_count.store(cache ? cache->Count() : 0);
312  min_frames_ahead.store(-1);
313  std::this_thread::sleep_for(double_micro_sec(50000));
314  continue;
315  }
316 
317  // init local vars
319 
320  Timeline* timeline = dynamic_cast<Timeline*>(reader);
321  if (!timeline) {
322  std::this_thread::sleep_for(double_micro_sec(50000));
323  continue;
324  }
325  int64_t timeline_end = timeline->GetMaxFrame();
326  int64_t playhead = requested_display_frame.load();
327  bool paused = (speed.load() == 0);
328  int64_t preroll_frames = computePrerollFrames(settings);
329 
330  cached_frame_count.store(cache->Count());
331 
332  // Compute effective direction (±1)
333  int dir = computeDirection();
334  if (speed.load() != 0) {
335  last_dir.store(dir);
336  }
337 
338  // Compute bytes_per_frame, max_bytes, and capacity once
339  int64_t bytes_per_frame = getBytes(
340  (timeline->preview_width ? timeline->preview_width : reader->info.width),
341  (timeline->preview_height ? timeline->preview_height : reader->info.height),
345  );
346  int64_t max_bytes = cache->GetMaxBytes();
347  int64_t capacity = 0;
348  if (max_bytes > 0 && bytes_per_frame > 0) {
349  capacity = max_bytes / bytes_per_frame;
350  if (capacity > settings->VIDEO_CACHE_MAX_FRAMES) {
351  capacity = settings->VIDEO_CACHE_MAX_FRAMES;
352  }
353  }
354 
355  // Handle a user-initiated seek
356  bool did_user_seek = false;
357  bool use_preroll = false;
358  {
359  std::lock_guard<std::mutex> guard(seek_state_mutex);
360  playhead = requested_display_frame.load();
361  did_user_seek = userSeeked.load();
362  use_preroll = preroll_on_next_fill.load();
363  if (did_user_seek) {
364  userSeeked.store(false);
365  preroll_on_next_fill.store(false);
366  }
367  }
368  if (did_user_seek) {
369  if (use_preroll) {
370  handleUserSeekWithPreroll(playhead, dir, timeline_end, preroll_frames);
371  }
372  else {
373  handleUserSeek(playhead, dir);
374  }
375  }
376  else if (!paused && capacity >= 1) {
377  // In playback mode, check if last_cached_index drifted outside the new window
378  int64_t base_ahead = static_cast<int64_t>(capacity * settings->VIDEO_CACHE_PERCENT_AHEAD);
379 
380  int64_t window_begin, window_end;
382  playhead,
383  dir,
384  base_ahead,
385  timeline_end,
386  window_begin,
387  window_end
388  );
389 
390  bool outside_window =
391  (dir > 0 && last_cached_index.load() > window_end) ||
392  (dir < 0 && last_cached_index.load() < window_begin);
393  if (outside_window) {
394  handleUserSeek(playhead, dir);
395  }
396  }
397 
398  // If capacity is insufficient, sleep and retry
399  if (capacity < 1) {
400  std::this_thread::sleep_for(double_micro_sec(50000));
401  continue;
402  }
403  int64_t ahead_count = static_cast<int64_t>(capacity *
404  settings->VIDEO_CACHE_PERCENT_AHEAD);
405  int64_t window_size = ahead_count + 1;
406  if (window_size < 1) {
407  window_size = 1;
408  }
409  int64_t ready_target = window_size - 1;
410  if (ready_target < 0) {
411  ready_target = 0;
412  }
413  int64_t configured_min = settings->VIDEO_CACHE_MIN_PREROLL_FRAMES;
414  min_frames_ahead.store(std::min<int64_t>(configured_min, ready_target));
415 
416  // If paused and playhead is no longer in cache, clear everything
417  bool did_clear = clearCacheIfPaused(playhead, paused, cache);
418  if (did_clear) {
419  handleUserSeekWithPreroll(playhead, dir, timeline_end, preroll_frames);
420  }
421 
422  // Compute the current caching window
423  int64_t window_begin, window_end;
424  computeWindowBounds(playhead,
425  dir,
426  ahead_count,
427  timeline_end,
428  window_begin,
429  window_end);
430 
431  // Attempt to fill any missing frames in that window
432  bool window_full = prefetchWindow(cache, window_begin, window_end, dir, reader);
433 
434  // If paused and window was already full, keep playhead fresh
435  if (paused && window_full) {
436  cache->Touch(playhead);
437  }
438 
439  // Sleep a short fraction of a frame interval
440  int64_t sleep_us = static_cast<int64_t>(
441  1000000.0 / reader->info.fps.ToFloat() / 4.0
442  );
443  std::this_thread::sleep_for(double_micro_sec(sleep_us));
444  }
445  }
446 
447 } // namespace openshot
Settings.h
Header file for global Settings class.
openshot::ReaderInfo::sample_rate
int sample_rate
The number of audio samples per second (44100 is a common sample rate)
Definition: ReaderBase.h:60
openshot::VideoCacheThread::VideoCacheThread
VideoCacheThread()
Constructor: initializes member variables and assumes forward direction on first launch.
Definition: VideoCacheThread.cpp:26
openshot::Fraction::ToFloat
float ToFloat()
Return this fraction as a float (i.e. 1/2 = 0.5)
Definition: Fraction.cpp:35
openshot::Settings::VIDEO_CACHE_PERCENT_AHEAD
float VIDEO_CACHE_PERCENT_AHEAD
Percentage of cache in front of the playhead (0.0 to 1.0)
Definition: Settings.h:89
openshot::VideoCacheThread::seek_state_mutex
std::mutex seek_state_mutex
Protects coherent seek state updates/consumption.
Definition: VideoCacheThread.h:197
openshot::TimelineBase::preview_width
int preview_width
Optional preview width of timeline image. If your preview window is smaller than the timeline,...
Definition: TimelineBase.h:44
openshot::VideoCacheThread::StartThread
bool StartThread()
Start the cache thread at high priority. Returns true if it’s actually running.
Definition: VideoCacheThread.cpp:114
openshot::VideoCacheThread::last_speed
std::atomic< int > last_speed
Last non-zero speed (for tracking).
Definition: VideoCacheThread.h:181
openshot::ReaderBase::GetFrame
virtual std::shared_ptr< openshot::Frame > GetFrame(int64_t number)=0
openshot::VideoCacheThread::prefetchWindow
bool prefetchWindow(CacheBase *cache, int64_t window_begin, int64_t window_end, int dir, ReaderBase *reader)
Prefetch all missing frames in [window_begin ... window_end] or [window_end ... window_begin].
Definition: VideoCacheThread.cpp:256
openshot::VideoCacheThread::preroll_on_next_fill
std::atomic< bool > preroll_on_next_fill
True if next cache rebuild should include preroll offset.
Definition: VideoCacheThread.h:184
openshot
This namespace is the default namespace for all code in the openshot library.
Definition: Compressor.h:28
openshot::TimelineBase::preview_height
int preview_height
Optional preview width of timeline image. If your preview window is smaller than the timeline,...
Definition: TimelineBase.h:45
openshot::CacheBase::Add
virtual void Add(std::shared_ptr< openshot::Frame > frame)=0
Add a Frame to the cache.
openshot::VideoCacheThread::last_cached_index
std::atomic< int64_t > last_cached_index
Index of the most recently cached frame.
Definition: VideoCacheThread.h:196
openshot::VideoCacheThread::computeDirection
int computeDirection() const
Definition: VideoCacheThread.cpp:170
openshot::VideoCacheThread::reader
ReaderBase * reader
The source reader (e.g., Timeline, FFmpegReader).
Definition: VideoCacheThread.h:193
openshot::ReaderBase::info
openshot::ReaderInfo info
Information about the current media file.
Definition: ReaderBase.h:88
openshot::Settings
This class is contains settings used by libopenshot (and can be safely toggled at any point)
Definition: Settings.h:26
Timeline.h
Header file for Timeline class.
openshot::VideoCacheThread::handleUserSeek
void handleUserSeek(int64_t playhead, int dir)
If userSeeked is true, reset last_cached_index just behind the playhead.
Definition: VideoCacheThread.cpp:180
openshot::VideoCacheThread::computePrerollFrames
int64_t computePrerollFrames(const Settings *settings) const
Compute preroll frame count from settings.
Definition: VideoCacheThread.cpp:203
openshot::Settings::ENABLE_PLAYBACK_CACHING
bool ENABLE_PLAYBACK_CACHING
Enable/Disable the cache thread to pre-fetch and cache video frames before we need them.
Definition: Settings.h:101
openshot::ReaderInfo::width
int width
The width of the video (in pixesl)
Definition: ReaderBase.h:46
openshot::CacheBase
All cache managers in libopenshot are based on this CacheBase class.
Definition: CacheBase.h:34
openshot::Settings::VIDEO_CACHE_MAX_FRAMES
int VIDEO_CACHE_MAX_FRAMES
Max number of frames (when paused) to cache for playback.
Definition: Settings.h:98
CacheBase.h
Header file for CacheBase class.
openshot::VideoCacheThread::cached_frame_count
std::atomic< int64_t > cached_frame_count
Estimated count of frames currently stored in cache.
Definition: VideoCacheThread.h:188
openshot::OutOfBoundsFrame
Exception for frames that are out of bounds.
Definition: Exceptions.h:300
openshot::VideoCacheThread::~VideoCacheThread
~VideoCacheThread() override
Definition: VideoCacheThread.cpp:45
openshot::ReaderInfo::video_length
int64_t video_length
The number of frames in the video stream.
Definition: ReaderBase.h:53
openshot::ReaderInfo::height
int height
The height of the video (in pixels)
Definition: ReaderBase.h:45
openshot::Settings::VIDEO_CACHE_MAX_PREROLL_FRAMES
int VIDEO_CACHE_MAX_PREROLL_FRAMES
Max number of frames (ahead of playhead) to cache during playback.
Definition: Settings.h:95
openshot::VideoCacheThread::userSeeked
std::atomic< bool > userSeeked
True if Seek(..., true) was called (forces a cache reset).
Definition: VideoCacheThread.h:183
openshot::Settings::VIDEO_CACHE_MIN_PREROLL_FRAMES
int VIDEO_CACHE_MIN_PREROLL_FRAMES
Minimum number of frames to cache before playback begins.
Definition: Settings.h:92
openshot::Timeline
This class represents a timeline.
Definition: Timeline.h:154
openshot::VideoCacheThread::setSpeed
void setSpeed(int new_speed)
Set playback speed/direction. Positive = forward, negative = rewind, zero = pause.
Definition: VideoCacheThread.cpp:89
openshot::VideoCacheThread::speed
std::atomic< int > speed
Current playback speed (0=paused, >0 forward, <0 backward).
Definition: VideoCacheThread.h:180
openshot::Settings::Instance
static Settings * Instance()
Create or get an instance of this logger singleton (invoke the class with this method)
Definition: Settings.cpp:23
openshot::VideoCacheThread::requested_display_frame
std::atomic< int64_t > requested_display_frame
Frame index the user requested.
Definition: VideoCacheThread.h:186
openshot::CacheBase::Touch
virtual void Touch(int64_t frame_number)=0
Move frame to front of queue (so it lasts longer)
Frame.h
Header file for Frame class.
openshot::VideoCacheThread::run
void run() override
Thread entry point: loops until threadShouldExit() is true.
Definition: VideoCacheThread.cpp:300
openshot::CacheBase::Count
virtual int64_t Count()=0
Count the frames in the queue.
VideoCacheThread.h
Header file for VideoCacheThread class.
openshot::VideoCacheThread::getBytes
int64_t getBytes(int width, int height, int sample_rate, int channels, float fps)
Estimate memory usage for a single frame (video + audio).
Definition: VideoCacheThread.cpp:100
openshot::VideoCacheThread::clearCacheIfPaused
bool clearCacheIfPaused(int64_t playhead, bool paused, CacheBase *cache)
When paused and playhead is outside current cache, clear all frames.
Definition: VideoCacheThread.cpp:219
openshot::VideoCacheThread::last_dir
std::atomic< int > last_dir
Last direction sign (+1 forward, –1 backward).
Definition: VideoCacheThread.h:182
openshot::VideoCacheThread::StopThread
bool StopThread(int timeoutMs=0)
Stop the cache thread (wait up to timeoutMs ms). Returns true if it stopped.
Definition: VideoCacheThread.cpp:123
openshot::CacheBase::GetMaxBytes
int64_t GetMaxBytes()
Gets the maximum bytes value.
Definition: CacheBase.h:101
openshot::ReaderInfo::fps
openshot::Fraction fps
Frames per second, as a fraction (i.e. 24/1 = 24 fps)
Definition: ReaderBase.h:48
openshot::CacheBase::Contains
virtual bool Contains(int64_t frame_number)=0
Check if frame is already contained in cache.
openshot::ReaderBase
This abstract class is the base class, used by all readers in libopenshot.
Definition: ReaderBase.h:75
openshot::Timeline::GetMaxFrame
int64_t GetMaxFrame()
Look up the end frame number of the latest element on the timeline.
Definition: Timeline.cpp:477
openshot::VideoCacheThread::computeWindowBounds
void computeWindowBounds(int64_t playhead, int dir, int64_t ahead_count, int64_t timeline_end, int64_t &window_begin, int64_t &window_end) const
Compute the “window” of frames to cache around playhead.
Definition: VideoCacheThread.cpp:234
openshot::VideoCacheThread::min_frames_ahead
std::atomic< int64_t > min_frames_ahead
Minimum number of frames considered “ready” (pre-roll).
Definition: VideoCacheThread.h:190
openshot::VideoCacheThread::Seek
void Seek(int64_t new_position)
Seek to a specific frame (no preroll).
Definition: VideoCacheThread.cpp:165
openshot::VideoCacheThread::handleUserSeekWithPreroll
void handleUserSeekWithPreroll(int64_t playhead, int dir, int64_t timeline_end, int64_t preroll_frames)
Reset last_cached_index to start caching with a directional preroll offset.
Definition: VideoCacheThread.cpp:186
openshot::ReaderInfo::channels
int channels
The number of audio channels used in the audio stream.
Definition: ReaderBase.h:61
openshot::VideoCacheThread::isReady
bool isReady()
Definition: VideoCacheThread.cpp:50
openshot::ReaderBase::GetCache
virtual openshot::CacheBase * GetCache()=0
Get the cache object used by this reader (note: not all readers use cache)
Exceptions.h
Header file for all Exception classes.