Pull to refresh
652.2
Яндекс
Как мы делаем Яндекс

Свой плеер для DASH: вошли и вышли, приключение на 20 минут. Доклад Яндекса

Reading time20 min
Views2.7K

Меня зовут Оля, я разработчик в Yandex Infrastructure и я делаю веб‑плеер — библиотеку для воспроизведения видео на разных сервисах Яндекса (например, на Кинопоиске, Диске, Практикуме и Погоде).

Эта история о том, как мы отказались от опенсорс‑решения для воспроизведения потокового видео и написали свой велосипед. Я расскажу об архитектуре нашего решения и о том, с чем мы столкнулись, когда воплощали его в жизнь. Также покажу, какие эксперименты мы проводили и на какие метрики ориентировались.

Точка А

Для доставки контента пользователю мы используем технологию стриминга. Существует несколько общепринятых стандартов стриминга, которые, например, определяют, как поток разделяется на сегменты и раздаётся по сети. Наиболее популярные из них: HLS и DASH. Их главное отличие — в HLS у нас два критических похода по сети до первого кадра, а в DASH — один. Для нас это было ключевым пунктом при выборе, но были и несколько дополнительных соображений.

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

Но у HLS есть огромный‑огромный плюс: это нативная поддержка, включая Safari на айфонах, некоторые Android‑устройства. У DASH тоже есть небольшой интересный плюс. Из‑за того, что этот стандарт разрабатывается консорциумом, этот консорциум должен иногда собираться, обсуждать и тусить вместе. Они это делают примерно каждый квартал.

Итак, мы выбрали MPEG‑DASH. Мы до сих пор любим MPEG‑DASH и стараемся увеличивать долю DASH в наших сервисах. Но нужно как‑то проигрывать DASH: нужна библиотека, которая будет реализовывать этот стандарт.

Если обратиться в поисковик за опенсорс‑библиотеками, которые реализуют стандарт, мы увидим огромный список. Если спросить YandexGPT и ChatGPT, они тоже выдадут длинное перечисление.

Но если присмотреться к списку, окажется, что многие библиотеки воспроизводят DASH за счёт других библиотек. Например, под капотом Video.js работает Dash.js. JW Player работает поверх Shaka‑player. MediaElement.js, Flowplayer, Kaltura Media Player тоже работают поверх какой‑то другой библиотеки. Cineast, Plex и MSE — это просто странные вещи, которые выдаются поисковиком.

Итого, мы можем вбивать разные запросы, получать огромные списки, в которых каждый раз будут какие‑то новые названия, но на самом деле под капотом у них будут три основные библиотеки. Это shaka‑player, dash.js и редко вспоминаемый rx‑player.

Dash.js vs Shaka Player vs rx-player

Окей, нам надо выбрать из трёх библиотек. Чем они отличаются?

  • Разработчик.

    • Dash.js разрабатывается тем же самым консорциумом DASH IF как идеальный эталонный клиент для стандарта MPEG‑DASH.

    • Shaka Player разрабатывается Google. И нет, YouTube не работает через shaka, это отдельный проект. Я думаю, они держат этот опенсорс‑проект для сбора разных багов, чтобы комьюнити приносило им репорты, и они могли более широко собирать проблемы.

    • Rx‑player разрабатывается французским телекомом Canal+.

  • Имплементация стандарта.

    • Dash.js — эталонная библиотека, так что она реализует весь стандарт MPEG‑DASH.

    • Shaka Player и rx‑player могут позволить себе реализовывать лишь часть стандарта.

  • Вес сборки dash.js из‑за этого существенно отличается от веса сборки других библиотек. В таблице я привела продовую минифицированную сборку уже после Gzip.

  • Поддержка остальных стандартов. Все три поддерживают MSS. Shaka Player здесь выделяется, она поддерживает ещё и HLS.

