Проверка точности и округления CMTime на облачном Mac
Редактор сохраняет время окончания клипа в виде числа секунд с плавающей точкой, а после повторного открытия проекта на стыке появляется лишний кадр. Не спешите менять настройки проигрывателя. Отделите расчёты времени от интерфейса и зафиксируйте входные данные, правила преобразования и проверки на облачном 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?
Нет. Не всякая длительность кадра точно представима в этой шкале. Сохраняйте исходные значения, задавайте правило округления на границах интерфейсов и проверяйте hasBeenRounded согласно требованиям к точности.
Передайте задачи сборки на выделенный облачный Mac
Выберите выделенный узел Apple Silicon и получите в своё распоряжение всю машину — с нативным графическим интерфейсом macOS и командной строкой. Модель, срок аренды и узел указаны в подтверждении заказа.