myYouTube/analytics/2026-09-25-subtitles-off-by-default.md
vrubelroman 43adec5224 Fix download status handling and the blank page after download
- Never let a non-authoritative MeTube 'updated' event downgrade a
  terminal job (it refilled the progress bar after completion).
- Self-heal stale active jobs against MeTube history on status polls,
  so a missed event no longer leaves a job stuck in 'queued'.
- Build YT.Player on an imperatively created child div: React keeps
  owning the container, so switching to the local copy after a
  download no longer throws removeChild and blanks the page.
- Autoplay on open and seek controls; subtitle experiment reverted.
2026-09-27 23:36:09 +00:00

17 KiB
Raw Blame History

Субтитры выключены по умолчанию в обоих плеерах (кнопка CC остаётся)

Статус (2026-09-25): решение ОТМЕНЕНО, правки откатываются. Пользователь отказался от обходных путей ради субтитров: force-off у YouTube без потери кнопки CC не существует, поэтому субтитры пользователь отключит сам в настройках YouTube-аккаунта (Настройки → Воспроизведение и производительность → Субтитры и CC → снять «Всегда показывать субтитры»). Актуальное состояние — в журнале ниже.

Задача

На странице видео субтитры не должны показываться сами по себе: по умолчанию они ВЫКЛЮЧЕНЫ и в локальном <video>, и в YouTube-embed. Кнопка/меню субтитров (CC) остаётся доступной — пользователь может включить дорожку вручную, как и раньше.

Три точки изменения в frontend/src/components/Player.tsx:

  1. Локальная ветка <video>: на событии loadedmetadata пройтись по video.textTracks и выставить mode = 'disabled' всем дорожкам (стандартный HTML5 API). Нативная кнопка CC в контролах плеера останется и сможет включить дорожку вручную.
  2. YouTube-ветка (IFrame Player API): добавить cc_load_policy: 0 в playerVars (существующие playsinline: 1, autoplay: 1 сохранить).
  3. Фолбэк plain-iframe: параметр cc_load_policy=0 в src (аналог).

Контекст

  • Текущее состояние Player.tsx (300 строк, последний коммит b1faeb3 «Autoplay video when opening the video page»; рабочая ветка master, дерево чистое):
    • Локальная ветка (строки 251–261): <video ref={videoRef} controls autoPlay playsInline src={video.local.media_url!} onError={…} /> — обработчика loadedmetadata и какой-либо работы с textTracks нет (grep по frontend/src: textTracks, loadedmetadata, cc_load_policy не встречаются).
    • <track>-элементов в JSX нет и backend .vtt/субтитры не отдаёт (grep по *.py пуст): в локальном плеере дорожки возможны только встроенные в медиаконтейнер (in-band, например MP4), отданные через media_url MeTube-прокси.
    • YouTube-ветка (строки 178–193): new YT.Player(host, { videoId, host: 'https://www.youtube-nocookie.com', playerVars: { playsinline: 1, autoplay: 1 }, events: { onReady } }) — cc_load_policy отсутствует.
    • Фолбэк при таймауте API (строки 279–285): <iframe src="https://www.youtube-nocookie.com/embed/{id}?autoplay=1" … />.
  • Player монтируется только в VideoPage.tsx:24 с key={video.youtube_video_id}: смена видео — ремоунт, обработчик loadedmetadata будет срабатывать на каждый свежий монтаж, отдельной навигационной логики не требуется.
  • HTML5 API (факт для приёмки): у TextTrack.mode три значения — 'disabled' / 'hidden' / 'showing'. По WHATWG HTML дорожка <track> без атрибута default стартует с mode='disabled', однако у in-band дорожек браузеры могут включить показ по флагам контейнера (forced) или по пользовательским настройкам («always show captions» в Chrome/системных настройках субтитров) — поэтому явный проход с mode='disabled' на loadedmetadata является защитной мерой, а не пустым действием. Присваивание mode у in-band дорожки легально (свойство writable); дорожки kind='metadata' не отображаются в любом случае, установка им mode='disabled' безвредна.
  • Нативная CC-кнопка (факт для приёмки): в нативных контролах браузера кнопка/меню субтитров отображается, когда video.textTracks.length > 0. Установка mode='disabled' не удаляет дорожки из TextTrackList и не прячет кнопку: выбор дорожки из меню ставит mode='showing'. Требуется ручная проверка в реальном браузере (Chrome/Firefox) — зафиксирована в критериях.
  • YouTube cc_load_policy (факт для рисков): по документации IFrame API значение 1 принудительно включает субтитры; 0/отсутствие параметра — «по предпочтению пользователя». Force-off у YouTube не существует: 0 означает «не включать принудительно», но если у пользователя субтитры включены в настройках YouTube (embed хранит предпочтение в cookie), они могут показаться и с 0.
  • Frontend без тестовой инфраструктуры; проверки — npm run lint (oxlint) и npm run build (tsc -b && vite build). Backend, миграции — не трогаем.