Несколько лет назад мы выбирали из двух вариантов: rx‑player особо не учитывали, так как эта библиотека не очень популярна и, возможно, нам тогда не попалась на глаза. В итоге выбрали Shaka Player, поскольку она весила меньше, чем dash.js. И здесь стоит немного поговорить о Shaka и перейти к последствиям этого выбора.

Итак, Shaka Player. Напомню, что Shaka умеет в стандарт DASH, HLS и MSS, но MSS возможен только для VOD. В целом, она умеет воспроизводить и VOD, и Live (в том числе Low‑latency live), а также поддерживает самые разные DRM‑системы: PlayReady, Widevine.

У библиотеки есть особенности. Особенность первая: её архитектура устроена таким образом, что приличная часть блоков может быть заменена с помощью инъекций — dependency injection. Она собирается и минифицируется с помощью Closure Compiler. Closure Compiler несёт свои особенности: в этой библиотеке используется своя модульная система. Вместо знакомых импортов и экспортов, там goog.provide и goog.require.

Кроме того, в Shaka Player долгое время типы описывались специальной аннотацией для JSDoc. Поэтому типы для TypeScript приходилось описывать самостоятельно.

Отдельно остановлюсь на архитектуре. Она достаточно простая, основные блоки приведены на схеме, взятой с сайта самой Shaka.

На что стоит обратить внимание:

  • Player — наша точка входа;

  • cвязанный с ним AbrManager отвечает за ABR (Adaptive BitRate — динамический подбор качества в процессе воспроизведения). Он находится отдельно от всего и практически не связан с другими блоками;

  • StreamingEngine — движок, который работает с MSE;

  • NetworkingEngine — сетевой слой;

  • DrmEngine для реализации DRM;

  • ManifestParser, который, соответственно, парсит манифест;

  • Manifest — блок, который управляет дорожками, следит за аудио, видео и субтитрами.

В целом, мы разобрались в нашей ситуации, с чего мы начинали несколько лет назад. Так что же нам не подошло в выбранной библиотеке?

Что не так с Shaka Player и что нам пришлось доработать 

Свой сетевой слой

Мы активно пользовались возможностью заменять блоки Shaka и написали свой кастомный сетевой слой, чтобы ходить за сегментами через него.

Какие проблемы с этим возникли? Частью стандарта DASH является схема с несколькими BaseURL. Они нужны для того, чтобы иметь возможность переключаться на другой edge‑сервер.

Это нужно, если плеер пытается скачать сегмент с определённого CDN. CDN не смог отдать данные за какое‑то время, например, за 10 секунд. Возможно, что‑то пошло не так, сервер перегружен или ведутся ремонтные работы. Пойдём за данными в другой CDN.

Но в Shaka была такая особенность. Допустим, мы идём за первым сегментом в первый CDN. Он нам не ответил. Сходили во второй. Когда мы идём в следующий раз за вторым сегментом, было бы классно, наверное, запомнить, что с первым что‑то не так, и сразу пойти во второй. Но нет, Shaka этого не помнит, у неё нет стейта для такого модуля. Она идёт опять‑таки в первый CDN, снова таймаутится, у пользователя в это время stalled, — и только потом переключается на второй. Мы, по сути, просто сделали эту stateless‑реализацию stateful.

В последние годы появился стандарт Content Steering, чтобы управлять переключением между CDN‑ами с сервера. Кто‑то может подумать, что она противоречит предыдущей схеме, но нет.

Что появляется? Если в предыдущей схеме переключением управлял клиент (то есть плеер), то в новой схеме появляется серверная часть. Теперь бэкенд может управлять переключением плеера. Плеер периодически ходит в Steering Server, спрашивает, не изменился ли список актуальных BaseURL. Если ситуация изменилась, плеер переключается на тот CDN, который рекомендует сервер.

Мы планируем взять лучшее от обеих схем: и хранить стейт с забаненными хостами у себя, и обращаться к бэкенду за данными, что же лучше использовать сейчас.

Тег Location

