Pull to refresh

О применении CMTime

Reading time 6 min
Views 9K
Original author: Warren Moore
image Привет, Хабр! Предлагаю ознакомиться с переводом статьи Уоррена Мура о понимании и применении CMTime. В своём предыдущем посте, связанном с работой с видео, я обещал более подробно рассказать об этих замечательных и столь важных структурах. Основной акцент будет сфокусирован на важности взгляда на время, описании структуры CMTime, но и без простейших примеров не обойдётся.

О важности и точности оценки времени

Большинству людей не нужно задумываться о точности времени. Возьмём предельный случай, быть может, вы олимпийский бегун, заботящийся о разнице в миллисекунды между вашей скоростью и мировым рекордом на стометровке. Эти результаты почти одинаковы. Тем не менее, когда речь идёт о своевременной информации, мы заботимся об очень коротких промежутках (десятки микросекунд), но иногда эти отрезки составляют несколько дней или недель.
Предположим, нужно установить точный кадр в фильме, например, на 35:06. Наивно будет представлять время в виде числа двойной точности с плавающей запятой, например:
NSTimeInterval t = 2106.0;

Довольно часто такое представление вполне подходит к условиям задачи, но неприменимо, когда речь идёт об очень длительных промежутках с дроблением на мелкие составляющие. Если не вдаваться в специфику чисел с плавающей точкой, double может содержать 16 (в десятичной системе) значащих цифр в восьми байтах:
sizeof(NSTimeInterval) == sizeof(Float64) == sizeof(double) == 8

Числа с плавающей точкой объединяет одна проблема: повторные операции (сложение, умножение и т.д.) приводят к увеличению неточности, которая может привести к заметному расхождению после длительного периода времени. А это, в свою очередь, может привести к ошибкам в синхронизации нескольких потоков информации.

Простейший пример
Суммируя миллион элементов, равных 0.000001 мы в итоге получим 1.0000000000079181. Ошибка вызвана тем фактом, что 1e-6 не может быть точно представлено в формате с плавающей точкой (используемом в примере), вместо этого мы используем двоичную аппроксимацию, которая отличается менее значимыми битами. Эта ошибка не такая значительная, но если речь идёт об использовании HTTP Streaming Server, погрешность увеличивается несколько тысяч раз в секунду в течение неопределённого периода времени.


Именно это побудило найти способ более точного представления времени, покончив с использованием числа с плавающей точкой и этой неточностью (не говоря уже о жёстком поведении при округлении).

Время как рациональное число

Существует множество структур данных, созданных в Apple, для представления времени и для MAC OS, и для iOS. C появлением MAC OS X 10.7 и iOS 4 были добавлены ещё две:
CMTime
CMTimeRange

Разобравшись с первой, вы без труда поймёте и вторую, поэтому в статье внимание читателя будет сосредоточено на CMTime.

На самом деле, CMTime не больше, чем сишная структура с четырьмя членами, и выглядит следующим образом:
typedef struct
{
   CMTimeValue    value;
   CMTimeScale    timescale;
   CMTimeFlags    flags;
   CMTimeEpoch    epoch;
} CMTime;


Далее, рассмотрим подробно value и timescale, но и flags заслужили упоминания. Различные значения флагов показывают нам, что timestamp (в дальнейшем, временной отрезок) может быть равен плюсу или минусу бесконечности, или же был округлен в результате некоторых расчётов. Таким образом, стуктура является более выразительной по сравнению с NSTimeInterval и имеет целый ряд преимуществ. Что же на самом деле выражено в СMTime?

Очень и очень важно понимать, что value и timescale хранятся в виде целых чисел: 64 и 32 бита, соответственно. Это должно быть очевидно из текста выше, так как value представлено в таком виде, чтобы избежать ошибок, идентичных тем, которые были приведены в примере. Кроме того, за счет выделения всего 64 бит к знаменателю, мы можем использовать до 19 десятичных цифр для каждого из возможных значений timescale.

Что же такое timescale? Он представляет собой количество «срезов», на которые делится каждая секунда. Это важно, потому что точность CMTime объекта в целом ограничивается этой величиной.

Ещё один пример
При значении timescale, равном 1, временной отрезок, представленный в объекте, будет не короче одной секунды, шаг также будет составлять одну секунду. Аналогично, если timescale=1000, каждая секунда будет делиться на 1000 кусочков, а value будет содержать количество миллисекунд, которые мы хотим обозначить.