Затронутые подсистемы и файлы

Только frontend, один файл:

  • frontend/src/components/Player.tsx:
    • локальная ветка: на <video> добавить onLoadedMetadata — обойти textTracks, всем mode = 'disabled';
    • YouTube-ветка: playerVars: { playsinline: 1, autoplay: 1 } → { playsinline: 1, autoplay: 1, cc_load_policy: 0 };
    • фолбэк-iframe: src → https://www.youtube-nocookie.com/embed/{id}?autoplay=1&cc_load_policy=0.

App.css, VideoPage.tsx, backend, тесты, миграции, README — без изменений.

Критерии приёмки

  1. Локальный плеер: субтитры не показываются по умолчанию — после loadedmetadata у всех дорожек video.textTracks[*].mode === 'disabled'; проверено на файле со встроенной дорожкой (in-band MP4).
  2. YouTube-embed: субтитры не включаются принудительно — cc_load_policy: 0 присутствует и в playerVars, и в src фолбэк-iframe.
  3. Кнопка CC доступна и работает: в локальном плеере при наличии дорожек нативная кнопка/меню CC в контролах видна и ручной выбор дорожки включает её показ; у YouTube-embed CC-кнопка в UI плеера работает как раньше.
  4. Существующая логика не сломана: autoplay (обе ветки, включая фолбэк), двойной тап локального плеера с индикатором, кнопки «−10 сек»/«+10 сек» (disabled до onReady в YouTube-ветке), onError→переключение на YouTube, onReady-логика (сброс таймаута), фолбэк plain-iframe по таймауту, destroy() в cleanup — работают как раньше.
  5. npm run lint и npm run build в frontend/ чистые.
  6. Backend не тронут: изменения только в frontend/src/components/Player.tsx и analytics/; pytest не требуется.
  7. Деплой по правилу: docker compose up -d --build, curl http://localhost:8080/api/health → OK.

План

  1. Player.tsx, локальная ветка (строки 251–261): к <video> добавить React-обработчик onLoadedMetadata, который по e.currentTarget.textTracks выставляет mode = 'disabled' всем дорожкам, например:
    onLoadedMetadata={(e) => {
      Array.from(e.currentTarget.textTracks).forEach((track) => {
        track.mode = 'disabled'
      })
    }}
    
    Существующие атрибуты (controls, autoPlay, playsInline, src, onError) не трогать. Array.from по TextTrackList (iterable) корректен и по TS DOM lib.
  2. Player.tsx, YouTube-ветка (строка 181): playerVars: { playsinline: 1, autoplay: 1, cc_load_policy: 0 } (тип Record<string, string | number> — число допустимо). onReady, таймеры, host: youtube-nocookie.com не трогать.
  3. Player.tsx, фолбэк (строка 281): src → `https://www.youtube-nocookie.com/embed/${video.youtube_video_id}?autoplay=1&cc_load_policy=0`. title/allow/allowFullScreen не менять.
  4. Проверки: npm run lint, npm run build; ручная — локальное видео со встроенной дорожкой (дорожки не показываются, CC-кнопка есть и включает), локальное видео без дорожек (поведение не изменилось), YouTube-видео (субтитры не включаются сами; автозапуск, кнопки ±10 сек, фолбэк при блокировке youtube.com в DevTools); деплой + health.

