From ba34acfacb9a41aeae24dcedf530830755c1cbc2 Mon Sep 17 00:00:00 2001 From: Eric Reinecke Date: Fri, 3 Apr 2026 13:43:28 -0700 Subject: [PATCH 1/8] Initial pass implementing stream/channel mapping Signed-off-by: Eric Reinecke --- .../otio-serialized-schema-only-fields.md | 40 +++ docs/tutorials/otio-serialized-schema.md | 92 ++++++ src/opentimelineio/CMakeLists.txt | 12 + src/opentimelineio/CORE_VERSION_MAP.cpp | 6 + src/opentimelineio/audioMixMatrix.cpp | 33 ++ src/opentimelineio/audioMixMatrix.h | 81 +++++ src/opentimelineio/indexStreamAddress.cpp | 29 ++ src/opentimelineio/indexStreamAddress.h | 42 +++ src/opentimelineio/mediaReference.cpp | 28 ++ src/opentimelineio/mediaReference.h | 28 +- src/opentimelineio/streamAddress.cpp | 27 ++ src/opentimelineio/streamAddress.h | 39 +++ src/opentimelineio/streamInfo.cpp | 37 +++ src/opentimelineio/streamInfo.h | 133 ++++++++ src/opentimelineio/streamSelector.cpp | 34 ++ src/opentimelineio/streamSelector.h | 63 ++++ src/opentimelineio/stringStreamAddress.cpp | 29 ++ src/opentimelineio/stringStreamAddress.h | 42 +++ src/opentimelineio/typeRegistry.cpp | 12 + .../otio_serializableObjects.cpp | 166 +++++++++- .../opentimelineio/schema/__init__.py | 25 +- .../opentimelineio/schema/audio_mix_matrix.py | 15 + .../schema/index_stream_address.py | 15 + .../opentimelineio/schema/stream_address.py | 15 + .../opentimelineio/schema/stream_info.py | 35 +++ .../opentimelineio/schema/stream_selector.py | 35 +++ .../schema/string_stream_address.py | 17 + tests/test_media_reference.py | 99 ++++-- tests/test_stream_mapping.py | 292 ++++++++++++++++++ 29 files changed, 1493 insertions(+), 28 deletions(-) create mode 100644 src/opentimelineio/audioMixMatrix.cpp create mode 100644 src/opentimelineio/audioMixMatrix.h create mode 100644 src/opentimelineio/indexStreamAddress.cpp create mode 100644 src/opentimelineio/indexStreamAddress.h create mode 100644 src/opentimelineio/streamAddress.cpp create mode 100644 src/opentimelineio/streamAddress.h create mode 100644 src/opentimelineio/streamInfo.cpp create mode 100644 src/opentimelineio/streamInfo.h create mode 100644 src/opentimelineio/streamSelector.cpp create mode 100644 src/opentimelineio/streamSelector.h create mode 100644 src/opentimelineio/stringStreamAddress.cpp create mode 100644 src/opentimelineio/stringStreamAddress.h create mode 100644 src/py-opentimelineio/opentimelineio/schema/audio_mix_matrix.py create mode 100644 src/py-opentimelineio/opentimelineio/schema/index_stream_address.py create mode 100644 src/py-opentimelineio/opentimelineio/schema/stream_address.py create mode 100644 src/py-opentimelineio/opentimelineio/schema/stream_info.py create mode 100644 src/py-opentimelineio/opentimelineio/schema/stream_selector.py create mode 100644 src/py-opentimelineio/opentimelineio/schema/string_stream_address.py create mode 100644 tests/test_stream_mapping.py diff --git a/docs/tutorials/otio-serialized-schema-only-fields.md b/docs/tutorials/otio-serialized-schema-only-fields.md index 09a4c2b254..dbeedf0231 100644 --- a/docs/tutorials/otio-serialized-schema-only-fields.md +++ b/docs/tutorials/otio-serialized-schema-only-fields.md @@ -139,6 +139,15 @@ parameters: ## Module: opentimelineio.schema +### AudioMixMatrix.1 + +parameters: +- *effect_name* +- *enabled* +- *matrix* +- *metadata* +- *name* + ### Clip.2 parameters: @@ -215,6 +224,11 @@ parameters: - *start_frame* - *target_url_base* +### IndexStreamAddress.1 + +parameters: +- *index* + ### LinearTimeWarp.1 parameters: @@ -258,6 +272,32 @@ parameters: - *name* - *source_range* +### StreamAddress.1 + +parameters: + +### StreamInfo.1 + +parameters: +- *address* +- *kind* +- *metadata* +- *name* + +### StreamSelector.1 + +parameters: +- *effect_name* +- *enabled* +- *metadata* +- *name* +- *output_streams* + +### StringStreamAddress.1 + +parameters: +- *address* + ### TimeEffect.1 parameters: diff --git a/docs/tutorials/otio-serialized-schema.md b/docs/tutorials/otio-serialized-schema.md index 73919a0943..22004e557b 100644 --- a/docs/tutorials/otio-serialized-schema.md +++ b/docs/tutorials/otio-serialized-schema.md @@ -286,6 +286,25 @@ parameters: ## Module: opentimelineio.schema +### AudioMixMatrix.1 + +*full module path*: `opentimelineio.schema.AudioMixMatrix` + +*documentation*: + +``` +An effect that mixes audio streams using a coefficient matrix. Output keys SHOULD use +StreamInfo.Identifier values (e.g. left, right). Input keys identify source streams and SHOULD match + keys in the upstream available_streams map. +``` + +parameters: +- *effect_name*: +- *enabled*: If true, the Effect is applied. If false, the Effect is omitted. +- *matrix*: Output-keyed mixing matrix (output_name -> {input_name -> coefficient}). Output keys SHOULD be StreamInfo.Identifier values; input keys SHOULD match keys in the upstream available_streams map. +- *metadata*: +- *name*: + ### Clip.2 *full module path*: `opentimelineio.schema.Clip` @@ -498,6 +517,19 @@ parameters: - *start_frame*: The first frame number used in file names. - *target_url_base*: Everything leading up to the file name in the ``target_url``. +### IndexStreamAddress.1 + +*full module path*: `opentimelineio.schema.IndexStreamAddress` + +*documentation*: + +``` +Addresses a stream by integer index (e.g. ffmpeg stream index). +``` + +parameters: +- *index*: Integer index identifying the stream within its container. + ### LinearTimeWarp.1 *full module path*: `opentimelineio.schema.LinearTimeWarp` @@ -601,6 +633,66 @@ parameters: - *name*: - *source_range*: +### StreamAddress.1 + +*full module path*: `opentimelineio.schema.StreamAddress` + +*documentation*: + +``` +Base class for addressing a specific stream within a media reference. +``` + +parameters: + +### StreamInfo.1 + +*full module path*: `opentimelineio.schema.StreamInfo` + +*documentation*: + +``` +Describes a single stream available within a media reference, such as a video track, audio channel, +or camera view. The name field MAY be any descriptive string intended for human consumption, +analogous to the name on a Clip or Marker. +``` + +parameters: +- *address*: The address used to identify the stream within its media. +- *kind*: A string identifying the kind of stream (e.g. "Video", "Audio"). +- *metadata*: +- *name*: + +### StreamSelector.1 + +*full module path*: `opentimelineio.schema.StreamSelector` + +*documentation*: + +``` +An effect that selects specific named output streams from an item. +``` + +parameters: +- *effect_name*: +- *enabled*: If true, the Effect is applied. If false, the Effect is omitted. +- *metadata*: +- *name*: +- *output_streams*: List of stream identifier strings to select. + +### StringStreamAddress.1 + +*full module path*: `opentimelineio.schema.StringStreamAddress` + +*documentation*: + +``` +Addresses a stream by string identifier (e.g. channel label). +``` + +parameters: +- *address*: String identifier for the stream. + ### TimeEffect.1 *full module path*: `opentimelineio.schema.TimeEffect` diff --git a/src/opentimelineio/CMakeLists.txt b/src/opentimelineio/CMakeLists.txt index 607cb6dec2..46f59f0c4f 100644 --- a/src/opentimelineio/CMakeLists.txt +++ b/src/opentimelineio/CMakeLists.txt @@ -30,6 +30,12 @@ set(OPENTIMELINEIO_HEADER_FILES serialization.h stack.h stackAlgorithm.h + indexStreamAddress.h + streamAddress.h + streamInfo.h + stringStreamAddress.h + streamSelector.h + audioMixMatrix.h timeEffect.h timeline.h track.h @@ -66,6 +72,12 @@ add_library(opentimelineio ${OTIO_SHARED_OR_STATIC_LIB} serialization.cpp stack.cpp stackAlgorithm.cpp + indexStreamAddress.cpp + streamAddress.cpp + streamInfo.cpp + stringStreamAddress.cpp + streamSelector.cpp + audioMixMatrix.cpp stringUtils.cpp stringUtils.h # stringUtils.h is a private header timeEffect.cpp diff --git a/src/opentimelineio/CORE_VERSION_MAP.cpp b/src/opentimelineio/CORE_VERSION_MAP.cpp index 8d6e58da69..1a1ab50894 100644 --- a/src/opentimelineio/CORE_VERSION_MAP.cpp +++ b/src/opentimelineio/CORE_VERSION_MAP.cpp @@ -209,6 +209,7 @@ const label_to_schema_version_map CORE_VERSION_MAP{ { "0.19.0.dev1", { { "Adapter", 1 }, + { "AudioMixMatrix", 1 }, { "Clip", 2 }, { "Composable", 1 }, { "Composition", 1 }, @@ -219,6 +220,7 @@ const label_to_schema_version_map CORE_VERSION_MAP{ { "GeneratorReference", 1 }, { "HookScript", 1 }, { "ImageSequenceReference", 1 }, + { "IndexStreamAddress", 1 }, { "Item", 1 }, { "LinearTimeWarp", 1 }, { "Marker", 2 }, @@ -231,6 +233,10 @@ const label_to_schema_version_map CORE_VERSION_MAP{ { "SerializableObject", 1 }, { "SerializableObjectWithMetadata", 1 }, { "Stack", 1 }, + { "StreamAddress", 1 }, + { "StreamInfo", 1 }, + { "StreamSelector", 1 }, + { "StringStreamAddress", 1 }, { "Test", 1 }, { "TimeEffect", 1 }, { "Timeline", 1 }, diff --git a/src/opentimelineio/audioMixMatrix.cpp b/src/opentimelineio/audioMixMatrix.cpp new file mode 100644 index 0000000000..29bcfec71e --- /dev/null +++ b/src/opentimelineio/audioMixMatrix.cpp @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#include "opentimelineio/audioMixMatrix.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION_NS { + +AudioMixMatrix::AudioMixMatrix( + std::string const& name, + std::string const& effect_name, + MixMatrix const& matrix, + AnyDictionary const& metadata) + : Parent(name, effect_name, metadata) + , _matrix(matrix) +{} + +AudioMixMatrix::~AudioMixMatrix() +{} + +bool +AudioMixMatrix::read_from(Reader& reader) +{ + return reader.read_if_present("matrix", &_matrix) && Parent::read_from(reader); +} + +void +AudioMixMatrix::write_to(Writer& writer) const +{ + Parent::write_to(writer); + writer.write("matrix", _matrix); +} + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION_NS diff --git a/src/opentimelineio/audioMixMatrix.h b/src/opentimelineio/audioMixMatrix.h new file mode 100644 index 0000000000..12f16f86bc --- /dev/null +++ b/src/opentimelineio/audioMixMatrix.h @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#pragma once + +#include "opentimelineio/effect.h" +#include "opentimelineio/version.h" + +#include +#include + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION_NS { + +/// @brief An effect that mixes audio streams using a coefficient matrix. +/// +/// The matrix maps output stream names to a dictionary of input stream names +/// and their mix coefficients. Output keys SHOULD use values from +/// `StreamInfo::Identifier` (e.g. `left`, `right`) where applicable; they +/// correspond to the keys that will appear in the downstream available_streams +/// map after mixing. Input keys identify the source streams and SHOULD match +/// the corresponding keys in the upstream `MediaReference::available_streams`. +/// +/// Example — matrix to downmix 5.1 surround to Lo/Ro stereo : +/// @code{.json} +/// { +/// "left": { +/// "left": 1.0, +/// "surround_center_front": 0.707, +/// "surround_left_rear": 0.707 +/// }, +/// "right": { +/// "right": 1.0, +/// "surround_center_front": 0.707, +/// "surround_right_rear": 0.707 +/// } +/// } +/// @endcode +class OTIO_API_TYPE AudioMixMatrix : public Effect +{ +public: + /// @brief This struct provides the AudioMixMatrix schema. + struct Schema + { + static char constexpr name[] = "AudioMixMatrix"; + static int constexpr version = 1; + }; + + using Parent = Effect; + + using MixMatrix = std::map>; + + // @brief Create a new audio mix matrix effect. + /// + /// @param matrix Mixing matrix structured as: + /// ``` + /// {output_name: {source_name: contribution_coefficient}} + /// ``` + /// @param metadata Any additional metadata to persist. + OTIO_API AudioMixMatrix( + std::string const& name = std::string(), + std::string const& effect_name = std::string(), + MixMatrix const& matrix = MixMatrix(), + AnyDictionary const& metadata = AnyDictionary()); + + /// @brief Return a const reference to the mix matrix. + OTIO_API MixMatrix const& matrix() const noexcept { return _matrix; } + + /// @brief Set the mix matrix. + OTIO_API void set_matrix(MixMatrix const& matrix) { _matrix = matrix; } + +protected: + virtual ~AudioMixMatrix(); + + bool read_from(Reader&) override; + void write_to(Writer&) const override; + +private: + MixMatrix _matrix; +}; + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION_NS diff --git a/src/opentimelineio/indexStreamAddress.cpp b/src/opentimelineio/indexStreamAddress.cpp new file mode 100644 index 0000000000..5a54b900bc --- /dev/null +++ b/src/opentimelineio/indexStreamAddress.cpp @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#include "opentimelineio/indexStreamAddress.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION_NS { + +IndexStreamAddress::IndexStreamAddress(int64_t index) + : Parent() + , _index(index) +{} + +IndexStreamAddress::~IndexStreamAddress() +{} + +bool +IndexStreamAddress::read_from(Reader& reader) +{ + return reader.read("index", &_index) && Parent::read_from(reader); +} + +void +IndexStreamAddress::write_to(Writer& writer) const +{ + Parent::write_to(writer); + writer.write("index", _index); +} + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION_NS diff --git a/src/opentimelineio/indexStreamAddress.h b/src/opentimelineio/indexStreamAddress.h new file mode 100644 index 0000000000..13f2463762 --- /dev/null +++ b/src/opentimelineio/indexStreamAddress.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#pragma once + +#include "opentimelineio/streamAddress.h" +#include "opentimelineio/version.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION_NS { + +/// @brief Addresses a stream by integer index (e.g. a channel index in WAV). +class OTIO_API_TYPE IndexStreamAddress : public StreamAddress +{ +public: + /// @brief This struct provides the IndexStreamAddress schema. + struct Schema + { + static char constexpr name[] = "IndexStreamAddress"; + static int constexpr version = 1; + }; + + using Parent = StreamAddress; + + OTIO_API explicit IndexStreamAddress(int64_t index = 0); + + /// @brief Return the stream index. + OTIO_API int64_t index() const noexcept { return _index; } + + /// @brief Set the stream index. + OTIO_API void set_index(int64_t index) noexcept { _index = index; } + +protected: + virtual ~IndexStreamAddress(); + + bool read_from(Reader&) override; + void write_to(Writer&) const override; + +private: + int64_t _index; +}; + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION_NS diff --git a/src/opentimelineio/mediaReference.cpp b/src/opentimelineio/mediaReference.cpp index 6219cfeaa7..b8dcbce026 100644 --- a/src/opentimelineio/mediaReference.cpp +++ b/src/opentimelineio/mediaReference.cpp @@ -24,6 +24,29 @@ MediaReference::is_missing_reference() const return false; } +MediaReference::AvailableStreams +MediaReference::available_streams() const noexcept +{ + AvailableStreams result; + for (auto const& s: _available_streams) + { + result.insert( + { s.first, dynamic_retainer_cast(s.second) }); + } + return result; +} + +void +MediaReference::set_available_streams(AvailableStreams const& available_streams) +{ + _available_streams.clear(); + for (auto const& s: available_streams) + { + _available_streams[s.first] = s.second; + } +} + + bool MediaReference::read_from(Reader& reader) { @@ -31,6 +54,7 @@ MediaReference::read_from(Reader& reader) && reader.read_if_present( "available_image_bounds", &_available_image_bounds) + && reader.read_if_present("available_streams", &_available_streams) && Parent::read_from(reader); } @@ -40,6 +64,10 @@ MediaReference::write_to(Writer& writer) const Parent::write_to(writer); writer.write("available_range", _available_range); writer.write("available_image_bounds", _available_image_bounds); + if (!_available_streams.empty()) + { + writer.write("available_streams", _available_streams); + } } }} // namespace opentimelineio::OPENTIMELINEIO_VERSION_NS diff --git a/src/opentimelineio/mediaReference.h b/src/opentimelineio/mediaReference.h index 1d2c95dcf9..b455efaa63 100644 --- a/src/opentimelineio/mediaReference.h +++ b/src/opentimelineio/mediaReference.h @@ -4,9 +4,11 @@ #pragma once #include "opentimelineio/serializableObjectWithMetadata.h" +#include "opentimelineio/streamInfo.h" #include "opentimelineio/version.h" #include +#include namespace opentimelineio { namespace OPENTIMELINEIO_VERSION_NS { @@ -66,6 +68,27 @@ class OTIO_API_TYPE MediaReference : public SerializableObjectWithMetadata _available_image_bounds = available_image_bounds; } + using AvailableStreams = std::map; + + /// @brief Return the map of available streams keyed by stream identifier. + /// + /// Keys should use values from `StreamInfo::Identifier` to signal "primary" + /// streams (e.g. `StreamInfo::Identifier::monocular` for a "traditional" + /// video). + /// Additional streams should prefix a `StreamInfo::Identifier` value to + /// form a key unique within that media reference (e.g. something like + /// "music_stereo_right" could be used for a music stem provided in addition + /// to the "primary" audio). + /// However, keys may be any unique string — + /// for example, spatial audio objects whose identity is only meaningful + /// alongside positional renderomg metadata, or production audio tracks + /// labelled as the character's lavalier mic or boom mic used for that shot. + OTIO_API AvailableStreams available_streams() const noexcept; + + /// @brief Set the map of available streams. + /// @see available_streams() + OTIO_API void set_available_streams(AvailableStreams const& available_streams); + protected: virtual ~MediaReference(); @@ -73,8 +96,9 @@ class OTIO_API_TYPE MediaReference : public SerializableObjectWithMetadata void write_to(Writer&) const override; private: - std::optional _available_range; - std::optional _available_image_bounds; + std::optional _available_range; + std::optional _available_image_bounds; + std::map> _available_streams; }; }} // namespace opentimelineio::OPENTIMELINEIO_VERSION_NS diff --git a/src/opentimelineio/streamAddress.cpp b/src/opentimelineio/streamAddress.cpp new file mode 100644 index 0000000000..25fbe24c96 --- /dev/null +++ b/src/opentimelineio/streamAddress.cpp @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#include "opentimelineio/streamAddress.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION_NS { + +StreamAddress::StreamAddress() + : Parent() +{} + +StreamAddress::~StreamAddress() +{} + +bool +StreamAddress::read_from(Reader& reader) +{ + return Parent::read_from(reader); +} + +void +StreamAddress::write_to(Writer& writer) const +{ + Parent::write_to(writer); +} + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION_NS diff --git a/src/opentimelineio/streamAddress.h b/src/opentimelineio/streamAddress.h new file mode 100644 index 0000000000..7370909401 --- /dev/null +++ b/src/opentimelineio/streamAddress.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#pragma once + +#include "opentimelineio/serializableObject.h" +#include "opentimelineio/version.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION_NS { + +/// @brief Base class for addressing a specific media stream within a media +/// reference. +/// @see `StreamInfo` for more detail on media streams. +/// Specific stream address subclasses provide different mechansims for +/// identifying a stream within a container format. The semantics of which +/// StreamAddress subclass to use and how to interpret it are dependent on the +/// container format the media is accessed within. +class OTIO_API_TYPE StreamAddress : public SerializableObject +{ +public: + /// @brief This struct provides the StreamAddress schema. + struct Schema + { + static char constexpr name[] = "StreamAddress"; + static int constexpr version = 1; + }; + + using Parent = SerializableObject; + + OTIO_API StreamAddress(); + +protected: + virtual ~StreamAddress(); + + bool read_from(Reader&) override; + void write_to(Writer&) const override; +}; + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION_NS diff --git a/src/opentimelineio/streamInfo.cpp b/src/opentimelineio/streamInfo.cpp new file mode 100644 index 0000000000..8589f1c5ca --- /dev/null +++ b/src/opentimelineio/streamInfo.cpp @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#include "opentimelineio/streamInfo.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION_NS { + +StreamInfo::StreamInfo( + std::string const& name, + StreamAddress* address, + std::string const& kind, + AnyDictionary const& metadata) + : Parent(name, metadata) + , _address(address) + , _kind(kind) +{} + +StreamInfo::~StreamInfo() +{} + +bool +StreamInfo::read_from(Reader& reader) +{ + return reader.read_if_present("address", &_address) + && reader.read_if_present("kind", &_kind) + && Parent::read_from(reader); +} + +void +StreamInfo::write_to(Writer& writer) const +{ + Parent::write_to(writer); + writer.write("address", _address); + writer.write("kind", _kind); +} + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION_NS diff --git a/src/opentimelineio/streamInfo.h b/src/opentimelineio/streamInfo.h new file mode 100644 index 0000000000..94794e723a --- /dev/null +++ b/src/opentimelineio/streamInfo.h @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#pragma once + +#include "opentimelineio/serializableObjectWithMetadata.h" +#include "opentimelineio/indexStreamAddress.h" +#include "opentimelineio/streamAddress.h" +#include "opentimelineio/stringStreamAddress.h" +#include "opentimelineio/version.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION_NS { + +/// @brief Describes a single media stream provided within a source media. +/// +/// A media stream is the smallest unit of temporal media source such as a +/// single eye's video, an isolated audio channel, or a camera view within a 3D +/// scene. +/// `StreamAddress` provides a mechanism for addressing a specific stream within +/// a media container. +class OTIO_API_TYPE StreamInfo : public SerializableObjectWithMetadata +{ +public: + /// @brief Well-known strings for identifying common semantic roles of media + /// streams. These generally map to presentation devices (e.g. a spcific + /// speaker or one eye's view in a head-mounted display). + /// + /// In aural media, stereo pairs are called out distinctly from + /// immmersive/surround channels because they are not totally + /// interchangeable - simply presenting the left and right from a surround + /// mix is generally not a desired experience. + /// + /// The surround names are inspired by ITU-R BS.2051-3 using the following + /// modifications: + /// - Use the US spelling consistent with current OTIO spelling. + /// - Add "surround" prefix to group surround audio channel names + /// - Replace the "surround" postfix with "rear" + /// - Add "front" postfix to clarify names like "surround_left" + /// Additional names can be added from that specification using a similar + /// pattern if needed. + struct Identifier + { + // Visual media + + /// A single video stream that contains all views, e.g. a traditional 2D video + static char constexpr monocular[] = "monocular"; + /// The left eye's video stream in a stereo pair. + static char constexpr left_eye[] = "left_eye"; + /// The right eye's video stream in a stereo pair. + static char constexpr right_eye[] = "right_eye"; + + // Simple aural media + + /// A single monaural audio stream. + static char constexpr monaural[] = "monaural"; + /// The left channel of a stereo pair. + static char constexpr stereo_left[] = "stereo_left"; + /// The right channel of a stereo pair. + static char constexpr stereo_right[] = "stereo_right"; + + // Immersive or surround aural media + + /// The left front surround channel (L). + static char constexpr surround_left_front[] = "surround_left_front"; + /// The right front surround channel (R). + static char constexpr surround_right_front[] = "surround_right_front"; + /// The center surround channel (C). + static char constexpr surround_center_front[] = "surround_center_front"; + /// The left rear surround channel (Ls). + static char constexpr surround_left_rear[] = "surround_left_rear"; + /// The right rear surround channel (Rs). + static char constexpr surround_right_rear[] = "surround_right_rear"; + /// The low frequency effects (LFE) channel. + static char constexpr surround_low_frequency_effects[] = "surround_low_frequency_effects"; + }; + + /// @brief This struct provides the StreamInfo schema. + struct Schema + { + static char constexpr name[] = "StreamInfo"; + static int constexpr version = 1; + }; + + using Parent = SerializableObjectWithMetadata; + + /// @brief Create a new StreamInfo. + /// + /// @param name A human-readable label for this stream. May be any + /// descriptive string, e.g. "Alice lavalier (boom backup)", + /// "left eye 4K". + /// @param address The address of the stream within the media container. + /// @param kind A string identifying the kind of stream. When applicable, + /// use a value `Track::Kind`. + /// @param metadata Optional metadata dictionary. + OTIO_API StreamInfo( + std::string const& name = std::string(), + StreamAddress* address = nullptr, + std::string const& kind = std::string(), + AnyDictionary const& metadata = AnyDictionary()); + + /// @brief The stream address. + StreamAddress* address() const noexcept + { + return dynamic_retainer_cast(_address); + } + + /// @brief Set the stream address. + void set_address(StreamAddress* address) { _address = address; } + + /// @brief The stream kind. + /// + /// This describes the kind of media represented by this stream. + /// When applicable, this should be a value from `Track::Kind`. + std::string kind() const noexcept { return _kind; } + + /// @brief Set the stream kind. + /// + /// This describes the kind of media represented by this stream. + /// When applicable, this should be a value from `Track::Kind`. + void set_kind(std::string const& kind) { _kind = kind; } + +protected: + virtual ~StreamInfo(); + + bool read_from(Reader&) override; + void write_to(Writer&) const override; + +private: + Retainer _address; + std::string _kind; +}; + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION_NS diff --git a/src/opentimelineio/streamSelector.cpp b/src/opentimelineio/streamSelector.cpp new file mode 100644 index 0000000000..7812973b27 --- /dev/null +++ b/src/opentimelineio/streamSelector.cpp @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#include "opentimelineio/streamSelector.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION_NS { + +StreamSelector::StreamSelector( + std::string const& name, + std::string const& effect_name, + std::vector const& output_streams, + AnyDictionary const& metadata) + : Parent(name, effect_name, metadata) + , _output_streams(output_streams) +{} + +StreamSelector::~StreamSelector() +{} + +bool +StreamSelector::read_from(Reader& reader) +{ + return reader.read_if_present("output_streams", &_output_streams) + && Parent::read_from(reader); +} + +void +StreamSelector::write_to(Writer& writer) const +{ + Parent::write_to(writer); + writer.write("output_streams", _output_streams); +} + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION_NS diff --git a/src/opentimelineio/streamSelector.h b/src/opentimelineio/streamSelector.h new file mode 100644 index 0000000000..8cace6d392 --- /dev/null +++ b/src/opentimelineio/streamSelector.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#pragma once + +#include "opentimelineio/effect.h" +#include "opentimelineio/version.h" + +#include + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION_NS { + +/// @brief An effect that selects specific named output streams from an item. +/// +/// Use this to select a stereo view, specific audio channels, etc. +/// The clip will expose these streams out with the same naming. +class OTIO_API_TYPE StreamSelector : public Effect +{ +public: + /// @brief This struct provides the StreamSelector schema. + struct Schema + { + static char constexpr name[] = "StreamSelector"; + static int constexpr version = 1; + }; + + using Parent = Effect; + + /// @brief Create a new StreamSelector. + /// + /// @param name The name of the effect object. + /// @param effect_name The name of the effect. + /// @param output_streams The list of stream identifiers to select. + /// @param metadata The metadata for the effect. + OTIO_API StreamSelector( + std::string const& name = std::string(), + std::string const& effect_name = std::string(), + std::vector const& output_streams = std::vector(), + AnyDictionary const& metadata = AnyDictionary()); + + /// @brief Return the list of output stream identifiers. + std::vector const& output_streams() const noexcept + { + return _output_streams; + } + + /// @brief Set the list of output stream identifiers. + void set_output_streams(std::vector const& output_streams) + { + _output_streams = output_streams; + } + +protected: + virtual ~StreamSelector(); + + bool read_from(Reader&) override; + void write_to(Writer&) const override; + +private: + std::vector _output_streams; +}; + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION_NS diff --git a/src/opentimelineio/stringStreamAddress.cpp b/src/opentimelineio/stringStreamAddress.cpp new file mode 100644 index 0000000000..e7de0bf455 --- /dev/null +++ b/src/opentimelineio/stringStreamAddress.cpp @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#include "opentimelineio/stringStreamAddress.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION_NS { + +StringStreamAddress::StringStreamAddress(std::string const& address) + : Parent() + , _address(address) +{} + +StringStreamAddress::~StringStreamAddress() +{} + +bool +StringStreamAddress::read_from(Reader& reader) +{ + return reader.read("address", &_address) && Parent::read_from(reader); +} + +void +StringStreamAddress::write_to(Writer& writer) const +{ + Parent::write_to(writer); + writer.write("address", _address); +} + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION_NS diff --git a/src/opentimelineio/stringStreamAddress.h b/src/opentimelineio/stringStreamAddress.h new file mode 100644 index 0000000000..2051d9d59b --- /dev/null +++ b/src/opentimelineio/stringStreamAddress.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#pragma once + +#include "opentimelineio/streamAddress.h" +#include "opentimelineio/version.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION_NS { + +/// @brief Addresses a stream by string identifier (e.g. codec name or channel label). +class OTIO_API_TYPE StringStreamAddress : public StreamAddress +{ +public: + /// @brief This struct provides the StringStreamAddress schema. + struct Schema + { + static char constexpr name[] = "StringStreamAddress"; + static int constexpr version = 1; + }; + + using Parent = StreamAddress; + + OTIO_API explicit StringStreamAddress(std::string const& address = std::string()); + + /// @brief Return the stream address string. + std::string address() const noexcept { return _address; } + + /// @brief Set the stream address string. + void set_address(std::string const& address) { _address = address; } + +protected: + virtual ~StringStreamAddress(); + + bool read_from(Reader&) override; + void write_to(Writer&) const override; + +private: + std::string _address; +}; + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION_NS diff --git a/src/opentimelineio/typeRegistry.cpp b/src/opentimelineio/typeRegistry.cpp index 8e4f012f2e..8a2a2e281c 100644 --- a/src/opentimelineio/typeRegistry.cpp +++ b/src/opentimelineio/typeRegistry.cpp @@ -4,6 +4,7 @@ #include "opentimelineio/typeRegistry.h" #include "anyDictionary.h" +#include "opentimelineio/audioMixMatrix.h" #include "opentimelineio/clip.h" #include "opentimelineio/composable.h" #include "opentimelineio/composition.h" @@ -22,6 +23,11 @@ #include "opentimelineio/serializableObject.h" #include "opentimelineio/serializableObjectWithMetadata.h" #include "opentimelineio/stack.h" +#include "opentimelineio/indexStreamAddress.h" +#include "opentimelineio/streamAddress.h" +#include "opentimelineio/streamInfo.h" +#include "opentimelineio/stringStreamAddress.h" +#include "opentimelineio/streamSelector.h" #include "opentimelineio/timeEffect.h" #include "opentimelineio/timeline.h" #include "opentimelineio/track.h" @@ -82,6 +88,12 @@ TypeRegistry::TypeRegistry() nullptr); 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 e928336ad1..f4ee1b7023 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp @@ -6,6 +6,7 @@ #include #include "otio_errorStatusHandler.h" +#include "opentimelineio/audioMixMatrix.h" #include "opentimelineio/clip.h" #include "opentimelineio/color.h" #include "opentimelineio/composable.h" @@ -22,6 +23,11 @@ #include "opentimelineio/mediaReference.h" #include "opentimelineio/missingReference.h" #include "opentimelineio/stack.h" +#include "opentimelineio/indexStreamAddress.h" +#include "opentimelineio/streamAddress.h" +#include "opentimelineio/streamInfo.h" +#include "opentimelineio/stringStreamAddress.h" +#include "opentimelineio/streamSelector.h" #include "opentimelineio/timeEffect.h" #include "opentimelineio/timeline.h" #include "opentimelineio/track.h" @@ -702,6 +708,149 @@ Should be subclassed (for example by :class:`.Track` and :class:`.Stack`), not u }, "descended_from_type"_a = py::none(), "search_range"_a = std::nullopt, "shallow_search"_a = false); } +static void define_stream_address_and_info(py::module m) { + py::class_>(m, "StreamAddress", py::dynamic_attr(), + "Base class for addressing a specific stream within a media reference.") + .def(py::init([]() { return new StreamAddress(); })); + + py::class_>(m, "IndexStreamAddress", py::dynamic_attr(), + "Addresses a stream by integer index (e.g. ffmpeg stream index).") + .def(py::init([](int64_t index) { + return new IndexStreamAddress(index); }), + "index"_a = 0LL) + .def_property("index", &IndexStreamAddress::index, &IndexStreamAddress::set_index, + "Integer index identifying the stream within its container."); + + py::class_>(m, "StringStreamAddress", py::dynamic_attr(), + "Addresses a stream by string identifier (e.g. channel label).") + .def(py::init([](std::string address) { + return new StringStreamAddress(address); }), + "address"_a = std::string()) + .def_property("address", &StringStreamAddress::address, &StringStreamAddress::set_address, + "String identifier for the stream."); + + auto stream_info_class = + py::class_>(m, "StreamInfo", py::dynamic_attr(), + "Describes a single media stream provided within a source media. " + "A media stream is the smallest unit of temporal media, such as a single eye's video, " + "an isolated audio channel, or a camera view within a 3D scene. " + "StreamAddress provides a mechanism for addressing a specific stream within a media container.") + .def(py::init([](std::string name, + StreamAddress* address, + std::string kind, + py::object metadata) { + return new StreamInfo(name, address, kind, py_to_any_dictionary(metadata)); }), + py::arg_v("name"_a = std::string()), + "address"_a = py::none(), + "kind"_a = std::string(), + py::arg_v("metadata"_a = py::none())) + .def_property("address", &StreamInfo::address, &StreamInfo::set_address, + "The address used to identify the stream within its media.") + .def_property("kind", &StreamInfo::kind, &StreamInfo::set_kind, + "A string identifying the kind of stream (e.g. \"Video\", \"Audio\")."); + + py::class_(stream_info_class, "Identifier", + "Well-known strings identifying common semantic roles of media streams. " + "These generally map to presentation devices (e.g. a specific speaker or one eye's view). " + "Keys SHOULD use these values to signal primary streams " + "(e.g. Identifier.monocular for a traditional video stream); " + "additional streams SHOULD prefix one of these values to form a unique key " + "(e.g. \"surround_left_front_atmos_obj_42\"). " + "Keys MAY be any unique string when no well-known identifier applies.") + .def_property_readonly_static("left_eye", [](py::object) { return StreamInfo::Identifier::left_eye; }) + .def_property_readonly_static("right_eye", [](py::object) { return StreamInfo::Identifier::right_eye; }) + .def_property_readonly_static("monocular", [](py::object) { return StreamInfo::Identifier::monocular; }) + .def_property_readonly_static("monaural", [](py::object) { return StreamInfo::Identifier::monaural; }) + .def_property_readonly_static("stereo_left", [](py::object) { return StreamInfo::Identifier::stereo_left; }) + .def_property_readonly_static("stereo_right", [](py::object) { return StreamInfo::Identifier::stereo_right; }) + .def_property_readonly_static("surround_left_front", [](py::object) { return StreamInfo::Identifier::surround_left_front; }) + .def_property_readonly_static("surround_right_front", [](py::object) { return StreamInfo::Identifier::surround_right_front; }) + .def_property_readonly_static("surround_center_front", [](py::object) { return StreamInfo::Identifier::surround_center_front; }) + .def_property_readonly_static("surround_left_rear", [](py::object) { return StreamInfo::Identifier::surround_left_rear; }) + .def_property_readonly_static("surround_right_rear", [](py::object) { return StreamInfo::Identifier::surround_right_rear; }) + .def_property_readonly_static("surround_low_frequency_effects", [](py::object) { return StreamInfo::Identifier::surround_low_frequency_effects; }); +} + +static void define_stream_effects(py::module m) { + py::class_>(m, "StreamSelector", py::dynamic_attr(), + "An effect that selects specific named output streams from an item. " + "Use this to select a stereo view, specific audio channels, etc. " + "The item will expose these streams downstream with the same naming.") + .def(py::init([](std::string name, + std::string effect_name, + std::vector output_streams, + py::object metadata) { + return new StreamSelector(name, effect_name, output_streams, + py_to_any_dictionary(metadata)); }), + py::arg_v("name"_a = std::string()), + "effect_name"_a = std::string(), + "output_streams"_a = std::vector(), + py::arg_v("metadata"_a = py::none())) + .def_property("output_streams", + &StreamSelector::output_streams, + &StreamSelector::set_output_streams, + "List of stream identifier strings to select."); + + py::class_>(m, "AudioMixMatrix", py::dynamic_attr(), + "An effect that mixes audio streams using a coefficient matrix. " + "The matrix maps output stream names to a dict of input stream names and their mix coefficients. " + "Output keys SHOULD use StreamInfo.Identifier values (e.g. stereo_left, stereo_right) where applicable; " + "they correspond to the keys that will appear in the downstream available_streams map after mixing. " + "Input keys identify source streams and SHOULD match keys in the upstream available_streams map.") + .def(py::init([](std::string name, + py::object matrix, + py::object metadata) { + AudioMixMatrix::MixMatrix m; + if (!matrix.is_none()) { + for (auto& outer : matrix.cast()) { + AudioMixMatrix::MixMatrix::mapped_type row; + for (auto& inner : outer.second.cast()) { + row[inner.first.cast()] = + inner.second.cast(); + } + m[outer.first.cast()] = std::move(row); + } + } + return new AudioMixMatrix(name, "AudioMixMatrix", m, + py_to_any_dictionary(metadata)); }), + py::arg_v("name"_a = std::string()), + py::arg_v("matrix"_a = py::none()), + py::arg_v("metadata"_a = py::none())) + .def_property("matrix", + [](AudioMixMatrix const* a) { + py::dict outer; + for (auto const& row : a->matrix()) { + py::dict inner; + for (auto const& col : row.second) { + inner[py::str(col.first)] = col.second; + } + outer[py::str(row.first)] = inner; + } + return outer; + }, + [](AudioMixMatrix* a, py::dict d) { + AudioMixMatrix::MixMatrix m; + for (auto& outer : d) { + AudioMixMatrix::MixMatrix::mapped_type row; + for (auto& inner : outer.second.cast()) { + row[inner.first.cast()] = + inner.second.cast(); + } + m[outer.first.cast()] = std::move(row); + } + a->set_matrix(m); + }, + "Output-keyed mixing matrix (output_name -> {input_name -> coefficient}). " + "Output keys SHOULD use StreamInfo.Identifier values where applicable; " + "input keys SHOULD match keys in the upstream available_streams map."); +} + static void define_effects(py::module m) { py::class_>(m, "Effect", py::dynamic_attr()) .def(py::init([](std::string name, @@ -765,8 +914,19 @@ 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_readonly("is_missing_reference", &MediaReference::is_missing_reference); + .def_property("available_image_bounds", &MediaReference::available_image_bounds, &MediaReference::set_available_image_bounds) + .def_property_readonly("is_missing_reference", &MediaReference::is_missing_reference) + .def("available_streams", &MediaReference::available_streams, + "Return a dict mapping stream identifier keys to StreamInfo objects. " + "Keys SHOULD use StreamInfo.Identifier values to signal primary streams " + "(e.g. Identifier.monocular for a traditional video). " + "Additional streams SHOULD prefix an Identifier value to form a unique key " + "(e.g. \"music_stereo_right\" for a music stem alongside the primary audio). " + "Keys MAY be any unique string — for example, spatial audio object IDs or " + "production audio tracks identified by character lavalier or boom mic.") + .def("set_available_streams", &MediaReference::set_available_streams, + "available_streams"_a, + "Set the available streams map. See available_streams() for key conventions."); py::class_>(m, "GeneratorReference", py::dynamic_attr()) @@ -977,7 +1137,9 @@ This is roughly equivalent to: void otio_serializable_object_bindings(py::module m) { define_bases1(m); define_bases2(m); + define_stream_address_and_info(m); define_effects(m); + define_stream_effects(m); define_media_references(m); define_items_and_compositions(m); } diff --git a/src/py-opentimelineio/opentimelineio/schema/__init__.py b/src/py-opentimelineio/opentimelineio/schema/__init__.py index c7fc31bfcb..977da70a11 100644 --- a/src/py-opentimelineio/opentimelineio/schema/__init__.py +++ b/src/py-opentimelineio/opentimelineio/schema/__init__.py @@ -6,6 +6,7 @@ """User facing classes.""" from .. _otio import ( + AudioMixMatrix, Box2d, Clip, Effect, @@ -16,10 +17,15 @@ Gap, GeneratorReference, ImageSequenceReference, + IndexStreamAddress, Marker, MissingReference, SerializableCollection, Stack, + StreamAddress, + StreamInfo, + StreamSelector, + StringStreamAddress, Timeline, Track, Transition, @@ -27,6 +33,7 @@ ) MarkerColor = Marker.Color +StreamIdentifier = StreamInfo.Identifier TrackKind = Track.Kind TransitionTypes = Transition.Type NeighborGapPolicy = Track.NeighborGapPolicy @@ -36,14 +43,20 @@ ) from . import ( + audio_mix_matrix, box2d, clip, effect, external_reference, generator_reference, image_sequence_reference, + index_stream_address, marker, serializable_collection, + stream_address, + stream_info, + stream_selector, + string_stream_address, timeline, transition, v2d, @@ -56,6 +69,7 @@ def timeline_from_clips(clips): return Timeline(tracks=[trck]) __all__ = [ + 'AudioMixMatrix', 'Box2d', 'Clip', 'Effect', @@ -66,13 +80,20 @@ def timeline_from_clips(clips): 'Gap', 'GeneratorReference', 'ImageSequenceReference', + 'IndexStreamAddress', 'Marker', 'MissingReference', 'SerializableCollection', + 'SchemaDef', 'Stack', + 'StreamAddress', + 'StreamIdentifier', + 'StreamInfo', + 'StreamSelector', + 'StringStreamAddress', 'Timeline', 'Transition', - 'SchemaDef', 'timeline_from_clips', - 'V2d' + 'V2d', + 'Track', ] diff --git a/src/py-opentimelineio/opentimelineio/schema/audio_mix_matrix.py b/src/py-opentimelineio/opentimelineio/schema/audio_mix_matrix.py new file mode 100644 index 0000000000..65c4ad1f84 --- /dev/null +++ b/src/py-opentimelineio/opentimelineio/schema/audio_mix_matrix.py @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +from .. core._core_utils import add_method +from .. import _otio + + +@add_method(_otio.AudioMixMatrix) +def __str__(self): + return "AudioMixMatrix({})".format(str(self.name)) + + +@add_method(_otio.AudioMixMatrix) +def __repr__(self): + return "otio.schema.AudioMixMatrix(name={})".format(repr(self.name)) diff --git a/src/py-opentimelineio/opentimelineio/schema/index_stream_address.py b/src/py-opentimelineio/opentimelineio/schema/index_stream_address.py new file mode 100644 index 0000000000..e4a9891787 --- /dev/null +++ b/src/py-opentimelineio/opentimelineio/schema/index_stream_address.py @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +from .. core._core_utils import add_method +from .. import _otio + + +@add_method(_otio.IndexStreamAddress) +def __str__(self): + return "IndexStreamAddress({})".format(self.index) + + +@add_method(_otio.IndexStreamAddress) +def __repr__(self): + return "otio.schema.IndexStreamAddress(index={})".format(repr(self.index)) diff --git a/src/py-opentimelineio/opentimelineio/schema/stream_address.py b/src/py-opentimelineio/opentimelineio/schema/stream_address.py new file mode 100644 index 0000000000..437de73f5b --- /dev/null +++ b/src/py-opentimelineio/opentimelineio/schema/stream_address.py @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +from .. core._core_utils import add_method +from .. import _otio + + +@add_method(_otio.StreamAddress) +def __str__(self): + return "StreamAddress()" + + +@add_method(_otio.StreamAddress) +def __repr__(self): + return "otio.schema.StreamAddress()" diff --git a/src/py-opentimelineio/opentimelineio/schema/stream_info.py b/src/py-opentimelineio/opentimelineio/schema/stream_info.py new file mode 100644 index 0000000000..3365120dfc --- /dev/null +++ b/src/py-opentimelineio/opentimelineio/schema/stream_info.py @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +from .. core._core_utils import add_method +from .. import _otio + + +@add_method(_otio.StreamInfo) +def __str__(self): + return ( + "StreamInfo(" + "{}, " + "{}, " + "{}" + ")".format( + str(self.name), + str(self.address), + str(self.kind), + ) + ) + + +@add_method(_otio.StreamInfo) +def __repr__(self): + return ( + "otio.schema.StreamInfo(" + "name={}, " + "address={}, " + "kind={}" + ")".format( + repr(self.name), + repr(self.address), + repr(self.kind), + ) + ) diff --git a/src/py-opentimelineio/opentimelineio/schema/stream_selector.py b/src/py-opentimelineio/opentimelineio/schema/stream_selector.py new file mode 100644 index 0000000000..4f9c753101 --- /dev/null +++ b/src/py-opentimelineio/opentimelineio/schema/stream_selector.py @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +from .. core._core_utils import add_method +from .. import _otio + + +@add_method(_otio.StreamSelector) +def __str__(self): + return ( + "StreamSelector(" + "{}, " + "{}, " + "{}" + ")".format( + str(self.name), + str(self.effect_name), + str(self.output_streams), + ) + ) + + +@add_method(_otio.StreamSelector) +def __repr__(self): + return ( + "otio.schema.StreamSelector(" + "name={}, " + "effect_name={}, " + "output_streams={}" + ")".format( + repr(self.name), + repr(self.effect_name), + repr(self.output_streams), + ) + ) diff --git a/src/py-opentimelineio/opentimelineio/schema/string_stream_address.py b/src/py-opentimelineio/opentimelineio/schema/string_stream_address.py new file mode 100644 index 0000000000..6a68aeaeb2 --- /dev/null +++ b/src/py-opentimelineio/opentimelineio/schema/string_stream_address.py @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +from .. core._core_utils import add_method +from .. import _otio + + +@add_method(_otio.StringStreamAddress) +def __str__(self): + return "StringStreamAddress({})".format(self.address) + + +@add_method(_otio.StringStreamAddress) +def __repr__(self): + return "otio.schema.StringStreamAddress(address={})".format( + repr(self.address) + ) diff --git a/tests/test_media_reference.py b/tests/test_media_reference.py index a810e4832d..bb73656b53 100755 --- a/tests/test_media_reference.py +++ b/tests/test_media_reference.py @@ -5,38 +5,34 @@ import opentimelineio as otio import opentimelineio.test_utils as otio_test_utils +from opentimelineio.schema import StreamInfo import unittest class MediaReferenceTests(unittest.TestCase, otio_test_utils.OTIOAssertions): - def test_cons(self): tr = otio.opentime.TimeRange( - otio.opentime.RationalTime(5, 24), - otio.opentime.RationalTime(10, 24.0) + otio.opentime.RationalTime(5, 24), otio.opentime.RationalTime(10, 24.0) ) mr = otio.schema.MissingReference( - available_range=tr, - metadata={'show': 'OTIOTheMovie'} + available_range=tr, metadata={"show": "OTIOTheMovie"} ) self.assertEqual(mr.available_range, tr) mr = otio.schema.MissingReference() self.assertIsNone(mr.available_range) + self.assertEqual(mr.available_streams(), {}) def test_str_missing(self): missing = otio.schema.MissingReference() - self.assertMultiLineEqual( - str(missing), - "MissingReference(\'\', None, None, {})" - ) + self.assertMultiLineEqual(str(missing), "MissingReference('', None, None, {})") self.assertMultiLineEqual( repr(missing), "otio.schema.MissingReference(" "name='', available_range=None, available_image_bounds=None, metadata={}" - ")" + ")", ) encoded = otio.adapters.otio_json.write_to_string(missing) @@ -46,14 +42,11 @@ def test_str_missing(self): def test_filepath(self): filepath = otio.schema.ExternalReference("/var/tmp/foo.mov") self.assertMultiLineEqual( - str(filepath), - 'ExternalReference("/var/tmp/foo.mov")' + str(filepath), 'ExternalReference("/var/tmp/foo.mov")' ) self.assertMultiLineEqual( repr(filepath), - "otio.schema.ExternalReference(" - "target_url='/var/tmp/foo.mov'" - ")" + "otio.schema.ExternalReference(target_url='/var/tmp/foo.mov')", ) # round trip serialize @@ -63,17 +56,13 @@ def test_filepath(self): def test_equality(self): filepath = otio.schema.ExternalReference(target_url="/var/tmp/foo.mov") - filepath2 = otio.schema.ExternalReference( - target_url="/var/tmp/foo.mov" - ) + filepath2 = otio.schema.ExternalReference(target_url="/var/tmp/foo.mov") self.assertIsOTIOEquivalentTo(filepath, filepath2) bl = otio.schema.MissingReference() self.assertNotEqual(filepath, bl) - filepath2 = otio.schema.ExternalReference( - target_url="/var/tmp/foo2.mov" - ) + filepath2 = otio.schema.ExternalReference(target_url="/var/tmp/foo2.mov") self.assertNotEqual(filepath, filepath2) self.assertEqual(filepath == filepath2, False) @@ -85,5 +74,71 @@ def test_is_missing(self): self.assertTrue(mr.is_missing_reference) -if __name__ == '__main__': +class TestAvailableStreamsOnMediaReference(unittest.TestCase): + def test_set_and_get_available_streams(self): + ref = otio.schema.ExternalReference(target_url="/foo/bar.mov") + video_info = StreamInfo( + name="left eye 4K", + address=otio.schema.IndexStreamAddress(index=0), + kind=otio.schema.TrackKind.Video, + ) + audio_left = StreamInfo( + name="left channel", + address=otio.schema.IndexStreamAddress(index=1), + kind=otio.schema.TrackKind.Audio, + ) + ref.set_available_streams( + { + StreamInfo.Identifier.left_eye: video_info, + StreamInfo.Identifier.stereo_left: audio_left, + } + ) + streams = ref.available_streams() + self.assertIn(StreamInfo.Identifier.left_eye, streams) + self.assertIn(StreamInfo.Identifier.stereo_left, streams) + self.assertIsInstance(streams[StreamInfo.Identifier.left_eye], StreamInfo) + self.assertEqual( + streams[StreamInfo.Identifier.left_eye].kind, otio.schema.TrackKind.Video + ) + self.assertEqual( + streams[StreamInfo.Identifier.stereo_left].kind, otio.schema.TrackKind.Audio + ) + + def test_round_trip_serialization(self): + ref = otio.schema.ExternalReference(target_url="/show/shot.mov") + ref.set_available_streams( + { + StreamInfo.Identifier.left_eye: StreamInfo( + name="v0_left", + address=otio.schema.IndexStreamAddress(index=0), + kind=otio.schema.TrackKind.Video, + ), + StreamInfo.Identifier.stereo_left: StreamInfo( + name="a1_left", + address=otio.schema.IndexStreamAddress(index=1), + kind=otio.schema.TrackKind.Audio, + ), + } + ) + json_str = ref.to_json_string() + restored = otio.adapters.read_from_string(json_str, "otio_json") + self.assertIsInstance(restored, otio.schema.ExternalReference) + self.assertEqual(restored.target_url, "/show/shot.mov") + streams = restored.available_streams() + self.assertIn(StreamInfo.Identifier.left_eye, streams) + self.assertIn(StreamInfo.Identifier.stereo_left, streams) + self.assertEqual( + streams[StreamInfo.Identifier.left_eye].kind, otio.schema.TrackKind.Video + ) + self.assertEqual( + streams[StreamInfo.Identifier.stereo_left].kind, otio.schema.TrackKind.Audio + ) + self.assertIsInstance( + streams[StreamInfo.Identifier.left_eye].address, + otio.schema.IndexStreamAddress, + ) + self.assertEqual(streams[StreamInfo.Identifier.left_eye].address.index, 0) + + +if __name__ == "__main__": unittest.main() diff --git a/tests/test_stream_mapping.py b/tests/test_stream_mapping.py new file mode 100644 index 0000000000..a11060f5fe --- /dev/null +++ b/tests/test_stream_mapping.py @@ -0,0 +1,292 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +"""Tests for stream mapping schemas.""" + +import unittest +import opentimelineio as otio +from opentimelineio.schema import StreamInfo + + +class TestIndexStreamAddress(unittest.TestCase): + def test_create_default(self): + addr = otio.schema.IndexStreamAddress() + self.assertEqual(addr.index, 0) + self.assertEqual(addr.schema_name(), "IndexStreamAddress") + self.assertEqual(addr.schema_version(), 1) + + def test_create_with_index(self): + addr = otio.schema.IndexStreamAddress(index=3) + self.assertEqual(addr.index, 3) + + def test_set_index(self): + addr = otio.schema.IndexStreamAddress() + addr.index = 7 + self.assertEqual(addr.index, 7) + + def test_round_trip_serialization(self): + addr = otio.schema.IndexStreamAddress(index=5) + json_str = addr.to_json_string() + restored = otio.adapters.read_from_string(json_str, "otio_json") + self.assertIsInstance(restored, otio.schema.IndexStreamAddress) + self.assertEqual(restored.index, 5) + + +class TestStringStreamAddress(unittest.TestCase): + def test_create_default(self): + addr = otio.schema.StringStreamAddress() + self.assertEqual(addr.address, "") + self.assertEqual(addr.schema_name(), "StringStreamAddress") + self.assertEqual(addr.schema_version(), 1) + + def test_create_with_address(self): + addr = otio.schema.StringStreamAddress(address="/World/potatoSet/cam1") + self.assertEqual(addr.address, "/World/potatoSet/cam1") + + def test_set_address(self): + addr = otio.schema.StringStreamAddress() + addr.address = "/World/potatoSet/cam1" + self.assertEqual(addr.address, "/World/potatoSet/cam1") + + def test_round_trip_serialization(self): + addr = otio.schema.StringStreamAddress(address="/World/potatoSet/cam1") + json_str = addr.to_json_string() + restored = otio.adapters.read_from_string(json_str, "otio_json") + self.assertIsInstance(restored, otio.schema.StringStreamAddress) + self.assertEqual(restored.address, "/World/potatoSet/cam1") + + +class TestStreamInfo(unittest.TestCase): + def test_create_default(self): + info = otio.schema.StreamInfo() + self.assertEqual(info.name, "") + self.assertIsNone(info.address) + self.assertEqual(info.kind, "") + self.assertEqual(info.metadata, {}) + self.assertEqual(info.schema_name(), "StreamInfo") + self.assertEqual(info.schema_version(), 1) + + def test_create_with_fields(self): + addr = otio.schema.IndexStreamAddress(index=0) + info = otio.schema.StreamInfo( + name="main_video", address=addr, kind=otio.schema.TrackKind.Video + ) + self.assertEqual(info.name, "main_video") + self.assertIsInstance(info.address, otio.schema.IndexStreamAddress) + self.assertEqual(info.address.index, 0) + self.assertEqual(info.kind, otio.schema.TrackKind.Video) + + def test_round_trip_with_index_address(self): + addr = otio.schema.IndexStreamAddress(index=2) + info = otio.schema.StreamInfo( + name="audio_left", + address=addr, + kind=otio.schema.TrackKind.Audio, + metadata={"ITU-R_BS.2051": {"label": "L"}}, + ) + json_str = info.to_json_string() + restored = otio.adapters.read_from_string(json_str, "otio_json") + self.assertIsInstance(restored, otio.schema.StreamInfo) + self.assertEqual(restored.name, "audio_left") + self.assertEqual(restored.kind, otio.schema.TrackKind.Audio) + self.assertIsInstance(restored.address, otio.schema.IndexStreamAddress) + self.assertEqual(restored.address.index, 2) + self.assertEqual(restored.metadata["ITU-R_BS.2051"]["label"], "L") + + def test_round_trip_with_string_address(self): + addr = otio.schema.StringStreamAddress(address="/World/potatoSet/cam1") + info = otio.schema.StreamInfo( + name="cam1", address=addr, kind=otio.schema.TrackKind.Video + ) + json_str = info.to_json_string() + restored = otio.adapters.read_from_string(json_str, "otio_json") + self.assertIsInstance(restored, otio.schema.StreamInfo) + self.assertIsInstance(restored.address, otio.schema.StringStreamAddress) + self.assertEqual(restored.address.address, "/World/potatoSet/cam1") + + +class TestStreamSelector(unittest.TestCase): + def test_create_default(self): + sel = otio.schema.StreamSelector() + self.assertEqual(sel.name, "") + self.assertEqual(sel.effect_name, "") + self.assertEqual(sel.output_streams, []) + + def test_create_with_streams(self): + sel = otio.schema.StreamSelector( + name="stereo_select", + effect_name="StreamSelector", + output_streams=[ + StreamInfo.Identifier.stereo_left, + StreamInfo.Identifier.stereo_right, + ], + ) + self.assertEqual( + sel.output_streams, + [ + StreamInfo.Identifier.stereo_left, + StreamInfo.Identifier.stereo_right, + ], + ) + + def test_set_output_streams(self): + sel = otio.schema.StreamSelector() + sel.output_streams = [ + StreamInfo.Identifier.surround_center_front, + StreamInfo.Identifier.surround_low_frequency_effects, + ] + self.assertEqual( + sel.output_streams, + [ + StreamInfo.Identifier.surround_center_front, + StreamInfo.Identifier.surround_low_frequency_effects, + ], + ) + + def test_schema_name(self): + sel = otio.schema.StreamSelector() + self.assertEqual(sel.schema_name(), "StreamSelector") + self.assertEqual(sel.schema_version(), 1) + + def test_round_trip_serialization(self): + sel = otio.schema.StreamSelector( + name="select_stereo", + output_streams=[ + StreamInfo.Identifier.stereo_left, + StreamInfo.Identifier.stereo_right, + ], + ) + json_str = sel.to_json_string() + restored = otio.adapters.read_from_string(json_str, "otio_json") + self.assertIsInstance(restored, otio.schema.StreamSelector) + self.assertEqual(restored.name, "select_stereo") + self.assertEqual( + restored.output_streams, + [ + StreamInfo.Identifier.stereo_left, + StreamInfo.Identifier.stereo_right, + ], + ) + + def test_as_clip_effect(self): + clip = otio.schema.Clip(name="my_clip") + sel = otio.schema.StreamSelector( + output_streams=[StreamInfo.Identifier.stereo_left] + ) + clip.effects.append(sel) + json_str = clip.to_json_string() + restored = otio.adapters.read_from_string(json_str, "otio_json") + self.assertEqual(len(restored.effects), 1) + self.assertIsInstance(restored.effects[0], otio.schema.StreamSelector) + self.assertEqual( + restored.effects[0].output_streams, [StreamInfo.Identifier.stereo_left] + ) + + +class TestAudioMixMatrix(unittest.TestCase): + def test_create_default(self): + m = otio.schema.AudioMixMatrix() + self.assertEqual(m.name, "") + self.assertEqual(m.effect_name, "AudioMixMatrix") + self.assertEqual(len(m.matrix), 0) + + def test_create_with_matrix(self): + m = otio.schema.AudioMixMatrix( + name="stereo_downmix", + matrix={ + StreamInfo.Identifier.stereo_left: { + StreamInfo.Identifier.stereo_left: 1.0, + StreamInfo.Identifier.surround_center_front: 0.707, + }, + StreamInfo.Identifier.stereo_right: { + StreamInfo.Identifier.stereo_right: 1.0, + StreamInfo.Identifier.surround_center_front: 0.707, + }, + }, + ) + self.assertIn(StreamInfo.Identifier.stereo_left, m.matrix) + self.assertIn(StreamInfo.Identifier.stereo_right, m.matrix) + + def test_schema_name(self): + m = otio.schema.AudioMixMatrix() + self.assertEqual(m.schema_name(), "AudioMixMatrix") + self.assertEqual(m.schema_version(), 1) + + def test_round_trip_serialization(self): + m = otio.schema.AudioMixMatrix( + name="5_1_downmix", + matrix={ + StreamInfo.Identifier.stereo_left: { + StreamInfo.Identifier.surround_left_front: 1.0, + StreamInfo.Identifier.surround_center_front: 0.707, + StreamInfo.Identifier.surround_left_rear: 0.707, + }, + StreamInfo.Identifier.stereo_right: { + StreamInfo.Identifier.surround_right_front: 1.0, + StreamInfo.Identifier.surround_center_front: 0.707, + StreamInfo.Identifier.surround_right_rear: 0.707, + }, + }, + ) + json_str = m.to_json_string() + restored = otio.adapters.read_from_string(json_str, "otio_json") + self.assertIsInstance(restored, otio.schema.AudioMixMatrix) + self.assertEqual(restored.name, "5_1_downmix") + self.assertIn(StreamInfo.Identifier.stereo_left, restored.matrix) + self.assertIn(StreamInfo.Identifier.stereo_right, restored.matrix) + + +class TestStreamIdentifier(unittest.TestCase): + def test_video_identifiers(self): + self.assertEqual(StreamInfo.Identifier.left_eye, "left_eye") + self.assertEqual(StreamInfo.Identifier.right_eye, "right_eye") + self.assertEqual(StreamInfo.Identifier.monocular, "monocular") + + def test_audio_identifiers(self): + self.assertEqual(StreamInfo.Identifier.monaural, "monaural") + self.assertEqual(StreamInfo.Identifier.stereo_left, "stereo_left") + self.assertEqual(StreamInfo.Identifier.stereo_right, "stereo_right") + + def test_surround_identifiers(self): + self.assertEqual( + StreamInfo.Identifier.surround_left_front, "surround_left_front" + ) + self.assertEqual( + StreamInfo.Identifier.surround_right_front, "surround_right_front" + ) + self.assertEqual( + StreamInfo.Identifier.surround_center_front, "surround_center_front" + ) + self.assertEqual(StreamInfo.Identifier.surround_left_rear, "surround_left_rear") + self.assertEqual( + StreamInfo.Identifier.surround_right_rear, "surround_right_rear" + ) + self.assertEqual( + StreamInfo.Identifier.surround_low_frequency_effects, + "surround_low_frequency_effects", + ) + + def test_module_alias(self): + self.assertIs(otio.schema.StreamIdentifier, StreamInfo.Identifier) + + def test_use_in_available_streams(self): + ref = otio.schema.ExternalReference(target_url="/foo/bar.mov") + ref.set_available_streams( + { + StreamInfo.Identifier.left_eye: StreamInfo( + kind=otio.schema.TrackKind.Video, + address=otio.schema.IndexStreamAddress(0), + ), + StreamInfo.Identifier.stereo_left: StreamInfo( + kind=otio.schema.TrackKind.Audio, + address=otio.schema.IndexStreamAddress(1), + ), + } + ) + streams = ref.available_streams() + self.assertIn(StreamInfo.Identifier.left_eye, streams) + self.assertIn(StreamInfo.Identifier.stereo_left, streams) + + +if __name__ == "__main__": + unittest.main() From 16d2b783a3f910d7aeeaed064cc70f11e225f969 Mon Sep 17 00:00:00 2001 From: Eric Reinecke Date: Fri, 3 Apr 2026 14:32:35 -0700 Subject: [PATCH 2/8] Added StreamChannelIndexStreamAddress Signed-off-by: Eric Reinecke --- src/opentimelineio/CMakeLists.txt | 2 + src/opentimelineio/CORE_VERSION_MAP.cpp | 1 + .../streamChannelIndexStreamAddress.cpp | 35 ++++++++++ .../streamChannelIndexStreamAddress.h | 67 +++++++++++++++++++ src/opentimelineio/typeRegistry.cpp | 2 + .../otio_serializableObjects.cpp | 20 ++++++ .../opentimelineio/schema/__init__.py | 3 + .../stream_channel_index_stream_address.py | 22 ++++++ tests/test_stream_mapping.py | 50 ++++++++++++++ 9 files changed, 202 insertions(+) create mode 100644 src/opentimelineio/streamChannelIndexStreamAddress.cpp create mode 100644 src/opentimelineio/streamChannelIndexStreamAddress.h create mode 100644 src/py-opentimelineio/opentimelineio/schema/stream_channel_index_stream_address.py diff --git a/src/opentimelineio/CMakeLists.txt b/src/opentimelineio/CMakeLists.txt index 46f59f0c4f..35dc6fe60b 100644 --- a/src/opentimelineio/CMakeLists.txt +++ b/src/opentimelineio/CMakeLists.txt @@ -32,6 +32,7 @@ set(OPENTIMELINEIO_HEADER_FILES stackAlgorithm.h indexStreamAddress.h streamAddress.h + streamChannelIndexStreamAddress.h streamInfo.h stringStreamAddress.h streamSelector.h @@ -74,6 +75,7 @@ add_library(opentimelineio ${OTIO_SHARED_OR_STATIC_LIB} stackAlgorithm.cpp indexStreamAddress.cpp streamAddress.cpp + streamChannelIndexStreamAddress.cpp streamInfo.cpp stringStreamAddress.cpp streamSelector.cpp diff --git a/src/opentimelineio/CORE_VERSION_MAP.cpp b/src/opentimelineio/CORE_VERSION_MAP.cpp index 1a1ab50894..9f37296758 100644 --- a/src/opentimelineio/CORE_VERSION_MAP.cpp +++ b/src/opentimelineio/CORE_VERSION_MAP.cpp @@ -221,6 +221,7 @@ const label_to_schema_version_map CORE_VERSION_MAP{ { "HookScript", 1 }, { "ImageSequenceReference", 1 }, { "IndexStreamAddress", 1 }, + { "StreamChannelIndexStreamAddress", 1 }, { "Item", 1 }, { "LinearTimeWarp", 1 }, { "Marker", 2 }, diff --git a/src/opentimelineio/streamChannelIndexStreamAddress.cpp b/src/opentimelineio/streamChannelIndexStreamAddress.cpp new file mode 100644 index 0000000000..b4c4e26847 --- /dev/null +++ b/src/opentimelineio/streamChannelIndexStreamAddress.cpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#include "opentimelineio/streamChannelIndexStreamAddress.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION_NS { + +StreamChannelIndexStreamAddress::StreamChannelIndexStreamAddress( + int64_t stream_index, + int64_t channel_index) + : Parent() + , _stream_index(stream_index) + , _channel_index(channel_index) +{} + +StreamChannelIndexStreamAddress::~StreamChannelIndexStreamAddress() +{} + +bool +StreamChannelIndexStreamAddress::read_from(Reader& reader) +{ + return reader.read("stream_index", &_stream_index) + && reader.read("channel_index", &_channel_index) + && Parent::read_from(reader); +} + +void +StreamChannelIndexStreamAddress::write_to(Writer& writer) const +{ + Parent::write_to(writer); + writer.write("stream_index", _stream_index); + writer.write("channel_index", _channel_index); +} + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION_NS diff --git a/src/opentimelineio/streamChannelIndexStreamAddress.h b/src/opentimelineio/streamChannelIndexStreamAddress.h new file mode 100644 index 0000000000..65f4a9fa5a --- /dev/null +++ b/src/opentimelineio/streamChannelIndexStreamAddress.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#pragma once + +#include "opentimelineio/streamAddress.h" +#include "opentimelineio/version.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION_NS { + +/// @brief Addresses a stream by track index and channel index within that track. +/// +/// Use this for container formats that organise media into discrete tracks, +/// each of which may contain one or more channels — for example MP4/MOV and +/// MXF. The `stream_index` identifies the track and the `channel_index` +/// identifies the channel within that track. +class OTIO_API_TYPE StreamChannelIndexStreamAddress : public StreamAddress +{ +public: + /// @brief This struct provides the StreamChannelIndexStreamAddress schema. + struct Schema + { + static char constexpr name[] = "StreamChannelIndexStreamAddress"; + static int constexpr version = 1; + }; + + using Parent = StreamAddress; + + /// @brief Create a new StreamChannelIndexStreamAddress. + /// + /// @param stream_index Integer index of the media track within its + /// container (e.g. the ffmpeg/libav stream index). + /// @param channel_index Integer index of the channel within that track. + OTIO_API explicit StreamChannelIndexStreamAddress( + int64_t stream_index = 0, + int64_t channel_index = 0); + + /// @brief Return the stream (track) index. + OTIO_API int64_t stream_index() const noexcept { return _stream_index; } + + /// @brief Set the stream (track) index. + OTIO_API void set_stream_index(int64_t stream_index) noexcept + { + _stream_index = stream_index; + } + + /// @brief Return the channel index within the stream. + OTIO_API int64_t channel_index() const noexcept { return _channel_index; } + + /// @brief Set the channel index within the stream. + OTIO_API void set_channel_index(int64_t channel_index) noexcept + { + _channel_index = channel_index; + } + +protected: + virtual ~StreamChannelIndexStreamAddress(); + + bool read_from(Reader&) override; + void write_to(Writer&) const override; + +private: + int64_t _stream_index; + int64_t _channel_index; +}; + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION_NS diff --git a/src/opentimelineio/typeRegistry.cpp b/src/opentimelineio/typeRegistry.cpp index 8a2a2e281c..2af6e3426c 100644 --- a/src/opentimelineio/typeRegistry.cpp +++ b/src/opentimelineio/typeRegistry.cpp @@ -25,6 +25,7 @@ #include "opentimelineio/stack.h" #include "opentimelineio/indexStreamAddress.h" #include "opentimelineio/streamAddress.h" +#include "opentimelineio/streamChannelIndexStreamAddress.h" #include "opentimelineio/streamInfo.h" #include "opentimelineio/stringStreamAddress.h" #include "opentimelineio/streamSelector.h" @@ -90,6 +91,7 @@ TypeRegistry::TypeRegistry() 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 f4ee1b7023..9f4eb3bd3d 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/stack.h" #include "opentimelineio/indexStreamAddress.h" #include "opentimelineio/streamAddress.h" +#include "opentimelineio/streamChannelIndexStreamAddress.h" #include "opentimelineio/streamInfo.h" #include "opentimelineio/stringStreamAddress.h" #include "opentimelineio/streamSelector.h" @@ -723,6 +724,25 @@ static void define_stream_address_and_info(py::module m) { .def_property("index", &IndexStreamAddress::index, &IndexStreamAddress::set_index, "Integer index identifying the stream within its container."); + py::class_>( + m, "StreamChannelIndexStreamAddress", py::dynamic_attr(), + "Addresses a stream by track index and channel index within that track. " + "Use this for container formats that organise media into discrete tracks " + "each of which may contain one or more channels, such as MP4/MOV and MXF.") + .def(py::init([](int64_t stream_index, int64_t channel_index) { + return new StreamChannelIndexStreamAddress(stream_index, channel_index); }), + "stream_index"_a = 0LL, + "channel_index"_a = 0LL) + .def_property("stream_index", + &StreamChannelIndexStreamAddress::stream_index, + &StreamChannelIndexStreamAddress::set_stream_index, + "Integer index of the media track within its container.") + .def_property("channel_index", + &StreamChannelIndexStreamAddress::channel_index, + &StreamChannelIndexStreamAddress::set_channel_index, + "Integer index of the channel within the stream."); + py::class_>(m, "StringStreamAddress", py::dynamic_attr(), "Addresses a stream by string identifier (e.g. channel label).") diff --git a/src/py-opentimelineio/opentimelineio/schema/__init__.py b/src/py-opentimelineio/opentimelineio/schema/__init__.py index 977da70a11..6d2e197fd5 100644 --- a/src/py-opentimelineio/opentimelineio/schema/__init__.py +++ b/src/py-opentimelineio/opentimelineio/schema/__init__.py @@ -18,6 +18,7 @@ GeneratorReference, ImageSequenceReference, IndexStreamAddress, + StreamChannelIndexStreamAddress, Marker, MissingReference, SerializableCollection, @@ -51,6 +52,7 @@ generator_reference, image_sequence_reference, index_stream_address, + stream_channel_index_stream_address, marker, serializable_collection, stream_address, @@ -81,6 +83,7 @@ def timeline_from_clips(clips): 'GeneratorReference', 'ImageSequenceReference', 'IndexStreamAddress', + 'StreamChannelIndexStreamAddress', 'Marker', 'MissingReference', 'SerializableCollection', diff --git a/src/py-opentimelineio/opentimelineio/schema/stream_channel_index_stream_address.py b/src/py-opentimelineio/opentimelineio/schema/stream_channel_index_stream_address.py new file mode 100644 index 0000000000..64887247db --- /dev/null +++ b/src/py-opentimelineio/opentimelineio/schema/stream_channel_index_stream_address.py @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +from .. core._core_utils import add_method +from .. import _otio + + +@add_method(_otio.StreamChannelIndexStreamAddress) +def __str__(self): + return "StreamChannelIndexStreamAddress({}, {})".format( + self.stream_index, self.channel_index + ) + + +@add_method(_otio.StreamChannelIndexStreamAddress) +def __repr__(self): + return ( + "otio.schema.StreamChannelIndexStreamAddress(" + "stream_index={}, channel_index={})".format( + repr(self.stream_index), repr(self.channel_index) + ) + ) diff --git a/tests/test_stream_mapping.py b/tests/test_stream_mapping.py index a11060f5fe..ab5f261ec7 100644 --- a/tests/test_stream_mapping.py +++ b/tests/test_stream_mapping.py @@ -32,6 +32,56 @@ def test_round_trip_serialization(self): self.assertEqual(restored.index, 5) +class TestStreamChannelIndexStreamAddress(unittest.TestCase): + def test_create_default(self): + addr = otio.schema.StreamChannelIndexStreamAddress() + self.assertEqual(addr.stream_index, 0) + self.assertEqual(addr.channel_index, 0) + self.assertEqual(addr.schema_name(), "StreamChannelIndexStreamAddress") + self.assertEqual(addr.schema_version(), 1) + + def test_create_with_indices(self): + addr = otio.schema.StreamChannelIndexStreamAddress( + stream_index=1, channel_index=2 + ) + self.assertEqual(addr.stream_index, 1) + self.assertEqual(addr.channel_index, 2) + + def test_set_indices(self): + addr = otio.schema.StreamChannelIndexStreamAddress() + addr.stream_index = 3 + addr.channel_index = 5 + self.assertEqual(addr.stream_index, 3) + self.assertEqual(addr.channel_index, 5) + + def test_round_trip_serialization(self): + addr = otio.schema.StreamChannelIndexStreamAddress( + stream_index=1, channel_index=2 + ) + json_str = addr.to_json_string() + restored = otio.adapters.read_from_string(json_str, "otio_json") + self.assertIsInstance(restored, otio.schema.StreamChannelIndexStreamAddress) + self.assertEqual(restored.stream_index, 1) + self.assertEqual(restored.channel_index, 2) + + def test_use_in_stream_info(self): + addr = otio.schema.StreamChannelIndexStreamAddress( + stream_index=1, channel_index=2 + ) + info = StreamInfo( + name="right surround", + address=addr, + kind=otio.schema.TrackKind.Audio, + ) + json_str = info.to_json_string() + restored = otio.adapters.read_from_string(json_str, "otio_json") + self.assertIsInstance( + restored.address, otio.schema.StreamChannelIndexStreamAddress + ) + self.assertEqual(restored.address.stream_index, 1) + self.assertEqual(restored.address.channel_index, 2) + + class TestStringStreamAddress(unittest.TestCase): def test_create_default(self): addr = otio.schema.StringStreamAddress() From 9c866cc9defb07cb8c7c05c91a8daa2b038d238a Mon Sep 17 00:00:00 2001 From: Eric Reinecke Date: Fri, 3 Apr 2026 15:41:51 -0700 Subject: [PATCH 3/8] Added StreamMapper effect Signed-off-by: Eric Reinecke --- src/opentimelineio/CMakeLists.txt | 2 + src/opentimelineio/CORE_VERSION_MAP.cpp | 1 + src/opentimelineio/streamMapper.cpp | 34 +++++++ src/opentimelineio/streamMapper.h | 77 ++++++++++++++++ src/opentimelineio/typeRegistry.cpp | 2 + .../otio_serializableObjects.cpp | 27 ++++++ .../opentimelineio/schema/__init__.py | 3 + .../opentimelineio/schema/stream_mapper.py | 15 ++++ tests/test_stream_mapping.py | 89 +++++++++++++++++++ 9 files changed, 250 insertions(+) create mode 100644 src/opentimelineio/streamMapper.cpp create mode 100644 src/opentimelineio/streamMapper.h create mode 100644 src/py-opentimelineio/opentimelineio/schema/stream_mapper.py diff --git a/src/opentimelineio/CMakeLists.txt b/src/opentimelineio/CMakeLists.txt index 35dc6fe60b..7480b700cf 100644 --- a/src/opentimelineio/CMakeLists.txt +++ b/src/opentimelineio/CMakeLists.txt @@ -35,6 +35,7 @@ set(OPENTIMELINEIO_HEADER_FILES streamChannelIndexStreamAddress.h streamInfo.h stringStreamAddress.h + streamMapper.h streamSelector.h audioMixMatrix.h timeEffect.h @@ -78,6 +79,7 @@ add_library(opentimelineio ${OTIO_SHARED_OR_STATIC_LIB} streamChannelIndexStreamAddress.cpp streamInfo.cpp stringStreamAddress.cpp + streamMapper.cpp streamSelector.cpp audioMixMatrix.cpp stringUtils.cpp diff --git a/src/opentimelineio/CORE_VERSION_MAP.cpp b/src/opentimelineio/CORE_VERSION_MAP.cpp index 9f37296758..49d88e28ae 100644 --- a/src/opentimelineio/CORE_VERSION_MAP.cpp +++ b/src/opentimelineio/CORE_VERSION_MAP.cpp @@ -236,6 +236,7 @@ const label_to_schema_version_map CORE_VERSION_MAP{ { "Stack", 1 }, { "StreamAddress", 1 }, { "StreamInfo", 1 }, + { "StreamMapper", 1 }, { "StreamSelector", 1 }, { "StringStreamAddress", 1 }, { "Test", 1 }, diff --git a/src/opentimelineio/streamMapper.cpp b/src/opentimelineio/streamMapper.cpp new file mode 100644 index 0000000000..a85af15cfb --- /dev/null +++ b/src/opentimelineio/streamMapper.cpp @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#include "opentimelineio/streamMapper.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION_NS { + +StreamMapper::StreamMapper( + std::string const& name, + std::string const& effect_name, + StreamMap const& stream_map, + AnyDictionary const& metadata) + : Parent(name, effect_name, metadata) + , _stream_map(stream_map) +{} + +StreamMapper::~StreamMapper() +{} + +bool +StreamMapper::read_from(Reader& reader) +{ + return reader.read_if_present("stream_map", &_stream_map) + && Parent::read_from(reader); +} + +void +StreamMapper::write_to(Writer& writer) const +{ + Parent::write_to(writer); + writer.write("stream_map", _stream_map); +} + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION_NS diff --git a/src/opentimelineio/streamMapper.h b/src/opentimelineio/streamMapper.h new file mode 100644 index 0000000000..de14a2efad --- /dev/null +++ b/src/opentimelineio/streamMapper.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#pragma once + +#include "opentimelineio/effect.h" +#include "opentimelineio/version.h" + +#include +#include + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION_NS { + +/// @brief An effect that remaps stream identifiers to new names. +/// +/// Each entry in `stream_map` maps an output stream name (the key as it will +/// appear downstream) to an input stream name (the key as it appears in the +/// upstream `MediaReference::available_streams`). +/// +/// A typical use is to normalize a source-specific identifier into a +/// well-known `StreamInfo::Identifier` value — for example, to expose the +/// left eye of a stereo source as the conventional monocular stream: +/// +/// @code{.json} +/// { +/// "monocular": "left_eye" +/// } +/// @endcode +class OTIO_API_TYPE StreamMapper : public Effect +{ +public: + /// @brief This struct provides the StreamMapper schema. + struct Schema + { + static char constexpr name[] = "StreamMapper"; + static int constexpr version = 1; + }; + + using Parent = Effect; + + using StreamMap = std::map; + + /// @brief Create a new StreamMapper. + /// + /// @param name The name of the effect object. + /// @param effect_name The name of the effect. + /// @param stream_map Mapping of output stream name to input stream name. + /// @param metadata Optional metadata dictionary. + OTIO_API StreamMapper( + std::string const& name = std::string(), + std::string const& effect_name = std::string(), + StreamMap const& stream_map = StreamMap(), + AnyDictionary const& metadata = AnyDictionary()); + + /// @brief Return the stream name mapping. + /// + /// Keys are output stream names (as seen downstream); values are input + /// stream names (keys in the upstream available_streams map). + OTIO_API StreamMap const& stream_map() const noexcept { return _stream_map; } + + /// @brief Set the stream name mapping. + OTIO_API void set_stream_map(StreamMap const& stream_map) + { + _stream_map = stream_map; + } + +protected: + virtual ~StreamMapper(); + + bool read_from(Reader&) override; + void write_to(Writer&) const override; + +private: + StreamMap _stream_map; +}; + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION_NS diff --git a/src/opentimelineio/typeRegistry.cpp b/src/opentimelineio/typeRegistry.cpp index 2af6e3426c..94cd5b36fd 100644 --- a/src/opentimelineio/typeRegistry.cpp +++ b/src/opentimelineio/typeRegistry.cpp @@ -28,6 +28,7 @@ #include "opentimelineio/streamChannelIndexStreamAddress.h" #include "opentimelineio/streamInfo.h" #include "opentimelineio/stringStreamAddress.h" +#include "opentimelineio/streamMapper.h" #include "opentimelineio/streamSelector.h" #include "opentimelineio/timeEffect.h" #include "opentimelineio/timeline.h" @@ -94,6 +95,7 @@ TypeRegistry::TypeRegistry() 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 9f4eb3bd3d..6f77b5c1aa 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp @@ -28,6 +28,7 @@ #include "opentimelineio/streamChannelIndexStreamAddress.h" #include "opentimelineio/streamInfo.h" #include "opentimelineio/stringStreamAddress.h" +#include "opentimelineio/streamMapper.h" #include "opentimelineio/streamSelector.h" #include "opentimelineio/timeEffect.h" #include "opentimelineio/timeline.h" @@ -816,6 +817,32 @@ static void define_stream_effects(py::module m) { &StreamSelector::set_output_streams, "List of stream identifier strings to select."); + py::class_>(m, "StreamMapper", py::dynamic_attr(), + "An effect that remaps stream identifiers to new names. " + "Each entry in stream_map maps an output stream name (the key as it will " + "appear downstream) to an input stream name (the key as it appears in the " + "upstream MediaReference available_streams). " + "A typical use is to normalize a source-specific identifier into a " + "well-known StreamInfo.Identifier value -- for example, to expose the " + "left eye of a stereo source as the conventional monocular stream.") + .def(py::init([](std::string name, + std::string effect_name, + StreamMapper::StreamMap stream_map, + py::object metadata) { + return new StreamMapper(name, effect_name, stream_map, + py_to_any_dictionary(metadata)); }), + py::arg_v("name"_a = std::string()), + "effect_name"_a = std::string(), + "stream_map"_a = StreamMapper::StreamMap(), + py::arg_v("metadata"_a = py::none())) + .def_property("stream_map", + &StreamMapper::stream_map, + &StreamMapper::set_stream_map, + "Mapping of output stream name to input stream name. " + "Keys SHOULD use StreamInfo.Identifier values where applicable; " + "values SHOULD match keys in the upstream available_streams map."); + py::class_>(m, "AudioMixMatrix", py::dynamic_attr(), "An effect that mixes audio streams using a coefficient matrix. " diff --git a/src/py-opentimelineio/opentimelineio/schema/__init__.py b/src/py-opentimelineio/opentimelineio/schema/__init__.py index 6d2e197fd5..69d848ca2a 100644 --- a/src/py-opentimelineio/opentimelineio/schema/__init__.py +++ b/src/py-opentimelineio/opentimelineio/schema/__init__.py @@ -25,6 +25,7 @@ Stack, StreamAddress, StreamInfo, + StreamMapper, StreamSelector, StringStreamAddress, Timeline, @@ -57,6 +58,7 @@ serializable_collection, stream_address, stream_info, + stream_mapper, stream_selector, string_stream_address, timeline, @@ -92,6 +94,7 @@ def timeline_from_clips(clips): 'StreamAddress', 'StreamIdentifier', 'StreamInfo', + 'StreamMapper', 'StreamSelector', 'StringStreamAddress', 'Timeline', diff --git a/src/py-opentimelineio/opentimelineio/schema/stream_mapper.py b/src/py-opentimelineio/opentimelineio/schema/stream_mapper.py new file mode 100644 index 0000000000..61e787cc8e --- /dev/null +++ b/src/py-opentimelineio/opentimelineio/schema/stream_mapper.py @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +from .. core._core_utils import add_method +from .. import _otio + + +@add_method(_otio.StreamMapper) +def __str__(self): + return "StreamMapper({})".format(str(self.name)) + + +@add_method(_otio.StreamMapper) +def __repr__(self): + return "otio.schema.StreamMapper(name={})".format(repr(self.name)) diff --git a/tests/test_stream_mapping.py b/tests/test_stream_mapping.py index ab5f261ec7..26b1edcc94 100644 --- a/tests/test_stream_mapping.py +++ b/tests/test_stream_mapping.py @@ -338,5 +338,94 @@ def test_use_in_available_streams(self): self.assertIn(StreamInfo.Identifier.stereo_left, streams) +class TestStreamMapper(unittest.TestCase): + def test_create_default(self): + mapper = otio.schema.StreamMapper() + self.assertEqual(mapper.stream_map, {}) + self.assertEqual(mapper.schema_name(), "StreamMapper") + self.assertEqual(mapper.schema_version(), 1) + + def test_create_with_map(self): + stream_map = { + StreamInfo.Identifier.monocular: StreamInfo.Identifier.left_eye + } + mapper = otio.schema.StreamMapper(stream_map=stream_map) + self.assertEqual(mapper.stream_map, stream_map) + + def test_set_map(self): + mapper = otio.schema.StreamMapper() + stream_map = { + StreamInfo.Identifier.stereo_left: StreamInfo.Identifier.surround_left_front, + StreamInfo.Identifier.stereo_right: StreamInfo.Identifier.surround_right_front, + } + mapper.stream_map = stream_map + self.assertEqual(mapper.stream_map, stream_map) + + def test_round_trip_serialization(self): + stream_map = { + StreamInfo.Identifier.monocular: StreamInfo.Identifier.left_eye + } + mapper = otio.schema.StreamMapper( + name="remap_to_mono", + stream_map=stream_map, + ) + json_str = mapper.to_json_string() + restored = otio.adapters.read_from_string(json_str, "otio_json") + self.assertIsInstance(restored, otio.schema.StreamMapper) + self.assertEqual(restored.name, "remap_to_mono") + self.assertEqual(restored.stream_map, stream_map) + + def test_left_eye_to_monocular_use_case(self): + """StreamMapper can remap left_eye to monocular for downstream consumers.""" + clip = otio.schema.Clip( + name="stereo_shot", + media_reference=otio.schema.ExternalReference( + target_url="/path/to/stereo.mov" + ), + effects=[ + otio.schema.StreamMapper( + stream_map={ + StreamInfo.Identifier.monocular: StreamInfo.Identifier.left_eye + } + ) + ] + ) + mapper = clip.effects[0] + self.assertIsInstance(mapper, otio.schema.StreamMapper) + self.assertEqual( + mapper.stream_map[StreamInfo.Identifier.monocular], + StreamInfo.Identifier.left_eye + ) + + def test_use_as_clip_effect(self): + """StreamMapper round-trips correctly when embedded in a clip.""" + clip = otio.schema.Clip( + name="remapped_clip", + media_reference=otio.schema.ExternalReference( + target_url="/path/to/source.mov" + ), + effects=[ + otio.schema.StreamMapper( + name="a very bad stereo downmix", + stream_map={ + StreamInfo.Identifier.stereo_left: StreamInfo.Identifier.surround_left_front, + StreamInfo.Identifier.stereo_right: StreamInfo.Identifier.surround_right_front, + } + ) + ] + ) + json_str = clip.to_json_string() + restored = otio.adapters.read_from_string(json_str, "otio_json") + self.assertIsInstance(restored, otio.schema.Clip) + self.assertEqual(len(restored.effects), 1) + mapper = restored.effects[0] + self.assertIsInstance(mapper, otio.schema.StreamMapper) + self.assertEqual(mapper.name, "A very bad stereo downmix") + self.assertEqual( + mapper.stream_map[StreamInfo.Identifier.stereo_left], + StreamInfo.Identifier.surround_left_front + ) + + if __name__ == "__main__": unittest.main() From 3cfc7f3b9e070a0d6ac02ba63dc0be31181fdb2c Mon Sep 17 00:00:00 2001 From: Eric Reinecke Date: Fri, 3 Apr 2026 15:59:05 -0700 Subject: [PATCH 4/8] Updated constructors and reprs to better match expectations Signed-off-by: Eric Reinecke --- .../otio_serializableObjects.cpp | 8 ++------ .../opentimelineio/schema/stream_mapper.py | 20 +++++++++++++++++-- .../opentimelineio/schema/stream_selector.py | 4 ---- tests/test_stream_mapping.py | 5 ++--- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp index 6f77b5c1aa..aeb514da1e 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp @@ -803,13 +803,11 @@ static void define_stream_effects(py::module m) { "Use this to select a stereo view, specific audio channels, etc. " "The item will expose these streams downstream with the same naming.") .def(py::init([](std::string name, - std::string effect_name, std::vector output_streams, py::object metadata) { - return new StreamSelector(name, effect_name, output_streams, + return new StreamSelector(name, "StreamSelector", output_streams, py_to_any_dictionary(metadata)); }), py::arg_v("name"_a = std::string()), - "effect_name"_a = std::string(), "output_streams"_a = std::vector(), py::arg_v("metadata"_a = py::none())) .def_property("output_streams", @@ -827,13 +825,11 @@ static void define_stream_effects(py::module m) { "well-known StreamInfo.Identifier value -- for example, to expose the " "left eye of a stereo source as the conventional monocular stream.") .def(py::init([](std::string name, - std::string effect_name, StreamMapper::StreamMap stream_map, py::object metadata) { - return new StreamMapper(name, effect_name, stream_map, + return new StreamMapper(name, "StreamMapper", stream_map, py_to_any_dictionary(metadata)); }), py::arg_v("name"_a = std::string()), - "effect_name"_a = std::string(), "stream_map"_a = StreamMapper::StreamMap(), py::arg_v("metadata"_a = py::none())) .def_property("stream_map", diff --git a/src/py-opentimelineio/opentimelineio/schema/stream_mapper.py b/src/py-opentimelineio/opentimelineio/schema/stream_mapper.py index 61e787cc8e..25a89bde4e 100644 --- a/src/py-opentimelineio/opentimelineio/schema/stream_mapper.py +++ b/src/py-opentimelineio/opentimelineio/schema/stream_mapper.py @@ -7,9 +7,25 @@ @add_method(_otio.StreamMapper) def __str__(self): - return "StreamMapper({})".format(str(self.name)) + return ( + "StreamMapper(" + "{}, " + "{}" + ")".format( + str(self.name), + str(self.stream_map), + ) + ) @add_method(_otio.StreamMapper) def __repr__(self): - return "otio.schema.StreamMapper(name={})".format(repr(self.name)) + return ( + "otio.schema.StreamMapper(" + "name={}, " + "stream_map={}" + ")".format( + repr(self.name), + repr(self.stream_map), + ) + ) diff --git a/src/py-opentimelineio/opentimelineio/schema/stream_selector.py b/src/py-opentimelineio/opentimelineio/schema/stream_selector.py index 4f9c753101..41dbd7e205 100644 --- a/src/py-opentimelineio/opentimelineio/schema/stream_selector.py +++ b/src/py-opentimelineio/opentimelineio/schema/stream_selector.py @@ -10,11 +10,9 @@ def __str__(self): return ( "StreamSelector(" "{}, " - "{}, " "{}" ")".format( str(self.name), - str(self.effect_name), str(self.output_streams), ) ) @@ -25,11 +23,9 @@ def __repr__(self): return ( "otio.schema.StreamSelector(" "name={}, " - "effect_name={}, " "output_streams={}" ")".format( repr(self.name), - repr(self.effect_name), repr(self.output_streams), ) ) diff --git a/tests/test_stream_mapping.py b/tests/test_stream_mapping.py index 26b1edcc94..3095e0ab7e 100644 --- a/tests/test_stream_mapping.py +++ b/tests/test_stream_mapping.py @@ -159,13 +159,12 @@ class TestStreamSelector(unittest.TestCase): def test_create_default(self): sel = otio.schema.StreamSelector() self.assertEqual(sel.name, "") - self.assertEqual(sel.effect_name, "") + self.assertEqual(sel.effect_name, "StreamSelector") self.assertEqual(sel.output_streams, []) def test_create_with_streams(self): sel = otio.schema.StreamSelector( name="stereo_select", - effect_name="StreamSelector", output_streams=[ StreamInfo.Identifier.stereo_left, StreamInfo.Identifier.stereo_right, @@ -420,7 +419,7 @@ def test_use_as_clip_effect(self): self.assertEqual(len(restored.effects), 1) mapper = restored.effects[0] self.assertIsInstance(mapper, otio.schema.StreamMapper) - self.assertEqual(mapper.name, "A very bad stereo downmix") + self.assertEqual(mapper.name, "a very bad stereo downmix") self.assertEqual( mapper.stream_map[StreamInfo.Identifier.stereo_left], StreamInfo.Identifier.surround_left_front From e08d0cd42a43987eb5c1720a49802dc43888d9fb Mon Sep 17 00:00:00 2001 From: Eric Reinecke Date: Fri, 3 Apr 2026 17:05:49 -0700 Subject: [PATCH 5/8] Added/updated docs and version map Signed-off-by: Eric Reinecke --- docs/index.rst | 1 + docs/tutorials/channel-mapping.md | 373 ++++++++++++++++++ .../otio-serialized-schema-only-fields.md | 15 + docs/tutorials/otio-serialized-schema.md | 57 ++- src/opentimelineio/CORE_VERSION_MAP.cpp | 2 +- 5 files changed, 439 insertions(+), 9 deletions(-) create mode 100644 docs/tutorials/channel-mapping.md diff --git a/docs/index.rst b/docs/index.rst index f8fcf55b97..5c8737f057 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -42,6 +42,7 @@ Tutorials tutorials/feature-matrix tutorials/otio-timeline-structure tutorials/time-ranges + tutorials/channel-mapping tutorials/otio-filebundles tutorials/write-an-adapter tutorials/write-a-media-linker diff --git a/docs/tutorials/channel-mapping.md b/docs/tutorials/channel-mapping.md new file mode 100644 index 0000000000..18733c9a16 --- /dev/null +++ b/docs/tutorials/channel-mapping.md @@ -0,0 +1,373 @@ +# Stream and Channel Mapping + +## Overview + +OTIO's stream and channel mapping support allows media references to describe the individual streams they contain (video eyes, audio channels, etc.) and for timeline items to select or remix those streams via effects. The system has three layers: **addressing** a stream within a container, **describing** a stream's semantic role, and **selecting or mixing** streams on a clip. + +--- + +## Layer 1: Addressing a Stream Within a Container + +A `StreamAddress` identifies where a specific stream lives inside a media file. The appropriate `StreamAddress` subclass depends on how the container format identifies its streams. The `StreamAddress` subclass should be chosen based on how closely it matches the container's native identification scheme. + +| Class | Use when | +|---|---| +| `IndexStreamAddress` | The container uses a single integer index per stream (e.g. WAV channel index, ffmpeg stream index) | +| `StreamChannelIndexStreamAddress` | The container organises media into tracks, each with one or more channels (e.g. MP4/MOV, MXF) | +| `StringStreamAddress` | The container uses string identifiers (e.g. codec name, channel label, USD scene path) | + +### Format reference + +Below is a chart of well-known media formats and the recommended `StreamAddress` subclass usage for each. + +| Format | StreamAddress class | Notes | +|---|---|--------------------------------------------------------------------------------------------| +| WAV | `IndexStreamAddress` | | +| AIFF | `IndexStreamAddress` | | +| MOV / MP4 | `StreamChannelIndexStreamAddress` | When there is only one channel in a track (e.g. single-view video), use `channel_index=0`. | +| USD | `StringStreamAddress` | Use the USD path. | +| PNG, JPEG, TIFF, GIF | *(single-image formats — no stream address needed)* | | + +The above is not an exhaustive list. If you are working with a format not listed here, consider how its streams are identified and choose the `StreamAddress` subclass that best matches that scheme. + +### StreamAddress + +`StreamAddress` is the base class for all stream address types. You will not instantiate it directly; use one of the concrete subclasses below. + +### IndexStreamAddress + +Addresses a stream by a single integer index. Use for formats where each stream is identified by a flat index (e.g. a WAV file's channel index). + +```python +# Stream 2 of a WAV file +addr = otio.schema.IndexStreamAddress(index=2) +``` + +### StreamChannelIndexStreamAddress + +Addresses a stream by a track index and a channel index within that track. Use for container formats that organise media into named or numbered tracks, each potentially containing multiple channels (MOV, MP4). + +```python +# Stream 1, channel 2 of an MP4 +addr = otio.schema.StreamChannelIndexStreamAddress(stream_index=1, channel_index=2) +``` + +When a track contains only one channel (e.g. a single-view video track), use `channel_index=0`. + +### StringStreamAddress + +Addresses a stream by a string label or path. Use for formats where streams are identified by name rather than numeric index (e.g. a USD scene path, a codec channel label). + +```python +# A camera in a USD scene +addr = otio.schema.StringStreamAddress(address="/World/set/cam_left") +``` + +--- + +## Layer 2: Describing a Stream's Semantic Role + +Once you can locate a stream, the next layer describes what that stream *is* — its media kind, human-readable name, and semantic role. This information lives on the media reference and is keyed by well-known identifier strings. + +### StreamInfo + +`StreamInfo` describes a single media stream within a source. It is the smallest unit of temporal media — one eye's video, one audio channel, one camera view. + +**Fields:** + +- **`name`** — Human-readable label for this stream. May be any descriptive string (e.g. `"Alice lavalier (boom backup)"`, `"left eye"`). Not a semantic identifier. +- **`address`** — A `StreamAddress` subclass locating the stream in its container. +- **`kind`** — The type of media. Favor constants from `otio.schema.TrackKind` wherever appropriate. +- **`metadata`** — Arbitrary metadata dictionary for additional properties (e.g. ITU-R channel labels). + +```python +stream = otio.schema.StreamInfo( + name="Low Frequency Effects", + address=otio.schema.StreamChannelIndexStreamAddress(stream_index=1, channel_index=3), + kind=otio.schema.TrackKind.Audio, + metadata={ + "ITU_label": "LFE", + }, +) +``` + +### StreamInfo.Identifier + +`StreamInfo.Identifier` provides well-known strings that identify the **semantic role** of a stream — what presentation device it targets. These constants are the canonical keys for `available_streams`. + +`otio.schema.StreamIdentifier` is an alias for `StreamInfo.Identifier`. + +#### Visual streams + +| Constant | Meaning | +|---|---| +| `monocular` | A traditional 2D video stream (single-view) | +| `left_eye` | Left eye in a stereo 3D pair | +| `right_eye` | Right eye in a stereo 3D pair | + +#### Simple aural streams + +| Constant | Meaning | +|---|---| +| `monaural` | Single mono audio channel | +| `stereo_left` | Left channel of a stereo pair | +| `stereo_right` | Right channel of a stereo pair | + +> **Note:** Stereo and surround channels are kept separate because presenting the left/right channels of a surround mix as stereo is generally not a desirable result. + +#### Surround aural streams (loosely based on ITU-R BS.2051-3) + +| Constant | ITU label | +|---|---| +| `surround_left_front` | L | +| `surround_right_front` | R | +| `surround_center_front` | C | +| `surround_low_frequency_effects` | LFE | +| `surround_left_rear` | Ls | +| `surround_right_rear` | Rs | + +### MediaReference.available_streams + +`available_streams()` returns a dict keyed by stream identifier string mapping to `StreamInfo` objects. Use `set_available_streams()` to populate it. + +#### Key conventions + +1. **Presentation-ready streams** SHOULD use an `Identifier` constant directly as the key. This signals to consumers which stream serves a well-known role. + + ```python + ref.set_available_streams({ + StreamInfo.Identifier.monocular: otio.schema.StreamInfo( + name="main camera", + address=otio.schema.IndexStreamAddress(index=0), + kind=otio.schema.TrackKind.Video, + ), + StreamInfo.Identifier.stereo_left: otio.schema.StreamInfo( + name="mix L", + address=otio.schema.IndexStreamAddress(index=1), + kind=otio.schema.TrackKind.Audio, + ), + }) + ``` + +2. **Additional streams** SHOULD prefix an `Identifier` value to form a unique key within the media reference. + + ```python + # A music stem alongside the audio mix + "music_stereo_left": otio.schema.StreamInfo(...) + ``` + +3. **Arbitrary streams** MAY use any unique string when no well-known identifier applies — for example, spatial audio object IDs, or production audio tracks identified by mic placement. + + ```python + "alice_lavalier": otio.schema.StreamInfo(...) + ``` + +When generating these keys, try to avoid names that might collide with future `Identifier` constants unless you think your use case is in line with the precedent in that constant. In these cases, consider submitting a new constant to OTIO to include. + +--- + +## Layer 3: Selecting or Mixing Streams on a Clip + +Stream effects are `Effect` subclasses that attach to a clip's `effects` list and transform which streams are visible downstream and under what names. They operate on the stream identifiers declared in `available_streams`. + +### StreamSelector + +`StreamSelector` selects a subset of named streams from a clip and exposes them downstream with the **same names**. Use it to filter away streams you do not need without renaming them. + +- **`output_streams`** — List of stream identifier strings to pass through. Values SHOULD be `Identifier` constants. + +```python +# Select only the left eye from a stereo 3D clip +clip.effects.append( + otio.schema.StreamSelector( + output_streams=[StreamInfo.Identifier.left_eye] + ) +) + +# Select a stereo pair from an 8 channel (5.1 + Stereo Mixdown) source +clip.effects.append( + otio.schema.StreamSelector( + output_streams=[ + StreamInfo.Identifier.stereo_left, + StreamInfo.Identifier.stereo_right, + ] + ) +) +``` + +### StreamMapper + +`StreamMapper` renames stream identifiers as they pass through a clip. Each entry maps an **output** stream name (as it will appear downstream) to an **input** stream name (the key in the upstream `available_streams` map). + +Use `StreamMapper` when a source uses one identifier but a downstream consumer expects a different one. + +- **`stream_map`** — `{output_name: input_name}` dict. Keys and values SHOULD be `Identifier` constants where applicable. + +```python +# Expose the left eye of a stereo source as the conventional monocular stream +clip.effects.append( + otio.schema.StreamMapper( + stream_map={ + StreamInfo.Identifier.monocular: StreamInfo.Identifier.left_eye + } + ) +) +``` + +After this effect, downstream consumers that look up `StreamInfo.Identifier.monocular` would use the content addressed by the upstream `StreamInfo.Identifier.left_eye` entry. + +### AudioMixMatrix + +`AudioMixMatrix` mixes audio streams using a coefficient matrix. The structure is: + +``` +{ output_stream_name: { input_stream_name: coefficient, ... }, ... } +``` + +- **Output keys** SHOULD use `Identifier` constants where applicable. They correspond to the stream names that will appear in the downstream `available_streams` map after mixing. +- **Input keys** SHOULD match keys in the upstream `MediaReference.available_streams`. + +```python +# Downmix 5.1 surround to Lo/Ro stereo +clip.effects.append( + otio.schema.AudioMixMatrix( + name="5.1_to_stereo", + matrix={ + StreamInfo.Identifier.stereo_left: { + StreamInfo.Identifier.surround_left_front: 1.0, + StreamInfo.Identifier.surround_center_front: 0.707, + StreamInfo.Identifier.surround_left_rear: 0.707, + }, + StreamInfo.Identifier.stereo_right: { + StreamInfo.Identifier.surround_right_front: 1.0, + StreamInfo.Identifier.surround_center_front: 0.707, + StreamInfo.Identifier.surround_right_rear: 0.707, + }, + }, + ) +) +``` + +### Comparison of Stream Effects + +| Effect | Input | Output | Use when | +|---|---|---|---| +| `StreamSelector` | List of stream names to keep | Same streams, same names | You want to filter down to a subset of streams without renaming them | +| `StreamMapper` | `{output_name: input_name}` mapping | Renamed streams | A source uses one identifier but downstream expects a different name | +| `AudioMixMatrix` | `{output_name: {input_name: coefficient}}` matrix | New mixed streams | You need to combine or downmix audio channels with weighting | + +These effects can be combined: for example, use `StreamSelector` to isolate a stereo pair from a source with 8-channel audio, then `AudioMixMatrix` on a downstream clip to downmix it further. + +--- + +## Putting It Together + +### Stereo 3D video clip + 5.1 audio downmix to stereo + +This example models a more complex scenario: a single MOV file +containing stereo 3D video in stream 0 and a 5.1 surround audio mix in +stream 1. Two clips are built from the same reference — one that remaps the +left eye to monocular, and one that downmixes the surround channels to stereo. + +```python +import opentimelineio as otio +from opentimelineio.schema import StreamInfo + +# Single MOV reference. +# Stream 0: stereo 3D video (both eyes packed into one track, e.g. MV-HEVC or scalable AV1). +# Stream 1: 5.1 audio, six channels addressed by channel index. +ref = otio.schema.ExternalReference(target_url="/show/shot.mov") +ref.set_available_streams({ + # Video — stream 0. The Identifier distinguishes which eye to extract. + StreamInfo.Identifier.left_eye: otio.schema.StreamInfo( + name="left eye", + address=otio.schema.StreamChannelIndexStreamAddress( + stream_index=0, channel_index=0 + ), + kind=otio.schema.TrackKind.Video, + ), + StreamInfo.Identifier.right_eye: otio.schema.StreamInfo( + name="right eye", + address=otio.schema.StreamChannelIndexStreamAddress( + stream_index=0, channel_index=1 + ), + kind=otio.schema.TrackKind.Video, + ), + # Audio — stream 1, one entry per surround channel. + StreamInfo.Identifier.surround_left_front: otio.schema.StreamInfo( + name="L", + address=otio.schema.StreamChannelIndexStreamAddress( + stream_index=1, channel_index=0 + ), + kind=otio.schema.TrackKind.Audio, + ), + StreamInfo.Identifier.surround_right_front: otio.schema.StreamInfo( + name="R", + address=otio.schema.StreamChannelIndexStreamAddress( + stream_index=1, channel_index=1 + ), + kind=otio.schema.TrackKind.Audio, + ), + StreamInfo.Identifier.surround_center_front: otio.schema.StreamInfo( + name="C", + address=otio.schema.StreamChannelIndexStreamAddress( + stream_index=1, channel_index=2 + ), + kind=otio.schema.TrackKind.Audio, + ), + StreamInfo.Identifier.surround_low_frequency_effects: otio.schema.StreamInfo( + name="LFE", + address=otio.schema.StreamChannelIndexStreamAddress( + stream_index=1, channel_index=3 + ), + kind=otio.schema.TrackKind.Audio, + ), + StreamInfo.Identifier.surround_left_rear: otio.schema.StreamInfo( + name="Ls", + address=otio.schema.StreamChannelIndexStreamAddress( + stream_index=1, channel_index=4 + ), + kind=otio.schema.TrackKind.Audio, + ), + StreamInfo.Identifier.surround_right_rear: otio.schema.StreamInfo( + name="Rs", + address=otio.schema.StreamChannelIndexStreamAddress( + stream_index=1, channel_index=5 + ), + kind=otio.schema.TrackKind.Audio, + ), +}) + +# Video clip — remap left_eye to monocular for downstream consumers. +video_clip = otio.schema.Clip(name="shot_001_video", media_reference=ref) +video_clip.effects.append( + otio.schema.StreamMapper( + name="expose_left_eye_as_monocular", + stream_map={ + StreamInfo.Identifier.monocular: StreamInfo.Identifier.left_eye, + } + ) +) + +# Audio clip — downmix 5.1 surround to Lo/Ro stereo. +audio_clip = otio.schema.Clip(name="shot_001_audio", media_reference=ref) +audio_clip.effects.append( + otio.schema.AudioMixMatrix( + name="5.1_to_stereo", + matrix={ + StreamInfo.Identifier.stereo_left: { + StreamInfo.Identifier.surround_left_front: 1.0, + StreamInfo.Identifier.surround_center_front: 0.707, + StreamInfo.Identifier.surround_left_rear: 0.707, + StreamInfo.Identifier.surround_low_frequency_effects: 0.707, + }, + StreamInfo.Identifier.stereo_right: { + StreamInfo.Identifier.surround_right_front: 1.0, + StreamInfo.Identifier.surround_center_front: 0.707, + StreamInfo.Identifier.surround_right_rear: 0.707, + StreamInfo.Identifier.surround_low_frequency_effects: 0.707, + }, + }, + ) +) +``` diff --git a/docs/tutorials/otio-serialized-schema-only-fields.md b/docs/tutorials/otio-serialized-schema-only-fields.md index dbeedf0231..935d8217fc 100644 --- a/docs/tutorials/otio-serialized-schema-only-fields.md +++ b/docs/tutorials/otio-serialized-schema-only-fields.md @@ -276,6 +276,12 @@ parameters: parameters: +### StreamChannelIndexStreamAddress.1 + +parameters: +- *channel_index* +- *stream_index* + ### StreamInfo.1 parameters: @@ -284,6 +290,15 @@ parameters: - *metadata* - *name* +### StreamMapper.1 + +parameters: +- *effect_name* +- *enabled* +- *metadata* +- *name* +- *stream_map* + ### StreamSelector.1 parameters: diff --git a/docs/tutorials/otio-serialized-schema.md b/docs/tutorials/otio-serialized-schema.md index 22004e557b..132625a0f4 100644 --- a/docs/tutorials/otio-serialized-schema.md +++ b/docs/tutorials/otio-serialized-schema.md @@ -293,15 +293,17 @@ parameters: *documentation*: ``` -An effect that mixes audio streams using a coefficient matrix. Output keys SHOULD use -StreamInfo.Identifier values (e.g. left, right). Input keys identify source streams and SHOULD match - keys in the upstream available_streams map. +An effect that mixes audio streams using a coefficient matrix. The matrix maps output stream names +to a dict of input stream names and their mix coefficients. Output keys SHOULD use +StreamInfo.Identifier values (e.g. stereo_left, stereo_right) where applicable; they correspond to +the keys that will appear in the downstream available_streams map after mixing. Input keys identify +source streams and SHOULD match keys in the upstream available_streams map. ``` parameters: - *effect_name*: - *enabled*: If true, the Effect is applied. If false, the Effect is omitted. -- *matrix*: Output-keyed mixing matrix (output_name -> {input_name -> coefficient}). Output keys SHOULD be StreamInfo.Identifier values; input keys SHOULD match keys in the upstream available_streams map. +- *matrix*: Output-keyed mixing matrix (output_name -> {input_name -> coefficient}). Output keys SHOULD use StreamInfo.Identifier values where applicable; input keys SHOULD match keys in the upstream available_streams map. - *metadata*: - *name*: @@ -645,6 +647,22 @@ Base class for addressing a specific stream within a media reference. parameters: +### StreamChannelIndexStreamAddress.1 + +*full module path*: `opentimelineio.schema.StreamChannelIndexStreamAddress` + +*documentation*: + +``` +Addresses a stream by track index and channel index within that track. Use this for container +formats that organise media into discrete tracks each of which may contain one or more channels, +such as MP4/MOV and MXF. +``` + +parameters: +- *channel_index*: Integer index of the channel within the stream. +- *stream_index*: Integer index of the media track within its container. + ### StreamInfo.1 *full module path*: `opentimelineio.schema.StreamInfo` @@ -652,9 +670,10 @@ parameters: *documentation*: ``` -Describes a single stream available within a media reference, such as a video track, audio channel, -or camera view. The name field MAY be any descriptive string intended for human consumption, -analogous to the name on a Clip or Marker. +Describes a single media stream provided within a source media. A media stream is the smallest unit +of temporal media, such as a single eye's video, an isolated audio channel, or a camera view within +a 3D scene. StreamAddress provides a mechanism for addressing a specific stream within a media +container. ``` parameters: @@ -663,6 +682,27 @@ parameters: - *metadata*: - *name*: +### StreamMapper.1 + +*full module path*: `opentimelineio.schema.StreamMapper` + +*documentation*: + +``` +An effect that remaps stream identifiers to new names. Each entry in stream_map maps an output +stream name (the key as it will appear downstream) to an input stream name (the key as it appears in + the upstream MediaReference available_streams). A typical use is to normalize a source-specific +identifier into a well-known StreamInfo.Identifier value -- for example, to expose the left eye of a + stereo source as the conventional monocular stream. +``` + +parameters: +- *effect_name*: +- *enabled*: If true, the Effect is applied. If false, the Effect is omitted. +- *metadata*: +- *name*: +- *stream_map*: Mapping of output stream name to input stream name. Keys SHOULD use StreamInfo.Identifier values where applicable; values SHOULD match keys in the upstream available_streams map. + ### StreamSelector.1 *full module path*: `opentimelineio.schema.StreamSelector` @@ -670,7 +710,8 @@ parameters: *documentation*: ``` -An effect that selects specific named output streams from an item. +An effect that selects specific named output streams from an item. Use this to select a stereo view, + specific audio channels, etc. The item will expose these streams downstream with the same naming. ``` parameters: diff --git a/src/opentimelineio/CORE_VERSION_MAP.cpp b/src/opentimelineio/CORE_VERSION_MAP.cpp index 49d88e28ae..081f031581 100644 --- a/src/opentimelineio/CORE_VERSION_MAP.cpp +++ b/src/opentimelineio/CORE_VERSION_MAP.cpp @@ -221,7 +221,6 @@ const label_to_schema_version_map CORE_VERSION_MAP{ { "HookScript", 1 }, { "ImageSequenceReference", 1 }, { "IndexStreamAddress", 1 }, - { "StreamChannelIndexStreamAddress", 1 }, { "Item", 1 }, { "LinearTimeWarp", 1 }, { "Marker", 2 }, @@ -235,6 +234,7 @@ const label_to_schema_version_map CORE_VERSION_MAP{ { "SerializableObjectWithMetadata", 1 }, { "Stack", 1 }, { "StreamAddress", 1 }, + { "StreamChannelIndexStreamAddress", 1 }, { "StreamInfo", 1 }, { "StreamMapper", 1 }, { "StreamSelector", 1 }, From c6d9646dde7ffcdcb8739b796c88bf65a39d4225 Mon Sep 17 00:00:00 2001 From: Eric Reinecke Date: Fri, 3 Apr 2026 17:17:15 -0700 Subject: [PATCH 6/8] Fixed linting Signed-off-by: Eric Reinecke --- tests/test_stream_mapping.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/tests/test_stream_mapping.py b/tests/test_stream_mapping.py index 3095e0ab7e..a14d06305f 100644 --- a/tests/test_stream_mapping.py +++ b/tests/test_stream_mapping.py @@ -345,25 +345,25 @@ def test_create_default(self): self.assertEqual(mapper.schema_version(), 1) def test_create_with_map(self): - stream_map = { - StreamInfo.Identifier.monocular: StreamInfo.Identifier.left_eye - } + stream_map = {StreamInfo.Identifier.monocular: StreamInfo.Identifier.left_eye} mapper = otio.schema.StreamMapper(stream_map=stream_map) self.assertEqual(mapper.stream_map, stream_map) def test_set_map(self): mapper = otio.schema.StreamMapper() stream_map = { - StreamInfo.Identifier.stereo_left: StreamInfo.Identifier.surround_left_front, - StreamInfo.Identifier.stereo_right: StreamInfo.Identifier.surround_right_front, + StreamInfo.Identifier.stereo_left: ( + StreamInfo.Identifier.surround_left_front + ), + StreamInfo.Identifier.stereo_right: ( + StreamInfo.Identifier.surround_right_front + ), } mapper.stream_map = stream_map self.assertEqual(mapper.stream_map, stream_map) def test_round_trip_serialization(self): - stream_map = { - StreamInfo.Identifier.monocular: StreamInfo.Identifier.left_eye - } + stream_map = {StreamInfo.Identifier.monocular: StreamInfo.Identifier.left_eye} mapper = otio.schema.StreamMapper( name="remap_to_mono", stream_map=stream_map, @@ -387,13 +387,13 @@ def test_left_eye_to_monocular_use_case(self): StreamInfo.Identifier.monocular: StreamInfo.Identifier.left_eye } ) - ] + ], ) mapper = clip.effects[0] self.assertIsInstance(mapper, otio.schema.StreamMapper) self.assertEqual( mapper.stream_map[StreamInfo.Identifier.monocular], - StreamInfo.Identifier.left_eye + StreamInfo.Identifier.left_eye, ) def test_use_as_clip_effect(self): @@ -407,11 +407,15 @@ def test_use_as_clip_effect(self): otio.schema.StreamMapper( name="a very bad stereo downmix", stream_map={ - StreamInfo.Identifier.stereo_left: StreamInfo.Identifier.surround_left_front, - StreamInfo.Identifier.stereo_right: StreamInfo.Identifier.surround_right_front, - } + StreamInfo.Identifier.stereo_left: ( + StreamInfo.Identifier.surround_left_front + ), + StreamInfo.Identifier.stereo_right: ( + StreamInfo.Identifier.surround_right_front + ), + }, ) - ] + ], ) json_str = clip.to_json_string() restored = otio.adapters.read_from_string(json_str, "otio_json") @@ -422,7 +426,7 @@ def test_use_as_clip_effect(self): self.assertEqual(mapper.name, "a very bad stereo downmix") self.assertEqual( mapper.stream_map[StreamInfo.Identifier.stereo_left], - StreamInfo.Identifier.surround_left_front + StreamInfo.Identifier.surround_left_front, ) From d531ed0a33b74e414febc8fa6cdf6fc551638ea5 Mon Sep 17 00:00:00 2001 From: Eric Reinecke Date: Tue, 7 Apr 2026 16:55:31 -0500 Subject: [PATCH 7/8] Fixed sphinx documentation warnings Signed-off-by: Eric Reinecke --- .../opentimelineio/algorithms/timeline_algo.py | 2 +- .../opentimelineio/algorithms/track_algo.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/py-opentimelineio/opentimelineio/algorithms/timeline_algo.py b/src/py-opentimelineio/opentimelineio/algorithms/timeline_algo.py index dc99341cc3..54a501a834 100644 --- a/src/py-opentimelineio/opentimelineio/algorithms/timeline_algo.py +++ b/src/py-opentimelineio/opentimelineio/algorithms/timeline_algo.py @@ -19,7 +19,7 @@ def timeline_trimmed_to_range(in_timeline, trim_range): .. note:: the timeline is never expanded, only shortened. Please note that you could do nearly the same thing non-destructively by - just setting the :py:class:`.Track`\'s source_range but sometimes you want to + just setting the :py:class:`opentimelineio.schema.Track`\'s source_range but sometimes you want to really cut away the stuff outside and that's what this function is meant for. :param Timeline in_timeline: Timeline to trim diff --git a/src/py-opentimelineio/opentimelineio/algorithms/track_algo.py b/src/py-opentimelineio/opentimelineio/algorithms/track_algo.py index 745fc6ef21..f230cb3eea 100644 --- a/src/py-opentimelineio/opentimelineio/algorithms/track_algo.py +++ b/src/py-opentimelineio/opentimelineio/algorithms/track_algo.py @@ -21,13 +21,13 @@ def track_trimmed_to_range(in_track, trim_range): .. note:: The track is never expanded, only shortened. Please note that you could do nearly the same thing non-destructively by - just setting the :py:class:`.Track`\'s source_range but sometimes you want + just setting the :py:class:`opentimelineio.schema.Track`\'s source_range but sometimes you want to really cut away the stuff outside and that's what this function is meant for. - :param Track in_track: Track to trim + :param opentimelineio.schema.Track in_track: Track to trim :param TimeRange trim_range: :returns: New trimmed track - :rtype: Track + :rtype: opentimelineio.schema.Track """ new_track = copy.deepcopy(in_track) @@ -94,9 +94,9 @@ def track_with_expanded_transitions(in_track): .. note:: The items used in a transition are encapsulated in tuples. - :param Track in_track: Track to expand + :param opentimelineio.schema.Track in_track: Track to expand :returns: Track - :rtype: list[Track] + :rtype: list[opentimelineio.schema.Track] """ result_track = [] From b3bae93f11233136913a8d7de3c9c962dad8364e Mon Sep 17 00:00:00 2001 From: Eric Reinecke Date: Tue, 7 Apr 2026 17:06:13 -0500 Subject: [PATCH 8/8] Fixed linting issues Signed-off-by: Eric Reinecke --- .../algorithms/timeline_algo.py | 12 ++-- .../opentimelineio/algorithms/track_algo.py | 60 +++++++------------ 2 files changed, 26 insertions(+), 46 deletions(-) diff --git a/src/py-opentimelineio/opentimelineio/algorithms/timeline_algo.py b/src/py-opentimelineio/opentimelineio/algorithms/timeline_algo.py index 54a501a834..99746e6791 100644 --- a/src/py-opentimelineio/opentimelineio/algorithms/timeline_algo.py +++ b/src/py-opentimelineio/opentimelineio/algorithms/timeline_algo.py @@ -5,9 +5,7 @@ import copy -from . import ( - track_algo -) +from . import track_algo def timeline_trimmed_to_range(in_timeline, trim_range): @@ -19,8 +17,9 @@ def timeline_trimmed_to_range(in_timeline, trim_range): .. note:: the timeline is never expanded, only shortened. Please note that you could do nearly the same thing non-destructively by - just setting the :py:class:`opentimelineio.schema.Track`\'s source_range but sometimes you want to - really cut away the stuff outside and that's what this function is meant for. + just setting the :py:class:`opentimelineio.schema.Track`\'s source_range but + sometimes you want to really cut away the stuff outside and that's what this + function is meant for. :param Timeline in_timeline: Timeline to trim :param TimeRange trim_range: @@ -38,8 +37,7 @@ def timeline_trimmed_to_range(in_timeline, trim_range): # trim the track and assign it to the new stack. new_timeline.tracks[track_num] = track_algo.track_trimmed_to_range( - child_track, - trim_range + child_track, trim_range ) return new_timeline diff --git a/src/py-opentimelineio/opentimelineio/algorithms/track_algo.py b/src/py-opentimelineio/opentimelineio/algorithms/track_algo.py index f230cb3eea..ef9fdc2801 100644 --- a/src/py-opentimelineio/opentimelineio/algorithms/track_algo.py +++ b/src/py-opentimelineio/opentimelineio/algorithms/track_algo.py @@ -21,8 +21,9 @@ def track_trimmed_to_range(in_track, trim_range): .. note:: The track is never expanded, only shortened. Please note that you could do nearly the same thing non-destructively by - just setting the :py:class:`opentimelineio.schema.Track`\'s source_range but sometimes you want - to really cut away the stuff outside and that's what this function is meant for. + just setting the :py:class:`opentimelineio.schema.Track`\'s source_range but + sometimes you want to really cut away the stuff outside and that's what this + function is meant for. :param opentimelineio.schema.Track in_track: Track to trim :param TimeRange trim_range: @@ -56,8 +57,7 @@ def track_trimmed_to_range(in_track, trim_range): trim_amount = trim_range.start_time - child_range.start_time child_source_range = opentime.TimeRange( start_time=child_source_range.start_time + trim_amount, - duration=child_source_range.duration - trim_amount - + duration=child_source_range.duration - trim_amount, ) # should we trim the end? @@ -67,8 +67,7 @@ def track_trimmed_to_range(in_track, trim_range): trim_amount = child_end - trim_end child_source_range = opentime.TimeRange( start_time=child_source_range.start_time, - duration=child_source_range.duration - trim_amount - + duration=child_source_range.duration - trim_amount, ) # set the new child's trims @@ -122,11 +121,7 @@ def track_with_expanded_transitions(in_track): next_transition = next_thing result_track.append( - _trim_from_transitions( - thing, - pre=pre_transition, - post=next_transition - ) + _trim_from_transitions(thing, pre=pre_transition, post=next_transition) ) # loop @@ -138,13 +133,12 @@ def track_with_expanded_transitions(in_track): def _expand_transition(target_transition, from_track): - """ Expand transitions into the portions of pre-and-post clips that + """Expand transitions into the portions of pre-and-post clips that overlap with the transition. """ result = from_track.neighbors_of( - target_transition, - schema.NeighborGapPolicy.around_transitions + target_transition, schema.NeighborGapPolicy.around_transitions ) trx_duration = target_transition.in_offset + target_transition.out_offset @@ -154,21 +148,15 @@ def _expand_transition(target_transition, from_track): if isinstance(pre, schema.Transition): raise exceptions.TransitionFollowingATransitionError( - "cannot put two transitions next to each other in a track: " - "{}, {}".format( - pre, - target_transition + "cannot put two transitions next to each other in a track: {}, {}".format( + pre, target_transition ) ) if target_transition.in_offset is None: - raise RuntimeError( - f"in_offset is None on: {target_transition}" - ) + raise RuntimeError(f"in_offset is None on: {target_transition}") if target_transition.out_offset is None: - raise RuntimeError( - f"out_offset is None on: {target_transition}" - ) + raise RuntimeError(f"out_offset is None on: {target_transition}") pre.name = (pre.name or "") + "_transition_pre" @@ -176,21 +164,15 @@ def _expand_transition(target_transition, from_track): tr = pre.trimmed_range() pre.source_range = opentime.TimeRange( - start_time=( - tr.end_time_exclusive() - target_transition.in_offset - ), - duration=trx_duration.rescaled_to( - tr.start_time - ) + start_time=(tr.end_time_exclusive() - target_transition.in_offset), + duration=trx_duration.rescaled_to(tr.start_time), ) post = copy.deepcopy(result[1]) if isinstance(post, schema.Transition): raise exceptions.TransitionFollowingATransitionError( - "cannot put two transitions next to each other in a track: " - "{}, {}".format( - target_transition, - post + "cannot put two transitions next to each other in a track: {}, {}".format( + target_transition, post ) ) @@ -200,17 +182,17 @@ def _expand_transition(target_transition, from_track): tr = post.trimmed_range() post.source_range = opentime.TimeRange( - start_time=( - tr.start_time - target_transition.in_offset - ).rescaled_to(tr.start_time), - duration=trx_duration.rescaled_to(tr.start_time) + start_time=(tr.start_time - target_transition.in_offset).rescaled_to( + tr.start_time + ), + duration=trx_duration.rescaled_to(tr.start_time), ) return pre, target_transition, post def _trim_from_transitions(thing, pre=None, post=None): - """ Trim clips next to transitions. """ + """Trim clips next to transitions.""" result = copy.deepcopy(thing)