クラウドMacで検証するiOSのCMTime精度と区間境界

クラウドMacで検証するiOSのCMTime精度と区間境界

クラウドMacで検証するiOSのCMTime精度と区間境界

クリップの切り替わりで字幕が一瞬ちらつく、トリミング結果に1フレーム余分に入る。そんなときも、まずプレーヤーを修正するのは待ちましょう。不具合が起きた時刻をクラウドMacに持ち込み、独立したSwiftスクリプトで計算を確認してから、アプリに戻ってデコード結果を検証します。これなら「時刻の計算ミス」と「再生位置が正確に合っていない問題」を切り分けられ、素材全体を何度もアップロードする必要もありません。

秒に変換する前に、時刻の扱いを決める

CMTimeは、整数の分子valueとタイムスケールtimescaleで時刻を表し、有効性、丸めの状態、epochも保持します。たとえば1/30秒は正確に表現できますが、整数のミリ秒に変換すると正確ではなくなります。secondsを小数点以下3桁で表示するのは、あくまで表示上の方針です。その表示を根拠に、2つの時刻が等しいかどうかを判断してはいけません。

まず、リポジトリに4つのルールを明記します。入力は数値として扱える時刻であること、同じタイムライン上で演算する時刻は同じ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を使い、2つのvalueを直接比較しないでください。加算も分子を直接足すのではなく、CMTimeAddを使います。整数演算が同等の処理になり得るのは、タイムスケールとエポックが一致し、オーバーフローの扱いも明確な場合だけです。

タイムスケールを変換するときは、丸め方法を明示する必要があります。トリミングの開始位置を早い時刻側に動かすか、遅い時刻側に動かすかで、境界のフレームが残るかどうかが変わります。製品としての判断をデフォルトの挙動に委ねてはいけません。スクリプトにある3分の1秒の変換は、検証用のサンプルにすぎません。すべてのメディアでミリ秒に統一することを推奨しているわけではありません。

hasBeenRoundedは、結果に丸めの履歴があることを示しますが、誤差の大きさは示しません。また、前段の演算から引き継がれる場合もあります。許容誤差を管理する必要があるなら、元の時刻を保持し、変換前後の差を計算して、アプリの仕様で認められる値と比較します。フラグがあるだけで一律に失敗扱いにしたり、フラグがないからと入力の検証を省略したりしないでください。

境界のサンプルで区間を検証する

隣り合うクリップに、それぞれ[0, 1)と[1, 2)の区間を設定します。時刻がちょうど1秒なら、該当するのは後者だけです。このルールを字幕の切り替え、クリップの選択、ドラッグ時のスナップで共有すれば、両方のクリップが同時にアクティブになることを防げます。

アプリで扱う各区間について、少なくとも開始時刻、終了時刻の直前にある有効なサンプル点、終了時刻、その次のサンプル点を検証します。サンプル点の間隔は、実際のトラックやアプリに必要な時間精度に基づいて決める必要があります。一律に1ミリ秒を引いて「直前のフレーム」の代わりにしてはいけません。特に可変フレームレートの素材では、総再生時間をフレーム数で割って、すべての境界を求めることはできません。

負の時刻も、それだけで無効とは限りません。オフセットを表している可能性があるためです。許可するかどうかはインターフェース側で決めるべきで、一律にゼロへ切り上げるべきではありません。一方、継続時間が不明な場合に、ゼロ秒へ変換して計算を続けてはいけません。まだ読み込まれていないメディアを、空のクリップと誤判定してしまいます。

回帰テストに組み込み、再生結果は別途確認する

スクリプトをリポジトリのツール用ディレクトリに置き、実行時のシステムバージョンとxcodebuild -versionを記録して、失敗したときに環境を特定できるようにします。続いて、同じサンプルをプロジェクトのテストにも移し、本番コードの時刻変換関数と区間判定関数を直接呼び出します。フレームワークの関数だけをテストしても、アプリの処理内で時刻を再び浮動小数点数に変換している問題は検出できません。

コミット前には、数値としての有効性とエポックを検証済みであること、継続時間に負の値がないこと、タイムスケールの変換箇所が明確な入口に集約されていること、丸め方向のテストがあること、隣接区間に重複して所属する時刻がないこと、無効・無限・不定の時刻がエラー処理に進むことを確認します。

最後に、短い素材を使い、プレーヤーに要求した位置、実際に返された位置、最終的に表示されたフレームを確認します。シークの許容誤差、キーフレーム、デコードのスケジューリングはいずれも到達位置に影響します。計算が正しくても、映像が正確な位置に表示されるとは限りません。まず時刻の扱いを安定させてから再生動作を調べれば、原因を探す範囲を大幅に絞り込めます。

よくある質問

すべてミリ秒に変換すれば誤差をなくせますか?

なくせません。毎秒30フレームの一フレームは1/30秒であり、整数のミリ秒では正確に表せません。元のタイムスケールを保ち、変換する境界で丸め方向と許容誤差を確認します。

iOSシミュレータの起動は必要ですか?

CMTimeの演算と区間判定は、利用可能なmacOS SDKがあればxcrun swiftで検証できます。プレイヤーのシーク、実際のデコード結果、音声と映像の同期は別途アプリで検証します。

ワークフローに専用Macを追加

macOSをそのまま使って、ビルドやテスト、検証を実行できます。構成と利用期間を比較して、用途に合った物理マシンをお選びください。

クラウドMacをレンタル プランを見る

次のビルドはクラウドMacで

仮想マシンではなく、専用の物理マシンをご利用いただけます。プロジェクトに合わせて構成と利用期間をお選びください。