From 554004e4fb989b557965499dd0a33dd286e90a21 Mon Sep 17 00:00:00 2001 From: Joshua Minor Date: Tue, 3 Mar 2026 18:38:38 -0800 Subject: [PATCH] Added getMetadataJSON() and setMetadataJSON() Signed-off-by: Joshua Minor --- Sources/objc/include/opentimelineio.h | 7 +- Sources/objc/opentimelineio.mm | 97 ++++++++++++++----- Sources/swift/Metadata.swift | 2 +- .../SerializableObjectWithMetadata.swift | 32 +++++- Tests/OpenTimelineIOTests/testSO.swift | 56 ++++++++++- 5 files changed, 162 insertions(+), 32 deletions(-) diff --git a/Sources/objc/include/opentimelineio.h b/Sources/objc/include/opentimelineio.h index 8641494..8ed4d7e 100644 --- a/Sources/objc/include/opentimelineio.h +++ b/Sources/objc/include/opentimelineio.h @@ -11,6 +11,7 @@ #import "CxxAnyVectorMutationStamp.h" #import "CxxVectorProperty.h" #import "errorStruct.h" +#import "CxxAny.h" #if defined(__cplusplus) extern "C" { @@ -81,10 +82,14 @@ NSString* unknown_schema_original_schema_name(CxxRetainer* self); int unknown_schema_original_schema_version(CxxRetainer* self); // MARK: - SerializableObjectWithMetadata - + NSString* serializable_object_with_metadata_name(CxxRetainer* self); void serializable_object_with_metadata_set_name(CxxRetainer* self, NSString* name); void* serializable_object_with_metadata_metadata(CxxRetainer* self); + +// MARK: - AnyValue JSON +NSString* _Nullable any_value_to_json_string(CxxAny* _Nonnull value, int indent, CxxErrorStruct* _Nonnull err); +bool any_value_from_json_string(NSString* _Nonnull input, CxxAny* _Nonnull result, CxxErrorStruct* _Nonnull err); // MARK: - Clip void* _Nullable clip_media_reference(CxxRetainer* self); diff --git a/Sources/objc/opentimelineio.mm b/Sources/objc/opentimelineio.mm index ebcb141..c261435 100644 --- a/Sources/objc/opentimelineio.mm +++ b/Sources/objc/opentimelineio.mm @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -22,17 +23,19 @@ #include #include #include +#include #include +#include +#include +#include +#include #include #include #include +#include #include -#include -#include #include -#include -#include -#include + #import "opentimelineio.h" #import "opentime.h" @@ -97,11 +100,11 @@ otio::AnyDictionary d, d2; d["abc"] = 123; d["xyz"] = 456; - + d2["r1"] = otio::RationalTime(1,2); d2["r2"] = otio::RationalTime(100,200); d2["plugh"] = 37; - + d["nested"] = d2; c->metadata() = d; return c;*/ @@ -193,7 +196,6 @@ void serializable_object_to_json_file(CxxRetainer* self, NSString* filename, sch _AutoErrorHandler aeh(cxxErr); otio::schema_version_map map = _to_cxx_schema_version_map(target_family_label_spec); self.retainer.value->to_json_file(filename.UTF8String, &aeh.error_status, &map, indent); - } NSString* serializable_object_to_json_string(CxxRetainer* self, schema_version_map *target_family_label_spec, int indent, CxxErrorStruct* cxxErr) { @@ -216,7 +218,7 @@ void serializable_object_to_json_file(CxxRetainer* self, NSString* filename, sch _AutoErrorHandler aeh(cxxErr); return self.retainer.value->clone(&aeh.error_status); } - + bool serializable_object_is_equivalent_to(CxxRetainer* lhs, CxxRetainer* rhs) { return lhs.retainer.value->is_equivalent_to(*rhs.retainer.value); } @@ -276,6 +278,51 @@ void serializable_object_with_metadata_set_name(CxxRetainer* self, NSString* nam return &SO_cast(self)->metadata(); } +// MARK: - AnyValue JSON + +NSString* any_value_to_json_string(CxxAny* value, int indent, CxxErrorStruct* cxxErr) { + _AutoErrorHandler aeh(cxxErr); + std::any otioAny = cxx_any_to_otio_any(*value); + std::string result = otio::serialize_json_to_string(otioAny, nullptr, &aeh.error_status, indent); + if (aeh.error_status.outcome != otio::ErrorStatus::OK) { + return nil; + } + return make_nsstring(result); +} + +bool any_value_from_json_string(NSString* input, CxxAny* result, CxxErrorStruct* cxxErr) { + // Thread-local buffers keep heap-pointer values alive after otioAny goes out of scope. + static thread_local std::string s_string_value; + static thread_local otio::AnyDictionary s_dict_value; + static thread_local otio::AnyVector s_vector_value; + + _AutoErrorHandler aeh(cxxErr); + std::any otioAny; + bool ok = otio::deserialize_json_from_string(input.UTF8String, &otioAny, &aeh.error_status); + if (!ok) { + return false; + } + otio_any_to_cxx_any(otioAny, result); + // Fix up pointer-typed results whose pointers would dangle once otioAny is destroyed. + switch (result->type_code) { + case CxxAny::STRING: + s_string_value = std::any_cast(otioAny); + result->value.s = s_string_value.c_str(); + break; + case CxxAny::DICTIONARY: + s_dict_value = std::any_cast(otioAny); + result->value.ptr = &s_dict_value; + break; + case CxxAny::VECTOR: + s_vector_value = std::any_cast(otioAny); + result->value.ptr = &s_vector_value; + break; + default: + break; + } + return true; +} + // MARK: - Composable void* composable_parent(CxxRetainer* self) { return SO_cast(self)->parent(); @@ -382,7 +429,7 @@ bool item_trimmed_range_in_parent(CxxRetainer* self, CxxTimeRange* tr, CxxErrorS _AutoErrorHandler aeh(cxxErr); auto item = SO_cast(self); auto result = item->trimmed_range_in_parent(&aeh.error_status); - + if (result) { *tr = cxxTimeRange(*result); return true; @@ -479,7 +526,7 @@ void clip_set_media_reference(CxxRetainer* self, CxxRetainer* media_reference) { CxxVectorProperty* create_composition_children_vector_property(CxxRetainer* self) { auto composition = SO_cast(self); auto p = [CxxVectorProperty new]; - + // Yes, I know: but we're not going to mutate this and neither is anybody else. // We're only going to look at it. auto& children = const_cast>&>(composition->children()); @@ -521,13 +568,13 @@ void composition_append_child(CxxRetainer* self, CxxRetainer* child_retainer, Cx auto dict = [NSMutableDictionary new]; _AutoErrorHandler aeh(cxxErr); auto result = SO_cast(self)->range_of_all_children(&aeh.error_status); - + for (auto item: result) { auto tr = cxxTimeRange(item.second); [dict setObject: [NSValue valueWithBytes:&tr objCType:@encode(CxxTimeRange)] forKey: [NSValue valueWithPointer:item.first]]; } - + return dict; } @@ -548,7 +595,7 @@ void composition_handles_of_child(CxxRetainer* self, CxxRetainer* composable, bool* hasLeft, bool* hasRight, CxxErrorStruct* cxxErr) { _AutoErrorHandler aeh(cxxErr); auto result = SO_cast(self)->handles_of_child(SO_cast(composable), &aeh.error_status); - + if (result.first) { *hasLeft = true; *rt1 = cxxRationalTime(*(result.first)); @@ -556,7 +603,7 @@ void composition_handles_of_child(CxxRetainer* self, CxxRetainer* composable, else { *hasLeft = false; } - + if (result.second) { *hasRight = true; *rt2 = cxxRationalTime(*(result.second)); @@ -645,27 +692,27 @@ void media_reference_clear_available_range(CxxRetainer* self) { // If true, value of passed in rect is set. If false, there was no media reference bounds bool media_reference_available_image_bounds(CxxRetainer* self, CGRect* rect) { std::optional iBox2D = SO_cast(self)->available_image_bounds(); - + if (iBox2D) { rect->origin.x = iBox2D->min.x; rect->origin.y = iBox2D->min.y; rect->size.width = iBox2D->max.x - iBox2D->min.x; rect->size.height = iBox2D->max.y - iBox2D->min.y; - + return true; } - + return false; } void media_reference_set_available_image_bounds(CxxRetainer* self, CGRect image_bounds) { std::optional iBox2D = std::optional(); - + iBox2D->min.x = image_bounds.origin.x; iBox2D->min.y = image_bounds.origin.y; iBox2D->max.x = image_bounds.size.width + image_bounds.origin.x; iBox2D->max.y = image_bounds.size.height + image_bounds.origin.y; - + SO_cast(self)->set_available_image_bounds(iBox2D); } @@ -718,7 +765,7 @@ CxxTimeRange timeline_range_of_child(CxxRetainer* self, CxxRetainer* child, CxxE } return array; } - + NSArray* timeline_video_tracks(CxxRetainer* self) { auto array = [NSMutableArray new]; for (auto t: SO_cast(self)->video_tracks()) { @@ -769,7 +816,7 @@ void effect_set_name(CxxRetainer* self, NSString* name) { NSString* external_reference_get_target_url(CxxRetainer* self) { return make_nsstring(SO_cast(self)->target_url()); } - + void external_reference_set_target_url(CxxRetainer* self, NSString* target_url) { SO_cast(self)->set_target_url([target_url UTF8String]); } @@ -782,7 +829,7 @@ void external_reference_set_target_url(CxxRetainer* self, NSString* target_url) void generator_reference_set_generator_kind(CxxRetainer* self, NSString* kind) { SO_cast(self)->set_generator_kind([kind UTF8String]); } - + void* generator_reference_parameters(CxxRetainer* self) { return &SO_cast(self)->parameters(); } @@ -791,7 +838,7 @@ void generator_reference_set_generator_kind(CxxRetainer* self, NSString* kind) { double linear_time_warp_get_time_scalar(CxxRetainer* self) { return SO_cast(self)->time_scalar(); } - + void linear_time_warp_set_time_scalar(CxxRetainer* self, double time_scalar) { SO_cast(self)->set_time_scalar(time_scalar); } @@ -806,7 +853,7 @@ void linear_time_warp_set_time_scalar(CxxRetainer* self, double time_scalar) { void* algorithms_flatten_stack(CxxRetainer* in_stack, CxxErrorStruct* cxxErr) { _AutoErrorHandler aeh(cxxErr); - return otio::flatten_stack(SO_cast(in_stack), &aeh.error_status); + return otio::flatten_stack(SO_cast(in_stack), &aeh.error_status); } void* algorithms_flatten_track_array(NSArray* tracks, CxxErrorStruct* cxxErr) { diff --git a/Sources/swift/Metadata.swift b/Sources/swift/Metadata.swift index 216223c..d0c76a9 100644 --- a/Sources/swift/Metadata.swift +++ b/Sources/swift/Metadata.swift @@ -417,7 +417,7 @@ public enum Metadata { return CxxAny(type_code: Int32(type.rawValue), value: value) } - private static func withCxxAny(_ value: MetadataValue, work: (CxxAny) -> ()) { + static func withCxxAny(_ value: MetadataValue, work: (CxxAny) -> ()) { switch value.metadataType { case .none: work(createCxxAny(.none, .init(i: 0))) diff --git a/Sources/swift/SerializableObjectWithMetadata.swift b/Sources/swift/SerializableObjectWithMetadata.swift index de5fb07..84dc3b1 100644 --- a/Sources/swift/SerializableObjectWithMetadata.swift +++ b/Sources/swift/SerializableObjectWithMetadata.swift @@ -35,22 +35,46 @@ public class SerializableObjectWithMetadata : SerializableObject { } } } - + override public var description: String { let addr = ObjectIdentifier(self).hashValue let addr2 = Int(bitPattern: cxxSerializableObject()) return "\(String(describing: type(of: self))) named '\(name)' " } - + public var name: String { get { return serializable_object_with_metadata_name(self) } set { serializable_object_with_metadata_set_name(self, newValue) } } - + public var metadata: Metadata.Dictionary { get { return Metadata.Dictionary.wrap(anyDictionaryPtr: serializable_object_with_metadata_metadata(self), cxxRetainer: self) } } - + + public func getMetadataJSON(_ key: String) throws -> String? { + guard let value: MetadataValue = metadata[key] else { + return nil + } + var jsonString: String? = nil + var thrownError: Error? = nil + Metadata.withCxxAny(value) { cxxAny in + var mutableCxxAny = cxxAny + do { + jsonString = try OTIOError.returnOrThrow { any_value_to_json_string(&mutableCxxAny, 4, &$0) } + } catch { + thrownError = error + } + } + if let error = thrownError { throw error } + return jsonString + } + + public func setMetadataJSON(_ key: String, _ value: String) throws { + var cxxAny = CxxAny() + _ = try OTIOError.returnOrThrow { any_value_from_json_string(value, &cxxAny, &$0) } + metadata[key] = Metadata.cxxAnyToMetadataValue(cxxAny) + } + override internal init(_ cxxPtr: CxxSerializableObjectPtr) { super.init(cxxPtr) } diff --git a/Tests/OpenTimelineIOTests/testSO.swift b/Tests/OpenTimelineIOTests/testSO.swift index 1e923cc..20af3c7 100644 --- a/Tests/OpenTimelineIOTests/testSO.swift +++ b/Tests/OpenTimelineIOTests/testSO.swift @@ -65,7 +65,7 @@ class testSO: XCTestCase { XCTAssert(uso.originalSchemaVersion == 3 && uso.originalSchemaName == "BogusName") } - func test_SerializableObjectWithMetadata() { + func test_SerializableObjectWithMetadata() throws { let sowm = SerializableObjectWithMetadata() let clip = Clip() sowm.metadata["anInt"] = 1 @@ -76,6 +76,8 @@ class testSO: XCTestCase { let sowm2 = try! sowm.clone() as! SerializableObjectWithMetadata XCTAssert(sowm2.isEquivalent(to: sowm)) + try XCTAssert(sowm.toJSON() == sowm2.toJSON()) + let sowm3 = SerializableObjectWithMetadata(name: sowm.name, metadata: sowm.metadata) let sowm4 = SerializableObjectWithMetadata(name: sowm3.name, metadata: sowm3.metadata.map { $0 }) XCTAssert(sowm3.isEquivalent(to: sowm4)) @@ -83,6 +85,58 @@ class testSO: XCTestCase { print(try! sowm.toJSON()) } + func test_MetadataGetSet() throws { + let sowm = SerializableObjectWithMetadata() + + // Note: We're matching OTIO's pretty-printing here exactly. + // That includes the spacing, sorted keys, etc. + let meta1 = """ + { + "aDict": { + "bar": 12, + "baz": "fibble" + }, + "aList": [ + "a", + "b", + "c" + ], + "aString": "foo", + "anInt": 1 + } + """ + let meta2 = """ + [ + "x", + "y", + 12 + ] + """ + let meta3 = """ + "Tuesday" + """ + let meta4 = """ + true + """ + + try sowm.setMetadataJSON("meta1", meta1) + try sowm.setMetadataJSON("meta2", meta2) + try sowm.setMetadataJSON("meta3", meta3) + try sowm.setMetadataJSON("meta4", meta4) + + XCTAssertEqual(try sowm.getMetadataJSON("meta1"), meta1) + XCTAssertEqual(try sowm.getMetadataJSON("meta2"), meta2) + XCTAssertEqual(try sowm.getMetadataJSON("meta3"), meta3) + XCTAssertEqual(try sowm.getMetadataJSON("meta4"), meta4) + + let sowm2 = try! sowm.clone() as! SerializableObjectWithMetadata + XCTAssert(sowm2.isEquivalent(to: sowm)) + + try XCTAssert(sowm.toJSON() == sowm2.toJSON()) + + print(try! sowm.toJSON()) + } + func test_Composable() { let c = Composable() c.name = "composable"