Как выбрать разумный timescale, чтобы не получить обрезанный кусок? Apple рекомендует 600 для видео (объясняя это тем, что 600 универсален для большинства видео с частотой 24, 25 и 30 кадров в секунду). Возможно, вам захочется установить значение в 60000, если необходима точная индексация для аудио-файла. Что ещё классного в этом 64-битном value, так это то, что можно представить 5,8 млн лет с шагом 1/60000 секунды на этом пути.

Количество секунд во временном отрезке это не что иное, как t.value / t.timescale. Вы можете использовать CMTimeGetSeconds, чтобы легко превратить CMTime в float64 таким образом, чтобы обеспечить максимальную точность.

Не-такие-уж-и-удобные удобные методы

CMTimeGetSeconds может быть дружественным и полезным методом, но его злобный брат-близнец, CMTimeMakeWithSeconds далеко не такой дружелюбный к нашему брату:
CMTime CMTimeMakeWithSeconds (
   Float64 seconds,
   int32_t preferredTimeScale
);


Первые пару раз при использовании этой функции, я не мог заставить ее делать то, что я хотел (прим. переводчика, чертовски жизненно). Теперь, когда мы прошли через все внутренности CMTime, я надеюсь, что вам будут понятны мои ошибки. Я пытался выполнить представление в 0,5 секунды, чтобы получать периодические обратные вызовы от объекта AVPlayer (потокового MP3).
Я пытался сделать следующее:
CMTime interval = CMTimeMakeWithSeconds(0.5, 1);

Если вы дочитали до этого момента, то знаете, что интервал на самом деле будет равен нулю, а не 0,5. Не имеет смысла запрашивать половину единицы в последовательности целых чисел, но это именно то, что я попытался сделать. Наше value обрезалось, потому что здесь timescale недостаточно точен.

The Core Media API включает в себя полный набор функций для построения, сравнения и выполнения арифметических операций, связанных с CMTime. Хотя выполнение арифметических операций на CMTime с помощью этих функций непростое, важно использовать их, если вы хотите сохранить точность и целостность. Каждая из этих функций выполняет специальные проверки переполнения и округления и устанавливает соответствующие флаги для CMTime-объектов, когда возникает такая необходимость.

Для сложения двух CMTime, используйте функцию CMTimeAdd. Для сравнения используйте CMTimeCompare (возвращаемое значение этой функции следует соглашению компаратора функций, который используется в стандартной библиотеке C и знаком по названию qsort). Чтобы узнать большее из CMTime, используйте CMTimeMaximum. Ознакомьтесь с документацией для изучения остальных методов, но этот набор функций обычно является достаточным.

Неопределённости и бесконечности: установите ваш «странный» флаг

Финальной частью статьи, связанной с CMTime, является представление флагов бесконечности, неопределенных значений, и округления. «Неопределенное» время — время, значение которого неизвестно. Вы можете получить такой временной отрезок объекта, если вы запросите его прошедшее время прежде, чем он будет инициализирован. Значения флагов могут быть проверены для каждой исключительной ситуации:
kCMTimeFlags_PositiveInfinity
kCMTimeFlags_NegativeInfinity
kCMTimeFlags_Indefinite

Так как побитовый OR (в оригинале, OR'ing), применяется не всеми, существуют полезные макросы для проверки наличия этих флагов:
CMTIME_IS_POSITIVE_INFINITY
CMTIME_IS_NEGATIVE_INFINITY
CMTIME_IS_INDEFINITE

Кстати, их также можно использовать для проверки действительности временного отрезка. Результатом такой операции будет NO, если timestamp не действителен.

Кроме того, вот длинный список того, как работает сравнение различных категорий времени:
  • Недействительное время (Invalid time) будет равно другому недействительному времени, и больше, чем любое другое время.

  • Плюс бесконечность (Positive infinity) будет меньше любого недействительного времени, равно другой плюс бесконечности и больше любого другого времени.

  • Неопределённое время (An indefinite time) будет меньше любого недействительного времени, меньше плюс бесконечности, равно такому же неопределённому времени, больше любого другого времени.

  • Минус бесконечность (Negative infinity) будет равно такому же и меньше любого другого.


Теперь вы знаете.

Примечание:
Конечно, рассматриваемая тема довольно узкая, но всё же некоторым может показаться интересной. Тем более, что информации в рунете чуть менее, чем ноль. Для получения одобрения автора, а также уточнения некоторых моментов пришлось связаться с ним по почте, и в итоге получилось то, что получилось. Пожалуйста, замечания и ошибки не пишите в комментарии, а присылайте на внутреннюю почту, спасибо.
Tags:
Hubs:
-2
Comments 1
Comments Comments 1

Articles