From c53c9364f3ffdc393bb83d81c645e8d4ef1dd7fc Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Wed, 7 Sep 2016 00:40:01 -0500 Subject: [PATCH] Added new CacheDisk class, which caches frames to the hard drive, dramatically speeding up preview speeds, at the expense of IO operations. New unittests for caching framework. Fixed a few bugs with Frame constructor, which was causing invalid # width & height. Integrated JSON into the cache framework, to quickly share the state of the cache (including ranges of cached frame numbers). Fixed a bug where some Timeline frames could have no audio samples. --- include/CacheBase.h | 22 +- include/CacheDisk.h | 136 +++++++++ include/CacheMemory.h | 30 +- include/Frame.h | 4 +- include/OpenShot.h | 1 + include/Qt/VideoCacheThread.h | 4 +- include/ReaderBase.h | 2 +- include/Timeline.h | 9 +- src/CMakeLists.txt | 1 + src/CacheBase.cpp | 24 +- src/CacheDisk.cpp | 529 +++++++++++++++++++++++++++++++++ src/CacheMemory.cpp | 187 +++++++++++- src/Frame.cpp | 10 +- src/Qt/VideoCacheThread.cpp | 4 +- src/Timeline.cpp | 89 ++++-- src/bindings/python/openshot.i | 2 + src/bindings/ruby/openshot.i | 2 + tests/Cache_Tests.cpp | 114 +++++++ tests/ReaderBase_Tests.cpp | 2 +- 19 files changed, 1109 insertions(+), 63 deletions(-) create mode 100644 include/CacheDisk.h create mode 100644 src/CacheDisk.cpp diff --git a/include/CacheBase.h b/include/CacheBase.h index 2721ba85..c3c46164 100644 --- a/include/CacheBase.h +++ b/include/CacheBase.h @@ -31,6 +31,7 @@ #include #include "Frame.h" #include "Exceptions.h" +#include "Json.h" namespace openshot { @@ -44,7 +45,8 @@ namespace openshot { class CacheBase { protected: - int64 max_bytes; ///< This is the max number of bytes to cache (0 = no limit) + string cache_type; ///< This is a friendly type name of the derived cache instance + long long int max_bytes; ///< This is the max number of bytes to cache (0 = no limit) /// Section lock for multiple threads CriticalSection *cacheCriticalSection; @@ -56,7 +58,7 @@ namespace openshot { /// @brief Constructor that sets the max bytes to cache /// @param max_bytes The maximum bytes to allow in the cache. Once exceeded, the cache will purge the oldest frames. - CacheBase(int64 max_bytes); + CacheBase(long long int max_bytes); /// @brief Add a Frame to the cache /// @param frame The openshot::Frame object needing to be cached. @@ -73,7 +75,7 @@ namespace openshot { virtual tr1::shared_ptr GetFrame(long int frame_number) = 0; /// Gets the maximum bytes value - virtual int64 GetBytes() = 0; + virtual long long int GetBytes() = 0; /// Get the smallest frame number virtual tr1::shared_ptr GetSmallestFrame() = 0; @@ -82,12 +84,17 @@ namespace openshot { /// @param frame_number The frame number of the cached frame virtual void Remove(long int frame_number) = 0; + /// @brief Remove a range of frames + /// @param start_frame_number The starting frame number of the cached frame + /// @param end_frame_number The ending frame number of the cached frame + virtual void Remove(long int start_frame_number, long int end_frame_number) = 0; + /// Gets the maximum bytes value - int64 GetMaxBytes() { return max_bytes; }; + long long int GetMaxBytes() { return max_bytes; }; /// @brief Set maximum bytes to a different amount /// @param number_of_bytes The maximum bytes to allow in the cache. Once exceeded, the cache will purge the oldest frames. - void SetMaxBytes(int64 number_of_bytes) { max_bytes = number_of_bytes; }; + void SetMaxBytes(long long int number_of_bytes) { max_bytes = number_of_bytes; }; /// @brief Set maximum bytes to a different amount based on a ReaderInfo struct /// @param number_of_frames The maximum number of frames to hold in cache @@ -97,6 +104,11 @@ namespace openshot { /// @param channels The number of audio channels in the frame void SetMaxBytesFromInfo(long int number_of_frames, int width, int height, int sample_rate, int channels); + /// Get and Set JSON methods + virtual string Json() = 0; ///< Generate JSON string of this object + virtual void SetJson(string value) throw(InvalidJSON) = 0; ///< Load JSON string into this object + virtual Json::Value JsonValue() = 0; ///< Generate Json::JsonValue for this object + virtual void SetJsonValue(Json::Value root) = 0; ///< Load Json::JsonValue into this object }; diff --git a/include/CacheDisk.h b/include/CacheDisk.h new file mode 100644 index 00000000..8fb35290 --- /dev/null +++ b/include/CacheDisk.h @@ -0,0 +1,136 @@ +/** + * @file + * @brief Header file for CacheDisk class + * @author Jonathan Thomas + * + * @section LICENSE + * + * Copyright (c) 2008-2014 OpenShot Studios, LLC + * . This file is part of + * OpenShot Library (libopenshot), an open-source project dedicated to + * delivering high quality video editing and animation solutions to the + * world. For more information visit . + * + * OpenShot Library (libopenshot) is free software: you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * OpenShot Library (libopenshot) is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with OpenShot Library. If not, see . + */ + +#ifndef OPENSHOT_CACHE_DISK_H +#define OPENSHOT_CACHE_DISK_H + +#include +#include +#include +#include "CacheBase.h" +#include "Frame.h" +#include "Exceptions.h" +#include +#include +#include + +namespace openshot { + + /** + * @brief This class is a disk-based cache manager for Frame objects. + * + * It is used by the Timeline class, if enabled, to cache video and audio frames to disk, to cut down on CPU + * and memory utilization. This will thrash a user's disk, but save their memory and CPU. It's a trade off that + * sometimes makes perfect sense. You can also set the max number of bytes to cache. + */ + class CacheDisk : public CacheBase { + private: + QDir path; ///< This is the folder path of the cache directory + map frames; ///< This map holds the frame number and Frame objects + deque frame_numbers; ///< This queue holds a sequential list of cached Frame numbers + string image_format; + float image_quality; + float image_scale; + + long long int frame_size_bytes; ///< The size of the cached frame in bytes + bool needs_range_processing; ///< Something has changed, and the range data needs to be re-calculated + Json::Value ranges; ///< JSON ranges of frame numbers + vector ordered_frame_numbers; ///< Ordered list of frame numbers used by cache + map frame_ranges; ///< This map holds the ranges of frames, useful for quickly displaying the contents of the cache + long int range_version; ///< The version of the JSON range data (incremented with each change) + + /// Clean up cached frames that exceed the max number of bytes + void CleanUp(); + + /// Init path directory + void InitPath(string cache_path); + + /// Calculate ranges of frames + void CalculateRanges(); + + public: + /// @brief Default constructor, no max bytes + /// @param cache_path The folder path of the cache directory (empty string = /tmp/preview-cache/) + /// @param format The image format for disk caching (ppm, jpg, png) + /// @param quality The quality of the image (1.0=highest quality/slowest speed, 0.0=worst quality/fastest speed) + /// @param scale The scale factor for the preview images (1.0 = original size, 0.5=half size, 0.25=quarter size, etc...) + CacheDisk(string cache_path, string format, float quality, float scale); + + /// @brief Constructor that sets the max bytes to cache + /// @param cache_path The folder path of the cache directory (empty string = /tmp/preview-cache/) + /// @param format The image format for disk caching (ppm, jpg, png) + /// @param quality The quality of the image (1.0=highest quality/slowest speed, 0.0=worst quality/fastest speed) + /// @param scale The scale factor for the preview images (1.0 = original size, 0.5=half size, 0.25=quarter size, etc...) + /// @param max_bytes The maximum bytes to allow in the cache. Once exceeded, the cache will purge the oldest frames. + CacheDisk(string cache_path, string format, float quality, float scale, long long int max_bytes); + + // Default destructor + ~CacheDisk(); + + /// @brief Add a Frame to the cache + /// @param frame The openshot::Frame object needing to be cached. + void Add(tr1::shared_ptr frame); + + /// Clear the cache of all frames + void Clear(); + + /// Count the frames in the queue + long int Count(); + + /// @brief Get a frame from the cache + /// @param frame_number The frame number of the cached frame + tr1::shared_ptr GetFrame(long int frame_number); + + /// Gets the maximum bytes value + long long int GetBytes(); + + /// Get the smallest frame number + tr1::shared_ptr GetSmallestFrame(); + + /// @brief Move frame to front of queue (so it lasts longer) + /// @param frame_number The frame number of the cached frame + void MoveToFront(long int frame_number); + + /// @brief Remove a specific frame + /// @param frame_number The frame number of the cached frame + void Remove(long int frame_number); + + /// @brief Remove a range of frames + /// @param start_frame_number The starting frame number of the cached frame + /// @param end_frame_number The ending frame number of the cached frame + void Remove(long int start_frame_number, long int end_frame_number); + + /// Get and Set JSON methods + string Json(); ///< Generate JSON string of this object + void SetJson(string value) throw(InvalidJSON); ///< Load JSON string into this object + Json::Value JsonValue(); ///< Generate Json::JsonValue for this object + void SetJsonValue(Json::Value root) throw(InvalidFile, ReaderClosed); ///< Load Json::JsonValue into this object + }; + +} + +#endif diff --git a/include/CacheMemory.h b/include/CacheMemory.h index b0753e21..058a842f 100644 --- a/include/CacheMemory.h +++ b/include/CacheMemory.h @@ -1,6 +1,6 @@ /** * @file - * @brief Header file for Cache class + * @brief Header file for CacheMemory class * @author Jonathan Thomas * * @section LICENSE @@ -25,8 +25,8 @@ * along with OpenShot Library. If not, see . */ -#ifndef OPENSHOT_CACHE_H -#define OPENSHOT_CACHE_H +#ifndef OPENSHOT_CACHE_MEMORY_H +#define OPENSHOT_CACHE_MEMORY_H #include #include @@ -38,7 +38,7 @@ namespace openshot { /** - * @brief This class is a cache manager for Frame objects. + * @brief This class is a memory-based cache manager for Frame objects. * * It is used by FileReaders (such as FFmpegReader) to cache recently accessed frames. Due to the * high cost of decoding streams, once a frame is decoded, converted to RGB, and a Frame object is created, @@ -50,9 +50,17 @@ namespace openshot { map > frames; ///< This map holds the frame number and Frame objects deque frame_numbers; ///< This queue holds a sequential list of cached Frame numbers + bool needs_range_processing; ///< Something has changed, and the range data needs to be re-calculated + Json::Value ranges; ///< JSON ranges of frame numbers + vector ordered_frame_numbers; ///< Ordered list of frame numbers used by cache + map frame_ranges; ///< This map holds the ranges of frames, useful for quickly displaying the contents of the cache + long int range_version; ///< The version of the JSON range data (incremented with each change) + /// Clean up cached frames that exceed the max number of bytes void CleanUp(); + /// Calculate ranges of frames + void CalculateRanges(); public: /// Default constructor, no max bytes @@ -60,7 +68,7 @@ namespace openshot { /// @brief Constructor that sets the max bytes to cache /// @param max_bytes The maximum bytes to allow in the cache. Once exceeded, the cache will purge the oldest frames. - CacheMemory(int64 max_bytes); + CacheMemory(long long int max_bytes); // Default destructor ~CacheMemory(); @@ -80,7 +88,7 @@ namespace openshot { tr1::shared_ptr GetFrame(long int frame_number); /// Gets the maximum bytes value - int64 GetBytes(); + long long int GetBytes(); /// Get the smallest frame number tr1::shared_ptr GetSmallestFrame(); @@ -93,6 +101,16 @@ namespace openshot { /// @param frame_number The frame number of the cached frame void Remove(long int frame_number); + /// @brief Remove a range of frames + /// @param start_frame_number The starting frame number of the cached frame + /// @param end_frame_number The ending frame number of the cached frame + void Remove(long int start_frame_number, long int end_frame_number); + + /// Get and Set JSON methods + string Json(); ///< Generate JSON string of this object + void SetJson(string value) throw(InvalidJSON); ///< Load JSON string into this object + Json::Value JsonValue(); ///< Generate Json::JsonValue for this object + void SetJsonValue(Json::Value root) throw(InvalidFile, ReaderClosed); ///< Load Json::JsonValue into this object }; } diff --git a/include/Frame.h b/include/Frame.h index 66fdb11f..ac9d483d 100644 --- a/include/Frame.h +++ b/include/Frame.h @@ -158,10 +158,10 @@ namespace openshot ~Frame(); /// Add (or replace) pixel data to the frame (based on a solid color) - void AddColor(int width, int height, string color); + void AddColor(int new_width, int new_height, string color); /// Add (or replace) pixel data to the frame - void AddImage(int width, int height, int bytes_per_pixel, QImage::Format type, const unsigned char *pixels_); + void AddImage(int new_width, int new_height, int bytes_per_pixel, QImage::Format type, const unsigned char *pixels_); /// Add (or replace) pixel data to the frame void AddImage(tr1::shared_ptr new_image); diff --git a/include/OpenShot.h b/include/OpenShot.h index 110c3d23..b57d191e 100644 --- a/include/OpenShot.h +++ b/include/OpenShot.h @@ -99,6 +99,7 @@ #include "AudioBufferSource.h" #include "AudioReaderSource.h" #include "AudioResampler.h" +#include "CacheDisk.h" #include "CacheMemory.h" #include "ChunkReader.h" #include "ChunkWriter.h" diff --git a/include/Qt/VideoCacheThread.h b/include/Qt/VideoCacheThread.h index 7a3f9c60..d8fe5da3 100644 --- a/include/Qt/VideoCacheThread.h +++ b/include/Qt/VideoCacheThread.h @@ -61,10 +61,10 @@ namespace openshot /// Get Speed (The speed and direction to playback a reader (1=normal, 2=fast, 3=faster, -1=rewind, etc...) int getSpeed() const { return speed; } - /// Play the audio + /// Play the video void Play(); - /// Seek the audio thread + /// Seek the reader to a particular frame number void Seek(int new_position); /// Set the currently displaying frame number diff --git a/include/ReaderBase.h b/include/ReaderBase.h index c9a507b3..7e766a0a 100644 --- a/include/ReaderBase.h +++ b/include/ReaderBase.h @@ -117,7 +117,7 @@ namespace openshot void DrawFrameOnScene(string path, long _graphics_scene_address); /// Get the cache object used by this reader (note: not all readers use cache) - virtual CacheMemory* GetCache() = 0; + virtual CacheBase* GetCache() = 0; /// This method is required for all derived classes of ReaderBase, and returns the /// openshot::Frame object, which contains the image and audio information for that diff --git a/include/Timeline.h b/include/Timeline.h index 7759a56b..2241a6c2 100644 --- a/include/Timeline.h +++ b/include/Timeline.h @@ -32,6 +32,8 @@ #include #include #include +#include "CacheBase.h" +#include "CacheDisk.h" #include "CacheMemory.h" #include "Color.h" #include "Clip.h" @@ -147,7 +149,7 @@ namespace openshot { list closing_clips; /// open_clips; /// effects; /// new_frame, Clip* source_clip, long int clip_frame_number, long int timeline_frame_number, bool is_top_clip); @@ -228,7 +230,10 @@ namespace openshot { list Effects() { return effects; }; /// Get the cache object used by this reader - CacheMemory* GetCache() { return &final_cache; }; + CacheBase* GetCache() { return final_cache; }; + + /// Get the cache object used by this reader + void SetCache(CacheBase* new_cache); /// Get an openshot::Frame object for a specific frame number of this timeline. /// diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 640ea7de..be53da5d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -188,6 +188,7 @@ SET ( OPENSHOT_SOURCE_FILES AudioReaderSource.cpp AudioResampler.cpp CacheBase.cpp + CacheDisk.cpp CacheMemory.cpp ChunkReader.cpp ChunkWriter.cpp diff --git a/src/CacheBase.cpp b/src/CacheBase.cpp index 9ab1956a..c7218105 100644 --- a/src/CacheBase.cpp +++ b/src/CacheBase.cpp @@ -37,7 +37,7 @@ CacheBase::CacheBase() : max_bytes(0) { }; // Constructor that sets the max frames to cache -CacheBase::CacheBase(int64 max_bytes) : max_bytes(max_bytes) { +CacheBase::CacheBase(long long int max_bytes) : max_bytes(max_bytes) { // Init the critical section cacheCriticalSection = new CriticalSection(); }; @@ -46,7 +46,27 @@ CacheBase::CacheBase(int64 max_bytes) : max_bytes(max_bytes) { void CacheBase::SetMaxBytesFromInfo(long int number_of_frames, int width, int height, int sample_rate, int channels) { // n frames X height X width X 4 colors of chars X audio channels X 4 byte floats - int64 bytes = number_of_frames * (height * width * 4 + (sample_rate * channels * 4)); + long long int bytes = number_of_frames * (height * width * 4 + (sample_rate * channels * 4)); SetMaxBytes(bytes); } +// Generate Json::JsonValue for this object +Json::Value CacheBase::JsonValue() { + + // Create root json object + Json::Value root; + stringstream max_bytes_stream; + max_bytes_stream << max_bytes; + root["max_bytes"] = max_bytes_stream.str(); + + // return JsonValue + return root; +} + +// Load Json::JsonValue into this object +void CacheBase::SetJsonValue(Json::Value root) { + + // Set data from Json (if key is found) + if (!root["max_bytes"].isNull()) + max_bytes = atoll(root["max_bytes"].asString().c_str()); +} \ No newline at end of file diff --git a/src/CacheDisk.cpp b/src/CacheDisk.cpp new file mode 100644 index 00000000..9691f51d --- /dev/null +++ b/src/CacheDisk.cpp @@ -0,0 +1,529 @@ +/** + * @file + * @brief Source file for CacheDisk class + * @author Jonathan Thomas + * + * @section LICENSE + * + * Copyright (c) 2008-2014 OpenShot Studios, LLC + * . This file is part of + * OpenShot Library (libopenshot), an open-source project dedicated to + * delivering high quality video editing and animation solutions to the + * world. For more information visit . + * + * OpenShot Library (libopenshot) is free software: you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * OpenShot Library (libopenshot) is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with OpenShot Library. If not, see . + */ + +#include "../include/CacheDisk.h" + +using namespace std; +using namespace openshot; + +// Default constructor, no max bytes +CacheDisk::CacheDisk(string cache_path, string format, float quality, float scale) : CacheBase(0) { + // Set cache type name + cache_type = "CacheDisk"; + range_version = 0; + needs_range_processing = false; + frame_size_bytes = 0; + image_format = format; + image_quality = quality; + image_scale = scale; + + // Init path directory + InitPath(cache_path); +}; + +// Constructor that sets the max bytes to cache +CacheDisk::CacheDisk(string cache_path, string format, float quality, float scale, long long int max_bytes) : CacheBase(max_bytes) { + // Set cache type name + cache_type = "CacheDisk"; + range_version = 0; + needs_range_processing = false; + frame_size_bytes = 0; + image_format = format; + image_quality = quality; + image_scale = scale; + + // Init path directory + InitPath(cache_path); +}; + +// Initialize cache directory +void CacheDisk::InitPath(string cache_path) { + QString qpath; + + if (!cache_path.empty()) { + // Init QDir with cache directory + qpath = QString(cache_path.c_str()); + + } else { + // Init QDir with user's temp directory + qpath = QDir::tempPath() + QString("/preview-cache/"); + } + + // Init QDir with cache directory + path = QDir(qpath); + + // Check if cache directory exists + if (!path.exists()) + // Create + path.mkpath(qpath); +} + +// Calculate ranges of frames +void CacheDisk::CalculateRanges() { + // Only calculate when something has changed + if (needs_range_processing) { + + // Create a scoped lock, to protect the cache from multiple threads + const GenericScopedLock lock(*cacheCriticalSection); + + // Sort ordered frame #s, and calculate JSON ranges + std::sort(ordered_frame_numbers.begin(), ordered_frame_numbers.end()); + + // Clear existing JSON variable + ranges.clear(); + ranges = Json::Value(Json::arrayValue); + + // Increment range version + range_version++; + + vector::iterator itr_ordered; + long int starting_frame = *ordered_frame_numbers.begin(); + long int ending_frame = *ordered_frame_numbers.begin(); + + // Loop through all known frames (in sequential order) + for (itr_ordered = ordered_frame_numbers.begin(); itr_ordered != ordered_frame_numbers.end(); ++itr_ordered) { + long int frame_number = *itr_ordered; + if (frame_number - ending_frame > 1) { + // End of range detected + Json::Value range; + + // Add JSON object with start/end attributes + // Use strings, since long ints are supported in JSON + stringstream start_str; + start_str << starting_frame; + stringstream end_str; + end_str << ending_frame; + range["start"] = start_str.str(); + range["end"] = end_str.str(); + ranges.append(range); + + // Set new starting range + starting_frame = frame_number; + } + + // Set current frame as end of range, and keep looping + ending_frame = frame_number; + } + + // APPEND FINAL VALUE + Json::Value range; + + // Add JSON object with start/end attributes + // Use strings, since long ints are supported in JSON + stringstream start_str; + start_str << starting_frame; + stringstream end_str; + end_str << ending_frame; + range["start"] = start_str.str(); + range["end"] = end_str.str(); + ranges.append(range); + + // Reset needs_range_processing + needs_range_processing = false; + } +} + +// Default destructor +CacheDisk::~CacheDisk() +{ + frames.clear(); + frame_numbers.clear(); + ordered_frame_numbers.clear(); + + // remove critical section + delete cacheCriticalSection; + cacheCriticalSection = NULL; +} + +// Add a Frame to the cache +void CacheDisk::Add(tr1::shared_ptr frame) +{ + // Create a scoped lock, to protect the cache from multiple threads + const GenericScopedLock lock(*cacheCriticalSection); + long int frame_number = frame->number; + + // Freshen frame if it already exists + if (frames.count(frame_number)) + // Move frame to front of queue + MoveToFront(frame_number); + + else + { + // Add frame to queue and map + frames[frame_number] = frame_number; + frame_numbers.push_front(frame_number); + ordered_frame_numbers.push_back(frame_number); + needs_range_processing = true; + + // Save image to disk (if needed) + QString frame_path(path.path() + "/" + QString("%1.").arg(frame_number) + QString(image_format.c_str()).toLower()); + frame->Save(frame_path.toStdString(), image_scale, image_format, image_quality); + if (frame_size_bytes == 0) { + // Get compressed size of frame image (to correctly apply max size against) + QFile image_file(frame_path); + frame_size_bytes = image_file.size(); + } + + // Save audio data (if needed) + if (frame->has_audio_data) { + QString audio_path(path.path() + "/" + QString("%1").arg(frame_number) + ".audio"); + QFile audio_file(audio_path); + + if (audio_file.open(QIODevice::WriteOnly)) { + QTextStream audio_stream(&audio_file); + audio_stream << frame->SampleRate() << endl; + audio_stream << frame->GetAudioChannelsCount() << endl; + audio_stream << frame->GetAudioSamplesCount() << endl; + audio_stream << frame->ChannelsLayout() << endl; + + // Loop through all samples + for (int channel = 0; channel < frame->GetAudioChannelsCount(); channel++) + { + // Get audio for this channel + float *samples = frame->GetAudioSamples(channel); + for (int sample = 0; sample < frame->GetAudioSamplesCount(); sample++) + audio_stream << samples[sample] << endl; + } + + } + + } + + // Clean up old frames + CleanUp(); + } +} + +// Get a frame from the cache (or NULL shared_ptr if no frame is found) +tr1::shared_ptr CacheDisk::GetFrame(long int frame_number) +{ + // Create a scoped lock, to protect the cache from multiple threads + const GenericScopedLock lock(*cacheCriticalSection); + + // Does frame exists in cache? + if (frames.count(frame_number)) { + // Does frame exist on disk + QString frame_path(path.path() + "/" + QString("%1.").arg(frame_number) + QString(image_format.c_str()).toLower()); + if (path.exists(frame_path)) { + + // Load image file + tr1::shared_ptr image = tr1::shared_ptr(new QImage()); + bool success = image->load(QString::fromStdString(frame_path.toStdString())); + + // Set pixel formatimage-> + image = tr1::shared_ptr(new QImage(image->convertToFormat(QImage::Format_RGBA8888))); + + // Create frame object + tr1::shared_ptr frame(new Frame()); + frame->number = frame_number; + frame->AddImage(image); + + // Get audio data (if found) + QString audio_path(path.path() + "/" + QString("%1").arg(frame_number) + ".audio"); + QFile audio_file(audio_path); + if (audio_file.exists()) { + // Open audio file + QTextStream in(&audio_file); + if (audio_file.open(QIODevice::ReadOnly)) { + int sample_rate = in.readLine().toInt(); + int channels = in.readLine().toInt(); + int sample_count = in.readLine().toInt(); + int channel_layout = in.readLine().toInt(); + + // Set basic audio properties + frame->ResizeAudio(channels, sample_count, sample_rate, (ChannelLayout) channel_layout); + + // Loop through audio samples and add to frame + int current_channel = 0; + int current_sample = 0; + float *channel_samples = new float[sample_count]; + while (!in.atEnd()) { + // Add sample to channel array + channel_samples[current_sample] = in.readLine().toFloat(); + current_sample++; + + if (current_sample == sample_count) { + // Add audio to frame + frame->AddAudio(true, current_channel, 0, channel_samples, sample_count, 1.0); + + // Increment channel, and reset sample position + current_channel++; + current_sample = 0; + } + + } + } + } + + // return the Frame object + return frame; + } + } + + // no Frame found + return tr1::shared_ptr(); +} + +// Get the smallest frame number (or NULL shared_ptr if no frame is found) +tr1::shared_ptr CacheDisk::GetSmallestFrame() +{ + // Create a scoped lock, to protect the cache from multiple threads + const GenericScopedLock lock(*cacheCriticalSection); + tr1::shared_ptr f; + + // Loop through frame numbers + deque::iterator itr; + long int smallest_frame = -1; + for(itr = frame_numbers.begin(); itr != frame_numbers.end(); ++itr) + { + if (*itr < smallest_frame || smallest_frame == -1) + smallest_frame = *itr; + } + + // Return frame + f = GetFrame(smallest_frame); + + return f; +} + +// Gets the maximum bytes value +long long int CacheDisk::GetBytes() +{ + // Create a scoped lock, to protect the cache from multiple threads + const GenericScopedLock lock(*cacheCriticalSection); + + long long int total_bytes = 0; + + // Loop through frames, and calculate total bytes + deque::reverse_iterator itr; + for(itr = frame_numbers.rbegin(); itr != frame_numbers.rend(); ++itr) + total_bytes += frame_size_bytes; + + return total_bytes; +} + +// Remove a specific frame +void CacheDisk::Remove(long int frame_number) +{ + Remove(frame_number, frame_number); +} + +// Remove range of frames +void CacheDisk::Remove(long int start_frame_number, long int end_frame_number) +{ + // Create a scoped lock, to protect the cache from multiple threads + const GenericScopedLock lock(*cacheCriticalSection); + + // Loop through frame numbers + deque::iterator itr = frame_numbers.begin(); + while (itr != frame_numbers.end()) + { + //deque::iterator current = itr++; + if (*itr >= start_frame_number && *itr <= end_frame_number) + { + // erase frame number + itr = frame_numbers.erase(itr++); + } else + ++itr; + } + + // Loop through ordered frame numbers + vector::iterator itr_ordered = ordered_frame_numbers.begin(); + while (itr_ordered != ordered_frame_numbers.end()) + { + if (*itr_ordered >= start_frame_number && *itr_ordered <= end_frame_number) + { + // erase frame number + frames.erase(*itr_ordered); + + // Remove the image file (if it exists) + QString frame_path(path.path() + "/" + QString("%1.").arg(*itr_ordered) + QString(image_format.c_str()).toLower()); + QFile image_file(frame_path); + if (image_file.exists()) + image_file.remove(); + + // Remove audio file (if it exists) + QString audio_path(path.path() + "/" + QString("%1").arg(*itr_ordered) + ".audio"); + QFile audio_file(audio_path); + if (audio_file.exists()) + audio_file.remove(); + + itr_ordered = ordered_frame_numbers.erase(itr_ordered++); + } else + ++itr_ordered; + } + + // Needs range processing (since cache has changed) + needs_range_processing = true; +} + +// Move frame to front of queue (so it lasts longer) +void CacheDisk::MoveToFront(long int frame_number) +{ + // Create a scoped lock, to protect the cache from multiple threads + const GenericScopedLock lock(*cacheCriticalSection); + + // Does frame exists in cache? + /* FIXME if the frame number isn't present, the loop will do nothing, so why protect it? + * Is it to save time by avoiding a loop? + * Do we really need to optmize the case where we've been given a nonexisting frame_number? */ + if (frames.count(frame_number)) + { + // Loop through frame numbers + deque::iterator itr; + for(itr = frame_numbers.begin(); itr != frame_numbers.end(); ++itr) + { + if (*itr == frame_number) + { + // erase frame number + frame_numbers.erase(itr); + + // add frame number to 'front' of queue + frame_numbers.push_front(frame_number); + break; + } + } + } +} + +// Clear the cache of all frames +void CacheDisk::Clear() +{ + // Create a scoped lock, to protect the cache from multiple threads + const GenericScopedLock lock(*cacheCriticalSection); + + // Clear all containers + frames.clear(); + frame_numbers.clear(); + ordered_frame_numbers.clear(); + needs_range_processing = true; + frame_size_bytes = 0; + + // Delete cache directory, and recreate it + QString current_path = path.path(); + path.removeRecursively(); + + // Re-init folder + InitPath(current_path.toStdString()); +} + +// Count the frames in the queue +long int CacheDisk::Count() +{ + // Create a scoped lock, to protect the cache from multiple threads + const GenericScopedLock lock(*cacheCriticalSection); + + // Return the number of frames in the cache + return frames.size(); +} + +// Clean up cached frames that exceed the number in our max_bytes variable +void CacheDisk::CleanUp() +{ + // Create a scoped lock, to protect the cache from multiple threads + const GenericScopedLock lock(*cacheCriticalSection); + + // Do we auto clean up? + if (max_bytes > 0) + { + while (GetBytes() > max_bytes && frame_numbers.size() > 20) + { + // Get the oldest frame number. + long int frame_to_remove = frame_numbers.back(); + + // Remove frame_number and frame + Remove(frame_to_remove); + } + } +} + +// Generate JSON string of this object +string CacheDisk::Json() { + + // Return formatted string + return JsonValue().toStyledString(); +} + +// Generate Json::JsonValue for this object +Json::Value CacheDisk::JsonValue() { + + // Proccess range data (if anything has changed) + CalculateRanges(); + + // Create root json object + Json::Value root = CacheBase::JsonValue(); // get parent properties + root["type"] = cache_type; + root["path"] = path.path().toStdString(); + root["ranges"] = ranges; + + Json::Value version; + stringstream range_version_str; + range_version_str << range_version; + root["version"] = range_version_str.str(); + + // return JsonValue + return root; +} + +// Load JSON string into this object +void CacheDisk::SetJson(string value) throw(InvalidJSON) { + + // Parse JSON string into JSON objects + Json::Value root; + Json::Reader reader; + bool success = reader.parse( value, root ); + if (!success) + // Raise exception + throw InvalidJSON("JSON could not be parsed (or is invalid)", ""); + + try + { + // Set all values that match + SetJsonValue(root); + } + catch (exception e) + { + // Error parsing JSON (or missing keys) + throw InvalidJSON("JSON is invalid (missing keys or invalid data types)", ""); + } +} + +// Load Json::JsonValue into this object +void CacheDisk::SetJsonValue(Json::Value root) throw(InvalidFile, ReaderClosed) { + + // Close timeline before we do anything (this also removes all open and closing clips) + Clear(); + + // Set parent data + CacheBase::SetJsonValue(root); + + if (!root["type"].isNull()) + cache_type = root["type"].asString(); + if (!root["path"].isNull()) + // Update duration of timeline + InitPath(root["path"].asString()); +} \ No newline at end of file diff --git a/src/CacheMemory.cpp b/src/CacheMemory.cpp index 54123819..2279f834 100644 --- a/src/CacheMemory.cpp +++ b/src/CacheMemory.cpp @@ -32,12 +32,18 @@ using namespace openshot; // Default constructor, no max bytes CacheMemory::CacheMemory() : CacheBase(0) { - + // Set cache type name + cache_type = "CacheMemory"; + range_version = 0; + needs_range_processing = false; }; // Constructor that sets the max bytes to cache -CacheMemory::CacheMemory(int64 max_bytes) : CacheBase(max_bytes) { - +CacheMemory::CacheMemory(long long int max_bytes) : CacheBase(max_bytes) { + // Set cache type name + cache_type = "CacheMemory"; + range_version = 0; + needs_range_processing = false; }; // Default destructor @@ -45,12 +51,79 @@ CacheMemory::~CacheMemory() { frames.clear(); frame_numbers.clear(); + ordered_frame_numbers.clear(); // remove critical section delete cacheCriticalSection; cacheCriticalSection = NULL; } + +// Calculate ranges of frames +void CacheMemory::CalculateRanges() { + // Only calculate when something has changed + if (needs_range_processing) { + + // Create a scoped lock, to protect the cache from multiple threads + const GenericScopedLock lock(*cacheCriticalSection); + + // Sort ordered frame #s, and calculate JSON ranges + std::sort(ordered_frame_numbers.begin(), ordered_frame_numbers.end()); + + // Clear existing JSON variable + ranges.clear(); + ranges = Json::Value(Json::arrayValue); + + // Increment range version + range_version++; + + vector::iterator itr_ordered; + long int starting_frame = *ordered_frame_numbers.begin(); + long int ending_frame = *ordered_frame_numbers.begin(); + + // Loop through all known frames (in sequential order) + for (itr_ordered = ordered_frame_numbers.begin(); itr_ordered != ordered_frame_numbers.end(); ++itr_ordered) { + long int frame_number = *itr_ordered; + if (frame_number - ending_frame > 1) { + // End of range detected + Json::Value range; + + // Add JSON object with start/end attributes + // Use strings, since long ints are supported in JSON + stringstream start_str; + start_str << starting_frame; + stringstream end_str; + end_str << ending_frame; + range["start"] = start_str.str(); + range["end"] = end_str.str(); + ranges.append(range); + + // Set new starting range + starting_frame = frame_number; + } + + // Set current frame as end of range, and keep looping + ending_frame = frame_number; + } + + // APPEND FINAL VALUE + Json::Value range; + + // Add JSON object with start/end attributes + // Use strings, since long ints are supported in JSON + stringstream start_str; + start_str << starting_frame; + stringstream end_str; + end_str << ending_frame; + range["start"] = start_str.str(); + range["end"] = end_str.str(); + ranges.append(range); + + // Reset needs_range_processing + needs_range_processing = false; + } +} + // Add a Frame to the cache void CacheMemory::Add(tr1::shared_ptr frame) { @@ -68,6 +141,8 @@ void CacheMemory::Add(tr1::shared_ptr frame) // Add frame to queue and map frames[frame_number] = frame; frame_numbers.push_front(frame_number); + ordered_frame_numbers.push_back(frame_number); + needs_range_processing = true; // Clean up old frames CleanUp(); @@ -113,12 +188,12 @@ tr1::shared_ptr CacheMemory::GetSmallestFrame() } // Gets the maximum bytes value -int64 CacheMemory::GetBytes() +long long int CacheMemory::GetBytes() { // Create a scoped lock, to protect the cache from multiple threads const GenericScopedLock lock(*cacheCriticalSection); - int64 total_bytes = 0; + long long int total_bytes = 0; // Loop through frames, and calculate total bytes deque::reverse_iterator itr; @@ -132,24 +207,43 @@ int64 CacheMemory::GetBytes() // Remove a specific frame void CacheMemory::Remove(long int frame_number) +{ + Remove(frame_number, frame_number); +} + +// Remove range of frames +void CacheMemory::Remove(long int start_frame_number, long int end_frame_number) { // Create a scoped lock, to protect the cache from multiple threads const GenericScopedLock lock(*cacheCriticalSection); // Loop through frame numbers - deque::iterator itr; - for(itr = frame_numbers.begin(); itr != frame_numbers.end(); ++itr) + deque::iterator itr = frame_numbers.begin(); + while (itr != frame_numbers.end()) { - if (*itr == frame_number) + if (*itr >= start_frame_number && *itr <= end_frame_number) { // erase frame number - frame_numbers.erase(itr); - break; - } + itr = frame_numbers.erase(itr++); + }else + ++itr; } - // Remove frame from map. If frame_number doesn't exist, frames.erase returns zero. - frames.erase(frame_number); + // Loop through ordered frame numbers + vector::iterator itr_ordered = ordered_frame_numbers.begin(); + while (itr_ordered != ordered_frame_numbers.end()) + { + if (*itr_ordered >= start_frame_number && *itr_ordered <= end_frame_number) + { + // erase frame number + frames.erase(*itr_ordered); + itr_ordered = ordered_frame_numbers.erase(itr_ordered++); + }else + ++itr_ordered; + } + + // Needs range processing (since cache has changed) + needs_range_processing = true; } // Move frame to front of queue (so it lasts longer) @@ -161,7 +255,7 @@ void CacheMemory::MoveToFront(long int frame_number) // Does frame exists in cache? /* FIXME if the frame number isn't present, the loop will do nothing, so why protect it? * Is it to save time by avoiding a loop? - * Do we really need to optmize the case where we've been given a nonexisting frame_number? */ + * Do we really need to optimize the case where we've been given a nonexisting frame_number? */ if (frames.count(frame_number)) { // Loop through frame numbers @@ -189,6 +283,7 @@ void CacheMemory::Clear() frames.clear(); frame_numbers.clear(); + ordered_frame_numbers.clear(); } // Count the frames in the queue @@ -220,3 +315,67 @@ void CacheMemory::CleanUp() } } } + + +// Generate JSON string of this object +string CacheMemory::Json() { + + // Return formatted string + return JsonValue().toStyledString(); +} + +// Generate Json::JsonValue for this object +Json::Value CacheMemory::JsonValue() { + + // Proccess range data (if anything has changed) + CalculateRanges(); + + // Create root json object + Json::Value root = CacheBase::JsonValue(); // get parent properties + root["type"] = cache_type; + root["ranges"] = ranges; + + Json::Value version; + stringstream range_version_str; + range_version_str << range_version; + root["version"] = range_version_str.str(); + + // return JsonValue + return root; +} + +// Load JSON string into this object +void CacheMemory::SetJson(string value) throw(InvalidJSON) { + + // Parse JSON string into JSON objects + Json::Value root; + Json::Reader reader; + bool success = reader.parse( value, root ); + if (!success) + // Raise exception + throw InvalidJSON("JSON could not be parsed (or is invalid)", ""); + + try + { + // Set all values that match + SetJsonValue(root); + } + catch (exception e) + { + // Error parsing JSON (or missing keys) + throw InvalidJSON("JSON is invalid (missing keys or invalid data types)", ""); + } +} + +// Load Json::JsonValue into this object +void CacheMemory::SetJsonValue(Json::Value root) throw(InvalidFile, ReaderClosed) { + + // Close timeline before we do anything (this also removes all open and closing clips) + Clear(); + + // Set parent data + CacheBase::SetJsonValue(root); + + if (!root["type"].isNull()) + cache_type = root["type"].asString(); +} \ No newline at end of file diff --git a/src/Frame.cpp b/src/Frame.cpp index 78a23a68..a1200b9b 100644 --- a/src/Frame.cpp +++ b/src/Frame.cpp @@ -670,11 +670,11 @@ int Frame::constrain(int color_value) } // Add (or replace) pixel data to the frame (based on a solid color) -void Frame::AddColor(int width, int height, string color) +void Frame::AddColor(int new_width, int new_height, string color) { // Create new image object, and fill with pixel data const GenericScopedLock lock(addingImageSection); - image = tr1::shared_ptr(new QImage(width, height, QImage::Format_RGBA8888)); + image = tr1::shared_ptr(new QImage(new_width, new_height, QImage::Format_RGBA8888)); // Fill with solid color image->fill(QColor(QString::fromStdString(color))); @@ -686,18 +686,18 @@ void Frame::AddColor(int width, int height, string color) } // Add (or replace) pixel data to the frame -void Frame::AddImage(int width, int height, int bytes_per_pixel, QImage::Format type, const unsigned char *pixels_) +void Frame::AddImage(int new_width, int new_height, int bytes_per_pixel, QImage::Format type, const unsigned char *pixels_) { // Create new buffer const GenericScopedLock lock(addingImageSection); - int buffer_size = width * height * bytes_per_pixel; + int buffer_size = new_width * new_height * bytes_per_pixel; qbuffer = new unsigned char[buffer_size](); // Copy buffer data memcpy((unsigned char*)qbuffer, pixels_, buffer_size); // Create new image object, and fill with pixel data - image = tr1::shared_ptr(new QImage(qbuffer, width, height, width * bytes_per_pixel, type, (QImageCleanupFunction) &openshot::Frame::cleanUpBuffer, (void*) qbuffer)); + image = tr1::shared_ptr(new QImage(qbuffer, new_width, new_height, new_width * bytes_per_pixel, type, (QImageCleanupFunction) &openshot::Frame::cleanUpBuffer, (void*) qbuffer)); // Always convert to RGBA8888 (if different) if (image->format() != QImage::Format_RGBA8888) diff --git a/src/Qt/VideoCacheThread.cpp b/src/Qt/VideoCacheThread.cpp index e0e41b8a..46d81ed8 100644 --- a/src/Qt/VideoCacheThread.cpp +++ b/src/Qt/VideoCacheThread.cpp @@ -56,13 +56,13 @@ namespace openshot current_display_frame = current_frame_number; } - // Seek the audio thread + // Seek the reader to a particular frame number void VideoCacheThread::Seek(int new_position) { position = new_position; } - // Play the audio + // Play the video void VideoCacheThread::Play() { // Start playing is_playing = true; diff --git a/src/Timeline.cpp b/src/Timeline.cpp index 7a955e33..4d2b86e8 100644 --- a/src/Timeline.cpp +++ b/src/Timeline.cpp @@ -57,7 +57,8 @@ Timeline::Timeline(int width, int height, Fraction fps, int sample_rate, int cha info.video_length = info.fps.ToFloat() * info.duration; // Init cache - final_cache.SetMaxBytesFromInfo(OPEN_MP_NUM_PROCESSORS * 2, info.width, info.height, info.sample_rate, info.channels); + final_cache = new CacheMemory(); + final_cache->SetMaxBytesFromInfo(OPEN_MP_NUM_PROCESSORS * 2, info.width, info.height, info.sample_rate, info.channels); } // Add an openshot::Clip to the timeline @@ -125,7 +126,7 @@ void Timeline::apply_mapper_to_clip(Clip* clip) void Timeline::ApplyMapperToClips() { // Clear all cached frames - final_cache.Clear(); + final_cache->Clear(); // Loop through all clips list::iterator clip_itr; @@ -582,7 +583,7 @@ void Timeline::Close() is_open = false; // Clear cache - final_cache.Clear(); + final_cache->Clear(); } // Open the reader (and start consuming resources) @@ -609,7 +610,7 @@ tr1::shared_ptr Timeline::GetFrame(long int requested_frame) throw(Reader requested_frame = 1; // Check cache - tr1::shared_ptr frame = final_cache.GetFrame(requested_frame); + tr1::shared_ptr frame = final_cache->GetFrame(requested_frame); if (frame) { // Debug output ZmqLogger::Instance()->AppendDebugMethod("Timeline::GetFrame (Cached frame found)", "requested_frame", requested_frame, "", -1, "", -1, "", -1, "", -1, "", -1); @@ -623,7 +624,7 @@ tr1::shared_ptr Timeline::GetFrame(long int requested_frame) throw(Reader const GenericScopedLock lock(getFrameCriticalSection); // Check cache again (due to locking) - frame = final_cache.GetFrame(requested_frame); + frame = final_cache->GetFrame(requested_frame); if (frame) { // Debug output ZmqLogger::Instance()->AppendDebugMethod("Timeline::GetFrame (Cached frame found on 2nd look)", "requested_frame", requested_frame, "", -1, "", -1, "", -1, "", -1, "", -1); @@ -683,6 +684,7 @@ tr1::shared_ptr Timeline::GetFrame(long int requested_frame) throw(Reader // Create blank frame (which will become the requested frame) tr1::shared_ptr new_frame(tr1::shared_ptr(new Frame(frame_number, info.width, info.height, "#000000", samples_in_frame, info.channels))); + new_frame->AddAudioSilence(samples_in_frame); new_frame->SampleRate(info.sample_rate); new_frame->ChannelsLayout(info.channel_layout); @@ -748,7 +750,7 @@ tr1::shared_ptr Timeline::GetFrame(long int requested_frame) throw(Reader ZmqLogger::Instance()->AppendDebugMethod("Timeline::GetFrame (Add frame to cache)", "frame_number", frame_number, "info.width", info.width, "info.height", info.height, "", -1, "", -1, "", -1); // Add final frame to cache - final_cache.Add(new_frame); + final_cache->Add(new_frame); } // end frame loop } // end parallel @@ -757,7 +759,7 @@ tr1::shared_ptr Timeline::GetFrame(long int requested_frame) throw(Reader ZmqLogger::Instance()->AppendDebugMethod("Timeline::GetFrame (end parallel region)", "requested_frame", requested_frame, "omp_get_thread_num()", omp_get_thread_num(), "", -1, "", -1, "", -1, "", -1); // Return frame (or blank frame) - return final_cache.GetFrame(requested_frame); + return final_cache->GetFrame(requested_frame); } } @@ -810,6 +812,12 @@ vector Timeline::find_intersecting_clips(long int requested_frame, int nu return matching_clips; } +// Get the cache object used by this reader +void Timeline::SetCache(CacheBase* new_cache) { + // Set new cache + final_cache = new_cache; +} + // Generate JSON string of this object string Timeline::Json() { @@ -943,9 +951,6 @@ void Timeline::SetJsonValue(Json::Value root) throw(InvalidFile, ReaderClosed) { // Apply a special formatted JSON object, which represents a change to the timeline (insert, update, delete) void Timeline::ApplyJsonDiff(string value) throw(InvalidJSON, InvalidJSONKey) { - // Clear internal cache (since things are about to change) - final_cache.Clear(); - // Parse JSON string into JSON objects Json::Value root; Json::Reader reader; @@ -982,9 +987,6 @@ void Timeline::ApplyJsonDiff(string value) throw(InvalidJSON, InvalidJSONKey) { // Error parsing JSON (or missing keys) throw InvalidJSON("JSON is invalid (missing keys or invalid data types)", ""); } - - // Adjust cache (in case something changed) - final_cache.SetMaxBytesFromInfo(OPEN_MP_NUM_PROCESSORS * 2, info.width, info.height, info.sample_rate, info.channels); } // Apply JSON diff to clips @@ -1053,6 +1055,11 @@ void Timeline::apply_json_to_clips(Json::Value change) throw(InvalidJSONKey) { } } + // Calculate start and end frames that this impacts, and remove those frames from the cache + long int new_starting_frame = change["value"]["position"].asDouble() * info.fps.ToDouble(); + long int new_ending_frame = (change["value"]["position"].asDouble() + change["value"]["end"].asDouble() - change["value"]["start"].asDouble()) * info.fps.ToDouble(); + final_cache->Remove(new_starting_frame - 1, new_ending_frame + 1); + // Determine type of change operation if (change_type == "insert") { @@ -1064,14 +1071,30 @@ void Timeline::apply_json_to_clips(Json::Value change) throw(InvalidJSONKey) { } else if (change_type == "update") { // Update existing clip - if (existing_clip) - existing_clip->SetJsonValue(change["value"]); // Update clip properties from JSON + if (existing_clip) { + + // Calculate start and end frames that this impacts, and remove those frames from the cache + long int old_starting_frame = existing_clip->Position() * info.fps.ToDouble(); + long int old_ending_frame = (existing_clip->Position() + existing_clip->End() - existing_clip->Start()) * info.fps.ToDouble(); + final_cache->Remove(old_starting_frame - 1, old_ending_frame + 1); + + // Update clip properties from JSON + existing_clip->SetJsonValue(change["value"]); + } } else if (change_type == "delete") { // Remove existing clip - if (existing_clip) - RemoveClip(existing_clip); // Remove clip from timeline + if (existing_clip) { + + // Calculate start and end frames that this impacts, and remove those frames from the cache + long int old_starting_frame = existing_clip->Position() * info.fps.ToDouble(); + long int old_ending_frame = (existing_clip->Position() + existing_clip->End() - existing_clip->Start()) * info.fps.ToDouble(); + final_cache->Remove(old_starting_frame - 1, old_ending_frame + 1); + + // Remove clip from timeline + RemoveClip(existing_clip); + } } @@ -1124,6 +1147,11 @@ void Timeline::apply_json_to_effects(Json::Value change, EffectBase* existing_ef // Get key and type of change string change_type = change["type"].asString(); + // Calculate start and end frames that this impacts, and remove those frames from the cache + long int new_starting_frame = change["value"]["position"].asDouble() * info.fps.ToDouble(); + long int new_ending_frame = (change["value"]["position"].asDouble() + change["value"]["end"].asDouble() - change["value"]["start"].asDouble()) * info.fps.ToDouble(); + final_cache->Remove(new_starting_frame - 1, new_ending_frame + 1); + // Determine type of change operation if (change_type == "insert") { @@ -1145,14 +1173,30 @@ void Timeline::apply_json_to_effects(Json::Value change, EffectBase* existing_ef } else if (change_type == "update") { // Update existing effect - if (existing_effect) - existing_effect->SetJsonValue(change["value"]); // Update effect properties from JSON + if (existing_effect) { + + // Calculate start and end frames that this impacts, and remove those frames from the cache + long int old_starting_frame = existing_effect->Position() * info.fps.ToDouble(); + long int old_ending_frame = (existing_effect->Position() + existing_effect->End() - existing_effect->Start()) * info.fps.ToDouble(); + final_cache->Remove(old_starting_frame - 1, old_ending_frame + 1); + + // Update effect properties from JSON + existing_effect->SetJsonValue(change["value"]); + } } else if (change_type == "delete") { // Remove existing effect - if (existing_effect) - RemoveEffect(existing_effect); // Remove effect from timeline + if (existing_effect) { + + // Calculate start and end frames that this impacts, and remove those frames from the cache + long int old_starting_frame = existing_effect->Position() * info.fps.ToDouble(); + long int old_ending_frame = (existing_effect->Position() + existing_effect->End() - existing_effect->Start()) * info.fps.ToDouble(); + final_cache->Remove(old_starting_frame - 1, old_ending_frame + 1); + + // Remove effect from timeline + RemoveEffect(existing_effect); + } } } @@ -1167,6 +1211,9 @@ void Timeline::apply_json_to_timeline(Json::Value change) throw(InvalidJSONKey) if (change["key"].size() >= 2) sub_key = change["key"][(uint)1].asString(); + // Clear entire cache + final_cache->Clear(); + // Determine type of change operation if (change_type == "insert" || change_type == "update") { diff --git a/src/bindings/python/openshot.i b/src/bindings/python/openshot.i index bdd5b5cd..de0bfd3d 100644 --- a/src/bindings/python/openshot.i +++ b/src/bindings/python/openshot.i @@ -56,6 +56,7 @@ #include "../../../include/ReaderBase.h" #include "../../../include/WriterBase.h" #include "../../../include/CacheBase.h" +#include "../../../include/CacheDisk.h" #include "../../../include/CacheMemory.h" #include "../../../include/ChannelLayouts.h" #include "../../../include/ChunkReader.h" @@ -117,6 +118,7 @@ %include "../../../include/ReaderBase.h" %include "../../../include/WriterBase.h" %include "../../../include/CacheBase.h" +%include "../../../include/CacheDisk.h" %include "../../../include/CacheMemory.h" %include "../../../include/ChannelLayouts.h" %include "../../../include/ChunkReader.h" diff --git a/src/bindings/ruby/openshot.i b/src/bindings/ruby/openshot.i index cc2c8961..f704287c 100644 --- a/src/bindings/ruby/openshot.i +++ b/src/bindings/ruby/openshot.i @@ -62,6 +62,7 @@ namespace tr1 #include "../../../include/ReaderBase.h" #include "../../../include/WriterBase.h" #include "../../../include/CacheBase.h" +#include "../../../include/CacheDisk.h" #include "../../../include/CacheMemory.h" #include "../../../include/ChannelLayouts.h" #include "../../../include/ChunkReader.h" @@ -112,6 +113,7 @@ namespace tr1 %include "../../../include/ReaderBase.h" %include "../../../include/WriterBase.h" %include "../../../include/CacheBase.h" +%include "../../../include/CacheDisk.h" %include "../../../include/CacheMemory.h" %include "../../../include/ChannelLayouts.h" %include "../../../include/ChunkReader.h" diff --git a/tests/Cache_Tests.cpp b/tests/Cache_Tests.cpp index 4c8ab9e6..02c9fca1 100644 --- a/tests/Cache_Tests.cpp +++ b/tests/Cache_Tests.cpp @@ -27,6 +27,7 @@ #include "UnitTest++.h" #include "../include/OpenShot.h" +#include "../include/Json.h" using namespace std; using namespace openshot; @@ -270,3 +271,116 @@ TEST(Cache_Set_Max_Bytes) c.SetMaxBytes(4 * 1024); CHECK_EQUAL(4 * 1024, c.GetMaxBytes()); } + +TEST(CacheDisk_Set_Max_Bytes) +{ + // Create cache object (using platform /temp/ directory) + CacheDisk c("", "PPM", 1.0, 0.25); + + // Add frames to disk cache + for (int i = 0; i < 20; i++) + { + // Add blank frame to the cache + tr1::shared_ptr f(new Frame()); + f->number = i; + // Add some picture data + f->AddColor(1280, 720, "Blue"); + f->ResizeAudio(2, 500, 44100, LAYOUT_STEREO); + f->AddAudioSilence(500); + c.Add(f); + } + + CHECK_EQUAL(0, c.GetMaxBytes()); // Cache defaults max frames to -1, unlimited frames + + // Set max frames + c.SetMaxBytes(8 * 1024); + CHECK_EQUAL(8 * 1024, c.GetMaxBytes()); + + // Set max frames + c.SetMaxBytes(4 * 1024); + CHECK_EQUAL(4 * 1024, c.GetMaxBytes()); + + // Read frames from disk cache + tr1::shared_ptr f = c.GetFrame(5); + CHECK_EQUAL(320, f->GetWidth()); + CHECK_EQUAL(180, f->GetHeight()); + CHECK_EQUAL(2, f->GetAudioChannelsCount()); + CHECK_EQUAL(500, f->GetAudioSamplesCount()); + CHECK_EQUAL(LAYOUT_STEREO, f->ChannelsLayout()); + CHECK_EQUAL(44100, f->SampleRate()); + +} + +TEST(CacheDisk_JSON) +{ + // Create cache object (using platform /temp/ directory) + CacheDisk c("", "PPM", 1.0, 0.25); + + // Add some frames (out of order) + tr1::shared_ptr f3(new Frame(3, 1280, 720, "Blue", 500, 2)); + c.Add(f3); + CHECK_EQUAL(1, c.JsonValue()["ranges"].size()); + CHECK_EQUAL("1", c.JsonValue()["version"].asString()); + + // Add some frames (out of order) + tr1::shared_ptr f1(new Frame(1, 1280, 720, "Blue", 500, 2)); + c.Add(f1); + CHECK_EQUAL(2, c.JsonValue()["ranges"].size()); + CHECK_EQUAL("2", c.JsonValue()["version"].asString()); + + // Add some frames (out of order) + tr1::shared_ptr f2(new Frame(2, 1280, 720, "Blue", 500, 2)); + c.Add(f2); + CHECK_EQUAL(1, c.JsonValue()["ranges"].size()); + CHECK_EQUAL("3", c.JsonValue()["version"].asString()); + + // Add some frames (out of order) + tr1::shared_ptr f5(new Frame(5, 1280, 720, "Blue", 500, 2)); + c.Add(f5); + CHECK_EQUAL(2, c.JsonValue()["ranges"].size()); + CHECK_EQUAL("4", c.JsonValue()["version"].asString()); + + // Add some frames (out of order) + tr1::shared_ptr f4(new Frame(4, 1280, 720, "Blue", 500, 2)); + c.Add(f4); + CHECK_EQUAL(1, c.JsonValue()["ranges"].size()); + CHECK_EQUAL("5", c.JsonValue()["version"].asString()); + +} + +TEST(CacheMemory_JSON) +{ + // Create memory cache object + CacheMemory c; + + // Add some frames (out of order) + tr1::shared_ptr f3(new Frame(3, 1280, 720, "Blue", 500, 2)); + c.Add(f3); + CHECK_EQUAL(1, c.JsonValue()["ranges"].size()); + CHECK_EQUAL("1", c.JsonValue()["version"].asString()); + + // Add some frames (out of order) + tr1::shared_ptr f1(new Frame(1, 1280, 720, "Blue", 500, 2)); + c.Add(f1); + CHECK_EQUAL(2, c.JsonValue()["ranges"].size()); + CHECK_EQUAL("2", c.JsonValue()["version"].asString()); + + // Add some frames (out of order) + tr1::shared_ptr f2(new Frame(2, 1280, 720, "Blue", 500, 2)); + c.Add(f2); + CHECK_EQUAL(1, c.JsonValue()["ranges"].size()); + CHECK_EQUAL("3", c.JsonValue()["version"].asString()); + + // Add some frames (out of order) + tr1::shared_ptr f5(new Frame(5, 1280, 720, "Blue", 500, 2)); + c.Add(f5); + CHECK_EQUAL(2, c.JsonValue()["ranges"].size()); + CHECK_EQUAL("4", c.JsonValue()["version"].asString()); + + // Add some frames (out of order) + tr1::shared_ptr f4(new Frame(4, 1280, 720, "Blue", 500, 2)); + c.Add(f4); + CHECK_EQUAL(1, c.JsonValue()["ranges"].size()); + CHECK_EQUAL("5", c.JsonValue()["version"].asString()); + +} \ No newline at end of file diff --git a/tests/ReaderBase_Tests.cpp b/tests/ReaderBase_Tests.cpp index ac8f8c76..6c4b31d5 100644 --- a/tests/ReaderBase_Tests.cpp +++ b/tests/ReaderBase_Tests.cpp @@ -40,7 +40,7 @@ TEST(ReaderBase_Derived_Class) { public: TestReader() { }; - CacheMemory* GetCache() { return NULL; }; + CacheBase* GetCache() { return NULL; }; tr1::shared_ptr GetFrame(long int number) { tr1::shared_ptr f(new Frame()); return f; } void Close() { }; void Open() { };