雲端 Mac 的 iOS 媒體時間軸驗收:CMTime 精度與邊界

雲端 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,讓失敗案例能對應到當時的環境。接著把相同的測試案例移入專案測試,直接呼叫正式程式碼中的時間轉換與區間判定函式;只測試框架函式,無法發現業務層又將時間轉回浮點數的問題。

提交前請確認:已驗證數值與時代;時長沒有負值;時間刻度轉換集中在明確的入口;捨入方向已有測試;相鄰區間沒有重複歸屬;無效、無限與不確定時間都會進入錯誤處理分支。

最後,用短素材檢查播放器請求的位置、實際傳回的位置,以及最終呈現的影格。定位容許誤差、關鍵影格與解碼排程都會影響實際落點,算術檢查通過不等於畫面精準。先穩定時間契約,再調查播放行為,就能大幅縮小排查範圍。

常見問題

全部改用毫秒就能避免時間誤差嗎?

不能。每秒 30 格的一格長度是 1/30 秒,無法用整數毫秒精確表示。應保留來源時間尺度,僅在明確的介面邊界轉換,並檢查捨入方向與容許誤差。

這些測試需要啟動 iOS 模擬器嗎?

單純的 CMTime 運算及區間檢查,可在具備可用 macOS SDK 的 Mac 上以 xcrun swift 執行。播放器跳轉、實際解碼影格與音畫同步仍須另外進行應用程式測試。

為你的工作流程接上專屬 Mac

使用原生 macOS 完成建置、測試與實驗。先比較規格與計費週期,再選擇適合的實體節點。

立即租用雲端 Mac 查看機型方案

把下一次建置交給雲端 Mac

專屬實體機,非虛擬機。依照專案週期選擇規格與租用期間。