Для работы прямых трансляций плееру нужно периодически перезапрашивать манифест, чтобы узнать о появлении новых частей видео. Согласно стандарту, тег Location позволяет изменить ссылку на плейлист. И проблема заключалась в том, что Shaka один раз получала значение из тега Location, а в последующие разы нет. Мы это поправили.

Предзагрузка

Самая большая для нас боль — это предзагрузка. Хочется, чтобы часть контента загружалась до того, как пользователь нажмет Play.

Продуктовый пример — непрерывный просмотр. Когда пользователь смотрит сериал, и текущая серия подходит к концу, мы предзагружаем начало следующей. Технология, способствующая binge watching.

Но архитектура Shaka Player не предполагала воспроизведение нескольких потоков. Нам пришлось сделать свою достаточно костыльную схему:

  • Для воспроизведения текущей серии создается контроллер и первый инстанс Shaka Player.

  • Для предзагрузки следующей серии создается PreloadController со вторым инстансом Shaka Player. Именно он ходит по сети за данными второй серии и складывает их в кеш.

  • При переключении на следующую серию мы выкидываем всё, что было раньше, и создаём новый контроллер с новым инстансом Shaka Player и новым сетевым слоем. Новый сетевой слой берёт предзагруженные данные из кеша. Сущность кеша — синглтон на странице.

Дело в том, что Shaka Player сразу складывает скачанные данные в MSE. Получается, на странице используется три видеотега для одного плеера:

  • Первый для текущей серии.

  • Второй только для предзагрузки. Он не нужен и не будет встроен в DOM. Но из‑за архитектуры Shaka он будет создан и подключен к MSE. В MSE будут перекладываться данные, которые мы пока не собираемся показывать пользователю. Из‑за чего начнут работать декодеры, а устройство будет зря нагружаться.

  • Третий для следующей серии.

В момент, когда надо переключиться на предзагруженный контент, плеер выкидывает старые инстансы Shaka Player и создает новый Controller, новую Shaka, новый видеотег.

Сравним подход Shaka Player с подходом hls.js: можно создать инстанс и загружать данные отдельно. А в нужный момент времени подключаться к видеотегу и передавать их в MSE.

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

ABR 

Ещё одна вещь, которая нас не устраивала, это ABR. Подробнее про алгоритмы ABR я рассказывала в другом докладе.

Мы воспользовались возможностью заменить ABR в Shaka на собственный. Но здесь были нюансы. Дело в том, что Shaka оперирует вариантами — композицией качеств видео и аудио.

Эта особенность связана с тем, что Shaka работает с двумя стандартами: DASH и HLS. И чтобы унифицировать работу с этими стандартами, в DASH поддержаны варианты. В нашем кастомном модуле AbrManager приходилось обратно раскладывать варианты на отдельные массивы качеств аудио и видео.

Кроме того, мы заметили, что изменение конфига Shaka сбрасывает ABR полностью. И у нас в коде стоят комментарии не дергать определённые методы слишком часто, иначе ломается ABR и capping. То есть нам приходилось человеческим ресурсом отслеживать, что мы не делаем что‑то странное.

Помимо этого, нас не устраивал BandwidthEstimator. Он неплох, это общепринятая в опенсорс‑библиотеках стандартная реализация.

И ещё один момент, который нас смущал. ABR вызывается в произвольный для нас момент времени. Shaka его вызывает тогда, когда ей нужно, а нам бы хотелось иметь возможность управлять этим моментом.

Shaka вызывает ABR:

  • в момент инициализации;

  • перед загрузкой следующего сегмента;

  • на изменение конфига Shaka. Этот пункт нас не устраивал.

Но здесь ещё не весь список вызовов ABR:

  • изменение аудиодорожки;

  • выбор видеодорожки;

  • изменение конфига DRM;

  • таймаут загрузки. Если при походе за следующим сегментом сервер не ответил за какое‑то время, то Shaka отменяет запрос и пересчитывает качество. Это происходит, если у пользователя упала пропускная способность сети, и предыдущий выбор больше не «пролезает» в неё. Нам хотелось бы отойти от этой схемы в пользу той, что описана ниже.

