Skip to content

ТЗ — Видеохостинг (cms/video-hosting)

Слой: 🟡 функц-модуль · Зрелость доноров: ★ · Донор: — Статус: ТЗ к разработке

Назначение и возможности

Встраивание видео с внешних хостингов (VK Video, YouTube, Rutube) в контент сайта: блок «видео» с lazy-load плеера, oEmbed-резолв метаданных и превью в медиатеке без загрузки самого плеера до клика пользователя.

  • Блок «видео» (BlockRegistry) — вставка по ссылке на VK Video/YouTube/Rutube
  • Lazy-load: до клика пользователя грузится только превью-картинка, не iframe-плеер
  • oEmbed-резолв метаданных (заголовок, превью, длительность) при добавлении ссылки
  • Локальное превью в медиатеке — картинка сохраняется в cms/storage-s3, не тянется с хостинга каждый раз
  • Кеш метаданных oEmbed — повторное добавление той же ссылки не делает новый запрос
  • Фоновая проверка битых видео (удалённых/недоступных на хостинге) с отчётом редактору
  • Декларация доменов провайдеров для CSP (frame-src) через канонический фильтр FilterBus security.csp — модуль не расширяет CSP сам

Зависимости и выключение

requires: ядро (cms/core-contracts, BlockRegistry), cms/integrations-bus, cms/storage-s3 · suggests: cms/cookie-consent (выбор cookie-домена YouTube по состоянию согласия)

Поведение при выключении: ранее вставленные блоки видео на страницах перестают резолвить новые ссылки (существующие превью показываются из кеша) — редакторы не могут добавлять новые видео-блоки до включения модуля. Kill-switch резолва (см. «Настройки») даёт более мягкую степень деградации, чем полное выключение модуля: превью и уже резолвленные видео продолжают отдаваться, останавливается только обращение к внешним API.

Модель данных

ТаблицаКлючевые поляПримечание
cms_video_hosting_embedsid, provider, external_id, url, title, preview_path, duration_seconds, width, height, resolve_status, resolve_error, last_checked_atрезолвленные видео с локальным превью

Индексы: provider — PHP Enum; уникальный индекс на provider+external_id; индекс на resolve_status (выборка битых видео для отчёта); last_checked_at — под батч-джобу периодической проверки. width/height — из oEmbed, нужны фронтенду для aspect-ratio placeholder (см. «Производительность и кеш»).

ПДн-паспорт: модуль ПДн не хранит — ссылка на видео, заголовок, превью и метаданные хостинга не являются персональными данными; хуки «выгрузить/забыть по субъекту» не применимы (декларация, не реализация).

Входные и выходные данные

Всё, что не перечислено ниже, модуль отвергает (whitelist-принцип §11 стандарта).

Входы:

ИсточникДанные/поляЧем валидируется
Редактор страниц (вставка блока «видео»)urlFormRequest: формат URL провайдера из enabled_providers (regex на домен/id по каждому провайдеру), провайдер вне whitelist — отклонено
oEmbed-ответ провайдера (через cms/integrations-bus)title, thumbnail_url, html, width, height, durationсхема ответа провайдера валидируется перед сохранением; thumbnail_url — домен из whitelist провайдера перед скачиванием в cms/storage-s3
Filament: настройки модуляenabled_providers[], lazy_load, oembed_cache_ttl_days, max_videos_per_page, oembed_resolve_enabled, youtube_nocookie, embed_timeout_secondsFormRequest, whitelist значений enabled_providers
Legacy-импорт (cms:video-hosting:import-legacy)старые <iframe>-вставки страниц донорапарсинг src по whitelist доменов провайдеров, извлечение external_id регэкспом провайдера (ReDoS-safe, ядро)
Событие MediaDeleted (ядро, слушает)media_idесли media_id совпадает с preview_path записи — запись помечается «превью отсутствует»

Выходы:

ПотребительДанныеФормат
Рендер блока «видео» на страницепревью-картинка (fixed aspect-ratio по width/height) + play-иконка; после клика — iframe с whitelisted srcHTML, публичный, page-cache-friendly (блок не персонализирован)
Галерея/related-контент (внутри страницы)title, duration_seconds, preview_pathJSON внутри данных блока
Событие VideoEmbedResolved / VideoEmbedResolveFailedprovider, external_id[, url, error]канал 1, для наблюдаемости/health
Отчёт cms:video-hosting:check-broken --jsonсписок страниц/блоков с недоступными видеоJSON/CSV, скачиваемый в Filament
Канонический фильтр security.csp (ядро)список доменов frame-src для активных enabled_providersподписка на FilterBus-фильтр в boot(), не HTTP (ревизия ядра №2, п. 5)

Настройки (группа video-hosting)

КлючТипДефолтaffectsPageCacheОписание
video-hosting.enabled_providersarray["youtube","vk","rutube"]нетРазрешённые хостинги для блока видео
video-hosting.lazy_loadbooltrueдаЗагружать плеер только по клику
video-hosting.oembed_cache_ttl_daysint30нетСрок жизни кеша метаданных oEmbed
video-hosting.max_videos_per_pageint20даЛимит числа видео-блоков на одной странице (защита от злоупотребления и раздувания веса страницы)
video-hosting.oembed_resolve_enabledbooltrueнетKill-switch: отключить резолв новых oEmbed-ссылок без выключения модуля; уже резолвленные видео продолжают отдаваться из кеша
video-hosting.youtube_nocookiebooltrueдаВстраивать YouTube через youtube-nocookie.com (без трекинг-cookie до согласия)
video-hosting.embed_timeout_secondsint3нетТаймаут синхронного резолва oEmbed при вставке ссылки в редакторе; при превышении — graceful fallback, ссылка сохраняется, метаданные подтягиваются фоновой джобой

Секретов у модуля нет — публичные oEmbed-эндпоинты провайдеров не требуют авторизации.

API

Отдельного публичного API нет — админ-CRUD через Filament (блок видео настраивается в редакторе страниц ядра). Статус резолва конкретной вставки виден редактору прямо в форме блока (см. «UX-требования»); отчёт битых видео и остаток при исчерпании провайдерских лимитов (если включён YouTube Data API вместо чистого oEmbed) — в Filament-ресурсе модуля.

Компоненты

  • Блоки: «видео» — схема полей url (+ автоопределяемый provider), версия _v1, demo-props (по одному демо-видео на провайдера для галереи /_gallery); fallback при выключении модуля — существующее превью из кеша, без резолва новых ссылок.
  • Виджеты: нет — модуль работает только через блок контента.
  • Filament: ресурс «Видео» (список резолвленных cms_video_hosting_embeds, статус резолва, кнопка «обновить метаданные»), вкладка «Битые видео» с отчётом (check-broken) и списком затронутых страниц, скачиваемым CSV.
  • Команды: cms:video-hosting:refresh-metadata --json (обновление устаревших метаданных), cms:video-hosting:check-broken --json (батч-проверка доступности, формирует отчёт), cms:video-hosting:import-legacy --source=<профиль> --dry-run (легаси-импорт, см. «Донорский код»).
  • Фронтенд-бюджет: до клика на странице — только <img> превью (webp/avif) и инлайновая SVG play-иконка, JS модуля — лёгкий наблюдатель клика/видимости (IntersectionObserver), без загрузки SDK провайдера; после клика подгружается iframe провайдера — его вес вне бюджета модуля, но домен обязан быть в CSP frame-src (см. «Безопасность»).
  • A11y: play-кнопка — семантический <button> с aria-label="Воспроизвести видео: {title}", доступна с клавиатуры (Tab + Enter/Space запускает загрузку плеера, не только клик мышью).
  • Демо-контент: сидер с демо-видео по одному на провайдера — галерея /_gallery и playground показывают блок без ручного ввода ссылок.

События и обмен

