Принимая архитектурные решения, часто так или иначе приходится идти на компромисс: между качеством и скоростью разработки, сложностью реализации и удобством поддержки, быстротой решения бизнес-задачи и гибкостью. Со временем небольшие уступки накапливаются, и проект покрывается легаси. Даже если исправно разгребать техдолг, то на достаточно длинной дистанции решения и технологии в любом случае устареют, и станет невозможно обойтись «генеральной уборкой» — потребуется смена архитектуры.  

В статье расскажу, как мы столкнулись с неизбежной необходимостью переделки веб-плеера RUTUBE — сервиса, который существует с 2006 года, пережил несколько смен команд и парадигм разработки и при этом достаточно большой и высоконагруженный, чтобы нельзя было «просто так взять и всё переписать». 

Меня зовут Паша Фомин, я лид платформенной команды плеера RUTUBE. Я пришёл в компанию в 2022-м году, тогда код веб-плеера представлял из себя кучу легаси, а его архитектура была в таком состоянии, что мешала развивать продукт и реализовывать бизнес-цели. 

Основными легаси-проблемами в коде плеера состояли в следующем:

  • 2500 строк кода в главном управляющем компоненте;

  • дублирование состояний в Redux и в теге video; 

  • анти-паттерн props drilling через 5+ компонентов;

  • огромное количество связных useEffect'ов — более 50. 

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

Видео в вебе

Кажется, есть же <video>, берешь его и готово — в браузере можно воспроизводить видеофайлы:

<video
  Id="player"
  src="video.mp4"
  controls
  autoplay
  poster="preview.jpg"
></video>

Основные атрибуты тега <video>:

  • src — источник видео;

  • controls — показать или спрятать элементы управления;

  • autoplay — включить или выключить автоматическое воспроизведение;

  • poster — постер, который будет показываться до воспроизведения видео.

Также у тега <video> довольно развитый API, в котором, например, есть такие полезные методы и свойства: 

  • play() — начать воспроизведение;

  • pause() — приостановить;

  • currentTime — текущая позиция, которую можно получить или выставить;

  • volume — управление громкостью;

  • duration — длительность видео;

  • requestFullscreen() — переход в фуллскрин. 

Естественно, это неполный список, и на первый взгляд, этого в целом достаточно, чтобы работать с видео в вебе. Однако у тега <video> есть существенные ограничения, главные из которых для нас состоят в следующем: 

  • отсутствует гибкое управление буфером, не получится контролировать расход трафика;

  • невозможно добиться бесшовного переключения качеств, поскольку <video> подгружает статический файл и для смены разрешения надо напрямую поменять ссылку на видео и перезагрузить страницу;

  • невозможно проигрывать прямые трансляции;

  • нет гибкой настройки UI. 

Решение большинства из этих проблем — потоковое видео. 

Потоковое видео

В нашем контексте воспроизведение потокового видео означает, что вместо скачивания всего файла клиент получает его потоком (на самом деле, конечно, небольшими фрагментами — чанками).

Нативная поддержка потокового видео есть только у Safari, а для воспроизведения потокового видео в остальных случаях есть два основных решения: DASH (технология разработана в рамках группы MPEG) и HLS (протокол, созданный Apple). Для обоих есть соответствующие JS-библиотеки: HLS.js и dash.js. 

Рассмотрим на примере HLS, что из себя представляет поток и как с ним работает плеер.

Клиент, вместо того чтобы загрузить статический видеофайл, получает манифест с информацией о доступных качествах видео и расположении файлов/чанков. В стандарте HLS для организации потоковой передачи требуется мастер-манифест и контентный манифест. Ниже фрагмент мастер-манифеста:

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

Это фрагмент контентного HLS-манифеста с метаинформацией и ссылками на чанки с их длительностью: 

DASH устроен похожим образом, но там манифест один и в нём сразу есть ссылки на качества. 

