2015-06-01 00:20:14 -07:00
|
|
|
/**
|
|
|
|
|
* @file
|
|
|
|
|
* @brief Source file for VideoCacheThread class
|
|
|
|
|
* @author Jonathan Thomas <jonathan@openshot.org>
|
|
|
|
|
*
|
2019-06-09 08:31:04 -04:00
|
|
|
* @ref License
|
|
|
|
|
*/
|
|
|
|
|
|
2025-06-03 16:23:17 -05:00
|
|
|
// Copyright (c) 2008-2025 OpenShot Studios, LLC
|
2021-10-16 01:26:26 -04:00
|
|
|
//
|
|
|
|
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
2015-06-01 00:20:14 -07:00
|
|
|
|
2020-10-18 07:43:37 -04:00
|
|
|
#include "VideoCacheThread.h"
|
2021-10-27 14:34:05 -04:00
|
|
|
#include "CacheBase.h"
|
2021-01-26 10:52:04 -05:00
|
|
|
#include "Exceptions.h"
|
2021-10-27 14:34:05 -04:00
|
|
|
#include "Frame.h"
|
2022-09-15 18:33:06 -05:00
|
|
|
#include "Settings.h"
|
|
|
|
|
#include "Timeline.h"
|
2025-06-03 16:23:17 -05:00
|
|
|
#include <thread>
|
|
|
|
|
#include <chrono>
|
2020-09-02 02:07:54 -04:00
|
|
|
|
2015-06-01 00:20:14 -07:00
|
|
|
namespace openshot
|
|
|
|
|
{
|
2025-06-03 16:23:17 -05:00
|
|
|
// Constructor
|
|
|
|
|
VideoCacheThread::VideoCacheThread()
|
|
|
|
|
: Thread("video-cache")
|
|
|
|
|
, speed(0)
|
|
|
|
|
, last_speed(1)
|
|
|
|
|
, last_dir(1) // Assume forward (+1) on first launch
|
|
|
|
|
, is_playing(false)
|
|
|
|
|
, userSeeked(false)
|
|
|
|
|
, requested_display_frame(1)
|
|
|
|
|
, current_display_frame(1)
|
|
|
|
|
, cached_frame_count(0)
|
|
|
|
|
, min_frames_ahead(4)
|
|
|
|
|
, max_frames_ahead(8)
|
|
|
|
|
, timeline_max_frame(0)
|
|
|
|
|
, should_pause_cache(false)
|
|
|
|
|
, should_break(false)
|
|
|
|
|
, reader(nullptr)
|
|
|
|
|
, force_directional_cache(false)
|
2015-06-01 00:20:14 -07:00
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Destructor
|
2025-06-03 16:23:17 -05:00
|
|
|
VideoCacheThread::~VideoCacheThread()
|
2015-06-01 00:20:14 -07:00
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:23:17 -05:00
|
|
|
// Seek the reader to a particular frame number
|
|
|
|
|
void VideoCacheThread::setSpeed(int new_speed)
|
2022-01-26 17:56:33 -06:00
|
|
|
{
|
2025-06-03 16:23:17 -05:00
|
|
|
// Only update last_speed and last_dir when new_speed is non-zero.
|
2022-10-04 18:35:16 -05:00
|
|
|
if (new_speed != 0) {
|
|
|
|
|
last_speed = new_speed;
|
2025-06-03 16:23:17 -05:00
|
|
|
last_dir = (new_speed > 0 ? 1 : -1);
|
2022-10-04 18:35:16 -05:00
|
|
|
}
|
|
|
|
|
speed = new_speed;
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-20 16:55:07 -05:00
|
|
|
// Get the size in bytes of a frame (rough estimate)
|
|
|
|
|
int64_t VideoCacheThread::getBytes(int width, int height, int sample_rate, int channels, float fps)
|
|
|
|
|
{
|
2025-06-03 16:23:17 -05:00
|
|
|
// Estimate memory for RGBA video frame
|
|
|
|
|
int64_t bytes = static_cast<int64_t>(width) * height * sizeof(char) * 4;
|
2022-09-20 16:55:07 -05:00
|
|
|
|
2025-06-03 16:23:17 -05:00
|
|
|
// Approximate audio memory: (sample_rate * channels) samples per second,
|
|
|
|
|
// divided across fps frames, each sample is sizeof(float).
|
|
|
|
|
bytes += ((sample_rate * channels) / fps) * sizeof(float);
|
2022-09-20 16:55:07 -05:00
|
|
|
|
2025-06-03 16:23:17 -05:00
|
|
|
return bytes;
|
2022-09-20 16:55:07 -05:00
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:23:17 -05:00
|
|
|
// Play the video
|
|
|
|
|
void VideoCacheThread::Play()
|
|
|
|
|
{
|
|
|
|
|
is_playing = true;
|
|
|
|
|
}
|
2015-06-01 00:20:14 -07:00
|
|
|
|
2025-06-03 16:23:17 -05:00
|
|
|
// Stop the audio
|
|
|
|
|
void VideoCacheThread::Stop()
|
|
|
|
|
{
|
|
|
|
|
is_playing = false;
|
|
|
|
|
}
|
2015-06-01 00:20:14 -07:00
|
|
|
|
2025-06-03 16:23:17 -05:00
|
|
|
// Is cache ready for playback (pre-roll)
|
|
|
|
|
bool VideoCacheThread::isReady()
|
|
|
|
|
{
|
|
|
|
|
// Return true when pre-roll has cached at least min_frames_ahead frames.
|
|
|
|
|
return (cached_frame_count > min_frames_ahead);
|
|
|
|
|
}
|
2022-01-26 17:56:33 -06:00
|
|
|
|
2015-06-01 00:20:14 -07:00
|
|
|
// Start the thread
|
|
|
|
|
void VideoCacheThread::run()
|
|
|
|
|
{
|
2025-06-03 16:23:17 -05:00
|
|
|
using micro_sec = std::chrono::microseconds;
|
2022-01-18 13:08:32 -06:00
|
|
|
using double_micro_sec = std::chrono::duration<double, micro_sec::period>;
|
2015-06-01 00:20:14 -07:00
|
|
|
|
2025-06-03 16:46:04 -05:00
|
|
|
// last_cached_index: Index of the most recently cached frame.
|
|
|
|
|
// cache_start_index: Base index from which we build the window.
|
2025-06-03 16:23:17 -05:00
|
|
|
int64_t last_cached_index = 0;
|
|
|
|
|
int64_t cache_start_index = 0;
|
|
|
|
|
bool last_paused = true;
|
2022-09-20 16:55:07 -05:00
|
|
|
|
2025-06-03 16:23:17 -05:00
|
|
|
while (!threadShouldExit()) {
|
|
|
|
|
Settings* settings = Settings::Instance();
|
|
|
|
|
CacheBase* cache = reader ? reader->GetCache() : nullptr;
|
2022-09-15 18:33:06 -05:00
|
|
|
|
2025-06-03 16:23:17 -05:00
|
|
|
// If caching is disabled or no reader is assigned, wait briefly and retry
|
|
|
|
|
if (!settings->ENABLE_PLAYBACK_CACHING || !cache) {
|
|
|
|
|
std::this_thread::sleep_for(double_micro_sec(50000));
|
2022-02-24 15:56:39 -06:00
|
|
|
continue;
|
2025-06-03 16:23:17 -05:00
|
|
|
}
|
2022-02-28 15:45:46 -06:00
|
|
|
|
2025-06-03 16:23:17 -05:00
|
|
|
Timeline* timeline = static_cast<Timeline*>(reader);
|
|
|
|
|
int64_t timeline_end = timeline->GetMaxFrame();
|
|
|
|
|
int64_t playhead = requested_display_frame;
|
|
|
|
|
bool paused = (speed == 0);
|
2022-02-28 15:45:46 -06:00
|
|
|
|
2025-06-03 16:23:17 -05:00
|
|
|
// Determine the effective direction:
|
|
|
|
|
// If speed != 0, dir = sign(speed).
|
|
|
|
|
// Otherwise (speed == 0), use last_dir to continue caching in the same direction.
|
|
|
|
|
int dir = (speed != 0 ? (speed > 0 ? 1 : -1) : last_dir);
|
|
|
|
|
|
2025-06-03 16:46:04 -05:00
|
|
|
// On transition from paused (speed == 0) to playing (speed != 0), reset window base.
|
2025-06-03 16:23:17 -05:00
|
|
|
if (!paused && last_paused) {
|
|
|
|
|
cache_start_index = playhead;
|
|
|
|
|
last_cached_index = playhead - dir;
|
|
|
|
|
}
|
|
|
|
|
last_paused = paused;
|
|
|
|
|
|
|
|
|
|
// Calculate bytes needed for one frame in cache
|
|
|
|
|
int64_t bytes_per_frame = getBytes(
|
|
|
|
|
(timeline->preview_width ? timeline->preview_width : reader->info.width),
|
|
|
|
|
(timeline->preview_height ? timeline->preview_height : reader->info.height),
|
|
|
|
|
reader->info.sample_rate,
|
|
|
|
|
reader->info.channels,
|
|
|
|
|
reader->info.fps.ToFloat()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
int64_t max_bytes = cache->GetMaxBytes();
|
|
|
|
|
if (max_bytes <= 0 || bytes_per_frame <= 0) {
|
|
|
|
|
std::this_thread::sleep_for(double_micro_sec(50000));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int64_t capacity = max_bytes / bytes_per_frame;
|
|
|
|
|
if (capacity < 1) {
|
|
|
|
|
std::this_thread::sleep_for(double_micro_sec(50000));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:46:04 -05:00
|
|
|
// Number of frames to keep ahead (or behind) based on settings
|
2025-06-03 16:23:17 -05:00
|
|
|
int64_t ahead_count = static_cast<int64_t>(capacity * settings->VIDEO_CACHE_PERCENT_AHEAD);
|
|
|
|
|
|
2025-06-03 16:46:04 -05:00
|
|
|
// Handle user-initiated seek: always reset window base
|
2025-06-03 16:23:17 -05:00
|
|
|
bool user_seek = userSeeked;
|
|
|
|
|
if (user_seek) {
|
|
|
|
|
cache_start_index = playhead;
|
|
|
|
|
last_cached_index = playhead - dir;
|
|
|
|
|
userSeeked = false;
|
|
|
|
|
}
|
2025-06-03 16:46:04 -05:00
|
|
|
else if (!paused) {
|
|
|
|
|
// In playing mode, if playhead moves beyond last_cached, reset window
|
|
|
|
|
if ((dir > 0 && playhead > last_cached_index) || (dir < 0 && playhead < last_cached_index)) {
|
|
|
|
|
cache_start_index = playhead;
|
|
|
|
|
last_cached_index = playhead - dir;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-03 16:23:17 -05:00
|
|
|
|
2025-06-03 16:46:04 -05:00
|
|
|
// ----------------------------------------
|
|
|
|
|
// PAUSED MODE: Continue caching in 'dir' without advancing playhead
|
|
|
|
|
// ----------------------------------------
|
2025-06-03 16:23:17 -05:00
|
|
|
if (paused) {
|
2025-06-03 16:46:04 -05:00
|
|
|
// If the playhead is not in cache, clear and restart from playhead
|
2025-06-03 16:23:17 -05:00
|
|
|
if (!cache->Contains(playhead)) {
|
|
|
|
|
timeline->ClearAllCache();
|
|
|
|
|
cache_start_index = playhead;
|
|
|
|
|
last_cached_index = playhead - dir;
|
2022-02-28 15:45:46 -06:00
|
|
|
}
|
2025-06-03 16:46:04 -05:00
|
|
|
}
|
2022-02-28 15:45:46 -06:00
|
|
|
|
2025-06-03 16:46:04 -05:00
|
|
|
// Compute window bounds based on dir
|
|
|
|
|
int64_t window_begin, window_end;
|
|
|
|
|
if (dir > 0) {
|
|
|
|
|
// Forward: [cache_start_index ... cache_start_index + ahead_count]
|
|
|
|
|
window_begin = cache_start_index;
|
|
|
|
|
window_end = cache_start_index + ahead_count;
|
|
|
|
|
} else {
|
|
|
|
|
// Backward: [cache_start_index - ahead_count ... cache_start_index]
|
|
|
|
|
window_begin = cache_start_index - ahead_count;
|
|
|
|
|
window_end = cache_start_index;
|
|
|
|
|
}
|
2025-06-03 16:23:17 -05:00
|
|
|
|
2025-06-03 16:46:04 -05:00
|
|
|
// Clamp to valid timeline range
|
|
|
|
|
window_begin = std::max<int64_t>(window_begin, 1);
|
|
|
|
|
window_end = std::min<int64_t>(window_end, timeline_end);
|
2025-06-03 16:23:17 -05:00
|
|
|
|
2025-06-03 16:46:04 -05:00
|
|
|
// Prefetch loop: start from just beyond last_cached_index toward window_end
|
|
|
|
|
int64_t next_frame = last_cached_index + dir;
|
|
|
|
|
bool window_full = true;
|
|
|
|
|
|
|
|
|
|
while ((dir > 0 && next_frame <= window_end) || (dir < 0 && next_frame >= window_begin)) {
|
|
|
|
|
if (threadShouldExit()) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Interrupt if a new seek happened
|
|
|
|
|
if (userSeeked) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!cache->Contains(next_frame)) {
|
|
|
|
|
// Missing frame: fetch and add to cache
|
|
|
|
|
try {
|
|
|
|
|
auto framePtr = reader->GetFrame(next_frame);
|
|
|
|
|
cache->Add(framePtr);
|
|
|
|
|
++cached_frame_count;
|
2025-06-03 16:23:17 -05:00
|
|
|
}
|
2025-06-03 16:46:04 -05:00
|
|
|
catch (const OutOfBoundsFrame&) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
window_full = false; // We had to fetch at least one frame
|
2025-06-03 16:23:17 -05:00
|
|
|
}
|
|
|
|
|
else {
|
2025-06-03 16:46:04 -05:00
|
|
|
cache->Touch(next_frame);
|
2022-02-28 15:45:46 -06:00
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:46:04 -05:00
|
|
|
last_cached_index = next_frame;
|
|
|
|
|
next_frame += dir;
|
2022-01-14 15:16:04 -06:00
|
|
|
}
|
2015-08-05 23:40:58 -05:00
|
|
|
|
2025-06-03 16:46:04 -05:00
|
|
|
// In paused mode, if the entire window was already filled, touch playhead
|
|
|
|
|
if (paused && window_full) {
|
|
|
|
|
cache->Touch(playhead);
|
2022-02-09 17:29:04 -06:00
|
|
|
}
|
2022-01-14 15:16:04 -06:00
|
|
|
|
2025-06-03 16:46:04 -05:00
|
|
|
// Sleep a short fraction of a frame interval to throttle CPU usage
|
|
|
|
|
int64_t sleep_us = static_cast<int64_t>(1000000.0 / reader->info.fps.ToFloat() / 4.0);
|
|
|
|
|
std::this_thread::sleep_for(double_micro_sec(sleep_us));
|
2025-06-03 16:23:17 -05:00
|
|
|
}
|
2015-06-01 00:20:14 -07:00
|
|
|
}
|
2025-06-03 16:23:17 -05:00
|
|
|
|
|
|
|
|
void VideoCacheThread::Seek(int64_t new_position, bool start_preroll)
|
|
|
|
|
{
|
|
|
|
|
if (start_preroll) {
|
|
|
|
|
userSeeked = true;
|
|
|
|
|
}
|
|
|
|
|
requested_display_frame = new_position;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void VideoCacheThread::Seek(int64_t new_position)
|
|
|
|
|
{
|
|
|
|
|
Seek(new_position, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace openshot
|