Lip-sync в HLS и DASH: где он обычно ломается

Автор: Николай СапуновОбновлено: август 202617 мин чтения
Содержание статьи +

Коротко

В адаптивном стриминге через HLS и DASH аудио и видео передаются как отдельные дорожки сегментов, которые плеер объединяет обратно по временным меткам, встроенным в каждый сегмент. Поэтому синхронизация губ (lip-sync) сохраняется ровно до тех пор, пока эти метки остаются корректными – от упаковщика до буфера плеера. Чаще всего она нарушается в двух местах: при audio priming – тишине, которую аудиокодек добавляет в начало, и которую обычный поток MPEG-TS описать не может, из-за чего она вызывает небольшой сдвиг, – и при вставке рекламы, когда новый HLS-разрыв или новый DASH-период сбрасывают временные часы, а неверный presentationTimeOffset помещает следующий сегмент не туда в буфере. Исправить это почти всегда невозможно на стороне плеера: нужны согласованные edit list во всех рендициях, единый CMAF-энкод, совместимый с обоими протоколами, и правильная арифметика периодов и разрывов на стороне упаковщика. Эта статья простыми словами разбирает работу временных меток и показывает, где именно и почему уходит синхронизация.

Почему это важно

Если вы управляете OTT-сервисом, платформой записи вебинаров, библиотекой для онлайн-обучения или любым продуктом видео по запросу, жалоба «звук чуть позади картинки» – одна из самых разрушительных, потому что профессионально снятый контент начинает выглядеть дешёвым. Особенно обидно, что один и тот же файл может идеально воспроизводиться в одном плеере и «плыть» в другом – и команда начинает гоняться за плеером, хотя настоящая причина – в том, как был упакован контент. Продакт-менеджеру важно понимать, почему поток, прошедший QA в десктопном браузере, теряет синхронизацию при запуске рекламы, а инженеру – какую именно настройку (edit list, смещение периода, последовательность разрывов) нужно изменить. Эта статья даёт обоим участникам ментальную модель, чтобы быстро находить причину и чётко говорить команде упаковки, что именно нужно исправить.

Рисунок 1. Аудио и видео кодируются и упаковываются как отдельные дорожки, затем заново выравниваются на плеере. Каждая передача – место, где тайминг может уйти, и большинство сбоев возникает ещё до того, как сегменты покидают упаковщик.

Сначала – что вообще значит «в синхроне»

В реальном времени приёмник синхронизирует два живых потока по общим настенным часам. Адаптивный стриминг устроен иначе: нет единого источника времени, к которому можно было бы привязаться, и плеер получает не один мультиплексированный файл. Он скачивает список небольших сегментов – обычно продолжительностью от двух до десяти секунд – для видеодорожки и отдельный список сегментов для каждой аудиодорожки, и должен корректно расположить их на внутренней временной шкале так, чтобы нужный звук воспроизводился одновременно с соответствующим кадром.

Число, задающее это «нужное» значение, совпадает с тем, что используется в вещании. Стандарт ITU-R BT.1359-1 устанавливает предел допустимого сдвига на уровне: «звук опережает изображение примерно на 90 миллисекунд или отстаёт примерно на 185 миллисекунд». В пределах этого окна почти никто ничего не замечает. За его границами говорящий выглядит так, будто его слова плохо синхронизированы с губами – как в плохом дубляже, а продукт воспринимается как сломанный. Значит, задача плеера – не идеальное выравнивание сдвига до нуля, а удержание его в допустимых пределах на всём протяжении воспроизведения, включая переключения качества и рекламные паузы.

Для этого плеер полностью полагается на временные метки, записанные в каждый сегмент. Отдельной «дорожки синхронизации» не предусмотрено. Временная метка представления, или PTS, указывает плееру, в какой момент шкалы времени должны быть отображены каждый аудиосэмпл и каждый видеокадр. Если значения PTS на аудио- и видеосегментах корректно описывают одну и ту же шкалу, синхронизация получается автоматически. Если же по цепочке происходит сдвиг PTS одной дорожки относительно другой, плеер честно воспроизведёт эту ошибку.