Риски и ограничения

  • У YouTube нет force-off (главный риск, принят осознанно): cc_load_policy: 0 — это «не включать принудительно», поведение равно дефолту и учитывает предпочтение пользователя. Если у пользователя субтитры включены в настройках YouTube (embed-контекст хранит выбор в cookie), они могут показаться несмотря на 0. Это ограничение YouTube, а не баг; явное 0 фиксирует намерение и защищает от случайного перехода на 1 в будущем.
  • In-band дорожки и момент появления: обычно textTracks заполнены уже к loadedmetadata; в редких случаях браузер добавляет дорожки позже (событие addtrack). Приёмка включает проверку на реальном файле с дорожкой; если окажется, что дорожки появляются после loadedmetadata, усилить обработчиком addtrack на video.textTracks (превентивно не делать — держим минимальный дифф).
  • CC-кнопка появляется только при наличии дорожек: если в медиафайле дорожек нет, нативной кнопки CC нет — это ожидаемо (нечего показывать), а не регресс. При наличии дорожек mode='disabled' кнопку не прячет.
  • Браузерные/системные настройки пользователя: Chrome/Edge «always show captions», системные субтитры Windows/macOS могут заставлять браузер показывать дорожки даже после нашей установки 'disabled'; это вне контроля приложения и относится к пользовательским предпочтениям, а не к дефолту плеера.
  • Не трогать существующую логику: изменение аддитивное — один обработчик на <video>, один ключ в playerVars, один query-параметр в src; onError/recheck, таймауты, cleanup, рендер фолбэка не затрагиваются.
  • Ремоунт по key: onLoadedMetadata срабатывает при каждом монтировании Player (переход на страницу, смена видео) — это ожидаемо и идемпотентно.

Журнал изменений

  • 2026-09-25: документ создан перед реализацией. Зафиксированы факты по коду: <video controls autoPlay playsInline> без onLoadedMetadata и без работы с textTracks (Player.tsx:251–261); <track> в JSX нет, backend .vtt не отдаёт — локальные дорожки только in-band через media_url; playerVars: { playsinline: 1, autoplay: 1 } без cc_load_policy (строка 181); фолбэк-iframe src = ?autoplay=1 (строки 279–285); Player монтируется только в VideoPage.tsx:24 с key. Решения: onLoadedMetadata с mode='disabled' по всем textTracks (нативная CC-кнопка остаётся, ручное включение работает — проверяется в приёмке); cc_load_policy: 0 в playerVars и &cc_load_policy=0 в src фолбэка; отсутствие force-off у YouTube зафиксировано в «Рисках». README не обновляется (описывает плеер на уровне «YouTube-плеер», README.md:10, без деталей опций); backend не трогаем.
  • 2026-09-25 (follow-up): подтвердились оба риска из «Рисков» — cc_load_policy: 0 не отключает субтитры при включённом пользовательском предпочтении (force-off не существует), и in-band дорожки могут появляться после loadedmetadata. Продолжение зафиксировано как Баг 3 в analytics/2026-09-25-download-status-fixes.md: сброс unloadModule('captions') + loadModule('captions') через IFrame API после onReady (YouTube) и обработчик addtrack на textTracks, отключающий новые дорожки (локальное видео). Реализация выполняется в рамках той задачи; этот документ не переписывается.
  • 2026-09-25 (ОТКАТ): решение отменено пользователем. Обходные пути ради субтитров больше не делаем — все правки этой задачи откатываются в frontend/src/components/Player.tsx: убраны cc_load_policy: 0 из playerVars и cc_load_policy=0 из src фолбэк-iframe, вызовы unloadModule('captions')/loadModule('captions') в onReady и соответствующие методы loadModule/unloadModule из интерфейса YoutubePlayerApi, onLoadedMetadata с переводом textTracks в mode='disabled', отдельный useEffect с обработчиком addtrack. Причина: у YouTube нет force-off субтитров без потери кнопки CC (подтверждено практикой реализации) — пользователь сам отключит субтитры в настройках YouTube-аккаунта (Настройки → Воспроизведение и производительность → Субтитры и CC → снять «Всегда показывать субтитры»). Незатронутые правки сохраняются: autoplay-атрибуты обеих веток, императивный дочерний div (фикс чёрной страницы), кнопки ±10 сек, двойной тап с индикатором, таймаут-фолбэк, cleanup. Баг 3 в analytics/2026-09-25-download-status-fixes.md откатывается вместе с этой задачей; Баги 1 и 2 там остаются в силе. Критерий отката: в Player.tsx не остаётся ни одного упоминания captions/cc_load_policy/textTracks/addtrack.