Skip to content

Commit 3dce4a6

Browse files
committed
Added StreamMapper effect
Signed-off-by: Eric Reinecke <ereinecke@netflix.com>
1 parent 30ce3ad commit 3dce4a6

9 files changed

Lines changed: 250 additions & 0 deletions

File tree

src/opentimelineio/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ set(OPENTIMELINEIO_HEADER_FILES
3535
streamChannelIndexStreamAddress.h
3636
streamInfo.h
3737
stringStreamAddress.h
38+
streamMapper.h
3839
streamSelector.h
3940
audioMixMatrix.h
4041
timeEffect.h
@@ -77,6 +78,7 @@ add_library(opentimelineio ${OTIO_SHARED_OR_STATIC_LIB}
7778
streamChannelIndexStreamAddress.cpp
7879
streamInfo.cpp
7980
stringStreamAddress.cpp
81+
streamMapper.cpp
8082
streamSelector.cpp
8183
audioMixMatrix.cpp
8284
stringUtils.cpp

src/opentimelineio/CORE_VERSION_MAP.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ const label_to_schema_version_map CORE_VERSION_MAP{
236236
{ "Stack", 1 },
237237
{ "StreamAddress", 1 },
238238
{ "StreamInfo", 1 },
239+
{ "StreamMapper", 1 },
239240
{ "StreamSelector", 1 },
240241
{ "StringStreamAddress", 1 },
241242
{ "Test", 1 },
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Contributors to the OpenTimelineIO project
3+
4+
#include "opentimelineio/streamMapper.h"
5+
6+
namespace opentimelineio { namespace OPENTIMELINEIO_VERSION_NS {
7+
8+
StreamMapper::StreamMapper(
9+
std::string const& name,
10+
std::string const& effect_name,
11+
StreamMap const& stream_map,
12+
AnyDictionary const& metadata)
13+
: Parent(name, effect_name, metadata)
14+
, _stream_map(stream_map)
15+
{}
16+
17+
StreamMapper::~StreamMapper()
18+
{}
19+
20+
bool
21+
StreamMapper::read_from(Reader& reader)
22+
{
23+
return reader.read_if_present("stream_map", &_stream_map)
24+
&& Parent::read_from(reader);
25+
}
26+
27+
void
28+
StreamMapper::write_to(Writer& writer) const
29+
{
30+
Parent::write_to(writer);
31+
writer.write("stream_map", _stream_map);
32+
}
33+
34+
}} // namespace opentimelineio::OPENTIMELINEIO_VERSION_NS

src/opentimelineio/streamMapper.h

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Contributors to the OpenTimelineIO project
3+
4+
#pragma once
5+
6+
#include "opentimelineio/effect.h"
7+
#include "opentimelineio/version.h"
8+
9+
#include <map>
10+
#include <string>
11+
12+
namespace opentimelineio { namespace OPENTIMELINEIO_VERSION_NS {
13+
14+
/// @brief An effect that remaps stream identifiers to new names.
15+
///
16+
/// Each entry in `stream_map` maps an output stream name (the key as it will
17+
/// appear downstream) to an input stream name (the key as it appears in the
18+
/// upstream `MediaReference::available_streams`).
19+
///
20+
/// A typical use is to normalize a source-specific identifier into a
21+
/// well-known `StreamInfo::Identifier` value — for example, to expose the
22+
/// left eye of a stereo source as the conventional monocular stream:
23+
///
24+
/// @code{.json}
25+
/// {
26+
/// "monocular": "left_eye"
27+
/// }
28+
/// @endcode
29+
class OTIO_API_TYPE StreamMapper : public Effect
30+
{
31+
public:
32+
/// @brief This struct provides the StreamMapper schema.
33+
struct Schema
34+
{
35+
static char constexpr name[] = "StreamMapper";
36+
static int constexpr version = 1;
37+
};
38+
39+
using Parent = Effect;
40+
41+
using StreamMap = std::map<std::string, std::string>;
42+
43+
/// @brief Create a new StreamMapper.
44+
///
45+
/// @param name The name of the effect object.
46+
/// @param effect_name The name of the effect.
47+
/// @param stream_map Mapping of output stream name to input stream name.
48+
/// @param metadata Optional metadata dictionary.
49+
OTIO_API StreamMapper(
50+
std::string const& name = std::string(),
51+
std::string const& effect_name = std::string(),
52+
StreamMap const& stream_map = StreamMap(),
53+
AnyDictionary const& metadata = AnyDictionary());
54+
55+
/// @brief Return the stream name mapping.
56+
///
57+
/// Keys are output stream names (as seen downstream); values are input
58+
/// stream names (keys in the upstream available_streams map).
59+
OTIO_API StreamMap const& stream_map() const noexcept { return _stream_map; }
60+
61+
/// @brief Set the stream name mapping.
62+
OTIO_API void set_stream_map(StreamMap const& stream_map)
63+
{
64+
_stream_map = stream_map;
65+
}
66+
67+
protected:
68+
virtual ~StreamMapper();
69+
70+
bool read_from(Reader&) override;
71+
void write_to(Writer&) const override;
72+
73+
private:
74+
StreamMap _stream_map;
75+
};
76+
77+
}} // namespace opentimelineio::OPENTIMELINEIO_VERSION_NS

src/opentimelineio/typeRegistry.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include "opentimelineio/streamChannelIndexStreamAddress.h"
2929
#include "opentimelineio/streamInfo.h"
3030
#include "opentimelineio/stringStreamAddress.h"
31+
#include "opentimelineio/streamMapper.h"
3132
#include "opentimelineio/streamSelector.h"
3233
#include "opentimelineio/timeEffect.h"
3334
#include "opentimelineio/timeline.h"
@@ -94,6 +95,7 @@ TypeRegistry::TypeRegistry()
9495
register_type<StreamChannelIndexStreamAddress>();
9596
register_type<StringStreamAddress>();
9697
register_type<StreamInfo>();
98+
register_type<StreamMapper>();
9799
register_type<StreamSelector>();
98100
register_type<AudioMixMatrix>();
99101
register_type<TimeEffect>();

src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include "opentimelineio/streamChannelIndexStreamAddress.h"
2929
#include "opentimelineio/streamInfo.h"
3030
#include "opentimelineio/stringStreamAddress.h"
31+
#include "opentimelineio/streamMapper.h"
3132
#include "opentimelineio/streamSelector.h"
3233
#include "opentimelineio/timeEffect.h"
3334
#include "opentimelineio/timeline.h"
@@ -816,6 +817,32 @@ static void define_stream_effects(py::module m) {
816817
&StreamSelector::set_output_streams,
817818
"List of stream identifier strings to select.");
818819

820+
py::class_<StreamMapper, Effect,
821+
managing_ptr<StreamMapper>>(m, "StreamMapper", py::dynamic_attr(),
822+
"An effect that remaps stream identifiers to new names. "
823+
"Each entry in stream_map maps an output stream name (the key as it will "
824+
"appear downstream) to an input stream name (the key as it appears in the "
825+
"upstream MediaReference available_streams). "
826+
"A typical use is to normalize a source-specific identifier into a "
827+
"well-known StreamInfo.Identifier value -- for example, to expose the "
828+
"left eye of a stereo source as the conventional monocular stream.")
829+
.def(py::init([](std::string name,
830+
std::string effect_name,
831+
StreamMapper::StreamMap stream_map,
832+
py::object metadata) {
833+
return new StreamMapper(name, effect_name, stream_map,
834+
py_to_any_dictionary(metadata)); }),
835+
py::arg_v("name"_a = std::string()),
836+
"effect_name"_a = std::string(),
837+
"stream_map"_a = StreamMapper::StreamMap(),
838+
py::arg_v("metadata"_a = py::none()))
839+
.def_property("stream_map",
840+
&StreamMapper::stream_map,
841+
&StreamMapper::set_stream_map,
842+
"Mapping of output stream name to input stream name. "
843+
"Keys SHOULD use StreamInfo.Identifier values where applicable; "
844+
"values SHOULD match keys in the upstream available_streams map.");
845+
819846
py::class_<AudioMixMatrix, Effect,
820847
managing_ptr<AudioMixMatrix>>(m, "AudioMixMatrix", py::dynamic_attr(),
821848
"An effect that mixes audio streams using a coefficient matrix. "

src/py-opentimelineio/opentimelineio/schema/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
Stack,
2626
StreamAddress,
2727
StreamInfo,
28+
StreamMapper,
2829
StreamSelector,
2930
StringStreamAddress,
3031
Timeline,
@@ -57,6 +58,7 @@
5758
serializable_collection,
5859
stream_address,
5960
stream_info,
61+
stream_mapper,
6062
stream_selector,
6163
string_stream_address,
6264
timeline,
@@ -92,6 +94,7 @@ def timeline_from_clips(clips):
9294
'StreamAddress',
9395
'StreamIdentifier',
9496
'StreamInfo',
97+
'StreamMapper',
9598
'StreamSelector',
9699
'StringStreamAddress',
97100
'Timeline',
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright Contributors to the OpenTimelineIO project
3+
4+
from .. core._core_utils import add_method
5+
from .. import _otio
6+
7+
8+
@add_method(_otio.StreamMapper)
9+
def __str__(self):
10+
return "StreamMapper({})".format(str(self.name))
11+
12+
13+
@add_method(_otio.StreamMapper)
14+
def __repr__(self):
15+
return "otio.schema.StreamMapper(name={})".format(repr(self.name))

tests/test_stream_mapping.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,5 +338,94 @@ def test_use_in_available_streams(self):
338338
self.assertIn(StreamInfo.Identifier.stereo_left, streams)
339339

340340

341+
class TestStreamMapper(unittest.TestCase):
342+
def test_create_default(self):
343+
mapper = otio.schema.StreamMapper()
344+
self.assertEqual(mapper.stream_map, {})
345+
self.assertEqual(mapper.schema_name(), "StreamMapper")
346+
self.assertEqual(mapper.schema_version(), 1)
347+
348+
def test_create_with_map(self):
349+
stream_map = {
350+
StreamInfo.Identifier.monocular: StreamInfo.Identifier.left_eye
351+
}
352+
mapper = otio.schema.StreamMapper(stream_map=stream_map)
353+
self.assertEqual(mapper.stream_map, stream_map)
354+
355+
def test_set_map(self):
356+
mapper = otio.schema.StreamMapper()
357+
stream_map = {
358+
StreamInfo.Identifier.stereo_left: StreamInfo.Identifier.surround_left_front,
359+
StreamInfo.Identifier.stereo_right: StreamInfo.Identifier.surround_right_front,
360+
}
361+
mapper.stream_map = stream_map
362+
self.assertEqual(mapper.stream_map, stream_map)
363+
364+
def test_round_trip_serialization(self):
365+
stream_map = {
366+
StreamInfo.Identifier.monocular: StreamInfo.Identifier.left_eye
367+
}
368+
mapper = otio.schema.StreamMapper(
369+
name="remap_to_mono",
370+
stream_map=stream_map,
371+
)
372+
json_str = mapper.to_json_string()
373+
restored = otio.adapters.read_from_string(json_str, "otio_json")
374+
self.assertIsInstance(restored, otio.schema.StreamMapper)
375+
self.assertEqual(restored.name, "remap_to_mono")
376+
self.assertEqual(restored.stream_map, stream_map)
377+
378+
def test_left_eye_to_monocular_use_case(self):
379+
"""StreamMapper can remap left_eye to monocular for downstream consumers."""
380+
clip = otio.schema.Clip(
381+
name="stereo_shot",
382+
media_reference=otio.schema.ExternalReference(
383+
target_url="/path/to/stereo.mov"
384+
),
385+
effects=[
386+
otio.schema.StreamMapper(
387+
stream_map={
388+
StreamInfo.Identifier.monocular: StreamInfo.Identifier.left_eye
389+
}
390+
)
391+
]
392+
)
393+
mapper = clip.effects[0]
394+
self.assertIsInstance(mapper, otio.schema.StreamMapper)
395+
self.assertEqual(
396+
mapper.stream_map[StreamInfo.Identifier.monocular],
397+
StreamInfo.Identifier.left_eye
398+
)
399+
400+
def test_use_as_clip_effect(self):
401+
"""StreamMapper round-trips correctly when embedded in a clip."""
402+
clip = otio.schema.Clip(
403+
name="remapped_clip",
404+
media_reference=otio.schema.ExternalReference(
405+
target_url="/path/to/source.mov"
406+
),
407+
effects=[
408+
otio.schema.StreamMapper(
409+
name="a very bad stereo downmix",
410+
stream_map={
411+
StreamInfo.Identifier.stereo_left: StreamInfo.Identifier.surround_left_front,
412+
StreamInfo.Identifier.stereo_right: StreamInfo.Identifier.surround_right_front,
413+
}
414+
)
415+
]
416+
)
417+
json_str = clip.to_json_string()
418+
restored = otio.adapters.read_from_string(json_str, "otio_json")
419+
self.assertIsInstance(restored, otio.schema.Clip)
420+
self.assertEqual(len(restored.effects), 1)
421+
mapper = restored.effects[0]
422+
self.assertIsInstance(mapper, otio.schema.StreamMapper)
423+
self.assertEqual(mapper.name, "A very bad stereo downmix")
424+
self.assertEqual(
425+
mapper.stream_map[StreamInfo.Identifier.stereo_left],
426+
StreamInfo.Identifier.surround_left_front
427+
)
428+
429+
341430
if __name__ == "__main__":
342431
unittest.main()

0 commit comments

Comments
 (0)