雲端 Mac 上的 iOS 媒體時間驗收:CMTime 精度與捨入

雲端 Mac 上的 iOS 媒體時間驗收:CMTime 精度與捨入

雲端 Mac 上的 iOS 媒體時間驗收:CMTime 精度與捨入

剪輯工具把片段的結束時間儲存為浮點秒數,重新開啟專案後,接合處卻多出一個影格。先別急著調整播放器。將時間計算從介面中抽離,在雲端 Mac 上固定輸入、轉換規則與斷言,比拖曳播放進度列更容易找出問題。以下只使用 Swift 與 CoreMedia,不需要影片素材,也不依賴圖形工作階段。

先約定時間的意義

CMTime 以 value / timescale 表示有理數時間,另外還有旗標與 epoch。時間刻度不是影格率,也不是精度越高就越正確。以每秒 30000/1001 個影格為例,一個影格的時長應表示為 1001/30000 秒,不要先把影格率簡化成 29.97 再取倒數。

這次的契約限定:所有值都必須是數值時間,且 epoch 為零;持續時間不得為負,偏移量則可以為負。輸入不符合條件時就回傳錯誤,不能以零代替,否則損壞的紀錄會被解讀成片頭。

「顯示為相同秒數」不代表「時間相等」。介面可以格式化顯示,內部的邊界比較則應保留 CMTime,並使用 CMTimeCompare。

也要區分時間點與時長。兩者雖然共用同一個型別,業務意義卻不同;不同時間軸上的時間點,不能未經映射就直接相加。

建立可執行的精度測試樣本

在建置使用者的工作目錄中,將以下內容儲存為 media-time.swift。它會先檢查輸入,再驗證影格時長累加、等值分數,以及轉換時的捨入。檢查函式會在失敗時明確結束執行,不依賴可能被最佳化設定移除的斷言。

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")

執行前先確認開發工具可用,再執行指令稿:

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

預期只會顯示成功標記;失敗時則回傳非零結束碼。這裡驗證的是時間運算,不是解碼速度,也不是某個節點的效能。

將捨入留在介面邊界

統一使用 600 的時間刻度,並不能消除誤差。它能精確表示三分之一秒,卻無法精確表示上述影格時長。如果每個影格都先轉換再累加,捨入誤差就會隨次數累積;應保留來源刻度,完成運算後,再依輸出介面的要求轉換。

.roundHalfAwayFromZero 的中點規則對正負值是對稱的:落在半個目標單位時,一律朝遠離零的方向捨入。如果業務要求朝零截斷或向下取整,就必須更換方法並補上負數樣本,不能只測試正向播放。

檢查轉換後的證據

失敗紀錄至少要包含輸入的 value、timescale、epoch,以及目標刻度、捨入方法與輸出旗標。hasBeenRounded 表示精度曾經損失,不是自動判定失敗的條件,也無法單獨說明誤差大小。可容許多少誤差,應由匯出格式或產品規則明確訂定。

用半開區間檢查接合點

將剪輯區間定義為 [start, end):起點屬於目前片段,終點屬於下一個片段。這樣相鄰片段共用邊界時,就不會同時落入兩個片段。在前面的指令稿末尾加入以下檢查:

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"
)

同一個邊界也應分別以 1/1 和 600/600 輸入,確認判斷不依賴分子或刻度是否相同。再補測起點前一個刻度、終點前一個刻度,以及起終點顛倒的區間;最後一種情況必須回報錯誤,不能默默交換起終點。

接入建置流程並劃清驗收範圍

將指令稿與業務時間函式放在同一個儲存庫,修改時間演算法時一併執行。獨立指令稿用來確認底層語意,專案測試則還必須直接呼叫正式環境使用的實作,避免「樣本正確,應用程式卻仍走舊邏輯」。

每次任務都要記錄提交識別碼、xcrun swift --version 與結束碼。時間測試失敗時應阻擋後續匯出,不要加入重試來掩蓋確定性的錯誤。新增迴歸測試樣本時,保留原始分數,避免在樣本檔案中預先寫成小數。

最後分三層驗收:有理數運算是否正確、時間軸映射是否正確,以及實際媒體是否如預期播放。本指令稿只涵蓋第一層與明確約定的區間規則;可變影格率素材應讀取實際時間戳記,不能靠固定影格時長推算。先讓每一層都有清楚的輸入與結論,再排查畫面,才能知道那一個影格的差異究竟從何而來。

常見問題

可以將 CMTime 轉成 Double 秒數再比較嗎?

顯示進度時可以轉換,判斷精確邊界時應使用 CMTimeCompare。浮點秒數可能損失有理數精度,也無法單獨表達 epoch 和無效時間等狀態。

所有媒體時間統一轉換成 600 刻度就安全了嗎?

不是。600 無法精確表示所有影格時間。應保留來源時間,在介面邊界依明確規則轉換,並檢查 hasBeenRounded;能否接受捨入由業務契約決定。

將建置工作交給專屬雲端 Mac

選擇 Apple Silicon 物理節點,獨享整台機器,使用原生 macOS 圖形介面與命令列。機型、租用週期與節點,請以訂單確認內容為準。