Библиотеки HLS.js и dash.js, кроме того, что позволяют делать запросы за манифестами и чанками, занимаются следующим: 

  • работают с буфером с помощью MediaSource API; 

  • позволяют реализовать контроль ошибок (воспроизведения, сетевых и других);

  • ABR — Adoptive Bitrate, то есть подстройкой качества в зависимости от пропускной способности сети;

  • контролируют загрузку фрагментов;

  • реализуют DRM Module — модуль для работы с лицензионным контентом, который находится под защитой Digital Right Management и другое. 

Плеер это не только воспроизведение видео

Кроме собственно воспроизведения видео плеер RUTUBE (и плеер любой другой платформы) имеет довольно богатую функциональность и развитый UI. В частности, плеер должен уметь: 

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

  • Логировать события статистики и отправлять соответствующие сообщения, если произошла ошибка. 

  • Работать через внешний API, то есть обрабатывать колбеки для внешних пользователей нашего плеера, который может эмбедироваться через iframe.

  • Показывать субтитры, что кажется простым, но тоже скрывает за собой разветвлённую логику, потому что у нас может быть несколько файлов с субтитрами (например, русские и английские) — нужно организовать выбор языка. Файл субтитров может быть в формате .srt или .vtt, но браузер умеет нативно работать только со вторым. А в плане UI нативной стилизации нам недостаточно, поэтому надо самим рендерить и стилизовать субтитры. 

  • Применять настройки, например, язык цвет и размер упомянутых выше субтитров, то есть модуль субтитров должен быть взаимосвязан с модулем настроек. 

  • Виджеты рекомендаций или следующих серий в серийном контенте: во время паузы, в конце видео и т.д. Чтобы пользователю было удобно продолжить просмотр. 

Пример виджета серий во время паузы видео
Пример виджета серий во время паузы видео

Получается, что несмотря на то что у плеера, конечно, есть специфика работы с видео, плеер — это тоже фронтенд. И мы сталкиваемся с теми же самыми проблемами и задачами, которые есть в любом сложном фронтенде. Поэтому давайте вернёмся к проблемам, которые были у нас в старом плеере, и обсудим способы с ними справиться. 

Проблемы старого плеера

Проблема номер один — 2500 строчек кода в одном файле. И это не шаблонный код, он действительно реализует бизнес-функциональность. Вот скриншот для подтверждения: 

Следующая связанная проблема — props drilling или пробрасывание пропсов. Например, обработчик HandlePlay путешествовал сквозь компоненты на пять уровней вложенности: raichu-wrapper.tsx → raichu-player-ui.tsx → raichu-video-player.tsx → raichu-player-controls.tsx → raichu-player-left-controls.tsx. 

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

Огромное количество useEffect’ов также не помогало поддержанию порядка. Ниже пример нескольких useEffect’ов, которые по цепочке запускают друг друга: меняется id видео → запрашиваем информацию по этому видео → получили из playOptions информацию про видео →… → еще что-то происходит, например, запускается реклама и т.д.  

Это приводит к двум видам проблем. Во-первых, все связанные useEffect’ы находятся рядом только на скриншоте для статьи, а на самом деле они разбросаны по разным частям кода, и отлаживать что-то с их участием очень неудобно. Во-вторых, в таких сценариях сложно отслеживать состояние плеера. Плюс ко всему в таком клубке из useEffect’ов могут возникнуть циклические зависимости.

Но на самом деле мы справлялись! Мы пытались писать хороший изолированный модульный код и потихоньку где-то что-то рефакторить. Пока не появилась задача на плеер вертикальных видео.

Плеер для вертикального видео

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

  • Слишком большая разница в UI, контролы и бизнес-функциональность отличается (плеер вертикальных видео обычно проще).

  • Зато вертикальный плеер должен поддерживать не только потоковое видео, но и MP4.

  • Необходимо поддерживать несколько независимых инстансов плеера в ленте коротких видео, с чем Redux довольно плохо справился бы из-за глобального стора. 

  • Необходимость интеграции с контентом от Yappy — сервисом коротких видео, который тоже входит в «Газпром-Медиа Холдинг». 

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

  • переехали с Redux на MobX;

  • перешли с функционального подхода на ООП; 

  • разбили архитектуру на «слои» (грубо говоря, это разбиение на папки по бизнес-функциональности, например, слой UI, слой статистики, слой playback и т.д.). 

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

