Skip to content
Merged
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
15 changes: 13 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
95 changes: 48 additions & 47 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -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 += [
Expand Down
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| ---------------- | ------------------------ | ------------------- | ----------------- | --------- |
Expand All @@ -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 |
| --------- | --------- | --------------- |
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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
```

Expand Down
4 changes: 4 additions & 0 deletions Sources/SwiftTimecode/SwiftTimecode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -29,6 +30,7 @@ extension Timecode {
try await self.init(from: itemProviders, propertiesForString: propertiesForTimecodeString)
}
}
#endif

@_documentation(visibility: internal)
extension String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
// © 2020-2025 Steffan Andrews • Licensed under MIT License
//

import Darwin
import Foundation

extension Timecode: Equatable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@
/// ----------------------------------------------

#if canImport(Darwin)

import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#endif

// MARK: - ceiling / floor

Expand All @@ -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
}
}

Expand Down Expand Up @@ -126,5 +142,3 @@ extension FloatingPoint {
integralAndFraction.fraction
}
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions Sources/SwiftTimecodeUI/Formatter/TextFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -130,7 +134,9 @@ extension Timecode {
)
: NSAttributedString(string: stringForObj, attributes: attrs)
)
#if canImport(Darwin)
.addingAttribute(alignment: alignment)
#endif
}

// grab properties from the formatter
Expand All @@ -151,7 +157,9 @@ extension Timecode {
)
: NSAttributedString(string: stringForObj, attributes: attrs)
)
#if canImport(Darwin)
.addingAttribute(alignment: alignment)
#endif
}

override public func getObjectValue(
Expand Down
Loading
Loading