Мы хотели иметь возможность дёргать ABR в любой момент загрузки. На протяжении всего скачивания сегмента спрашивать ABR: нам всё ещё подходит скачиваемое сейчас качество? Или выгоднее переключиться на качество пониже и докачать его быстрее? То есть, мы хотели отказаться от таймаута в принципе и держать запрос за сегментом нужного качества столько, сколько считаем нужным.

Таким получился итоговый список
Таким получился итоговый список

Low-latency

Ещё один момент, который не устраивал в Shaka, это Low latency. Когда нам понадобилось делать трансляции с низкими задержками, в Shaka уже были сырые наработки, похожие на MVP. Мои коллеги уже рассказывали, как мы пилили Low latency, так что расскажу кратко, что нам пришлось сделать в плеере, чтобы эта схема завелась.

Наш Low latency работает поверх CMAF. Что происходит: в обычной схеме у нас в сегменте есть один moof, MP4-атом с метаданными, и один mdat, MP4-атом с кадрами.

CMAF‑сегмент — это подробленный сегмент, там есть несколько moof и несколько mdat, то есть несколько MP4-атомов, содержащих кадры. Это делается, чтобы мы могли перекладывать в MSE кадры до того, как мы закончим скачивать сегмент целиком. Для этого нужно читать данные из потока. Мы уже развивали свой форк Shaka Player, и в нашей версии чтения данных из потока ещё не было. Пришлось приносить самим.

Данные не передаются по сети в виде цельных MP4-атомов. Из‑за этого нельзя просто взять и вставить часть MP4-атома в MSE — если атом неполный, MSE просто не сможет его обработать. Поэтому нам пришлось внедрить дополнительную логику для парсинга MP4 в сетевой слой, чтобы корректно работать с потоковыми данными.

Потом оказалось, что если мы начинаем играть контент до того, как скачали сегмент целиком, у нас ломается BandwidthEstimator. Нужно как‑то учитывать эту ситуацию и учитывать время подготовки сегмента — мы ждём данные не потому, что у пользователя плохая сеть, а потому, что данные ещё не готовы. Для этого мои коллеги изобрели схему, в которой у нас добавляется ещё один MP4-атом UUID, и в нём передаётся серверное время. Мы научили плеер читать этот атом и передавать полученное значение в BandwidthEstimator.

И по мелочи пришлось доработать ABR и подправить коэффициент сглаживания α в формулах EWMA для оценки пропускной способности сети.

Но это не всё. Пока мы реализовывали Low latency, мы породили несколько багов.

Баги, которые мы породили 

Первый баг: наш бэкенд иногда возвращает пустые mdat‑атомы. То есть MP4-атомы, которые должны содержать кадры, оказываются пустыми. Несмотря на то, что это допустимо с точки зрения стандартов, браузер Safari с таким не справляется. В итоге нам пришлось вновь доработать парсинг MP4-атомов.

Второй баг был более неприятным. В какой‑то момент мы допустили race condition, из‑за которого иногда в MSE складывался init‑сегмент не той дорожки. Например, мы планировали воспроизвести 1080p, а в MSE попал init‑сегмент для 720p. В результате картинка рассыпалась, и пользователь видел зелёный экран.

Чтобы решить эту проблему, нам пришлось многое изменить: мы добавили большое количество логов в Shaka Player, собрали кастомную версию, интегрировали её в наш плеер и провели серию экспериментов. Этот процесс оказался крайне трудоёмким и сложным.

Есть ещё один момент. У нас реализован серверный capping — специальный механизм, с помощью которого бэкенд через манифест сообщает нам качество, выше которого нельзя играть.