Переход плеера на новую архитектуру

Для новой архитектуры горизонтального плеера мы взяли за основу плеер вертикальных видео, но столкнулись с определенными нюансами. Не все решения, которые хорошо рабо��али там, были оптимальными для основного более сложного и нагруженного плеера. 

  • Отказались от слоёв в пользу плагинов. «Слои» не подходили для масштабирования. Они хорошо работали на не очень сложном проекте, но с ростом объема функций и кода деление стало бы менее логически однозначным и удобным.

  • Entrypoint'ы для кастомных сборок стали решением вопроса с быстрыми сборками и конфигурациями для разных проектов. 

  • Внедрение dependency injection стало ответом на то, что с ростом количества классов стало сложнее поддерживать зависимости. 

Как нам помог MobX

Мы переехали на MobX с Redux в первую очередь, чтобы избавиться от глобального стора и получить возможность работать с множественными инстансами. MobX это поддерживает из коробки, поскольку каждый экземпляр store в MobX представляет из себя просто обычный js-класс.

Во-вторых, с MobX мы получили декларативную реактивность вместо императивного диспатча. В Redux изменение состояний обрабатывается через action, reducer и dispatch. При этом всегда создается новый корневой state-объект, изменения пишутся в store, React пересчитывает все подключенные компоненты, чтобы затем выяснить, что изменилось, и перерисовать это. Это создает лишнюю работу и приходится гораздо больше следить за правильным использованием селекторов, эффектов и т.д. 

MobX же точечно отслеживает зависимости. Если компонент использует только поле player.currentTime, он будет перерисован, только когда изменится именно это поле, даже если в store изменилось что-то еще. Для плеера, где обновления интерфейса идут постоянно (таймлайн, буферизация), это дало заметный прирост к производительности.

В-третьих, MobX идеально совместим с ООП. Наша новая архитектура — это мир классов: PlaybackPlugin, AdvertPlugin и т.д. MobX отлично вписывается в парадигму ООП. Не нужны дополнительные библиотеки или сложные паттерны, достаточно просто пометить поля классов декоратором как observable, а методы — как action, и получишь полноценное реактивное состояние, тесно связанное с бизнес-логикой самого класса.

Короче говоря, мы поменяли не столько библиотеку, сколько парадигму управления состоянием:

  • Было (Redux): глобальное, иммутабельное, централизованное состояние. Идеально для данных приложения, но не для изолированных инстансов сложных модулей.

  • Стало (MobX): локализованное, мутабельное, реактивное состояние. Идеально для нашего случая, где каждый плеер — это самостоятельный «мир» со своей сложной внутренней логикой.

ООП

Естественно, выбор ООП не был самоцелью. Мы пришли к нему потому, что его принципы отлично легли на решение наших конкретных проблем — тех самых, с которых мы начали: props drilling, дублирование состояния и монолитность.

Инкапсуляция упростила работу за счёт скрытия внутренней сложности. Раньше у нас было 50+ useEffect — состояние и логика были размазаны по всей кодовой базе. Теперь вся логика, скажем, работы с субтитрами, инкапсулирована внутри одного класса — CaptionPlugin. Он сам управляет своим состоянием, сам обрабатывает события. Извне мы просто вызываем его методы. Больше никто не может случайно сломать его внутреннее состоян��е.

Принцип единственной ответственности (SRP) по сути является прямым воплощением идеи плагинов. Наш монолит на 2500 строк делал всё: и UI, и воспроизведение, и рекламу. Теперь же AdvertPlugin отвечает только за рекламу, StatsPlugin — только за статистику и т.д. Если нужно изменить логику показа рекламы, мы идём только в один файл и знаем, что не сломаем ничего другого.

Наследование и полиморфизм были критически важны для поддержки разных форматов видео и интеграций. 

