diff --git a/Sources/objc/include/opentimelineio.h b/Sources/objc/include/opentimelineio.h index 880925a..d99ec30 100644 --- a/Sources/objc/include/opentimelineio.h +++ b/Sources/objc/include/opentimelineio.h @@ -202,6 +202,8 @@ CxxTimeRange timeline_range_of_child(CxxRetainer* self, CxxRetainer* child, CxxE NSArray* timeline_audio_tracks(CxxRetainer* self); NSArray* timeline_video_tracks(CxxRetainer* self); +NSArray* timeline_find_clips(CxxRetainer* self, CxxErrorStruct* cxxErr); + // MARK: - Track NSString* track_get_kind(CxxRetainer* self); void track_set_kind(CxxRetainer* self, NSString*); diff --git a/Sources/objc/opentimelineio.mm b/Sources/objc/opentimelineio.mm index e109983..57f2bcf 100644 --- a/Sources/objc/opentimelineio.mm +++ b/Sources/objc/opentimelineio.mm @@ -717,6 +717,15 @@ CxxTimeRange timeline_range_of_child(CxxRetainer* self, CxxRetainer* child, CxxE return array; } +NSArray* timeline_find_clips(CxxRetainer* self, CxxErrorStruct* cxxErr) { + auto array = [NSMutableArray new]; + _AutoErrorHandler aeh(cxxErr); + for (auto t: SO_cast(self)->find_clips(&aeh.error_status)) { + [array addObject: [NSValue valueWithPointer: t]]; + } + return array; +} + // MARK: - Track NSString* track_get_kind(CxxRetainer* self) { return make_nsstring(SO_cast(self)->kind()); diff --git a/Sources/swift/Timeline.swift b/Sources/swift/Timeline.swift index afa092e..0802edb 100644 --- a/Sources/swift/Timeline.swift +++ b/Sources/swift/Timeline.swift @@ -77,6 +77,17 @@ public class Timeline : SerializableObjectWithMetadata { return try OTIOError.returnOrThrow { TimeRange(timeline_range_of_child(self, child, &$0)) } } + public func findClips() throws -> [Clip] { + let children_array = try OTIOError.returnOrThrow { timeline_find_clips(self, &$0) } + var result = [Clip]() + for child in children_array { + if let nsptr = child as? NSValue, let cxxPtr = nsptr.pointerValue { + result.append(SerializableObject.findOrCreate(cxxPtr: cxxPtr) as! Clip) + } + } + return result + } + override internal init(_ cxxPtr: CxxSerializableObjectPtr) { super.init(cxxPtr) } diff --git a/Tests/OpenTimelineIOTests/testTimeline.swift b/Tests/OpenTimelineIOTests/testTimeline.swift index 01dfd2f..5f05910 100644 --- a/Tests/OpenTimelineIOTests/testTimeline.swift +++ b/Tests/OpenTimelineIOTests/testTimeline.swift @@ -10,72 +10,84 @@ import XCTest import Foundation final class testTimeline: XCTestCase { + enum Error: Swift.Error { + case SetupFailed(String) + } + override func setUpWithError() throws { } override func tearDownWithError() throws { } - func testMetadataRead() { - let inputName = "data/timeline.otio" + func testMetadataRead() throws { let knownDictKey = "foo" let knownKey = "some_key" let knownValue = "some_value" - guard let timelineInputPath = Bundle.module.path(forResource: inputName, ofType: "") else { - XCTFail("Missing test data `\(inputName)`") - return - } - - do { - let otio = try SerializableObject.fromJSON(filename: timelineInputPath) - - guard let timeline = otio as? Timeline else { - XCTFail("Could not create Timeline object from \(timelineInputPath)") - return - } + let timeline = try timeline(from: "data/timeline.otio") + let timelineMetadata = timeline.metadata - let timelineMetadata = timeline.metadata - - if let knownMetadata = timelineMetadata[knownDictKey] as? Metadata.Dictionary { - if let value = knownMetadata[knownKey] as? String { - XCTAssertTrue(value == knownValue) - } else { - XCTFail("Expects (\(knownKey), \(knownValue)), but found none in \(knownMetadata)") - } + if let knownMetadata = timelineMetadata[knownDictKey] as? Metadata.Dictionary { + if let value = knownMetadata[knownKey] as? String { + XCTAssertTrue(value == knownValue) } else { - XCTFail("Cannot read timeline metadata \(String(describing: timelineMetadata[knownDictKey])) as `Metadata.Dictionary`") + XCTFail("Expects (\(knownKey), \(knownValue)), but found none in \(knownMetadata)") } - } catch let error { - XCTFail("Cannot read OTIO file `\(timelineInputPath)`: \(error)") + } else { + XCTFail("Cannot read timeline metadata \(String(describing: timelineMetadata[knownDictKey])) as `Metadata.Dictionary`") } } - func testTimelineClipAvailableBounds() { - let inputName = "data/clip_example.otio" + func testTimelineClipAvailableBounds() throws { + let timeline = try timeline(from: "data/clip_example.otio") - guard let timelineInputPath = Bundle.module.path(forResource: inputName, ofType: "") else { - XCTFail("Missing test data `\(inputName)`") - return + if let firstClip = timeline.videoTracks.first!.children[1] as? Clip, + let mediaReference = firstClip.mediaReference, + let availableBounds = mediaReference.availableImageBounds + { + XCTAssertEqual(availableBounds, CGRect(origin: .zero, size: CGSize(width: 16, height: 9))) + } + } + + func testTimelineFindClips() throws { + // SETUP + let timeline = try timeline(from: "data/nested_example.otio") + + // EXERCISE + let clips = try timeline.findClips() + + // VERIFY + XCTAssertEqual( + clips.map(\.name), + [ + "Normal Clip 1", + "Clip Inside A Stack 1", + "Normal Clip 2", + "Clip Inside A Stack 2", + "Normal Clip 3", + "Clip Inside A Track", + "Normal Clip 4" + ] + ) + } + + func timeline(from inputFilePath: String) throws -> Timeline { + guard let timelineInputPath = Bundle.module.path(forResource: inputFilePath, ofType: "") else { + throw Error.SetupFailed("Missing test data `\(inputFilePath)`") } do { let otio = try SerializableObject.fromJSON(filename: timelineInputPath) - + guard let timeline = otio as? Timeline else { - XCTFail("Could not create Timeline object from \(timelineInputPath)") - return + throw Error.SetupFailed("Could not create Timeline object from \(timelineInputPath)") } - if let firstClip = timeline.videoTracks.first!.children[1] as? Clip, - let mediaReference = firstClip.mediaReference, - let availableBounds = mediaReference.availableImageBounds - { - XCTAssertEqual(availableBounds, CGRect(origin: .zero, size: CGSize(width: 16, height: 9))) - } + return timeline } catch let error { - XCTFail("Cannot read OTIO file `\(timelineInputPath)`: \(error)") + throw Error.SetupFailed("Cannot read OTIO file `\(timelineInputPath)`: \(error)") } } diff --git a/Tests/data/nested_example.otio b/Tests/data/nested_example.otio new file mode 100644 index 0000000..ff89c60 --- /dev/null +++ b/Tests/data/nested_example.otio @@ -0,0 +1,303 @@ +{ + "OTIO_SCHEMA": "Timeline.1", + "metadata": {}, + "name": "My Timeline", + "tracks": { + "OTIO_SCHEMA": "Stack.1", + "children": [ + { + "OTIO_SCHEMA": "Track.1", + "children": [ + { + "OTIO_SCHEMA": "Clip.1", + "effects": [], + "markers": [], + "enabled": true, + "media_reference": { + "OTIO_SCHEMA": "MissingReference.1", + "available_range": null, + "metadata": {}, + "name": null + }, + "metadata": {}, + "name": "Normal Clip 1", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 238 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 135 + } + } + }, + { + "OTIO_SCHEMA": "Stack.1", + "children": [ + { + "OTIO_SCHEMA": "Clip.1", + "effects": [], + "markers": [], + "enabled": true, + "media_reference": { + "OTIO_SCHEMA": "MissingReference.1", + "available_range": null, + "metadata": {}, + "name": null + }, + "metadata": {}, + "name": "Clip Inside A Stack 1", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 37 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 373 + } + } + } + ], + "effects": [], + "markers": [], + "enabled": true, + "metadata": {}, + "name": "Nested Stack 1", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 31 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 0 + } + } + }, + { + "OTIO_SCHEMA": "Clip.1", + "effects": [], + "markers": [], + "enabled": true, + "media_reference": { + "OTIO_SCHEMA": "MissingReference.1", + "available_range": null, + "metadata": {}, + "name": null + }, + "metadata": {}, + "name": "Normal Clip 2", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 33 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 101 + } + } + }, + { + "OTIO_SCHEMA": "Stack.1", + "children": [ + { + "OTIO_SCHEMA": "Clip.1", + "effects": [], + "markers": [], + "enabled": true, + "media_reference": { + "OTIO_SCHEMA": "MissingReference.1", + "available_range": null, + "metadata": {}, + "name": null + }, + "metadata": {}, + "name": "Clip Inside A Stack 2", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 24 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 429 + } + } + } + ], + "effects": [ + { + "OTIO_SCHEMA": "LinearTimeWarp.1", + "effect_name": "LinearTimeWarp", + "metadata": {}, + "name": null, + "time_scalar": 0.8545454551724138 + } + ], + "markers": [], + "enabled": true, + "metadata": {}, + "name": "Nested Stack 2 (with effects)", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 29 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 0 + } + } + }, + { + "OTIO_SCHEMA": "Clip.1", + "effects": [], + "markers": [], + "enabled": true, + "media_reference": { + "OTIO_SCHEMA": "MissingReference.1", + "available_range": null, + "metadata": {}, + "name": null + }, + "metadata": {}, + "name": "Normal Clip 3", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 63 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 179 + } + } + }, + { + "OTIO_SCHEMA": "Track.1", + "children": [ + { + "OTIO_SCHEMA": "Clip.1", + "effects": [], + "markers": [], + "enabled": true, + "media_reference": { + "OTIO_SCHEMA": "MissingReference.1", + "available_range": null, + "metadata": {}, + "name": null + }, + "metadata": {}, + "name": "Clip Inside A Track", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 1 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 471 + } + } + } + ], + "effects": [ + { + "OTIO_SCHEMA": "FreezeFrame.1", + "effect_name": "FreezeFrame", + "metadata": {}, + "name": null, + "time_scalar": 0 + } + ], + "kind": "Video", + "markers": [], + "enabled": true, + "metadata": {}, + "name": "Nested Track", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 4 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 0 + } + } + }, + { + "OTIO_SCHEMA": "Clip.1", + "effects": [], + "markers": [], + "enabled": true, + "media_reference": { + "OTIO_SCHEMA": "MissingReference.1", + "available_range": null, + "metadata": {}, + "name": null + }, + "metadata": {}, + "name": "Normal Clip 4", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 238 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 135 + } + } + } + ], + "effects": [], + "kind": "Video", + "markers": [], + "enabled": true, + "metadata": {}, + "name": "Top Level Track", + "source_range": null + } + ], + "effects": [], + "markers": [], + "enabled": true, + "metadata": {}, + "name": "Top Level Stack", + "source_range": null + } +}