OpenShot Library | libopenshot 0.6.0
Loading...
Searching...
No Matches
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
23namespace 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
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
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
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
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:88
virtual std::shared_ptr< openshot::Frame > GetFrame(int64_t number)=0
virtual openshot::CacheBase * GetCache()=0
Get the cache object used by this reader (note: not all readers use cache)
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:92
int VIDEO_CACHE_MAX_FRAMES
Max number of frames (when paused) to cache for playback.
Definition Settings.h:98
float VIDEO_CACHE_PERCENT_AHEAD
Percentage of cache in front of the playhead (0.0 to 1.0)
Definition Settings.h:89
bool ENABLE_PLAYBACK_CACHING
Enable/Disable the cache thread to pre-fetch and cache video frames before we need them.
Definition Settings.h:101
int VIDEO_CACHE_MAX_PREROLL_FRAMES
Max number of frames (ahead of playhead) to cache during playback.
Definition Settings.h:95
int preview_height
Optional preview width of timeline image. If your preview window is smaller than the timeline,...
int preview_width
Optional preview width of timeline image. If your preview window is smaller than the timeline,...
This class represents a timeline.
Definition Timeline.h:153
void ClearAllCache(bool deep=false)
uint64_t CacheEpoch() const
Return the current cache invalidation epoch.
Definition Timeline.h:324
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