Рассмотрим на примере. Есть базовый класс class PlaybackPlugin, который отвечает за инициализацию движка и методы работы с воспроизведением видео: load(), play(), pause().

Когда теперь нам нужно работать с DRM (мы строим единую платформу с онлайн-кинотеатром PREMIER), мы просто наследуемся от базового класса, добавляем в него логику работы с DRM и подтягиваем кроме видеопотока ещё и лицензию, чтобы расшифровывать поток. Вот так: class PremierPlaybackPlugin extends PlaybackPlugin. 

Также поступим и для поддержки других форматов и других интеграций. Например, VideoPlaybackPlugin для MP4 или HlsPlaybackPlugin для HLS по-своему реализуют метод load(). Но для всего остального кода плеера это неважно — он работает с ними через общий интерфейс. Это и есть полиморфизм.

Внедрение зависимостей (Dependency Injection) — это то, что связало всё вместе. Раньше зависимости прокидывались в конструктор вручную, это было больно. Теперь плагин просто декларативно заявляет, что ему нужно для работы, и DI-контейнер автоматически это предоставляет. Так мы теперь можем делать различные сборки.

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

Следующий пример показывает инициализацию контроллов. Они также лежат внутри класса: UI, который относится к end screen, лежит в плагине end screen; элементы управления, относящиеся к рекламе, — в плагине рекламы.

Когда понадобится аудиоплеер без UI — просто не будем регистрировать ненужные плагины.

Ниже пример того, как мы, собственно, регистрируем плагин: оборачиваем в декоратор @Plugin и прописываем список зависимостей. При сборке плеера экземпляр плагина добавляется в массив, а уже при инициализации механизм dependency injection проверяет зависимости и запускает конструкторы классов. 

Подход с плагинами делает приложение удобно тестируемым — любой плагин легко подменить на mock-объект.

Как вы видите из примеров, у нас получилась архитектура, которая не просто написана на классах — она спроектирована на принципах ООП.

Что получилось в результате 

  • Быстрая интеграция с кастомными сборками.

  • Небольшая связность кода, изолированная функциональность — меньше вероятность нечаянно что-то сломать.

  • Лучше Developer experience — с кодом в текущей архитектуре гораздо приятнее работать.

  • Как следствие можем гораздо быстрее вносить изменения. 

Несмотря на то, что ООП во фронтенде используется редко, оно классно работает тогда, когда очень много бизнес-логики и не так много UI. Полная смена концепции — это не так страшно, как может показаться. Иногда полезно смотреть по сторонам и выходить за рамки привычных шаблонов. Но при этом, конечно, в выборе инструментов нужно ориентироваться на задачи, а не гнаться за хайпом. 

Также о том, как мы перестраиваем архитектуру фронтенда RUTUBE, читайте в статье «Универсальный BFF для всех платформ». И подписывайтесь на канал Смотри за IT: там рассказываем о создании медиасервисов в Цифровых активах «Газпром-Медиа Холдинга» таких, как RUTUBE, PREMIER, Yappy.