Два мира контейнеров: MPEG-TS и fragmented MP4

Почти любой баг синхронизации в HLS и DASH сводится к тому, какой контейнер используется для медиа, поэтому важно быть точным в отношении обоих форматов.

Исторически HLS использовал сегменты транспортного потока MPEG – файлы .ts. MPEG-TS пришёл из эфирного вещания, где потоки непрерывны и отсутствует понятие файла с чётко определённым началом и концом. Современные технологии используют fragmented MP4 – сегменты .m4s или .mp4 на основе ISO Base Media File Format, который стал основой упаковки CMAF, теперь поддерживаемой как HLS, так и DASH. DASH изначально работал с fragmented MP4. HLS начал поддерживать этот формат с 2016 года. Common Media Application Format, стандартизированный как ISO/IEC 23000-19, позволяет одному набору fMP4-сегментов обслуживать оба протокола – различается лишь манифест: .m3u8 для HLS и .mpd для DASH.

Это различие не академическое. В fragmented MP4 есть функция под названием edit list – небольшой бокс внутри файла с именем elst, который указывает: «обрежь столько-то сэмплов спереди перед воспроизведением». В MPEG-TS аналогичного механизма нет. Как вы увидите дальше, именно это единственное отсутствие – причина самой частой скрытой ошибки синхронизации в HLS.

Где ломается #1: аудио-прайминг

Это сбой, которого почти никто не ожидает, потому что он заложен в самой природе сжатия звука.

Когда аудиокодек – обычно это AAC – начинает сжимать сигнал, его фильтрам требуется время на «разгон». Они не могут выдать корректный первый выходной сэмпл, пока не обработают часть входного сигнала, поэтому кодек добавляет в начало короткую серию тихих сэмплов – это и есть priming, или encoder delay. Декодер воспроизводит эту тишину в начале, из-за чего декодированное аудио оказывается сдвинутым на длительность priming. Его величина зависит от кодека и строго определена: кодек AAC от Apple по умолчанию использует 2112 сэмплов, широко применяемый FDK-AAC обычно даёт 2048, а нативный AAC в FFmpeg – 1024. При частоте 48 000 Гц распишем арифметику:

задержка priming (секунды) = сэмплы priming ÷ частота дискретизации
2112 сэмплов ÷ 48 000 Гц = 0,044 секунды = 44 миллисекунды

Сорок четыре миллисекунды задержки звука, заложенные в энкодер ещё до отправки контента, – это половина допустимого опережения по ITU-R, «съеденная» проблемой, о которой вы даже не подозревали.

В мире fragmented MP4 всё обрабатывается чисто. Упаковщик записывает edit list – бокс elst, в котором media_time равно количеству сэмплов priming, – и совместимый плеер читает его, обрезая priming перед воспроизведением, чтобы звук встал на своё место. Это тот же механизм, что обеспечивает gapless-воспроизведение в музыкальных файлах. Проблема в том, что MPEG-TS не поддерживает edit list. Когда HLS упаковывает звук в сегменты .ts, тишина priming нигде не описывается, поэтому она воспроизводится как настоящий звук. В результате получается небольшая фиксированная задержка аудио, которую плеер не может устранить.

«Подводный камень – ловушка «в Safari же всё ок». Реальный задокументированный случай в проекте hls.js показал исходный MP4, у которого видеодорожка несла media_time edit list, равный 1024, а аудиодорожка – 2048. Safari, который понимает edit list, отрисовал это одним образом; браузерные плееры на Media Source Extensions забуферили видео со стартовым разрывом, равным composition offset первого кадра – около 83 миллисекунд на клипе 24 кадра в секунду, – и рендеры перестали совпадать между собой. Тот же контент, тот же манифест, разное поведение плееров – именно поэтому команды ставят неверный диагноз «баг плеера» вместо «проблемы упаковки».»

