云端 Mac 上的 iOS 媒体时间轴验收:CMTime 精度与边界

云端 Mac 上的 iOS 媒体时间轴验收:CMTime 精度与边界

字幕在片段交界处闪一下,裁剪结果多出一帧,先别改播放器。把出错的时间点搬到云端 Mac,用独立 Swift 脚本检查运算,再回到应用验证解码结果。这样能把“时间算错”和“播放没有准确落点”分开,不必反复上传整段素材。

先约定时间,不要先换成秒

CMTime 用整数分子 value 和时间尺度 timescale 表示时间,还携带有效性、舍入状态与 epoch。其中 1/30 秒可以精确表示,但转成整数毫秒后就不再精确。把 seconds 格式化成三位小数,只是显示策略,不能据此判断两个时间是否相等。

先在仓库写下四条契约:输入必须是数值时间;参与同一时间轴运算的时间采用同一 epoch;片段用起点和时长表示;结束点不属于当前片段。时长不得为负,零时长片段按业务规则拒绝或视为空片段。

时间尺度不是帧率。视频帧率、音频采样率和业务接口的时间单位,不能因为都是整数就混用。

本例只处理 epoch 为零的普通时间轴。跨轮次播放或其他时代编号的数据,应先定义映射,不能只取分子分母重新构造。

在云端 Mac 跑最小检查

准备可用的 Xcode 命令行环境,运行 xcrun --find swift 和 xcrun --sdk macosx --show-sdk-path,确认编译器与 SDK 能找到。下面代码保存为 timeline-check.swift,只使用系统框架,不需要下载媒体文件。

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

通过时脚本静默结束;断言失败会返回非零状态。流水线直接使用该退出状态,不要再用日志里是否出现某个词决定成功。代码改用显式检查,而不是可能在优化构建中被移除的 assert。

把精度损失留在接口边界

同一个数值可以有不同表示,因此比较时间用 CMTimeCompare,不要比较两个 value。相加用 CMTimeAdd,不要直接加分子;只有尺度相同、时代一致且溢出策略明确时,整数运算才可能等价。

转换尺度时必须明确舍入方式。裁剪起点向前还是向后移动,会影响是否保留边界帧;不能让默认行为替产品做决定。脚本里的三分之一秒转换只是验收样本,不是推荐所有媒体统一使用毫秒。

hasBeenRounded 表示结果带有舍入痕迹,不提供误差大小。它也可能从上游运算继承。需要误差预算时,保留原始时间,计算转换前后的差,再与业务允许值比较;不要看到标记就一律失败,也不要没有标记就跳过输入检查。

用交界样本检查区间

为相邻片段分别构造 [0, 1) 和 [1, 2)。时间正好等于一秒时,只能命中第二段。这样字幕交接、片段选中和拖动吸附能共享一套规则,不会两段同时激活。

每个业务区间至少检查起点、终点前一个合法采样点、终点、终点后一个采样点。采样点间隔要来自实际轨道或业务精度,不能固定减一毫秒冒充“上一帧”。可变帧率素材尤其不能按总时长除帧数推算所有边界。

负时间也不天然无效:它可能表示偏移量。应由接口决定是否允许,而不是统一截成零。未知时长则不能转成零秒继续计算,否则尚未加载的媒体会被误判为空片段。

接入回归,但不替代播放验收

把脚本放进仓库的工具目录,记录运行时的系统版本与 xcodebuild -version,让失败能对应到环境。再把同样的样本移入工程测试,直接调用生产代码中的时间转换和区间判定函数;只测框架函数无法发现业务层重新转成浮点数的问题。

提交前确认:数值与时代已校验;时长没有负值;尺度转换集中在明确入口;舍入方向有测试;相邻区间没有重复归属;无效、无限与不确定时间走错误分支。

最后用短素材检查播放器请求位置、实际返回位置与最终呈现帧。寻址容差、关键帧和解码调度都会影响落点,算术通过不等于画面精确。先让时间契约稳定,再调查播放行为,排查范围会小得多。

常见问题

把所有 CMTime 都换成毫秒,能消除时间误差吗?

不能。每秒 30 帧中的单帧时长是 1/30 秒,无法用整数毫秒精确表示。应保留源时间尺度,只在明确的接口边界转换,并验证舍入方向与允许误差。

这些检查必须启动 iOS 模拟器吗?

纯 CMTime 运算和区间检查可在装有可用 macOS SDK 的云端 Mac 上用 xcrun swift 执行。播放器寻址、实际解码帧和音画同步仍需在应用测试中单独验收。

给你的工作流接一台独享 Mac

用原生 macOS 完成构建、测试与实验。先比较配置和计费周期,再选择适合的物理节点。

立即租用云端 Mac 查看机型方案

把下一次构建交给云端 Mac

独享物理机,非虚拟机。按你的项目周期选择配置与租期。