Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Sources/objc/include/opentimelineio.h
Original file line number Diff line number Diff line change
Expand Up @@ -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*);
Expand Down
9 changes: 9 additions & 0 deletions Sources/objc/opentimelineio.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this PR!

I have a naive q - apologies if this is obvious! WRT to memory management, is there a possible leak with the Clips returned from SO_cast<otio::Timeline>(self)->find_clips(&aeh.error_status)) via NSValue?

Are the Clip objects life time managed by the SO_cast correctly via NSValue when vended to Swift in the findClips()?

I see the pointer values survive through to swift, via result.append(SerializableObject.findOrCreate(cxxPtr: cxxPtr) as! Clip) - are they cleaned up by the dynamic cast within the SO_cast?

_AutoErrorHandler aeh(cxxErr);
for (auto t: SO_cast<otio::Timeline>(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<otio::Track>(self)->kind());
Expand Down
11 changes: 11 additions & 0 deletions Sources/swift/Timeline.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May I suggest a

if let clip = SerializableObject.findOrCreate(cxxPtr: cxxPtr) as? Clip to avoid a exception / crash on a bad cast?

}
}
return result
}

override internal init(_ cxxPtr: CxxSerializableObjectPtr) {
super.init(cxxPtr)
}
Expand Down
92 changes: 52 additions & 40 deletions Tests/OpenTimelineIOTests/testTimeline.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}
}

Expand Down
Loading