Когда много пользователей одновременно смотрят, например, чемпионат мира по футболу, наш бэкенд оказывается под высокой нагрузкой. В таких ситуациях он сообщает нам, что автокачество не должно превышать 720p. Теоретически, пользователь всё ещё может вручную выбрать более высокое качество, но ABR плеера не выбирает его автоматически. Получается, что парсинг плейлиста выполняется и в Shaka Player, и в нашей реализации AbrManager. То есть мы делаем двойную работу.

Но и это ещё не всё.

Что мы не смогли сделать в Shaka

Первое — это MSE в Web Worker.

Плеер — достаточно тяжелая библиотека, которая значительно нагружает страницу пользователя. Было бы замечательно перенести такие задачи, как запрос сегментов, загрузка манифеста, его парсинг, парсинг сегментов и перекладывание данных в MSE в параллельный поток с использованием Web Worker.

Это стало возможным совсем недавно. В 2020–2021 годах были разработаны демо‑страницы MSE в Web Worker. В Chrome 105 эта функция появилась под экспериментальным флагом, а начиная с Chrome 108 она доступна по умолчанию.

Демка доступна по QR-коду и ссылке: она имитирует высоконагруженную страницу
Демка доступна по QR-коду и ссылке: она имитирует высоконагруженную страницу

Использование MSE в Web Worker выглядит очень перспективно, однако поддержка этой функции для Shaka Player пока не планируется. В этом году на GitHub один из разработчиков задал вопрос о планах по поддержке MSE в Web Worker, на что получил ответ: если эта функция необходима, нужно создать запрос на её добавление.

Ещё одна вещь, которую мы хотели реализовать, — это виртуальный буфер и кеширование. Нам хотелось уметь загружать две, четыре, десять минут или даже целый фильм для офлайн‑просмотра. Однако это невозможно, потому что Shaka сразу скачивает данные в MSE, что приводит к ограничениям этого API. Думаю, многие знакомы с этой таблицей:

Браузер ограничивает количество данных, которые мы можем поместить в MSE. Да, этого хватает, чтобы загрузить пять минут видео в качестве 720p с хорошим звуком, но этого недостаточно. Например, это стало одним из препятствий для реализации BBA (Buffer‑Based Approach) — алгоритма адаптивного битрейта, основанного на отслеживании заполненности буфера, а не пропускной способности сети. Нам хотелось бы получить такую систему, где существует виртуальный буфер, в который мы можем загружать данные в любом объёме по нашему усмотрению, а затем переносить их в MSE.

Ещё один момент — это перекачка буфера. Хотя этот случай редкий, его также хотелось бы обработать. Представьте, что пользователь просматривает страницу с плеером в небольшом контейнере и благодаря хорошему интернет‑соединению успевает загрузить значительный объём данных. Затем пользователь переключается в полноэкранный режим.

Было бы замечательно, если бы в момент перехода в полноэкранный режим при хорошем интернете можно было перекачать буфер в качестве, которое соответствует новому размеру экрана, выкинув при этом текущий буфер. Однако это сложно реализовать, так как Shaka Player загружает данные сразу в MSE. Для реализации этой фичи нужно было снова патчить сетевой слой с неизвестными побочными эффектами.

Нам бы хотелось иметь более гибкие правила для ABR, которые учитывали бы не только состояние сети, но и другие параметры. Например, такие как TTFB (Time To First Byte). Также мы хотели бы учитывать степень заполненности буфера и другие факторы. Другими словами, мы стремимся к тому, чтобы ABR предоставлял больше возможностей кастомизации и получения дополнительных данных.

В итоге Shaka Player — отличная библиотека, однако она стала нам мала.

Нас не устраивало, что Shaka оперирует вариантами. Нас не устраивало то, что Shaka Player сразу загружала контент в MSE, из‑за чего мы сталкивались со всеми ограничениями этого API, не могли перекачивать буфер и использовать виртуальный буфер и кеширование. В Shaka Player нет MSE‑in‑WebWorker, а мы очень хотели его использовать. У нас уже был патченый‑перепатченный сетевой слой, и его приходилось бы дальше дорабатывать. А так как у нас накопилось много собственных, мы не могли без проблем обновиться до новой версии, это было крайне болезненно. Думаю, вы понимаете, когда яндексоиды недовольны каким‑либо открытым решением, они создают свой велосипед.

