From 5cf8adf238ef08e1aa2355ba44db238125fa04ef Mon Sep 17 00:00:00 2001 From: Mike Dyer Date: Tue, 17 Jun 2025 09:40:19 +1000 Subject: [PATCH 01/27] [RIV-97269] Extend OpenTimeline effects This commit adds the following effects: * VideoCrop * VideoScale * VideoRotate * VideoPosition * AudioVolume * AudioFade It also adds a canvas size parameter to the timeline, and updates the timeline schema version to 2 C++ tests for the new effects are also added --- src/opentimelineio/CMakeLists.txt | 8 +- src/opentimelineio/CORE_VERSION_MAP.cpp | 8 +- src/opentimelineio/deserialization.cpp | 8 + src/opentimelineio/serializableObject.h | 5 + src/opentimelineio/serialization.cpp | 9 ++ src/opentimelineio/timeline.cpp | 10 +- src/opentimelineio/timeline.h | 28 +++- src/opentimelineio/transformEffects.cpp | 58 +++++++ src/opentimelineio/transformEffects.h | 197 ++++++++++++++++++++++++ src/opentimelineio/typeRegistry.cpp | 11 ++ src/opentimelineio/volumeEffects.cpp | 29 ++++ src/opentimelineio/volumeEffects.h | 99 ++++++++++++ tests/CMakeLists.txt | 4 +- tests/test_clip.cpp | 20 ++- tests/test_serialization.cpp | 21 ++- tests/test_transform_effects.cpp | 189 +++++++++++++++++++++++ tests/test_volume_effects.cpp | 140 +++++++++++++++++ 17 files changed, 825 insertions(+), 19 deletions(-) create mode 100644 src/opentimelineio/transformEffects.cpp create mode 100644 src/opentimelineio/transformEffects.h create mode 100644 src/opentimelineio/volumeEffects.cpp create mode 100644 src/opentimelineio/volumeEffects.h create mode 100644 tests/test_transform_effects.cpp create mode 100644 tests/test_volume_effects.cpp diff --git a/src/opentimelineio/CMakeLists.txt b/src/opentimelineio/CMakeLists.txt index cf5190c57a..51fae81b78 100644 --- a/src/opentimelineio/CMakeLists.txt +++ b/src/opentimelineio/CMakeLists.txt @@ -32,11 +32,13 @@ set(OPENTIMELINEIO_HEADER_FILES timeline.h track.h trackAlgorithm.h + transformEffects.h transition.h typeRegistry.h unknownSchema.h vectorIndexing.h - version.h) + version.h + volumeEffects.h) add_library(opentimelineio ${OTIO_SHARED_OR_STATIC_LIB} clip.cpp @@ -69,9 +71,11 @@ add_library(opentimelineio ${OTIO_SHARED_OR_STATIC_LIB} timeline.cpp track.cpp trackAlgorithm.cpp + transformEffects.cpp transition.cpp typeRegistry.cpp - unknownSchema.cpp + unknownSchema.cpp + volumeEffects.cpp CORE_VERSION_MAP.cpp ${OPENTIMELINEIO_HEADER_FILES}) diff --git a/src/opentimelineio/CORE_VERSION_MAP.cpp b/src/opentimelineio/CORE_VERSION_MAP.cpp index 1043902253..e943d7875a 100644 --- a/src/opentimelineio/CORE_VERSION_MAP.cpp +++ b/src/opentimelineio/CORE_VERSION_MAP.cpp @@ -145,6 +145,8 @@ const label_to_schema_version_map CORE_VERSION_MAP{ { "0.18.0.dev1", { { "Adapter", 1 }, + { "AudioFade", 1 }, + { "AudioVolume", 1 }, { "Clip", 2 }, { "Composable", 1 }, { "Composition", 1 }, @@ -169,10 +171,14 @@ const label_to_schema_version_map CORE_VERSION_MAP{ { "Stack", 1 }, { "Test", 1 }, { "TimeEffect", 1 }, - { "Timeline", 1 }, + { "Timeline", 2 }, { "Track", 1 }, { "Transition", 1 }, { "UnknownSchema", 1 }, + { "VideoCrop", 1 }, + { "VideoPosition", 1 }, + { "VideoRotate", 1 }, + { "VideoScale", 1 }, } }, // {next} }; diff --git a/src/opentimelineio/deserialization.cpp b/src/opentimelineio/deserialization.cpp index 9090e23ade..c036b31b29 100644 --- a/src/opentimelineio/deserialization.cpp +++ b/src/opentimelineio/deserialization.cpp @@ -824,6 +824,14 @@ SerializableObject::Reader::read( return _read_optional(key, value); } +bool +SerializableObject::Reader::read( + std::string const& key, + std::optional* value) +{ + return _read_optional(key, value); +} + bool SerializableObject::Reader::read( std::string const& key, diff --git a/src/opentimelineio/serializableObject.h b/src/opentimelineio/serializableObject.h index 63b096a038..9423178f30 100644 --- a/src/opentimelineio/serializableObject.h +++ b/src/opentimelineio/serializableObject.h @@ -137,6 +137,8 @@ class SerializableObject bool read(std::string const& key, std::optional* dest); bool read(std::string const& key, std::optional* dest); bool read(std::string const& key, std::optional* dest); + bool read(std::string const& key, + std::optional* value); bool read( std::string const& key, std::optional* value); @@ -451,6 +453,9 @@ class SerializableObject void write(std::string const& key, IMATH_NAMESPACE::Box2d value); void write(std::string const& key, std::optional value); void write(std::string const& key, std::optional value); + void write( + std::string const& key, + std::optional value); void write( std::string const& key, std::optional value); diff --git a/src/opentimelineio/serialization.cpp b/src/opentimelineio/serialization.cpp index 0b9b2de2ba..71918672be 100644 --- a/src/opentimelineio/serialization.cpp +++ b/src/opentimelineio/serialization.cpp @@ -924,6 +924,15 @@ SerializableObject::Writer::write( value ? _encoder.write_value(*value) : _encoder.write_null_value(); } +void +SerializableObject::Writer::write( + std::string const& key, + std::optional value) +{ + _encoder_write_key(key); + value ? _encoder.write_value(*value) : _encoder.write_null_value(); +} + void SerializableObject::Writer::write(std::string const& key, TimeTransform value) { diff --git a/src/opentimelineio/timeline.cpp b/src/opentimelineio/timeline.cpp index d2f38571e8..e76d8c3d91 100644 --- a/src/opentimelineio/timeline.cpp +++ b/src/opentimelineio/timeline.cpp @@ -7,11 +7,13 @@ namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { Timeline::Timeline( - std::string const& name, - std::optional global_start_time, - AnyDictionary const& metadata) + std::string const& name, + std::optional global_start_time, + std::optional canvas_size, + AnyDictionary const& metadata) : SerializableObjectWithMetadata(name, metadata) , _global_start_time(global_start_time) + , _canvas_size(canvas_size) , _tracks(new Stack("tracks")) {} @@ -29,6 +31,7 @@ Timeline::read_from(Reader& reader) { return reader.read("tracks", &_tracks) && reader.read_if_present("global_start_time", &_global_start_time) + && reader.read_if_present("canvas_size", &_canvas_size) && Parent::read_from(reader); } @@ -37,6 +40,7 @@ Timeline::write_to(Writer& writer) const { Parent::write_to(writer); writer.write("global_start_time", _global_start_time); + writer.write("canvas_size", _canvas_size); writer.write("tracks", _tracks); } diff --git a/src/opentimelineio/timeline.h b/src/opentimelineio/timeline.h index d18c629d83..baaf128774 100644 --- a/src/opentimelineio/timeline.h +++ b/src/opentimelineio/timeline.h @@ -20,7 +20,7 @@ class Timeline : public SerializableObjectWithMetadata struct Schema { static auto constexpr name = "Timeline"; - static int constexpr version = 1; + static int constexpr version = 2; }; using Parent = SerializableObjectWithMetadata; @@ -29,11 +29,13 @@ class Timeline : public SerializableObjectWithMetadata /// /// @param name The timeline name. /// @param global_start_time The global start time of the timeline. + /// @param canvas_size The dimensions of the target canvas /// @param metadata The metadata for the timeline. Timeline( - std::string const& name = std::string(), - std::optional global_start_time = std::nullopt, - AnyDictionary const& metadata = AnyDictionary()); + std::string const& name = std::string(), + std::optional global_start_time = std::nullopt, + std::optional canvas_size = std::nullopt, + AnyDictionary const& metadata = AnyDictionary()); /// @brief Return the timeline stack. Stack* tracks() const noexcept { return _tracks; } @@ -59,6 +61,19 @@ class Timeline : public SerializableObjectWithMetadata _global_start_time = global_start_time; } + /// @brief Return the canvas size + std::optional canvas_size() const noexcept + { + return _canvas_size; + } + + /// @brief Set the canvas size + void + set_canvas_size(std::optional const& canvas_size) + { + _canvas_size = canvas_size; + } + /// @brief Return the duration of the timeline. RationalTime duration(ErrorStatus* error_status = nullptr) const { @@ -116,8 +131,9 @@ class Timeline : public SerializableObjectWithMetadata void write_to(Writer&) const override; private: - std::optional _global_start_time; - Retainer _tracks; + std::optional _global_start_time; + std::optional _canvas_size; + Retainer _tracks; }; template diff --git a/src/opentimelineio/transformEffects.cpp b/src/opentimelineio/transformEffects.cpp new file mode 100644 index 0000000000..b200d1c184 --- /dev/null +++ b/src/opentimelineio/transformEffects.cpp @@ -0,0 +1,58 @@ +#include "opentimelineio/transformEffects.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { +bool VideoScale::read_from(Reader &reader) +{ + return reader.read("width", &_width) + && reader.read("height", &_height) + && Parent::read_from(reader); +} + +void VideoScale::write_to(Writer &writer) const { + Parent::write_to(writer); + writer.write("width", _width); + writer.write("height", _height); +} + +bool VideoCrop::read_from(Reader &reader) +{ + return reader.read("left", &_left) + && reader.read("right", &_right) + && reader.read("top", &_top) + && reader.read("bottom", &_bottom) + && Parent::read_from(reader); +} + +void VideoCrop::write_to(Writer &writer) const { + Parent::write_to(writer); + writer.write("left", _left); + writer.write("right", _right); + writer.write("top", _top); + writer.write("bottom", _bottom); +} + +bool VideoPosition::read_from(Reader &reader) +{ + return reader.read("x", &_x) + && reader.read("y", &_y) + && Parent::read_from(reader); +} + +void VideoPosition::write_to(Writer &writer) const { + Parent::write_to(writer); + writer.write("x", _x); + writer.write("y", _y); +} + +bool VideoRotate::read_from(Reader &reader) +{ + return reader.read("angle", &_angle) + && Parent::read_from(reader); +} + +void VideoRotate::write_to(Writer &writer) const { + Parent::write_to(writer); + writer.write("angle", _angle); +} + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/transformEffects.h b/src/opentimelineio/transformEffects.h new file mode 100644 index 0000000000..60efd44714 --- /dev/null +++ b/src/opentimelineio/transformEffects.h @@ -0,0 +1,197 @@ +#pragma once + +#include "opentimelineio/effect.h" +#include "opentimelineio/version.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { + +/// @brief An scaling effect +class VideoScale : public Effect +{ +public: + /// @brief This struct provides the Effect schema. + struct Schema + { + static auto constexpr name = "VideoScale"; + static int constexpr version = 1; + }; + + using Parent = Effect; + + /// @brief Create a new scaling effect. + /// + /// @param name The name of the effect object. + /// @param width How much to scale the width by. + /// @param height How much to scale the height by. + /// @param metadata The metadata for the effect. + /// @param enabled Whether the effect is enabled. + VideoScale( + std::string const& name = std::string(), + int64_t width = 0, + int64_t height = 0, + AnyDictionary const& metadata = AnyDictionary()) + : Effect(name, Schema::name, metadata) + , _width(width) + , _height(height) + {} + + int64_t width() const noexcept { return _width; } + int64_t height() const noexcept { return _height; } + + void set_width(int64_t width) noexcept { _width = width; } + void set_height(int64_t height) noexcept { _height = height; } + +protected: + + virtual ~VideoScale() = default; + bool read_from(Reader&) override; + void write_to(Writer&) const override; + + int64_t _width; ///< The scaled width + int64_t _height; ///< The scaled height +}; + +/// @brief An crop effect +class VideoCrop : public Effect +{ +public: + /// @brief This struct provides the Effect schema. + struct Schema + { + static auto constexpr name = "VideoCrop"; + static int constexpr version = 1; + }; + + using Parent = Effect; + + /// @brief Create a new crop effect. + /// + /// @param name The name of the effect object. + /// @param left The amount to crop from the left. + /// @param right The amount to crop from the right. + /// @param top The amount to crop from the top. + /// @param bottom The amount to crop from the bottom. + /// @param metadata The metadata for the effect. + /// @param enabled Whether the effect is enabled. + VideoCrop( + std::string const& name = std::string(), + int64_t left = 0, + int64_t right = 0, + int64_t top = 0, + int64_t bottom = 0, + AnyDictionary const& metadata = AnyDictionary(), + bool enabled = true) + : Effect(name, Schema::name, metadata, enabled) + , _left(left) + , _right(right) + , _top(top) + , _bottom(bottom) + {} + + int64_t left() const noexcept { return _left; } + int64_t right() const noexcept { return _right; } + int64_t top() const noexcept { return _top; } + int64_t bottom() const noexcept { return _bottom; } + + void set_left(int64_t left) noexcept { _left = left; } + void set_right(int64_t right) noexcept { _right = right; } + void set_top(int64_t top) noexcept { _top = top; } + void set_bottom(int64_t bottom) noexcept { _bottom = bottom; } + +protected: + virtual ~VideoCrop() = default; + bool read_from(Reader&) override; + void write_to(Writer&) const override; + + int64_t _left; ///< The amount to crop from the left. + int64_t _right; ///< The amount to crop from the right. + int64_t _top; ///< The amount to crop from the top. + int64_t _bottom; ///< The amount to crop from the bottom. +}; + +/// @brief An position effect +class VideoPosition : public Effect +{ +public: + /// @brief This struct provides the Effect schema. + struct Schema + { + static auto constexpr name = "VideoPosition"; + static int constexpr version = 1; + }; + + using Parent = Effect; + + /// @brief Create a new position effect. + /// + /// @param name The name of the effect object. + /// @param x Distance of top left corner from left edge of canvas + /// @param y Distance of top left corner from top edge of canvas + /// @param metadata The metadata for the effect. + /// @param enabled Whether the effect is enabled. + VideoPosition( + std::string const& name = std::string(), + int64_t x = 0, + int64_t y = 0, + AnyDictionary const& metadata = AnyDictionary(), + bool enabled = true) + : Effect(name, Schema::name, metadata, enabled) + , _x(x) + , _y(y) + {} + + int64_t x() const noexcept { return _x; } + int64_t y() const noexcept { return _y; } + + void set_x(int64_t x) noexcept { _x = x; } + void set_y(int64_t y) noexcept { _y = y; } + +protected: + virtual ~VideoPosition() = default; + bool read_from(Reader&) override; + void write_to(Writer&) const override; + + int64_t _x; ///< The horizontal position. + int64_t _y; ///< The vertical position. +}; + +/// @brief An rotation effect +class VideoRotate : public Effect +{ +public: + /// @brief This struct provides the Effect schema. + struct Schema + { + static auto constexpr name = "VideoRotate"; + static int constexpr version = 1; + }; + + using Parent = Effect; + + /// @brief Create a new rotation effect. + /// + /// @param name The name of the effect object. + /// @param angle The amount of rotation, degrees clockwise + /// @param metadata The metadata for the effect. + /// @param enabled Whether the effect is enabled. + VideoRotate( + std::string const& name = std::string(), + double angle = 0.0, + AnyDictionary const& metadata = AnyDictionary(), + bool enabled = true) + : Effect(name, Schema::name, metadata, enabled) + , _angle(angle) + {} + + double angle() const noexcept { return _angle; } + void set_angle(double angle) noexcept { _angle = angle; } + +protected: + virtual ~VideoRotate() = default; + bool read_from(Reader&) override; + void write_to(Writer&) const override; + + double _angle; ///< The angle of rotation, degrees clockwise +}; + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/typeRegistry.cpp b/src/opentimelineio/typeRegistry.cpp index 7254079496..7d68425ef5 100644 --- a/src/opentimelineio/typeRegistry.cpp +++ b/src/opentimelineio/typeRegistry.cpp @@ -25,8 +25,10 @@ #include "opentimelineio/timeEffect.h" #include "opentimelineio/timeline.h" #include "opentimelineio/track.h" +#include "opentimelineio/transformEffects.h" #include "opentimelineio/transition.h" #include "opentimelineio/unknownSchema.h" +#include "opentimelineio/volumeEffects.h" #include "stringUtils.h" #include @@ -54,6 +56,9 @@ TypeRegistry::TypeRegistry() }, "UnknownSchema"); + register_type(); + register_type(); + register_type(); register_type(); register_type(); @@ -86,6 +91,12 @@ TypeRegistry::TypeRegistry() register_type(); register_type(); register_type_from_existing_type("Sequence", 1, "Track", nullptr); + + register_type(); + register_type(); + register_type(); + register_type(); + register_type(); /* diff --git a/src/opentimelineio/volumeEffects.cpp b/src/opentimelineio/volumeEffects.cpp new file mode 100644 index 0000000000..70f260d1eb --- /dev/null +++ b/src/opentimelineio/volumeEffects.cpp @@ -0,0 +1,29 @@ +#include "volumeEffects.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { + +bool AudioVolume::read_from(Reader& reader) { + return reader.read("gain",& _gain) + && Parent::read_from(reader); +} + +void AudioVolume::write_to(Writer &writer) const { + Parent::write_to(writer); + writer.write("gain", _gain); +} + +bool AudioFade::read_from(Reader& reader) { + return reader.read("fade_in", &_fade_in) + && reader.read("start_time", &_start_time) + && reader.read("duration", &_duration) + && Parent::read_from(reader); +} + +void AudioFade::write_to(Writer& writer) const { + Parent::write_to(writer); + writer.write("fade_in", _fade_in); + writer.write("start_time", _start_time); + writer.write("duration", _duration); +} + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/volumeEffects.h b/src/opentimelineio/volumeEffects.h new file mode 100644 index 0000000000..f700fe5ddf --- /dev/null +++ b/src/opentimelineio/volumeEffects.h @@ -0,0 +1,99 @@ +#pragma once + +#include "opentimelineio/effect.h" +#include "opentimelineio/version.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { + +/// @brief Sets the audio volume +class AudioVolume : public Effect +{ +public: + /// @brief This struct provides the Effect schema. + struct Schema + { + static auto constexpr name = "AudioVolume"; + static int constexpr version = 1; + }; + + using Parent = Effect; + + /// @brief Create a new volume effect. + /// + /// @param name The name of the effect object. + /// @param gain Gain value + /// @param metadata The metadata for the effect. + /// @param enabled Whether the effect is enabled. + AudioVolume( + std::string const& name = std::string(), + double gain = 1.0, + AnyDictionary const& metadata = AnyDictionary()) + : Effect(name, Schema::name, metadata) + , _gain(gain) + {} + + double gain() const noexcept { return _gain; } + void set_gain(double gain) noexcept { _gain = gain; } + +protected: + + virtual ~AudioVolume() = default; + bool read_from(Reader&) override; + void write_to(Writer&) const override; + + double _gain; ///< the gain +}; + +/// @brief Describes an audio fade effect +class AudioFade : public Effect +{ +public: + /// @brief This struct provides the Effect schema. + struct Schema + { + static auto constexpr name = "AudioFade"; + static int constexpr version = 1; + }; + + using Parent = Effect; + + /// @brief Create a new audio fade effect. + /// + /// @param name The name of the effect object. + /// @param fade_in Whether this is a fade-in (true) or fade-out (false). + /// @param start_time The start time of the fade in seconds. + /// @param duration Duration of the fade in seconds. + /// @param metadata The metadata for the effect. + AudioFade( + std::string const& name = std::string(), + bool fade_in = true, + double start_time = 0.0, + double duration = 0.0, + AnyDictionary const& metadata = AnyDictionary()) + : Effect(name, Schema::name, metadata) + , _fade_in(fade_in) + , _start_time(start_time) + , _duration(duration) + {} + + bool fade_in() const noexcept { return _fade_in; } + void set_fade_in(bool fade_in) noexcept { _fade_in = fade_in; } + + double start_time() const noexcept { return _start_time; } + void set_start_time(double start_time) noexcept { _start_time = start_time; } + + double duration() const noexcept { return _duration; } + void set_duration(double duration) noexcept { _duration = duration; } + +protected: + + virtual ~AudioFade() = default; + bool read_from(Reader&) override; + void write_to(Writer&) const override; + + bool _fade_in; ///< true for fade-in, false for fade-out + double _start_time; ///< start time of the fade in seconds + double _duration; ///< duration of the fade in seconds +}; + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a17de5c8e8..d07c079b2f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,13 +15,13 @@ foreach(test ${tests_opentime}) WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) endforeach() -list(APPEND tests_opentimelineio test_clip test_serialization test_serializableCollection test_stack_algo test_timeline test_track test_editAlgorithm) +list(APPEND tests_opentimelineio test_clip test_serialization test_serializableCollection test_stack_algo test_timeline test_track test_editAlgorithm test_transform_effects test_volume_effects) foreach(test ${tests_opentimelineio}) add_executable(${test} utils.h utils.cpp ${test}.cpp) target_link_libraries(${test} opentimelineio) set_target_properties(${test} PROPERTIES FOLDER tests) - add_test(NAME ${test} + add_test(NAME ${test} COMMAND ${test} # Set the pwd to the source directory so we can load the samples # like the python tests do diff --git a/tests/test_clip.cpp b/tests/test_clip.cpp index 285bbd76e0..d889365d5c 100644 --- a/tests/test_clip.cpp +++ b/tests/test_clip.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include @@ -154,12 +155,21 @@ main(int argc, char** argv) using namespace otio; static constexpr auto time_scalar = 1.5; + static constexpr auto width = 1920; + static constexpr auto height = 1280; SerializableObject::Retainer ltw(new LinearTimeWarp( LinearTimeWarp::Schema::name, LinearTimeWarp::Schema::name, time_scalar)); - std::vector effects = { ltw }; + + SerializableObject::Retainer vscl( + new VideoScale( + VideoScale::Schema::name, + 100, + 200)); + + std::vector effects = { ltw, vscl }; static constexpr auto red = Marker::Color::red; @@ -247,11 +257,15 @@ main(int argc, char** argv) clip->set_media_references({ { "cloud", ref4 } }, "cloud"); assertEqual(clip->media_reference(), ref4.value); - // basic test for an effect + // basic test for effects assertEqual(clip->effects().size(), effects.size()); auto effect = dynamic_cast( - clip->effects().front().value); + clip->effects()[0].value); assertEqual(effect->time_scalar(), time_scalar); + auto scale = dynamic_cast( + clip->effects()[1].value); + assertEqual(scale->width(), 100); + assertEqual(scale->height(), 200); // basic test for a marker assertEqual(clip->markers().size(), markers.size()); diff --git a/tests/test_serialization.cpp b/tests/test_serialization.cpp index 53c352b2ca..e0c55f9477 100644 --- a/tests/test_serialization.cpp +++ b/tests/test_serialization.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -13,6 +14,7 @@ #include #include +#include namespace otime = opentime::OPENTIME_VERSION; namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; @@ -26,6 +28,10 @@ main(int argc, char** argv) "success with default indent", [] { otio::SerializableObject::Retainer cl = new otio::Clip(); + + otio::SerializableObject::Retainer vs = + new otio::VideoScale("scale", 1920, 1280); + cl->effects().push_back(vs); otio::SerializableObject::Retainer tr = new otio::Track(); tr->append_child(cl); @@ -37,10 +43,11 @@ main(int argc, char** argv) auto output = tl.value->to_json_string(&err, {}); assertFalse(otio::is_error(err)); assertEqual(output.c_str(), R"CONTENT({ - "OTIO_SCHEMA": "Timeline.1", + "OTIO_SCHEMA": "Timeline.2", "metadata": {}, "name": "", "global_start_time": null, + "canvas_size": null, "tracks": { "OTIO_SCHEMA": "Stack.1", "metadata": {}, @@ -64,7 +71,17 @@ main(int argc, char** argv) "metadata": {}, "name": "", "source_range": null, - "effects": [], + "effects": [ + { + "OTIO_SCHEMA": "VideoScale.1", + "metadata": {}, + "name": "scale", + "effect_name": "VideoScale", + "enabled": true, + "width": 1920, + "height": 1280 + } + ], "markers": [], "enabled": true, "media_references": { diff --git a/tests/test_transform_effects.cpp b/tests/test_transform_effects.cpp new file mode 100644 index 0000000000..05663eac36 --- /dev/null +++ b/tests/test_transform_effects.cpp @@ -0,0 +1,189 @@ +#include "utils.h" + +#include +#include +#include +#include + +namespace otime = opentime::OPENTIME_VERSION; +namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; + +int +main(int argc, char** argv) +{ + Tests tests; + tests.add_test("test_video_transform_read", [] { + using namespace otio; + + otio::ErrorStatus status; + SerializableObject::Retainer<> so = + SerializableObject::from_json_string( + R"( + { + "OTIO_SCHEMA": "Clip.1", + "media_reference": { + "OTIO_SCHEMA": "ExternalReference.1", + "target_url": "unit_test_url", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 8 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 10 + } + } + }, + "effects": [ + { + "OTIO_SCHEMA": "VideoScale.1", + "name": "scale", + "width": 100, + "height": 120, + "effect_name": "VideoScale", + "enabled": true + }, + { + "OTIO_SCHEMA": "VideoPosition.1", + "name": "position", + "x": 10, + "y": 20, + "effect_name": "VideoPosition", + "enabled": true + }, + { + "OTIO_SCHEMA": "VideoRotate.1", + "name": "rotate", + "angle": 45.5, + "effect_name": "VideoRotate", + "enabled": true + }, + { + "OTIO_SCHEMA": "VideoCrop.1", + "name": "crop", + "left": 5, + "right": 6, + "top": 7, + "bottom": 8, + "effect_name": "VideoCrop", + "enabled": true + } + ] + })", + &status); + + assertFalse(is_error(status)); + + const Clip* clip = dynamic_cast(so.value); + assertNotNull(clip); + + auto effects = clip->effects(); + assertEqual(effects.size(), 4); + + auto video_scale = dynamic_cast(effects[0].value); + assertNotNull(video_scale); + assertEqual(video_scale->width(), 100); + assertEqual(video_scale->height(), 120); + + auto video_position = dynamic_cast(effects[1].value); + assertNotNull(video_position); + assertEqual(video_position->x(), 10); + assertEqual(video_position->y(), 20); + + auto video_rotate = dynamic_cast(effects[2].value); + assertNotNull(video_rotate); + assertEqual(video_rotate->angle(), 45.5); + + auto video_crop = dynamic_cast(effects[3].value); + assertNotNull(video_crop); + assertEqual(video_crop->left(), 5); + assertEqual(video_crop->right(), 6); + assertEqual(video_crop->top(), 7); + assertEqual(video_crop->bottom(), 8); + }); + + tests.add_test("test_video_transform_write", [] { + using namespace otio; + + SerializableObject::Retainer clip(new otio::Clip( + "unit_clip", + new otio::ExternalReference("unit_test_url"), + std::nullopt, + otio::AnyDictionary(), + { new otio::VideoScale("scale", 100, 120), + new otio::VideoPosition("position", 10, 20), + new otio::VideoRotate("rotate", 40.5), + new otio::VideoCrop("crop", 1, 2, 3, 4) })); + + auto json = clip.value->to_json_string(); + + std::string expected_json = R"({ + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "unit_clip", + "source_range": null, + "effects": [ + { + "OTIO_SCHEMA": "VideoScale.1", + "metadata": {}, + "name": "scale", + "effect_name": "VideoScale", + "enabled": true, + "width": 100, + "height": 120 + }, + { + "OTIO_SCHEMA": "VideoPosition.1", + "metadata": {}, + "name": "position", + "effect_name": "VideoPosition", + "enabled": true, + "x": 10, + "y": 20 + }, + { + "OTIO_SCHEMA": "VideoRotate.1", + "metadata": {}, + "name": "rotate", + "effect_name": "VideoRotate", + "enabled": true, + "angle": 40.5 + }, + { + "OTIO_SCHEMA": "VideoCrop.1", + "metadata": {}, + "name": "crop", + "effect_name": "VideoCrop", + "enabled": true, + "left": 1, + "right": 2, + "top": 3, + "bottom": 4 + } + ], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": {}, + "name": "", + "available_range": null, + "available_image_bounds": null, + "target_url": "unit_test_url" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +})"; + + assertEqual(json, expected_json); + + }); + + tests.run(argc, argv); + return 0; +} diff --git a/tests/test_volume_effects.cpp b/tests/test_volume_effects.cpp new file mode 100644 index 0000000000..c7d40d511c --- /dev/null +++ b/tests/test_volume_effects.cpp @@ -0,0 +1,140 @@ +#include "utils.h" + +#include +#include +#include +#include + +namespace otime = opentime::OPENTIME_VERSION; +namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; + +int +main(int argc, char** argv) +{ + Tests tests; + tests.add_test("test_audio_volume_read", [] { + using namespace otio; + + otio::ErrorStatus status; + SerializableObject::Retainer<> so = + SerializableObject::from_json_string( + R"( + { + "OTIO_SCHEMA": "Clip.1", + "media_reference": { + "OTIO_SCHEMA": "ExternalReference.1", + "target_url": "unit_test_url", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 8 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 10 + } + } + }, + "effects": [ + { + "OTIO_SCHEMA": "AudioVolume.1", + "name": "volume", + "gain": 0.5, + "effect_name": "AudioVolume", + "enabled": true + }, + { + "OTIO_SCHEMA": "AudioFade.1", + "name": "fade", + "fade_in": false, + "start_time": 1.5, + "duration": 5.0, + "effect_name": "AudioFade", + "enabled": true + } + ] + })", + &status); + + assertFalse(is_error(status)); + + const Clip* clip = dynamic_cast(so.value); + assertNotNull(clip); + + auto effects = clip->effects(); + assertEqual(effects.size(), 2); + + auto audio_volume = dynamic_cast(effects[0].value); + assertNotNull(audio_volume); + assertEqual(audio_volume->gain(), 0.5); + + auto audio_fade = dynamic_cast(effects[1].value); + assertNotNull(audio_fade); + assertEqual(audio_fade->fade_in(), false); + assertEqual(audio_fade->start_time(), 1.5); + assertEqual(audio_fade->duration(), 5.0); + }); + + tests.add_test("test_audio_volume_write", [] { + using namespace otio; + + SerializableObject::Retainer clip(new otio::Clip( + "unit_clip", + new otio::ExternalReference("unit_test_url"), + std::nullopt, + otio::AnyDictionary(), + { new otio::AudioVolume("volume", 0.75), + new otio::AudioFade("fade", true, 2.0, 10.5)})); + + auto json = clip.value->to_json_string(); + + std::string expected_json = R"({ + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "unit_clip", + "source_range": null, + "effects": [ + { + "OTIO_SCHEMA": "AudioVolume.1", + "metadata": {}, + "name": "volume", + "effect_name": "AudioVolume", + "enabled": true, + "gain": 0.75 + }, + { + "OTIO_SCHEMA": "AudioFade.1", + "metadata": {}, + "name": "fade", + "effect_name": "AudioFade", + "enabled": true, + "fade_in": true, + "start_time": 2.0, + "duration": 10.5 + } + ], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": {}, + "name": "", + "available_range": null, + "available_image_bounds": null, + "target_url": "unit_test_url" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +})"; + + assertEqual(json, expected_json); + + }); + + tests.run(argc, argv); + return 0; +} From 4b195f3c94421fe32ea90bc5cd5533c6a7c7beef Mon Sep 17 00:00:00 2001 From: Mike Dyer Date: Tue, 17 Jun 2025 09:46:42 +1000 Subject: [PATCH 02/27] [RIV-97311] Add effect bindings This commit: * Add python bindings for new effects * Updates timeline bindings for canvas size * Adds/fixes unit tests --- .../otio-serialized-schema-only-fields.md | 64 ++++- docs/tutorials/otio-serialized-schema.md | 128 ++++++++- .../otio_serializableObjects.cpp | 91 +++++- .../opentimelineio/schema/__init__.py | 14 +- tests/baselines/empty_timeline.json | 3 +- tests/sample_data/screening_example.otio | 3 +- tests/test_console.py | 2 +- tests/test_timeline_algo.py | 6 +- tests/test_transform_effects.py | 265 ++++++++++++++++++ tests/test_volume_effects.py | 122 ++++++++ 10 files changed, 689 insertions(+), 9 deletions(-) create mode 100644 tests/test_transform_effects.py create mode 100644 tests/test_volume_effects.py diff --git a/docs/tutorials/otio-serialized-schema-only-fields.md b/docs/tutorials/otio-serialized-schema-only-fields.md index b9496d9d99..569fac0cfa 100644 --- a/docs/tutorials/otio-serialized-schema-only-fields.md +++ b/docs/tutorials/otio-serialized-schema-only-fields.md @@ -128,6 +128,26 @@ parameters: ## Module: opentimelineio.schema +### AudioFade.1 + +parameters: +- *duration* +- *effect_name* +- *enabled* +- *fade_in* +- *metadata* +- *name* +- *start_time* + +### AudioVolume.1 + +parameters: +- *effect_name* +- *enabled* +- *gain* +- *metadata* +- *name* + ### Clip.2 parameters: @@ -252,9 +272,10 @@ parameters: - *metadata* - *name* -### Timeline.1 +### Timeline.2 parameters: +- *canvas_size* - *global_start_time* - *metadata* - *name* @@ -280,6 +301,47 @@ parameters: - *out_offset* - *transition_type* +### VideoCrop.1 + +parameters: +- *bottom* +- *effect_name* +- *enabled* +- *left* +- *metadata* +- *name* +- *right* +- *top* + +### VideoPosition.1 + +parameters: +- *effect_name* +- *enabled* +- *metadata* +- *name* +- *x* +- *y* + +### VideoRotate.1 + +parameters: +- *angle* +- *effect_name* +- *enabled* +- *metadata* +- *name* + +### VideoScale.1 + +parameters: +- *effect_name* +- *enabled* +- *height* +- *metadata* +- *name* +- *width* + ### SchemaDef.1 parameters: diff --git a/docs/tutorials/otio-serialized-schema.md b/docs/tutorials/otio-serialized-schema.md index 90706b3633..f2ffbcbdb5 100644 --- a/docs/tutorials/otio-serialized-schema.md +++ b/docs/tutorials/otio-serialized-schema.md @@ -274,6 +274,48 @@ parameters: ## Module: opentimelineio.schema +### AudioFade.1 + +*full module path*: `opentimelineio.schema.AudioFade` + +*documentation*: + +``` + +An effect that defines an audio fade. +If fade_in is true, audio is fading in from the start time for the duration +If fade_in is false, the audio is fading out from the start time for the duration + +``` + +parameters: +- *duration*: Fade duration +- *effect_name*: +- *enabled*: If true, the Effect is applied. If false, the Effect is omitted. +- *fade_in*: Fade direction +- *metadata*: +- *name*: +- *start_time*: Fade start time + +### AudioVolume.1 + +*full module path*: `opentimelineio.schema.AudioVolume` + +*documentation*: + +``` + +An effect that multiplies the audio volume by a given gain value + +``` + +parameters: +- *effect_name*: +- *enabled*: If true, the Effect is applied. If false, the Effect is omitted. +- *gain*: Gain multiplier +- *metadata*: +- *name*: + ### Clip.2 *full module path*: `opentimelineio.schema.Clip` @@ -614,7 +656,7 @@ parameters: - *metadata*: - *name*: -### Timeline.1 +### Timeline.2 *full module path*: `opentimelineio.schema.Timeline` @@ -625,6 +667,7 @@ None ``` parameters: +- *canvas_size*: - *global_start_time*: - *metadata*: - *name*: @@ -667,6 +710,89 @@ parameters: - *out_offset*: Amount of the next clip this transition overlaps, exclusive. - *transition_type*: Kind of transition, as defined by the :class:`Type` enum. +### VideoCrop.1 + +*full module path*: `opentimelineio.schema.VideoCrop` + +*documentation*: + +``` + +An effect that crops video by a given amount of pixels on each side. + +``` + +parameters: +- *bottom*: +- *effect_name*: +- *enabled*: If true, the Effect is applied. If false, the Effect is omitted. +- *left*: +- *metadata*: +- *name*: +- *right*: +- *top*: + +### VideoPosition.1 + +*full module path*: `opentimelineio.schema.VideoPosition` + +*documentation*: + +``` + +An effect that positions video by a given offset in the frame. +The position is the location of the top left of the image on the canvas + +``` + +parameters: +- *effect_name*: +- *enabled*: If true, the Effect is applied. If false, the Effect is omitted. +- *metadata*: +- *name*: +- *x*: +- *y*: + +### VideoRotate.1 + +*full module path*: `opentimelineio.schema.VideoRotate` + +*documentation*: + +``` + +An effect that rotates video by a given amount. +The rotation is specified in degrees clockwise. + +``` + +parameters: +- *angle*: Rotation angle in degrees clockwise +- *effect_name*: +- *enabled*: If true, the Effect is applied. If false, the Effect is omitted. +- *metadata*: +- *name*: + +### VideoScale.1 + +*full module path*: `opentimelineio.schema.VideoScale` + +*documentation*: + +``` + +An effect that scales video to the given dimensions. + +``` + +parameters: +- *effect_name*: +- *enabled*: If true, the Effect is applied. If false, the Effect is omitted. +- *height*: Height to scale to +- *metadata*: +- *name*: +- *width*: Width to scale to + ### SchemaDef.1 *full module path*: `opentimelineio.schema.SchemaDef` diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp index 0b91a1641d..096dc4b61d 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp @@ -24,10 +24,12 @@ #include "opentimelineio/timeEffect.h" #include "opentimelineio/timeline.h" #include "opentimelineio/track.h" +#include "opentimelineio/transformEffects.h" #include "opentimelineio/transition.h" #include "opentimelineio/serializableCollection.h" #include "opentimelineio/stack.h" #include "opentimelineio/unknownSchema.h" +#include "opentimelineio/volumeEffects.h" #include "otio_utils.h" #include "otio_anyDictionary.h" @@ -630,9 +632,10 @@ Should be subclassed (for example by :class:`.Track` and :class:`.Stack`), not u .def(py::init([](std::string name, std::optional> children, std::optional global_start_time, + std::optional canvas_size, py::object metadata) { auto composable_children = vector_or_default(children); - Timeline* t = new Timeline(name, global_start_time, + Timeline* t = new Timeline(name, global_start_time, canvas_size, py_to_any_dictionary(metadata)); if (!composable_children.empty()) t->tracks()->set_children(composable_children, ErrorStatusHandler()); @@ -641,8 +644,10 @@ Should be subclassed (for example by :class:`.Track` and :class:`.Stack`), not u py::arg_v("name"_a = std::string()), "tracks"_a = py::none(), "global_start_time"_a = std::nullopt, + "canvas_size"_a = std::nullopt, py::arg_v("metadata"_a = py::none())) .def_property("global_start_time", &Timeline::global_start_time, &Timeline::set_global_start_time) + .def_property("canvas_size", &Timeline::canvas_size, &Timeline::set_canvas_size) .def_property("tracks", &Timeline::tracks, &Timeline::set_tracks) .def("duration", [](Timeline* t) { return t->duration(ErrorStatusHandler()); @@ -707,6 +712,90 @@ Instead it affects the speed of the media displayed within that item. return new FreezeFrame(name, py_to_any_dictionary(metadata)); }), py::arg_v("name"_a = std::string()), py::arg_v("metadata"_a = py::none())); + + py::class_>(m, "VideoScale", py::dynamic_attr(), R"docstring( +An effect that scales video to the given dimensions. +)docstring") + .def(py::init([](std::string name, int64_t width, int64_t height, py::object metadata) { + return new VideoScale(name, width, height, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "width"_a = 0, + "height"_a = 0, + "metadata"_a = py::none()) + .def_property("width", &VideoScale::width, &VideoScale::set_width, "Width to scale to") + .def_property("height", &VideoScale::height, &VideoScale::set_height, "Height to scale to"); + + py::class_>(m, "VideoCrop", py::dynamic_attr(), R"docstring( +An effect that crops video by a given amount of pixels on each side. +)docstring") + .def(py::init([](std::string name, int64_t left, int64_t right, int64_t top, int64_t bottom, py::object metadata) { + return new VideoCrop(name, left, right, top, bottom, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "left"_a = 0, + "right"_a = 0, + "top"_a = 0, + "bottom"_a = 0, + "metadata"_a = py::none()) + .def_property("left", &VideoCrop::left, &VideoCrop::set_left) + .def_property("right", &VideoCrop::right, &VideoCrop::set_right) + .def_property("top", &VideoCrop::top, &VideoCrop::set_top) + .def_property("bottom", &VideoCrop::bottom, &VideoCrop::set_bottom); + + py::class_>(m, "VideoPosition", py::dynamic_attr(), R"docstring( +An effect that positions video by a given offset in the frame. +The position is the location of the top left of the image on the canvas +)docstring") + .def(py::init([](std::string name, int64_t x, int64_t y, py::object metadata) { + return new VideoPosition(name, x, y, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "x"_a = 0, + "y"_a = 0, + "metadata"_a = py::none()) + .def_property("x", &VideoPosition::x, &VideoPosition::set_x) + .def_property("y", &VideoPosition::y, &VideoPosition::set_y); + + py::class_>(m, "VideoRotate", py::dynamic_attr(), R"docstring( +An effect that rotates video by a given amount. +The rotation is specified in degrees clockwise. +)docstring") + .def(py::init([](std::string name, double rotation, py::object metadata) { + return new VideoRotate(name, rotation, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "angle"_a = 0.0, + "metadata"_a = py::none()) + .def_property("angle", &VideoRotate::angle, &VideoRotate::set_angle, "Rotation angle in degrees clockwise"); + + py::class_>(m, "AudioVolume", py::dynamic_attr(), R"docstring( +An effect that multiplies the audio volume by a given gain value +)docstring") + .def(py::init([](std::string name, double gain, py::object metadata) { + return new AudioVolume(name, gain, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "gain"_a = 1.0, + "metadata"_a = py::none()) + .def_property("gain", &AudioVolume::gain, &AudioVolume::set_gain, "Gain multiplier"); + + py::class_>(m, "AudioFade", py::dynamic_attr(), R"docstring( +An effect that defines an audio fade. +If fade_in is true, audio is fading in from the start time for the duration +If fade_in is false, the audio is fading out from the start time for the duration +)docstring") + .def(py::init([](std::string name, bool fade_in, double start_time, double duration, py::object metadata) { + return new AudioFade(name, fade_in, start_time, duration, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "fade_in"_a = true, + "start_time"_a = 0, + "duration"_a = 0, + "metadata"_a = py::none()) + .def_property("fade_in", &AudioFade::fade_in, &AudioFade::set_fade_in, "Fade direction") + .def_property("start_time", &AudioFade::start_time, &AudioFade::set_start_time, "Fade start time") + .def_property("duration", &AudioFade::duration, &AudioFade::set_duration, "Fade duration"); } static void define_media_references(py::module m) { diff --git a/src/py-opentimelineio/opentimelineio/schema/__init__.py b/src/py-opentimelineio/opentimelineio/schema/__init__.py index c7fc31bfcb..3216f5366f 100644 --- a/src/py-opentimelineio/opentimelineio/schema/__init__.py +++ b/src/py-opentimelineio/opentimelineio/schema/__init__.py @@ -6,6 +6,8 @@ """User facing classes.""" from .. _otio import ( + AudioFade, + AudioVolume, Box2d, Clip, Effect, @@ -24,6 +26,10 @@ Track, Transition, V2d, + VideoCrop, + VideoPosition, + VideoRotate, + VideoScale, ) MarkerColor = Marker.Color @@ -56,6 +62,8 @@ def timeline_from_clips(clips): return Timeline(tracks=[trck]) __all__ = [ + 'AudioFade', + 'AudioVolume', 'Box2d', 'Clip', 'Effect', @@ -74,5 +82,9 @@ def timeline_from_clips(clips): 'Transition', 'SchemaDef', 'timeline_from_clips', - 'V2d' + 'V2d', + 'VideoCrop', + 'VideoPosition', + 'VideoRotate', + 'VideoScale', ] diff --git a/tests/baselines/empty_timeline.json b/tests/baselines/empty_timeline.json index 456930f1f5..0eb9f7256d 100644 --- a/tests/baselines/empty_timeline.json +++ b/tests/baselines/empty_timeline.json @@ -1,6 +1,7 @@ { - "OTIO_SCHEMA" : "Timeline.1", + "OTIO_SCHEMA" : "Timeline.2", "global_start_time" : null, + "canvas_size" : null, "metadata" : { "comments" : "adding some stuff to metadata to try out", "a number" : 1.0 diff --git a/tests/sample_data/screening_example.otio b/tests/sample_data/screening_example.otio index dff5649e5a..2cd516bf3e 100644 --- a/tests/sample_data/screening_example.otio +++ b/tests/sample_data/screening_example.otio @@ -1,5 +1,6 @@ { - "OTIO_SCHEMA": "Timeline.1", + "OTIO_SCHEMA": "Timeline.2", + "canvas": null, "metadata": {}, "name": "Example_Screening.01", "global_start_time": null, diff --git a/tests/test_console.py b/tests/test_console.py index 845abb7207..7cf47a9018 100755 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -111,7 +111,7 @@ class OTIOStatTest(ConsoleTester, unittest.TestCase): def test_basic(self): sys.argv = ['otiostat', SCREENING_EXAMPLE_PATH] self.run_test() - self.assertIn("top level object: Timeline.1", sys.stdout.getvalue()) + self.assertIn("top level object: Timeline.2", sys.stdout.getvalue()) OTIOStatTest_ShellOut = CreateShelloutTest(OTIOStatTest) diff --git a/tests/test_timeline_algo.py b/tests/test_timeline_algo.py index 8153b5c2ea..a5bac5cdaf 100644 --- a/tests/test_timeline_algo.py +++ b/tests/test_timeline_algo.py @@ -18,7 +18,8 @@ def make_sample_timeline(self): result = otio.adapters.read_from_string( """ { - "OTIO_SCHEMA": "Timeline.1", + "OTIO_SCHEMA": "Timeline.2", + "canvas_size": null, "metadata": {}, "name": null, "tracks": { @@ -264,7 +265,8 @@ def test_trim_with_transitions(self): expected = otio.adapters.read_from_string( """ { - "OTIO_SCHEMA": "Timeline.1", + "OTIO_SCHEMA": "Timeline.2", + "canvas_size": null, "metadata": {}, "name": null, "tracks": { diff --git a/tests/test_transform_effects.py b/tests/test_transform_effects.py new file mode 100644 index 0000000000..764c8888aa --- /dev/null +++ b/tests/test_transform_effects.py @@ -0,0 +1,265 @@ +"""Transform effects class test harness.""" + +import unittest +from fractions import Fraction + +import opentimelineio as otio +import opentimelineio.test_utils as otio_test_utils + +class VideoScaleTests(unittest.TestCase, otio_test_utils.OTIOAssertions): + def test_constructor(self): + scale = otio.schema.VideoScale( + name="ScaleIt", + width=100, + height=120, + metadata={ + "foo": "bar" + } + ) + self.assertEqual(scale.width, 100) + self.assertEqual(scale.height, 120) + self.assertEqual(scale.name, "ScaleIt") + self.assertEqual(scale.metadata, {"foo": "bar"}) + + def test_eq(self): + scale1 = otio.schema.VideoScale( + name="ScaleIt", + width=120, + height=130, + metadata={ + "foo": "bar" + } + ) + scale2 = otio.schema.VideoScale( + name="ScaleIt", + width=120, + height=130, + metadata={ + "foo": "bar" + } + ) + self.assertIsOTIOEquivalentTo(scale1, scale2) + + def test_serialize(self): + scale = otio.schema.VideoScale( + name="ScaleIt", + width=130, + height=140, + metadata={ + "foo": "bar" + } + ) + encoded = otio.adapters.otio_json.write_to_string(scale) + decoded = otio.adapters.otio_json.read_from_string(encoded) + self.assertIsOTIOEquivalentTo(scale, decoded) + + def test_setters(self): + scale = otio.schema.VideoScale( + name="ScaleIt", + width=140, + height=150, + metadata={ + "foo": "bar" + } + ) + self.assertEqual(scale.width, 140) + scale.width = 100 + self.assertEqual(scale.width,100) + self.assertEqual(scale.height, 150) + scale.height = 100 + self.assertEqual(scale.height,100) + +class VideoCropTests(unittest.TestCase, otio_test_utils.OTIOAssertions): + def test_cons(self): + crop = otio.schema.VideoCrop( + name="CropIt", + left=2, + right=3, + top=4, + bottom=5, + metadata={ + "baz": "qux" + } + ) + self.assertEqual(crop.left, 2) + self.assertEqual(crop.right, 3) + self.assertEqual(crop.top, 4) + self.assertEqual(crop.bottom, 5) + self.assertEqual(crop.name, "CropIt") + self.assertEqual(crop.metadata, {"baz": "qux"}) + + def test_eq(self): + crop1 = otio.schema.VideoCrop( + name="CropIt", + left=2, + right=3, + top=4, + bottom=5, + metadata={ + "baz": "qux" + } + ) + crop2 = otio.schema.VideoCrop( + name="CropIt", + left=2, + right=3, + top=4, + bottom=5, + metadata={ + "baz": "qux" + } + ) + self.assertIsOTIOEquivalentTo(crop1, crop2) + + def test_serialize(self): + crop = otio.schema.VideoCrop( + name="CropIt", + left=2, + right=3, + top=4, + bottom=5, + metadata={ + "baz": "qux" + } + ) + encoded = otio.adapters.otio_json.write_to_string(crop) + decoded = otio.adapters.otio_json.read_from_string(encoded) + self.assertIsOTIOEquivalentTo(crop, decoded) + + def test_setters(self): + crop = otio.schema.VideoCrop( + name="CropIt", + left=2, + right=3, + top=4, + bottom=5, + metadata={ + "baz": "qux" + } + ) + self.assertEqual(crop.left, 2) + crop.left = 1 + self.assertEqual(crop.left, 1) + crop.right = 3 + self.assertEqual(crop.right, 3) + crop.top = 4 + self.assertEqual(crop.top, 4) + crop.bottom = 7 + self.assertEqual(crop.bottom, 7) + +class VideoPositionTests(unittest.TestCase, otio_test_utils.OTIOAssertions): + def test_constructor(self): + position = otio.schema.VideoPosition( + name="PositionIt", + x=11, + y=12, + metadata={ + "alpha": "beta" + } + ) + self.assertEqual(position.x, 11) + self.assertEqual(position.y, 12) + self.assertEqual(position.name, "PositionIt") + self.assertEqual(position.metadata, {"alpha": "beta"}) + + def test_eq(self): + pos1 = otio.schema.VideoPosition( + name="PositionIt", + x=11, + y=12, + metadata={ + "alpha": "beta" + } + ) + pos2 = otio.schema.VideoPosition( + name="PositionIt", + x=11, + y=12, + metadata={ + "alpha": "beta" + } + ) + self.assertIsOTIOEquivalentTo(pos1, pos2) + + def test_serialize(self): + position = otio.schema.VideoPosition( + name="PositionIt", + x=11, + y=12, + metadata={ + "alpha": "beta" + } + ) + encoded = otio.adapters.otio_json.write_to_string(position) + decoded = otio.adapters.otio_json.read_from_string(encoded) + self.assertIsOTIOEquivalentTo(position, decoded) + + def test_setters(self): + position = otio.schema.VideoPosition( + name="PositionIt", + x=11, + y=12, + metadata={ + "alpha": "beta" + } + ) + self.assertEqual(position.x, 11) + position.x = 1 + self.assertEqual(position.x, 1) + self.assertEqual(position.y, 12) + position.y = 2 + self.assertEqual(position.y, 2) + +class VideoRotateTests(unittest.TestCase, otio_test_utils.OTIOAssertions): + def test_constructor(self): + rotate = otio.schema.VideoRotate( + name="RotateIt", + angle=45.25, + metadata={ + "rot": "val" + } + ) + self.assertEqual(rotate.angle,45.25) + self.assertEqual(rotate.name, "RotateIt") + self.assertEqual(rotate.metadata, {"rot": "val"}) + + def test_eq(self): + rot1 = otio.schema.VideoRotate( + name="RotateIt", + angle=45.25, + metadata={ + "rot": "val" + } + ) + rot2 = otio.schema.VideoRotate( + name="RotateIt", + angle=45.25, + metadata={ + "rot": "val" + } + ) + self.assertIsOTIOEquivalentTo(rot1, rot2) + + def test_serialize(self): + rotate = otio.schema.VideoRotate( + name="RotateIt", + angle=45.25, + metadata={ + "rot": "val" + } + ) + encoded = otio.adapters.otio_json.write_to_string(rotate) + decoded = otio.adapters.otio_json.read_from_string(encoded) + self.assertIsOTIOEquivalentTo(rotate, decoded) + + def test_setters(self): + rotate = otio.schema.VideoRotate( + name="RotateIt", + angle=45.25, + metadata={ + "rot": "val" + } + ) + self.assertEqual(rotate.angle, 45.25) + rotate.angle = 90.0 + self.assertEqual(rotate.angle, 90.0) diff --git a/tests/test_volume_effects.py b/tests/test_volume_effects.py new file mode 100644 index 0000000000..240254c7ad --- /dev/null +++ b/tests/test_volume_effects.py @@ -0,0 +1,122 @@ +"""Volume effects class test harness.""" + +import unittest + +import opentimelineio as otio +import opentimelineio.test_utils as otio_test_utils + +class AudioVolumeTests(unittest.TestCase, otio_test_utils.OTIOAssertions): + def test_constructor(self): + scale = otio.schema.AudioVolume( + name="volume", + gain=2.5, + metadata={ + "foo": "bar" + } + ) + self.assertEqual(scale.gain, 2.5) + self.assertEqual(scale.name, "volume") + self.assertEqual(scale.metadata, {"foo": "bar"}) + + def test_eq(self): + scale1 = otio.schema.AudioVolume( + name="volume", + gain=2.5, + metadata={ + "foo": "bar" + } + ) + scale2 = otio.schema.AudioVolume( + name="volume", + gain=2.5, + metadata={ + "foo": "bar" + } + ) + self.assertIsOTIOEquivalentTo(scale1, scale2) + + def test_serialize(self): + scale = otio.schema.AudioVolume( + name="volume", + gain=0.6, + metadata={ + "foo": "bar" + } + ) + encoded = otio.adapters.otio_json.write_to_string(scale) + decoded = otio.adapters.otio_json.read_from_string(encoded) + self.assertIsOTIOEquivalentTo(scale, decoded) + + def test_setters(self): + scale = otio.schema.AudioVolume( + name="volume", + gain=0.8, + metadata={ + "foo": "bar" + } + ) + self.assertEqual(scale.gain, 0.8) + scale.gain = 0.25 + self.assertEqual(scale.gain, 0.25) + +class AudioFadeTests(unittest.TestCase, otio_test_utils.OTIOAssertions): + def test_constructor(self): + fade = otio.schema.AudioFade( + name="fade", + fade_in=True, + start_time=12.0, + duration=8.0, + metadata={"baz": "qux"} + ) + self.assertEqual(fade.name, "fade") + self.assertEqual(fade.fade_in, True) + self.assertEqual(fade.start_time, 12.0) + self.assertEqual(fade.duration, 8.0) + self.assertEqual(fade.metadata, {"baz": "qux"}) + + def test_eq(self): + fade1 = otio.schema.AudioFade( + name="fade", + fade_in=False, + start_time=5.0, + duration=3.0, + metadata={"baz": "qux"} + ) + fade2 = otio.schema.AudioFade( + name="fade", + fade_in=False, + start_time=5.0, + duration=3.0, + metadata={"baz": "qux"} + ) + self.assertIsOTIOEquivalentTo(fade1, fade2) + + def test_serialize(self): + fade = otio.schema.AudioFade( + name="fade", + fade_in=True, + start_time=2.5, + duration=1.5, + metadata={"baz": "qux"} + ) + encoded = otio.adapters.otio_json.write_to_string(fade) + decoded = otio.adapters.otio_json.read_from_string(encoded) + self.assertIsOTIOEquivalentTo(fade, decoded) + + def test_setters(self): + fade = otio.schema.AudioFade( + name="fade", + fade_in=False, + start_time=4.0, + duration=2.0, + metadata={"baz": "qux"} + ) + self.assertEqual(fade.fade_in, False) + self.assertEqual(fade.start_time, 4.0) + self.assertEqual(fade.duration, 2.0) + fade.fade_in = True + fade.start_time = 7.5 + fade.duration = 3.5 + self.assertEqual(fade.fade_in, True) + self.assertEqual(fade.start_time, 7.5) + self.assertEqual(fade.duration, 3.5) From 6b598460334037baec95d624cb5059c65164401f Mon Sep 17 00:00:00 2001 From: Mike Dyer Date: Tue, 2 Sep 2025 13:47:58 +1000 Subject: [PATCH 03/27] Fix child range calculations When iterating down the timeline, respect source range durations, as well as start times. --- src/opentimelineio/composition.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/opentimelineio/composition.cpp b/src/opentimelineio/composition.cpp index 57dfc4c0b1..62d7c91e5f 100644 --- a/src/opentimelineio/composition.cpp +++ b/src/opentimelineio/composition.cpp @@ -337,7 +337,7 @@ Composition::range_of_child(Composable const* child, ErrorStatus* error_status) result_range = TimeRange( result_range->start_time() + parent_range.start_time(), - result_range->duration()); + std::min(result_range->duration(), parent_range.duration())); current = parent; } @@ -388,7 +388,7 @@ Composition::trimmed_range_of_child( result_range = TimeRange( result_range->start_time() + parent_range.start_time(), - result_range->duration()); + std::min(result_range->duration(), parent_range.duration())); } if (!source_range()) From c476560567bf0eec3b61505ba0e9d62191cd3410 Mon Sep 17 00:00:00 2001 From: Uria Ashkenazy Adler Date: Sun, 7 Sep 2025 16:36:01 +0300 Subject: [PATCH 04/27] ignore generated files --- .gitignore | 4 ++++ src/.gitignore | 1 + 2 files changed, 5 insertions(+) create mode 100644 src/.gitignore diff --git a/.gitignore b/.gitignore index dd2e1c7e4a..69ad3d8d94 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,10 @@ htmlcov xcuserdata/ .venv/ .cache +cmake-build-debug/ +bin/ +lib/ +pyvenv.cfg # Pycharm metadata .idea/ diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000000..15e412d188 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1 @@ +ts-opentimelineio/ From f56737f1830e1bfd2af63998c388bdbcb169ddd2 Mon Sep 17 00:00:00 2001 From: Uria Ashkenazy Adler Date: Sun, 7 Sep 2025 16:36:21 +0300 Subject: [PATCH 05/27] update version map --- src/opentimelineio/CORE_VERSION_MAP.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/opentimelineio/CORE_VERSION_MAP.cpp b/src/opentimelineio/CORE_VERSION_MAP.cpp index e943d7875a..3aeeed2afd 100644 --- a/src/opentimelineio/CORE_VERSION_MAP.cpp +++ b/src/opentimelineio/CORE_VERSION_MAP.cpp @@ -179,6 +179,7 @@ const label_to_schema_version_map CORE_VERSION_MAP{ { "VideoPosition", 1 }, { "VideoRotate", 1 }, { "VideoScale", 1 }, + { "VideoRoundedCorners", 1 }, } }, // {next} }; From 8c01ee963c4ef8b520ea334aeeff339d7899ec88 Mon Sep 17 00:00:00 2001 From: Uria Ashkenazy Adler Date: Sun, 7 Sep 2025 16:38:08 +0300 Subject: [PATCH 06/27] add VideoRoundedCorners effect --- src/opentimelineio/transformEffects.cpp | 11 +++++++ src/opentimelineio/transformEffects.h | 40 +++++++++++++++++++++++++ src/opentimelineio/typeRegistry.cpp | 1 + tests/test_transform_effects.cpp | 23 ++++++++++++-- 4 files changed, 72 insertions(+), 3 deletions(-) diff --git a/src/opentimelineio/transformEffects.cpp b/src/opentimelineio/transformEffects.cpp index b200d1c184..2a1b81b241 100644 --- a/src/opentimelineio/transformEffects.cpp +++ b/src/opentimelineio/transformEffects.cpp @@ -55,4 +55,15 @@ void VideoRotate::write_to(Writer &writer) const { writer.write("angle", _angle); } +bool VideoRoundedCorners::read_from(Reader &reader) +{ + return reader.read("radius", &_radius) + && Parent::read_from(reader); +} + +void VideoRoundedCorners::write_to(Writer &writer) const { + Parent::write_to(writer); + writer.write("radius", _radius); +} + }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/transformEffects.h b/src/opentimelineio/transformEffects.h index 60efd44714..533387aa2f 100644 --- a/src/opentimelineio/transformEffects.h +++ b/src/opentimelineio/transformEffects.h @@ -194,4 +194,44 @@ class VideoRotate : public Effect double _angle; ///< The angle of rotation, degrees clockwise }; +/// @brief A rounded corner effect +class VideoRoundedCorners : public Effect +{ +public: + /// @brief This struct provides the Effect schema. + struct Schema + { + static auto constexpr name = "VideoRoundedCorners"; + static int constexpr version = 1; + }; + + using Parent = Effect; + + /// @brief Create a new rounded corner effect. + /// + /// @param name The name of the effect object. + /// @param radius The corner radius. + /// @param metadata The metadata for the effect. + /// @param enabled Whether the effect is enabled. + VideoRoundedCorners( + std::string const& name = std::string(), + int64_t const radius = 0, + AnyDictionary const& metadata = AnyDictionary(), + bool const enabled = true) + : Effect(name, Schema::name, metadata, enabled), + _radius(radius) + {} + + int64_t radius() const noexcept { return _radius; } + + void set_radius(int64_t radius) noexcept { _radius = radius; } + +protected: + ~VideoRoundedCorners() override = default; + bool read_from(Reader&) override; + void write_to(Writer&) const override; + + int64_t _radius; +}; + }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/typeRegistry.cpp b/src/opentimelineio/typeRegistry.cpp index 7d68425ef5..a716778a9a 100644 --- a/src/opentimelineio/typeRegistry.cpp +++ b/src/opentimelineio/typeRegistry.cpp @@ -96,6 +96,7 @@ TypeRegistry::TypeRegistry() register_type(); register_type(); register_type(); + register_type(); register_type(); diff --git a/tests/test_transform_effects.cpp b/tests/test_transform_effects.cpp index 05663eac36..d92edfff83 100644 --- a/tests/test_transform_effects.cpp +++ b/tests/test_transform_effects.cpp @@ -71,18 +71,26 @@ main(int argc, char** argv) "bottom": 8, "effect_name": "VideoCrop", "enabled": true + }, + { + "OTIO_SCHEMA": "VideoRoundedCorners.1", + "name": "roundedCorners", + "radius": 80, + "effect_name": "VideoRoundedCorners", + "enabled": true } ] })", &status); - assertFalse(is_error(status)); + if (is_error(status)) + throw std::invalid_argument(status.details); const Clip* clip = dynamic_cast(so.value); assertNotNull(clip); auto effects = clip->effects(); - assertEqual(effects.size(), 4); + assertEqual(effects.size(), 5); auto video_scale = dynamic_cast(effects[0].value); assertNotNull(video_scale); @@ -117,7 +125,8 @@ main(int argc, char** argv) { new otio::VideoScale("scale", 100, 120), new otio::VideoPosition("position", 10, 20), new otio::VideoRotate("rotate", 40.5), - new otio::VideoCrop("crop", 1, 2, 3, 4) })); + new otio::VideoCrop("crop", 1, 2, 3, 4), + new otio::VideoRoundedCorners("roundedCorners",80)})); auto json = clip.value->to_json_string(); @@ -163,6 +172,14 @@ main(int argc, char** argv) "right": 2, "top": 3, "bottom": 4 + }, + { + "OTIO_SCHEMA": "VideoRoundedCorners.1", + "metadata": {}, + "name": "roundedCorners", + "effect_name": "VideoRoundedCorners", + "enabled": true, + "radius": 80 } ], "markers": [], From d33ead1dad1ab2e091b38162dc7a100fd52bba0d Mon Sep 17 00:00:00 2001 From: Mike Dyer Date: Mon, 8 Sep 2025 09:54:52 +1000 Subject: [PATCH 07/27] Add Python bindings I think these help the 'make ' work --- .../otio-serialized-schema-only-fields.md | 9 ++++ docs/tutorials/otio-serialized-schema.md | 19 +++++++ src/opentimelineio/CORE_VERSION_MAP.cpp | 2 +- src/opentimelineio/transformEffects.h | 1 - .../otio_serializableObjects.cpp | 25 ++++++--- .../opentimelineio/schema/__init__.py | 1 + tests/test_transform_effects.py | 54 +++++++++++++++++++ 7 files changed, 102 insertions(+), 9 deletions(-) diff --git a/docs/tutorials/otio-serialized-schema-only-fields.md b/docs/tutorials/otio-serialized-schema-only-fields.md index 569fac0cfa..115af409fd 100644 --- a/docs/tutorials/otio-serialized-schema-only-fields.md +++ b/docs/tutorials/otio-serialized-schema-only-fields.md @@ -332,6 +332,15 @@ parameters: - *metadata* - *name* +### VideoRoundedCorners.1 + +parameters: +- *effect_name* +- *enabled* +- *metadata* +- *name* +- *radius* + ### VideoScale.1 parameters: diff --git a/docs/tutorials/otio-serialized-schema.md b/docs/tutorials/otio-serialized-schema.md index f2ffbcbdb5..70cd190f61 100644 --- a/docs/tutorials/otio-serialized-schema.md +++ b/docs/tutorials/otio-serialized-schema.md @@ -773,6 +773,25 @@ parameters: - *metadata*: - *name*: +### VideoRoundedCorners.1 + +*full module path*: `opentimelineio.schema.VideoRoundCorners` + +*documentation*: + +``` + +An effect that rounds the corners of a video + +``` + +parameters: +- *effect_name*: +- *enabled*: If true, the Effect is applied. If false, the Effect is omitted. +- *metadata*: +- *name*: +- *radius*: Radius of the corners + ### VideoScale.1 *full module path*: `opentimelineio.schema.VideoScale` diff --git a/src/opentimelineio/CORE_VERSION_MAP.cpp b/src/opentimelineio/CORE_VERSION_MAP.cpp index 3aeeed2afd..ac6091e34a 100644 --- a/src/opentimelineio/CORE_VERSION_MAP.cpp +++ b/src/opentimelineio/CORE_VERSION_MAP.cpp @@ -178,8 +178,8 @@ const label_to_schema_version_map CORE_VERSION_MAP{ { "VideoCrop", 1 }, { "VideoPosition", 1 }, { "VideoRotate", 1 }, - { "VideoScale", 1 }, { "VideoRoundedCorners", 1 }, + { "VideoScale", 1 }, } }, // {next} }; diff --git a/src/opentimelineio/transformEffects.h b/src/opentimelineio/transformEffects.h index 533387aa2f..64bb015feb 100644 --- a/src/opentimelineio/transformEffects.h +++ b/src/opentimelineio/transformEffects.h @@ -223,7 +223,6 @@ class VideoRoundedCorners : public Effect {} int64_t radius() const noexcept { return _radius; } - void set_radius(int64_t radius) noexcept { _radius = radius; } protected: diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp index 096dc4b61d..88a08dbf96 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp @@ -442,14 +442,14 @@ Contains a :class:`.MediaReference` and a trim on that media reference. "effects"_a = py::none(), "markers"_a = py::none(), "active_media_reference"_a = std::string(Clip::default_media_key)) - .def_property_readonly_static("DEFAULT_MEDIA_KEY",[](py::object /* self */) { - return Clip::default_media_key; + .def_property_readonly_static("DEFAULT_MEDIA_KEY",[](py::object /* self */) { + return Clip::default_media_key; }) .def_property("media_reference", &Clip::media_reference, &Clip::set_media_reference) - .def_property("active_media_reference_key", &Clip::active_media_reference_key, [](Clip* clip, std::string const& new_active_key) { - clip->set_active_media_reference_key(new_active_key, ErrorStatusHandler()); + .def_property("active_media_reference_key", &Clip::active_media_reference_key, [](Clip* clip, std::string const& new_active_key) { + clip->set_active_media_reference_key(new_active_key, ErrorStatusHandler()); }) - .def("media_references", &Clip::media_references) + .def("media_references", &Clip::media_references) .def("set_media_references", [](Clip* clip, Clip::MediaReferences const& media_references, std::string const& new_active_key) { clip->set_media_references(media_references, new_active_key, ErrorStatusHandler()); }); @@ -769,6 +769,17 @@ The rotation is specified in degrees clockwise. "metadata"_a = py::none()) .def_property("angle", &VideoRotate::angle, &VideoRotate::set_angle, "Rotation angle in degrees clockwise"); + py::class_>(m, "VideoRoundCorners", py::dynamic_attr(), R"docstring( +An effect that rounds the corners of a video +)docstring") + .def(py::init([](std::string name, double radius, py::object metadata) { + return new VideoRoundedCorners(name, radius, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "radius"_a = 0.0, + "metadata"_a = py::none()) + .def_property("radius", &VideoRoundedCorners::radius, &VideoRoundedCorners::set_radius, "Radius of the corners"); + py::class_>(m, "AudioVolume", py::dynamic_attr(), R"docstring( An effect that multiplies the audio volume by a given gain value )docstring") @@ -812,7 +823,7 @@ static void define_media_references(py::module m) { "available_image_bounds"_a = std::nullopt) .def_property("available_range", &MediaReference::available_range, &MediaReference::set_available_range) - .def_property("available_image_bounds", &MediaReference::available_image_bounds, &MediaReference::set_available_image_bounds) + .def_property("available_image_bounds", &MediaReference::available_image_bounds, &MediaReference::set_available_image_bounds) .def_property_readonly("is_missing_reference", &MediaReference::is_missing_reference); py::class_ Date: Mon, 8 Sep 2025 14:02:57 +1000 Subject: [PATCH 08/27] Add VideoFlip effect Add an effect to signal flipping of video about the horizontal or vertical axis --- .../otio-serialized-schema-only-fields.md | 10 +++ docs/tutorials/otio-serialized-schema.md | 20 ++++++ src/opentimelineio/CORE_VERSION_MAP.cpp | 1 + src/opentimelineio/transformEffects.cpp | 13 ++++ src/opentimelineio/transformEffects.h | 45 +++++++++++++ src/opentimelineio/typeRegistry.cpp | 1 + .../otio_serializableObjects.cpp | 27 +++++--- .../opentimelineio/schema/__init__.py | 1 + tests/test_transform_effects.cpp | 27 +++++++- tests/test_transform_effects.py | 63 +++++++++++++++++++ 10 files changed, 199 insertions(+), 9 deletions(-) diff --git a/docs/tutorials/otio-serialized-schema-only-fields.md b/docs/tutorials/otio-serialized-schema-only-fields.md index 569fac0cfa..df535d0382 100644 --- a/docs/tutorials/otio-serialized-schema-only-fields.md +++ b/docs/tutorials/otio-serialized-schema-only-fields.md @@ -313,6 +313,16 @@ parameters: - *right* - *top* +### VideoFlip.1 + +parameters: +- *effect_name* +- *enabled* +- *flip_horizontally* +- *flip_vertically* +- *metadata* +- *name* + ### VideoPosition.1 parameters: diff --git a/docs/tutorials/otio-serialized-schema.md b/docs/tutorials/otio-serialized-schema.md index f2ffbcbdb5..26a1b2f33c 100644 --- a/docs/tutorials/otio-serialized-schema.md +++ b/docs/tutorials/otio-serialized-schema.md @@ -732,6 +732,26 @@ parameters: - *right*: - *top*: +### VideoFlip.1 + +*full module path*: `opentimelineio.schema.VideoFlip` + +*documentation*: + +``` + +An effect that flips video horizontally or vertically. + +``` + +parameters: +- *effect_name*: +- *enabled*: If true, the Effect is applied. If false, the Effect is omitted. +- *flip_horizontally*: +- *flip_vertically*: +- *metadata*: +- *name*: + ### VideoPosition.1 *full module path*: `opentimelineio.schema.VideoPosition` diff --git a/src/opentimelineio/CORE_VERSION_MAP.cpp b/src/opentimelineio/CORE_VERSION_MAP.cpp index e943d7875a..9760dbb6c2 100644 --- a/src/opentimelineio/CORE_VERSION_MAP.cpp +++ b/src/opentimelineio/CORE_VERSION_MAP.cpp @@ -176,6 +176,7 @@ const label_to_schema_version_map CORE_VERSION_MAP{ { "Transition", 1 }, { "UnknownSchema", 1 }, { "VideoCrop", 1 }, + { "VideoFlip", 1 }, { "VideoPosition", 1 }, { "VideoRotate", 1 }, { "VideoScale", 1 }, diff --git a/src/opentimelineio/transformEffects.cpp b/src/opentimelineio/transformEffects.cpp index b200d1c184..8704cb5339 100644 --- a/src/opentimelineio/transformEffects.cpp +++ b/src/opentimelineio/transformEffects.cpp @@ -55,4 +55,17 @@ void VideoRotate::write_to(Writer &writer) const { writer.write("angle", _angle); } +bool VideoFlip::read_from(Reader &reader) +{ + return reader.read("flip_horizontally", &_flip_horizontally) + && reader.read("flip_vertically", &_flip_vertically) + && Parent::read_from(reader); +} + +void VideoFlip::write_to(Writer &writer) const { + Parent::write_to(writer); + writer.write("flip_horizontally", _flip_horizontally); + writer.write("flip_vertically", _flip_vertically); +} + }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/transformEffects.h b/src/opentimelineio/transformEffects.h index 60efd44714..ba4cc58a6a 100644 --- a/src/opentimelineio/transformEffects.h +++ b/src/opentimelineio/transformEffects.h @@ -194,4 +194,49 @@ class VideoRotate : public Effect double _angle; ///< The angle of rotation, degrees clockwise }; +/// @brief A flip effect +class VideoFlip : public Effect +{ +public: + struct Schema + { + static auto constexpr name = "VideoFlip"; + static int constexpr version = 1; + }; + + using Parent = Effect; + + /// @brief Create a new flip effect. + /// + /// @param name The name of the effect object. + /// @param flip_horizontally Whether to flip horizontally. + /// @param flip_vertically Whether to flip vertically. + /// @param metadata The metadata for the effect. + /// @param enabled Whether the effect is enabled. + VideoFlip( + std::string const& name = std::string(), + bool flip_horizontally = false, + bool flip_vertically = false, + AnyDictionary const& metadata = AnyDictionary(), + bool enabled = true) + : Effect(name, Schema::name, metadata, enabled) + , _flip_horizontally(flip_horizontally) + , _flip_vertically(flip_vertically) + {} + + bool flip_horizontally() const noexcept { return _flip_horizontally; } + void set_flip_horizontally(bool flip_horizontally) noexcept { _flip_horizontally = flip_horizontally; } + + bool flip_vertically() const noexcept { return _flip_vertically; } + void set_flip_vertically(bool flip_vertically) noexcept { _flip_vertically = flip_vertically; } + +protected: + virtual ~VideoFlip() = default; + bool read_from(Reader&) override; + void write_to(Writer&) const override; + + bool _flip_horizontally; ///< Whether to flip horizontally + bool _flip_vertically; ///< Whether to flip vertically +}; + }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/typeRegistry.cpp b/src/opentimelineio/typeRegistry.cpp index 7d68425ef5..ac871a4bd0 100644 --- a/src/opentimelineio/typeRegistry.cpp +++ b/src/opentimelineio/typeRegistry.cpp @@ -93,6 +93,7 @@ TypeRegistry::TypeRegistry() register_type_from_existing_type("Sequence", 1, "Track", nullptr); register_type(); + register_type(); register_type(); register_type(); register_type(); diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp index 096dc4b61d..cdc3423960 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp @@ -442,14 +442,14 @@ Contains a :class:`.MediaReference` and a trim on that media reference. "effects"_a = py::none(), "markers"_a = py::none(), "active_media_reference"_a = std::string(Clip::default_media_key)) - .def_property_readonly_static("DEFAULT_MEDIA_KEY",[](py::object /* self */) { - return Clip::default_media_key; + .def_property_readonly_static("DEFAULT_MEDIA_KEY",[](py::object /* self */) { + return Clip::default_media_key; }) .def_property("media_reference", &Clip::media_reference, &Clip::set_media_reference) - .def_property("active_media_reference_key", &Clip::active_media_reference_key, [](Clip* clip, std::string const& new_active_key) { - clip->set_active_media_reference_key(new_active_key, ErrorStatusHandler()); + .def_property("active_media_reference_key", &Clip::active_media_reference_key, [](Clip* clip, std::string const& new_active_key) { + clip->set_active_media_reference_key(new_active_key, ErrorStatusHandler()); }) - .def("media_references", &Clip::media_references) + .def("media_references", &Clip::media_references) .def("set_media_references", [](Clip* clip, Clip::MediaReferences const& media_references, std::string const& new_active_key) { clip->set_media_references(media_references, new_active_key, ErrorStatusHandler()); }); @@ -769,6 +769,19 @@ The rotation is specified in degrees clockwise. "metadata"_a = py::none()) .def_property("angle", &VideoRotate::angle, &VideoRotate::set_angle, "Rotation angle in degrees clockwise"); + py::class_>(m, "VideoFlip", py::dynamic_attr(), R"docstring( +An effect that flips video horizontally or vertically. +)docstring") + .def(py::init([](std::string name, bool flip_horizontally, bool flip_vertically, py::object metadata) { + return new VideoFlip(name, flip_horizontally, flip_vertically, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "flip_horizontally"_a = false, + "flip_vertically"_a = false, + "metadata"_a = py::none()) + .def_property("flip_horizontally", &VideoFlip::flip_horizontally, &VideoFlip::set_flip_horizontally) + .def_property("flip_vertically", &VideoFlip::flip_vertically, &VideoFlip::set_flip_vertically); + py::class_>(m, "AudioVolume", py::dynamic_attr(), R"docstring( An effect that multiplies the audio volume by a given gain value )docstring") @@ -812,7 +825,7 @@ static void define_media_references(py::module m) { "available_image_bounds"_a = std::nullopt) .def_property("available_range", &MediaReference::available_range, &MediaReference::set_available_range) - .def_property("available_image_bounds", &MediaReference::available_image_bounds, &MediaReference::set_available_image_bounds) + .def_property("available_image_bounds", &MediaReference::available_image_bounds, &MediaReference::set_available_image_bounds) .def_property_readonly("is_missing_reference", &MediaReference::is_missing_reference); py::class_effects(); - assertEqual(effects.size(), 4); + assertEqual(effects.size(), 5); auto video_scale = dynamic_cast(effects[0].value); assertNotNull(video_scale); @@ -104,6 +112,11 @@ main(int argc, char** argv) assertEqual(video_crop->right(), 6); assertEqual(video_crop->top(), 7); assertEqual(video_crop->bottom(), 8); + + auto video_flip = dynamic_cast(effects[4].value); + assertNotNull(video_flip); + assertEqual(video_flip->flip_horizontally(), true); + assertEqual(video_flip->flip_vertically(), false); }); tests.add_test("test_video_transform_write", [] { @@ -117,7 +130,8 @@ main(int argc, char** argv) { new otio::VideoScale("scale", 100, 120), new otio::VideoPosition("position", 10, 20), new otio::VideoRotate("rotate", 40.5), - new otio::VideoCrop("crop", 1, 2, 3, 4) })); + new otio::VideoCrop("crop", 1, 2, 3, 4), + new otio::VideoFlip("flip", true, false) })); auto json = clip.value->to_json_string(); @@ -163,6 +177,15 @@ main(int argc, char** argv) "right": 2, "top": 3, "bottom": 4 + }, + { + "OTIO_SCHEMA": "VideoFlip.1", + "metadata": {}, + "name": "flip", + "effect_name": "VideoFlip", + "enabled": true, + "flip_horizontally": true, + "flip_vertically": false } ], "markers": [], diff --git a/tests/test_transform_effects.py b/tests/test_transform_effects.py index 764c8888aa..c82d8b8f4a 100644 --- a/tests/test_transform_effects.py +++ b/tests/test_transform_effects.py @@ -263,3 +263,66 @@ def test_setters(self): self.assertEqual(rotate.angle, 45.25) rotate.angle = 90.0 self.assertEqual(rotate.angle, 90.0) + +class VideoFlipTests(unittest.TestCase, otio_test_utils.OTIOAssertions): + def test_constructor(self): + flip = otio.schema.VideoFlip( + name="FlipIt", + flip_horizontally=True, + flip_vertically=False, + metadata={ + "flip": "val" + } + ) + self.assertEqual(flip.flip_horizontally, True) + self.assertEqual(flip.flip_vertically, False) + self.assertEqual(flip.name, "FlipIt") + self.assertEqual(flip.metadata, {"flip": "val"}) + + def test_eq(self): + flip1 = otio.schema.VideoFlip( + name="FlipIt", + flip_horizontally=True, + flip_vertically=False, + metadata={ + "flip": "val" + } + ) + flip2 = otio.schema.VideoFlip( + name="FlipIt", + flip_horizontally=True, + flip_vertically=False, + metadata={ + "flip": "val" + } + ) + self.assertIsOTIOEquivalentTo(flip1, flip2) + + def test_serialize(self): + flip = otio.schema.VideoFlip( + name="FlipIt", + flip_horizontally=True, + flip_vertically=False, + metadata={ + "flip": "val" + } + ) + encoded = otio.adapters.otio_json.write_to_string(flip) + decoded = otio.adapters.otio_json.read_from_string(encoded) + self.assertIsOTIOEquivalentTo(flip, decoded) + + def test_setters(self): + flip = otio.schema.VideoFlip( + name="FlipIt", + flip_horizontally=True, + flip_vertically=False, + metadata={ + "flip": "val" + } + ) + self.assertEqual(flip.flip_horizontally, True) + flip.flip_horizontally = False + self.assertEqual(flip.flip_horizontally, False) + self.assertEqual(flip.flip_vertically, False) + flip.flip_vertically = True + self.assertEqual(flip.flip_vertically, True) \ No newline at end of file From 7510377db72f4a3a235ba2cbbadbeb435eaa985a Mon Sep 17 00:00:00 2001 From: Mor Arusi Date: Tue, 9 Sep 2025 17:51:37 +0300 Subject: [PATCH 09/27] attempt to fix trimmed_range_of_child --- src/opentimelineio/composition.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/opentimelineio/composition.cpp b/src/opentimelineio/composition.cpp index 62d7c91e5f..cd8b3da39f 100644 --- a/src/opentimelineio/composition.cpp +++ b/src/opentimelineio/composition.cpp @@ -386,9 +386,10 @@ Composition::trimmed_range_of_child( continue; } - result_range = TimeRange( - result_range->start_time() + parent_range.start_time(), - std::min(result_range->duration(), parent_range.duration())); + result_range = TimeRange( + std::max(result_range->start_time(), parent_range.start_time()), + std::min(result_range->duration(), parent_range.duration())); + current = parent; } if (!source_range()) From 3f3a4c2de4bff83cf25aa08cfa032781229c2d27 Mon Sep 17 00:00:00 2001 From: Mor Arusi Date: Wed, 10 Sep 2025 12:38:12 +0300 Subject: [PATCH 10/27] actual fix for trimmed_range_of_child --- src/opentimelineio/composition.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/opentimelineio/composition.cpp b/src/opentimelineio/composition.cpp index cd8b3da39f..ffe11831a8 100644 --- a/src/opentimelineio/composition.cpp +++ b/src/opentimelineio/composition.cpp @@ -386,9 +386,19 @@ Composition::trimmed_range_of_child( continue; } - result_range = TimeRange( - std::max(result_range->start_time(), parent_range.start_time()), - std::min(result_range->duration(), parent_range.duration())); + auto untrimmed_range = parent->range_of_child_at_index(index, error_status); + if (is_error(error_status)) + { + return TimeRange(); + } + + auto current_trimmed_range = static_cast(current)->trimmed_range(); + auto untrimmed_start_time = result_range->start_time() - current_trimmed_range.start_time(); + + auto start_time = std::max(untrimmed_start_time, parent_range.start_time()); + result_range = TimeRange::range_from_start_end_time( + start_time, + std::min(start_time + result_range->duration(), parent_range.end_time_exclusive())); current = parent; } From 8654f3c8d9d3c79f371928fe6cfa0db1603df1ae Mon Sep 17 00:00:00 2001 From: Mor Arusi Date: Wed, 10 Sep 2025 18:44:54 +0300 Subject: [PATCH 11/27] formatting --- src/opentimelineio/composition.cpp | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/opentimelineio/composition.cpp b/src/opentimelineio/composition.cpp index ffe11831a8..d47daefa5f 100644 --- a/src/opentimelineio/composition.cpp +++ b/src/opentimelineio/composition.cpp @@ -386,20 +386,26 @@ Composition::trimmed_range_of_child( continue; } - auto untrimmed_range = parent->range_of_child_at_index(index, error_status); + auto untrimmed_range = + parent->range_of_child_at_index(index, error_status); if (is_error(error_status)) { return TimeRange(); } - auto current_trimmed_range = static_cast(current)->trimmed_range(); - auto untrimmed_start_time = result_range->start_time() - current_trimmed_range.start_time(); - - auto start_time = std::max(untrimmed_start_time, parent_range.start_time()); - result_range = TimeRange::range_from_start_end_time( - start_time, - std::min(start_time + result_range->duration(), parent_range.end_time_exclusive())); - current = parent; + auto current_trimmed_range = + static_cast(current)->trimmed_range(); + auto untrimmed_start_time = + result_range->start_time() - current_trimmed_range.start_time(); + + auto start_time = + std::max(untrimmed_start_time, parent_range.start_time()); + result_range = TimeRange::range_from_start_end_time( + start_time, + std::min( + start_time + result_range->duration(), + parent_range.end_time_exclusive())); + current = parent; } if (!source_range()) From 52c93c3066d23d37019cb01fb9c35e4bb02807b7 Mon Sep 17 00:00:00 2001 From: Mor Arusi Date: Thu, 11 Sep 2025 16:58:19 +0300 Subject: [PATCH 12/27] fix the fix for trimmed_range_of_child --- src/opentimelineio/composition.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/opentimelineio/composition.cpp b/src/opentimelineio/composition.cpp index d47daefa5f..a96744bffa 100644 --- a/src/opentimelineio/composition.cpp +++ b/src/opentimelineio/composition.cpp @@ -396,14 +396,14 @@ Composition::trimmed_range_of_child( auto current_trimmed_range = static_cast(current)->trimmed_range(); auto untrimmed_start_time = - result_range->start_time() - current_trimmed_range.start_time(); + result_range->start_time() - current_trimmed_range.start_time() + untrimmed_range.start_time(); auto start_time = std::max(untrimmed_start_time, parent_range.start_time()); result_range = TimeRange::range_from_start_end_time( start_time, std::min( - start_time + result_range->duration(), + untrimmed_start_time + result_range->duration(), parent_range.end_time_exclusive())); current = parent; } From eddea11efaa43c7b0c2ac7c9de034f2510a6e5a9 Mon Sep 17 00:00:00 2001 From: Cesar Guirao Date: Tue, 16 Sep 2025 08:52:29 -0700 Subject: [PATCH 13/27] Memoization of range calculation --- src/opentimelineio/stack.cpp | 21 ++++++++++++++++++--- src/opentimelineio/stack.h | 3 +++ src/opentimelineio/track.cpp | 22 +++++++++++++++++++--- src/opentimelineio/track.h | 2 ++ 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/opentimelineio/stack.cpp b/src/opentimelineio/stack.cpp index 070d8944e1..3dfab9bd3b 100644 --- a/src/opentimelineio/stack.cpp +++ b/src/opentimelineio/stack.cpp @@ -41,6 +41,11 @@ Stack::write_to(Writer& writer) const TimeRange Stack::range_of_child_at_index(int index, ErrorStatus* error_status) const { + auto it = _childRangesCacche.find(index); + if (it != _childRangesCacche.end()) { + return it->second; + } + index = adjusted_vector_index(index, children()); if (index < 0 || index >= int(children().size())) { @@ -48,6 +53,7 @@ Stack::range_of_child_at_index(int index, ErrorStatus* error_status) const { *error_status = ErrorStatus::ILLEGAL_INDEX; } + _childRangesCacche[index] = TimeRange(); return TimeRange(); } @@ -55,10 +61,13 @@ Stack::range_of_child_at_index(int index, ErrorStatus* error_status) const auto duration = child->duration(error_status); if (is_error(error_status)) { + _childRangesCacche[index] = TimeRange(); return TimeRange(); } - return TimeRange(RationalTime(0, duration.rate()), duration); + auto result = TimeRange(RationalTime(0, duration.rate()), duration); + _childRangesCacche[index] = result; + return result; } std::map @@ -118,9 +127,14 @@ Stack::trimmed_range_of_child_at_index(int index, ErrorStatus* error_status) TimeRange Stack::available_range(ErrorStatus* error_status) const { + if (_availableRangeCache.has_value()) { + return _availableRangeCache.value(); + } + if (children().empty()) { - return TimeRange(); + _availableRangeCache = TimeRange(); + return _availableRangeCache.value(); } auto duration = children()[0].value->duration(error_status); @@ -130,7 +144,8 @@ Stack::available_range(ErrorStatus* error_status) const std::max(duration, children()[i].value->duration(error_status)); } - return TimeRange(RationalTime(0, duration.rate()), duration); + _availableRangeCache = TimeRange(RationalTime(0, duration.rate()), duration); + return _availableRangeCache.value(); } std::vector> diff --git a/src/opentimelineio/stack.h b/src/opentimelineio/stack.h index 3894d91ad4..312d244171 100644 --- a/src/opentimelineio/stack.h +++ b/src/opentimelineio/stack.h @@ -76,6 +76,9 @@ class Stack : public Composition bool read_from(Reader&) override; void write_to(Writer&) const override; + + mutable std::unordered_map _childRangesCacche; + mutable std::optional _availableRangeCache; }; }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/track.cpp b/src/opentimelineio/track.cpp index 3f81f27148..317fbccb9f 100644 --- a/src/opentimelineio/track.cpp +++ b/src/opentimelineio/track.cpp @@ -44,6 +44,11 @@ Track::write_to(Writer& writer) const TimeRange Track::range_of_child_at_index(int index, ErrorStatus* error_status) const { + auto it = _childRangesCacche.find(index); + if (it != _childRangesCacche.end()) { + return it->second; + } + index = adjusted_vector_index(index, children()); if (index < 0 || index >= int(children().size())) { @@ -51,6 +56,7 @@ Track::range_of_child_at_index(int index, ErrorStatus* error_status) const { *error_status = ErrorStatus::ILLEGAL_INDEX; } + _childRangesCacche[index] = TimeRange(); return TimeRange(); } @@ -58,6 +64,7 @@ Track::range_of_child_at_index(int index, ErrorStatus* error_status) const RationalTime child_duration = child->duration(error_status); if (is_error(error_status)) { + _childRangesCacche[index] = TimeRange(); return TimeRange(); } @@ -72,6 +79,7 @@ Track::range_of_child_at_index(int index, ErrorStatus* error_status) const } if (is_error(error_status)) { + _childRangesCacche[index] = TimeRange(); return TimeRange(); } } @@ -81,7 +89,9 @@ Track::range_of_child_at_index(int index, ErrorStatus* error_status) const start_time -= transition->in_offset(); } - return TimeRange(start_time, child_duration); + auto result = TimeRange(start_time, child_duration); + _childRangesCacche[index] = result; + return result; } TimeRange @@ -110,6 +120,10 @@ Track::trimmed_range_of_child_at_index(int index, ErrorStatus* error_status) TimeRange Track::available_range(ErrorStatus* error_status) const { + if (_availableRangeCache.has_value()) { + return _availableRangeCache.value(); + } + RationalTime duration; for (const auto& child: children()) { @@ -118,7 +132,8 @@ Track::available_range(ErrorStatus* error_status) const duration += item->duration(error_status); if (is_error(error_status)) { - return TimeRange(); + _availableRangeCache = TimeRange(); + return _availableRangeCache.value(); } } } @@ -137,7 +152,8 @@ Track::available_range(ErrorStatus* error_status) const } } - return TimeRange(RationalTime(0, duration.rate()), duration); + _availableRangeCache = TimeRange(RationalTime(0, duration.rate()), duration); + return _availableRangeCache.value(); } std::pair, std::optional> diff --git a/src/opentimelineio/track.h b/src/opentimelineio/track.h index 177a982a8d..64730b0382 100644 --- a/src/opentimelineio/track.h +++ b/src/opentimelineio/track.h @@ -102,6 +102,8 @@ class Track : public Composition private: std::string _kind; + mutable std::unordered_map _childRangesCacche; + mutable std::optional _availableRangeCache; }; }} // namespace opentimelineio::OPENTIMELINEIO_VERSION From a2247605bae4b5101c7b8b1ad5dbbce86dd4b938 Mon Sep 17 00:00:00 2001 From: Cesar Guirao Date: Tue, 16 Sep 2025 15:38:18 -0700 Subject: [PATCH 14/27] Erase cache when children are modified --- src/opentimelineio/composition.cpp | 3 +++ src/opentimelineio/composition.h | 3 +++ src/opentimelineio/stack.cpp | 6 ++++++ src/opentimelineio/stack.h | 3 +++ src/opentimelineio/track.cpp | 7 +++++++ src/opentimelineio/track.h | 2 ++ 6 files changed, 24 insertions(+) diff --git a/src/opentimelineio/composition.cpp b/src/opentimelineio/composition.cpp index a96744bffa..c8f4ed4e63 100644 --- a/src/opentimelineio/composition.cpp +++ b/src/opentimelineio/composition.cpp @@ -98,6 +98,7 @@ Composition::insert_child( } _child_set.insert(child); + invalidate_cache(); return true; } @@ -130,6 +131,7 @@ Composition::set_child(int index, Composable* child, ErrorStatus* error_status) child->_set_parent(this); _children[index] = child; _child_set.insert(child); + invalidate_cache(); } return true; } @@ -161,6 +163,7 @@ Composition::remove_child(int index, ErrorStatus* error_status) _children[index]->_set_parent(nullptr); _children.erase(_children.begin() + index); } + invalidate_cache(); return true; } diff --git a/src/opentimelineio/composition.h b/src/opentimelineio/composition.h index b65936ab1b..a0f5a66394 100644 --- a/src/opentimelineio/composition.h +++ b/src/opentimelineio/composition.h @@ -158,6 +158,9 @@ class Composition : public Item std::optional search_range = std::nullopt, bool shallow_search = false) const; + + virtual void invalidate_cache() const {}; + protected: virtual ~Composition(); diff --git a/src/opentimelineio/stack.cpp b/src/opentimelineio/stack.cpp index 3dfab9bd3b..e5457d0a8c 100644 --- a/src/opentimelineio/stack.cpp +++ b/src/opentimelineio/stack.cpp @@ -189,4 +189,10 @@ Stack::available_image_bounds(ErrorStatus* error_status) const return box; } +void +Stack::invalidate_cache() const +{ + _availableRangeCache = std::nullopt; + _childRangesCacche.clear(); +} }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/stack.h b/src/opentimelineio/stack.h index 312d244171..6c44368807 100644 --- a/src/opentimelineio/stack.h +++ b/src/opentimelineio/stack.h @@ -69,6 +69,9 @@ class Stack : public Composition std::optional const& search_range = std::nullopt, bool shallow_search = false) const; + + + void invalidate_cache() const override; protected: virtual ~Stack(); diff --git a/src/opentimelineio/track.cpp b/src/opentimelineio/track.cpp index 317fbccb9f..e299cf1f92 100644 --- a/src/opentimelineio/track.cpp +++ b/src/opentimelineio/track.cpp @@ -321,4 +321,11 @@ Track::available_image_bounds(ErrorStatus* error_status) const return box; } +void +Track::invalidate_cache() const +{ + _availableRangeCache = std::nullopt; + _childRangesCacche.clear(); +} + }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/track.h b/src/opentimelineio/track.h index 64730b0382..18f0ae47f7 100644 --- a/src/opentimelineio/track.h +++ b/src/opentimelineio/track.h @@ -92,6 +92,8 @@ class Track : public Composition std::optional const& search_range = std::nullopt, bool shallow_search = false) const; + void invalidate_cache() const override; + protected: virtual ~Track(); From 42fa89000a69a1340ee947b6975af7648bf63a43 Mon Sep 17 00:00:00 2001 From: Mor Arusi Date: Wed, 17 Sep 2025 12:04:37 +0300 Subject: [PATCH 15/27] make range_of_child_at_index cache lookup use the correct key --- src/opentimelineio/stack.cpp | 2 +- src/opentimelineio/track.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/opentimelineio/stack.cpp b/src/opentimelineio/stack.cpp index e5457d0a8c..26e86782f0 100644 --- a/src/opentimelineio/stack.cpp +++ b/src/opentimelineio/stack.cpp @@ -41,12 +41,12 @@ Stack::write_to(Writer& writer) const TimeRange Stack::range_of_child_at_index(int index, ErrorStatus* error_status) const { + index = adjusted_vector_index(index, children()); auto it = _childRangesCacche.find(index); if (it != _childRangesCacche.end()) { return it->second; } - index = adjusted_vector_index(index, children()); if (index < 0 || index >= int(children().size())) { if (error_status) diff --git a/src/opentimelineio/track.cpp b/src/opentimelineio/track.cpp index e299cf1f92..26e1abb39e 100644 --- a/src/opentimelineio/track.cpp +++ b/src/opentimelineio/track.cpp @@ -44,12 +44,12 @@ Track::write_to(Writer& writer) const TimeRange Track::range_of_child_at_index(int index, ErrorStatus* error_status) const { + index = adjusted_vector_index(index, children()); auto it = _childRangesCacche.find(index); if (it != _childRangesCacche.end()) { return it->second; } - index = adjusted_vector_index(index, children()); if (index < 0 || index >= int(children().size())) { if (error_status) From 255318c3de18f2b0708fb4f61eef5b5c2c9c56fd Mon Sep 17 00:00:00 2001 From: mike-dyer Date: Thu, 18 Sep 2025 02:01:07 +1000 Subject: [PATCH 16/27] Background effect schema (#7) --- .../otio-serialized-schema-only-fields.md | 10 +++ docs/tutorials/otio-serialized-schema.md | 20 ++++++ src/opentimelineio/CORE_VERSION_MAP.cpp | 1 + src/opentimelineio/deserialization.cpp | 8 +++ src/opentimelineio/serializableObject.h | 1 + src/opentimelineio/transformEffects.cpp | 36 ++++++++++ src/opentimelineio/transformEffects.h | 50 +++++++++++++ src/opentimelineio/typeRegistry.cpp | 1 + .../otio_serializableObjects.cpp | 22 ++++++ .../opentimelineio/schema/__init__.py | 7 +- tests/test_transform_effects.cpp | 59 ++++++++++++++- tests/test_transform_effects.py | 71 ++++++++++++++++++- 12 files changed, 282 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/otio-serialized-schema-only-fields.md b/docs/tutorials/otio-serialized-schema-only-fields.md index 2c411a01af..f81c586818 100644 --- a/docs/tutorials/otio-serialized-schema-only-fields.md +++ b/docs/tutorials/otio-serialized-schema-only-fields.md @@ -323,6 +323,16 @@ parameters: - *metadata* - *name* +### VideoMask.1 + +parameters: +- *effect_name* +- *enabled* +- *mask_type* +- *mask_url* +- *metadata* +- *name* + ### VideoPosition.1 parameters: diff --git a/docs/tutorials/otio-serialized-schema.md b/docs/tutorials/otio-serialized-schema.md index 8dc1d9b2fc..af39c48dad 100644 --- a/docs/tutorials/otio-serialized-schema.md +++ b/docs/tutorials/otio-serialized-schema.md @@ -752,6 +752,26 @@ parameters: - *metadata*: - *name*: +### VideoMask.1 + +*full module path*: `opentimelineio.schema.VideoMask` + +*documentation*: + +``` + +An effect that applies a mask to a video + +``` + +parameters: +- *effect_name*: +- *enabled*: If true, the Effect is applied. If false, the Effect is omitted. +- *mask_type*: +- *mask_url*: +- *metadata*: +- *name*: + ### VideoPosition.1 *full module path*: `opentimelineio.schema.VideoPosition` diff --git a/src/opentimelineio/CORE_VERSION_MAP.cpp b/src/opentimelineio/CORE_VERSION_MAP.cpp index 10919f2baf..c3b7150534 100644 --- a/src/opentimelineio/CORE_VERSION_MAP.cpp +++ b/src/opentimelineio/CORE_VERSION_MAP.cpp @@ -177,6 +177,7 @@ const label_to_schema_version_map CORE_VERSION_MAP{ { "UnknownSchema", 1 }, { "VideoCrop", 1 }, { "VideoFlip", 1 }, + { "VideoMask", 1 }, { "VideoPosition", 1 }, { "VideoRotate", 1 }, { "VideoRoundedCorners", 1 }, diff --git a/src/opentimelineio/deserialization.cpp b/src/opentimelineio/deserialization.cpp index c036b31b29..a4fb24fddd 100644 --- a/src/opentimelineio/deserialization.cpp +++ b/src/opentimelineio/deserialization.cpp @@ -800,6 +800,14 @@ SerializableObject::Reader::read( return _read_optional(key, value); } +bool +SerializableObject::Reader::read( + std::string const& key, + std::optional* value) +{ + return _read_optional(key, value); +} + bool SerializableObject::Reader::read( std::string const& key, diff --git a/src/opentimelineio/serializableObject.h b/src/opentimelineio/serializableObject.h index 9423178f30..8daae71a81 100644 --- a/src/opentimelineio/serializableObject.h +++ b/src/opentimelineio/serializableObject.h @@ -134,6 +134,7 @@ class SerializableObject bool read(std::string const& key, std::optional* dest); bool read(std::string const& key, std::optional* dest); bool read(std::string const& key, std::optional* dest); + bool read(std::string const& key, std::optional* dest); bool read(std::string const& key, std::optional* dest); bool read(std::string const& key, std::optional* dest); bool read(std::string const& key, std::optional* dest); diff --git a/src/opentimelineio/transformEffects.cpp b/src/opentimelineio/transformEffects.cpp index 71555ff164..d5b0b85ffa 100644 --- a/src/opentimelineio/transformEffects.cpp +++ b/src/opentimelineio/transformEffects.cpp @@ -79,4 +79,40 @@ void VideoFlip::write_to(Writer &writer) const { writer.write("flip_vertically", _flip_vertically); } +bool VideoMask::read_from(Reader &reader) +{ + bool result = reader.read("mask_type", &_mask_type) + && reader.read("mask_url", &_mask_url) + && reader.read_if_present("mask_replacement_url", &_mask_replacement_url) + && reader.read_if_present("blur_radius", &_blur_radius) + && Parent::read_from(reader); + + if (result) { + // Check optionals are present for the mask type + if (_mask_type == MaskType::replace) { + if (!_mask_replacement_url) { + return false; + } + } else if (_mask_type == MaskType::blur) { + if (!_blur_radius) { + return false; + } + } + } + + return result; +} + +void VideoMask::write_to(Writer &writer) const { + Parent::write_to(writer); + writer.write("mask_type", _mask_type); + writer.write("mask_url", _mask_url); + if (_mask_replacement_url) { + writer.write("mask_replacement_url", _mask_replacement_url.value()); + } + if (_blur_radius) { + writer.write("blur_radius", _blur_radius.value()); + } +} + }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/transformEffects.h b/src/opentimelineio/transformEffects.h index 0ee69c535f..86b78bc546 100644 --- a/src/opentimelineio/transformEffects.h +++ b/src/opentimelineio/transformEffects.h @@ -278,4 +278,54 @@ class VideoFlip : public Effect bool _flip_vertically; ///< Whether to flip vertically }; +class VideoMask : public Effect +{ +public: + + struct MaskType + { + static auto constexpr remove = "REMOVE"; + static auto constexpr replace = "REPLACE"; + static auto constexpr blur = "BLUR"; + }; + + struct Schema + { + static auto constexpr name = "VideoMask"; + static int constexpr version = 1; + }; + + using Parent = Effect; + + VideoMask( + std::string const& name = std::string(), + std::string const& mask_type = MaskType::remove, + std::string const& mask_url = std::string(), + AnyDictionary const& metadata = AnyDictionary(), + bool enabled = true) + : Effect(name, Schema::name, metadata, enabled) + , _mask_type(mask_type) + , _mask_url(mask_url) + {} + + std::string mask_type() const noexcept { return _mask_type; } + void set_mask_type(std::string mask_type) noexcept { _mask_type = mask_type; } + std::string mask_url() const noexcept { return _mask_url; } + void set_mask_url(std::string mask_url) noexcept { _mask_url = mask_url; } + std::optional mask_replacement_url() const noexcept { return _mask_replacement_url; } + void set_mask_replacement_url(std::optional mask_replacement_url) noexcept { _mask_replacement_url = mask_replacement_url; } + std::optional blur_radius() const noexcept { return _blur_radius; } + void set_blur_radius(std::optional blur_radius) noexcept { _blur_radius = blur_radius; } + +protected: + virtual ~VideoMask() = default; + bool read_from(Reader&) override; + void write_to(Writer&) const override; + + std::string _mask_type; + std::string _mask_url; + std::optional _mask_replacement_url; + std::optional _blur_radius; +}; + }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/typeRegistry.cpp b/src/opentimelineio/typeRegistry.cpp index e133daa433..72be1ce210 100644 --- a/src/opentimelineio/typeRegistry.cpp +++ b/src/opentimelineio/typeRegistry.cpp @@ -94,6 +94,7 @@ TypeRegistry::TypeRegistry() register_type(); register_type(); + register_type(); register_type(); register_type(); register_type(); diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp index eb7956a1a8..3630a8fd9a 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp @@ -793,6 +793,28 @@ An effect that flips video horizontally or vertically. .def_property("flip_horizontally", &VideoFlip::flip_horizontally, &VideoFlip::set_flip_horizontally) .def_property("flip_vertically", &VideoFlip::flip_vertically, &VideoFlip::set_flip_vertically); +auto video_mask_class = + py::class_>(m, "VideoMask", py::dynamic_attr(), R"docstring( +An effect that applies a mask to a video +)docstring"); +video_mask_class + .def(py::init([](std::string name, std::string const& mask_type, const std::string& mask_url, py::object metadata) { + return new VideoMask(name, mask_type, mask_url, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "mask_type"_a = VideoMask::MaskType::remove, + "mask_url"_a = std::string(), + "metadata"_a = py::none()) + .def_property("mask_type", &VideoMask::mask_type, &VideoMask::set_mask_type) + .def_property("mask_url", &VideoMask::mask_url, &VideoMask::set_mask_url) + .def_property("mask_replacement_url", &VideoMask::mask_replacement_url, &VideoMask::set_mask_replacement_url) + .def_property("blur_radius", &VideoMask::blur_radius, &VideoMask::set_blur_radius); + + py::class_(video_mask_class, "MaskType") + .def_property_readonly_static("REMOVE", [](py::object /* self */) { return VideoMask::MaskType::remove; }) + .def_property_readonly_static("REPLACE", [](py::object /* self */) { return VideoMask::MaskType::replace; }) + .def_property_readonly_static("BLUR", [](py::object /* self */) { return VideoMask::MaskType::blur; }); + py::class_>(m, "AudioVolume", py::dynamic_attr(), R"docstring( An effect that multiplies the audio volume by a given gain value )docstring") diff --git a/src/py-opentimelineio/opentimelineio/schema/__init__.py b/src/py-opentimelineio/opentimelineio/schema/__init__.py index 42052a5ea4..d8d7a87973 100644 --- a/src/py-opentimelineio/opentimelineio/schema/__init__.py +++ b/src/py-opentimelineio/opentimelineio/schema/__init__.py @@ -28,13 +28,15 @@ V2d, VideoCrop, VideoFlip, + VideoMask, VideoPosition, VideoRotate, - VideoScale, VideoRoundCorners, + VideoScale, ) MarkerColor = Marker.Color +MaskType = VideoMask.MaskType TrackKind = Track.Kind TransitionTypes = Transition.Type NeighborGapPolicy = Track.NeighborGapPolicy @@ -86,7 +88,10 @@ def timeline_from_clips(clips): 'timeline_from_clips', 'V2d', 'VideoCrop', + 'VideoFlip', + 'VideoMask', 'VideoPosition', 'VideoRotate', + 'VideoRoundCorners', 'VideoScale', ] diff --git a/tests/test_transform_effects.cpp b/tests/test_transform_effects.cpp index b8e36cc2eb..98b42d417c 100644 --- a/tests/test_transform_effects.cpp +++ b/tests/test_transform_effects.cpp @@ -86,6 +86,32 @@ main(int argc, char** argv) "flip_vertically": false, "effect_name": "VideoFlip", "enabled": true + }, + { + "OTIO_SCHEMA": "VideoMask.1", + "name": "mask", + "mask_type": "REMOVE", + "mask_url": "mask_url", + "effect_name": "VideoMaskRemove", + "enabled": true + }, + { + "OTIO_SCHEMA": "VideoMask.1", + "name": "mask", + "mask_type": "REPLACE", + "mask_url": "mask_url", + "effect_name": "VideoMaskReplace", + "mask_replacement_url": "mask_replacement_url", + "enabled": true + }, + { + "OTIO_SCHEMA": "VideoMask.1", + "name": "mask", + "mask_type": "BLUR", + "mask_url": "mask_url", + "effect_name": "VideoMaskBlur", + "blur_radius": 10.1, + "enabled": true } ] })", @@ -98,7 +124,7 @@ main(int argc, char** argv) assertNotNull(clip); auto effects = clip->effects(); - assertEqual(effects.size(), 6); + assertEqual(effects.size(), 9); auto video_scale = dynamic_cast(effects[0].value); assertNotNull(video_scale); @@ -129,6 +155,23 @@ main(int argc, char** argv) assertNotNull(video_flip); assertEqual(video_flip->flip_horizontally(), true); assertEqual(video_flip->flip_vertically(), false); + + auto video_mask_remove = dynamic_cast(effects[6].value); + assertNotNull(video_mask_remove); + assertEqual(video_mask_remove->mask_type(), std::string(VideoMask::MaskType::remove)); + assertEqual(video_mask_remove->mask_url(), std::string("mask_url")); + + auto video_mask_replace = dynamic_cast(effects[7].value); + assertNotNull(video_mask_replace); + assertEqual(video_mask_replace->mask_type(), std::string(VideoMask::MaskType::replace)); + assertEqual(video_mask_replace->mask_url(), std::string("mask_url")); + assertEqual(video_mask_replace->mask_replacement_url().value(), std::string("mask_replacement_url")); + + auto video_mask_blur = dynamic_cast(effects[8].value); + assertNotNull(video_mask_blur); + assertEqual(video_mask_blur->mask_type(), std::string(VideoMask::MaskType::blur)); + assertEqual(video_mask_blur->mask_url(), std::string("mask_url")); + assertEqual(video_mask_blur->blur_radius().value(), 10.1); }); tests.add_test("test_video_transform_write", [] { @@ -144,7 +187,9 @@ main(int argc, char** argv) new otio::VideoRotate("rotate", 40.5), new otio::VideoCrop("crop", 1, 2, 3, 4), new otio::VideoRoundedCorners("roundedCorners",80), - new otio::VideoFlip("flip", true, false)})); + new otio::VideoFlip("flip", true, false), + new otio::VideoMask("mask", otio::VideoMask::MaskType::remove, "mask_url") + })); auto json = clip.value->to_json_string(); @@ -207,6 +252,15 @@ main(int argc, char** argv) "enabled": true, "flip_horizontally": true, "flip_vertically": false + }, + { + "OTIO_SCHEMA": "VideoMask.1", + "metadata": {}, + "name": "mask", + "effect_name": "VideoMask", + "enabled": true, + "mask_type": "REMOVE", + "mask_url": "mask_url" } ], "markers": [], @@ -224,6 +278,7 @@ main(int argc, char** argv) "active_media_reference_key": "DEFAULT_MEDIA" })"; + assertEqual(json, expected_json); }); diff --git a/tests/test_transform_effects.py b/tests/test_transform_effects.py index e7d57e3c62..ae02913e7c 100644 --- a/tests/test_transform_effects.py +++ b/tests/test_transform_effects.py @@ -379,4 +379,73 @@ def test_setters(self): self.assertEqual(flip.flip_horizontally, False) self.assertEqual(flip.flip_vertically, False) flip.flip_vertically = True - self.assertEqual(flip.flip_vertically, True) \ No newline at end of file + self.assertEqual(flip.flip_vertically, True) + +class VideoMaskTests(unittest.TestCase, otio_test_utils.OTIOAssertions): + def test_constructor(self): + mask = otio.schema.VideoMask( + name="MaskIt", + mask_type=otio.schema.MaskType.REMOVE, + mask_url="mask_url", + metadata={ + "mask": "val" + } + ) + self.assertEqual(mask.mask_type, otio.schema.MaskType.REMOVE) + self.assertEqual(mask.mask_url, "mask_url") + self.assertEqual(mask.name, "MaskIt") + self.assertEqual(mask.metadata, {"mask": "val"}) + + def test_eq(self): + mask1 = otio.schema.VideoMask( + name="MaskIt", + mask_type=otio.schema.MaskType.REMOVE, + mask_url="mask_url", + metadata={ + "mask": "val" + } + ) + mask2 = otio.schema.VideoMask( + name="MaskIt", + mask_type=otio.schema.MaskType.REMOVE, + mask_url="mask_url", + metadata={ + "mask": "val" + } + ) + self.assertIsOTIOEquivalentTo(mask1, mask2) + + def test_serialize(self): + mask = otio.schema.VideoMask( + name="MaskIt", + mask_type=otio.schema.MaskType.REMOVE, + mask_url="mask_url", + metadata={ + "mask": "val" + } + ) + encoded = otio.adapters.otio_json.write_to_string(mask) + decoded = otio.adapters.otio_json.read_from_string(encoded) + self.assertIsOTIOEquivalentTo(mask, decoded) + + def test_setters(self): + mask = otio.schema.VideoMask( + name="MaskIt", + mask_type=otio.schema.MaskType.REMOVE, + mask_url="mask_url", + metadata={ + "mask": "val" + } + ) + self.assertEqual(mask.mask_type, otio.schema.MaskType.REMOVE) + mask.mask_type = otio.schema.MaskType.REPLACE + self.assertEqual(mask.mask_type, otio.schema.MaskType.REPLACE) + mask.mask_url = "mask_url_2" + self.assertEqual(mask.mask_url, "mask_url_2") + mask.mask_replacement_url = "mask_replacement_url" + self.assertEqual(mask.mask_replacement_url, "mask_replacement_url") + mask.blur_radius = 10.0 + self.assertEqual(mask.blur_radius, 10.0) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 77799268778114bbb9cb31da02490968a906aa8c Mon Sep 17 00:00:00 2001 From: Eugen Sendroiu Date: Tue, 16 Sep 2025 17:16:33 +0200 Subject: [PATCH 17/27] Add effects for color management --- src/opentimelineio/CMakeLists.txt | 2 + src/opentimelineio/CORE_VERSION_MAP.cpp | 5 + src/opentimelineio/colorManagementEffects.cpp | 60 +++++++ src/opentimelineio/colorManagementEffects.h | 158 ++++++++++++++++++ src/opentimelineio/typeRegistry.cpp | 5 + 5 files changed, 230 insertions(+) create mode 100644 src/opentimelineio/colorManagementEffects.cpp create mode 100644 src/opentimelineio/colorManagementEffects.h diff --git a/src/opentimelineio/CMakeLists.txt b/src/opentimelineio/CMakeLists.txt index 51fae81b78..3371ba5194 100644 --- a/src/opentimelineio/CMakeLists.txt +++ b/src/opentimelineio/CMakeLists.txt @@ -5,6 +5,7 @@ set(OPENTIMELINEIO_HEADER_FILES anyDictionary.h anyVector.h clip.h + colorManagementEffects.h composable.h composition.h deserialization.h @@ -42,6 +43,7 @@ set(OPENTIMELINEIO_HEADER_FILES add_library(opentimelineio ${OTIO_SHARED_OR_STATIC_LIB} clip.cpp + colorManagementEffects.cpp composable.cpp composition.cpp deserialization.cpp diff --git a/src/opentimelineio/CORE_VERSION_MAP.cpp b/src/opentimelineio/CORE_VERSION_MAP.cpp index c3b7150534..2a2549e00c 100644 --- a/src/opentimelineio/CORE_VERSION_MAP.cpp +++ b/src/opentimelineio/CORE_VERSION_MAP.cpp @@ -175,12 +175,17 @@ const label_to_schema_version_map CORE_VERSION_MAP{ { "Track", 1 }, { "Transition", 1 }, { "UnknownSchema", 1 }, + { "VideoBrightness", 1 }, + { "VideoContrast", 1 }, + { "VideoContrast", 1 }, + { "VideoColorTemperature", 1 }, { "VideoCrop", 1 }, { "VideoFlip", 1 }, { "VideoMask", 1 }, { "VideoPosition", 1 }, { "VideoRotate", 1 }, { "VideoRoundedCorners", 1 }, + { "VideoSaturation", 1 }, { "VideoScale", 1 }, } }, // {next} diff --git a/src/opentimelineio/colorManagementEffects.cpp b/src/opentimelineio/colorManagementEffects.cpp new file mode 100644 index 0000000000..642894208b --- /dev/null +++ b/src/opentimelineio/colorManagementEffects.cpp @@ -0,0 +1,60 @@ +#include "opentimelineio/colorManagementEffects.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { + +bool VideoBrightness::read_from(Reader &reader) +{ + return reader.read("brightness", &_brightness) + && Parent::read_from(reader); +} + +void VideoBrightness::write_to(Writer &writer) const { + Parent::write_to(writer); + writer.write("brightness", _brightness); +} + +bool VideoContrast::read_from(Reader &reader) +{ + return reader.read("contrast", &_contrast) + && Parent::read_from(reader); +} + +void VideoContrast::write_to(Writer &writer) const { + Parent::write_to(writer); + writer.write("contrast", _contrast); +} + +bool VideoSaturation::read_from(Reader &reader) +{ + return reader.read("saturation", &_saturation) + && Parent::read_from(reader); +} + +void VideoSaturation::write_to(Writer &writer) const { + Parent::write_to(writer); + writer.write("saturation", _saturation); +} + +bool VideoLightness::read_from(Reader &reader) +{ + return reader.read("lightness", &_lightness) + && Parent::read_from(reader); +} + +void VideoLightness::write_to(Writer &writer) const { + Parent::write_to(writer); + writer.write("lightness", _lightness); +} + +bool VideoColorTemperature::read_from(Reader &reader) +{ + return reader.read("temperature", &_temperature) + && Parent::read_from(reader); +} + +void VideoColorTemperature::write_to(Writer &writer) const { + Parent::write_to(writer); + writer.write("temperature", _temperature); +} + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/colorManagementEffects.h b/src/opentimelineio/colorManagementEffects.h new file mode 100644 index 0000000000..15c8a97454 --- /dev/null +++ b/src/opentimelineio/colorManagementEffects.h @@ -0,0 +1,158 @@ +#pragma once + +#include "opentimelineio/effect.h" +#include "opentimelineio/version.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { + +/// @brief A brightness effect +class VideoBrightness : public Effect +{ +public: + struct Schema { + static auto constexpr name = "VideoBrightness"; + static int constexpr version = 1; + }; + using Parent = Effect; + + VideoBrightness( + std::string const& name = std::string(), + int64_t brightness = 0, + AnyDictionary const& metadata = AnyDictionary(), + bool enabled = true) + : Effect(name, Schema::name, metadata, enabled) + , _brightness(brightness) + {} + + int64_t brightness() const noexcept { return _brightness; } + void set_brightness(int64_t brightness) noexcept { _brightness = brightness; } + +protected: + virtual ~VideoBrightness() = default; + bool read_from(Reader&) override; + void write_to(Writer&) const override; + + int64_t _brightness; +}; + +/// @brief A contrast effect +class VideoContrast : public Effect +{ +public: + struct Schema { + static auto constexpr name = "VideoContrast"; + static int constexpr version = 1; + }; + using Parent = Effect; + + VideoContrast( + std::string const& name = std::string(), + int64_t contrast = 0, + AnyDictionary const& metadata = AnyDictionary(), + bool enabled = true) + : Effect(name, Schema::name, metadata, enabled) + , _contrast(contrast) + {} + + int64_t contrast() const noexcept { return _contrast; } + void set_contrast(int64_t contrast) noexcept { _contrast = contrast; } + +protected: + virtual ~VideoContrast() = default; + bool read_from(Reader&) override; + void write_to(Writer&) const override; + + int64_t _contrast; +}; + +/// @brief A saturation effect +class VideoSaturation : public Effect +{ +public: + struct Schema { + static auto constexpr name = "VideoSaturation"; + static int constexpr version = 1; + }; + using Parent = Effect; + + VideoSaturation( + std::string const& name = std::string(), + int64_t saturation = 0, + AnyDictionary const& metadata = AnyDictionary(), + bool enabled = true) + : Effect(name, Schema::name, metadata, enabled) + , _saturation(saturation) + {} + + int64_t saturation() const noexcept { return _saturation; } + void set_saturation(int64_t saturation) noexcept { _saturation = saturation; } + +protected: + virtual ~VideoSaturation() = default; + bool read_from(Reader&) override; + void write_to(Writer&) const override; + + int64_t _saturation; +}; + +/// @brief A lightness effect +class VideoLightness : public Effect +{ +public: + struct Schema { + static auto constexpr name = "VideoLightness"; + static int constexpr version = 1; + }; + using Parent = Effect; + + VideoLightness( + std::string const& name = std::string(), + int64_t lightness = 0, + AnyDictionary const& metadata = AnyDictionary(), + bool enabled = true) + : Effect(name, Schema::name, metadata, enabled) + , _lightness(lightness) + {} + + int64_t lightness() const noexcept { return _lightness; } + void set_lightness(int64_t lightness) noexcept { _lightness = lightness; } + +protected: + virtual ~VideoLightness() = default; + bool read_from(Reader&) override; + void write_to(Writer&) const override; + + int64_t _lightness; +}; + +/// @brief A color temperature effect +class VideoColorTemperature : public Effect +{ +public: + struct Schema { + static auto constexpr name = "VideoColorTemperature"; + static int constexpr version = 1; + }; + using Parent = Effect; + + VideoColorTemperature( + std::string const& name = std::string(), + int64_t temperature = 0, + AnyDictionary const& metadata = AnyDictionary(), + bool enabled = true) + : Effect(name, Schema::name, metadata, enabled) + , _temperature(temperature) + {} + + int64_t temperature() const noexcept { return _temperature; } + void set_temperature(int64_t temperature) noexcept { _temperature = temperature; } + +protected: + virtual ~VideoColorTemperature() = default; + bool read_from(Reader&) override; + void write_to(Writer&) const override; + + int64_t _temperature; +}; + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/typeRegistry.cpp b/src/opentimelineio/typeRegistry.cpp index 72be1ce210..d053ff393d 100644 --- a/src/opentimelineio/typeRegistry.cpp +++ b/src/opentimelineio/typeRegistry.cpp @@ -5,6 +5,7 @@ #include "anyDictionary.h" #include "opentimelineio/clip.h" +#include "opentimelineio/colorManagementEffects.h" #include "opentimelineio/composable.h" #include "opentimelineio/composition.h" #include "opentimelineio/effect.h" @@ -92,9 +93,13 @@ TypeRegistry::TypeRegistry() register_type(); register_type_from_existing_type("Sequence", 1, "Track", nullptr); + register_type(); + register_type(); + register_type(); register_type(); register_type(); register_type(); + register_type(); register_type(); register_type(); register_type(); From 0bfc17a71ebfdd0d662f51288ff3d206389802fd Mon Sep 17 00:00:00 2001 From: Eugen Sendroiu Date: Tue, 16 Sep 2025 20:32:39 +0200 Subject: [PATCH 18/27] Add tests --- src/opentimelineio/CORE_VERSION_MAP.cpp | 3 +- .../otio_serializableObjects.cpp | 58 ++++++ .../opentimelineio/schema/__init__.py | 11 ++ tests/CMakeLists.txt | 2 +- tests/test_color_management_effects.cpp | 180 ++++++++++++++++++ tests/test_color_management_effects.py | 145 ++++++++++++++ 6 files changed, 396 insertions(+), 3 deletions(-) create mode 100644 tests/test_color_management_effects.cpp create mode 100644 tests/test_color_management_effects.py diff --git a/src/opentimelineio/CORE_VERSION_MAP.cpp b/src/opentimelineio/CORE_VERSION_MAP.cpp index 2a2549e00c..5ba4917dcb 100644 --- a/src/opentimelineio/CORE_VERSION_MAP.cpp +++ b/src/opentimelineio/CORE_VERSION_MAP.cpp @@ -176,9 +176,8 @@ const label_to_schema_version_map CORE_VERSION_MAP{ { "Transition", 1 }, { "UnknownSchema", 1 }, { "VideoBrightness", 1 }, - { "VideoContrast", 1 }, - { "VideoContrast", 1 }, { "VideoColorTemperature", 1 }, + { "VideoContrast", 1 }, { "VideoCrop", 1 }, { "VideoFlip", 1 }, { "VideoMask", 1 }, diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp index 3630a8fd9a..ccb1568bc9 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp @@ -25,6 +25,7 @@ #include "opentimelineio/timeline.h" #include "opentimelineio/track.h" #include "opentimelineio/transformEffects.h" +#include "opentimelineio/colorManagementEffects.h" #include "opentimelineio/transition.h" #include "opentimelineio/serializableCollection.h" #include "opentimelineio/stack.h" @@ -666,6 +667,63 @@ Should be subclassed (for example by :class:`.Track` and :class:`.Stack`), not u } static void define_effects(py::module m) { + + // Color Management Effects + py::class_>(m, "VideoBrightness", py::dynamic_attr(), R"docstring( +An effect that adjusts video brightness. +)docstring") + .def(py::init([](std::string name, int64_t brightness, py::object metadata) { + return new VideoBrightness(name, brightness, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "brightness"_a = 0, + "metadata"_a = py::none()) + .def_property("brightness", &VideoBrightness::brightness, &VideoBrightness::set_brightness, "Brightness value"); + + py::class_>(m, "VideoContrast", py::dynamic_attr(), R"docstring( +An effect that adjusts video contrast. +)docstring") + .def(py::init([](std::string name, int64_t contrast, py::object metadata) { + return new VideoContrast(name, contrast, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "contrast"_a = 0, + "metadata"_a = py::none()) + .def_property("contrast", &VideoContrast::contrast, &VideoContrast::set_contrast, "Contrast value"); + + py::class_>(m, "VideoSaturation", py::dynamic_attr(), R"docstring( +An effect that adjusts video saturation. +)docstring") + .def(py::init([](std::string name, int64_t saturation, py::object metadata) { + return new VideoSaturation(name, saturation, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "saturation"_a = 0, + "metadata"_a = py::none()) + .def_property("saturation", &VideoSaturation::saturation, &VideoSaturation::set_saturation, "Saturation value"); + + py::class_>(m, "VideoLightness", py::dynamic_attr(), R"docstring( +An effect that adjusts video lightness. +)docstring") + .def(py::init([](std::string name, int64_t lightness, py::object metadata) { + return new VideoLightness(name, lightness, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "lightness"_a = 0, + "metadata"_a = py::none()) + .def_property("lightness", &VideoLightness::lightness, &VideoLightness::set_lightness, "Lightness value"); + + py::class_>(m, "VideoColorTemperature", py::dynamic_attr(), R"docstring( +An effect that adjusts video color temperature. +)docstring") + .def(py::init([](std::string name, int64_t temperature, py::object metadata) { + return new VideoColorTemperature(name, temperature, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "temperature"_a = 0, + "metadata"_a = py::none()) + .def_property("temperature", &VideoColorTemperature::temperature, &VideoColorTemperature::set_temperature, "Color temperature value"); + py::class_>(m, "Effect", py::dynamic_attr()) .def(py::init([](std::string name, std::string effect_name, diff --git a/src/py-opentimelineio/opentimelineio/schema/__init__.py b/src/py-opentimelineio/opentimelineio/schema/__init__.py index d8d7a87973..551599a8f0 100644 --- a/src/py-opentimelineio/opentimelineio/schema/__init__.py +++ b/src/py-opentimelineio/opentimelineio/schema/__init__.py @@ -33,6 +33,11 @@ VideoRotate, VideoRoundCorners, VideoScale, + VideoBrightness, + VideoContrast, + VideoSaturation, + VideoLightness, + VideoColorTemperature, ) MarkerColor = Marker.Color @@ -94,4 +99,10 @@ def timeline_from_clips(clips): 'VideoRotate', 'VideoRoundCorners', 'VideoScale', + 'VideoRoundCorners', + 'VideoBrightness', + 'VideoContrast', + 'VideoSaturation', + 'VideoLightness', + 'VideoColorTemperature', ] diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d07c079b2f..3eeda461d3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,7 +15,7 @@ foreach(test ${tests_opentime}) WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) endforeach() -list(APPEND tests_opentimelineio test_clip test_serialization test_serializableCollection test_stack_algo test_timeline test_track test_editAlgorithm test_transform_effects test_volume_effects) +list(APPEND tests_opentimelineio test_clip test_serialization test_serializableCollection test_stack_algo test_timeline test_track test_editAlgorithm test_transform_effects test_volume_effects test_color_management_effects) foreach(test ${tests_opentimelineio}) add_executable(${test} utils.h utils.cpp ${test}.cpp) diff --git a/tests/test_color_management_effects.cpp b/tests/test_color_management_effects.cpp new file mode 100644 index 0000000000..e6d50bbb04 --- /dev/null +++ b/tests/test_color_management_effects.cpp @@ -0,0 +1,180 @@ +#include "utils.h" + +#include +#include +#include + +namespace otime = opentime::OPENTIME_VERSION; +namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; + +int +main(int argc, char** argv) +{ + Tests tests; + tests.add_test("test_color_management_effects_read", [] { + using namespace otio; + + otio::ErrorStatus status; + SerializableObject::Retainer<> so = + SerializableObject::from_json_string( + R"( + { + "OTIO_SCHEMA": "Clip.1", + "media_reference": { + "OTIO_SCHEMA": "ExternalReference.1", + "target_url": "unit_test_url" + }, + "effects": [ + { + "OTIO_SCHEMA": "VideoBrightness.1", + "name": "brightness", + "brightness": 50, + "effect_name": "VideoBrightness", + "enabled": true + }, + { + "OTIO_SCHEMA": "VideoContrast.1", + "name": "contrast", + "contrast": 20, + "effect_name": "VideoContrast", + "enabled": true + }, + { + "OTIO_SCHEMA": "VideoSaturation.1", + "name": "saturation", + "saturation": 70, + "effect_name": "VideoSaturation", + "enabled": true + }, + { + "OTIO_SCHEMA": "VideoLightness.1", + "name": "lightness", + "lightness": 10, + "effect_name": "VideoLightness", + "enabled": true + }, + { + "OTIO_SCHEMA": "VideoColorTemperature.1", + "name": "temperature", + "temperature": 6500, + "effect_name": "VideoColorTemperature", + "enabled": true + } + ] + })", + &status); + + if (is_error(status)) + throw std::invalid_argument(status.details); + + const Clip* clip = dynamic_cast(so.value); + assertNotNull(clip); + + auto effects = clip->effects(); + assertEqual(effects.size(), 5); + + auto video_brightness = dynamic_cast(effects[0].value); + assertNotNull(video_brightness); + assertEqual(video_brightness->brightness(), 50); + + auto video_contrast = dynamic_cast(effects[1].value); + assertNotNull(video_contrast); + assertEqual(video_contrast->contrast(), 20); + + auto video_saturation = dynamic_cast(effects[2].value); + assertNotNull(video_saturation); + assertEqual(video_saturation->saturation(), 70); + + auto video_lightness = dynamic_cast(effects[3].value); + assertNotNull(video_lightness); + assertEqual(video_lightness->lightness(), 10); + + auto video_temperature = dynamic_cast(effects[4].value); + assertNotNull(video_temperature); + assertEqual(video_temperature->temperature(), 6500); + }); + + tests.add_test("test_color_management_effects_write", [] { + using namespace otio; + + SerializableObject::Retainer clip(new otio::Clip( + "unit_clip", + new otio::ExternalReference("unit_test_url"), + std::nullopt, + otio::AnyDictionary(), + { new otio::VideoBrightness("brightness", 50), + new otio::VideoContrast("contrast", 20), + new otio::VideoSaturation("saturation", 70), + new otio::VideoLightness("lightness", 10), + new otio::VideoColorTemperature("temperature", 6500)})); + + auto json = clip.value->to_json_string(); + + std::string expected_json = R"({ + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "unit_clip", + "source_range": null, + "effects": [ + { + "OTIO_SCHEMA": "VideoBrightness.1", + "metadata": {}, + "name": "brightness", + "effect_name": "VideoBrightness", + "enabled": true, + "brightness": 50 + }, + { + "OTIO_SCHEMA": "VideoContrast.1", + "metadata": {}, + "name": "contrast", + "effect_name": "VideoContrast", + "enabled": true, + "contrast": 20 + }, + { + "OTIO_SCHEMA": "VideoSaturation.1", + "metadata": {}, + "name": "saturation", + "effect_name": "VideoSaturation", + "enabled": true, + "saturation": 70 + }, + { + "OTIO_SCHEMA": "VideoLightness.1", + "metadata": {}, + "name": "lightness", + "effect_name": "VideoLightness", + "enabled": true, + "lightness": 10 + }, + { + "OTIO_SCHEMA": "VideoColorTemperature.1", + "metadata": {}, + "name": "temperature", + "effect_name": "VideoColorTemperature", + "enabled": true, + "temperature": 6500 + } + ], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": {}, + "name": "", + "available_range": null, + "available_image_bounds": null, + "target_url": "unit_test_url" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +})"; + + assertEqual(json, expected_json); + }); + + tests.run(argc, argv); + return 0; +} diff --git a/tests/test_color_management_effects.py b/tests/test_color_management_effects.py new file mode 100644 index 0000000000..235ccf1901 --- /dev/null +++ b/tests/test_color_management_effects.py @@ -0,0 +1,145 @@ +"""Color management effects class test harness.""" + +import unittest +import opentimelineio as otio +import opentimelineio.test_utils as otio_test_utils + +class VideoBrightnessTests(unittest.TestCase, otio_test_utils.OTIOAssertions): + def test_constructor(self): + brightness = otio.schema.VideoBrightness( + name="BrightIt", + brightness=50, + metadata={"foo": "bar"} + ) + self.assertEqual(brightness.brightness, 50) + self.assertEqual(brightness.name, "BrightIt") + self.assertEqual(brightness.metadata, {"foo": "bar"}) + + def test_eq(self): + b1 = otio.schema.VideoBrightness(name="BrightIt", brightness=50, metadata={"foo": "bar"}) + b2 = otio.schema.VideoBrightness(name="BrightIt", brightness=50, metadata={"foo": "bar"}) + self.assertIsOTIOEquivalentTo(b1, b2) + + def test_serialize(self): + brightness = otio.schema.VideoBrightness(name="BrightIt", brightness=50, metadata={"foo": "bar"}) + encoded = otio.adapters.otio_json.write_to_string(brightness) + decoded = otio.adapters.otio_json.read_from_string(encoded) + self.assertIsOTIOEquivalentTo(brightness, decoded) + + def test_setters(self): + brightness = otio.schema.VideoBrightness(name="BrightIt", brightness=50, metadata={"foo": "bar"}) + self.assertEqual(brightness.brightness, 50) + brightness.brightness = 100 + self.assertEqual(brightness.brightness, 100) + +class VideoContrastTests(unittest.TestCase, otio_test_utils.OTIOAssertions): + def test_constructor(self): + contrast = otio.schema.VideoContrast( + name="ContrastIt", + contrast=20, + metadata={"foo": "bar"} + ) + self.assertEqual(contrast.contrast, 20) + self.assertEqual(contrast.name, "ContrastIt") + self.assertEqual(contrast.metadata, {"foo": "bar"}) + + def test_eq(self): + c1 = otio.schema.VideoContrast(name="ContrastIt", contrast=20, metadata={"foo": "bar"}) + c2 = otio.schema.VideoContrast(name="ContrastIt", contrast=20, metadata={"foo": "bar"}) + self.assertIsOTIOEquivalentTo(c1, c2) + + def test_serialize(self): + contrast = otio.schema.VideoContrast(name="ContrastIt", contrast=20, metadata={"foo": "bar"}) + encoded = otio.adapters.otio_json.write_to_string(contrast) + decoded = otio.adapters.otio_json.read_from_string(encoded) + self.assertIsOTIOEquivalentTo(contrast, decoded) + + def test_setters(self): + contrast = otio.schema.VideoContrast(name="ContrastIt", contrast=20, metadata={"foo": "bar"}) + self.assertEqual(contrast.contrast, 20) + contrast.contrast = 40 + self.assertEqual(contrast.contrast, 40) + +class VideoSaturationTests(unittest.TestCase, otio_test_utils.OTIOAssertions): + def test_constructor(self): + saturation = otio.schema.VideoSaturation( + name="SaturateIt", + saturation=70, + metadata={"foo": "bar"} + ) + self.assertEqual(saturation.saturation, 70) + self.assertEqual(saturation.name, "SaturateIt") + self.assertEqual(saturation.metadata, {"foo": "bar"}) + + def test_eq(self): + s1 = otio.schema.VideoSaturation(name="SaturateIt", saturation=70, metadata={"foo": "bar"}) + s2 = otio.schema.VideoSaturation(name="SaturateIt", saturation=70, metadata={"foo": "bar"}) + self.assertIsOTIOEquivalentTo(s1, s2) + + def test_serialize(self): + saturation = otio.schema.VideoSaturation(name="SaturateIt", saturation=70, metadata={"foo": "bar"}) + encoded = otio.adapters.otio_json.write_to_string(saturation) + decoded = otio.adapters.otio_json.read_from_string(encoded) + self.assertIsOTIOEquivalentTo(saturation, decoded) + + def test_setters(self): + saturation = otio.schema.VideoSaturation(name="SaturateIt", saturation=70, metadata={"foo": "bar"}) + self.assertEqual(saturation.saturation, 70) + saturation.saturation = 100 + self.assertEqual(saturation.saturation, 100) + +class VideoLightnessTests(unittest.TestCase, otio_test_utils.OTIOAssertions): + def test_constructor(self): + lightness = otio.schema.VideoLightness( + name="LightIt", + lightness=10, + metadata={"foo": "bar"} + ) + self.assertEqual(lightness.lightness, 10) + self.assertEqual(lightness.name, "LightIt") + self.assertEqual(lightness.metadata, {"foo": "bar"}) + + def test_eq(self): + l1 = otio.schema.VideoLightness(name="LightIt", lightness=10, metadata={"foo": "bar"}) + l2 = otio.schema.VideoLightness(name="LightIt", lightness=10, metadata={"foo": "bar"}) + self.assertIsOTIOEquivalentTo(l1, l2) + + def test_serialize(self): + lightness = otio.schema.VideoLightness(name="LightIt", lightness=10, metadata={"foo": "bar"}) + encoded = otio.adapters.otio_json.write_to_string(lightness) + decoded = otio.adapters.otio_json.read_from_string(encoded) + self.assertIsOTIOEquivalentTo(lightness, decoded) + + def test_setters(self): + lightness = otio.schema.VideoLightness(name="LightIt", lightness=10, metadata={"foo": "bar"}) + self.assertEqual(lightness.lightness, 10) + lightness.lightness = 20 + self.assertEqual(lightness.lightness, 20) + +class VideoColorTemperatureTests(unittest.TestCase, otio_test_utils.OTIOAssertions): + def test_constructor(self): + temp = otio.schema.VideoColorTemperature( + name="TempIt", + temperature=6500, + metadata={"foo": "bar"} + ) + self.assertEqual(temp.temperature, 6500) + self.assertEqual(temp.name, "TempIt") + self.assertEqual(temp.metadata, {"foo": "bar"}) + + def test_eq(self): + t1 = otio.schema.VideoColorTemperature(name="TempIt", temperature=6500, metadata={"foo": "bar"}) + t2 = otio.schema.VideoColorTemperature(name="TempIt", temperature=6500, metadata={"foo": "bar"}) + self.assertIsOTIOEquivalentTo(t1, t2) + + def test_serialize(self): + temp = otio.schema.VideoColorTemperature(name="TempIt", temperature=6500, metadata={"foo": "bar"}) + encoded = otio.adapters.otio_json.write_to_string(temp) + decoded = otio.adapters.otio_json.read_from_string(encoded) + self.assertIsOTIOEquivalentTo(temp, decoded) + + def test_setters(self): + temp = otio.schema.VideoColorTemperature(name="TempIt", temperature=6500, metadata={"foo": "bar"}) + self.assertEqual(temp.temperature, 6500) + temp.temperature = 7000 + self.assertEqual(temp.temperature, 7000) From 21f247b7014952675b7f59cc9d17c2ef7c2f79e8 Mon Sep 17 00:00:00 2001 From: Eugen Sendroiu Date: Tue, 16 Sep 2025 20:56:22 +0200 Subject: [PATCH 19/27] Fix test compilation --- tests/test_color_management_effects.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_color_management_effects.cpp b/tests/test_color_management_effects.cpp index e6d50bbb04..a6e21cd8ff 100644 --- a/tests/test_color_management_effects.cpp +++ b/tests/test_color_management_effects.cpp @@ -2,6 +2,7 @@ #include #include +#include #include namespace otime = opentime::OPENTIME_VERSION; From 944a33c14914552b079d1a3bdd4bb81bcdffefa9 Mon Sep 17 00:00:00 2001 From: Eugen Sendroiu Date: Wed, 17 Sep 2025 10:03:44 +0200 Subject: [PATCH 20/27] Add VideoLightness to schema. Fix tests. --- src/opentimelineio/CORE_VERSION_MAP.cpp | 1 + src/opentimelineio/typeRegistry.cpp | 3 +++ tests/test_color_management_effects.cpp | 15 ++++++++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/opentimelineio/CORE_VERSION_MAP.cpp b/src/opentimelineio/CORE_VERSION_MAP.cpp index 5ba4917dcb..ac2db48ae1 100644 --- a/src/opentimelineio/CORE_VERSION_MAP.cpp +++ b/src/opentimelineio/CORE_VERSION_MAP.cpp @@ -181,6 +181,7 @@ const label_to_schema_version_map CORE_VERSION_MAP{ { "VideoCrop", 1 }, { "VideoFlip", 1 }, { "VideoMask", 1 }, + { "VideoLightness", 1 }, { "VideoPosition", 1 }, { "VideoRotate", 1 }, { "VideoRoundedCorners", 1 }, diff --git a/src/opentimelineio/typeRegistry.cpp b/src/opentimelineio/typeRegistry.cpp index d053ff393d..8d38d6a90d 100644 --- a/src/opentimelineio/typeRegistry.cpp +++ b/src/opentimelineio/typeRegistry.cpp @@ -101,9 +101,12 @@ TypeRegistry::TypeRegistry() register_type(); register_type(); register_type(); + register_type(); register_type(); register_type(); register_type(); + register_type(); + register_type(); register_type(); diff --git a/tests/test_color_management_effects.cpp b/tests/test_color_management_effects.cpp index a6e21cd8ff..e43ba1ecbe 100644 --- a/tests/test_color_management_effects.cpp +++ b/tests/test_color_management_effects.cpp @@ -23,7 +23,20 @@ main(int argc, char** argv) "OTIO_SCHEMA": "Clip.1", "media_reference": { "OTIO_SCHEMA": "ExternalReference.1", - "target_url": "unit_test_url" + "target_url": "unit_test_url", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 8 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 10 + } + } }, "effects": [ { From db5648ce762a0cca264ae3eceddacf9d97495435 Mon Sep 17 00:00:00 2001 From: Eugen Sendroiu Date: Wed, 17 Sep 2025 10:09:43 +0200 Subject: [PATCH 21/27] Revert python changes --- .../otio_serializableObjects.cpp | 58 ------------------- .../opentimelineio/schema/__init__.py | 12 ---- 2 files changed, 70 deletions(-) diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp index ccb1568bc9..3630a8fd9a 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp @@ -25,7 +25,6 @@ #include "opentimelineio/timeline.h" #include "opentimelineio/track.h" #include "opentimelineio/transformEffects.h" -#include "opentimelineio/colorManagementEffects.h" #include "opentimelineio/transition.h" #include "opentimelineio/serializableCollection.h" #include "opentimelineio/stack.h" @@ -667,63 +666,6 @@ Should be subclassed (for example by :class:`.Track` and :class:`.Stack`), not u } static void define_effects(py::module m) { - - // Color Management Effects - py::class_>(m, "VideoBrightness", py::dynamic_attr(), R"docstring( -An effect that adjusts video brightness. -)docstring") - .def(py::init([](std::string name, int64_t brightness, py::object metadata) { - return new VideoBrightness(name, brightness, py_to_any_dictionary(metadata)); - }), - "name"_a = std::string(), - "brightness"_a = 0, - "metadata"_a = py::none()) - .def_property("brightness", &VideoBrightness::brightness, &VideoBrightness::set_brightness, "Brightness value"); - - py::class_>(m, "VideoContrast", py::dynamic_attr(), R"docstring( -An effect that adjusts video contrast. -)docstring") - .def(py::init([](std::string name, int64_t contrast, py::object metadata) { - return new VideoContrast(name, contrast, py_to_any_dictionary(metadata)); - }), - "name"_a = std::string(), - "contrast"_a = 0, - "metadata"_a = py::none()) - .def_property("contrast", &VideoContrast::contrast, &VideoContrast::set_contrast, "Contrast value"); - - py::class_>(m, "VideoSaturation", py::dynamic_attr(), R"docstring( -An effect that adjusts video saturation. -)docstring") - .def(py::init([](std::string name, int64_t saturation, py::object metadata) { - return new VideoSaturation(name, saturation, py_to_any_dictionary(metadata)); - }), - "name"_a = std::string(), - "saturation"_a = 0, - "metadata"_a = py::none()) - .def_property("saturation", &VideoSaturation::saturation, &VideoSaturation::set_saturation, "Saturation value"); - - py::class_>(m, "VideoLightness", py::dynamic_attr(), R"docstring( -An effect that adjusts video lightness. -)docstring") - .def(py::init([](std::string name, int64_t lightness, py::object metadata) { - return new VideoLightness(name, lightness, py_to_any_dictionary(metadata)); - }), - "name"_a = std::string(), - "lightness"_a = 0, - "metadata"_a = py::none()) - .def_property("lightness", &VideoLightness::lightness, &VideoLightness::set_lightness, "Lightness value"); - - py::class_>(m, "VideoColorTemperature", py::dynamic_attr(), R"docstring( -An effect that adjusts video color temperature. -)docstring") - .def(py::init([](std::string name, int64_t temperature, py::object metadata) { - return new VideoColorTemperature(name, temperature, py_to_any_dictionary(metadata)); - }), - "name"_a = std::string(), - "temperature"_a = 0, - "metadata"_a = py::none()) - .def_property("temperature", &VideoColorTemperature::temperature, &VideoColorTemperature::set_temperature, "Color temperature value"); - py::class_>(m, "Effect", py::dynamic_attr()) .def(py::init([](std::string name, std::string effect_name, diff --git a/src/py-opentimelineio/opentimelineio/schema/__init__.py b/src/py-opentimelineio/opentimelineio/schema/__init__.py index 551599a8f0..aa3ea04abb 100644 --- a/src/py-opentimelineio/opentimelineio/schema/__init__.py +++ b/src/py-opentimelineio/opentimelineio/schema/__init__.py @@ -32,12 +32,6 @@ VideoPosition, VideoRotate, VideoRoundCorners, - VideoScale, - VideoBrightness, - VideoContrast, - VideoSaturation, - VideoLightness, - VideoColorTemperature, ) MarkerColor = Marker.Color @@ -99,10 +93,4 @@ def timeline_from_clips(clips): 'VideoRotate', 'VideoRoundCorners', 'VideoScale', - 'VideoRoundCorners', - 'VideoBrightness', - 'VideoContrast', - 'VideoSaturation', - 'VideoLightness', - 'VideoColorTemperature', ] From 078231e1d17b95b1828bd1e4e11554660c1b9917 Mon Sep 17 00:00:00 2001 From: Eugen Sendroiu Date: Wed, 17 Sep 2025 11:04:42 +0200 Subject: [PATCH 22/27] Revert "Revert python changes" This reverts commit b0efcde07f517b229021f5699d3b952c1beffdec. --- .../otio_serializableObjects.cpp | 58 +++++++++++++++++++ .../opentimelineio/schema/__init__.py | 11 ++++ 2 files changed, 69 insertions(+) diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp index 3630a8fd9a..ccb1568bc9 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp @@ -25,6 +25,7 @@ #include "opentimelineio/timeline.h" #include "opentimelineio/track.h" #include "opentimelineio/transformEffects.h" +#include "opentimelineio/colorManagementEffects.h" #include "opentimelineio/transition.h" #include "opentimelineio/serializableCollection.h" #include "opentimelineio/stack.h" @@ -666,6 +667,63 @@ Should be subclassed (for example by :class:`.Track` and :class:`.Stack`), not u } static void define_effects(py::module m) { + + // Color Management Effects + py::class_>(m, "VideoBrightness", py::dynamic_attr(), R"docstring( +An effect that adjusts video brightness. +)docstring") + .def(py::init([](std::string name, int64_t brightness, py::object metadata) { + return new VideoBrightness(name, brightness, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "brightness"_a = 0, + "metadata"_a = py::none()) + .def_property("brightness", &VideoBrightness::brightness, &VideoBrightness::set_brightness, "Brightness value"); + + py::class_>(m, "VideoContrast", py::dynamic_attr(), R"docstring( +An effect that adjusts video contrast. +)docstring") + .def(py::init([](std::string name, int64_t contrast, py::object metadata) { + return new VideoContrast(name, contrast, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "contrast"_a = 0, + "metadata"_a = py::none()) + .def_property("contrast", &VideoContrast::contrast, &VideoContrast::set_contrast, "Contrast value"); + + py::class_>(m, "VideoSaturation", py::dynamic_attr(), R"docstring( +An effect that adjusts video saturation. +)docstring") + .def(py::init([](std::string name, int64_t saturation, py::object metadata) { + return new VideoSaturation(name, saturation, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "saturation"_a = 0, + "metadata"_a = py::none()) + .def_property("saturation", &VideoSaturation::saturation, &VideoSaturation::set_saturation, "Saturation value"); + + py::class_>(m, "VideoLightness", py::dynamic_attr(), R"docstring( +An effect that adjusts video lightness. +)docstring") + .def(py::init([](std::string name, int64_t lightness, py::object metadata) { + return new VideoLightness(name, lightness, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "lightness"_a = 0, + "metadata"_a = py::none()) + .def_property("lightness", &VideoLightness::lightness, &VideoLightness::set_lightness, "Lightness value"); + + py::class_>(m, "VideoColorTemperature", py::dynamic_attr(), R"docstring( +An effect that adjusts video color temperature. +)docstring") + .def(py::init([](std::string name, int64_t temperature, py::object metadata) { + return new VideoColorTemperature(name, temperature, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "temperature"_a = 0, + "metadata"_a = py::none()) + .def_property("temperature", &VideoColorTemperature::temperature, &VideoColorTemperature::set_temperature, "Color temperature value"); + py::class_>(m, "Effect", py::dynamic_attr()) .def(py::init([](std::string name, std::string effect_name, diff --git a/src/py-opentimelineio/opentimelineio/schema/__init__.py b/src/py-opentimelineio/opentimelineio/schema/__init__.py index aa3ea04abb..1543822ee6 100644 --- a/src/py-opentimelineio/opentimelineio/schema/__init__.py +++ b/src/py-opentimelineio/opentimelineio/schema/__init__.py @@ -32,6 +32,11 @@ VideoPosition, VideoRotate, VideoRoundCorners, + VideoBrightness, + VideoContrast, + VideoSaturation, + VideoLightness, + VideoColorTemperature, ) MarkerColor = Marker.Color @@ -93,4 +98,10 @@ def timeline_from_clips(clips): 'VideoRotate', 'VideoRoundCorners', 'VideoScale', + 'VideoRoundCorners', + 'VideoBrightness', + 'VideoContrast', + 'VideoSaturation', + 'VideoLightness', + 'VideoColorTemperature', ] From 9b2172a893ed883c8ac0b3532d8368825aa21ef7 Mon Sep 17 00:00:00 2001 From: Eugen Sendroiu Date: Wed, 17 Sep 2025 11:16:48 +0200 Subject: [PATCH 23/27] Fix python bindings; add docs --- .../otio-serialized-schema-only-fields.md | 38 ++++++ docs/tutorials/otio-serialized-schema.md | 81 ++++++++++++ .../otio_serializableObjects.cpp | 115 +++++++++--------- 3 files changed, 176 insertions(+), 58 deletions(-) diff --git a/docs/tutorials/otio-serialized-schema-only-fields.md b/docs/tutorials/otio-serialized-schema-only-fields.md index f81c586818..2a975aad81 100644 --- a/docs/tutorials/otio-serialized-schema-only-fields.md +++ b/docs/tutorials/otio-serialized-schema-only-fields.md @@ -301,6 +301,33 @@ parameters: - *out_offset* - *transition_type* +### VideoBrightness.1 + +parameters: +- *brightness* +- *effect_name* +- *enabled* +- *metadata* +- *name* + +### VideoColorTemperature.1 + +parameters: +- *effect_name* +- *enabled* +- *metadata* +- *name* +- *temperature* + +### VideoContrast.1 + +parameters: +- *contrast* +- *effect_name* +- *enabled* +- *metadata* +- *name* + ### VideoCrop.1 parameters: @@ -324,12 +351,14 @@ parameters: - *name* ### VideoMask.1 +### VideoLightness.1 parameters: - *effect_name* - *enabled* - *mask_type* - *mask_url* +- *lightness* - *metadata* - *name* @@ -361,6 +390,15 @@ parameters: - *name* - *radius* +### VideoSaturation.1 + +parameters: +- *effect_name* +- *enabled* +- *metadata* +- *name* +- *saturation* + ### VideoScale.1 parameters: diff --git a/docs/tutorials/otio-serialized-schema.md b/docs/tutorials/otio-serialized-schema.md index af39c48dad..2c4b28cbf3 100644 --- a/docs/tutorials/otio-serialized-schema.md +++ b/docs/tutorials/otio-serialized-schema.md @@ -710,6 +710,63 @@ parameters: - *out_offset*: Amount of the next clip this transition overlaps, exclusive. - *transition_type*: Kind of transition, as defined by the :class:`Type` enum. +### VideoBrightness.1 + +*full module path*: `opentimelineio.schema.VideoBrightness` + +*documentation*: + +``` + +An effect that adjusts video brightness. + +``` + +parameters: +- *brightness*: Brightness value +- *effect_name*: +- *enabled*: If true, the Effect is applied. If false, the Effect is omitted. +- *metadata*: +- *name*: + +### VideoColorTemperature.1 + +*full module path*: `opentimelineio.schema.VideoColorTemperature` + +*documentation*: + +``` + +An effect that adjusts video color temperature. + +``` + +parameters: +- *effect_name*: +- *enabled*: If true, the Effect is applied. If false, the Effect is omitted. +- *metadata*: +- *name*: +- *temperature*: Color temperature value + +### VideoContrast.1 + +*full module path*: `opentimelineio.schema.VideoContrast` + +*documentation*: + +``` + +An effect that adjusts video contrast. + +``` + +parameters: +- *contrast*: Contrast value +- *effect_name*: +- *enabled*: If true, the Effect is applied. If false, the Effect is omitted. +- *metadata*: +- *name*: + ### VideoCrop.1 *full module path*: `opentimelineio.schema.VideoCrop` @@ -755,12 +812,16 @@ parameters: ### VideoMask.1 *full module path*: `opentimelineio.schema.VideoMask` +### VideoLightness.1 + +*full module path*: `opentimelineio.schema.VideoLightness` *documentation*: ``` An effect that applies a mask to a video +An effect that adjusts video lightness. ``` @@ -769,6 +830,7 @@ parameters: - *enabled*: If true, the Effect is applied. If false, the Effect is omitted. - *mask_type*: - *mask_url*: +- *lightness*: Lightness value - *metadata*: - *name*: @@ -832,6 +894,25 @@ parameters: - *name*: - *radius*: Radius of the corners +### VideoSaturation.1 + +*full module path*: `opentimelineio.schema.VideoSaturation` + +*documentation*: + +``` + +An effect that adjusts video saturation. + +``` + +parameters: +- *effect_name*: +- *enabled*: If true, the Effect is applied. If false, the Effect is omitted. +- *metadata*: +- *name*: +- *saturation*: Saturation value + ### VideoScale.1 *full module path*: `opentimelineio.schema.VideoScale` diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp index ccb1568bc9..4270426125 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp @@ -667,64 +667,7 @@ Should be subclassed (for example by :class:`.Track` and :class:`.Stack`), not u } static void define_effects(py::module m) { - - // Color Management Effects - py::class_>(m, "VideoBrightness", py::dynamic_attr(), R"docstring( -An effect that adjusts video brightness. -)docstring") - .def(py::init([](std::string name, int64_t brightness, py::object metadata) { - return new VideoBrightness(name, brightness, py_to_any_dictionary(metadata)); - }), - "name"_a = std::string(), - "brightness"_a = 0, - "metadata"_a = py::none()) - .def_property("brightness", &VideoBrightness::brightness, &VideoBrightness::set_brightness, "Brightness value"); - - py::class_>(m, "VideoContrast", py::dynamic_attr(), R"docstring( -An effect that adjusts video contrast. -)docstring") - .def(py::init([](std::string name, int64_t contrast, py::object metadata) { - return new VideoContrast(name, contrast, py_to_any_dictionary(metadata)); - }), - "name"_a = std::string(), - "contrast"_a = 0, - "metadata"_a = py::none()) - .def_property("contrast", &VideoContrast::contrast, &VideoContrast::set_contrast, "Contrast value"); - - py::class_>(m, "VideoSaturation", py::dynamic_attr(), R"docstring( -An effect that adjusts video saturation. -)docstring") - .def(py::init([](std::string name, int64_t saturation, py::object metadata) { - return new VideoSaturation(name, saturation, py_to_any_dictionary(metadata)); - }), - "name"_a = std::string(), - "saturation"_a = 0, - "metadata"_a = py::none()) - .def_property("saturation", &VideoSaturation::saturation, &VideoSaturation::set_saturation, "Saturation value"); - - py::class_>(m, "VideoLightness", py::dynamic_attr(), R"docstring( -An effect that adjusts video lightness. -)docstring") - .def(py::init([](std::string name, int64_t lightness, py::object metadata) { - return new VideoLightness(name, lightness, py_to_any_dictionary(metadata)); - }), - "name"_a = std::string(), - "lightness"_a = 0, - "metadata"_a = py::none()) - .def_property("lightness", &VideoLightness::lightness, &VideoLightness::set_lightness, "Lightness value"); - - py::class_>(m, "VideoColorTemperature", py::dynamic_attr(), R"docstring( -An effect that adjusts video color temperature. -)docstring") - .def(py::init([](std::string name, int64_t temperature, py::object metadata) { - return new VideoColorTemperature(name, temperature, py_to_any_dictionary(metadata)); - }), - "name"_a = std::string(), - "temperature"_a = 0, - "metadata"_a = py::none()) - .def_property("temperature", &VideoColorTemperature::temperature, &VideoColorTemperature::set_temperature, "Color temperature value"); - - py::class_>(m, "Effect", py::dynamic_attr()) + py::class_>(m, "Effect", py::dynamic_attr()) .def(py::init([](std::string name, std::string effect_name, py::object metadata, @@ -872,6 +815,62 @@ video_mask_class .def_property_readonly_static("REMOVE", [](py::object /* self */) { return VideoMask::MaskType::remove; }) .def_property_readonly_static("REPLACE", [](py::object /* self */) { return VideoMask::MaskType::replace; }) .def_property_readonly_static("BLUR", [](py::object /* self */) { return VideoMask::MaskType::blur; }); + py::class_>(m, "VideoBrightness", py::dynamic_attr(), R"docstring( +An effect that adjusts video brightness. +)docstring") + .def(py::init([](std::string name, int64_t brightness, py::object metadata) { + return new VideoBrightness(name, brightness, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "brightness"_a = 0, + "metadata"_a = py::none()) + .def_property("brightness", &VideoBrightness::brightness, &VideoBrightness::set_brightness, "Brightness value"); + + py::class_>(m, "VideoContrast", py::dynamic_attr(), R"docstring( +An effect that adjusts video contrast. +)docstring") + .def(py::init([](std::string name, int64_t contrast, py::object metadata) { + return new VideoContrast(name, contrast, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "contrast"_a = 0, + "metadata"_a = py::none()) + .def_property("contrast", &VideoContrast::contrast, &VideoContrast::set_contrast, "Contrast value"); + + py::class_>(m, "VideoSaturation", py::dynamic_attr(), R"docstring( +An effect that adjusts video saturation. +)docstring") + .def(py::init([](std::string name, int64_t saturation, py::object metadata) { + return new VideoSaturation(name, saturation, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "saturation"_a = 0, + "metadata"_a = py::none()) + .def_property("saturation", &VideoSaturation::saturation, &VideoSaturation::set_saturation, "Saturation value"); + + py::class_>(m, "VideoLightness", py::dynamic_attr(), R"docstring( +An effect that adjusts video lightness. +)docstring") + .def(py::init([](std::string name, int64_t lightness, py::object metadata) { + return new VideoLightness(name, lightness, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "lightness"_a = 0, + "metadata"_a = py::none()) + .def_property("lightness", &VideoLightness::lightness, &VideoLightness::set_lightness, "Lightness value"); + + py::class_>(m, "VideoColorTemperature", py::dynamic_attr(), R"docstring( +An effect that adjusts video color temperature. +)docstring") + .def(py::init([](std::string name, int64_t temperature, py::object metadata) { + return new VideoColorTemperature(name, temperature, py_to_any_dictionary(metadata)); + }), + "name"_a = std::string(), + "temperature"_a = 0, + "metadata"_a = py::none()) + .def_property("temperature", &VideoColorTemperature::temperature, &VideoColorTemperature::set_temperature, "Color temperature value"); + + py::class_>(m, "AudioVolume", py::dynamic_attr(), R"docstring( An effect that multiplies the audio volume by a given gain value From 975a1d078819ac99b8f21f8629b2d276b02509d7 Mon Sep 17 00:00:00 2001 From: Eugen Sendroiu Date: Thu, 18 Sep 2025 14:17:56 +0200 Subject: [PATCH 24/27] Fix rebase --- .../otio-serialized-schema-only-fields.md | 3 --- docs/tutorials/otio-serialized-schema.md | 6 ------ src/opentimelineio/typeRegistry.cpp | 6 ++---- .../otio_serializableObjects.cpp | 3 ++- .../opentimelineio/schema/__init__.py | 19 +++++++++---------- 5 files changed, 13 insertions(+), 24 deletions(-) diff --git a/docs/tutorials/otio-serialized-schema-only-fields.md b/docs/tutorials/otio-serialized-schema-only-fields.md index 2a975aad81..544ceebfae 100644 --- a/docs/tutorials/otio-serialized-schema-only-fields.md +++ b/docs/tutorials/otio-serialized-schema-only-fields.md @@ -350,14 +350,11 @@ parameters: - *metadata* - *name* -### VideoMask.1 ### VideoLightness.1 parameters: - *effect_name* - *enabled* -- *mask_type* -- *mask_url* - *lightness* - *metadata* - *name* diff --git a/docs/tutorials/otio-serialized-schema.md b/docs/tutorials/otio-serialized-schema.md index 2c4b28cbf3..37e318c98d 100644 --- a/docs/tutorials/otio-serialized-schema.md +++ b/docs/tutorials/otio-serialized-schema.md @@ -809,9 +809,6 @@ parameters: - *metadata*: - *name*: -### VideoMask.1 - -*full module path*: `opentimelineio.schema.VideoMask` ### VideoLightness.1 *full module path*: `opentimelineio.schema.VideoLightness` @@ -820,7 +817,6 @@ parameters: ``` -An effect that applies a mask to a video An effect that adjusts video lightness. ``` @@ -828,8 +824,6 @@ An effect that adjusts video lightness. parameters: - *effect_name*: - *enabled*: If true, the Effect is applied. If false, the Effect is omitted. -- *mask_type*: -- *mask_url*: - *lightness*: Lightness value - *metadata*: - *name*: diff --git a/src/opentimelineio/typeRegistry.cpp b/src/opentimelineio/typeRegistry.cpp index 8d38d6a90d..cb45c5f6ed 100644 --- a/src/opentimelineio/typeRegistry.cpp +++ b/src/opentimelineio/typeRegistry.cpp @@ -94,14 +94,12 @@ TypeRegistry::TypeRegistry() register_type_from_existing_type("Sequence", 1, "Track", nullptr); register_type(); - register_type(); register_type(); + register_type(); register_type(); register_type(); - register_type(); - register_type(); - register_type(); register_type(); + register_type(); register_type(); register_type(); register_type(); diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp index 4270426125..714e9be2ae 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp @@ -667,7 +667,7 @@ Should be subclassed (for example by :class:`.Track` and :class:`.Stack`), not u } static void define_effects(py::module m) { - py::class_>(m, "Effect", py::dynamic_attr()) + py::class_>(m, "Effect", py::dynamic_attr()) .def(py::init([](std::string name, std::string effect_name, py::object metadata, @@ -815,6 +815,7 @@ video_mask_class .def_property_readonly_static("REMOVE", [](py::object /* self */) { return VideoMask::MaskType::remove; }) .def_property_readonly_static("REPLACE", [](py::object /* self */) { return VideoMask::MaskType::replace; }) .def_property_readonly_static("BLUR", [](py::object /* self */) { return VideoMask::MaskType::blur; }); + py::class_>(m, "VideoBrightness", py::dynamic_attr(), R"docstring( An effect that adjusts video brightness. )docstring") diff --git a/src/py-opentimelineio/opentimelineio/schema/__init__.py b/src/py-opentimelineio/opentimelineio/schema/__init__.py index 1543822ee6..82430d6668 100644 --- a/src/py-opentimelineio/opentimelineio/schema/__init__.py +++ b/src/py-opentimelineio/opentimelineio/schema/__init__.py @@ -26,17 +26,17 @@ Track, Transition, V2d, + VideoBrightness, + VideoColorTemperature, + VideoContrast, VideoCrop, VideoFlip, + VideoLightness, VideoMask, VideoPosition, VideoRotate, VideoRoundCorners, - VideoBrightness, - VideoContrast, VideoSaturation, - VideoLightness, - VideoColorTemperature, ) MarkerColor = Marker.Color @@ -91,17 +91,16 @@ def timeline_from_clips(clips): 'SchemaDef', 'timeline_from_clips', 'V2d', + 'VideoBrightness', + 'VideoColorTemperature', + 'VideoContrast', 'VideoCrop', 'VideoFlip', + 'VideoLightness', 'VideoMask', 'VideoPosition', 'VideoRotate', 'VideoRoundCorners', - 'VideoScale', - 'VideoRoundCorners', - 'VideoBrightness', - 'VideoContrast', 'VideoSaturation', - 'VideoLightness', - 'VideoColorTemperature', + 'VideoScale', ] From 55499837897334c1cbee4bf7c3a1c9f692abb5ca Mon Sep 17 00:00:00 2001 From: Eugen Sendroiu Date: Thu, 18 Sep 2025 14:33:40 +0200 Subject: [PATCH 25/27] Fix rebase --- .../otio-serialized-schema-only-fields.md | 10 ++++++++++ docs/tutorials/otio-serialized-schema.md | 20 +++++++++++++++++++ .../opentimelineio/schema/__init__.py | 1 + 3 files changed, 31 insertions(+) diff --git a/docs/tutorials/otio-serialized-schema-only-fields.md b/docs/tutorials/otio-serialized-schema-only-fields.md index 544ceebfae..d4efe3d8a9 100644 --- a/docs/tutorials/otio-serialized-schema-only-fields.md +++ b/docs/tutorials/otio-serialized-schema-only-fields.md @@ -359,6 +359,16 @@ parameters: - *metadata* - *name* +### VideoMask.1 + +parameters: +- *effect_name* +- *enabled* +- *mask_type* +- *mask_url* +- *metadata* +- *name* + ### VideoPosition.1 parameters: diff --git a/docs/tutorials/otio-serialized-schema.md b/docs/tutorials/otio-serialized-schema.md index 37e318c98d..8bb7392ac8 100644 --- a/docs/tutorials/otio-serialized-schema.md +++ b/docs/tutorials/otio-serialized-schema.md @@ -828,6 +828,26 @@ parameters: - *metadata*: - *name*: +### VideoMask.1 + +*full module path*: `opentimelineio.schema.VideoMask` + +*documentation*: + +``` + +An effect that applies a mask to a video + +``` + +parameters: +- *effect_name*: +- *enabled*: If true, the Effect is applied. If false, the Effect is omitted. +- *mask_type*: +- *mask_url*: +- *metadata*: +- *name*: + ### VideoPosition.1 *full module path*: `opentimelineio.schema.VideoPosition` diff --git a/src/py-opentimelineio/opentimelineio/schema/__init__.py b/src/py-opentimelineio/opentimelineio/schema/__init__.py index 82430d6668..ec280d1499 100644 --- a/src/py-opentimelineio/opentimelineio/schema/__init__.py +++ b/src/py-opentimelineio/opentimelineio/schema/__init__.py @@ -37,6 +37,7 @@ VideoRotate, VideoRoundCorners, VideoSaturation, + VideoScale, ) MarkerColor = Marker.Color From f190372cbbcfab0c55be9243a7a4e17a8c9f8eb5 Mon Sep 17 00:00:00 2001 From: mike-dyer Date: Tue, 7 Oct 2025 15:34:46 +1100 Subject: [PATCH 26/27] Add sledgehammer for invalidating cached range (#8) The unit tests began failing with cached time ranges, add a top level invalidator to make tests pass again --- src/opentimelineio/timeline.cpp | 23 +++++++++++++++++++ src/opentimelineio/timeline.h | 3 +++ .../otio_serializableObjects.cpp | 3 ++- tests/test_composition.py | 3 +++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/opentimelineio/timeline.cpp b/src/opentimelineio/timeline.cpp index e76d8c3d91..30e4c831d3 100644 --- a/src/opentimelineio/timeline.cpp +++ b/src/opentimelineio/timeline.cpp @@ -90,4 +90,27 @@ Timeline::find_clips( shallow_search); } +void +Timeline::invalidate_cache() const +{ + std::stack stack; + stack.push(_tracks.value); + while (!stack.empty()) + { + auto composition = stack.top(); + composition->invalidate_cache(); + + stack.pop(); + + for (auto child : composition->children()) + { + auto* next_composition = dynamic_cast(child.value); + if (next_composition) + { + stack.push(next_composition); + } + } + } +} + }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/timeline.h b/src/opentimelineio/timeline.h index baaf128774..6928cc2ac2 100644 --- a/src/opentimelineio/timeline.h +++ b/src/opentimelineio/timeline.h @@ -124,6 +124,9 @@ class Timeline : public SerializableObjectWithMetadata return _tracks.value->available_image_bounds(error_status); } + /// @brief Invalidate the cache. + void invalidate_cache() const; + protected: virtual ~Timeline(); diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp index 714e9be2ae..96c2df81b7 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp @@ -663,7 +663,8 @@ Should be subclassed (for example by :class:`.Track` and :class:`.Stack`), not u }, "search_range"_a = std::nullopt, "shallow_search"_a = false) .def("find_children", [](Timeline* t, py::object descended_from_type, std::optional const& search_range, bool shallow_search) { return find_children(t, descended_from_type, search_range, shallow_search); - }, "descended_from_type"_a = py::none(), "search_range"_a = std::nullopt, "shallow_search"_a = false); + }, "descended_from_type"_a = py::none(), "search_range"_a = std::nullopt, "shallow_search"_a = false) + .def("invalidate_cache", &Timeline::invalidate_cache); } static void define_effects(py::module m) { diff --git a/tests/test_composition.py b/tests/test_composition.py index 40d4e9f4be..7c6c88c19d 100755 --- a/tests/test_composition.py +++ b/tests/test_composition.py @@ -1895,6 +1895,7 @@ def _nest(self, item): wrapper = _nest(self, clip) wrappers.append(wrapper) + timeline.invalidate_cache() # nothing should have shifted at all # print otio.adapters.otio_json.write_to_string(timeline) @@ -1928,6 +1929,8 @@ def _nest(self, item): # print otio.adapters.otio_json.write_to_string(timeline) + timeline.invalidate_cache() + # the clip should be the same self.assertEqual(clip.duration(), onehundred) From 95670f6fb99ba2adea43ac49327db1a05f2afd36 Mon Sep 17 00:00:00 2001 From: Mike Dyer Date: Tue, 7 Oct 2025 15:37:42 +1100 Subject: [PATCH 27/27] Use doubles in colour management effects --- src/opentimelineio/CORE_VERSION_MAP.cpp | 2 +- src/opentimelineio/colorManagementEffects.h | 38 ++++++++++----------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/opentimelineio/CORE_VERSION_MAP.cpp b/src/opentimelineio/CORE_VERSION_MAP.cpp index ac2db48ae1..2a5d5d92cd 100644 --- a/src/opentimelineio/CORE_VERSION_MAP.cpp +++ b/src/opentimelineio/CORE_VERSION_MAP.cpp @@ -180,8 +180,8 @@ const label_to_schema_version_map CORE_VERSION_MAP{ { "VideoContrast", 1 }, { "VideoCrop", 1 }, { "VideoFlip", 1 }, - { "VideoMask", 1 }, { "VideoLightness", 1 }, + { "VideoMask", 1 }, { "VideoPosition", 1 }, { "VideoRotate", 1 }, { "VideoRoundedCorners", 1 }, diff --git a/src/opentimelineio/colorManagementEffects.h b/src/opentimelineio/colorManagementEffects.h index 15c8a97454..2586c034e1 100644 --- a/src/opentimelineio/colorManagementEffects.h +++ b/src/opentimelineio/colorManagementEffects.h @@ -17,22 +17,22 @@ class VideoBrightness : public Effect VideoBrightness( std::string const& name = std::string(), - int64_t brightness = 0, + double brightness = 0, AnyDictionary const& metadata = AnyDictionary(), bool enabled = true) : Effect(name, Schema::name, metadata, enabled) , _brightness(brightness) {} - int64_t brightness() const noexcept { return _brightness; } - void set_brightness(int64_t brightness) noexcept { _brightness = brightness; } + double brightness() const noexcept { return _brightness; } + void set_brightness(double brightness) noexcept { _brightness = brightness; } protected: virtual ~VideoBrightness() = default; bool read_from(Reader&) override; void write_to(Writer&) const override; - int64_t _brightness; + double _brightness; }; /// @brief A contrast effect @@ -47,15 +47,15 @@ class VideoContrast : public Effect VideoContrast( std::string const& name = std::string(), - int64_t contrast = 0, + double contrast = 0, AnyDictionary const& metadata = AnyDictionary(), bool enabled = true) : Effect(name, Schema::name, metadata, enabled) , _contrast(contrast) {} - int64_t contrast() const noexcept { return _contrast; } - void set_contrast(int64_t contrast) noexcept { _contrast = contrast; } + double contrast() const noexcept { return _contrast; } + void set_contrast(double contrast) noexcept { _contrast = contrast; } protected: virtual ~VideoContrast() = default; @@ -77,22 +77,22 @@ class VideoSaturation : public Effect VideoSaturation( std::string const& name = std::string(), - int64_t saturation = 0, + double saturation = 0, AnyDictionary const& metadata = AnyDictionary(), bool enabled = true) : Effect(name, Schema::name, metadata, enabled) , _saturation(saturation) {} - int64_t saturation() const noexcept { return _saturation; } - void set_saturation(int64_t saturation) noexcept { _saturation = saturation; } + double saturation() const noexcept { return _saturation; } + void set_saturation(double saturation) noexcept { _saturation = saturation; } protected: virtual ~VideoSaturation() = default; bool read_from(Reader&) override; void write_to(Writer&) const override; - int64_t _saturation; + double _saturation; }; /// @brief A lightness effect @@ -107,22 +107,22 @@ class VideoLightness : public Effect VideoLightness( std::string const& name = std::string(), - int64_t lightness = 0, + double lightness = 0, AnyDictionary const& metadata = AnyDictionary(), bool enabled = true) : Effect(name, Schema::name, metadata, enabled) , _lightness(lightness) {} - int64_t lightness() const noexcept { return _lightness; } - void set_lightness(int64_t lightness) noexcept { _lightness = lightness; } + double lightness() const noexcept { return _lightness; } + void set_lightness(double lightness) noexcept { _lightness = lightness; } protected: virtual ~VideoLightness() = default; bool read_from(Reader&) override; void write_to(Writer&) const override; - int64_t _lightness; + double _lightness; }; /// @brief A color temperature effect @@ -137,22 +137,22 @@ class VideoColorTemperature : public Effect VideoColorTemperature( std::string const& name = std::string(), - int64_t temperature = 0, + double temperature = 0, AnyDictionary const& metadata = AnyDictionary(), bool enabled = true) : Effect(name, Schema::name, metadata, enabled) , _temperature(temperature) {} - int64_t temperature() const noexcept { return _temperature; } - void set_temperature(int64_t temperature) noexcept { _temperature = temperature; } + double temperature() const noexcept { return _temperature; } + void set_temperature(double temperature) noexcept { _temperature = temperature; } protected: virtual ~VideoColorTemperature() = default; bool read_from(Reader&) override; void write_to(Writer&) const override; - int64_t _temperature; + double _temperature; }; }} // namespace opentimelineio::OPENTIMELINEIO_VERSION