Лечение – полная упаковка. Либо переход на fMP4 / CMAF-сегменты, чтобы edit list сохранялся, либо гарантия, что энкодер применяет согласованный, объявленный priming ко всем аудиорендициям – тогда сдвиг хотя бы будет одинаковым. Чего нужно избегать – разного priming в разных рендициях: когда плеер переключает качество аудио и priming меняется, звук «скачет» относительно видео посреди потока.

Где ломается #2: рендеры, не делящие один edit

Адаптивный стриминг предлагает несколько уровней качества – несколько видеобитрейтов, иногда и аудиобитрейтов – и плеер переключается между ними в зависимости от доступной полосы пропускания. Чтобы синхронизация сохранилась при переключении, каждая рендиция должна использовать одну и ту же временнúю шкалу. Если у видео высокого качества у первого кадра composition offset составляет 83 миллисекунды, а низкое качество закодировано без этого смещения, то при переключении плеера на низкое качество видео сместится относительно звука.

Это проблема выравнивания рендиций, и она коверна, потому что проявляется только под нагрузкой сети – условием, которое QA никогда не воспроизводит на быстром офисном канале. Плеер выравнивает сегменты по времени появления первого кадра, и если у части рендиций есть edit или composition offset, а у других – нет, выравнивание становится непоследовательным, и в буфере возникают дыры в точках переключения. Разработчики hls.js чётко формулируют правило: медиаинженеры обязаны предоставлять мезонины с согласованными edit во всех битрейтах. Синхрон – это не только соответствие аудио и видео, но и согласованность видео по всей лестнице битрейтов.

Где ломается #3: вставка рекламы

Если audio priming – самая частая тихая ошибка, то вставка рекламы – самая частая громкая: пауза, на которой синхрон зримо разваливается.

Причина в том, что реклама – это отдельный контент, закодированный иной системой, зачастую с другим аудиокодеком, а значит, с иным priming, и вставленный в основной поток в режиме реального времени. HLS и DASH по-разному маркируют эту вставку, и у каждого из них свой способ обработки сбоев.

В HLS склейка помечается тегом EXT-X-DISCONTINUITY. Разрыв сообщает плееру: «шкала, которой вы следовали, здесь обрывается; следующий сегмент начинает новый контекст тайминга со своими метками». Плеер сбрасывает временные отсчёты и заново привязывает часы к PTS нового сегмента. Это корректно и необходимо – у меток рекламы нет связи с основным контентом, – но означает, что все предположения плеера о синхронизации аудио и видео пересчитываются с нуля на границе. Если у звука рекламы иной priming или её сегменты внутренне не выровнены, синхрон, который был стабильным в основном контенте, может нарушиться в рекламе и сохраниться после её окончания.

В DASH эквивалентом является новый Period. MPD описывает основной контент как один Period, а рекламу – как другой, при этом время представления каждого сегмента отсчитывается от начала своего Period, а не от начала всего воспроизведения. Именно конкретное значение presentationTimeOffset определяет, будет ли синхронизация сохранена – и ошибка в этом числе является самой распространённой проблемой при вставке рекламы в DASH.

DASH `presentationTimeOffset`, по шагам

Плеер размещает каждый сегмент во внутреннем буфере, используя самое раннее время представления этого сегмента, рассчитанное на основе манифеста, с добавлением смещения. API W3C Media Source Extensions отображает это как timestampOffset. Соотношение, которое использует плеер:

позиция в буфере = MSE.timestampOffset + самое раннее время представления сегмента
MSE.timestampOffset = Period@start − Period@presentationTimeOffset

Возьмём конкретный мидролл: 8 секунд основного контента, затем 4-секундная реклама, затем снова основной контент. Третий период – возобновлённый основной контент – не сбрасывает шкалу сегментов в ноль; его первый сегмент сохраняет самое раннее время представления – 8 (то есть с того момента, где остановились). Если упаковщик задаёт только начало периода и оставляет presentationTimeOffset равным нулю, плеер вычислит:

Period@start = 12, presentationTimeOffset = 0
позиция в буфере = (12 − 0) + 8 = 20 секунд

Сегмент, который должен находиться на 12-й секунде шкалы, оказывается на 20-й. В буфере образуется разрыв длиной 8 секунд – плеер либо останавливается, либо пропускает этот участок, а аудио и видео могут отображаться с рассинхронизацией – типичный случай, когда «синхрон ухудшается после каждой рекламной паузы». Убедитесь, что смещение установлено правильно – оно должно равняться самому раннему времени представления первого сегмента данного Period:

Period@start = 12, presentationTimeOffset = 8
позиция в буфере = (12 − 8) + 8 = 12 секунд

Теперь сегмент попадает ровно туда, куда должен, и шкала времени становится непрерывной. Команда dash.js из Fraunhofer FOKUS, поддерживающая референсный DASH-плеер, называет ошибку смещения одной из самых частых причин сбоев при воспроизведении multi-period контента. Согласно их рекомендациям, presentationTimeOffset должен соответствовать самому раннему времени представления первого сегмента в Period, выраженному в собственной шкале времени timescale дорожки. Забывайте про timescale – и значение будет ошибаться на порядки.

Рисунок 2. Один и тот же мидролл, два манифеста. Слева `presentationTimeOffset` оставлен равным нулю, и возобновлённый контент смещается на восемь секунд вперёд. Справа смещение установлено равным самому раннему времени представления первого сегмента, и временная шкала остаётся непрерывной.

Где ломается #4: дыры в буфере

Оба предыдущих случая приводят к одному и тому же низовому симптому – разрыву, который медиабуфер плеера не может компенсировать. Браузерные плееры, использующие Media Source Extensions, строги: большинство из них не допускает разрывов в забуференной шкале и немедленно останавливаются, как только воспроизведение доходит до такого разрыва. Команда Fraunhofer выделяет две основные причины таких разрывов: несовпадение границ периодов или сегментов, а также суммарная длительность сэмплов в сегментах, которая оказывается короче заявленной в манифесте.

Вторая причина тонка и заслуживает отдельного внимания. Если манифест указывает, что сегмент длится ровно 2000 секунд, а аудиосэмплы в сумме составляют 1998 секунд – из-за того, что количество сэмплов и частота дискретизации не делятся нацело на длительность сегмента, – каждый сегмент оставляет зазор в 2 миллисекунды. Одна такая пауза – несущественна. Но к шестисотому сегменту длинного VOD-актива накопленный сдвиг звука превышает секунду, и он стабильно отстаёт от видео. Это тот же самый дрейф, который вы исправляете ресэмплингом или сбросом кадров в реальном времени, только здесь он зафиксирован в сегментах, и никакая хитрость плеера не может полностью его устранить.

Плееры защищаются от ошибок с помощью логики перепрыгивания – она реализована и в dash.js, и в hls.js, – но такой прыжок – это лишь временная заплатка на дефект упаковки, а не истинное решение. Он маскирует мелкие сбои ценой едва заметного скачка и не способен восстановить синхронизацию, которую ошибка нарушила.

Самое эффективное решение: кодировать один раз через CMAF

У большинства этих режимов сбоя общий корень: аудио и видео – или HLS- и DASH-варианты одного и того же контента – создаются отдельными процессами, не согласовавшими шкалу времени. Единственное структурное решение – отказаться от параллельной упаковки.

Единый CMAF-энкод создаёт один набор fMP4-сегментов с единым согласованным набором временных меток и edit list, на которые ссылаются как HLS-, так и DASH-манифесты. Аудио и видео выравниваются один раз – на этапе кодирования, и это выравнивание остаётся неизменным независимо от протокола плеера. Edit list сохраняются, поскольку их поддерживает фрагментированный MP4. Лестница битрейтов использует общую временную шкалу, потому что все сегменты нарезаны из одного мезонина. Вы устраняете целый класс ошибок вроде «работает в DASH, не работает в HLS», просто не давая двум форматам расходиться. Поэтому отраслевой консенсус 2026 года – отдавать оба протокола из единого CMAF-источника, а не поддерживать два отдельных конвейера упаковки.