YaSP

Мы сделали Yet another StreamPlayer или коротко YaSP.

Требования к новому движку

Когда мы проектировали движок, мы заложили несколько ключевых функций, которые хотели реализовать:

  • Мы хотели, чтобы наша видеореклама могла легко работать с нами как с видеотегом. То есть мы хотели расширить видеотег таким образом, чтобы он воспроизводил стандарт DASH. Как видеотег в Safari поддерживают воспроизведение HLS.

  • Предзагрузка.

  • Мы хотели перенести большую часть кода в отдельный поток, используя MSE‑in‑WebWorker.

  • Виртуальный буфер.

  • Перекачка буфера.

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

Архитектура

Как мы воплотили все эти пожелания в архитектуре? Нам понадобилась неделя: посидеть в другом городе, изучить материалы, нарисовать различные схемы. В итоге у нас получилось следующее.

Здесь представлена внешняя часть — та, что находится на основной странице браузера. Сюда входит видеотег, от которого наследуется YaSP‑видеоэлемент. Кроме того, внешняя часть RPC‑шины, которая обеспечивает взаимодействие между внешней частью плеера и компонентом, работающим внутри Web Worker. Всё самое интересное располагается именно внутри Web Worker.

Точка входа — Worker. Внутри есть блок VideoState, который синхронизирует состояние видеотега и компонентов, находящихся внутри Web Worker. Ключевые элементы — SourceManager и Source. Наш движок изначально был спроектирован для работы с несколькими источниками и поддержки предзагрузки. SourceManager контролирует количество источников, предотвращая утечки памяти.

Также здесь находится MSEEngine, который взаимодействует с MSE. Виртуальный буфер позволяет загружать нужное количество данных и своевременно их очищать. Fetcher отвечает за сетевой слой, а блок Manifesto занимается парсингом манифеста. Timeline контролирует аудио‑, видеодорожки и субтитры. AbrManager управляет адаптивным битрейтом. На схеме показаны ownership‑связи основных блоков.

Я хотела сравнить архитектуру нашего движка с архитектурой Shaka, но это не помещалось на слайде. Поэтому на иллюстрации ниже, в левом верхнем углу, показана Shaka, а в правом нижнем — часть YaSP, работающая внутри Web Worker. Эти две системы имеют схожие модули, например, AbrManager и TextEngine.

ManifestParser и Manifest также занимаются парсингом плейлистов. Fetcher и NetworkEngine представляют собой сетевые слои. Основное отличие заключается в том, что в Shaka нет компонента Source. Shaka изначально рассчитана на работу с одним источником, условно с одной серией. В отличие от этого, YaSP поддерживает работу с несколькими источниками и может предзагружать контент.

Подводные камни 

Дырки. С какими трудностями мы столкнулись при реализации всей этой красоты? Во‑первых, оказалось, что в потоках есть дырки. И в Shaka это явление было хорошо обработано. Но при разработке собственного движка нам пришлось самостоятельно решать проблему дырок.

Что же такое «дырка» в потоке? Это ситуация, когда конец текущего сегмента не совпадает с началом следующего. Иногда эти пробелы незначительны, и тогда браузер сам перескакивает их и продолжает воспроизведение — это благоприятный сценарий. Но иногда дырки бывают достаточно большими, и браузер не может их преодолеть, несмотря на наличие данных впереди. Это приводит к зависанию в состоянии буферизации, и нам приходится перематывать вручную.

Причины возникновения «дырок»:

  1. Невыравненность сегментов на границах периодов в плейлисте.

  2. Несоответствие заявленной в манифесте длины контента и реальной продолжительности сегмента. Эта проблема гораздо сложнее, так как она может возникать из‑за ошибок в округлении при транскодировании или перепаковке контента.