СобытиеКогдаPayload
VideoEmbedResolvedссылка на видео успешно резолвлена через oEmbedprovider, external_id
VideoEmbedResolveFailedрезолв метаданных завершился ошибкойprovider, url, error

Резолв oEmbed и загрузка превью в cms/storage-s3 — через cms/integrations-bus.

Таблица взаимодействий:

Сущность/модульКаналНаправлениеЧто происходит
BlockRegistry (ядро)регистрация в реестре (bootstrap)модуль → ядроблок «видео» регистрируется, рендерится ядром внутри контента страницы
cms/storage-s3прямой сервис-вызов (requires, канал 4)модуль → storage-s3загрузка и хранение локального превью-картинки
cms/integrations-busпрямой сервис-вызов (requires, канал 4)модуль → integrations-busвызов oEmbed API провайдера — единственный путь внешнего HTTP
cms/healthhealth-чек (реестр манифеста)модуль → healthдоступность oEmbed по каждому провайдеру отдельно, отставание очереди video-hosting
MediaDeleted (ядро, ревизия 14.07.2026)событие, канал 1, слушаетядро → модульпревью удалено из медиатеки вручную → запись помечается «превью отсутствует», следующий refresh-metadata перезаливает
security.csp (канонический фильтр FilterBus, ядро)2 — FilterBus, подписка при boot()модуль → ядромодуль дописывает frame-src-домены для активных enabled_providers в фильтр; ядро собирает итоговый CSP-заголовок (ревизия ядра №2, п. 5)
cms/cookie-consent (suggests, если установлен)provides-контракт / событие CookieConsentGivenмодуль ← cookie-consentвыбор cookie-домена YouTube (youtube.com vs youtube-nocookie.com) может учитывать состояние согласия; при отсутствии модуля — всегда youtube-nocookie.com по умолчанию

Фоновая работа

Обновление устаревших метаданных oEmbed — фоновая команда cms:video-hosting:refresh-metadata по расписанию, именованная очередь video-hosting. Батч-проверка битых видео (check-broken) — отдельное периодическое задание той же очереди, чанками по last_checked_at (не сканирует всю таблицу разом). Резолв при добавлении ссылки в редакторе — синхронный вызов с таймаутом embed_timeout_seconds и graceful fallback (заглушка при недоступном/удалённом хостингом ролике, страница не падает); при таймауте блок сохраняется с resolve_status=pending, метаданные дорезолвливаются фоновой джобой. Массовое добавление видео (импорт страниц с десятками ссылок) — резолв метаданных батчем через очередь, не блокирует сохранение страницы ожиданием всех oEmbed-ответов.

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

  • Горячий путь — рендер блока видео на странице: при попадании в page-cache — 0 запросов (готовый HTML с превью); при промахе page-cache блок читает метаданные из тега video-hosting:oembed:{provider}:{external_id} (0 запросов к БД на горячем пути), поход в БД — только на холодный кеш.
  • Тег video-hosting:oembed — кеш метаданных oEmbed (TTL oembed_cache_ttl_days), предотвращает повторный внешний запрос для той же ссылки; инвалидируется событиями VideoEmbedResolved и вручную по кнопке «обновить метаданные» в Filament.
  • Lazy-load, вес страницы и CLS: до клика на странице грузится только превью (лёгкая картинка) — вес плеера провайдера (SDK, iframe) не входит в начальную загрузку страницы; обязателен fixed aspect-ratio placeholder на основе width/height из метаданных oEmbed — без него вставка блока вызывает layout shift (CLS) при подгрузке превью, что бьёт по Core Web Vitals.
  • Ожидаемые объёмы: от единиц до низких тысяч записей cms_video_hosting_embeds на сайт; max_videos_per_page ограничивает нагрузку одной страницы.
  • Индексы: provider+external_id (уникальный, резолв по ссылке), resolve_status (выборка битых видео под отчёт), last_checked_at (чанкинг батч-проверки).

