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  , requested_display_frame(1)
33  , current_display_frame(1)
34  , cached_frame_count(0)
35  , min_frames_ahead(4)
36  , timeline_max_frame(0)
37  , reader(nullptr)
38  , force_directional_cache(false)
39  , last_cached_index(0)
40  {
41  }
42 
43  // Destructor
45  {
46  }
47 
48  // Is cache ready for playback (pre-roll)
50  {
51  if (!reader) {
52  return false;
53  }
54 
55  if (min_frames_ahead < 0) {
56  return true;
57  }
58 
60  }
61 
62  void VideoCacheThread::setSpeed(int new_speed)
63  {
64  // Only update last_speed and last_dir when new_speed != 0
65  if (new_speed != 0) {
66  last_speed = new_speed;
67  last_dir = (new_speed > 0 ? 1 : -1);
68  }
69  speed = new_speed;
70  }
71 
72  // Get the size in bytes of a frame (rough estimate)
73  int64_t VideoCacheThread::getBytes(int width,
74  int height,
75  int sample_rate,
76  int channels,
77  float fps)
78  {
79  // RGBA video frame
80  int64_t bytes = static_cast<int64_t>(width) * height * sizeof(char) * 4;
81  // Approximate audio: (sample_rate * channels)/fps samples per frame
82  bytes += ((sample_rate * channels) / fps) * sizeof(float);
83  return bytes;
84  }
85 
88  {
89  // JUCE’s startThread() returns void, so we launch it and then check if
90  // the thread actually started:
91  startThread(Priority::high);
92  return isThreadRunning();
93  }
94 
96  bool VideoCacheThread::StopThread(int timeoutMs)
97  {
98  stopThread(timeoutMs);
99  return !isThreadRunning();
100  }
101 
102  void VideoCacheThread::Seek(int64_t new_position, bool start_preroll)
103  {
104  if (start_preroll) {
105  userSeeked = true;
106 
107  CacheBase* cache = reader ? reader->GetCache() : nullptr;
108 
109  if (cache && !cache->Contains(new_position))
110  {
111  // If user initiated seek, and current frame not found (
112  Timeline* timeline = static_cast<Timeline*>(reader);
113  timeline->ClearAllCache();
114  cached_frame_count = 0;
115  }
116  else if (cache)
117  {
118  cached_frame_count = cache->Count();
119  }
120  }
121  requested_display_frame = new_position;
122  }
123 
124  void VideoCacheThread::Seek(int64_t new_position)
125  {
126  Seek(new_position, false);
127  }
128 
130  {
131  // If speed ≠ 0, use its sign; if speed==0, keep last_dir
132  return (speed != 0 ? (speed > 0 ? 1 : -1) : last_dir);
133  }
134 
135  void VideoCacheThread::handleUserSeek(int64_t playhead, int dir)
136  {
137  // Place last_cached_index just “behind” playhead in the given dir
138  last_cached_index = playhead - dir;
139  }
140 
142  bool paused,
143  CacheBase* cache)
144  {
145  if (paused && !cache->Contains(playhead)) {
146  // If paused and playhead not in cache, clear everything
147  Timeline* timeline = static_cast<Timeline*>(reader);
148  timeline->ClearAllCache();
149  cached_frame_count = 0;
150  return true;
151  }
152  return false;
153  }
154 
156  int dir,
157  int64_t ahead_count,
158  int64_t timeline_end,
159  int64_t& window_begin,
160  int64_t& window_end) const
161  {
162  if (dir > 0) {
163  // Forward window: [playhead ... playhead + ahead_count]
164  window_begin = playhead;
165  window_end = playhead + ahead_count;
166  }
167  else {
168  // Backward window: [playhead - ahead_count ... playhead]
169  window_begin = playhead - ahead_count;
170  window_end = playhead;
171  }
172  // Clamp to [1 ... timeline_end]
173  window_begin = std::max<int64_t>(window_begin, 1);
174  window_end = std::min<int64_t>(window_end, timeline_end);
175  }
176 
178  int64_t window_begin,
179  int64_t window_end,
180  int dir,
181  ReaderBase* reader)
182  {
183  bool window_full = true;
184  int64_t next_frame = last_cached_index + dir;
185 
186  // Advance from last_cached_index toward window boundary
187  while ((dir > 0 && next_frame <= window_end) ||
188  (dir < 0 && next_frame >= window_begin))
189  {
190  if (threadShouldExit()) {
191  break;
192  }
193  // If a Seek was requested mid-caching, bail out immediately
194  if (userSeeked) {
195  break;
196  }
197 
198  if (!cache->Contains(next_frame)) {
199  // Frame missing, fetch and add
200  try {
201  auto framePtr = reader->GetFrame(next_frame);
202  cache->Add(framePtr);
203  cached_frame_count = cache->Count();
204  }
205  catch (const OutOfBoundsFrame&) {
206  break;
207  }
208  window_full = false;
209  }
210  else {
211  cache->Touch(next_frame);
212  }
213 
214  last_cached_index = next_frame;
215  next_frame += dir;
216  }
217 
218  return window_full;
219  }
220 
222  {
223  using micro_sec = std::chrono::microseconds;
224  using double_micro_sec = std::chrono::duration<double, micro_sec::period>;
225 
226  while (!threadShouldExit()) {
227  Settings* settings = Settings::Instance();
228  CacheBase* cache = reader ? reader->GetCache() : nullptr;
229 
230  // If caching disabled or no reader, mark cache as ready and sleep briefly
231  if (!settings->ENABLE_PLAYBACK_CACHING || !cache) {
232  cached_frame_count = (cache ? cache->Count() : 0);
233  min_frames_ahead = -1;
234  std::this_thread::sleep_for(double_micro_sec(50000));
235  continue;
236  }
237 
238  // init local vars
240 
241  Timeline* timeline = static_cast<Timeline*>(reader);
242  int64_t timeline_end = timeline->GetMaxFrame();
243  int64_t playhead = requested_display_frame;
244  bool paused = (speed == 0);
245 
246  cached_frame_count = cache->Count();
247 
248  // Compute effective direction (±1)
249  int dir = computeDirection();
250  if (speed != 0) {
251  last_dir = dir;
252  }
253 
254  // Compute bytes_per_frame, max_bytes, and capacity once
255  int64_t bytes_per_frame = getBytes(
256  (timeline->preview_width ? timeline->preview_width : reader->info.width),
257  (timeline->preview_height ? timeline->preview_height : reader->info.height),
261  );
262  int64_t max_bytes = cache->GetMaxBytes();
263  int64_t capacity = 0;
264  if (max_bytes > 0 && bytes_per_frame > 0) {
265  capacity = max_bytes / bytes_per_frame;
266  if (capacity > settings->VIDEO_CACHE_MAX_FRAMES) {
267  capacity = settings->VIDEO_CACHE_MAX_FRAMES;
268  }
269  }
270 
271  // Handle a user-initiated seek
272  if (userSeeked) {
273  handleUserSeek(playhead, dir);
274  userSeeked = false;
275  }
276  else if (!paused && capacity >= 1) {
277  // In playback mode, check if last_cached_index drifted outside the new window
278  int64_t base_ahead = static_cast<int64_t>(capacity * settings->VIDEO_CACHE_PERCENT_AHEAD);
279 
280  int64_t window_begin, window_end;
282  playhead,
283  dir,
284  base_ahead,
285  timeline_end,
286  window_begin,
287  window_end
288  );
289 
290  bool outside_window =
291  (dir > 0 && last_cached_index > window_end) ||
292  (dir < 0 && last_cached_index < window_begin);
293  if (outside_window) {
294  handleUserSeek(playhead, dir);
295  }
296  }
297 
298  // If capacity is insufficient, sleep and retry
299  if (capacity < 1) {
300  std::this_thread::sleep_for(double_micro_sec(50000));
301  continue;
302  }
303  int64_t ahead_count = static_cast<int64_t>(capacity *
304  settings->VIDEO_CACHE_PERCENT_AHEAD);
305  int64_t window_size = ahead_count + 1;
306  if (window_size < 1) {
307  window_size = 1;
308  }
309  int64_t ready_target = window_size - 1;
310  if (ready_target < 0) {
311  ready_target = 0;
312  }
313  int64_t configured_min = settings->VIDEO_CACHE_MIN_PREROLL_FRAMES;
314  min_frames_ahead = std::min<int64_t>(configured_min, ready_target);
315 
316  // If paused and playhead is no longer in cache, clear everything
317  bool did_clear = clearCacheIfPaused(playhead, paused, cache);
318  if (did_clear) {
319  handleUserSeek(playhead, dir);
320  }
321 
322  // Compute the current caching window
323  int64_t window_begin, window_end;
324  computeWindowBounds(playhead,
325  dir,
326  ahead_count,
327  timeline_end,
328  window_begin,
329  window_end);
330 
331  // Attempt to fill any missing frames in that window
332  bool window_full = prefetchWindow(cache, window_begin, window_end, dir, reader);
333 
334  // If paused and window was already full, keep playhead fresh
335  if (paused && window_full) {
336  cache->Touch(playhead);
337  }
338 
339  // Sleep a short fraction of a frame interval
340  int64_t sleep_us = static_cast<int64_t>(
341  1000000.0 / reader->info.fps.ToFloat() / 4.0
342  );
343  std::this_thread::sleep_for(double_micro_sec(sleep_us));
344  }
345  }
346 
347 } // namespace openshot
Header file for CacheBase class.
Header file for all Exception classes.
Header file for Frame class.
Header file for global Settings class.
Header file for Timeline class.
Header file for VideoCacheThread class.
All cache managers in libopenshot are based on this CacheBase class.
Definition: CacheBase.h:35
virtual bool Contains(int64_t frame_number)=0
Check if frame is already contained in cache.
virtual int64_t Count()=0
Count the frames in the queue.
virtual void Add(std::shared_ptr< openshot::Frame > frame)=0
Add a Frame to the cache.
virtual void Touch(int64_t frame_number)=0
Move frame to front of queue (so it lasts longer)
int64_t GetMaxBytes()
Gets the maximum bytes value.
Definition: CacheBase.h:101
float ToFloat()
Return this fraction as a float (i.e. 1/2 = 0.5)
Definition: Fraction.cpp:35
Exception for frames that are out of bounds.
Definition: Exceptions.h:301
This abstract class is the base class, used by all readers in libopenshot.
Definition: ReaderBase.h:76
openshot::ReaderInfo info
Information about the current media file.
Definition: ReaderBase.h:88
virtual openshot::CacheBase * GetCache()=0
Get the cache object used by this reader (note: not all readers use cache)
virtual std::shared_ptr< openshot::Frame > GetFrame(int64_t number)=0
This class is contains settings used by libopenshot (and can be safely toggled at any point)
Definition: Settings.h:26
static Settings * Instance()
Create or get an instance of this logger singleton (invoke the class with this method)
Definition: Settings.cpp:23
int VIDEO_CACHE_MIN_PREROLL_FRAMES
Minimum number of frames to cache before playback begins.
Definition: Settings.h:89
int VIDEO_CACHE_MAX_FRAMES
Max number of frames (when paused) to cache for playback.
Definition: Settings.h:95
float VIDEO_CACHE_PERCENT_AHEAD
Percentage of cache in front of the playhead (0.0 to 1.0)
Definition: Settings.h:86
bool ENABLE_PLAYBACK_CACHING
Enable/Disable the cache thread to pre-fetch and cache video frames before we need them.
Definition: Settings.h:98
int preview_height
Optional preview width of timeline image. If your preview window is smaller than the timeline,...
Definition: TimelineBase.h:45
int preview_width
Optional preview width of timeline image. If your preview window is smaller than the timeline,...
Definition: TimelineBase.h:44
This class represents a timeline.
Definition: Timeline.h:154
int64_t GetMaxFrame()
Look up the end frame number of the latest element on the timeline.
Definition: Timeline.cpp:474
void ClearAllCache(bool deep=false)
Definition: Timeline.cpp:1746
bool StopThread(int timeoutMs=0)
Stop the cache thread (wait up to timeoutMs ms). Returns true if it stopped.
void setSpeed(int new_speed)
Set playback speed/direction. Positive = forward, negative = rewind, zero = pause.
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].
int64_t requested_display_frame
Frame index the user requested.
bool clearCacheIfPaused(int64_t playhead, bool paused, CacheBase *cache)
When paused and playhead is outside current cache, clear all frames.
int64_t last_cached_index
Index of the most recently cached frame.
int64_t cached_frame_count
Estimated count of frames currently stored in cache.
void run() override
Thread entry point: loops until threadShouldExit() is true.
int last_speed
Last non-zero speed (for tracking).
VideoCacheThread()
Constructor: initializes member variables and assumes forward direction on first launch.
void handleUserSeek(int64_t playhead, int dir)
If userSeeked is true, reset last_cached_index just behind the playhead.
int64_t getBytes(int width, int height, int sample_rate, int channels, float fps)
Estimate memory usage for a single frame (video + audio).
ReaderBase * reader
The source reader (e.g., Timeline, FFmpegReader).
void Seek(int64_t new_position)
Seek to a specific frame (no preroll).
bool userSeeked
True if Seek(..., true) was called (forces a cache reset).
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.
int64_t min_frames_ahead
Minimum number of frames considered “ready” (pre-roll).
int last_dir
Last direction sign (+1 forward, –1 backward).
bool StartThread()
Start the cache thread at high priority. Returns true if it’s actually running.
int speed
Current playback speed (0=paused, >0 forward, <0 backward).
This namespace is the default namespace for all code in the openshot library.
Definition: Compressor.h:29
int width
The width of the video (in pixesl)
Definition: ReaderBase.h:46
int channels
The number of audio channels used in the audio stream.
Definition: ReaderBase.h:61
openshot::Fraction fps
Frames per second, as a fraction (i.e. 24/1 = 24 fps)
Definition: ReaderBase.h:48
int height
The height of the video (in pixels)
Definition: ReaderBase.h:45
int sample_rate
The number of audio samples per second (44100 is a common sample rate)
Definition: ReaderBase.h:60