diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f147d19f..6b93a640 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,8 +25,8 @@ on: - cron: '40 11 * * *' # once a day @ 11:40am UTC (4:40am PST) env: - SCHEME: "swift-timecode" - WORKSPACEPATH: ".swiftpm/xcode/package.xcworkspace" + SCHEME: swift-timecode + WORKSPACEPATH: .swiftpm/xcode/package.xcworkspace jobs: macOS: @@ -105,3 +105,14 @@ jobs: ID: ${{ steps.sim-setup.outputs.id }} PLATFORM: ${{ steps.sim-setup.outputs.platform }} WORKSPACEPATH: ${{ steps.sim-setup.outputs.workspace-path }} + + linux: + name: Tests (Linux) + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@main + - name: Build + run: swift build + - name: Unit Tests + run: swift test diff --git a/Package.swift b/Package.swift index 397640e6..ab2b36c2 100644 --- a/Package.swift +++ b/Package.swift @@ -12,36 +12,22 @@ let package = Package( ], products: [ .library(name: "SwiftTimecode", targets: ["SwiftTimecode"]), - .library(name: "SwiftTimecodeCore", type: .static, targets: ["SwiftTimecodeCore"]), - .library(name: "SwiftTimecodeAV", targets: ["SwiftTimecodeAV"]), - .library(name: "SwiftTimecodeUI", targets: ["SwiftTimecodeUI"]) + .library(name: "SwiftTimecodeCore", type: .static, targets: ["SwiftTimecodeCore"]) ], dependencies: [ // used only for Dev tests, not part of regular unit tests .package(url: "https://github.com/apple/swift-numerics", from: "1.1.1"), - .package(url: "https://github.com/orchetect/swift-testing-extensions", from: "0.3.0"), - .package(url: "https://github.com/orchetect/xctest-extensions", from: "2.0.0") + .package(url: "https://github.com/orchetect/swift-testing-extensions", from: "0.2.4") ], targets: [ .target( name: "SwiftTimecode", - dependencies: ["SwiftTimecodeCore", "SwiftTimecodeAV", "SwiftTimecodeUI"] + dependencies: ["SwiftTimecodeCore"] ), .target( name: "SwiftTimecodeCore", dependencies: [] ), - .target( - name: "SwiftTimecodeAV", - dependencies: ["SwiftTimecodeCore"] - ), - .target( - name: "SwiftTimecodeUI", - dependencies: ["SwiftTimecodeCore"], - linkerSettings: [ - .linkedFramework("SwiftUI", .when(platforms: [.macOS, .macCatalyst, .iOS, .tvOS, .watchOS, .visionOS])) - ] - ), .testTarget( name: "SwiftTimecodeCoreTests", dependencies: [ @@ -50,39 +36,54 @@ let package = Package( .product(name: "TestingExtensions", package: "swift-testing-extensions"), ] ), - .testTarget( - name: "SwiftTimecodeAVTests", - dependencies: [ - "SwiftTimecodeAV", - .product(name: "TestingExtensions", package: "swift-testing-extensions"), - ], - resources: [.copy("TestResource/Media Files")] - ), - .testTarget( - name: "SwiftTimecodeUITests", - dependencies: [ - "SwiftTimecodeUI", - .product(name: "TestingExtensions", package: "swift-testing-extensions"), - ], - linkerSettings: [ - .linkedFramework("SwiftUI", .when(platforms: [.macOS, .macCatalyst, .iOS, .tvOS, .watchOS, .visionOS])) - ] - ), - // dev tests - // (not meant to be run as unit tests, but only to verify library's computational integrity - // when making major changes to the library, as these tests require modification to be meaningful) - .testTarget( - name: "SwiftTimecodeDevTests", - dependencies: [ - "SwiftTimecodeCore", - "SwiftTimecodeAV", - .product(name: "TestingExtensions", package: "swift-testing-extensions"), - .product(name: "XCTestExtensions", package: "xctest-extensions") - ] - ) ] ) +#if canImport(Darwin) +/// AV and UI targets are only compatible with Apple platforms. + +package.products += [ + .library(name: "SwiftTimecodeAV", targets: ["SwiftTimecodeAV"]), + .library(name: "SwiftTimecodeUI", targets: ["SwiftTimecodeUI"]) +] + +package.targets.first(where: { $0.name == "SwiftTimecode" })?.dependencies += [ + "SwiftTimecodeAV", "SwiftTimecodeUI" +] + +package.targets += [ + .target( + name: "SwiftTimecodeAV", + dependencies: ["SwiftTimecodeCore"] + ), + .target( + name: "SwiftTimecodeUI", + dependencies: ["SwiftTimecodeCore"], + linkerSettings: [ + .linkedFramework("SwiftUI", .when(platforms: [.macOS, .macCatalyst, .iOS, .tvOS, .watchOS, .visionOS])) + ] + ), + .testTarget( + name: "SwiftTimecodeAVTests", + dependencies: [ + "SwiftTimecodeAV", + .product(name: "TestingExtensions", package: "swift-testing-extensions"), + ], + resources: [.copy("TestResource/Media Files")] + ), + .testTarget( + name: "SwiftTimecodeUITests", + dependencies: [ + "SwiftTimecodeUI", + .product(name: "TestingExtensions", package: "swift-testing-extensions"), + ], + linkerSettings: [ + .linkedFramework("SwiftUI", .when(platforms: [.macOS, .macCatalyst, .iOS, .tvOS, .watchOS, .visionOS])) + ] + ) +] +#endif + /// Conditionally opt-in to Swift DocC Plugin when an environment flag is present. if ProcessInfo.processInfo.environment["ENABLE_DOCC_PLUGIN"] != nil { package.dependencies += [ diff --git a/README.md b/README.md index 2d29f2b7..33d677ed 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,15 @@ [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Forchetect%2Fswift-timecode%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/orchetect/swift-timecode) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Forchetect%2Fswift-timecode%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/orchetect/swift-timecode) [![Xcode 16](https://img.shields.io/badge/Xcode-16-blue.svg?style=flat)](https://developer.apple.com/swift) [![License: MIT](http://img.shields.io/badge/License-MIT-lightgrey.svg?style=flat)](https://github.com/orchetect/swift-timecode/blob/main/LICENSE) -The most robust, precise and complete Swift library for working with SMPTE/EBU timecode. Supports 23 industry-standard timecode frame rates with a suite of conversions, calculations and integrations with Apple AV frameworks. +The most robust, precise and complete multi-platform Swift library for working with SMPTE/EBU timecode. + +Supports 23 industry-standard timecode frame rates with a suite of conversions, calculations and integrations with Apple AV frameworks. Timecode is a broadcast and post-production standard for addressing video frames. It is often used for video burn-in timecode (BITC), and display in a DAW (Digital Audio Workstation) or video playback/editing applications. ## Supported Timecode Frame Rates -The following timecode rates and formats are supported. +The following timecode rates and formats are supported: | Film / ATSC / HD | PAL / SECAM / DVB / ATSC | NTSC / ATSC / PAL-M | NTSC Non-Standard | ATSC / HD | | ---------------- | ------------------------ | ------------------- | ----------------- | --------- | @@ -24,7 +26,7 @@ The following timecode rates and formats are supported. ## Supported Video Frame Rates -The following video frame rates are supported. (Video rates) +The following video frame rates are supported: | Film / HD | PAL | NTSC | | --------- | --------- | --------------- | @@ -38,14 +40,13 @@ The following video frame rates are supported. (Video rates) ## Core Features -- Convert timecode between: +- Convert timecode to/from: - timecode display string - total elapsed frame count - real wall-clock time - elapsed audio samples at any audio sample rate - rational time notation (such as `CMTime` or Final Cut Pro XML and AAF encoding) - feet + frames -- Convert timecode and/or frame rate to a rational fraction, and vice-versa (including `CMTime`) - Support for Days as a timecode component (some DAWs including Cubase support > 24 hour timecode) - Support for Subframes - Math operations: add, subtract, multiply, divide @@ -74,7 +75,7 @@ To add this package to a Swift package, add the dependency to your package and t ```swift let package = Package( dependencies: [ - .package(url: "https://github.com/orchetect/swift-timecode", from: "3.0.0") + .package(url: "https://github.com/orchetect/swift-timecode", from: "3.1.0") ], targets: [ .target( @@ -88,6 +89,8 @@ let package = Package( Import the entire library to use all features (core, AV, UI): ```swift +// on Apple platforms, imports Core/AV/UI. +// on Linux, imports Core. import SwiftTimecode ``` diff --git a/Sources/SwiftTimecode/SwiftTimecode.swift b/Sources/SwiftTimecode/SwiftTimecode.swift index 6392c42f..c534f524 100644 --- a/Sources/SwiftTimecode/SwiftTimecode.swift +++ b/Sources/SwiftTimecode/SwiftTimecode.swift @@ -9,8 +9,12 @@ @_documentation(visibility: internal) @_exported import SwiftTimecodeCore +#if canImport(Darwin) + @_documentation(visibility: internal) @_exported import SwiftTimecodeAV @_documentation(visibility: internal) @_exported import SwiftTimecodeUI + +#endif diff --git a/Sources/SwiftTimecodeCore/API Evolution/SwiftTimecodeCore-API-2.3.0.swift b/Sources/SwiftTimecodeCore/API Evolution/SwiftTimecodeCore-API-2.3.0.swift index 1a721d69..64cd824a 100644 --- a/Sources/SwiftTimecodeCore/API Evolution/SwiftTimecodeCore-API-2.3.0.swift +++ b/Sources/SwiftTimecodeCore/API Evolution/SwiftTimecodeCore-API-2.3.0.swift @@ -8,6 +8,7 @@ import Foundation // MARK: API Changes in swift-timecode 2.3.0 +#if canImport(UniformTypeIdentifiers) && canImport(CoreTransferable) @_documentation(visibility: internal) @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) extension Timecode { @@ -29,6 +30,7 @@ extension Timecode { try await self.init(from: itemProviders, propertiesForString: propertiesForTimecodeString) } } +#endif @_documentation(visibility: internal) extension String { diff --git a/Sources/SwiftTimecodeCore/Timecode/Protocol Adoptions/Comparable.swift b/Sources/SwiftTimecodeCore/Timecode/Protocol Adoptions/Comparable.swift index 132249bb..227301da 100644 --- a/Sources/SwiftTimecodeCore/Timecode/Protocol Adoptions/Comparable.swift +++ b/Sources/SwiftTimecodeCore/Timecode/Protocol Adoptions/Comparable.swift @@ -4,7 +4,6 @@ // © 2020-2025 Steffan Andrews • Licensed under MIT License // -import Darwin import Foundation extension Timecode: Equatable { diff --git a/Sources/SwiftTimecodeCore/Timecode/Protocol Adoptions/Strideable.swift b/Sources/SwiftTimecodeCore/Timecode/Protocol Adoptions/Strideable.swift index 8a3c3ab9..a611d200 100644 --- a/Sources/SwiftTimecodeCore/Timecode/Protocol Adoptions/Strideable.swift +++ b/Sources/SwiftTimecodeCore/Timecode/Protocol Adoptions/Strideable.swift @@ -4,7 +4,13 @@ // © 2020-2025 Steffan Andrews • Licensed under MIT License // +#if canImport(Darwin) import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#endif extension Timecode: Strideable { public typealias Stride = Int diff --git a/Sources/SwiftTimecodeCore/Timecode/Source/Timecode String.swift b/Sources/SwiftTimecodeCore/Timecode/Source/Timecode String.swift index dd516c27..f55fb5d3 100644 --- a/Sources/SwiftTimecodeCore/Timecode/Source/Timecode String.swift +++ b/Sources/SwiftTimecodeCore/Timecode/Source/Timecode String.swift @@ -6,12 +6,6 @@ import Foundation -#if os(macOS) -import AppKit -#elseif os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) -import UIKit -#endif - // MARK: - FormattedTimecodeSource extension String: _FormattedTimecodeSource { diff --git a/Sources/SwiftTimecodeCore/Utilities/Outsourced/FloatingPoint and Darwin.swift b/Sources/SwiftTimecodeCore/Utilities/Outsourced/FloatingPoint and Darwin.swift index 06f5a268..270af63f 100644 --- a/Sources/SwiftTimecodeCore/Utilities/Outsourced/FloatingPoint and Darwin.swift +++ b/Sources/SwiftTimecodeCore/Utilities/Outsourced/FloatingPoint and Darwin.swift @@ -10,8 +10,12 @@ /// ---------------------------------------------- #if canImport(Darwin) - import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#endif // MARK: - ceiling / floor @@ -20,14 +24,26 @@ extension FloatingPoint { /// (Functional convenience method) @_disfavoredOverload package var ceiling: Self { + #if canImport(Darwin) Darwin.ceil(self) + #elseif canImport(Glibc) + Glibc.ceil(self) + #elseif canImport(Musl) + Musl.ceil(self) + #endif } /// Same as `floor()` /// (Functional convenience method) @_disfavoredOverload package var floor: Self { + #if canImport(Darwin) Darwin.floor(self) + #elseif canImport(Glibc) + Glibc.floor(self) + #elseif canImport(Musl) + Musl.floor(self) + #endif } } @@ -126,5 +142,3 @@ extension FloatingPoint { integralAndFraction.fraction } } - -#endif diff --git a/Sources/SwiftTimecodeCore/Utilities/Outsourced/NSAttributedString.swift b/Sources/SwiftTimecodeCore/Utilities/Outsourced/NSAttributedString.swift index dcf2c4f0..5f7d01cf 100644 --- a/Sources/SwiftTimecodeCore/Utilities/Outsourced/NSAttributedString.swift +++ b/Sources/SwiftTimecodeCore/Utilities/Outsourced/NSAttributedString.swift @@ -9,12 +9,17 @@ /// ---------------------------------------------- /// ---------------------------------------------- -#if canImport(Foundation) +#if canImport(Darwin) +import struct Foundation.NSRange +import class Foundation.NSAttributedString +import class Foundation.NSMutableAttributedString #if os(macOS) -import AppKit +import class AppKit.NSMutableParagraphStyle +import enum AppKit.NSTextAlignment #else -import UIKit +import class UIKit.NSMutableParagraphStyle +import enum UIKit.NSTextAlignment #endif extension NSAttributedString { diff --git a/Sources/SwiftTimecodeUI/Formatter/TextFormatter.swift b/Sources/SwiftTimecodeUI/Formatter/TextFormatter.swift index 1f172c8e..2ee53976 100644 --- a/Sources/SwiftTimecodeUI/Formatter/TextFormatter.swift +++ b/Sources/SwiftTimecodeUI/Formatter/TextFormatter.swift @@ -26,9 +26,11 @@ extension Timecode { public var subFramesBase: SubFramesBase? public var stringFormat: Timecode.StringFormat = .default() + #if canImport(Darwin) /// The formatter's `attributedString(...) -> NSAttributedString` output will override a control's alignment (ie: `NSTextField`). /// Setting alignment here will add the appropriate paragraph alignment attribute to the output `NSAttributedString`. public var alignment: NSTextAlignment = .natural + #endif /// When set true, invalid timecode component values are individually attributed. public var showsValidation: Bool = false @@ -82,7 +84,9 @@ extension Timecode { subFramesBase = other.subFramesBase stringFormat = other.stringFormat + #if canImport(Darwin) alignment = other.alignment + #endif showsValidation = other.showsValidation invalidAttributes = other.invalidAttributes } @@ -130,7 +134,9 @@ extension Timecode { ) : NSAttributedString(string: stringForObj, attributes: attrs) ) + #if canImport(Darwin) .addingAttribute(alignment: alignment) + #endif } // grab properties from the formatter @@ -151,7 +157,9 @@ extension Timecode { ) : NSAttributedString(string: stringForObj, attributes: attrs) ) + #if canImport(Darwin) .addingAttribute(alignment: alignment) + #endif } override public func getObjectValue( diff --git a/Tests/SwiftTimecodeCoreTests/Timecode/Source/Timecode Rational CMTime Tests.swift b/Tests/SwiftTimecodeCoreTests/Timecode/Source/Timecode Rational CMTime Tests.swift index 3cbc1b8c..9ddfc88b 100644 --- a/Tests/SwiftTimecodeCoreTests/Timecode/Source/Timecode Rational CMTime Tests.swift +++ b/Tests/SwiftTimecodeCoreTests/Timecode/Source/Timecode Rational CMTime Tests.swift @@ -4,6 +4,8 @@ // © 2020-2025 Steffan Andrews • Licensed under MIT License // +#if canImport(CoreMedia) + import CoreMedia import SwiftTimecodeCore // do NOT import as @testable in this file import Testing @@ -185,3 +187,5 @@ import Testing #expect(tc.cmTimeValue.timescale == 30000) } } + +#endif diff --git a/Tests/SwiftTimecodeCoreTests/TimecodeFrameRate/TimecodeFrameRate Conversions Tests.swift b/Tests/SwiftTimecodeCoreTests/TimecodeFrameRate/TimecodeFrameRate Conversions Tests.swift index 4cf7cc62..1427866d 100644 --- a/Tests/SwiftTimecodeCoreTests/TimecodeFrameRate/TimecodeFrameRate Conversions Tests.swift +++ b/Tests/SwiftTimecodeCoreTests/TimecodeFrameRate/TimecodeFrameRate Conversions Tests.swift @@ -4,7 +4,6 @@ // © 2020-2025 Steffan Andrews • Licensed under MIT License // -import CoreMedia import Numerics import SwiftTimecodeCore import Testing diff --git a/Tests/SwiftTimecodeCoreTests/TimecodeInterval/TimecodeInterval Rational CMTime Tests.swift b/Tests/SwiftTimecodeCoreTests/TimecodeInterval/TimecodeInterval Rational CMTime Tests.swift index e8c4afd2..8e746ab0 100644 --- a/Tests/SwiftTimecodeCoreTests/TimecodeInterval/TimecodeInterval Rational CMTime Tests.swift +++ b/Tests/SwiftTimecodeCoreTests/TimecodeInterval/TimecodeInterval Rational CMTime Tests.swift @@ -4,6 +4,8 @@ // © 2020-2025 Steffan Andrews • Licensed under MIT License // +#if canImport(CoreMedia) + import CoreMedia import SwiftTimecodeCore import Testing @@ -43,3 +45,5 @@ import Testing #expect(try ti.absoluteInterval == Timecode(.components(s: 2), at: .fps24)) } } + +#endif diff --git a/Tests/SwiftTimecodeDevTests/Timecode Elapsed Frames ExtendedTests.swift b/Tests/SwiftTimecodeDevTests/Timecode Elapsed Frames ExtendedTests.swift deleted file mode 100644 index bf185ae2..00000000 --- a/Tests/SwiftTimecodeDevTests/Timecode Elapsed Frames ExtendedTests.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// Timecode Elapsed Frames ExtendedTests.swift -// swift-timecode • https://github.com/orchetect/swift-timecode -// © 2020-2025 Steffan Andrews • Licensed under MIT License -// - -// ============================================================================== -// NOTE: -// ============================================================================== -// This not a unit test. It is a brute-force development test not meant to be run -// frequently, but as a diagnostic testbed only when major changes are made to -// the library to ensure that conversions are accurate. -// Depending on how many limits and frame rates are enabled, this test may take -// several minutes to complete -- possibly 20+ minutes even on a modern Mac. -// ============================================================================== - -// @testable import SwiftTimecodeCore -// import Testing -// import TestingExtensions -// import struct XCTestExtensions.SegmentedProgress -// -// @Suite struct Timecode_ExtendedTests { -// // MARK: - Arguments -// -// static let limits: [Timecode.UpperLimit] = -// // Timecode.UpperLimit.allCases -// [.max24Hours] -// // .max100Days -// -// static let frameRates: [TimecodeFrameRate] = -// TimecodeFrameRate.allCases -// // TimecodeFrameRate.allDrop -// // TimecodeFrameRate.allNonDrop -// // [.fps24, .fps30] -// -// let isLoggingEnabled: Bool = false -// -// // MARK: - Dev Test -// -// /// Test conversions from `components(of:)` and `frameCount(of:)`. -// @Test(arguments: limits.flatMap { limit in frameRates.map { frameRate in (limit, frameRate) } }) -// func timecode_Iterative(limit: Timecode.UpperLimit, frameRate: TimecodeFrameRate) async { -// let tc = Timecode(.zero, at: frameRate, limit: limit) -// -// // log status -// if isLoggingEnabled { print("Testing all frames in \(tc.upperLimit) at \(frameRate.stringValue)... ", terminator: "") } -// -// var failures: [(Int, Timecode.Components)] = [] -// -// let ubound = tc.frameRate.maxTotalFrames(in: tc.upperLimit) -// -// var prog = SegmentedProgress(0 ... ubound, segments: 20, roundedToPlaces: 0) -// -// for i in 0 ... ubound { -// let vals = Timecode.components( -// of: .init(.frames(i), base: tc.subFramesBase), -// at: tc.frameRate -// ) -// -// if i != Timecode.frameCount( -// of: vals, -// at: tc.frameRate, -// base: tc.subFramesBase -// ).wholeFrames -// -// { failures.append((i, vals)) } -// -// // log status -// if isLoggingEnabled, let percentageToPrint = prog.progress(value: i) { -// print("\(percentageToPrint) ", terminator: "") -// } -// } -// print("") // finalize log with newline char -// -// #expect( -// failures.count == 0, -// "Failed iterative test for \(frameRate.stringValueVerbose) with \(failures.count) failures." -// ) -// -// if !failures.isEmpty { -// print( -// "First", -// frameRate.stringValueVerbose, -// "failure: input elapsed frames", -// failures.first!.0, -// "converted to components", -// failures.first!.1, -// "converted back to", -// Timecode.frameCount( -// of: failures.first!.1, -// at: tc.frameRate, -// base: tc.subFramesBase -// ), -// "elapsed frames." -// ) -// } -// if failures.count > 1 { -// print( -// "Last", -// frameRate.stringValueVerbose, -// "failure: input elapsed frames", -// failures.last!.0, -// "converted to components", -// failures.last!.1, -// "converted back to", -// Timecode.frameCount( -// of: failures.last!.1, -// at: tc.frameRate, -// base: tc.subFramesBase -// ), -// "elapsed frames." -// ) -// } -// -// if isLoggingEnabled { print("Done testing \(frameRate.stringValueVerbose).") } -// } -// } diff --git a/Tests/SwiftTimecodeDevTests/Timecode Track Write Tests.swift b/Tests/SwiftTimecodeDevTests/Timecode Track Write Tests.swift deleted file mode 100644 index 1a77aad7..00000000 --- a/Tests/SwiftTimecodeDevTests/Timecode Track Write Tests.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// Timecode Track Write Tests.swift -// swift-timecode • https://github.com/orchetect/swift-timecode -// © 2020-2025 Steffan Andrews • Licensed under MIT License -// - -import AVFoundation -@testable import SwiftTimecodeCore -@testable import SwiftTimecodeAV -import Testing - -// @Suite struct TimecodeTrackWriteTests { -// /// This is not a unit test. It is a development test harness. -// @available(macOS 13.0, iOS 9999, tvOS 9999, watchOS 9999, visionOS 9999, *) -// @Test -// func testReplaceTimecodeTrack() async throws { -// // parameters -// let inputURL = URL.desktopDirectory -// .appendingPathComponent("Movie.mp4") -// let outputURL = URL.desktopDirectory -// .appendingPathComponent("Movie-Processed.mov") -// -// // load movie off disk -// -// print("Loading movie from disk...") -// let movie = AVMovie(url: inputURL) -// -// print("Creating mutable copy of movie in memory...") -// let mutableMovie = try #require(movie.mutableCopy() as? AVMutableMovie) -// -// print("Adding timecode track to mutable movie...") -// -// // replace existing timecode track if it exists, otherwise add a new timecode track -// try await mutableMovie.replaceTimecodeTrack( -// startTimecode: Timecode(.components(h: 0, m: 59, s: 50, f: 00), at: .fps24), -// fileType: .mov -// ) -// -// print("Exporting mutated movie to disk...") -// -// // export -// let export = try #require( -// AVAssetExportSession( -// asset: mutableMovie, -// presetName: AVAssetExportPresetPassthrough -// ) -// ) -// -// export.outputFileType = .mov -// export.outputURL = outputURL -// -// // wait for export synchronously -// let exporter = ObservableExporter( -// session: export, -// pollingInterval: 1.0, -// progress: Binding( -// get: { 0 }, -// set: { prog, _ in -// let percentString = String(format: "%.0f", prog * 100) + "%" -// print(percentString) -// } -// ) -// ) -// let status = try await exporter.export() -// print("100%") -// print("Done, status:", status.rawValue) -// } -// } -// -// import SwiftUI -// -// /// Wrapper for `AVAssetExportSession` to update Combine/SwiftUI binding with progress -// /// at a specified interval. -// actor ObservableExporter { -// var progressTimer: Timer? -// let session: AVAssetExportSession -// public let pollingInterval: TimeInterval -// public let progress: Binding -// public private(set) var duration: TimeInterval? -// -// init(session: AVAssetExportSession, -// pollingInterval: TimeInterval = 0.1, -// progress: Binding) { -// self.session = session -// self.pollingInterval = pollingInterval -// self.progress = progress -// } -// -// func export() async throws -> AVAssetExportSession.Status { -// progressTimer = Timer(timeInterval: pollingInterval, repeats: true) { [weak self] timer in -// Task { await self?.timerFired() } -// } -// RunLoop.main.add(progressTimer!, forMode: .common) -// let startDate = Date() -// await session.export() -// progressTimer?.invalidate() -// let endDate = Date() -// duration = endDate.timeIntervalSince(startDate) -// if let error = session.error { -// throw error -// } else { -// return session.status -// } -// } -// -// private func timerFired() { -// self.progress.wrappedValue = Double(self.session.progress) -// } -// }