Безопасность

  • Плеер не загружается до клика пользователя (lazy-load) — снижает поверхность для стороннего кода хостинга на странице.

  • Недоступный/удалённый хостингом ролик не ломает страницу — отображается заглушка.

  • CSP frame-src: встраивание iframe провайдера требует явного whitelist доменов (youtube.com/youtube-nocookie.com, vk.com/vkvideo.ru, rutube.ru) в Content-Security-Policy страницы — модуль дописывает нужные домены каноническим фильтром FilterBus security.csp (ревизия ядра №2, п. 5; см. «События и обмен»); самодельное расширение frame-src * в коде модуля запрещено.

  • Скан загрузок превью (ревизия ядра №2, п. 4): локальное превью попадает в медиатеку через cms/storage-s3 тем же путём, что и обычная загрузка файла, — на него распространяется контракт upload-scanner: при подключённой реализации превью проходит карантин pending до вердикта наравне с любым другим файлом; 0 реализаций — скан пропускается (штатная деградация ядра, модуль ничего не делает дополнительно).

  • Режим без cookie (GDPR/152-ФЗ): YouTube по умолчанию встраивается через youtube-nocookie.com (youtube-hosting.youtube_nocookie=true) — без трекинг-cookie до явного согласия; переключение на youtube.com возможно при интеграции с cms/cookie-consent. Для VK/Rutube аналогичного no-cookie домена нет — ограничение документируется явно; риск частично снят тем, что плеер (и его cookie) не грузится до клика пользователя.

  • Ссылка вне enabled_providers или некорректного формата отклоняется на входе с понятной ошибкой редактору — мусорный блок не сохраняется.

  • Матрица ролей:

    Permissionадминменеджерредакторstudio
    video-hosting.view (список видео, статусы резолва, отчёт битых видео)
    video-hosting.manage (вставка блока, ручной пересчёт метаданных, запуск check-broken)
    Изменение настроек модуля (enabled_providers, CSP-влияющие параметры)
  • В логи и события не попадают ПДн (их у модуля нет) и секреты; идентификаторы — provider+external_id, не сырой URL пользователя, где это уместно.

UX-требования

Админ:

  • редактор видит статус резолва прямо при вставке ссылки в блок (успех — превью и заголовок подтянулись; ошибка — понятное сообщение «не удалось получить метаданные, видео сохранено, попробуем ещё раз в фоне»);
  • список «Битые видео» в Filament — пустое состояние «битых видео не найдено», при наличии — таблица со страницами и кнопкой перехода к редактированию блока; подтверждение не требуется (просмотр, не деструктивная операция);
  • массовое действие «обновить метаданные» для выбранных строк списка видео.

Посетитель:

  • превью с play-иконкой видно сразу (без layout shift), плеер грузится только по клику или по видимости (если lazy_load допускает предзагрузку по intersection — настройка явная, не поведение по умолчанию без документации);
  • при удалённом/недоступном видео — заглушка «видео больше недоступно» вместо битого iframe с ошибкой хостинга;
  • play-кнопка доступна с клавиатуры (см. a11y в «Компоненты»).

