OpenShot Library | libopenshot 1.1.1
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
50 {
51 // Raw readers remain caller-owned. Do not destroy state while decoding.
52 StopThread(-1);
53 }
54
55 // Is cache ready for playback (pre-roll)
57 {
58 std::lock_guard<std::mutex> guard(seek_state_mutex);
59 if (!reader) {
60 return false;
61 }
62
63 if (audio_cache_miss.load()) return false;
64 const auto* settings = Settings::Instance();
65 auto* timeline = dynamic_cast<Timeline*>(reader);
66 if (!settings->ENABLE_PLAYBACK_CACHING || !timeline || !reader->GetCache()
67 || settings->VIDEO_CACHE_MIN_PREROLL_FRAMES < 0)
68 return true;
69 if (timeline->CacheEpoch() != readiness_epoch) {
71 readiness_epoch = timeline->CacheEpoch();
72 }
74 return true;
75 const int64_t now = monotonicUs();
76 const bool current_request = readiness_generation == request_generation.load()
77 && !timeline->CacheRefreshPending();
78 const int dir = computeDirection();
80 const int64_t shifted = (playhead - readiness_playhead) * dir;
81 const int64_t contiguous = readiness_direction == dir && shifted >= 0
82 ? std::max<int64_t>(0, contiguous_ahead - shifted) : 0;
83 CacheBase* cache = reader->GetCache();
84 // Only two bounded cache lookups in the callback; the worker probes the
85 // larger startup range. These also reject holes introduced by eviction.
86 const bool playable = cache->Contains(playhead)
87 && (readiness_minimum_step == 0 || cache->Contains(playhead + readiness_minimum_step * dir));
88 const int64_t checked_ahead = preroll_policy.Released() && playable
89 ? readiness_minimum_step : contiguous;
90 return preroll_policy.Ready(now, current_request ? checked_ahead : 0,
91 current_request && playable, min_frames_ahead.load(), readiness_minimum_step);
92 }
93
95 {
98 }
99
100 void VideoCacheThread::updateReadiness(CacheBase* cache, int64_t playhead, int dir)
101 {
102 // Called under seek_state_mutex by the worker or a cached seek.
104 readiness_playhead = playhead;
107 const int64_t required = std::max(min_frames_ahead.load(), readiness_minimum_step);
108 while (contiguous_ahead < required && cache->Contains(playhead + (contiguous_ahead + 1) * dir))
110 }
111
112 void VideoCacheThread::setSpeed(int new_speed)
113 {
114 std::lock_guard<std::mutex> guard(seek_state_mutex);
115 const int old_direction = computeDirection();
116 if (new_speed != speed.load()) {
117 request_generation.fetch_add(1);
119 }
120 // Only update last_speed and last_dir when new_speed != 0
121 if (new_speed != 0) {
122 last_speed.store(new_speed);
123 last_dir.store(new_speed > 0 ? 1 : -1);
124 // Leaving paused/scrub context: resume normal cache behavior.
125 scrub_active.store(false);
126 }
127 speed.store(new_speed);
128 if (computeDirection() != old_direction) {
129 userSeeked.store(true);
131 }
132 notify();
133 }
134
135 // Get the size in bytes of a frame (rough estimate)
136 int64_t VideoCacheThread::getBytes(int width,
137 int height,
138 int sample_rate,
139 int channels,
140 float fps)
141 {
142 // RGBA video frame
143 int64_t bytes = static_cast<int64_t>(width) * height * sizeof(char) * 4;
144 // Approximate audio: (sample_rate * channels)/fps samples per frame
145 if (fps > 0) bytes += (static_cast<double>(sample_rate) * channels / fps) * sizeof(float);
146 return bytes;
147 }
148
150 {
151 return std::chrono::duration_cast<std::chrono::microseconds>(
152 std::chrono::steady_clock::now().time_since_epoch()).count();
153 }
154
156 {
157 request_generation.fetch_add(1);
158 if (started_us) {
159 request_time_us.store(started_us);
160 first_current_frame_us.store(0);
161 obsolete_completion_us.store(0);
162 acknowledgement_us.store(monotonicUs() - started_us);
163 }
164 notify();
165 }
166
168 {
169 std::lock_guard<std::mutex> guard(lifecycle_mutex);
170 // A timed stop may leave a decoder draining. Never start another worker.
171 if (isThreadRunning())
172 return !threadShouldExit();
173 startThread(Priority::high);
174 return isThreadRunning();
175 }
176
178 {
179 const int64_t started = diagnostics_enabled.load() ? monotonicUs() : 0;
180 std::lock_guard<std::mutex> guard(seek_state_mutex);
181 acknowledgeRequest(started);
182 signalThreadShouldExit();
183 notify();
184 }
185
187 {
188 std::lock_guard<std::mutex> guard(lifecycle_mutex);
189 Stop();
190 // JUCE stopThread(timeout) forcibly kills on timeout. A timeout here
191 // reports unfinished work; destruction/replacement explicitly drains.
192 return waitForThreadToExit(timeoutMs);
193 }
194
196 {
197 std::lock_guard<std::mutex> lifecycle_guard(lifecycle_mutex);
198 const bool restart = isThreadRunning();
199 if (restart) {
200 Stop();
201 waitForThreadToExit(-1);
202 }
203 {
204 std::lock_guard<std::mutex> guard(seek_state_mutex);
205 request_generation.fetch_add(1);
206 reader = new_reader;
207 cache_audio_only.store(false);
208 audio_cache_miss.store(false);
212 userSeeked.store(false);
214 }
215 if (restart)
216 startThread(Priority::high);
217 }
218
219 void VideoCacheThread::Seek(int64_t new_position, bool start_preroll)
220 {
221 const int64_t started = diagnostics_enabled.load() ? monotonicUs() : 0;
222 std::lock_guard<std::mutex> guard(seek_state_mutex);
223 const int64_t timeline_end = resolveTimelineEnd();
224 const int64_t clamped_new_position = clampToTimelineRange(new_position, timeline_end);
225 const int64_t current_requested = requested_display_frame.load();
226
227 bool should_mark_seek = false;
228 bool should_preroll = false;
229 int64_t new_cached_count = cached_frame_count.load();
230 bool entering_scrub = false;
231 bool leaving_scrub = false;
232 bool cache_contains = false;
233 bool should_clear_cache = false;
234 CacheBase* cache = reader ? reader->GetCache() : nullptr;
235 const bool same_frame_refresh = (new_position == current_requested);
236 if (cache) {
237 cache_contains = cache->Contains(clamped_new_position);
238 }
239
240 if (start_preroll) {
241 if (same_frame_refresh) {
242 const bool is_paused = (speed.load() == 0);
243 if (is_paused) {
244 const bool was_scrubbing = scrub_active.load();
245 if (was_scrubbing && cache && cache_contains) {
246 // Preserve in-range cache for paused scrub preview -> same-frame commit.
247 should_mark_seek = false;
248 should_preroll = false;
249 should_clear_cache = false;
250 new_cached_count = cache->Count();
251 } else {
252 // Paused same-frame edit refresh: force full cache refresh.
253 if (Timeline* timeline = dynamic_cast<Timeline*>(reader)) {
254 timeline->RequestClearAllCache();
255 }
256 new_cached_count = 0;
257 should_mark_seek = true;
258 should_preroll = true;
259 should_clear_cache = false;
260 }
261 } else {
262 // Same-frame refresh during playback should stay lightweight.
263 should_mark_seek = false;
264 should_preroll = false;
265 should_clear_cache = false;
266 if (cache && cache_contains) {
267 cache->Remove(clamped_new_position);
268 }
269 if (cache) {
270 new_cached_count = cache->Count();
271 }
272 }
273 } else {
274 if (cache && !cache_contains) {
275 should_mark_seek = true;
276 // Uncached commit seek: defer cache clear to cache thread loop.
277 new_cached_count = 0;
278 should_preroll = true;
279 should_clear_cache = true;
280 }
281 else if (cache)
282 {
283 // In-range commit seek preserves cache window/baseline.
284 should_mark_seek = false;
285 should_preroll = false;
286 should_clear_cache = false;
287 new_cached_count = cache->Count();
288 } else {
289 // No cache object to query: use normal seek behavior.
290 should_mark_seek = true;
291 }
292 }
293 leaving_scrub = true;
294 }
295 else {
296 // Non-preroll seeks cover paused scrubbing and live playback refresh.
297 const bool is_paused = (speed.load() == 0);
298 if (is_paused && same_frame_refresh) {
299 // Same-frame paused refresh updates only that frame.
300 should_mark_seek = false;
301 should_preroll = false;
302 should_clear_cache = false;
303 if (cache && cache_contains) {
304 cache->Remove(clamped_new_position);
305 }
306 if (cache) {
307 new_cached_count = cache->Count();
308 }
309 leaving_scrub = true;
310 }
311 else if (is_paused) {
312 if (cache && !cache_contains) {
313 should_mark_seek = true;
314 new_cached_count = 0;
315 should_clear_cache = true;
316 }
317 else if (cache) {
318 // In-range paused seek preserves cache continuity.
319 should_mark_seek = false;
320 new_cached_count = cache->Count();
321 } else {
322 should_mark_seek = true;
323 }
324 entering_scrub = true;
325 } else {
326 // During playback, keep seek/scrub side effects minimal.
327 should_mark_seek = false;
328 should_preroll = false;
329 should_clear_cache = false;
330 if (cache) {
331 new_cached_count = cache->Count();
332 }
333 leaving_scrub = true;
334 }
335 }
336
337 {
338 // Reset readiness baseline only when rebuilding cache.
339 const int dir = computeDirection();
340 if (should_mark_seek || should_preroll || should_clear_cache) {
341 last_cached_index.store(clamped_new_position - dir);
342 }
343 requested_display_frame.store(new_position);
344 cached_frame_count.store(new_cached_count);
345 preroll_on_next_fill.store(should_preroll);
346 // Clear behavior follows the latest seek intent.
347 clear_cache_on_next_fill.store(should_clear_cache);
348 userSeeked.store(should_mark_seek);
349 if (entering_scrub) {
350 scrub_active.store(true);
351 }
352 if (leaving_scrub) {
353 scrub_active.store(false);
354 }
355 }
357 acknowledgeRequest(started);
358 // A position-only seek can use existing decoded frames immediately.
359 // Do not wait for an unrelated in-flight decoder to acknowledge it.
360 // Keep processed_generation unchanged so the worker still reschedules
361 // prefetch at the new position when that decode completes.
362 auto* timeline = dynamic_cast<Timeline*>(reader);
363 if (!same_frame_refresh && speed.load() != 0 && cache_contains
364 && !should_clear_cache && readiness_physical_capacity > 0
365 && timeline && !timeline->CacheRefreshPending()
366 && timeline->CacheEpoch() == readiness_epoch) {
367 updateReadiness(cache, clamped_new_position, computeDirection());
368 }
369 }
370
371 void VideoCacheThread::Seek(int64_t new_position)
372 {
373 NotifyPlaybackPosition(new_position);
374 }
375
377 {
378 std::lock_guard<std::mutex> guard(seek_state_mutex);
379 if (new_position <= 0) {
380 return;
381 }
382 if (scrub_active.load()) {
383 return;
384 }
385
386 int64_t new_cached_count = cached_frame_count.load();
387 if (CacheBase* cache = reader ? reader->GetCache() : nullptr) {
388 new_cached_count = cache->Count();
389 }
390 {
391 requested_display_frame.store(new_position);
392 cached_frame_count.store(new_cached_count);
393 }
394 }
395
397 {
398 // If speed ≠ 0, use its sign; if speed==0, keep last_dir
399 const int current_speed = speed.load();
400 if (current_speed != 0) {
401 return (current_speed > 0 ? 1 : -1);
402 }
403 return last_dir.load();
404 }
405
406 void VideoCacheThread::handleUserSeek(int64_t playhead, int dir)
407 {
408 // Place last_cached_index just “behind” playhead in the given dir
409 last_cached_index.store(playhead - dir);
410 }
411
413 int dir,
414 int64_t timeline_end,
415 int64_t preroll_frames)
416 {
417 int64_t preroll_start = playhead;
418 if (preroll_frames > 0) {
419 if (dir > 0) {
420 preroll_start = std::max<int64_t>(1, playhead - preroll_frames);
421 }
422 else {
423 preroll_start = std::min<int64_t>(timeline_end, playhead + preroll_frames);
424 }
425 }
426 last_cached_index.store(preroll_start - dir);
427 }
428
430 {
431 if (!settings) {
432 return 0;
433 }
434 int64_t min_frames = settings->VIDEO_CACHE_MIN_PREROLL_FRAMES;
435 int64_t max_frames = settings->VIDEO_CACHE_MAX_PREROLL_FRAMES;
436 if (min_frames < 0) {
437 return 0;
438 }
439 if (max_frames > 0 && min_frames > max_frames) {
440 min_frames = max_frames;
441 }
442 return min_frames;
443 }
444
446 {
447 if (!reader) {
448 return 0;
449 }
450 int64_t timeline_end = reader->info.video_length;
451 if (auto* timeline = dynamic_cast<Timeline*>(reader)) {
452 const int64_t timeline_max = timeline->GetMaxFrame();
453 if (timeline_max > 0) {
454 timeline_end = timeline_max;
455 }
456 }
457 return timeline_end;
458 }
459
460 int64_t VideoCacheThread::clampToTimelineRange(int64_t frame, int64_t timeline_end) const
461 {
462 if (timeline_end < 1) {
463 return frame;
464 }
465 return std::clamp<int64_t>(frame, 1, timeline_end);
466 }
467
469 bool paused,
470 CacheBase* cache)
471 {
472 const int64_t timeline_end = resolveTimelineEnd();
473 int64_t cache_playhead = playhead;
474 if (reader) {
475 cache_playhead = clampToTimelineRange(playhead, timeline_end);
476 }
477 if (paused && !cache->Contains(cache_playhead)) {
478 // If paused and playhead not in cache, clear everything
479 if (Timeline* timeline = dynamic_cast<Timeline*>(reader)) {
480 timeline->ClearAllCache();
481 }
482 cached_frame_count.store(0);
483 return true;
484 }
485 return false;
486 }
487
489 int dir,
490 int64_t ahead_count,
491 int64_t timeline_end,
492 int64_t& window_begin,
493 int64_t& window_end) const
494 {
495 if (dir > 0) {
496 // Forward window: [playhead ... playhead + ahead_count]
497 window_begin = playhead;
498 window_end = playhead + ahead_count;
499 }
500 else {
501 // Backward window: [playhead - ahead_count ... playhead]
502 window_begin = playhead - ahead_count;
503 window_end = playhead;
504 }
505 // Clamp to [1 ... timeline_end]
506 window_begin = std::max<int64_t>(window_begin, 1);
507 window_end = std::min<int64_t>(window_end, timeline_end);
508 }
509
511 int64_t window_begin,
512 int64_t window_end,
513 int dir,
514 ReaderBase* reader,
515 int64_t max_frames_to_fetch,
516 uint64_t generation)
517 {
518 if (generation == UINT64_MAX)
519 generation = request_generation.load();
520 auto* timeline = dynamic_cast<Timeline*>(reader);
521 const uint64_t epoch = timeline ? timeline->CacheEpoch() : 0;
522 bool window_full = true;
523 int64_t next_frame = last_cached_index.load() + dir;
524 int64_t fetched_this_pass = 0;
525
526 // Advance from last_cached_index toward window boundary
527 while ((dir > 0 && next_frame <= window_end) ||
528 (dir < 0 && next_frame >= window_begin))
529 {
530 if (threadShouldExit()) {
531 break;
532 }
533 // If a Seek was requested mid-caching, bail out immediately
534 if (userSeeked.load() || request_generation.load() != generation
535 || (timeline && timeline->CacheEpoch() != epoch)) {
536 break;
537 }
538
539 if ((timeline && timeline->CacheRefreshPending()) || !cache->Contains(next_frame)) {
540 // Frame missing, fetch and add
541 try {
542 const int64_t decode_started = monotonicUs();
543 auto framePtr = reader->GetFrame(next_frame);
544 const int64_t decode_us = monotonicUs() - decode_started;
545 // The reader may have cached a valid reusable frame itself.
546 // Timeline edits own invalidation under their decoder lock;
547 // do not re-add its result after that lock has been released.
548 std::lock_guard<std::mutex> guard(seek_state_mutex);
549 if (threadShouldExit() || request_generation.load() != generation
550 || (timeline && timeline->CacheEpoch() != epoch)) {
551 if (diagnostics_enabled.load() && request_time_us.load())
553 return false;
554 }
555 preroll_policy.Observe(decode_us);
556 if (framePtr) observed_frame_bytes = std::max(observed_frame_bytes, framePtr->GetBytes());
557 if (!timeline)
558 cache->Add(framePtr);
559 cached_frame_count.store(cache->Count());
560 ++fetched_this_pass;
561 }
562 catch (const OutOfBoundsFrame&) {
563 break;
564 }
565 window_full = false;
566 }
567 else {
568 cache->Touch(next_frame);
569 }
570
571 {
572 std::lock_guard<std::mutex> guard(seek_state_mutex);
573 if (threadShouldExit() || request_generation.load() != generation
574 || (timeline && timeline->CacheEpoch() != epoch))
575 return false;
576 last_cached_index.store(next_frame);
577 if (timeline) {
578 const auto* settings = Settings::Instance();
579 const int64_t available = dir > 0 ? resolveTimelineEnd() - requested_display_frame.load()
580 : requested_display_frame.load() - 1;
582 std::min(readiness_capacity, std::max<int64_t>(0, available)),
583 settings->VIDEO_CACHE_MIN_PREROLL_FRAMES, settings->VIDEO_CACHE_MAX_PREROLL_FRAMES));
584 updateReadiness(cache, requested_display_frame.load(), dir);
585 }
586 if (diagnostics_enabled.load() && request_time_us.load()
587 && first_current_frame_us.load() == 0
588 && next_frame == clampToTimelineRange(requested_display_frame.load(),
589 timeline ? timeline->GetMaxFrame() : reader->info.video_length))
591 }
592 next_frame += dir;
593
594 // In active playback, avoid long uninterrupted prefetch bursts
595 // that can delay player thread frame retrieval.
596 if (max_frames_to_fetch > 0 && fetched_this_pass >= max_frames_to_fetch) {
597 break;
598 }
599 }
600
601 return window_full;
602 }
603
605 {
606 while (!threadShouldExit()) {
607 Settings* settings = Settings::Instance();
608 // Reader replacement waits for this worker before changing the raw
609 // pointer. The state lock never covers decoding or timeline clears.
610 CacheBase* cache = reader ? reader->GetCache() : nullptr;
611 Timeline* timeline = dynamic_cast<Timeline*>(reader);
612 uint64_t generation;
613 int64_t raw_playhead;
614 int dir;
615 bool paused, did_user_seek, use_preroll, should_clear;
616 {
617 std::lock_guard<std::mutex> guard(seek_state_mutex);
618 generation = request_generation.load();
619 raw_playhead = requested_display_frame.load();
620 dir = computeDirection();
621 paused = speed.load() == 0;
622 did_user_seek = userSeeked.exchange(false);
623 use_preroll = preroll_on_next_fill.exchange(false);
624 should_clear = clear_cache_on_next_fill.exchange(false);
625 }
626 // Position changes reset scheduling, not reusable reader caches.
627 // Full edit invalidation is deferred by Timeline::RequestClearAllCache.
628 if (request_generation.load() != generation)
629 continue;
630
631 if (!settings->ENABLE_PLAYBACK_CACHING || !cache) {
632 cache_audio_only.store(false);
633 min_frames_ahead.store(-1);
634 processed_generation.store(generation);
635 wait(50);
636 continue;
637 }
638
639 if (!timeline) {
640 // Window prefetch is Timeline-only. Direct FFmpeg/Dummy readers
641 // must not wait forever for a cache window this worker won't fill.
642 cache_audio_only.store(false);
643 min_frames_ahead.store(-1);
644 processed_generation.store(generation);
645 wait(50);
646 continue;
647 }
648 const int64_t timeline_end = resolveTimelineEnd();
649 const int64_t playhead = clampToTimelineRange(raw_playhead, timeline_end);
650 const int64_t preroll_frames = computePrerollFrames(settings);
651 const int64_t estimated_frame_bytes = getBytes(
652 timeline->preview_width ? timeline->preview_width : reader->info.width,
653 timeline->preview_height ? timeline->preview_height : reader->info.height,
655 int64_t bytes_per_frame;
656 {
657 std::lock_guard<std::mutex> guard(seek_state_mutex);
658 if (readiness_bytes_per_frame != estimated_frame_bytes) observed_frame_bytes = 0;
659 bytes_per_frame = std::max(estimated_frame_bytes, observed_frame_bytes);
660 }
661 const int64_t max_bytes = cache->GetMaxBytes();
662 const int64_t capacity = (max_bytes > 0 && bytes_per_frame > 0)
663 ? std::min<int64_t>(max_bytes / bytes_per_frame, settings->VIDEO_CACHE_MAX_FRAMES)
664 : 0;
665 const int64_t ahead_count = std::max<int64_t>(0, std::min<int64_t>(capacity - 1, static_cast<int64_t>(capacity * settings->VIDEO_CACHE_PERCENT_AHEAD)));
666 int64_t window_begin, window_end;
667 computeWindowBounds(playhead, dir, ahead_count, timeline_end, window_begin, window_end);
668 {
669 std::lock_guard<std::mutex> guard(seek_state_mutex);
670 if (request_generation.load() != generation)
671 continue;
672 const uint64_t epoch = timeline->CacheEpoch();
673 const bool epoch_changed = timeline_cache_epoch_initialized && epoch != seen_timeline_cache_epoch;
676 cached_frame_count.store(cache->Count());
677 if (epoch_changed || readiness_bytes_per_frame != estimated_frame_bytes) {
680 }
681 readiness_bytes_per_frame = estimated_frame_bytes;
682 cache_audio_only.store(capacity >= 1);
683 readiness_epoch = epoch;
685 readiness_timeline_end = timeline_end;
686 readiness_capacity = std::max<int64_t>(0, std::min(capacity - 1, ahead_count));
687 const int64_t available = dir > 0 ? timeline_end - playhead : playhead - 1;
688 readiness_minimum_step = std::min({readiness_capacity, available, std::max<int64_t>(1, std::abs(static_cast<int64_t>(speed.load())))});
690 std::min(readiness_capacity, available), settings->VIDEO_CACHE_MIN_PREROLL_FRAMES,
692 updateReadiness(cache, playhead, dir);
693 // Eviction can punch holes behind the prefetch high-water mark.
694 // Repair the first required hole, preserving reusable suffixes.
695 const int64_t required = std::max(min_frames_ahead.load(), readiness_minimum_step);
696 const bool missing_current = !cache->Contains(playhead);
697 if (missing_current || contiguous_ahead < required) {
698 const int64_t missing = missing_current ? playhead
699 : playhead + (contiguous_ahead + 1) * dir;
700 if ((last_cached_index.load() - missing) * dir >= 0)
701 last_cached_index.store(missing - dir);
702 }
703 if (epoch_changed || should_clear || processed_generation.load() != generation)
704 handleUserSeek(playhead, dir);
705 processed_generation.store(generation);
706 if (did_user_seek) {
707 if (use_preroll && paused)
708 handleUserSeekWithPreroll(playhead, dir, timeline_end, preroll_frames);
709 else
710 handleUserSeek(playhead, dir);
711 } else if (!paused && capacity >= 1) {
712 const bool outside_window = (dir > 0 && last_cached_index.load() > window_end)
713 || (dir < 0 && last_cached_index.load() < window_begin);
714 if (outside_window)
715 handleUserSeek(playhead, dir);
716 }
717 }
718 if (scrub_active.load()) {
719 wait(10);
720 continue;
721 }
722 if (capacity < 1) {
723 wait(50);
724 continue;
725 }
726 // Preserve reusable frames on position-only seeks. Full edit refresh
727 // is requested explicitly and consumed by Timeline's decoder.
728 bool window_full = prefetchWindow(cache, window_begin, window_end, dir,
729 reader, paused ? -1 : 8, generation);
730 if (request_generation.load() != generation)
731 continue; // The latest request restarts immediately, without sleep.
732 if (paused && window_full)
733 cache->Touch(playhead);
734 const double fps = reader->info.fps.ToFloat();
735 wait(fps > 0 ? std::max(1, static_cast<int>(1000.0 / fps / 4.0)) : 10);
736 }
737 }
738
739} // 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.
void Observe(int64_t duration_us)
void Reset(int64_t now)
int64_t Target(double fps, int speed, int64_t capacity, int64_t conservative, int64_t maximum)
bool Ready(int64_t now, int64_t contiguous_ahead, bool current_available, int64_t required, int64_t minimum_step=1)
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
double ToDouble() const
Return this fraction as a double (i.e. 1/2 = 0.5)
Definition: Fraction.cpp:40
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:91
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: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:154
uint64_t CacheEpoch() const
Return the current cache invalidation epoch.
Definition: Timeline.h:334
std::atomic< bool > diagnostics_enabled
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.
std::atomic< int64_t > acknowledgement_us
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.
int64_t clampToTimelineRange(int64_t frame, int64_t timeline_end) const
Clamp frame index to [1, timeline_end] when timeline_end is valid.
void Stop()
Stop method is unimplemented.
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 updateReadiness(CacheBase *cache, int64_t playhead, int dir)
Worker-only cache probing; never runs in the audio callback.
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::atomic< uint64_t > processed_generation
Last request whose scheduling baseline was consumed.
bool isReady()
Shared startup gate; bounded fallback resumes established video decoding.
std::mutex seek_state_mutex
Protects coherent seek state updates/consumption.
uint64_t readiness_generation
Request covered by the readiness snapshot.
bool prefetchWindow(CacheBase *cache, int64_t window_begin, int64_t window_end, int dir, ReaderBase *reader, int64_t max_frames_to_fetch=-1, uint64_t generation=UINT64_MAX)
Prefetch all missing frames in [window_begin ... window_end] or [window_end ... window_begin].
std::atomic< bool > cache_audio_only
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).
std::atomic< int64_t > first_current_frame_us
void Seek(int64_t new_position)
Backward-compatible alias for playback position updates (no seek side effects).
std::atomic< int64_t > request_time_us
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::mutex lifecycle_mutex
Start/stop/reader drain; never acquired by worker.
std::atomic< bool > userSeeked
True if Seek(..., true) was called (forces a cache reset).
std::atomic< bool > audio_cache_miss
std::atomic< uint64_t > request_generation
Invalidates obsolete fills, including seeks retaining cache.
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.
std::atomic< int64_t > obsolete_completion_us
void acknowledgeRequest(int64_t started_us)
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: AnimatedCurve.h:24
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