Skip to content

Commit 6f65472

Browse files
committed
AVAssetTrack: Added durationTimecode() method
1 parent 51fbd19 commit 6f65472

File tree

2 files changed

+130
-0
lines changed

2 files changed

+130
-0
lines changed

Sources/TimecodeKit/AVFoundation Extensions/AVAssetTrack Timecode Read.swift

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,68 @@ import AVFoundation
1313
// MARK: - Helper methods
1414

1515
extension AVAssetTrack {
16+
/// Returns the track duration expressed as timecode.
17+
///
18+
/// Passing a value to `frameRate` will override frame rate detection.
19+
/// Passing `nil` will detect frame rate from the asset's contents if possible.
20+
///
21+
/// - Throws: ``Timecode/MediaParseError`` or ``Timecode/ValidationError``
22+
@_disfavoredOverload
23+
public func durationTimecode(
24+
at frameRate: TimecodeFrameRate? = nil,
25+
limit: Timecode.UpperLimit = ._24hours,
26+
base: Timecode.SubFramesBase = .default(),
27+
format: Timecode.StringFormat = .default()
28+
) throws -> Timecode {
29+
guard let frameRate = try frameRate ?? self.asset?.timecodeFrameRate()
30+
else {
31+
throw Timecode.MediaParseError.missingOrNonStandardFrameRate
32+
}
33+
34+
let range = try timecodeRange(
35+
at: frameRate,
36+
limit: limit,
37+
base: base,
38+
format: format
39+
)
40+
41+
return range.upperBound - range.lowerBound
42+
}
43+
44+
// MARK: - Helpers
45+
46+
// Note:
47+
// This shouldn't be public because it's not terribly useful and might be misleading.
48+
// For example, if used on a timecode track ("tmcd"), this will often return a range
49+
// that starts from 0 and ends with the duration, instead of providing the actual timecode track's
50+
// start and end timecode. This is because it references the `timeRange` property.
51+
//
52+
/// Returns the track `timeRange` as a range of timecode.
53+
///
54+
/// Passing a value to `frameRate` will override frame rate detection.
55+
/// Passing `nil` will detect frame rate from the asset's contents if possible.
56+
///
57+
/// - Throws: ``Timecode/MediaParseError``
58+
@_disfavoredOverload
59+
internal func timecodeRange(
60+
at frameRate: TimecodeFrameRate? = nil,
61+
limit: Timecode.UpperLimit = ._24hours,
62+
base: Timecode.SubFramesBase = .default(),
63+
format: Timecode.StringFormat = .default()
64+
) throws -> ClosedRange<Timecode> {
65+
guard let frameRate = try frameRate ?? self.asset?.timecodeFrameRate()
66+
else {
67+
throw Timecode.MediaParseError.missingOrNonStandardFrameRate
68+
}
69+
70+
return try timeRange.timecodeRange(
71+
at: frameRate,
72+
limit: limit,
73+
base: base,
74+
format: format
75+
)
76+
}
77+
1678
/// Returns the start frame number from a timecode track.
1779
/// Returns `nil` if the track is not a timecode track.
1880
internal func readTimecodeSamples(
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//
2+
// AVAssetTrack Timecode Read Tests.swift
3+
// TimecodeKit • https://github.com/orchetect/TimecodeKit
4+
// © 2022 Steffan Andrews • Licensed under MIT License
5+
//
6+
7+
// AVAssetReader is unavailable on watchOS so we can't support any AVAsset operations
8+
#if shouldTestCurrentPlatform && canImport(AVFoundation) && !os(watchOS)
9+
10+
import XCTest
11+
@testable import TimecodeKit
12+
import AVFoundation
13+
14+
class AVAssetTrack_TimecodeRead_Tests: XCTestCase {
15+
override func setUp() { }
16+
override func tearDown() { }
17+
18+
// MARK: - Start/Duration/End Timecode
19+
20+
func testReadTimecodeRange_23_976fps() throws {
21+
let frameRate: TimecodeFrameRate = ._23_976
22+
let url = try TestResource.timecodeTrack_23_976_Start_00_58_40_00.url()
23+
let asset = AVAsset(url: url)
24+
let track = try XCTUnwrap(asset.tracks.first)
25+
26+
let correctStart = try TCC().toTimecode(at: frameRate)
27+
let correctEnd = try TCC(m: 24, s: 10, f: 19, sf: 03)
28+
.toTimecode(at: frameRate, format: [.showSubFrames])
29+
30+
// even though it's a timecode track, its timeRange property relates to overall timeline of the asset,
31+
// so its start is 0.
32+
// print(track.timeRange)
33+
// start: CMTime(value: 0, timescale: 1000, flags: __C.CMTimeFlags(rawValue: 1), epoch: 0)
34+
// duration: CMTime(value: 1452244, timescale: 1000, flags: __C.CMTimeFlags(rawValue: 1), epoch: 0)
35+
36+
// auto-detect frame rate
37+
do {
38+
let tcRange = try track.timecodeRange()
39+
XCTAssertEqual(tcRange.lowerBound, correctStart)
40+
XCTAssertEqual(tcRange.upperBound, correctEnd)
41+
}
42+
43+
// manually supply frame rate
44+
do {
45+
let tcRange = try track.timecodeRange(at: frameRate)
46+
XCTAssertEqual(tcRange.lowerBound, correctStart)
47+
XCTAssertEqual(tcRange.upperBound, correctEnd)
48+
}
49+
}
50+
51+
func testReadDurationTimecode_23_976fps() throws {
52+
let frameRate: TimecodeFrameRate = ._23_976
53+
let url = try TestResource.timecodeTrack_23_976_Start_00_58_40_00.url()
54+
let asset = AVAsset(url: url)
55+
let track = try XCTUnwrap(asset.tracks.first)
56+
57+
// duration
58+
let correctDur = try TCC(m: 24, s: 10, f: 19, sf: 03)
59+
.toTimecode(at: frameRate, format: [.showSubFrames])
60+
61+
// auto-detect frame rate
62+
XCTAssertEqual(try track.durationTimecode(), correctDur)
63+
// manually supply frame rate
64+
XCTAssertEqual(try track.durationTimecode(at: frameRate), correctDur)
65+
}
66+
}
67+
68+
#endif

0 commit comments

Comments
 (0)