Содержание статьи +
- Коротко
- Почему это важно
- Одна идея за каждой меткой времени
- Часы 90 kHz: сердцебиение тайминга в MPEG
- PTS: когда показать кадр
- DTS: когда декодировать кадр – и почему он отличается
- Где метки физически живут
- Timescale: гибкие часы MP4
- Частые ошибки, которые рождают реальные баги синхрона
- Разбор на примере: проверка синхрона в реальном файле
- Где здесь Фора Софт
- Главное
- Что почитать дальше
Коротко
Каждый кадр аудио и видео несёт две метки времени: метку презентации, PTS, которая говорит, когда его показать, и метку декодирования, DTS, которая говорит, когда его декодировать. Различаются они только потому, что часть видеокадров кодируется не в том порядке, в котором показывается, – и декодеру приходится обрабатывать их в одной последовательности, а выводить на экран в другой. В мире MPEG-2 (transport stream и program stream) обе метки – это 33-битные счётчики, тикающие 90 000 раз в секунду; в мире MP4 та же идея хранится как таблица длительностей каждого сэмпла плюс смещение на каждый сэмпл. Сделайте эти метки правильно – и звук с картинкой совпадут идеально; сделайте неправильно – и вы получите самую частую причину рассинхрона в записанном медиа.
Почему это важно
Если вы строите стриминговый сервис, OTT-приложение, функцию записи или что угодно, что мультиплексирует звук и видео в файл, метка времени – это контракт, который держит их в синхроне. Когда клиент сообщает «звук уплывает» или «в записи звук отстаёт на полсекунды», ответ почти всегда в метке времени, которую где-то в конвейере сгенерировали, скопировали или прочитали неправильно. Продакт-менеджеру нужно знать, что это существует, чтобы задать правильный вопрос; инженеру нужно знать, как именно это работает, чтобы прочитать дамп потока и найти плохое число. Эта статья даёт обоим читателям одну модель: метка времени – это показание часов, прикреплённое к каждому куску медиа, а синхронизация – это просто арифметика над этими показаниями.
Одна идея за каждой меткой времени
Начнём с задачи, которую решают метки времени. Медиафайл или поток – это не одна вещь, а как минимум две независимые: аудиодорожка и видеодорожка, снятые разным железом, сжатые разными кодерами, и теперь их надо воспроизвести так, будто они одно целое. Единственный способ для плеера правильно их собрать – чтобы каждый кусок нёс подпись: в какой именно момент по общим часам этот кусок должен прозвучать или появиться. Эта подпись и есть метка времени.
Подпись отвечает на один вопрос: в какой момент, отмеренный по согласованным часам, этот кусок медиа должен дойти до зрителя? Видеокадр с подписью «1,000 секунды» и аудиоблок с подписью «1,000 секунды» должны попасть в глаза и уши вместе. Вся работа плеера после прихода данных – прочитать эти подписи и выпустить каждый кусок в момент, который называет его подпись. Синхронизация на этом уровне – не магия, а чтение чисел и подчинение им.
Прежде чем говорить о двух видах меток, нужна единица, в которой они измеряются. У куска медиа, к которому крепится метка, есть формальное имя – access unit (единица доступа). Access unit – это наименьший самостоятельно воспроизводимый кусок потока: один сжатый видеокадр или один сжатый блок аудиосэмплов. Метки крепятся к access unit, а не к отдельным байтам. Как поток нарезается на эти единицы, мы разбираем в статье Фреймы, пакеты, гранулы: почему звук нарезается; здесь нам нужно лишь знать, что каждый access unit – это то, на что указывает метка.
Часы 90 kHz: сердцебиение тайминга в MPEG
Каждая метка – это показание часов, поэтому первый вопрос: какие часы и как быстро тикают? В мире MPEG-2 systems – мире transport stream (вещание, HLS, файлы .ts) и program stream (DVD) – ответ задан стандартом. Часы тикают 90 000 раз в секунду. Метка времени – это просто счётчик, сколько таких тиков прошло.
Почему 90 000, а не круглый миллион? Потому что 90 kHz нацело делится на частоты кадров, которые были важны при проектировании MPEG-2. При 25 кадрах в секунду один кадр длится ровно 90 000 ÷ 25 = 3 600 тиков. При 30 кадрах – ровно 90 000 ÷ 30 = 3 000 тиков. При 24 кадрах – ровно 90 000 ÷ 24 = 3 750 тиков. Часы, которые дают целое число тиков на каждую распространённую частоту кадров, не накапливают ошибку округления кадр за кадром. Вот и вся причина странного на вид числа.
Под этим лежат более глубокие часы. Главные системные часы в MPEG-2 на самом деле идут на 27 MHz – 27 миллионов тиков в секунду, – а часы меток 90 kHz получаются делением главных ровно на 300 (27 000 000 ÷ 300 = 90 000). Часы 27 MHz появляются в program clock reference – значении, которым вещатели держат часы декодера привязанными к часам кодера; об этом – в статье PCR в MPEG-TS: программные часы, которые ведут вещание. Для меток на отдельных кадрах читается и пишется именно число в 90 kHz.
Сама метка – это 33-битное число. Тридцать три бита считают до 2³³ − 1 = 8 589 934 591 тика. Делим на 90 000 тиков в секунду и получаем период переполнения:
максимум счётчика = 2^33 − 1 = 8 589 934 591 тиков
частота часов = 90 000 тиков в секунду
время переполнения = 8 589 934 591 ÷ 90 000 ≈ 95 443 с ≈ 26,5 часаТо есть 33-битный PTS считает чисто около 26,5 часа, затем сбрасывается в ноль и начинает заново. Для двухчасового фильма это неважно; для круглосуточного вещательного канала важно очень, и декодеры умеют обрабатывать переполнение. Это первая ловушка из раздела «частые ошибки» ниже.
PTS: когда показать кадр
Первая из двух меток – метка презентации, PTS. Она отвечает на простейший вопрос: при каком показании часов этот access unit нужно показать зрителю – вывести на экран или отдать в динамики? Видеокадр с PTS = 90 000 должен появиться ровно через секунду после нулевой точки часов, потому что 90 000 тиков ÷ 90 000 тиков в секунду = 1 секунда.
Для звука PTS – это почти вся история. Аудио декодируется в том же порядке, в каком играет: первый блок сэмплов – это и первый блок, который вы слышите. Поэтому для аудиодорожки порядок презентации и порядок декодирования совпадают. Аудио access unit должен лишь сказать «проиграй меня в этот момент» – и всё. Вот почему чисто аудиопотоки редко несут отдельную метку декодирования: переставлять нечего.
Сделаем PTS конкретным на примере. Пусть аудиопоток 48 kHz кодируется AAC, который упаковывает 1 024 сэмпла в каждый access unit (в терминах AAC – фрейм). Сколько длится один фрейм AAC и каков шаг PTS между соседними фреймами?
длительность фрейма (с) = сэмплов на фрейм ÷ частота дискретизации
= 1 024 ÷ 48 000
= 0,021333… с (около 21,3 мс)
шаг PTS на фрейм = длительность фрейма × 90 000 тиков в секунду
= 0,021333… × 90 000
= ровно 1 920 тиковТо есть у потока фреймов AAC на 48 kHz значения PTS равны 0, 1920, 3840, 5760 и так далее – подпись каждого фрейма на 1 920 тиков выше предыдущей. Если вы выгрузите PTS чистого потока AAC и они шагают ровно по 1 920 – тайминг аудио в порядке. Если шаг другой или скачет неровно – вы нашли баг.
DTS: когда декодировать кадр – и почему он отличается
Если бы порядок презентации и порядок декодирования всегда совпадали, хватило бы одной метки. Они не совпадают, и причина – в том, как работает современное сжатие видео.
Чтобы сжать видео, кодеры пользуются тем, что соседние кадры почти одинаковы. Вместо того чтобы хранить каждый кадр целиком, кодер хранит несколько полных кадров и описывает остальные как разницу с соседями. Есть три типа кадров, и их имена важны:
- I-кадр (intra-coded) – полная картинка, декодируется сам по себе и ни на что не ссылается.
- P-кадр (predicted) – описан как разница с предыдущим кадром.
- B-кадр (bi-directionally predicted) – описан как разница и с предыдущим, и с последующим кадром.
Именно последний тип – вся проблема. B-кадр зависит от кадра, который в порядке показа идёт после него. Декодер не может восстановить B-кадр, пока не декодирует тот будущий кадр, на который B-кадр ссылается. Поэтому кодер переставляет поток: он кладёт будущий опорный кадр раньше в файле, чем B-кадр, которому он нужен, хотя показывается этот опорный кадр позже. Данные приходят в порядке декодирования; зрителю нужен порядок презентации. Эти два порядка различаются.
Вот почему меток ровно две. Метка декодирования, DTS, говорит, когда декодер должен обработать access unit. Метка презентации, PTS, говорит, когда результат нужно показать. Для I-кадра или P-кадра, которые никогда не ждут будущий кадр, DTS и PTS равны. Для опорных кадров, нужных B-кадрам, DTS идёт раньше PTS, потому что кадр нужно декодировать заранее, чтобы он был готов, когда до него дойдут B-кадры.
Пройдём по крошечной группе картинок. Пусть порядок показа – I B B P: I-кадр, два B-кадра, затем P-кадр, от которого B-кадры зависят. Поскольку B-кадрам нужен P-кадр, кодер обязан положить P-кадр в поток раньше B-кадров. Порядок на диске становится I P B B. Теперь посмотрим на две метки для каждого кадра (в единицах кадров для наглядности; умножьте на число тиков на кадр, чтобы получить реальные PTS):
| Кадр | Позиция показа (PTS) | Позиция декодирования (DTS) | DTS = PTS? |
|---|---|---|---|
| I | 0 | 0 | да |
| P | 3 | 1 | нет – декодирован раньше |
| B | 1 | 2 | нет – декодирован после опорного |
| B | 2 | 3 | нет – декодирован после опорного |
Таблица 1. Группа из четырёх кадров с порядком показа I B B P. Декодер получает их в порядке DTS (I, P, B, B) и переставляет в порядок PTS (I, B, B, P) перед показом. P-кадр – главная улика: декодирован на позиции 1, показан на позиции 3.
Прочитайте таблицу медленно. Декодер снимает кадры с потока в порядке DTS – I, затем P, затем два B. Он держит P-кадр в буфере, декодирует с его помощью B-кадры, затем выпускает все четыре на экран в порядке PTS – I, B, B, P. DTS держит декодер накормленным в правильной последовательности; PTS держит картинку правильной на экране. Без DTS декодер попытался бы собрать B-кадр до того, как появился его опорный кадр, и сломался бы.
Чистое правило на вынос: DTS ≤ PTS, всегда. Кадр никогда не показывается раньше, чем декодируется. Для звука и для видео без B-кадров эти две метки равны. Разрыв между ними открывается только тогда, когда B-кадры вынуждают декодирование не по порядку.
Где метки физически живут
Идея двух меток универсальна, но место, где хранятся числа, зависит от контейнера. Два мира, которые вам встретятся, – это MPEG-2 systems (transport и program stream) и ISO base media file format (MP4 и его родственники).
В потоках MPEG-2: заголовок PES
В transport stream или program stream элементарные потоки сначала оборачиваются в пакеты packetized elementary stream – PES-пакеты. У каждого PES-пакета есть заголовок, и метки живут в нём. Двухбитное поле PTS_DTS_flags сообщает декодеру, что присутствует: значение 10 означает, что дальше идёт только PTS; значение 11 – что дальше идут и PTS, и DTS. Значение 00 означает, что нет ни того, ни другого, – обычно для продолжения большого кадра, разбитого на несколько пакетов.
33-битная метка хранится не как чистое 33-битное поле. Стандарт MPEG-2 systems растягивает её на 5 байт (40 бит), разбивая 33 бита данных на три группы (3 бита, 15 бит, 15 бит) и вставляя 1 – «маркерный бит» – после каждой группы плюс 4-битный префикс в начале. Префикс – 0010, когда присутствует только PTS, 0011 для PTS-половины пары PTS+DTS и 0001 для DTS-половины. Маркерные биты нужны, чтобы декодер, читающий повреждённый поток, мог проверить, что читает настоящую метку, а не случайные байты. Вручную это почти никогда не разбирают – инструменты вроде ffprobe делают это за вас, – но знание раскладки объясняет, почему «33-битное» значение занимает на проводе 5 байт.
В MP4 / ISOBMFF: таблицы, а не заголовки
MP4 идёт совершенно другим путём, и это сбивает с толку инженеров, которые впервые познакомились с метками на transport stream. Файл MP4 не штампует каждый кадр абсолютными PTS и DTS. Вместо этого он хранит таблицы в боксе sample table, а плеер вычисляет метки, проходя по таблицам.
Тайминг несут два бокса. Бокс stts («decoding time to sample») перечисляет длительность каждого сэмпла. Декодер стартует с нуля и складывает длительности, чтобы вычислить DTS каждого сэмпла, – то есть DTS неявный, он выводится накоплением, а не хранится напрямую. Бокс ctts («composition time to sample») хранит для каждого сэмпла смещение между временем декодирования и временем композиции (презентации). Это смещение и есть PTS − DTS. У файла без B-кадров бокса ctts вообще нет, потому что все смещения были бы нулевыми. У файла с B-кадрами есть бокс ctts, чьи ненулевые записи – это тот же разрыв перестановки, что мы видели в Таблице 1, выраженный по сэмплам.
Есть ещё один бокс, который тихо переписывает тайминг, – edit list, бокс elst. Он отображает внутреннюю шкалу медиа на шкалу презентации и именно так MP4 выражает вещи вроде «начать воспроизведение с 40-й миллисекунды аудиодорожки» или «задержать видео на два кадра». Удивительно большая доля реальных багов синхрона приходит от edit list, который один инструмент записал, а другой проигнорировал. Мы отмечаем это как ловушку ниже.
| Контейнер | Метки хранятся как | Источник DTS | Источник PTS | Признак B-кадров |
|---|---|---|---|---|
| MPEG-2 TS / PS | Явные значения в заголовке PES | Читается напрямую (если PTS_DTS_flags = 11) | Читается напрямую | Присутствуют и PTS, и DTS |
| MP4 / ISOBMFF | Таблицы в боксе sample table | Накапливается из длительностей stts | DTS + смещение ctts | Есть бокс ctts с ненулевыми смещениями |
Таблица 2. Где живут PTS и DTS в двух мирах контейнеров. Числа значат одно и то же; различаются лишь хранение и способ извлечения. Детали переноса по каждому контейнеру разобраны в Аудио в контейнерах: как MP4, MKV, fMP4, MPEG-TS несут звук.
Timescale: гибкие часы MP4
Часы 90 kHz зафиксированы в MPEG-2, но MP4 позволяет каждой дорожке объявить свою частоту часов – timescale. Timescale – это число тиков в секунду для дорожки, записанное в media header. Аудиодорожка с дискретизацией 48 kHz обычно использует timescale 48 000, так что один тик равен одному аудиосэмплу, а длительности – целые числа. Видеодорожка может использовать 30 000 (или вещательно-удобное 30 000 с длительностями сэмплов по 1 001, чтобы чисто выразить частоту 29,97 fps).
Timescale – причина, по которой нельзя прочитать метку MP4, не прочитав сначала частоту часов дорожки. Длительность stts в 1 024 означает 1 024 тика, а сколько это в секундах – зависит целиком от timescale. При timescale 48 000 это тот самый фрейм AAC на 21,3 мс из примера выше; при timescale 90 000 то же число значило бы другое. Всегда делите на timescale, чтобы получить секунды:
время в секундах = значение в тиках ÷ timescale
пример: длительность stts 1 024 при timescale 48 000
= 1 024 ÷ 48 000
= 0,021333… с (фрейм AAC на 21,3 мс)Эта гибкость на каждую дорожку – мощная вещь и источник второй частой ошибки: предполагать, что видео- и аудиодорожки делят один timescale. Обычно нет, и любой код, сравнивающий «сырой» тик видео с «сырым» тиком аудио без перевода обоих в секунды, посчитает чепуху.
Частые ошибки, которые рождают реальные баги синхрона
Метки – это простая арифметика, но режимы отказа конкретны и повторяются почти в каждой команде, которая трогает медиа. Четыре стоит назвать поимённо.
Принимать PTS за время по настенным часам. Самая частая концептуальная ошибка. PTS = 90 000 не означает «9 утра» или «90 секунд после того, как пользователь нажал play». Это счётчик собственных часов потока, нулевая точка которого произвольна – многие кодеры стартуют первый PTS с какого-то ненулевого значения. PTS говорит о соотношении кадров, а не о времени суток. Чтобы привязать его к настенному времени, нужен внешний якорь (program clock reference в вещании или RTCP sender report в WebRTC) – отдельный механизм, разобранный в Метки времени RTP, отчёты отправителя RTCP и синхронизация по NTP.
Игнорировать переполнение каждые 26,5 часа. Поскольку 33-битные часы сбрасываются примерно через 26,5 часа, наивное сравнение if (pts_a > pts_b) ломается в момент переполнения счётчика: кадр сразу после сброса имеет крошечный PTS, хотя он позже, чем кадр прямо перед сбросом с огромным PTS. Долго работающие каналы и записи обязаны обнаруживать переполнение и учитывать его, иначе они переставят или потеряют кадры вокруг точки сброса.
Терять бокс ctts или edit list при ремуксе. Инструменты, копирующие поток из одного контейнера в другой, иногда сохраняют длительности stts, но выбрасывают смещения композиции ctts или edit list elst. Результат – видео, чьи B-кадры теперь показываются в порядке декодирования (каждый второй кадр заметно не на месте), или звук, смещённый ровно на ту величину, которую edit list должен был исправить. Когда ремукс вносит ошибку синхрона, которой не было в исходнике, выброшенный ctts или elst – первый подозреваемый.
Сравнивать тики при несовпадающих часах. Как отмечено выше, аудиодорожку с timescale 48 000 и видеодорожку с timescale 30 000 нельзя сравнивать по «сырым» тикам. Сначала переведите обе в секунды. Пропуск перевода даёт смещение синхрона, которое растёт с позицией воспроизведения – маленькое в начале, большое к концу, – это и есть подпись несовпадения timescale, а не фиксированной задержки.
Разбор на примере: проверка синхрона в реальном файле
Соберём всё вместе. Пусть ffprobe сообщает о видеодорожке с timescale 90 000 и аудиодорожке с timescale 48 000, и вы хотите понять, должны ли первый аудиокадр и первый видеокадр играть вместе.
Видео: первый кадр DTS = 0, PTS = 3 750 тиков (timescale 90 000)
Аудио: первый кадр PTS = 0, DTS нет (timescale 48 000)
PTS первого видеокадра в секундах = 3 750 ÷ 90 000 = 0,041667 с (≈ 41,7 мс)
PTS первого аудиокадра в секундах = 0 ÷ 48 000 = 0,000000 сПервый показанный кадр видео – на 41,7 миллисекунды позже звука. DTS = 0 при PTS = 3 750 говорит, что у этого видео есть B-кадры: первый кадр в порядке декодирования – опорный, показанный на кадр позже (3 750 тиков = ровно один кадр при 90 000 ÷ 24 = 3 750, то есть это контент 24 fps). Edit list мог намеренно срезать эти 41,7 мс, чтобы свести оба к нулю, – именно поэтому вы проверяете бокс elst, прежде чем заключить, что файл рассинхронизирован. Само смещение здесь в любом случае глубоко внутри окна допуска lip-sync; почему 41,7 мс отставания видео незаметны – см. Что значит «в синхроне»: ITU-R BT.1359, окна lip-sync, заметность.
Смысл упражнения – в методе, а не в числах: прочитайте timescale каждой дорожки, переведите каждый тик в секунды, учтите перестановку DTS-против-PTS, затем проверьте edit list, прежде чем винить кодер. Эта последовательность находит почти любой баг синхрона в записанном медиа.
Где здесь Фора Софт
Мы строим функции записи, транскодирования и стриминга в видеоконференциях, OTT, e-learning, телемедицине и видеонаблюдении с 2005 года, и обработка меток времени – это место, где синхрон записанного медиа выигрывается или проигрывается. Повторяющаяся реальная проблема не экзотична: конвейер записи, мультиплексирующий WebRTC-звонок в MP4, обязан правильно переводить метки времени RTP в длительности stts и смещения ctts, и единственный потерянный edit list или несовпавший timescale позже проявляется как звук, уплывающий от видео на длинной записи. Когда клиент сообщает, что сохранённая сессия рассинхронизирована, хотя живой звонок был в порядке, шаг перевода меток – первое место, куда мы смотрим, потому что именно там стыкуются две системы часов.
Главное
- Каждый access unit несёт PTS (когда показать) и, при необходимости, DTS (когда декодировать).
- DTS отличается от PTS только потому, что B-кадры вынуждают декодирование не по порядку; DTS ≤ PTS всегда.
- MPEG-2 использует фиксированные 33-битные часы 90 kHz; значение живёт в заголовке PES.
- MP4 хранит тайминг таблицами: длительности stts дают DTS, смещения ctts дают PTS.
- Timescale в MP4 – на каждую дорожку; всегда переводите тики в секунды перед сравнением.
- PTS – не время по настенным часам, а 33-битные часы переполняются каждые ~26,5 часа.