Крайние случаи и типовые баги

  • Видео удалено/недоступно на хостинге (автор удалил ролик на YouTube/VK/Rutube) → блок на странице показывает заглушку «видео больше недоступно», не битый iframe; периодическая батч-джоба check-broken формирует отчёт битых видео со списком затронутых страниц для редактора.
  • Embed-код и CSP → iframe встраивания требует явного whitelist доменов провайдера в frame-src; модуль только дописывает нужные домены каноническим фильтром security.csp, самодельного frame-src * в коде модуля быть не должно.
  • GDPR-режим / встраивание без cookie → YouTube по умолчанию через youtube-nocookie.com до согласия (либо переключение по состоянию cms/cookie-consent); для VK/Rutube аналога нет — задокументированное ограничение, частично снятое тем, что cookie ставится только после клика по плееру.
  • Lazy-load без резерва места → отсутствие fixed aspect-ratio placeholder вызывает CLS при подгрузке превью — обязателен резерв по width/height из метаданных oEmbed.
  • oEmbed-резолв меняет формат ответа / провайдер меняет API (например, VK меняет домен vk.comvkvideo.ru) → изолированный сбой резолва конкретного провайдера, не всего модуля; остальные провайдеры продолжают резолвиться штатно, алерт разработчику через cms/health.
  • Ссылка некорректного формата/чужого провайдера (не из enabled_providers) → валидация на входе отклоняет с понятной ошибкой редактору (422), мусорный блок не сохраняется.
  • Массовое добавление видео (импорт страниц с десятками видео-ссылок) → резолв метаданных батчем через очередь, не блокирует сохранение страницы ожиданием всех oEmbed-ответов; статус каждой ссылки — pending до фоновой обработки.
  • Синхронный резолв превышает embed_timeout_seconds → блок сохраняется с resolve_status=pending и graceful fallback вместо зависшего сохранения страницы, фоновая джоба дорезолвливает.
  • Достигнут max_videos_per_page → вставка следующего видео-блока отклоняется понятной ошибкой редактору («лимит видео на странице исчерпан»), не тихим обрезанием списка блоков.
  • Kill-switch oembed_resolve_enabled=false → новые ссылки не резолвятся (ошибка редактору «резолв временно отключён»), уже резолвленные видео продолжают отображаться из кеша без деградации публичной части сайта.

Донорский код

Донор: — (новая разработка).

Легаси-импорт (§16 стандарта): если у проекта есть донор со страницами, где видео уже встроено как сырой <iframe> (не через модуль), команда cms:video-hosting:import-legacy --source=<профиль> --dry-run парсит старые iframe-вставки по whitelist доменов провайдеров, извлекает external_id, создаёт блок «видео» на месте старой вставки и запись в cms_video_hosting_embeds; идемпотентна — повторный прогон обновляет по ключу provider+external_id, не дублирует. Отчёт: сколько iframe-вставок распознано/создано/пропущено (нераспознанный домен — пропуск с причиной в отчёте, не тихий проглот).

Тесты и приёмка

  • [ ] Мок oEmbed-API: тест резолва метаданных для каждого поддерживаемого хостинга
  • [ ] Плеер не загружается до клика пользователя (тест lazy-load — только превью в DOM)
  • [ ] Кеш метаданных предотвращает повторный запрос oEmbed для той же ссылки
  • [ ] Недоступный/удалённый хостингом ролик не ломает страницу — отображается заглушка
  • [ ] check-broken формирует отчёт битых видео со списком затронутых страниц
  • [ ] Fixed aspect-ratio placeholder присутствует в разметке блока (контрактный тест на CLS)
  • [ ] Ссылка вне enabled_providers/некорректного формата отклоняется 422, блок не сохраняется
  • [ ] Достижение max_videos_per_page отклоняет вставку следующего блока с понятной ошибкой
  • [ ] Kill-switch oembed_resolve_enabled=false блокирует новый резолв, не трогает уже показанные видео
  • [ ] frame-src-домены модуля корректно попадают в CSP через фильтр security.csp (по активным enabled_providers)
  • [ ] Превью, загружаемое в медиатеку через cms/storage-s3, проходит карантин upload-scanner при подключённой реализации; 0 реализаций — скан пропускается без ошибки
  • [ ] youtube_nocookie=true рендерит iframe на youtube-nocookie.com
  • [ ] Массовое добавление видео резолвится батчем в очереди, не блокирует сохранение страницы
  • [ ] Legacy-импорт: парсинг тестовых iframe-вставок создаёт корректные блоки, повторный прогон не дублирует
  • [ ] При выключении модуля существующие превью продолжают отображаться из кеша
  • [ ] Матрица ролей: video-hosting.manage доступен редактору, изменение настроек — только admin/studio
  • [ ] Контрактный набор cms-testing зелёный, пакет тестируется в testbench-изоляции
  • [ ] Feature-тест на каждый роут; тестовая БД только video_hosting_test, migrate:fresh запрещён

Внутренняя база знаний студии