Test CMTime Precision and Rounding on a Cloud Mac

Test CMTime Precision and Rounding on a Cloud Mac

Test CMTime Precision and Rounding on a Cloud Mac

An editing tool stores clip end times as floating-point seconds, but an extra frame appears at a cut when the project is reopened. Before adjusting the player, isolate the time calculations from the UI. Fixing the inputs, conversion rules, and checks on a cloud Mac makes the issue easier to track down than dragging the playhead. The following tests use only Swift and CoreMedia—no video files or graphical session required.

Define the Time Contract First

CMTime represents rational time as value / timescale, along with flags and an epoch. The timescale is not the frame rate, and a higher timescale is not automatically more correct. For a frame rate of 30000/1001 frames per second, one frame should be represented as 1001/30000 seconds. Do not first reduce the frame rate to 29.97 and then take its reciprocal.

For these tests, the contract is as follows: every value must be a numeric time with a zero epoch; durations must not be negative, but offsets may be. Return an error for inputs that violate these conditions. Do not substitute zero, or a corrupt record will be interpreted as the start of the video.

“Displayed as the same number of seconds” does not mean “equal in time.” The UI may format values, but internal boundary comparisons should retain CMTime values and use CMTimeCompare.

Also distinguish time points from durations. They share a type but have different meanings in the application. Time points on different timelines cannot simply be added without a mapping.

Create Executable Precision Fixtures

Save the following file as media-time.swift in the build user's working directory. It validates inputs, then checks frame-duration accumulation, equivalent fractions, and rounding during conversion. The check function exits explicitly on failure rather than relying on assertions that optimization settings might remove.

import CoreMedia
import Foundation
import Darwin

func check(_ condition: Bool, _ name: String) {
    guard condition else {
        FileHandle.standardError.write(Data("FAIL: \(name)\n".utf8))
        exit(1)
    }
}

enum TimeError: Error {
    case unsupported
}

func accepted(_ time: CMTime) throws -> CMTime {
    guard time.isNumeric, time.epoch == 0 else {
        throw TimeError.unsupported
    }
    return time
}

let frame = try accepted(CMTime(value: 1001, timescale: 30000))
var total = CMTime.zero
for _ in 0..<30 {
    total = CMTimeAdd(total, frame)
}
check(
    CMTimeCompare(total, CMTime(value: 1001, timescale: 1000)) == 0,
    "frame accumulation"
)

let third = CMTime(value: 1, timescale: 3)
let exact = CMTimeConvertScale(
    third, timescale: 600, method: .roundHalfAwayFromZero
)
check(exact.value == 200, "exact conversion value")
check(!exact.hasBeenRounded, "exact conversion flag")

for sign: Int64 in [-1, 1] {
    let source = CMTime(value: sign, timescale: 1200)
    let rounded = CMTimeConvertScale(
        source, timescale: 600, method: .roundHalfAwayFromZero
    )
    check(rounded.value == sign, "signed halfway rounding")
    check(rounded.hasBeenRounded, "rounding flag")
}

for bad in [CMTime.invalid, CMTime.indefinite, CMTime.positiveInfinity] {
    do {
        _ = try accepted(bad)
        check(false, "reject nonnumeric time")
    } catch TimeError.unsupported {
    }
}

print("PASS: media time precision")

Confirm that the developer tools are available, then run the script:

set -eu
xcrun --find swift
xcrun swift media-time.swift

The script should print only the success marker; a failure returns a nonzero exit code. This checks time arithmetic—not decoding speed or the performance of any particular node.

Round at Interface Boundaries

Converting everything to a timescale of 600 does not eliminate error. That timescale can represent one-third of a second exactly, but not the frame duration above. If each frame is converted before accumulation, rounding errors accumulate with every addition. Keep the source timescale during calculations, then convert as required by the output interface.

The midpoint rule in .roundHalfAwayFromZero is symmetric for positive and negative values: a value halfway between target units is rounded away from zero. If the application requires truncation toward zero or rounding down, change the method and add negative test cases. Testing forward playback alone is not enough.

Inspect the Conversion Evidence

Failure records should include at least the input value, timescale, and epoch, plus the target timescale, rounding method, and output flags. hasBeenRounded signals that precision has been lost; it is not an automatic failure condition, nor does it quantify the error on its own. The export format or product rules must explicitly define the acceptable error.

Check Edit Boundaries with Half-Open Intervals

Define each editing interval as [start, end): the start belongs to the current clip, while the end belongs to the next one. Adjacent clips sharing a boundary will then never both match that boundary. Add the following checks to the end of the previous script:

func contains(_ point: CMTime, start: CMTime, end: CMTime) throws -> Bool {
    let p = try accepted(point)
    let a = try accepted(start)
    let b = try accepted(end)
    guard CMTimeCompare(a, b) <= 0 else {
        throw TimeError.unsupported
    }
    return CMTimeCompare(p, a) >= 0 && CMTimeCompare(p, b) < 0
}

let start = CMTime.zero
let end = CMTime(value: 1, timescale: 1)
check(try contains(start, start: start, end: end), "inclusive start")
check(!(try contains(end, start: start, end: end)), "exclusive end")
check(
    !(try contains(start, start: start, end: start)),
    "empty interval"
)

Also supply the same boundary as both 1/1 and 600/600 to confirm that the result does not depend on matching numerators or timescales. Add tests for one tick before the start, one tick before the end, and a reversed interval. The last case must return an error, not silently swap the start and end.

Integrate with the Build and Define the Acceptance Scope

Keep the script and the application's time functions in the same repository, and run them together whenever the time algorithms change. The standalone script verifies the underlying semantics. Project tests must also call the production implementation directly, so the fixtures cannot pass while the application still uses the old logic.

For every run, record the commit identifier, xcrun swift --version, and the exit code. A failed time test should block subsequent exports. Do not add retries to hide deterministic errors. Preserve the original fractions when adding regression fixtures rather than converting them to decimals in the fixture files.

Finally, verify three layers separately: rational arithmetic, timeline mapping, and actual media playback. This script covers only the first layer and the explicitly defined interval rules. For variable-frame-rate media, read the actual timestamps rather than deriving them from a fixed frame duration. Establish clear inputs and conclusions for each layer before investigating the picture; that is how you determine where the extra frame actually comes from.

Frequently asked questions

Can I compare CMTime values by converting them to Double seconds?

Use seconds for display, but use CMTimeCompare for exact boundaries. Floating-point conversion can lose rational precision, and a seconds value alone does not represent epochs or invalid time states.

Is converting every time to a timescale of 600 safe?

No. Some frame durations cannot be represented exactly at that scale. Preserve source values, choose an explicit conversion rule at interface boundaries and check hasBeenRounded against your precision requirements.

Hand build tasks to a dedicated cloud Mac

Choose an Apple Silicon physical host and get the entire machine to yourself, with native macOS GUI and command-line access. The model, rental period, and location are confirmed with your order.