От «дырок» в потоке невозможно избавиться совсем. Проще научиться перепрыгивать эти моменты с помощью GapJumper.

Мы должны искусственно перематывать контент в следующих случаях:

  • Видеотег должен находиться в состоянии буферизации, чтобы мы могли определить момент, когда он застопорился и не может продолжать воспроизведение.

  • Мы должны убедиться, что впереди есть данные, на которые можно перемотать.

  • И главное — у нас в процессе нет перемотки. GapJumper и перемотка имеют свои особенности. Когда пользователь слегка отматывает назад, с точки зрения GapJumper это выглядит как дырка, потому что текущих данных нет, мы находимся в состоянии буферизации, а впереди есть данные, на которые можно перемотать. Какое решение мы нашли? При перемотке назад мы очищаем буфер и таким образом устраняем дырку.

У нас была проблема с зацикленными видео. Оказалось, когда плеер доигрывает кусочек видео и возвращается к началу, GapJumper тоже воспринимает это как пробел. Решение оказалось достаточно простым: мы ограничили возможность прыжка дальше, чем на один сегмент.

Ещё одна проблема, с которой мы столкнулись, — рассинхрон скачивания аудио и видео. Обычно видео весит больше, чем аудио, поэтому скачивание аудиодорожки может существенно опережать видеодорожку. Для нас аудио без видео бесполезно и просто занимает канал, который можно было бы использовать для более быстрой загрузки видео. Решение здесь тоже оказалось несложным: мы ограничили скачивание каждой дорожки так, чтобы оно не опережало другую больше, чем на один сегмент. Таким образом, аудио не скачивается дальше, чем на один сегмент от текущего сегмента видео, и наоборот.

DRM. Ещё одно место, которое хорошо работало в Shaka и которое нам пришлось реализовывать у себя, это DRM.

Выяснилось, что спецификация EME (Encrypted Media Extensions) недостаточно полная и понятная. Были вопросы по правильному уничтожению CDM (Content Decryption Module) и переключению между различными DRM-системами. Мы пытались сделать DRM более удобным для наших клиентов, предлагая синхронный API, но столкнулись с проблемами на телевизорах. Например, на некоторых операционных системах, таких как VIDAA, у нас возникли сложности с кодом ниже.

if (buffered.length > 0) {
 buffered.end(0) // exception: buffered length === 0
}

Мы проверяем наличие buffered ranges — они вроде бы есть. Однако при попытке обратиться к ним возникает ошибка: оказывается, что на самом деле их нет. Мы так и не нашли, как это починить, поэтому пока отказались от синхронного API и оставили асинхронные вызовы.

В итоге, наша уверенность в себе немного поубавилась. Мы думали, что за четыре года накопили массу знаний, чтобы создать свой собственный плеер. Как оказалось, мы хорошо понимали, что не работает, но упустили из виду многие вещи, которые уже работают отлично. Мы столкнулись с необходимостью реализовать эти вещи самостоятельно и натолкнулись на те же проблемы, с которыми раньше сталкивались разработчики Shaka.

Эксперименты и метрики

Чтобы наша оценка всех проведённых экспериментов была яснее, сначала расскажу, на какие метрики мы смотрим:

  1. Ошибки: фатальные и нефатальные. Из‑за того, что у нас менялся движок, периодически менялось и то, как мы логируем ошибки. Поэтому были технические сбои. Иногда мы теряли ошибки, иногда мы получали технический рост ошибок, и всё это приходилось отфильтровывать.

  2. Количество и длина буферизаций. Именно эти параметры позволили понять, что у нас большие проблемы с дырками и с GapJumper.

  3. Скорость запуска. Именно она показала нам, что MSE в Web Worker тоже имеет смысл делать.

  4. TVT. Это единственная продуктовая метрика здесь, на которую мы смотрим.

  5. QoE. Подробнее о том, что мы вкладываем в Quality of Experience, мой коллега рассказал в отдельном докладе.

  6. Трафик. Как мы его экономим, я тоже рассказывала на предыдущем VideoTech. Так вот, когда мы переходили на новый движок, наша задача была не потерять тот профит, который мы получили, поэтому смотрели в том числе и на трафик.

