diff --git a/include/Clip.h b/include/Clip.h index 0fbed159..0c98526a 100644 --- a/include/Clip.h +++ b/include/Clip.h @@ -169,6 +169,9 @@ namespace openshot { /// Return the list of effects on the timeline std::list Effects() { return effects; }; + /// Look up an effect by ID + openshot::EffectBase* GetEffect(const std::string& id); + /// @brief Get an openshot::Frame object for a specific frame number of this timeline. /// /// @returns The requested frame (containing the image) @@ -253,8 +256,6 @@ namespace openshot { openshot::Keyframe has_audio; ///< An optional override to determine if this clip has audio (-1=undefined, 0=no, 1=yes) openshot::Keyframe has_video; ///< An optional override to determine if this clip has video (-1=undefined, 0=no, 1=yes) }; +} // namespace - -} - -#endif +#endif // OPENSHOT_CLIP_H diff --git a/include/ReaderBase.h b/include/ReaderBase.h index ab765753..c997b76d 100644 --- a/include/ReaderBase.h +++ b/include/ReaderBase.h @@ -111,10 +111,16 @@ namespace openshot openshot::ReaderInfo info; /// Parent clip object of this reader (which can be unparented and NULL) - openshot::ClipBase* GetClip(); + inline openshot::ClipBase* GetParentClip() { return parent; }; + + /// Deprecated alias for GetParentClip() + inline openshot::ClipBase* GetClip() { return parent; }; /// Set parent clip object of this reader - void SetClip(openshot::ClipBase* clip); + inline void SetParentClip(openshot::ClipBase* clip) { parent = clip; }; + + /// Deprecated alias for SetParentClip() + inline void SetClip(openshot::ClipBase* clip) { parent = clip; }; /// Close the reader (and any resources it was consuming) virtual void Close() = 0; diff --git a/include/Timeline.h b/include/Timeline.h index 932b04ac..5c275abb 100644 --- a/include/Timeline.h +++ b/include/Timeline.h @@ -61,7 +61,7 @@ namespace openshot { /// from lowest layer to top layer (since that is the sequence they need to be combined), and then /// by position (left to right). struct CompareClips{ - bool operator()( Clip* lhs, Clip* rhs){ + bool operator()( openshot::Clip* lhs, openshot::Clip* rhs){ if( lhs->Layer() < rhs->Layer() ) return true; if( lhs->Layer() == rhs->Layer() && lhs->Position() <= rhs->Position() ) return true; return false; @@ -71,13 +71,28 @@ namespace openshot { /// from lowest layer to top layer (since that is sequence clips are combined), and then by /// position, and then by effect order. struct CompareEffects{ - bool operator()( EffectBase* lhs, EffectBase* rhs){ + bool operator()( openshot::EffectBase* lhs, openshot::EffectBase* rhs){ if( lhs->Layer() < rhs->Layer() ) return true; if( lhs->Layer() == rhs->Layer() && lhs->Position() < rhs->Position() ) return true; if( lhs->Layer() == rhs->Layer() && lhs->Position() == rhs->Position() && lhs->Order() > rhs->Order() ) return true; return false; }}; + /// Comparison method for finding the far end of the timeline, by locating + /// the Clip with the highest end-frame number using std::max_element + struct CompareClipEndFrames { + bool operator()(const openshot::Clip* lhs, const openshot::Clip* rhs) { + return (lhs->Position() + lhs->Duration()) + <= (rhs->Position() + rhs->Duration()); + }}; + + /// Like CompareClipEndFrames, but for effects + struct CompareEffectEndFrames { + bool operator()(const openshot::EffectBase* lhs, const openshot::EffectBase* rhs) { + return (lhs->Position() + lhs->Duration()) + <= (rhs->Position() + rhs->Duration()); + }}; + /** * @brief This class represents a timeline * @@ -146,33 +161,33 @@ namespace openshot { * t.Close(); * @endcode */ - class Timeline : public ReaderBase { + class Timeline : public openshot::ReaderBase { private: bool is_open; /// clips; /// closing_clips; /// open_clips; /// effects; /// allocated_frame_mappers; ///< all the frame mappers we allocated and must free + std::list clips; /// closing_clips; /// open_clips; /// effects; /// allocated_frame_mappers; ///< all the frame mappers we allocated and must free bool managed_cache; ///< Does this timeline instance manage the cache object std::string path; ///< Optional path of loaded UTF-8 OpenShot JSON project file /// Process a new layer of video or audio - void add_layer(std::shared_ptr new_frame, Clip* source_clip, int64_t clip_frame_number, int64_t timeline_frame_number, bool is_top_clip, float max_volume); + void add_layer(std::shared_ptr new_frame, openshot::Clip* source_clip, int64_t clip_frame_number, int64_t timeline_frame_number, bool is_top_clip, float max_volume); /// Apply a FrameMapper to a clip which matches the settings of this timeline - void apply_mapper_to_clip(Clip* clip); + void apply_mapper_to_clip(openshot::Clip* clip); /// Apply JSON Diffs to various objects contained in this timeline void apply_json_to_clips(Json::Value change); /// find_intersecting_clips(int64_t requested_frame, int number_of_frames, bool include); + std::vector find_intersecting_clips(int64_t requested_frame, int number_of_frames, bool include); /// Get or generate a blank frame - std::shared_ptr GetOrCreateFrame(Clip* clip, int64_t number); + std::shared_ptr GetOrCreateFrame(openshot::Clip* clip, int64_t number); /// Apply effects to the source frame (if any) - std::shared_ptr apply_effects(std::shared_ptr frame, int64_t timeline_frame_number, int layer); + std::shared_ptr apply_effects(std::shared_ptr frame, int64_t timeline_frame_number, int layer); /// Compare 2 floating point numbers for equality bool isEqual(double a, double b); @@ -198,7 +213,7 @@ namespace openshot { void sort_effects(); /// Update the list of 'opened' clips - void update_open_clips(Clip *clip, bool does_clip_intersect); + void update_open_clips(openshot::Clip *clip, bool does_clip_intersect); public: @@ -209,7 +224,7 @@ namespace openshot { /// @param sample_rate The sample rate of the timeline's audio /// @param channels The number of audio channels of the timeline /// @param channel_layout The channel layout (i.e. mono, stereo, 3 point surround, etc...) - Timeline(int width, int height, Fraction fps, int sample_rate, int channels, ChannelLayout channel_layout); + Timeline(int width, int height, openshot::Fraction fps, int sample_rate, int channels, openshot::ChannelLayout channel_layout); /// @brief Constructor for the timeline (which loads a JSON structure from a file path, and initializes a timeline) /// @param projectPath The path of the UTF-8 *.osp project file (JSON contents). Contents will be loaded automatically. @@ -220,11 +235,11 @@ namespace openshot { /// @brief Add an openshot::Clip to the timeline /// @param clip Add an openshot::Clip to the timeline. A clip can contain any type of Reader. - void AddClip(Clip* clip); + void AddClip(openshot::Clip* clip); /// @brief Add an effect to the timeline /// @param effect Add an effect to the timeline. An effect can modify the audio or video of an openshot::Frame. - void AddEffect(EffectBase* effect); + void AddEffect(openshot::EffectBase* effect); /// Apply the timeline's framerate and samplerate to all clips void ApplyMapperToClips(); @@ -239,34 +254,48 @@ namespace openshot { void ClearAllCache(); /// Return a list of clips on the timeline - std::list Clips() { return clips; }; + std::list Clips() { return clips; }; + + /// Look up a single clip by ID + openshot::ClipBase* GetClip(const std::string& id); + + /// Look up a clip effect by ID + openshot::EffectBase* GetClipEffect(const std::string& id); + + /// Look up a timeline effect by ID + openshot::EffectBase* GetEffect(const std::string& id); + + /// Look up the end time of the latest timeline element + double GetMaxTime(); + /// Look up the end frame number of the latest element on the timeline + int64_t GetMaxFrame(); /// Close the timeline reader (and any resources it was consuming) void Close() override; /// Return the list of effects on the timeline - std::list Effects() { return effects; }; + std::list Effects() { return effects; }; /// Get the cache object used by this reader - CacheBase* GetCache() override { return final_cache; }; + openshot::CacheBase* GetCache() override { return final_cache; }; /// Set the cache object used by this reader. You must now manage the lifecycle /// of this cache object though (Timeline will not delete it for you). - void SetCache(CacheBase* new_cache); + void SetCache(openshot::CacheBase* new_cache); /// Get an openshot::Frame object for a specific frame number of this timeline. /// /// @returns The requested frame (containing the image) /// @param requested_frame The frame number that is requested. - std::shared_ptr GetFrame(int64_t requested_frame) override; + std::shared_ptr GetFrame(int64_t requested_frame) override; // Curves for the viewport - Keyframe viewport_scale; ///info.duration); - reader->SetClip(this); + reader->SetParentClip(this); } } @@ -210,7 +210,7 @@ Clip::Clip(std::string path) : resampler(NULL), reader(NULL), allocated_reader(N // Update duration and set parent if (reader) { End(reader->info.duration); - reader->SetClip(this); + reader->SetParentClip(this); allocated_reader = reader; init_reader_rotation(); } @@ -239,7 +239,7 @@ void Clip::Reader(ReaderBase* new_reader) reader = new_reader; // set parent - reader->SetClip(this); + reader->SetParentClip(this); // Init rotation (if any) init_reader_rotation(); @@ -368,6 +368,18 @@ std::shared_ptr Clip::GetFrame(int64_t requested_frame) throw ReaderClosed("No Reader has been initialized for this Clip. Call Reader(*reader) before calling this method."); } +// Look up an effect by ID +openshot::EffectBase* Clip::GetEffect(const std::string& id) +{ + // Find the matching effect (if any) + for (const auto& effect : effects) { + if (effect->Id() == id) { + return effect; + } + } + return nullptr; +} + // Get file extension std::string Clip::get_file_extension(std::string path) { @@ -993,7 +1005,7 @@ void Clip::SetJsonValue(const Json::Value root) { // mark as managed reader and set parent if (reader) { - reader->SetClip(this); + reader->SetParentClip(this); allocated_reader = reader; } diff --git a/src/FFmpegReader.cpp b/src/FFmpegReader.cpp index dfd8eb8d..a8380513 100644 --- a/src/FFmpegReader.cpp +++ b/src/FFmpegReader.cpp @@ -1296,7 +1296,7 @@ void FFmpegReader::ProcessVideoPacket(int64_t requested_frame) { if (max_height <= 0) max_height = info.height; - Clip *parent = (Clip *) GetClip(); + Clip *parent = (Clip *) GetParentClip(); if (parent) { if (parent->scale == SCALE_FIT || parent->scale == SCALE_STRETCH) { // Best fit or Stretch scaling (based on max timeline size * scaling keyframes) diff --git a/src/QtImageReader.cpp b/src/QtImageReader.cpp index cf64ef93..59060ccd 100644 --- a/src/QtImageReader.cpp +++ b/src/QtImageReader.cpp @@ -187,7 +187,7 @@ std::shared_ptr QtImageReader::GetFrame(int64_t requested_frame) if (max_height <= 0) max_height = info.height; - Clip* parent = (Clip*) GetClip(); + Clip* parent = (Clip*) GetParentClip(); if (parent) { if (parent->scale == SCALE_FIT || parent->scale == SCALE_STRETCH) { // Best fit or Stretch scaling (based on max timeline size * scaling keyframes) diff --git a/src/ReaderBase.cpp b/src/ReaderBase.cpp index 474dc624..6799d95e 100644 --- a/src/ReaderBase.cpp +++ b/src/ReaderBase.cpp @@ -249,13 +249,3 @@ void ReaderBase::SetJsonValue(const Json::Value root) { } } } - -/// Parent clip object of this reader (which can be unparented and NULL) -openshot::ClipBase* ReaderBase::GetClip() { - return parent; -} - -/// Set parent clip object of this reader -void ReaderBase::SetClip(openshot::ClipBase* clip) { - parent = clip; -} diff --git a/src/Timeline.cpp b/src/Timeline.cpp index 124058ac..b2f46519 100644 --- a/src/Timeline.cpp +++ b/src/Timeline.cpp @@ -263,6 +263,67 @@ void Timeline::RemoveClip(Clip* clip) clips.remove(clip); } +// Look up a clip +openshot::ClipBase* Timeline::GetClip(const std::string& id) +{ + // Find the matching clip (if any) + for (const auto& clip : clips) { + if (clip->Id() == id) { + return clip; + } + } + return nullptr; +} + +// Look up a timeline effect +openshot::EffectBase* Timeline::GetEffect(const std::string& id) +{ + // Find the matching effect (if any) + for (const auto& effect : effects) { + if (effect->Id() == id) { + return effect; + } + } + return nullptr; +} + +openshot::EffectBase* Timeline::GetClipEffect(const std::string& id) +{ + // Search all clips for matching effect ID + for (const auto& clip : clips) { + const auto e = clip->GetEffect(id); + if (e != nullptr) { + return e; + } + } + return nullptr; +} + +// Compute the end time of the latest timeline element +double Timeline::GetMaxTime() { + double last_clip = 0.0; + double last_effect = 0.0; + + if (!clips.empty()) { + const auto max_clip = std::max_element( + clips.begin(), clips.end(), CompareClipEndFrames()); + last_clip = (*max_clip)->Position() + (*max_clip)->Duration(); + } + if (!effects.empty()) { + const auto max_effect = std::max_element( + effects.begin(), effects.end(), CompareEffectEndFrames()); + last_effect = (*max_effect)->Position() + (*max_effect)->Duration(); + } + return std::max(last_clip, last_effect); +} + +// Compute the highest frame# based on the latest time and FPS +int64_t Timeline::GetMaxFrame() { + double fps = info.fps.ToDouble(); + auto max_time = GetMaxTime(); + return std::round(max_time * fps) + 1; +} + // Apply a FrameMapper to a clip which matches the settings of this timeline void Timeline::apply_mapper_to_clip(Clip* clip) { diff --git a/tests/Timeline_Tests.cpp b/tests/Timeline_Tests.cpp index 946693a1..47683b79 100644 --- a/tests/Timeline_Tests.cpp +++ b/tests/Timeline_Tests.cpp @@ -36,7 +36,10 @@ using namespace std; using namespace openshot; -TEST(Timeline_Constructor) +SUITE(Timeline) +{ + +TEST(Constructor) { // Create a default fraction (should be 1/1) Fraction fps(30000,1000); @@ -54,7 +57,7 @@ TEST(Timeline_Constructor) CHECK_EQUAL(240, t2.info.height); } -TEST(Timeline_Width_and_Height_Functions) +TEST(Width_and_Height_Functions) { // Create a default fraction (should be 1/1) Fraction fps(30000,1000); @@ -79,7 +82,7 @@ TEST(Timeline_Width_and_Height_Functions) CHECK_EQUAL(400, t1.info.height); } -TEST(Timeline_Framerate) +TEST(Framerate) { // Create a default fraction (should be 1/1) Fraction fps(24,1); @@ -89,7 +92,7 @@ TEST(Timeline_Framerate) CHECK_CLOSE(24.0f, t1.info.fps.ToFloat(), 0.00001); } -TEST(Timeline_Check_Two_Track_Video) +TEST(Check_Two_Track_Video) { // Create a reader stringstream path; @@ -186,7 +189,7 @@ TEST(Timeline_Check_Two_Track_Video) t.Close(); } -TEST(Timeline_Clip_Order) +TEST(Clip_Order) { // Create a timeline Timeline t(640, 480, Fraction(30, 1), 44100, 2, LAYOUT_STEREO); @@ -281,7 +284,7 @@ TEST(Timeline_Clip_Order) } -TEST(Timeline_Effect_Order) +TEST(Effect_Order) { // Create a timeline Timeline t(640, 480, Fraction(30, 1), 44100, 2, LAYOUT_STEREO); @@ -413,7 +416,127 @@ TEST(Timeline_Effect_Order) t.Close(); } -TEST(Timeline_Effect_Blur) +TEST(GetClip_by_id) +{ + Timeline t(640, 480, Fraction(30, 1), 44100, 2, LAYOUT_STEREO); + + stringstream path1; + path1 << TEST_MEDIA_PATH << "interlaced.png"; + auto media_path1 = path1.str(); + + stringstream path2; + path2 << TEST_MEDIA_PATH << "front.png"; + auto media_path2 = path2.str(); + + Clip clip1(media_path1); + std::string clip1_id("CLIP00001"); + clip1.Id(clip1_id); + clip1.Layer(1); + + Clip clip2(media_path2); + std::string clip2_id("CLIP00002"); + clip2.Id(clip2_id); + clip2.Layer(2); + + t.AddClip(&clip1); + t.AddClip(&clip2); + + auto matched = t.GetClip(clip1_id); + CHECK_EQUAL(clip1_id, matched->Id()); + CHECK_EQUAL(1, matched->Layer()); + + auto matched2 = t.GetClip(clip2_id); + CHECK_EQUAL(clip2_id, matched2->Id()); + CHECK_EQUAL(false, matched2->Layer() < 2); + + auto matched3 = t.GetClip("BAD_ID"); + CHECK_EQUAL(true, matched3 == nullptr); +} + +TEST(GetClipEffect_by_id) +{ + Timeline t(640, 480, Fraction(30, 1), 44100, 2, LAYOUT_STEREO); + + stringstream path1; + path1 << TEST_MEDIA_PATH << "interlaced.png"; + auto media_path1 = path1.str(); + + // Create a clip, nothing special + Clip clip1(media_path1); + std::string clip1_id("CLIP00001"); + clip1.Id(clip1_id); + clip1.Layer(1); + + // Add a blur effect + Keyframe horizontal_radius(5.0); + Keyframe vertical_radius(5.0); + Keyframe sigma(3.0); + Keyframe iterations(3.0); + Blur blur1(horizontal_radius, vertical_radius, sigma, iterations); + std::string blur1_id("EFFECT00011"); + blur1.Id(blur1_id); + clip1.AddEffect(&blur1); + + // A second clip, different layer + Clip clip2(media_path1); + std::string clip2_id("CLIP00002"); + clip2.Id(clip2_id); + clip2.Layer(2); + + // Some effects for clip2 + Negate neg2; + std::string neg2_id("EFFECT00021"); + neg2.Id(neg2_id); + neg2.Layer(2); + clip2.AddEffect(&neg2); + Blur blur2(horizontal_radius, vertical_radius, sigma, iterations); + std::string blur2_id("EFFECT00022"); + blur2.Id(blur2_id); + blur2.Layer(2); + clip2.AddEffect(&blur2); + + t.AddClip(&clip1); + + // Check that we can look up clip1's effect + auto match1 = t.GetClipEffect("EFFECT00011"); + CHECK_EQUAL(blur1_id, match1->Id()); + + // clip2 hasn't been added yet, shouldn't be found + match1 = t.GetClipEffect(blur2_id); + CHECK_EQUAL(true, match1 == nullptr); + + t.AddClip(&clip2); + + // Check that blur2 can now be found via clip2 + match1 = t.GetClipEffect(blur2_id); + CHECK_EQUAL(blur2_id, match1->Id()); + CHECK_EQUAL(2, match1->Layer()); +} + +TEST(GetEffect_by_id) +{ + Timeline t(640, 480, Fraction(30, 1), 44100, 2, LAYOUT_STEREO); + + // Create a timeline effect + Keyframe horizontal_radius(5.0); + Keyframe vertical_radius(5.0); + Keyframe sigma(3.0); + Keyframe iterations(3.0); + Blur blur1(horizontal_radius, vertical_radius, sigma, iterations); + std::string blur1_id("EFFECT00011"); + blur1.Id(blur1_id); + blur1.Layer(1); + t.AddEffect(&blur1); + + auto match1 = t.GetEffect(blur1_id); + CHECK_EQUAL(blur1_id, match1->Id()); + CHECK_EQUAL(1, match1->Layer()); + + match1 = t.GetEffect("NOSUCHNAME"); + CHECK_EQUAL(true, match1 == nullptr); +} + +TEST(Effect_Blur) { // Create a timeline Timeline t(640, 480, Fraction(30, 1), 44100, 2, LAYOUT_STEREO); @@ -443,3 +566,39 @@ TEST(Timeline_Effect_Blur) // Close reader t.Close(); } + +TEST(GetMaxFrame_GetMaxTime) +{ + // Create a timeline + Timeline t(640, 480, Fraction(30, 1), 44100, 2, LAYOUT_STEREO); + + stringstream path1; + path1 << TEST_MEDIA_PATH << "interlaced.png"; + Clip clip1(path1.str()); + clip1.Layer(1); + clip1.Position(50); + clip1.End(45); + t.AddClip(&clip1); + + CHECK_CLOSE(95.0, t.GetMaxTime(), 0.001); + CHECK_EQUAL(95 * 30 + 1, t.GetMaxFrame()); + + Clip clip2(path1.str()); + clip2.Layer(2); + clip2.Position(0); + clip2.End(55); + t.AddClip(&clip2); + + CHECK_EQUAL(95 * 30 + 1, t.GetMaxFrame()); + CHECK_CLOSE(95.0, t.GetMaxTime(), 0.001); + + clip2.Position(100); + clip1.Position(80); + CHECK_EQUAL(155 * 30 + 1, t.GetMaxFrame()); + CHECK_CLOSE(155.0, t.GetMaxTime(), 0.001); + t.RemoveClip(&clip2); + CHECK_EQUAL(125 * 30 + 1, t.GetMaxFrame()); + CHECK_CLOSE(125.0, t.GetMaxTime(), 0.001); +} + +} // SUITE