diff --git a/src/Clip.cpp b/src/Clip.cpp index 937f955c..f8c46590 100644 --- a/src/Clip.cpp +++ b/src/Clip.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #ifdef USE_IMAGEMAGICK #include "MagickUtilities.h" diff --git a/src/Qt/VideoCacheThread.cpp b/src/Qt/VideoCacheThread.cpp index a3c9aabd..2fdb6e3d 100644 --- a/src/Qt/VideoCacheThread.cpp +++ b/src/Qt/VideoCacheThread.cpp @@ -40,6 +40,8 @@ namespace openshot , reader(nullptr) , force_directional_cache(false) , last_cached_index(0) + , seen_timeline_cache_epoch(0) + , timeline_cache_epoch_initialized(false) { } @@ -130,6 +132,15 @@ namespace openshot return !isThreadRunning(); } + void VideoCacheThread::Reader(ReaderBase* new_reader) + { + std::lock_guard guard(seek_state_mutex); + reader = new_reader; + seen_timeline_cache_epoch = 0; + timeline_cache_epoch_initialized = false; + Play(); + } + void VideoCacheThread::Seek(int64_t new_position, bool start_preroll) { bool should_mark_seek = false; @@ -471,6 +482,27 @@ namespace openshot last_dir.store(dir); } + // If timeline-side cache invalidation occurred (e.g. ApplyJsonDiff / SetJson), + // restart fill from the active playhead window so invalidated gaps self-heal. + if (timeline) { + bool epoch_changed = false; + { + std::lock_guard guard(seek_state_mutex); + const uint64_t timeline_epoch = timeline->CacheEpoch(); + if (!timeline_cache_epoch_initialized) { + seen_timeline_cache_epoch = timeline_epoch; + timeline_cache_epoch_initialized = true; + } + else if (timeline_epoch != seen_timeline_cache_epoch) { + seen_timeline_cache_epoch = timeline_epoch; + epoch_changed = true; + } + } + if (epoch_changed) { + handleUserSeek(playhead, dir); + } + } + // Compute bytes_per_frame, max_bytes, and capacity once int64_t bytes_per_frame = getBytes( (timeline->preview_width ? timeline->preview_width : reader->info.width), diff --git a/src/Qt/VideoCacheThread.h b/src/Qt/VideoCacheThread.h index 58b7308f..2959725d 100644 --- a/src/Qt/VideoCacheThread.h +++ b/src/Qt/VideoCacheThread.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -84,7 +85,7 @@ namespace openshot * @brief Attach a ReaderBase (e.g. Timeline, FFmpegReader) and begin caching. * @param new_reader */ - void Reader(ReaderBase* new_reader) { reader = new_reader; Play(); } + void Reader(ReaderBase* new_reader); protected: /// Thread entry point: loops until threadShouldExit() is true. @@ -198,6 +199,8 @@ namespace openshot ReaderBase* reader; ///< The source reader (e.g., Timeline, FFmpegReader). bool force_directional_cache; ///< (Reserved for future use). + uint64_t seen_timeline_cache_epoch; ///< Last observed Timeline cache invalidation epoch. + bool timeline_cache_epoch_initialized; ///< True once an initial epoch snapshot has been taken. std::atomic last_cached_index; ///< Index of the most recently cached frame. mutable std::mutex seek_state_mutex; ///< Protects coherent seek state updates/consumption. diff --git a/src/Timeline.cpp b/src/Timeline.cpp index d5821ca9..a43f1484 100644 --- a/src/Timeline.cpp +++ b/src/Timeline.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -30,7 +31,7 @@ using namespace openshot; // Default Constructor for the timeline (which sets the canvas width and height) Timeline::Timeline(int width, int height, Fraction fps, int sample_rate, int channels, ChannelLayout channel_layout) : - is_open(false), auto_map_clips(true), managed_cache(true), path(""), max_time(0.0) + is_open(false), auto_map_clips(true), managed_cache(true), path(""), max_time(0.0), cache_epoch(0) { // Create CrashHandler and Attach (incase of errors) CrashHandler::Instance(); @@ -81,7 +82,7 @@ Timeline::Timeline(const ReaderInfo info) : Timeline::Timeline( // Constructor for the timeline (which loads a JSON structure from a file path, and initializes a timeline) Timeline::Timeline(const std::string& projectPath, bool convert_absolute_paths) : - is_open(false), auto_map_clips(true), managed_cache(true), path(projectPath), max_time(0.0) { + is_open(false), auto_map_clips(true), managed_cache(true), path(projectPath), max_time(0.0), cache_epoch(0) { // Create CrashHandler and Attach (incase of errors) CrashHandler::Instance(); @@ -1344,6 +1345,9 @@ void Timeline::SetJsonValue(const Json::Value root) { // Re-open if needed if (was_open) Open(); + + // Timeline content changed: notify cache clients to rescan active window. + BumpCacheEpoch(); } // Apply a special formatted JSON object, which represents a change to the timeline (insert, update, delete) @@ -1374,6 +1378,11 @@ void Timeline::ApplyJsonDiff(std::string value) { apply_json_to_timeline(change); } + + // Timeline content changed: notify cache clients to rescan active window. + if (!root.empty()) { + BumpCacheEpoch(); + } } catch (const std::exception& e) { @@ -1382,6 +1391,10 @@ void Timeline::ApplyJsonDiff(std::string value) { } } +void Timeline::BumpCacheEpoch() { + cache_epoch.fetch_add(1, std::memory_order_relaxed); +} + // Apply JSON diff to clips void Timeline::apply_json_to_clips(Json::Value change) { @@ -1791,6 +1804,9 @@ void Timeline::ClearAllCache(bool deep) { } catch (const ReaderClosed & e) { // ... } + + // Cache content changed: notify cache clients to rebuild their window baseline. + BumpCacheEpoch(); } // Set Max Image Size (used for performance optimization). Convenience function for setting diff --git a/src/Timeline.h b/src/Timeline.h index 83db07ec..98ba875c 100644 --- a/src/Timeline.h +++ b/src/Timeline.h @@ -15,11 +15,9 @@ #include #include -#include #include -#include -#include -#include +#include +#include #include "TimelineBase.h" #include "ReaderBase.h" @@ -167,6 +165,7 @@ namespace openshot { std::string path; ///< Optional path of loaded UTF-8 OpenShot JSON project file double max_time; ///> The max duration (in seconds) of the timeline, based on the furthest clip (right edge) double min_time; ///> The min duration (in seconds) of the timeline, based on the position of the first clip (left edge) + std::atomic cache_epoch; ///< Monotonic cache-invalidation epoch for playback cache reconciliation. std::map> tracked_objects; ///< map of TrackedObjectBBoxes and their IDs @@ -211,6 +210,9 @@ namespace openshot { /// Update the list of 'opened' clips void update_open_clips(openshot::Clip *clip, bool does_clip_intersect); + /// Increment the cache invalidation epoch. + void BumpCacheEpoch(); + public: /// @brief Constructor for the timeline (which configures the default frame properties) @@ -313,6 +315,9 @@ namespace openshot { /// of this cache object though (Timeline will not delete it for you). void SetCache(openshot::CacheBase* new_cache); + /// Return the current cache invalidation epoch. + uint64_t CacheEpoch() const { return cache_epoch.load(std::memory_order_relaxed); }; + /// Get an openshot::Frame object for a specific frame number of this timeline. /// /// @returns The requested frame (containing the image)