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

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

Коротко

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

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

Если вы ведёте OTT-сервис, платформу записей вебинаров, библиотеку для онлайн-обучения или любой видео-on-demand продукт, жалоба «звук чуть позади картинки» – одна из самых разрушительных, потому что профессионально снятый контент начинает казаться дешёвым. Обидно то, что один и тот же файл часто идеально играет в одном плеере и плывёт в другом – и команда гоняется за плеером, тогда как настоящая вина в том, как упаковали контент. Продакт-менеджеру нужно понимать, почему поток, прошедший 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: audio priming

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

Когда аудиокодек – обычно это 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, отрисовал это одним образом; браузерные плееры на 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-секундная реклама, затем снова основной контент. Третий Period – возобновлённый основной контент – не перезапускает шкалу сегментов в ноль; его первый сегмент несёт самое раннее время представления 8 (где остановились). Если упаковщик задаёт только начало Period и оставляет 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 называет две главные причины таких дыр: периоды или сегменты, не совпадающие на своих границах, и сегменты, чья суммарная длительность сэмплов короче длительности, заявленной манифестом.

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

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

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

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

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

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

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

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

Обратите внимание, чего нет в этом списке: кодек, 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.

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

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

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