CMAF не решает проблему вставки рекламы волшебным образом – вклеенная реклама остаётся отдельным контентом и по-прежнему требует корректной обработки разрывов и временных периодов, – но он устраняет ошибки, связанные с несовпадением контейнеров и рендиций, которые вызывают большую часть остальных проблем.

Как диагностировать: короткий путь решения

Когда поток рассинхронизирован, симптом подсказывает, куда смотреть. Небольшое постоянное отставание звука с первой же секунды, одинаковое на всех плеерах, не учитывающих edit list, почти всегда указывает на audio priming в конвейере на MPEG-TS или на обрезанный edit list. Синхронизация, нормальная в основном контенте, но нарушаемая с первой рекламной вставкой и ухудшающаяся после каждой паузы, – признак ошибки разрыва или смещения периода. Стабильный медленный дрейф, незаметный в начале, но становящийся очевидным к двадцатой минуте, – следствие округления длительности сегментов. Синхронизация, стабильная на быстром канале, но нарушаемая при переключении качества из-за сетевых условий, – проблема выравнивания рендиций, вызванная несогласованными edit list по лестнице.

Обратите внимание, чего нет в этом списке: кодек, CDN и плеер редко бывают настоящей причиной, хотя именно их чаще всего обвиняют в первую очередь. Причина почти всегда – в числе, записанном или не записанном на упаковщике.

Сравнительная таблица: механика синхронизации HLS против DASH

АспектHLSDASH
Исходный контейнерMPEG-TS (.ts), теперь fMP4/CMAFFragmented MP4 с самого начала
Edit list / обрезка primingТеряется в MPEG-TS, сохраняется в fMP4Сохраняется (fMP4)
Маркер рекламной паузыEXT-X-DISCONTINUITYНовый Period
Управление сбросом шкалыПоследовательность разрывовPeriod@start + presentationTimeOffset
Самый частый баг синхронаpriming в .ts; несовпадение edit рендицийНеверный presentationTimeOffset между периодами
Общее современное лечениеЕдиный CMAF-энкод (ISO/IEC 23000-19)Единый CMAF-энкод (ISO/IEC 23000-19)

Где здесь Фора Софт

Мы разрабатываем стриминговые и OTT-решения, где важны реальные практики, а не только теория: VOD-библиотеки для онлайн-обучения, прямой и архивный стриминг для медиаплатформ, телемедицинские системы, где врач просматривает запись консультаций, и системы видеонаблюдения. В каждом из этих случаев синхронизация звука и изображения (lip-sync), сохраняющаяся при переключении качества и переходе между рекламой или главами, – это то, что отличает качественный продукт от того, который кажется неработоспособным. Большая часть нашей работы над синхронизацией вообще не связана с плеером – она заключается в том, чтобы гарантировать, что конвейер упаковки формирует согласованные edit list, использует один CMAF-энкод одновременно для HLS и DASH и корректно обрабатывает границы периодов и разрывов. Именно на этом уровне стриминговый lip-sync либо достигается, либо теряется.

Главное

  • Аудио и видео в HLS и DASH – отдельные дорожки; плеер синхронизирует их по меткам сегментов.
  • Audio priming добавляет десятки миллисекунд задержки, которые MPEG-TS не может описать – используйте fMP4/ CMAF.
  • Вставка рекламы сбрасывает временные метки; неправильный DASH presentationTimeOffset – главная причина багов в multi-period.
  • Несогласованные edit list по рендициям нарушают синхронизацию только при переключении качества из-за сетевых условий.
  • Округление длительности сегментов вызывает медленный дрейф, накапливающийся при длительном воспроизведении.
  • Почти всегда проблема в упаковщике, а не в плеере – кодируйте один раз через CMAF.

Что почитать дальше

Строите такую систему?

Подберём параметры кодирования под ваш контент и посчитаем стоимость доставки до старта разработки.