Комментарии (19)


  1. Tolnik
    18.12.2025 13:06

    На Рутубе невозможно смотреть видео по причине засилья рекламы. Каждые 3 мин реклама на 15 сек. Для сравнения на ВК Видео каждые 15 мин реклама по 3 сек. Поэтому, все остальные прелести платформы несущественны.


    1. miulyano
      18.12.2025 13:06

      а подписку пробовали подключать? она вроде совсем рекламу отключает


      1. Tolnik
        18.12.2025 13:06

        Нет, не пробовал и пробовать не хочу. Просто пользуюсь ВК Видео и Матрешкой для размещения своего видео. А если смотреть и расслабиться - то Ютубом (с VPN) и ВК Видео.


      1. space2pacman
        18.12.2025 13:06

        а подписку пробовали подключать? она вроде совсем рекламу отключает

        miulyano Руководитель клиентской разработки в RUTUBE


    1. axe_chita
      18.12.2025 13:06

      ВК Видео фризится каждые 7-10 секунд, и это при предзагруженном видео (грубо говоря 3-4 минуты видео) пытался выставить низкое качество вплоть до 144 - не помогает. Более того он регулярно пытается это ограничение обойти и поднять качество до 1080. Так что ВК Видео далеко не айс


  1. V1tol
    18.12.2025 13:06

    Лучше почините скорость загрузки приложения на типичном китайском андроидтв. Она по 20 секунд стартует и тормозит как сволочь.


  1. denzill
    18.12.2025 13:06

    Кроме рекламы на рутубе сильно мешает отсутствие возможности добавлять в свои плейлисты чужие видео, и плейлист "смотреть позже", который не работает как плейлист.

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

    при этом в json название таки есть:

    это с лета уже так.

    Куда-то не туда рутуб эволюционирует...


    1. andreynekrasov
      18.12.2025 13:06

      С плейлистами на основе чужих видео согласен. Не понятно, почему не сделают. На ютубе все что нужно - распределяется по своим плейлистам, не нужно опять искать.


  1. Ralory
    18.12.2025 13:06

    Забавно что в статье от команды рутуба скрин пиратского контента)


  1. 0Bannon
    18.12.2025 13:06

    А еще, зачем кнопка выкл автовоспроизведение, если видео всё равно начинает проигрываться?


  1. monah_tuk
    18.12.2025 13:06

    Была у них киллер фича: проигрывание в фоне из коробки. Теперь хотят денег за это. Мне проще отказаться от использования. Ну или подождём rutube revanced... Хотя... Вряд-ли он появится. Думаю все понимают почему ;-)

    А, ещё до сих пор на ДВ иногда работают через ВПН лучше, чем без оного)


  1. qiper
    18.12.2025 13:06

    В статье расскажу, как мы столкнулись с неизбежной необходимостью переделки веб-плеера RUTUBE — сервиса, который существует с 2006 года

    Там бы весь сайт переделать, ему даже блокировка YT не помогает.

    Количество подписчиков на популярных каналах порой разница 2 порядка


  1. ooko
    18.12.2025 13:06

    Смелый шаг идти вразрез с линией партии. Не боитесь «положить партбилет на стол»?

    Мне вот все ещё нравится подход с фабрикой. И мутабельный подход тоже предпочитаю, но react упорно делает ставку на имутабельность


    1. pfomin90 Автор
      18.12.2025 13:06

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

      На мой взгляд, это всего лишь прикладные вещи, чтобы вспомнить тот же redux или, например, верстку, которой в плеере мало - много времени не понадобится


      1. ooko
        18.12.2025 13:06

        В разрез - потому что в документации react именно функциональные компоненты. И хайп сейчас на хуки. Я не критикую вас, наоборот респект


        1. alpatovdanila
          18.12.2025 13:06

          Судя по статье, компоненты остаются функциональными, речь идет только про слой стора/бизнес-логики, этот слой на хуках делать - это подписывать себе приговор на рефактор через пару лет. Что, собственно, и произошло.


          1. pfomin90 Автор
            18.12.2025 13:06

            Да, всё так
            У нас было много бизнес логики в компонентах - унесли её в классы, а с redux уехали, во многом из-за сложности интеграций в глобальный стор и как раз таки изолирования бизнес логики


  1. aio350
    18.12.2025 13:06

    "переехали с Redux на MobX;
    перешли с функционального подхода на ООП;"
    звучит как "переехали с устаревшей архитектуры на еще более устаревшую архитектуру" :)
    По моему скромному субъективному мнению, @tanstack/react-query + zustand - лучшее, что есть на сегодняшний день для управления состоянием в React-приложениях


    1. pfomin90 Автор
      18.12.2025 13:06

      У нас бизнес-логика превалирует над UI - в плеере его не так много, и он не очень сложный
      Нам хотелось сделать максимально изолированные плагины, в теории не зависеть от ui библиотеки - и выбранный подход решает эти проблемы

      К тому же, у части команды был опыт работы с mobx, написанные кастомные линтеры, решенные проблемы, а с Zustand, увы, нет