Содержание статьи +
- Коротко
- Почему это важно
- Задача: два потока, двое часов, нет общего нуля
- Что на самом деле считает RTP-метка времени
- NTP-метка: универсальные стенные часы
- RTCP-отчёт отправителя: записка, привязывающая часы к времени
- Рабочий пример: совмещаем аудио и видео
- Почему синхронизация защёлкивается не сразу – и как WebRTC её ускоряет
- Что ломается: дрейф, джиттер и пропавший якорь
- Синхронизация RTP/RTCP против вещательной модели PCR
- Где это в конвейере WebRTC
- Где здесь Фора Софт
- Главное
- Что почитать дальше
Коротко
В реальном звонке аудио и видео идут как два отдельных потока RTP-пакетов, и каждый поток несёт свои собственные часы, которые стартуют со случайного числа и считают в своих единицах, – поэтому напрямую сравнить эти двое часов нельзя. Решение – RTCP-отчёт отправителя (sender report, SR): маленький управляющий пакет, который каждый отправитель шлёт раз в несколько секунд и в котором сказано: «моё показание медиа-часов X произошло в такое-то реальное время по стенным часам», выраженное в формате Network Time Protocol (NTP) как число секунд от 1900 года. Приёмник собирает по одному такому соответствию на каждый поток, переводит оба потока на одни общие стенные часы и только тогда может совместить нужный сэмпл аудио с нужным кадром видео. Сделайте это соответствие правильно – и lip-sync держится часами; потеряйте его (неверные SR, уплывающие часы или их полное отсутствие) – и звук уползёт от картинки, как бы чисто ни выглядел каждый поток по отдельности.
Почему это важно
Если вы строите что-либо, что шлёт живое медиа через интернет – приложение видеоконференций, платформу телемедицины, SFU для трансляций, WebRTC-игру, – синхронизация аудио и видео не возникает сама собой, а машинерия, которая её обеспечивает, – это RTP-метка времени плюс RTCP-отчёт отправителя. Продакт-менеджеру нужно знать, что они существуют, чтобы понимать: «аудио и видео по отдельности в порядке, но расходятся» – это реальный, частый и исправимый класс багов, а не загадка. Инженеру нужно знать ровно, что считает RTP-метка, что несёт SR и как приёмник превращает двое несвязанных часов в одну временную шкалу, чтобы прочитать дамп статистики WebRTC и отличить ошибку синхронизации от обычного джиттера. Эта статья даёт обоим читателям одну модель: у каждого потока свои часы, отчёт отправителя – это записка, привязывающая часы к реальному времени, а lip-sync – это приёмник, делающий арифметику над этими записками.
Задача: два потока, двое часов, нет общего нуля
Начнём с факта, который удивляет тех, кто впервые сталкивается с реальным временем медиа. Когда вы заходите в видеозвонок, аудио микрофона и видео камеры не едут вместе. Их кодируют по отдельности, пакетируют по отдельности и шлют как два независимых потока маленьких пакетов по протоколу Real-time Transport Protocol, RTP. Каждый пакет помечен RTP-меткой времени – числом, которое говорит, когда, по медиа-часам отправителя, был оцифрован звук или кадр в этом пакете. Казалось бы, этого достаточно, чтобы держать их вместе. Это не так, по двум причинам, которые усугубляют друг друга.
Первая причина – двое часов считают в разных единицах. По давней традиции часы аудиопотока и часы видеопотока идут с разной частотой, поэтому «RTP-метка 96000» означает одно прошедшее время на аудиопотоке и совсем другое на видеопотоке. Вторая причина хуже: RFC 3550, стандарт, определяющий RTP, требует, чтобы метка времени каждого потока стартовала со случайного значения. Это намеренная мера безопасности – предсказуемая стартовая точка облегчила бы некоторые атаки, – но она означает, что ноль аудио-часов и ноль видео-часов не связаны между собой никак. Двое часов, две разные скорости, две случайные стартовые точки. Сравнить их нельзя.
Этот пробел и заполняет RTCP-отчёт отправителя. Рядом с медиапотоками каждый отправитель периодически шлёт маленький управляющий пакет по протоколу RTP Control Protocol, RTCP. Самый важный из них – отчёт отправителя, SR. Он несёт одну ключевую пару чисел: показание RTP-медиачасов этого потока и реальное время по стенным часам в тот же момент, записанное в универсальном формате. Соберите по одной такой паре для аудиопотока и для видеопотока – и вдруг обои случайные, по-разному идущие часы пришпилены к одной внешней шкале. RTP-метка говорит приёмнику, когда относительно старта самого потока; отчёт отправителя говорит, что это значит в реальном времени. Lip-sync – это то, что получается, когда вы делаете это для двух потоков сразу.
Что на самом деле считает RTP-метка времени
Самое частое заблуждение про RTP – что метка времени это стенное время вроде «14:32:07». Это не так. RTP-метка считает тики собственных часов оцифровки этого медиа, и RFC 3550 конкретен в том, как она ведёт себя: она растёт монотонно и линейно во времени, должна происходить от часов с постоянной частотой и – что критично – считает медиа-время, а не пакеты и не реальное время.
Конкретный аудиопример проясняет это. Допустим, вы шлёте поток аудио Opus. Каждый RTP-пакет обычно несёт 20 миллисекунд звука. Формат RTP-полезной нагрузки для Opus, RFC 7587, фиксирует частоту часов на 48 000 тиков в секунду для любого режима Opus и любой внутренней частоты дискретизации. Поэтому метка растёт на фиксированную величину на пакет:
тиков на пакет = частота часов × длительность пакета
= 48 000 тиков/с × 0,020 с
= 960 тиков на 20-мс пакетЕсли метка одного пакета 96 000, у следующего – 96 960, у следующего – 97 920 и так далее, каждый пакет на 960 тиков позже предыдущего, независимо от того, когда он реально ушёл в сеть. Последняя оговорка – самое сердце. Метка фиксирует, когда звук был оцифрован, а не когда пакет ушёл. Если два пакета оцифрованы с интервалом 20 мс, но сеть задержала второй, их метки всё равно ровно на 960 различаются. Метка – это свойство медиа, а не передачи.
Видео работает так же, но с другой частотой. По традиции, заданной в RTP-профиле для аудио и видео RFC 3551 и соблюдаемой практически каждым видеокодеком, видео-RTP-потоки используют часы на 90 000 тиков в секунду. Девяносто килогерц выбрали потому, что они делятся нацело на распространённые частоты кадров: 90 000 ÷ 30 = 3 000 тиков на кадр при 30 fps, 90 000 ÷ 25 = 3 600 при 25 fps, 90 000 ÷ 24 = 3 750 при 24 fps, – так что интервал кадра всегда целое число тиков. Это те же часы 90 kHz, что проходят через MPEG-2 transport stream; если вы читали PTS, DTS и метка времени элементарного потока, это та же часовая область, переиспользованная.
Вот ловушка, которая делает синхронизацию трудной, сказанная прямо. Аудиопоток считает со скоростью 48 000 в секунду; видеопоток – 90 000 в секунду; и каждый стартовал со своего случайного числа. Нет арифметики, которую можно проделать над двумя RTP-метками отдельно и узнать, какой сэмпл аудио принадлежит какому кадру видео. Нужен внешний мост. Этот мост – отчёт отправителя.
| Медиа | Типичная частота RTP-часов | Тиков на единицу | Источник |
|---|---|---|---|
| Аудио Opus | 48 000 Hz | 960 на 20-мс пакет | RFC 7587 |
| Телефонное аудио (G.711) | 8 000 Hz | 160 на 20-мс пакет | RFC 3551 |
| Видео (все частые кодеки) | 90 000 Hz | 3 000 на кадр при 30 fps | RFC 3551 |
Таблица 1. Частоты RTP-медиачасов по типу медиа. Частоты различаются намеренно – именно поэтому две сырые RTP-метки нельзя сравнить без стенного моста из отчёта отправителя.
NTP-метка: универсальные стенные часы
Чтобы привязать медиа-часы к реальному времени, нужен способ записать реальное время, на котором сходятся все устройства. RTP заимствует формат протокола Network Time Protocol, NTP, того самого, что держит часы компьютеров и телефонов примерно правильными. Формат NTP-метки – это 64-битное число с фиксированной точкой, считающее секунды от фиксированной точки отсчёта: 0 часов UTC 1 января 1900 года. Старшие 32 бита хранят целые секунды; младшие 32 – долю секунды.
Разбиение 64 бит на 32 целых и 32 дробных даёт сразу огромный диапазон и тонкое разрешение. 32 бита целых секунд считают около 136 лет секунд (поэтому формат переполняется в 2036 году – известное и управляемое событие). 32 дробных бита делят каждую секунду примерно на 4,3 миллиарда частей, так что наименьший представимый шаг – около 233 пикосекунд, куда тоньше, чем нужно любому медиа-таймингу. Этот формат и использует отчёт отправителя, и тот же формат использует WebRTC-расширение заголовка «absolute capture time», когда штампует на пакет момент исходного захвата.
Одно практическое замечание, на котором спотыкаются инженеры, читающие сырые пакеты. Полная NTP-метка – 64 бита, но компактная форма «средних битов» в RTP (она встречается в некоторых полях на стороне приёмника) берёт младшие 16 бит секунд и старшие 16 бит доли, давая 32-битное значение, которое переполняется каждые несколько секунд, но годится для краткосрочной арифметики интервалов. Когда вы видите 32-битное NTP-подобное значение в дампе статистики, это почти всегда эта компактная форма, а не полная 64-битная.
RTCP-отчёт отправителя: записка, привязывающая часы к времени
Теперь центральная часть. Каждый активный отправитель в RTP-сессии периодически передаёт RTCP-отчёт отправителя. Внутри него блок «sender info» несёт четыре числа, и двое из них делают работу синхронизации:
NTP-метка (64 бита) – это стенное время в формате NTP выше, в момент, когда этот отчёт был сгенерирован. RTP-метка (32 бита) – это показание медиа-часов данного потока в тот же самый момент, и RFC 3550 явно говорит, что она соответствует тому же времени, что и NTP-метка, выражена в тех же единицах и с тем же случайным сдвигом, что и RTP-метки на пакетах данных. Остальные два поля – счётчик пакетов и счётчик октетов отправителя – это нарастающие итоги для статистики и оценки потерь, а не для синхронизации.
Эта пара – весь фокус. Пакеты данных дают приёмнику поток RTP-меток без реального смысла. Отчёт отправителя вручает одну сопоставленную пару – «RTP-метка R произошла в NTP-время T» – и этот один якорь позволяет приёмнику перевести любую RTP-метку этого потока в стенное время, потому что частота часов известна и постоянна:
Для аудиопотока с часами 48 000 Hz и отчётом отправителя,
говорящим, что RTP-метка R0 = NTP-время T0:
стенное_время(R) = T0 + (R − R0) ÷ 48 000 секундПроделайте то же для видеопотока с его часами 90 000 Hz и его собственным отчётом отправителя – и теперь у каждого сэмпла аудио и каждого кадра видео есть стенное время на одной и той же шкале. Lip-sync становится поиском: когда часы воспроизведения достигают стенного времени t, показать сэмпл аудио и кадр видео, чьи вычисленные стенные времена оба равны t. Эти два потока никогда не были сравнимы сами по себе; отчёты отправителя сделали их сравнимыми.
WebRTC связывает два потока ещё одной деталью – CNAME, каноническим именем в RTCP, которое говорит: «эти потоки из одного источника и их надо воспроизводить синхронно». Приёмник по CNAME понимает, что именно этот аудиопоток и тот видеопоток образуют пару для синхронизации, – без него приёмник в многоточечном звонке не знал бы, какое аудио к какому видео. RFC 8834, определяющий, как RTP используется в WebRTC, требует, чтобы все потоки одного источника несли один общий CNAME ровно по этой причине.
Рабочий пример: совмещаем аудио и видео
Сделаем арифметику конкретной одним расчётом синхронизации. Допустим, приёмник собрал по одному отчёту отправителя с каждого потока звонка:
Аудиопоток (Opus, часы 48 000 Hz):
SR говорит: RTP-метка 720 000 ↔ NTP-время T_a
Видеопоток (часы 90 000 Hz):
SR говорит: RTP-метка 1 350 000 ↔ NTP-время T_v (и T_v = T_a, тот же момент)Теперь приходит пакет данных на аудиопотоке с RTP-меткой 768 000. Насколько он позади аудио-якоря в реальном времени?
Δтиков = 768 000 − 720 000 = 48 000 тиков
Δвремя = 48 000 ÷ 48 000 Hz = 1,000 секунда после T_a
→ это аудио оцифровано в стенное время T_a + 1,000 сПриходит кадр видео с RTP-меткой 1 395 000. Насколько он позади видео-якоря?
Δтиков = 1 395 000 − 1 350 000 = 45 000 тиков
Δвремя = 45 000 ÷ 90 000 Hz = 0,500 секунды после T_v
→ этот кадр оцифрован в стенное время T_v + 0,500 сПоскольку T_a и T_v – один и тот же стенной момент, аудиопакет (якорь + 1,000 с) и кадр видео (якорь + 0,500 с) отстоят на 500 миллисекунд в реальном времени – аудио на полсекунды впереди этого кадра. Приёмник теперь знает, что должен придержать аудио или выпустить видео первым, чтобы нужный сэмпл и нужный кадр дошли до динамика и экрана вместе. Без двух отчётов отправителя эти две RTP-метки – 768 000 и 1 395 000 – было бы бессмысленно сравнивать. С ними сдвиг – это вычитание в одну строку. В этом весь механизм.
Почему синхронизация защёлкивается не сразу – и как WebRTC её ускоряет
В дизайне есть встроенная загвоздка. Отчёты отправителя приходят не с каждым пакетом; они едут по RTCP, а RTCP ограничен по скорости. RFC 3550 ограничивает весь RTCP-трафик примерно 5% от полосы сессии и задаёт рекомендуемый минимальный интервал в 5 секунд между отчётами одного отправителя (с уменьшенным минимумом для маленьких сессий). Следствие прямое: приёмник не может синхронизировать два потока, пока не получит хотя бы один отчёт отправителя на каждом потоке, поэтому в начале звонка может быть окно до нескольких секунд, где аудио и видео играют, но ещё не сцеплены. Вы наверняка это видели – первые секунду-две звонка губы и голос слегка не совпадают, а потом защёлкиваются на место.
Для большинства конференций это незаметно. Для приложений, которые этого не терпят, – слоистое видео, быстрое переключение каналов, поздние подключения к трансляции – IETF определил RFC 6051 «Rapid Synchronisation of RTP Flows». Он предлагает три механизма: ужесточённый тайминг RTCP, чтобы первые отчёты пришли раньше; сообщение обратной связи, позволяющее переключающему серверу потребовать немедленную ресинхронизацию; и новое расширение заголовка RTP, которое несёт NTP/RTP-соответствие внутри самих медиапакетов, так что приёмнику вообще не нужно ждать RTCP-отчёта.
WebRTC довёл идею расширения заголовка дальше всех своим расширением absolute capture time, которое штампует выбранные RTP-пакеты NTP-временем по стенным часам того момента, когда был захвачен самый первый кадр в этом пакете. Это больше, чем ускорение. Метка обычного отчёта отправителя генерируется в момент отправки отчёта, после того как захват, кодирование и буферизация уже добавили задержку; расширение absolute-capture-time несёт истинный момент захвата с вычтенными известными аппаратными задержками, так что совмещение на приёмнике привязано к реальности, а не к моменту генерации отчёта. Это же и позволяет синхронизации пережить микшер – SFU или MCU, который перепакетирует потоки и иначе уничтожил бы тайминг исходного отправителя. Расширение позволяет исходному времени захвата проехать сквозь микшер нетронутым.
Вот первая частая ошибка, которую стоит назвать, потому что её мы видим в продакшене чаще всего. Когда сервер посередине – SFU, пересылающий потоки, MCU, микширующий их, или конвейер записи, переставляющий метки на пакетах, – перегенерирует RTCP-отчёты отправителя по своим часам вместо того, чтобы сохранить исходное соответствие, каждый приёмник ниже по цепочке синхронизируется к неверному якорю. Аудио и видео по отдельности выглядят идеально; они просто расходятся, потому что записку, привязывавшую их к реальному времени, переписала коробка, которая никогда не видела исходного захвата. Когда клиент сообщает «синхронизация была в порядке peer-to-peer, но ломается, едва мы пускаем через сервер», обработка отчётов отправителя в этой коробке посередине – первое, что надо проверить.
Что ломается: дрейф, джиттер и пропавший якорь
Поскольку приёмник полностью доверяет NTP/RTP-паре отчёта отправителя, синхронизацию портят три разных отказа, у каждого свой почерк.
Дрейф часов – медленный. Медиа-часы отправителя и часы воспроизведения приёмника – это разные физические кварцы, и никакие два кварца не идут ровно с одной частотой. Несколько частей на миллион разницы – норма и неизбежность. Без коррекции даже крошечная разница частот накапливается: сдвиг в 10 частей на миллион уводит два потока на 36 миллисекунд за час – достаточно, чтобы к концу долгого звонка было заметно не в фазе. Отчёты отправителя противостоят этому тем, что приходят повторно – каждый свежий отчёт заново привязывает соответствие, так что оценка приёмника «где сейчас реальное время» постоянно корректируется, прежде чем дрейф успеет накопиться. Поэтому отчёты отправителя должны идти всю сессию, а не только в начале.
Сетевой джиттер – шумный. Пакеты приходят неравномерно; маршрутизаторы и коммутаторы сбивают их в кучу и растягивают. Это не портит RTP-метки – они зафиксированы у отправителя и описывают время оцифровки, а не прибытия, – но добавляет шум в измерение приёмником момента прибытия каждого отчёта, что слегка размывает оценку часов. RTP определяет статистику interarrival jitter (межприбытийный джиттер), отправляемую обратно в отчётах приёмника, именно чтобы это измерить: это сглаженное среднее разницы между ожидаемым и фактическим интервалом прибытия пакетов, в единицах часов самого потока. Высокий interarrival jitter – это число, которое говорит инженеру, что проблема в сети, а не в кодере.
Пропавший или неверный якорь – фатальный. Если отчёты отправителя не приходят вовсе – заблокированы файрволом, отброшены мидлбоксом или просто никогда не сгенерированы, – у приёмника есть RTP-метки, которые он может гладко проигрывать внутри каждого потока, но нет способа связать аудио с видео. Каждый поток сам по себе звучит и выглядит хорошо; вместе они без привязки. Это причина самых сбивающих с толку багов синхронизации, потому что любая метрика по отдельному потоку выглядит здоровой. Ошибка – в отсутствии межпотокового моста, а не в дефекте какого-то потока.
Видимые симптомы прямо отображают причины. Медленный, монотонный уход lip-sync, который усиливается за долгий звонок, – это дрейф часов, обгоняющий частоту отчётов. Синхронизация, которая дёргается и нестабильна, но не уползает в одну сторону, – это сетевой джиттер, размывающий якорь. Синхронизация, которая вообще не устанавливается – аудио и видео каждый в порядке, но навсегда сдвинуты, – это пропавший или переписанный отчёт отправителя. Что считать заметным сдвигом и какие окна допуска должен пересечь сбой, прежде чем зритель заметит, – см. Что значит «в синхроне»: ITU-R BT.1359, окна lip-sync, заметность.
| Отказ | Что это | Что видит приёмник | Типичный симптом |
|---|---|---|---|
| Дрейф часов | Кварцы отправителя и приёмника идут с чуть разной частотой | Якорь медленно устаревает между отчётами | Lip-sync уходит ровно; хуже чем дольше звонок |
| Сетевой джиттер | Пакеты приходят кучей и врастяжку | Шум в измеренном времени прибытия отчёта | Синхронизация дёргается, нестабильна, но не в одну сторону |
| Пропавший / неверный SR | Нет SR или он переписан мидлбоксом | Нет валидного NTP/RTP-якоря для потока | Потоки по отдельности в норме, вместе навсегда не в фазе |
| Задержка защёлкивания | RTCP ограничивает первые отчёты | Якоря ещё нет для одного или обоих потоков | Первые ~1–5 с звонка слегка не в фазе, затем защёлкивается |
Таблица 2. Четыре способа, которыми синхронизация RTP/RTCP отказывает, что переживает приёмник и о каком симптоме сообщает пользователь. Лимиты и статистики – из RFC 3550 (тайминг RTCP, interarrival jitter) и RFC 6051 (задержка защёлкивания).
Синхронизация RTP/RTCP против вещательной модели PCR
Стоит поставить это рядом с ответом вещательного мира на ту же задачу, потому что контраст проясняет оба. Спутниковый или кабельный transport stream решает «у декодера нет часов» отправкой программных часов, PCR, – периодического снимка одних мастер-часов, которые декодер восстанавливает и затем читает по ним каждый PTS и DTS. Один поток, одни часы, по одностороннему каналу. Это мы разбираем полностью в статье PCR в MPEG-TS: программные часы, на которых держится вещание.
RTP сталкивается с более трудной версией задачи. Нет единых мастер-часов и нет гарантированного одностороннего канала; есть два или более независимых потока, возможно даже с разных хостов, приходящие по теряющей пакеты сети с обратным каналом. Поэтому вместо одного встроенного эталона часов RTP использует пару самоописывающих записок – отчёты отправителя – и полагается на приёмник, который сшивает потоки на общую шкалу NTP. Вещательная модель предполагает управляемую трубу и одни часы; RTP-модель предполагает хаос и восстанавливает порядок на краю. PCR – это часы, которые декодер копирует; отчёт отправителя – это таблица перевода, которую приёмник применяет. Разные каналы, разный дизайн, одна и та же цель: придать каждой метке реальный смысл.
Когда эталонные часы действительно нужно разделять между устройствами – профессиональное живое производство по IP, где десятки источников должны быть с точностью до сэмпла, – индустрия идёт ещё на шаг и привязывает каждое устройство к общим часам Precision Time Protocol (PTP, IEEE 1588), сигнализируемым в описании сессии по RFC 7273 и предписанным SMPTE ST 2110 для IP-инфраструктуры студий. Там NTP-метки в отчётах отправителя не просто внутренне согласованы; они привязаны к одним физическим grandmaster-часам на всём объекте. Про контрибуцию и живой IP см. Живой звук: контрибуционные кодеки, AES67, SMPTE 2110-30.
Где это в конвейере WebRTC
Полезно расположить отчёт отправителя внутри большей машины. WebRTC-эндпоинт захватывает аудио и видео, кодирует каждый, пакетирует в RTP и шлёт – пока параллельный RTCP-канал несёт отчёты отправителя наружу и отчёты приёмника обратно. На приёмной стороне каждый поток проходит свой собственный jitter buffer, который поглощает сетевой джиттер и восстанавливает ровное воспроизведение для этого потока в отдельности. Слой синхронизации сидит над двумя jitter buffer-ами: он берёт стенные времена, выведенные из отчётов отправителя каждого потока, и говорит двум буферам, насколько задержать один поток относительно другого, чтобы совпадающие аудио и видео дошли до выхода вместе. Jitter buffer отвечает за плавность внутри потока; синхронизация по отчётам отправителя – за совмещение между потоками. Это разные задачи, выполняемые разными компонентами. Про половину с буфером см. Jitter buffer: NetEQ, мозг звука WebRTC, а про всю цепочку – Конвейер звука WebRTC целиком.
Где здесь Фора Софт
Мы строим продукты видеоконференций, телемедицины, онлайн-обучения и трансляций на WebRTC задолго до того, как он стал завершённым стандартом, и межпотоковая синхронизация – это место, где конвейеры реального времени тихо проходят или проваливаются. Реальная проблема редко в кодеках: звонок идеально синхронен peer-to-peer, затем добавляют медиасервер для масштабирования комнаты, и аудио с видео начинают расходиться – потому что сервер перегенерирует RTCP-отчёты отправителя по своим часам вместо того, чтобы сохранить исходное соответствие захвата или пробросить расширение absolute-capture-time. Когда клиент сообщает «синхронизация была в порядке в прототипе и сломалась, когда мы добавили SFU», обработка отчётов отправителя и расширений заголовка в этом сервере – первый шов, который мы осматриваем, потому что именно там мост между медиа-часами и реальным временем тихо переписывается.
Главное
- Аудио и видео – отдельные RTP-потоки с отдельными часами со случайным стартом.
- RTP-метка считает тики оцифровки медиа, а не пакеты и не стенное время.
- Аудио обычно тактируется на 48 000 Hz (Opus) или 8 000 Hz; видео – на 90 000 Hz.
- RTCP-отчёт отправителя сопоставляет одно RTP-показание со стенным временем NTP.
- Приёмник по одной паре на поток ставит оба на одну шкалу – это и есть lip-sync.
- Дрейф, джиттер или пропавший/переписанный SR ломают синхронизацию даже при чистых потоках.