Test iOS CMTime Precision and Timeline Boundaries on a Cloud Mac
If subtitles flash at a clip boundary or a trim includes an extra frame, leave the player alone for now. Take the timestamps involved to a cloud Mac, check the arithmetic with a standalone Swift script, then return to the app to verify the decoded result. This separates incorrect time calculations from playback that fails to land precisely, without repeatedly uploading the entire media file.
Define the Time Contract Before Converting to Seconds
CMTime represents time with an integer numerator, value, and a time scale, timescale. It also carries validity, rounding status, and an epoch. It can represent 1/30 of a second exactly, but that precision is lost when converted to integer milliseconds. Formatting seconds to three decimal places is only a display choice; it cannot tell you whether two times are equal.
Start by documenting four rules in the repository: inputs must be numeric times; times used in calculations on the same timeline must share an epoch; clips are represented by a start time and a duration; and the end time does not belong to the current clip. Durations must not be negative. Depending on your application’s rules, reject zero-duration clips or treat them as empty.
A time scale is not a frame rate. Video frame rates, audio sample rates, and the time units used by application interfaces are not interchangeable just because they are all integers.
This example handles only an ordinary timeline with an epoch of zero. For data from repeated playback cycles or other epoch values, define a mapping first. Do not reconstruct those times using only their numerators and denominators.
Run a Minimal Check on a Cloud Mac
Set up a working Xcode command-line environment, then run xcrun --find swift and xcrun --sdk macosx --show-sdk-path to confirm that the compiler and SDK are available. Save the following code as timeline-check.swift. It uses only system frameworks and requires no media downloads.
import CoreMedia
import Foundation
func check(_ condition: Bool, _ name: String) {
if !condition {
fatalError("Check failed: \(name)")
}
}
let frame = CMTime(value: 1, timescale: 30)
let sameFrame = CMTime(value: 20, timescale: 600)
check(frame.isNumeric, "numeric-input")
check(frame.epoch == 0, "expected-epoch")
check(CMTimeCompare(frame, sameFrame) == 0, "rational-equality")
let sum = CMTimeAdd(frame, frame)
check(
CMTimeCompare(sum, CMTime(value: 1, timescale: 15)) == 0,
"exact-addition"
)
let third = CMTime(value: 1, timescale: 3)
let exact = CMTimeConvertScale(
third,
timescale: 600,
method: .roundHalfAwayFromZero
)
check(exact.value == 200, "exact-conversion")
check(!exact.hasBeenRounded, "exact-rounding-flag")
let rounded = CMTimeConvertScale(
third,
timescale: 1000,
method: .roundHalfAwayFromZero
)
check(rounded.value == 333, "rounded-value")
check(rounded.hasBeenRounded, "rounded-flag")
let segment = CMTimeRange(
start: CMTime(value: 0, timescale: 30),
duration: CMTime(value: 30, timescale: 30)
)
check(CMTimeRangeContainsTime(segment, time: .zero), "start-included")
check(
CMTimeRangeContainsTime(
segment,
time: CMTime(value: 29, timescale: 30)
),
"last-frame-start-included"
)
check(
!CMTimeRangeContainsTime(segment, time: CMTimeRangeGetEnd(segment)),
"end-excluded"
)
check(!CMTime.invalid.isNumeric, "invalid-rejected")
check(!CMTime.indefinite.isNumeric, "indefinite-rejected")
check(!CMTime.positiveInfinity.isNumeric, "infinity-rejected")
xcrun swift timeline-check.swift
The script exits silently when all checks pass; a failed check produces a nonzero exit status. Have your pipeline use that status directly rather than deciding success based on whether a particular word appears in the logs. The code uses explicit checks instead of assert, which may be removed in optimized builds.
Keep Precision Loss at Interface Boundaries
The same numeric time can have different representations, so compare times with CMTimeCompare, not by comparing their value fields. Use CMTimeAdd for addition rather than adding the numerators directly. Integer arithmetic can be equivalent only when the time scales match, the epochs agree, and the overflow policy is explicit.
Always specify the rounding method when converting time scales. Moving a trim start earlier or later affects whether the boundary frame is retained; do not let a default behavior make that product decision for you. The one-third-second conversions in the script are acceptance-test cases, not a recommendation to use milliseconds for all media.
hasBeenRounded indicates that a result carries a rounding flag; it does not quantify the error. The flag may also be inherited from an upstream operation. If you need an error budget, preserve the original time, calculate the difference before and after conversion, and compare it with the tolerance allowed by your application. Do not fail every value with the flag set, or skip input validation just because the flag is absent.
Test Intervals at Their Boundaries
Construct adjacent clips as [0, 1) and [1, 2). A time of exactly one second must belong only to the second clip. This gives subtitle handoffs, clip selection, and drag snapping a shared rule, preventing both clips from becoming active at once.
For every application interval, test at least the start, the last valid sample point before the end, the end itself, and the first sample point after the end. Derive the sample spacing from the actual track or the precision required by the application. Subtracting a fixed millisecond is not a valid substitute for finding the “previous frame.” With variable-frame-rate media in particular, you cannot infer every boundary by dividing the total duration by the frame count.
Negative times are not inherently invalid: they may represent offsets. Let each interface decide whether to allow them instead of clamping them all to zero. Likewise, do not convert an unknown duration to zero seconds and continue calculating, or media that has not loaded yet will be mistaken for an empty clip.
Add Regression Checks Without Replacing Playback Validation
Put the script in the repository’s tools directory and record the system version and the output of xcodebuild -version when it runs, so failures can be traced to their environment. Then add the same cases to the project’s tests, calling the time-conversion and interval-membership functions in production code directly. Testing framework functions alone will not catch application code that converts times back to floating-point values.
Before committing, confirm that numeric validity and epochs have been checked; durations are nonnegative; time-scale conversions are centralized at explicit entry points; rounding directions are tested; adjacent intervals do not share a boundary time; and invalid, infinite, and indefinite times take the error path.
Finally, use a short media sample to check the position requested from the player, the actual position returned, and the frame ultimately displayed. Seek tolerances, keyframes, and decode scheduling all affect where playback lands, so correct arithmetic does not guarantee a frame-accurate result. Stabilize the time contract first, then investigate playback behavior: the scope of the investigation will be much smaller.
Frequently asked questions
Will converting every timestamp to milliseconds eliminate errors?
No. A frame at 30 frames per second lasts 1/30 of a second, which integer milliseconds cannot represent exactly. Keep the source timescale and test rounding direction and error tolerance at conversion boundaries.
Do these checks require an iOS simulator?
Pure CMTime arithmetic and interval checks can run through xcrun swift on a Mac with an available macOS SDK. Player seeking, decoded frames, and audiovisual synchronization still need separate application tests.
Add a dedicated Mac to your workflow
Build, test, and experiment on native macOS. Compare configurations and billing periods, then choose the right physical node.