OpenShot Library | libopenshot  0.7.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  , clear_cache_on_next_fill(false)
34  , scrub_active(false)
35  , requested_display_frame(1)
36  , current_display_frame(1)
37  , cached_frame_count(0)
38  , min_frames_ahead(4)
39  , timeline_max_frame(0)
40  , reader(nullptr)
41  , force_directional_cache(false)
42  , last_cached_index(0)
43  , seen_timeline_cache_epoch(0)
44  , timeline_cache_epoch_initialized(false)
45  {
46  }
47 
48  // Destructor
50  {
51  }
52 
53  // Is cache ready for playback (pre-roll)
55  {
56  if (!reader) {
57  return false;
58  }
59 
60  const int64_t ready_min = min_frames_ahead.load();
61  if (ready_min < 0) {
62  return true;
63  }
64 
65  const int64_t cached_index = last_cached_index.load();
66  int64_t playhead = requested_display_frame.load();
67  int dir = computeDirection();
68 
69  // Near timeline boundaries, don't require more pre-roll than can exist.
70  int64_t max_frame = reader->info.video_length;
71  if (auto* timeline = dynamic_cast<Timeline*>(reader)) {
72  const int64_t timeline_max = timeline->GetMaxFrame();
73  if (timeline_max > 0) {
74  max_frame = timeline_max;
75  }
76  }
77  if (max_frame < 1) {
78  return false;
79  }
80  playhead = clampToTimelineRange(playhead, max_frame);
81 
82  int64_t required_ahead = ready_min;
83  int64_t available_ahead = (dir > 0)
84  ? std::max<int64_t>(0, max_frame - playhead)
85  : std::max<int64_t>(0, playhead - 1);
86  required_ahead = std::min(required_ahead, available_ahead);
87 
88  if (dir > 0) {
89  return (cached_index >= playhead + required_ahead);
90  }
91  return (cached_index <= playhead - required_ahead);
92  }
93 
94  void VideoCacheThread::setSpeed(int new_speed)
95  {
96  // Only update last_speed and last_dir when new_speed != 0
97  if (new_speed != 0) {
98  last_speed.store(new_speed);
99  last_dir.store(new_speed > 0 ? 1 : -1);
100  // Leaving paused/scrub context: resume normal cache behavior.
101  scrub_active.store(false);
102  }
103  speed.store(new_speed);
104  }
105 
106  // Get the size in bytes of a frame (rough estimate)
107  int64_t VideoCacheThread::getBytes(int width,
108  int height,
109  int sample_rate,
110  int channels,
111  float fps)
112  {
113  // RGBA video frame
114  int64_t bytes = static_cast<int64_t>(width) * height * sizeof(char) * 4;
115  // Approximate audio: (sample_rate * channels)/fps samples per frame
116  bytes += ((sample_rate * channels) / fps) * sizeof(float);
117  return bytes;
118  }
119 
122  {
123  // JUCE’s startThread() returns void, so we launch it and then check if
124  // the thread actually started:
125  startThread(Priority::high);
126  return isThreadRunning();
127  }
128 
130  bool VideoCacheThread::StopThread(int timeoutMs)
131  {
132  stopThread(timeoutMs);
133  return !isThreadRunning();
134  }
135 
137  {
138  std::lock_guard<std::mutex> guard(seek_state_mutex);
139  reader = new_reader;
142  Play();
143  }
144 
145  void VideoCacheThread::Seek(int64_t new_position, bool start_preroll)
146  {
147  const int64_t timeline_end = resolveTimelineEnd();
148  const int64_t clamped_new_position = clampToTimelineRange(new_position, timeline_end);
149  const int64_t current_requested = requested_display_frame.load();
150 
151  bool should_mark_seek = false;
152  bool should_preroll = false;
153  int64_t new_cached_count = cached_frame_count.load();
154  bool entering_scrub = false;
155  bool leaving_scrub = false;
156  bool cache_contains = false;
157  bool should_clear_cache = false;
158  CacheBase* cache = reader ? reader->GetCache() : nullptr;
159  const bool same_frame_refresh = (new_position == current_requested);
160  if (cache) {
161  cache_contains = cache->Contains(clamped_new_position);
162  }
163 
164  if (start_preroll) {
165  if (same_frame_refresh) {
166  const bool is_paused = (speed.load() == 0);
167  if (is_paused) {
168  const bool was_scrubbing = scrub_active.load();
169  if (was_scrubbing && cache && cache_contains) {
170  // Preserve in-range cache for paused scrub preview -> same-frame commit.
171  should_mark_seek = false;
172  should_preroll = false;
173  should_clear_cache = false;
174  new_cached_count = cache->Count();
175  } else {
176  // Paused same-frame edit refresh: force full cache refresh.
177  if (Timeline* timeline = dynamic_cast<Timeline*>(reader)) {
178  timeline->ClearAllCache();
179  }
180  new_cached_count = 0;
181  should_mark_seek = true;
182  should_preroll = true;
183  should_clear_cache = false;
184  }
185  } else {
186  // Same-frame refresh during playback should stay lightweight.
187  should_mark_seek = false;
188  should_preroll = false;
189  should_clear_cache = false;
190  if (cache && cache_contains) {
191  cache->Remove(clamped_new_position);
192  }
193  if (cache) {
194  new_cached_count = cache->Count();
195  }
196  }
197  } else {
198  if (cache && !cache_contains) {
199  should_mark_seek = true;
200  // Uncached commit seek: defer cache clear to cache thread loop.
201  new_cached_count = 0;
202  should_preroll = true;
203  should_clear_cache = true;
204  }
205  else if (cache)
206  {
207  // In-range commit seek preserves cache window/baseline.
208  should_mark_seek = false;
209  should_preroll = false;
210  should_clear_cache = false;
211  new_cached_count = cache->Count();
212  } else {
213  // No cache object to query: use normal seek behavior.
214  should_mark_seek = true;
215  }
216  }
217  leaving_scrub = true;
218  }
219  else {
220  // Non-preroll seeks cover paused scrubbing and live playback refresh.
221  const bool is_paused = (speed.load() == 0);
222  if (is_paused && same_frame_refresh) {
223  // Same-frame paused refresh updates only that frame.
224  should_mark_seek = false;
225  should_preroll = false;
226  should_clear_cache = false;
227  if (cache && cache_contains) {
228  cache->Remove(clamped_new_position);
229  }
230  if (cache) {
231  new_cached_count = cache->Count();
232  }
233  leaving_scrub = true;
234  }
235  else if (is_paused) {
236  if (cache && !cache_contains) {
237  should_mark_seek = true;
238  new_cached_count = 0;
239  should_clear_cache = true;
240  }
241  else if (cache) {
242  // In-range paused seek preserves cache continuity.
243  should_mark_seek = false;
244  new_cached_count = cache->Count();
245  } else {
246  should_mark_seek = true;
247  }
248  entering_scrub = true;
249  } else {
250  // During playback, keep seek/scrub side effects minimal.
251  should_mark_seek = false;
252  should_preroll = false;
253  should_clear_cache = false;
254  if (cache) {
255  new_cached_count = cache->Count();
256  }
257  leaving_scrub = true;
258  }
259  }
260 
261  {
262  std::lock_guard<std::mutex> guard(seek_state_mutex);
263  // Reset readiness baseline only when rebuilding cache.
264  const int dir = computeDirection();
265  if (should_mark_seek || should_preroll || should_clear_cache) {
266  last_cached_index.store(clamped_new_position - dir);
267  }
268  requested_display_frame.store(new_position);
269  cached_frame_count.store(new_cached_count);
270  preroll_on_next_fill.store(should_preroll);
271  // Clear behavior follows the latest seek intent.
272  clear_cache_on_next_fill.store(should_clear_cache);
273  userSeeked.store(should_mark_seek);
274  if (entering_scrub) {
275  scrub_active.store(true);
276  }
277  if (leaving_scrub) {
278  scrub_active.store(false);
279  }
280  }
281  }
282 
283  void VideoCacheThread::Seek(int64_t new_position)
284  {
285  NotifyPlaybackPosition(new_position);
286  }
287 
288  void VideoCacheThread::NotifyPlaybackPosition(int64_t new_position)
289  {
290  if (new_position <= 0) {
291  return;
292  }
293  if (scrub_active.load()) {
294  return;
295  }
296 
297  int64_t new_cached_count = cached_frame_count.load();
298  if (CacheBase* cache = reader ? reader->GetCache() : nullptr) {
299  new_cached_count = cache->Count();
300  }
301  {
302  std::lock_guard<std::mutex> guard(seek_state_mutex);
303  requested_display_frame.store(new_position);
304  cached_frame_count.store(new_cached_count);
305  }
306  }
307 
309  {
310  // If speed ≠ 0, use its sign; if speed==0, keep last_dir
311  const int current_speed = speed.load();
312  if (current_speed != 0) {
313  return (current_speed > 0 ? 1 : -1);
314  }
315  return last_dir.load();
316  }
317 
318  void VideoCacheThread::handleUserSeek(int64_t playhead, int dir)
319  {
320  // Place last_cached_index just “behind” playhead in the given dir
321  last_cached_index.store(playhead - dir);
322  }
323 
325  int dir,
326  int64_t timeline_end,
327  int64_t preroll_frames)
328  {
329  int64_t preroll_start = playhead;
330  if (preroll_frames > 0) {
331  if (dir > 0) {
332  preroll_start = std::max<int64_t>(1, playhead - preroll_frames);
333  }
334  else {
335  preroll_start = std::min<int64_t>(timeline_end, playhead + preroll_frames);
336  }
337  }
338  last_cached_index.store(preroll_start - dir);
339  }
340 
341  int64_t VideoCacheThread::computePrerollFrames(const Settings* settings) const
342  {
343  if (!settings) {
344  return 0;
345  }
346  int64_t min_frames = settings->VIDEO_CACHE_MIN_PREROLL_FRAMES;
347  int64_t max_frames = settings->VIDEO_CACHE_MAX_PREROLL_FRAMES;
348  if (min_frames < 0) {
349  return 0;
350  }
351  if (max_frames > 0 && min_frames > max_frames) {
352  min_frames = max_frames;
353  }
354  return min_frames;
355  }
356 
358  {
359  if (!reader) {
360  return 0;
361  }
362  int64_t timeline_end = reader->info.video_length;
363  if (auto* timeline = dynamic_cast<Timeline*>(reader)) {
364  const int64_t timeline_max = timeline->GetMaxFrame();
365  if (timeline_max > 0) {
366  timeline_end = timeline_max;
367  }
368  }
369  return timeline_end;
370  }
371 
372  int64_t VideoCacheThread::clampToTimelineRange(int64_t frame, int64_t timeline_end) const
373  {
374  if (timeline_end < 1) {
375  return frame;
376  }
377  return std::clamp<int64_t>(frame, 1, timeline_end);
378  }
379 
381  bool paused,
382  CacheBase* cache)
383  {
384  const int64_t timeline_end = resolveTimelineEnd();
385  int64_t cache_playhead = playhead;
386  if (reader) {
387  cache_playhead = clampToTimelineRange(playhead, timeline_end);
388  }
389  if (paused && !cache->Contains(cache_playhead)) {
390  // If paused and playhead not in cache, clear everything
391  if (Timeline* timeline = dynamic_cast<Timeline*>(reader)) {
392  timeline->ClearAllCache();
393  }
394  cached_frame_count.store(0);
395  return true;
396  }
397  return false;
398  }
399 
401  int dir,
402  int64_t ahead_count,
403  int64_t timeline_end,
404  int64_t& window_begin,
405  int64_t& window_end) const
406  {
407  if (dir > 0) {
408  // Forward window: [playhead ... playhead + ahead_count]
409  window_begin = playhead;
410  window_end = playhead + ahead_count;
411  }
412  else {
413  // Backward window: [playhead - ahead_count ... playhead]
414  window_begin = playhead - ahead_count;
415  window_end = playhead;
416  }
417  // Clamp to [1 ... timeline_end]
418  window_begin = std::max<int64_t>(window_begin, 1);
419  window_end = std::min<int64_t>(window_end, timeline_end);
420  }
421 
423  int64_t window_begin,
424  int64_t window_end,
425  int dir,
426  ReaderBase* reader,
427  int64_t max_frames_to_fetch)
428  {
429  bool window_full = true;
430  int64_t next_frame = last_cached_index.load() + dir;
431  int64_t fetched_this_pass = 0;
432 
433  // Advance from last_cached_index toward window boundary
434  while ((dir > 0 && next_frame <= window_end) ||
435  (dir < 0 && next_frame >= window_begin))
436  {
437  if (threadShouldExit()) {
438  break;
439  }
440  // If a Seek was requested mid-caching, bail out immediately
441  if (userSeeked.load()) {
442  break;
443  }
444 
445  if (!cache->Contains(next_frame)) {
446  // Frame missing, fetch and add
447  try {
448  auto framePtr = reader->GetFrame(next_frame);
449  cache->Add(framePtr);
450  cached_frame_count.store(cache->Count());
451  ++fetched_this_pass;
452  }
453  catch (const OutOfBoundsFrame&) {
454  break;
455  }
456  window_full = false;
457  }
458  else {
459  cache->Touch(next_frame);
460  }
461 
462  last_cached_index.store(next_frame);
463  next_frame += dir;
464 
465  // In active playback, avoid long uninterrupted prefetch bursts
466  // that can delay player thread frame retrieval.
467  if (max_frames_to_fetch > 0 && fetched_this_pass >= max_frames_to_fetch) {
468  break;
469  }
470  }
471 
472  return window_full;
473  }
474 
476  {
477  using micro_sec = std::chrono::microseconds;
478  using double_micro_sec = std::chrono::duration<double, micro_sec::period>;
479 
480  while (!threadShouldExit()) {
481  Settings* settings = Settings::Instance();
482  CacheBase* cache = reader ? reader->GetCache() : nullptr;
483  Timeline* timeline = dynamic_cast<Timeline*>(reader);
484 
485  // Process deferred clears even when caching is currently disabled
486  // (e.g. active scrub mode), so stale ranges are removed promptly.
487  bool should_clear_cache = clear_cache_on_next_fill.exchange(false);
488  if (should_clear_cache && timeline) {
489  const int dir_on_clear = computeDirection();
490  const int64_t clear_playhead = clampToTimelineRange(
492  timeline->ClearAllCache();
493  cached_frame_count.store(0);
494  // Reset ready baseline immediately after clear. Otherwise a
495  // stale last_cached_index from the old cache window can make
496  // isReady() report true before new preroll is actually filled.
497  last_cached_index.store(clear_playhead - dir_on_clear);
498  }
499 
500  // If caching disabled or no reader, mark cache as ready and sleep briefly
501  if (!settings->ENABLE_PLAYBACK_CACHING || !cache) {
502  cached_frame_count.store(cache ? cache->Count() : 0);
503  min_frames_ahead.store(-1);
504  std::this_thread::sleep_for(double_micro_sec(50000));
505  continue;
506  }
507 
508  // init local vars
510 
511  if (!timeline) {
512  std::this_thread::sleep_for(double_micro_sec(50000));
513  continue;
514  }
515  int64_t timeline_end = resolveTimelineEnd();
516  int64_t raw_playhead = requested_display_frame.load();
517  int64_t playhead = clampToTimelineRange(raw_playhead, timeline_end);
518  bool paused = (speed.load() == 0);
519  int64_t preroll_frames = computePrerollFrames(settings);
520 
521  cached_frame_count.store(cache->Count());
522 
523  // Compute effective direction (±1)
524  int dir = computeDirection();
525  if (speed.load() != 0) {
526  last_dir.store(dir);
527  }
528 
529  // If timeline-side cache invalidation occurred (e.g. ApplyJsonDiff / SetJson),
530  // restart fill from the active playhead window so invalidated gaps self-heal.
531  if (timeline) {
532  bool epoch_changed = false;
533  {
534  std::lock_guard<std::mutex> guard(seek_state_mutex);
535  const uint64_t timeline_epoch = timeline->CacheEpoch();
537  seen_timeline_cache_epoch = timeline_epoch;
539  }
540  else if (timeline_epoch != seen_timeline_cache_epoch) {
541  seen_timeline_cache_epoch = timeline_epoch;
542  epoch_changed = true;
543  }
544  }
545  if (epoch_changed) {
546  handleUserSeek(playhead, dir);
547  }
548  }
549 
550  // Compute bytes_per_frame, max_bytes, and capacity once
551  int64_t bytes_per_frame = getBytes(
552  (timeline->preview_width ? timeline->preview_width : reader->info.width),
553  (timeline->preview_height ? timeline->preview_height : reader->info.height),
557  );
558  int64_t max_bytes = cache->GetMaxBytes();
559  int64_t capacity = 0;
560  if (max_bytes > 0 && bytes_per_frame > 0) {
561  capacity = max_bytes / bytes_per_frame;
562  if (capacity > settings->VIDEO_CACHE_MAX_FRAMES) {
563  capacity = settings->VIDEO_CACHE_MAX_FRAMES;
564  }
565  }
566 
567  // Handle a user-initiated seek
568  bool did_user_seek = false;
569  bool use_preroll = false;
570  {
571  std::lock_guard<std::mutex> guard(seek_state_mutex);
572  raw_playhead = requested_display_frame.load();
573  playhead = clampToTimelineRange(raw_playhead, timeline_end);
574  did_user_seek = userSeeked.load();
575  use_preroll = preroll_on_next_fill.load();
576  if (did_user_seek) {
577  userSeeked.store(false);
578  preroll_on_next_fill.store(false);
579  }
580  }
581  if (did_user_seek) {
582  // During active playback, prioritize immediate forward readiness
583  // from the playhead. Use directional preroll offset only while
584  // paused/scrubbing contexts.
585  if (use_preroll && paused) {
586  handleUserSeekWithPreroll(playhead, dir, timeline_end, preroll_frames);
587  }
588  else {
589  handleUserSeek(playhead, dir);
590  }
591  }
592  else if (!paused && capacity >= 1) {
593  // In playback mode, check if last_cached_index drifted outside the new window
594  int64_t base_ahead = static_cast<int64_t>(capacity * settings->VIDEO_CACHE_PERCENT_AHEAD);
595 
596  int64_t window_begin, window_end;
598  playhead,
599  dir,
600  base_ahead,
601  timeline_end,
602  window_begin,
603  window_end
604  );
605 
606  bool outside_window =
607  (dir > 0 && last_cached_index.load() > window_end) ||
608  (dir < 0 && last_cached_index.load() < window_begin);
609  if (outside_window) {
610  handleUserSeek(playhead, dir);
611  }
612  }
613 
614  // If a clear was requested by a seek that arrived after the loop
615  // began, apply it now before any additional prefetch work. This
616  // avoids "build then suddenly clear" behavior during playback.
617  bool should_clear_mid_loop = clear_cache_on_next_fill.exchange(false);
618  if (should_clear_mid_loop && timeline) {
619  timeline->ClearAllCache();
620  cached_frame_count.store(0);
621  last_cached_index.store(playhead - dir);
622  }
623 
624  // While user is dragging/scrubbing, skip cache prefetch work.
625  if (scrub_active.load()) {
626  std::this_thread::sleep_for(double_micro_sec(10000));
627  continue;
628  }
629 
630  // If capacity is insufficient, sleep and retry
631  if (capacity < 1) {
632  std::this_thread::sleep_for(double_micro_sec(50000));
633  continue;
634  }
635  int64_t ahead_count = static_cast<int64_t>(capacity *
636  settings->VIDEO_CACHE_PERCENT_AHEAD);
637  int64_t window_size = ahead_count + 1;
638  if (window_size < 1) {
639  window_size = 1;
640  }
641  int64_t ready_target = window_size - 1;
642  if (ready_target < 0) {
643  ready_target = 0;
644  }
645  int64_t configured_min = settings->VIDEO_CACHE_MIN_PREROLL_FRAMES;
646  const int64_t required_ahead = std::min<int64_t>(configured_min, ready_target);
647  min_frames_ahead.store(required_ahead);
648 
649  // If paused and playhead is no longer in cache, clear everything
650  bool did_clear = clearCacheIfPaused(playhead, paused, cache);
651  if (did_clear) {
652  handleUserSeekWithPreroll(playhead, dir, timeline_end, preroll_frames);
653  }
654 
655  // Compute the current caching window
656  int64_t window_begin, window_end;
657  computeWindowBounds(playhead,
658  dir,
659  ahead_count,
660  timeline_end,
661  window_begin,
662  window_end);
663 
664  // Attempt to fill any missing frames in that window
665  int64_t max_frames_to_fetch = -1;
666  if (!paused) {
667  // Keep cache thread responsive during playback seeks so player
668  // can start as soon as pre-roll is met instead of waiting for a
669  // full cache window pass.
670  max_frames_to_fetch = 8;
671  }
672  bool window_full = prefetchWindow(
673  cache,
674  window_begin,
675  window_end,
676  dir,
677  reader,
678  max_frames_to_fetch
679  );
680 
681  // If paused and window was already full, keep playhead fresh
682  if (paused && window_full) {
683  cache->Touch(playhead);
684  }
685 
686  // Sleep a short fraction of a frame interval
687  int64_t sleep_us = static_cast<int64_t>(
688  1000000.0 / reader->info.fps.ToFloat() / 4.0
689  );
690  std::this_thread::sleep_for(double_micro_sec(sleep_us));
691  }
692  }
693 
694 } // 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 void Remove(int64_t frame_number)=0
Remove a specific frame.
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:307
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:90
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:43
int VIDEO_CACHE_MIN_PREROLL_FRAMES
Minimum number of frames to cache before playback begins.
Definition: Settings.h:101
int VIDEO_CACHE_MAX_FRAMES
Max number of frames (when paused) to cache for playback.
Definition: Settings.h:107
float VIDEO_CACHE_PERCENT_AHEAD
Percentage of cache in front of the playhead (0.0 to 1.0)
Definition: Settings.h:98
bool ENABLE_PLAYBACK_CACHING
Enable/Disable the cache thread to pre-fetch and cache video frames before we need them.
Definition: Settings.h:110
int VIDEO_CACHE_MAX_PREROLL_FRAMES
Max number of frames (ahead of playhead) to cache during playback.
Definition: Settings.h:104
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:153
void ClearAllCache(bool deep=false)
Definition: Timeline.cpp:1779
uint64_t CacheEpoch() const
Return the current cache invalidation epoch.
Definition: Timeline.h:323
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.
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.
int64_t resolveTimelineEnd() const
Resolve timeline end frame from reader/timeline metadata.
void Play()
Play method is unimplemented.
int64_t clampToTimelineRange(int64_t frame, int64_t timeline_end) const
Clamp frame index to [1, timeline_end] when timeline_end is valid.
std::atomic< bool > preroll_on_next_fill
True if next cache rebuild should include preroll offset.
bool clearCacheIfPaused(int64_t playhead, bool paused, CacheBase *cache)
When paused and playhead is outside current cache, clear all frames.
std::atomic< int64_t > last_cached_index
Index of the most recently cached frame.
std::atomic< int > speed
Current playback speed (0=paused, >0 forward, <0 backward).
void run() override
Thread entry point: loops until threadShouldExit() is true.
int64_t computePrerollFrames(const Settings *settings) const
Compute preroll frame count from settings.
std::mutex seek_state_mutex
Protects coherent seek state updates/consumption.
VideoCacheThread()
Constructor: initializes member variables and assumes forward direction on first launch.
uint64_t seen_timeline_cache_epoch
Last observed Timeline cache invalidation epoch.
std::atomic< int64_t > requested_display_frame
Frame index the user requested.
void Reader(ReaderBase *new_reader)
Attach a ReaderBase (e.g. Timeline, FFmpegReader) and begin caching.
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)
Backward-compatible alias for playback position updates (no seek side effects).
bool prefetchWindow(CacheBase *cache, int64_t window_begin, int64_t window_end, int dir, ReaderBase *reader, int64_t max_frames_to_fetch=-1)
Prefetch all missing frames in [window_begin ... window_end] or [window_end ... window_begin].
std::atomic< int64_t > cached_frame_count
Estimated count of frames currently stored in cache.
std::atomic< int > last_dir
Last direction sign (+1 forward, –1 backward).
std::atomic< int64_t > min_frames_ahead
Minimum number of frames considered “ready” (pre-roll).
std::atomic< 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.
bool StartThread()
Start the cache thread at high priority. Returns true if it’s actually running.
bool timeline_cache_epoch_initialized
True once an initial epoch snapshot has been taken.
std::atomic< bool > scrub_active
True while user is dragging/scrubbing the playhead.
void NotifyPlaybackPosition(int64_t new_position)
Update playback position without triggering seek behavior or cache invalidation.
std::atomic< int > last_speed
Last non-zero speed (for tracking).
std::atomic< bool > clear_cache_on_next_fill
True if next cache loop should clear existing cache ranges.
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
int64_t video_length
The number of frames in the video stream.
Definition: ReaderBase.h:53
int sample_rate
The number of audio samples per second (44100 is a common sample rate)
Definition: ReaderBase.h:60