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

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

denzill
18.12.2025 13:06Кроме рекламы на рутубе сильно мешает отсутствие возможности добавлять в свои плейлисты чужие видео, и плейлист "смотреть позже", который не работает как плейлист.
В подписках отображаются не все новые видео, там где колокольчик не видно названий видео, только название канала, хотя у трансляций название есть:

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

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

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

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

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

qiper
18.12.2025 13:06В статье расскажу, как мы столкнулись с неизбежной необходимостью переделки веб-плеера RUTUBE — сервиса, который существует с 2006 года
Там бы весь сайт переделать, ему даже блокировка YT не помогает.
Количество подписчиков на популярных каналах порой разница 2 порядка

ooko
18.12.2025 13:06Смелый шаг идти вразрез с линией партии. Не боитесь «положить партбилет на стол»?
Мне вот все ещё нравится подход с фабрикой. И мутабельный подход тоже предпочитаю, но react упорно делает ставку на имутабельность

pfomin90 Автор
18.12.2025 13:06Да почему в разрез? Мы выбирали подход и стек исходя из потребностей, нам реально это очень сильно упростило жизнь. Мне кажется, что это более полезный навык и подход, чем гнаться за хайпом, либо по рельсам ехать
На мой взгляд, это всего лишь прикладные вещи, чтобы вспомнить тот же redux или, например, верстку, которой в плеере мало - много времени не понадобится
ooko
18.12.2025 13:06В разрез - потому что в документации react именно функциональные компоненты. И хайп сейчас на хуки. Я не критикую вас, наоборот респект

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

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

aio350
18.12.2025 13:06"переехали с Redux на MobX;
перешли с функционального подхода на ООП;"
звучит как "переехали с устаревшей архитектуры на еще более устаревшую архитектуру" :)
По моему скромному субъективному мнению, @tanstack/react-query + zustand - лучшее, что есть на сегодняшний день для управления состоянием в React-приложениях
pfomin90 Автор
18.12.2025 13:06У нас бизнес-логика превалирует над UI - в плеере его не так много, и он не очень сложный
Нам хотелось сделать максимально изолированные плагины, в теории не зависеть от ui библиотеки - и выбранный подход решает эти проблемы
К тому же, у части команды был опыт работы с mobx, написанные кастомные линтеры, решенные проблемы, а с Zustand, увы, нет
Tolnik
На Рутубе невозможно смотреть видео по причине засилья рекламы. Каждые 3 мин реклама на 15 сек. Для сравнения на ВК Видео каждые 15 мин реклама по 3 сек. Поэтому, все остальные прелести платформы несущественны.
miulyano
а подписку пробовали подключать? она вроде совсем рекламу отключает
Tolnik
Нет, не пробовал и пробовать не хочу. Просто пользуюсь ВК Видео и Матрешкой для размещения своего видео. А если смотреть и расслабиться - то Ютубом (с VPN) и ВК Видео.
space2pacman
axe_chita
ВК Видео фризится каждые 7-10 секунд, и это при предзагруженном видео (грубо говоря 3-4 минуты видео) пытался выставить низкое качество вплоть до 144 - не помогает. Более того он регулярно пытается это ограничение обойти и поднять качество до 1080. Так что ВК Видео далеко не айс