From f40c5091b6da99d48616c6785f83bd856cb5f13b Mon Sep 17 00:00:00 2001 From: Eric Reinecke Date: Wed, 13 Nov 2024 11:25:14 -0800 Subject: [PATCH 1/4] Added a list of applications integrating OTIO (#1812) * Added a list of applications integrating OTIO, also added linkcheck ignore for MIT license site perhaps blocking CI requests. --------- Signed-off-by: Eric Reinecke --- ADOPTERS.md | 14 ++++++++++++++ MANIFEST.in | 1 + docs/conf.py | 3 +++ 3 files changed, 18 insertions(+) create mode 100644 ADOPTERS.md diff --git a/ADOPTERS.md b/ADOPTERS.md new file mode 100644 index 0000000000..65106f8bb5 --- /dev/null +++ b/ADOPTERS.md @@ -0,0 +1,14 @@ +# OpenTimelineIO Adopters + +Below is a partial list of organizations and projects that are using OpenTimelineIO. If you would like to be added to this list, please submit a pull request to this file. + +| Name | Description | +|------|------------------------------------------------------------------------------------------------------------------------------------------------| +| [Adobe Premiere Pro](https://www.adobe.com/products/premiere.html) | [Timeline import/export](https://community.adobe.com/t5/premiere-pro-beta-discussions/new-in-beta-otio-import-and-export/td-p/14937493) (beta) | +| [AVID Media Composer](https://www.avid.com/media-composer) | Timeline export (preview) | +| [Black Magic Design DaVinci Resolve](https://www.blackmagicdesign.com/products/davinciresolve/) | Timeline import/export | +| [CineSync](https://www.backlight.co/product/cinesync) | Timeline viewing | | +| [ColorFront Transkoder](https://colorfront.com/index.php?page=SOFTWARE&spage=Transkoder) | Timeline import | +| [Nuke Studio](https://www.foundry.com/products/nuke) | Timeline import/export with full timeline round-tripping | +| [Hiero](https://www.foundry.com/products/nuke-family/hiero) | Timeline import/export | +| [OpenRV](https://github.com/AcademySoftwareFoundation/OpenRV) | Timeline import and viewing | \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index d73ee88970..9afb117be6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -12,6 +12,7 @@ exclude readthedocs-conda.yml exclude .codecov.yml exclude .gitlab-ci.yml exclude *.pdf +exclude ADOPTERS.md exclude CODE_OF_CONDUCT.md exclude CONTRIBUTING.md exclude CONTRIBUTORS.md diff --git a/docs/conf.py b/docs/conf.py index f83edac93b..a6fd5afbe0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -100,6 +100,9 @@ r'cxx/cxx' ] +# For some reason this URL gives 403 Forbidden when running in github actions +linkcheck_ignore = [r'https://opensource.org/licenses/MIT'] + # -- Options for MySt-Parser ----------------------------------------------------------- # https://myst-parser.readthedocs.io/en/latest/sphinx/reference.html From 3a8bad4e3e7aba7da28421256db83191c4f8392f Mon Sep 17 00:00:00 2001 From: Joshua Minor Date: Thu, 14 Nov 2024 08:14:41 -0800 Subject: [PATCH 2/4] Improve handling of rates when converting to timecode strings (#1477) * Deal with "close" timecode rates via a heuristic, instead of misleading entries in valid_timecode_rates table. * Renamed "valid_timecode" functions to "smpte_timecode" for clarity (old names are deprecated, but still work). * Both `is_smpte_timecode_rate` and `nearest_smpte_timecode_rate` now adhere to ST 12-1:2014 - SMPTE Standard - Time and Control Code. Signed-off-by: Joshua Minor --- src/opentime/errorStatus.cpp | 4 +- src/opentime/rationalTime.cpp | 83 +++++++++-------- src/opentime/rationalTime.h | 13 ++- .../opentime_rationalTime.cpp | 18 +++- tests/test_opentime.py | 93 +++++++++++-------- 5 files changed, 123 insertions(+), 88 deletions(-) diff --git a/src/opentime/errorStatus.cpp b/src/opentime/errorStatus.cpp index 13cd16db29..de4dbe0095 100644 --- a/src/opentime/errorStatus.cpp +++ b/src/opentime/errorStatus.cpp @@ -13,9 +13,9 @@ ErrorStatus::outcome_to_string(Outcome o) case OK: return std::string(); case INVALID_TIMECODE_RATE: - return "invalid timecode rate"; + return "SMPTE timecode does not support this rate"; case INVALID_TIMECODE_STRING: - return "string is not a valid timecode string"; + return "string is not a SMPTE timecode string"; case TIMECODE_RATE_MISMATCH: return "timecode specifies a frame higher than its rate"; case INVALID_TIME_STRING: diff --git a/src/opentime/rationalTime.cpp b/src/opentime/rationalTime.cpp index f4f2b9d18e..0082e91394 100644 --- a/src/opentime/rationalTime.cpp +++ b/src/opentime/rationalTime.cpp @@ -13,75 +13,68 @@ namespace opentime { namespace OPENTIME_VERSION { RationalTime RationalTime::_invalid_time{ 0, RationalTime::_invalid_rate }; -static constexpr std::array dropframe_timecode_rates{ { - // 23.976, - // 23.98, - // 23.97, - // 24000.0/1001.0, - 29.97, +static constexpr std::array dropframe_timecode_rates{ { 30000.0 / 1001.0, - 59.94, 60000.0 / 1001.0, } }; +// See the official source of these numbers here: +// ST 12-1:2014 - SMPTE Standard - Time and Control Code +// https://ieeexplore.ieee.org/document/7291029 +// static constexpr std::array smpte_timecode_rates{ - { 1.0, - 12.0, - 24000.0 / 1001.0, + { 24000.0 / 1001.0, 24.0, 25.0, 30000.0 / 1001.0, 30.0, + 48000.0 / 1001.0, 48.0, 50.0, 60000.0 / 1001.0, - 60.0 } -}; - -static constexpr std::array valid_timecode_rates{ - { 1.0, - 12.0, - 23.97, - 23.976, - 23.98, - 24000.0 / 1001.0, - 24.0, - 25.0, - 29.97, - 30000.0 / 1001.0, - 30.0, - 48.0, - 50.0, - 59.94, - 60000.0 / 1001.0, - 60.0 } + 60.0 + } }; +// deprecated in favor of `is_smpte_timecode_rate` bool RationalTime::is_valid_timecode_rate(double fps) { - auto b = valid_timecode_rates.begin(), e = valid_timecode_rates.end(); + return is_smpte_timecode_rate(fps); +} + +bool +RationalTime::is_smpte_timecode_rate(double fps) +{ + auto b = smpte_timecode_rates.begin(), e = smpte_timecode_rates.end(); return std::find(b, e, fps) != e; } +// deprecated in favor of `is_smpte_timecode_rate` double RationalTime::nearest_valid_timecode_rate(double rate) +{ + return nearest_smpte_timecode_rate(rate); +} + +double +RationalTime::nearest_smpte_timecode_rate(double rate) { double nearest_rate = 0; double min_diff = std::numeric_limits::max(); - for (auto valid_rate: smpte_timecode_rates) + for (auto smpte_rate: smpte_timecode_rates) { - if (valid_rate == rate) + if (smpte_rate == rate) { return rate; } - auto diff = std::abs(rate - valid_rate); + auto diff = std::abs(rate - smpte_rate); if (diff >= min_diff) { continue; } min_diff = diff; - nearest_rate = valid_rate; + nearest_rate = smpte_rate; } return nearest_rate; } @@ -200,7 +193,7 @@ RationalTime::from_timecode( double rate, ErrorStatus* error_status) { - if (!RationalTime::is_valid_timecode_rate(rate)) + if (!RationalTime::is_smpte_timecode_rate(rate)) { if (error_status) { @@ -331,7 +324,7 @@ RationalTime::from_time_string( double rate, ErrorStatus* error_status) { - if (!RationalTime::is_valid_timecode_rate(rate)) + if (!RationalTime::is_smpte_timecode_rate(rate)) { set_error( time_string, @@ -460,7 +453,12 @@ RationalTime::to_timecode( return std::string(); } - if (!is_valid_timecode_rate(rate)) + // It is common practice to use truncated or rounded values + // like 29.97 instead of exact SMPTE rates like 30000/1001 + // so as a convenience we will snap the rate to the nearest + // SMPTE rate if it is close enough. + double nearest_smpte_rate = nearest_smpte_timecode_rate(rate); + if (abs(nearest_smpte_rate - rate) > 0.1) { if (error_status) { @@ -469,6 +467,9 @@ RationalTime::to_timecode( return std::string(); } + // Let's assume this is the rate instead of the given rate. + rate = nearest_smpte_rate; + bool rate_is_dropframe = is_dropframe_rate(rate); if (drop_frame == IsDropFrameRate::ForceYes and not rate_is_dropframe) { @@ -504,11 +505,11 @@ RationalTime::to_timecode( } else { - if ((rate == 29.97) or (rate == 30000 / 1001.0)) + if (rate == 30000 / 1001.0) { dropframes = 2; } - else if (rate == 59.94) + else if (rate == 60000 / 1001.0) { dropframes = 4; } @@ -582,7 +583,7 @@ RationalTime::to_nearest_timecode( { *error_status = ErrorStatus(); - double nearest_rate = nearest_valid_timecode_rate(rate); + double nearest_rate = nearest_smpte_timecode_rate(rate); return to_timecode(nearest_rate, drop_frame, error_status); } diff --git a/src/opentime/rationalTime.h b/src/opentime/rationalTime.h index 12dacbc3aa..00cce4bfc7 100644 --- a/src/opentime/rationalTime.h +++ b/src/opentime/rationalTime.h @@ -171,13 +171,20 @@ class RationalTime start_time._rate }; } - /// @brief Returns true if the rate is valid for use with timecode. + /// @brief Returns true is the rate is supported by SMPTE timecode. + [[deprecated("Use is_smpte_timecode_rate() instead")]] static bool is_valid_timecode_rate(double rate); - /// @brief Returns the first valid timecode rate that has the least - /// difference from rate. + /// @brief Returns true is the rate is supported by SMPTE timecode. + static bool is_smpte_timecode_rate(double rate); + + /// @brief Returns the SMPTE timecode rate nearest to the given rate. + [[deprecated("Use nearest_smpte_timecode_rate() instead")]] static double nearest_valid_timecode_rate(double rate); + /// @brief Returns the SMPTE timecode rate nearest to the given rate. + static double nearest_smpte_timecode_rate(double rate); + /// @brief Convert a frame number and rate into a time. static constexpr RationalTime from_frames(double frame, double rate) noexcept diff --git a/src/py-opentimelineio/opentime-bindings/opentime_rationalTime.cpp b/src/py-opentimelineio/opentime-bindings/opentime_rationalTime.cpp index 3537a8485d..f0b80d810b 100644 --- a/src/py-opentimelineio/opentime-bindings/opentime_rationalTime.cpp +++ b/src/py-opentimelineio/opentime-bindings/opentime_rationalTime.cpp @@ -102,14 +102,22 @@ Compute the duration of samples from first to last (including last). This is not For example, the duration of a clip from frame 10 to frame 15 is 6 frames. Result will be in the rate of start_time. )docstring") - .def_static("is_valid_timecode_rate", &RationalTime::is_valid_timecode_rate, "rate"_a, "Returns true if the rate is valid for use with timecode.") + .def_static("is_valid_timecode_rate", &RationalTime::is_valid_timecode_rate, "rate"_a, + "Deprecated. Please use `is_smpte_timecode_rate` instead. This function will be removed in a future release.") + .def_static("is_smpte_timecode_rate", &RationalTime::is_smpte_timecode_rate, "rate"_a, + "Returns true if the rate is valid for use with SMPTE timecode.") .def_static("nearest_valid_timecode_rate", &RationalTime::nearest_valid_timecode_rate, "rate"_a, - "Returns the first valid timecode rate that has the least difference from the given value.") - .def_static("from_frames", &RationalTime::from_frames, "frame"_a, "rate"_a, "Turn a frame number and rate into a :class:`~RationalTime` object.") + "Deprecated. Please use `nearest_smpte_timecode_rate` instead. This function will be removed in a future release.") + .def_static("nearest_smpte_timecode_rate", &RationalTime::nearest_smpte_timecode_rate, "rate"_a, + "Returns the first SMPTE timecode rate that has the least difference from the given value.") + .def_static("from_frames", &RationalTime::from_frames, "frame"_a, "rate"_a, + "Turn a frame number and rate into a :class:`~RationalTime` object.") .def_static("from_seconds", static_cast (&RationalTime::from_seconds), "seconds"_a, "rate"_a) .def_static("from_seconds", static_cast (&RationalTime::from_seconds), "seconds"_a) - .def("to_frames", (int (RationalTime::*)() const) &RationalTime::to_frames, "Returns the frame number based on the current rate.") - .def("to_frames", (int (RationalTime::*)(double) const) &RationalTime::to_frames, "rate"_a, "Returns the frame number based on the given rate.") + .def("to_frames", (int (RationalTime::*)() const) &RationalTime::to_frames, + "Returns the frame number based on the current rate.") + .def("to_frames", (int (RationalTime::*)(double) const) &RationalTime::to_frames, "rate"_a, + "Returns the frame number based on the given rate.") .def("to_seconds", &RationalTime::to_seconds) .def("to_timecode", [](RationalTime rt, double rate, std::optional drop_frame) { return rt.to_timecode( diff --git a/tests/test_opentime.py b/tests/test_opentime.py index 81f03bd7f8..74143e29f1 100755 --- a/tests/test_opentime.py +++ b/tests/test_opentime.py @@ -200,29 +200,30 @@ def test_long_running_timecode_24(self): def test_timecode_23976_fps(self): # This should behave exactly like 24 fps + ntsc_23976 = 24000 / 1001.0 timecode = "00:00:01:00" - t = otio.opentime.RationalTime(value=24, rate=23.976) - self.assertEqual(t, otio.opentime.from_timecode(timecode, 23.976)) + t = otio.opentime.RationalTime(value=24, rate=ntsc_23976) + self.assertEqual(t, otio.opentime.from_timecode(timecode, ntsc_23976)) timecode = "00:01:00:00" - t = otio.opentime.RationalTime(value=24 * 60, rate=23.976) - self.assertEqual(t, otio.opentime.from_timecode(timecode, 23.976)) + t = otio.opentime.RationalTime(value=24 * 60, rate=ntsc_23976) + self.assertEqual(t, otio.opentime.from_timecode(timecode, ntsc_23976)) timecode = "01:00:00:00" - t = otio.opentime.RationalTime(value=24 * 60 * 60, rate=23.976) - self.assertEqual(t, otio.opentime.from_timecode(timecode, 23.976)) + t = otio.opentime.RationalTime(value=24 * 60 * 60, rate=ntsc_23976) + self.assertEqual(t, otio.opentime.from_timecode(timecode, ntsc_23976)) timecode = "24:00:00:00" - t = otio.opentime.RationalTime(value=24 * 60 * 60 * 24, rate=23.976) - self.assertEqual(t, otio.opentime.from_timecode(timecode, 23.976)) + t = otio.opentime.RationalTime(value=24 * 60 * 60 * 24, rate=ntsc_23976) + self.assertEqual(t, otio.opentime.from_timecode(timecode, ntsc_23976)) timecode = "23:59:59:23" t = otio.opentime.RationalTime( value=24 * 60 * 60 * 24 - 1, - rate=(24000 / 1001.0) + rate=ntsc_23976 ) self.assertEqual( - t, otio.opentime.from_timecode(timecode, (24000 / 1001.0)) + t, otio.opentime.from_timecode(timecode, ntsc_23976) ) def test_converting_negative_values_to_timecode(self): @@ -318,15 +319,17 @@ def test_dropframe_timecode_2997fps(self): ] } + ntsc_2997 = otio.opentime.RationalTime.nearest_smpte_timecode_rate(29.97) + self.assertEqual(ntsc_2997, 30000 / 1001.0) for time_key, time_values in test_values.items(): for value, tc in time_values: - t = otio.opentime.RationalTime(value, 29.97) + t = otio.opentime.RationalTime(value, ntsc_2997) self.assertEqual( tc, otio.opentime.to_timecode( - t, rate=29.97, drop_frame=True + t, rate=ntsc_2997, drop_frame=True ) ) - t1 = otio.opentime.from_timecode(tc, rate=29.97) + t1 = otio.opentime.from_timecode(tc, rate=ntsc_2997) self.assertEqual(t, t1) def test_timecode_ntsc_2997fps(self): @@ -351,12 +354,16 @@ def test_timecode_ntsc_2997fps(self): ) def test_timecode_infer_drop_frame(self): + # These rates are all non-integer SMPTE rates. + # When `to_timecode` is called without specifying + # a value for `drop_frame`, it will infer that these + # should be displayed as drop-frame timecode. frames = 1084319 rates = [ (29.97, '10:03:00;05'), (30000.0 / 1001.0, '10:03:00;05'), (59.94, '05:01:30;03'), - (60000.0 / 1001.0, '05:01:11;59') + (60000.0 / 1001.0, '05:01:30;03') ] for rate, timecode in rates: t = otio.opentime.RationalTime(frames, rate) @@ -373,12 +380,12 @@ def test_timecode_2997(self): (17983, '00:09:59:13', '00:10:00;01'), (17984, '00:09:59:14', '00:10:00;02'), ] - + ntsc_2997 = 30000 / 1001.0 for value, tc, dftc in ref_values: - t = otio.opentime.RationalTime(value, 29.97) - to_dftc = otio.opentime.to_timecode(t, rate=29.97, drop_frame=True) - to_tc = otio.opentime.to_timecode(t, rate=29.97, drop_frame=False) - to_auto_tc = otio.opentime.to_timecode(t, rate=29.97) + t = otio.opentime.RationalTime(value, ntsc_2997) + to_dftc = otio.opentime.to_timecode(t, rate=ntsc_2997, drop_frame=True) + to_tc = otio.opentime.to_timecode(t, rate=ntsc_2997, drop_frame=False) + to_auto_tc = otio.opentime.to_timecode(t, rate=ntsc_2997) # 29.97 should auto-detect dftc for backward compatability self.assertEqual(to_dftc, to_auto_tc) @@ -388,10 +395,10 @@ def test_timecode_2997(self): self.assertEqual(tc, to_tc) # Check they convert back - t1 = otio.opentime.from_timecode(to_dftc, rate=29.97) + t1 = otio.opentime.from_timecode(to_dftc, rate=ntsc_2997) self.assertEqual(t1, t) - t2 = otio.opentime.from_timecode(to_tc, rate=29.97) + t2 = otio.opentime.from_timecode(to_tc, rate=ntsc_2997) self.assertEqual(t2, t) def test_faulty_formatted_timecode_24(self): @@ -411,11 +418,16 @@ def test_faulty_formatted_timecode_24(self): with self.assertRaises(ValueError): otio.opentime.from_timecode('01:00:13;23', 24) + def test_faulty_time_string(self): + with self.assertRaises(ValueError): + otio.opentime.from_time_string("bogus", 24) + def test_invalid_rate_to_timecode_functions(self): - t = otio.opentime.RationalTime(100, 29.98) + # Use a bogus rate, expecting `to_timecode` to complain + t = otio.opentime.RationalTime(100, 999) with self.assertRaises(ValueError): - otio.opentime.to_timecode(t, 29.98) + otio.opentime.to_timecode(t, 777) with self.assertRaises(ValueError): otio.opentime.to_timecode(t) @@ -691,46 +703,53 @@ def test_passing_ndf_tc_at_df_rate(self): DF_TC = "01:00:02;05" NDF_TC = "00:59:58:17" frames = 107957 + ntsc_2997 = otio.opentime.RationalTime.nearest_smpte_timecode_rate(29.97) + self.assertEqual(ntsc_2997, 30000 / 1001.0) tc1 = otio.opentime.to_timecode( - otio.opentime.RationalTime(frames, 29.97) + otio.opentime.RationalTime(frames, ntsc_2997) ) self.assertEqual(tc1, DF_TC) tc2 = otio.opentime.to_timecode( - otio.opentime.RationalTime(frames, 29.97), - 29.97, + otio.opentime.RationalTime(frames, ntsc_2997), + ntsc_2997, drop_frame=False ) self.assertEqual(tc2, NDF_TC) - t1 = otio.opentime.from_timecode(DF_TC, 29.97) + t1 = otio.opentime.from_timecode(DF_TC, ntsc_2997) self.assertEqual(t1.value, frames) - t2 = otio.opentime.from_timecode(NDF_TC, 29.97) + t2 = otio.opentime.from_timecode(NDF_TC, ntsc_2997) self.assertEqual(t2.value, frames) - def test_nearest_valid_timecode_rate(self): - invalid_valid_rates = ( + def test_nearest_smpte_timecode_rate(self): + rate_pairs = ( (23.97602397602397, 24000.0 / 1001.0), (23.97, 24000.0 / 1001.0), (23.976, 24000.0 / 1001.0), (23.98, 24000.0 / 1001.0), (29.97, 30000.0 / 1001.0), (59.94, 60000.0 / 1001.0), + (24.0, 24.0), + (23.999999, 24.0), + (29.999999, 30.0), + (30.01, 30.0), + (60.01, 60.0) ) - for invalid_rate, nearest_valid_rate in invalid_valid_rates: + for wonky_rate, smpte_rate in rate_pairs: self.assertTrue( - otio.opentime.RationalTime.is_valid_timecode_rate( - nearest_valid_rate + otio.opentime.RationalTime.is_smpte_timecode_rate( + smpte_rate ) ) self.assertEqual( - otio.opentime.RationalTime.nearest_valid_timecode_rate( - invalid_rate + otio.opentime.RationalTime.nearest_smpte_timecode_rate( + wonky_rate ), - nearest_valid_rate, + smpte_rate, ) @@ -1334,7 +1353,7 @@ def test_to_timecode_mixed_rates(self): t = otio.opentime.from_timecode(timecode, 24) self.assertEqual(timecode, otio.opentime.to_timecode(t)) self.assertEqual(timecode, otio.opentime.to_timecode(t, 24)) - self.assertNotEqual(timecode, otio.opentime.to_timecode(t, 12)) + self.assertNotEqual(timecode, otio.opentime.to_timecode(t, 48)) time1 = otio.opentime.RationalTime(24.0, 24.0) time2 = otio.opentime.RationalTime(1.0, 1.0) From 5eafd409586663b63fc6f18e8cba4cbc87e0a3bb Mon Sep 17 00:00:00 2001 From: Peter Targett <131255921+peter-targett@users.noreply.github.com> Date: Thu, 5 Dec 2024 18:09:00 +0000 Subject: [PATCH 3/4] Add effect and marker initialization to Clip (#1808) * Add effect and marker initialization to Clip Extend Clip to allow effects and markers to be set. Signed-off-by: Peter Targett --------- Signed-off-by: Peter Targett --- src/opentimelineio/clip.cpp | 4 ++- src/opentimelineio/clip.h | 2 ++ .../otio_serializableObjects.cpp | 9 ++++- .../opentimelineio/schema/clip.py | 12 +++++-- tests/test_clip.cpp | 33 +++++++++++++++++++ tests/test_clip.py | 18 ++++++++++ 6 files changed, 73 insertions(+), 5 deletions(-) diff --git a/src/opentimelineio/clip.cpp b/src/opentimelineio/clip.cpp index e642b48f35..eb5be6380b 100644 --- a/src/opentimelineio/clip.cpp +++ b/src/opentimelineio/clip.cpp @@ -13,8 +13,10 @@ Clip::Clip( MediaReference* media_reference, std::optional const& source_range, AnyDictionary const& metadata, + std::vector const& effects, + std::vector const& markers, std::string const& active_media_reference_key) - : Parent{ name, source_range, metadata } + : Parent{ name, source_range, metadata, effects, markers } , _active_media_reference_key(active_media_reference_key) { set_media_reference(media_reference); diff --git a/src/opentimelineio/clip.h b/src/opentimelineio/clip.h index 5d7d5baccf..27f8bc9cf5 100644 --- a/src/opentimelineio/clip.h +++ b/src/opentimelineio/clip.h @@ -27,6 +27,8 @@ class Clip : public Item MediaReference* media_reference = nullptr, std::optional const& source_range = std::nullopt, AnyDictionary const& metadata = AnyDictionary(), + std::vector const& effects = std::vector(), + std::vector const& markers = std::vector(), std::string const& active_media_reference_key = default_media_key); void set_media_reference(MediaReference* media_reference); diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp index b8361d0942..5be092c3a9 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp @@ -425,13 +425,20 @@ Contains a :class:`.MediaReference` and a trim on that media reference. )docstring") .def(py::init([](std::string name, MediaReference* media_reference, std::optional source_range, py::object metadata, + std::optional> effects, + std::optional> markers, const std::string& active_media_reference) { - return new Clip(name, media_reference, source_range, py_to_any_dictionary(metadata), active_media_reference); + return new Clip(name, media_reference, source_range, py_to_any_dictionary(metadata), + vector_or_default(effects), + vector_or_default(markers), + active_media_reference); }), py::arg_v("name"_a = std::string()), "media_reference"_a = nullptr, "source_range"_a = std::nullopt, py::arg_v("metadata"_a = py::none()), + "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; diff --git a/src/py-opentimelineio/opentimelineio/schema/clip.py b/src/py-opentimelineio/opentimelineio/schema/clip.py index c0de97158b..cfa31a67cd 100644 --- a/src/py-opentimelineio/opentimelineio/schema/clip.py +++ b/src/py-opentimelineio/opentimelineio/schema/clip.py @@ -7,11 +7,13 @@ @add_method(_otio.Clip) def __str__(self): - return 'Clip("{}", {}, {}, {})'.format( + return 'Clip("{}", {}, {}, {}, {}, {})'.format( self.name, self.media_reference, self.source_range, - self.metadata + self.metadata, + self.effects, + self.markers ) @@ -22,12 +24,16 @@ def __repr__(self): 'name={}, ' 'media_reference={}, ' 'source_range={}, ' - 'metadata={}' + 'metadata={}, ' + 'effects={}, ' + 'markers={}' ')'.format( repr(self.name), repr(self.media_reference), repr(self.source_range), repr(self.metadata), + repr(self.effects), + repr(self.markers) ) ) diff --git a/tests/test_clip.cpp b/tests/test_clip.cpp index 467211d7f0..285bbd76e0 100644 --- a/tests/test_clip.cpp +++ b/tests/test_clip.cpp @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include #include @@ -150,6 +153,22 @@ main(int argc, char** argv) tests.add_test("test_clip_media_representation", [] { using namespace otio; + static constexpr auto time_scalar = 1.5; + + SerializableObject::Retainer ltw(new LinearTimeWarp( + LinearTimeWarp::Schema::name, + LinearTimeWarp::Schema::name, + time_scalar)); + std::vector effects = { ltw }; + + static constexpr auto red = Marker::Color::red; + + SerializableObject::Retainer m(new Marker( + LinearTimeWarp::Schema::name, + TimeRange(), + red)); + std::vector markers = { m }; + static constexpr auto high_quality = "high_quality"; static constexpr auto proxy_quality = "proxy_quality"; @@ -161,6 +180,8 @@ main(int argc, char** argv) media, std::nullopt, AnyDictionary(), + effects, + markers, high_quality)); assertEqual(clip->active_media_reference_key().c_str(), high_quality); @@ -225,6 +246,18 @@ main(int argc, char** argv) // should work clip->set_media_references({ { "cloud", ref4 } }, "cloud"); assertEqual(clip->media_reference(), ref4.value); + + // basic test for an effect + assertEqual(clip->effects().size(), effects.size()); + auto effect = dynamic_cast( + clip->effects().front().value); + assertEqual(effect->time_scalar(), time_scalar); + + // basic test for a marker + assertEqual(clip->markers().size(), markers.size()); + auto marker = dynamic_cast( + clip->markers().front().value); + assertEqual(marker->color().c_str(), red); }); tests.run(argc, argv); diff --git a/tests/test_clip.py b/tests/test_clip.py index 1cab19b185..cb2dafd7cf 100644 --- a/tests/test_clip.py +++ b/tests/test_clip.py @@ -21,10 +21,24 @@ def test_cons(self): target_url="/var/tmp/test.mov" ) + ltw = otio.schema.LinearTimeWarp( + name="linear_time_warp", + time_scalar=1.5) + effects = [] + effects.append(ltw) + + red = otio.schema.MarkerColor.RED + m = otio.schema.Marker( + name="red_marker", color=red) + markers = [] + markers.append(m) + cl = otio.schema.Clip( name=name, media_reference=mr, source_range=tr, + effects=effects, + markers=markers, # transition_in # transition_out ) @@ -32,6 +46,10 @@ def test_cons(self): self.assertEqual(cl.source_range, tr) self.assertIsOTIOEquivalentTo(cl.media_reference, mr) + self.assertTrue(isinstance(cl.effects[0], otio.schema.LinearTimeWarp)) + + self.assertEqual(cl.markers[0].color, red) + encoded = otio.adapters.otio_json.write_to_string(cl) decoded = otio.adapters.otio_json.read_from_string(encoded) self.assertIsOTIOEquivalentTo(cl, decoded) From 68d891a55ef0bfe2e29edcddd53b7908a55e78d5 Mon Sep 17 00:00:00 2001 From: Peter Targett <131255921+peter-targett@users.noreply.github.com> Date: Thu, 5 Dec 2024 18:10:42 +0000 Subject: [PATCH 4/4] Add AnyDictionary and AnyVector support for V2d and Box2d (#1811) * Add AnyDictionary and AnyVector support for V2d and Box2d Signed-off-by: Peter Targett --------- Signed-off-by: Peter Targett --- .../opentimelineio-bindings/otio_bindings.cpp | 4 ++++ .../opentimelineio-bindings/otio_utils.cpp | 4 ++++ tests/test_composable.py | 4 +++- tests/test_serializable_object.py | 10 ++++++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_bindings.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_bindings.cpp index b83c903f5d..ffa2da8c1e 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_bindings.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_bindings.cpp @@ -14,6 +14,8 @@ #include "opentimelineio/typeRegistry.h" #include "opentimelineio/stackAlgorithm.h" +#include + namespace py = pybind11; using namespace pybind11::literals; @@ -270,6 +272,8 @@ PYBIND11_MODULE(_otio, m) { .def(py::init([](RationalTime rt) { return new PyAny(rt); })) .def(py::init([](TimeRange tr) { return new PyAny(tr); })) .def(py::init([](TimeTransform tt) { return new PyAny(tt); })) + .def(py::init([](IMATH_NAMESPACE::V2d v2d) { return new PyAny(v2d); })) + .def(py::init([](IMATH_NAMESPACE::Box2d box2d) { return new PyAny(box2d); })) .def(py::init([](AnyVectorProxy* p) { return new PyAny(p->fetch_any_vector()); })) .def(py::init([](AnyDictionaryProxy* p) { return new PyAny(p->fetch_any_dictionary()); })) ; diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_utils.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_utils.cpp index 13e406c657..5ad4206f3c 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_utils.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_utils.cpp @@ -11,6 +11,8 @@ #include "opentimelineio/safely_typed_any.h" #include "opentimelineio/stringUtils.h" +#include + #include #include @@ -60,6 +62,8 @@ void _build_any_to_py_dispatch_table() { t[&typeid(RationalTime)] = [](std::any const& a, bool) { return py::cast(safely_cast_rational_time_any(a)); }; t[&typeid(TimeRange)] = [](std::any const& a, bool) { return py::cast(safely_cast_time_range_any(a)); }; t[&typeid(TimeTransform)] = [](std::any const& a, bool) { return py::cast(safely_cast_time_transform_any(a)); }; + t[&typeid(IMATH_NAMESPACE::V2d)] = [](std::any const& a, bool) { return py::cast(safely_cast_point_any(a)); }; + t[&typeid(IMATH_NAMESPACE::Box2d)] = [](std::any const& a, bool) { return py::cast(safely_cast_box_any(a)); }; t[&typeid(SerializableObject::Retainer<>)] = [](std::any const& a, bool) { SerializableObject* so = safely_cast_retainer_any(a); return py::cast(managing_ptr(so)); }; diff --git a/tests/test_composable.py b/tests/test_composable.py index e923ebdc68..fae9c8cab9 100644 --- a/tests/test_composable.py +++ b/tests/test_composable.py @@ -19,9 +19,11 @@ def test_constructor(self): self.assertEqual(seqi.metadata, {'foo': 'bar'}) def test_serialize(self): + b = otio.schema.Box2d( + otio.schema.V2d(0.0, 0.0), otio.schema.V2d(16.0, 9.0)) seqi = otio.core.Composable( name="test", - metadata={"foo": "bar"} + metadata={"box": b} ) encoded = otio.adapters.otio_json.write_to_string(seqi) decoded = otio.adapters.otio_json.read_from_string(encoded) diff --git a/tests/test_serializable_object.py b/tests/test_serializable_object.py index 788a3f7988..98a7866e38 100755 --- a/tests/test_serializable_object.py +++ b/tests/test_serializable_object.py @@ -136,6 +136,16 @@ def test_cycle_detection(self): with self.assertRaises(ValueError): o.clone() + def test_imath(self): + b = otio.schema.Box2d( + otio.schema.V2d(0.0, 0.0), otio.schema.V2d(16.0, 9.0)) + so = otio.core.SerializableObjectWithMetadata() + so.metadata["box"] = b + self.assertEqual(repr(so.metadata["box"]), repr(b)) + v = [b.min, b.max] + so.metadata["vectors"] = v + self.assertEqual(repr(so.metadata["vectors"]), repr(v)) + class VersioningTests(unittest.TestCase, otio_test_utils.OTIOAssertions): def test_schema_definition(self):