Теперь немного внутренней терминологии. В Яндексе эксперимент считается зелёным, если те метрики, которые должны падать, падают, а те, что должны расти, — растут. Эксперимент будет серым, если там ничего особо не меняется, нет статзначимых изменений. И красным, если растёт или падает то, что не надо.

Так вот, как мы принимали наш проект? Мы шли по серому относительно пропатченной Shaka. Может быть, хотелось услышать, как мы запустили один эксперимент, он показал невероятный профит, ускорение на 20%, рост TVT на 50%, всё такое классное, волосы мягкие и шелковистые. Но нет. Наша задача была хотя бы не ухудшить опыт пользователя.

И ещё момент. Не было единого эксперимента, мы оценивали итоги в несколько этапов.

Сначала мы провели эксперименты на обычном VOD‑контенте. Затем добавили поддержку DRM (Digital Rights Management), чтобы обеспечивать защиту видео. Поскольку Кинопоиск — платный сервис, мы начали экспериментировать на нём только после отладки работы на обычном контенте без DRM. Далее мы подключили live‑трансляции, и таким образом у нас были VOD, DRM VOD, Live и DRM Live. В финале мы внедрили поддержку режима низкой задержки (Low Latency).

Что ещё нам помогло? Диктатура покрытия юнит‑тестами. Наш руководитель ввёл обязательный чек на jest‑coverage перед влитием пул‑реквеста. Это заставляло разработчиков внимательнее просматривать код, искать участки, где можно написать дополнительные юнит‑тесты.

Кроме того, нам повезло: наш плеер на базе Shaka имел хорошее покрытие интеграционными тестами. Когда мы переключили эти тесты на новый движок YaSP, обнаружилось множество ошибок. Интеграционные тесты помогли выявить мелкие кейсы, которые могли бы остаться незамеченными в ходе экспериментов.

Выводы

Несколько ключевых пунктов:

  • Улучшение Quality of Experience за счёт включения MSE в Web Worker. Мы на 0,75% увеличили долю «зелёных» (хороших) сессий.

  • Снижение количества «красных» сессий. Пользователи, которые ранее испытывали серьёзные проблемы, больше не сталкиваются с ними.

  • Мы заложили основы для множества возможностей оптимизации. Теперь можно создавать различные правила ABR, связанные с сетью и буфером.

  • Мы внедрили виртуальный буфер, что позволяет накачивать большое количество данных.

Однако возникли трудности со сроками реализации. Мы рассчитывали выполнить проект за полгода, но в итоге это заняло два.

В рамках ретроспективы я спрашивала руководителей, стали бы они браться за этот проект, зная, что он займёт два года. И ответ был — нет, не стали бы.

Что могу сказать разработчикам: если у вас возникла подобная проблема и бизнесу нужны суперкастомные вещи — скорее всего, у вас нет выбора. Вам придётся пройти этот путь и столкнуться с теми же трудностями. Впоследствии, возможно, вы тоже сможете выступить на конференции и поделиться своим опытом. Но если у вас просто возникла мысль: «А не написать ли нам свой движок?», я скорее могу только предостеречь вас.

На этом всё, жду ваши вопросы. Подписывайтесь на наш телеграм‑канал, где мы с коллегами пишем про стриминг и наши исследования.

*В иллюстрациях использованы образы персонажей Rick and Morty series by Justin Roiland & Dan Harmon, The Cartoon Network, Inc.

Tags:
Hubs:
+19
Comments7

Articles

Information

Website
www.ya.ru
Registered
Founded
Employees
over 10,000 employees
Location
Россия