508 lines
59 KiB
Markdown
508 lines
59 KiB
Markdown
# Воспроизведение видео на Android TV: итоговые выводы
|
||
|
||
Итог работы над воспроизведением в клиенте Jellyfin для Android TV (2026-10-12…14). Документ написан так, чтобы по нему можно было **повторить то же качество воспроизведения в другом приложении**. В нём: как устроен движок, какие правила выбора, что сломано в media3, libVLC и на телевизорах MediaTek и как это обходится, как тестировать.
|
||
|
||
Исходное исследование, сделанное до кода, лежит в [`research/`](research/). Здесь только то, что подтвердилось или выяснилось на реальном устройстве.
|
||
|
||
Код, на который ссылается документ, находится в модулях `playback/*` этого репозитория:
|
||
- `playback/api` — интерфейсы;
|
||
- `playback/core` — сессия, выбор движка, опрос устройства;
|
||
- `playback/engine-exo` — ExoPlayer;
|
||
- `playback/engine-vlc` — libVLC.
|
||
|
||
---
|
||
|
||
## 0. Коротко: главные выводы
|
||
|
||
1. **Гибрид из двух движков работает.** media3/ExoPlayer — основной (аппаратный MediaCodec, HDR10/HDR10+/HLG/Dolby Vision, passthrough). libVLC — запасной (программный ffmpeg). На тестовой медиатеке (12 671 файл, треть — AVI) **ни один файл не потребовал серверного транскода**.
|
||
2. **Выбор движка — автоматический, по «весу потерь»**, до запроса к серверу. Плюс два сторожа во время просмотра: «нет первого кадра за 60 с» и «застой». По их сигналу сессия переходит на следующий движок с той же позиции.
|
||
3. **Большинство проблем оказались не в выборе движка, а в мелочах**, которые не видны в документации:
|
||
- экстракторы media3 ломаются на реальных файлах: MKV с цепочкой SeekHead, AC3 в AVI, MPEG-2 в VOB без PTS, DVD private stream;
|
||
- libVLC не дружит с HTTPS через reverse proxy и перекачивает данные при каждом шаге назад;
|
||
- на ТВ многоканальный PCM беззвучен, а видеослой «улучшает» SDR-картинку;
|
||
- убитый посреди видео процесс оставляет мусор в видеоконвейере MTK, и тот лечится только перезагрузкой ТВ.
|
||
4. **Путь вывода видео — отдельное решение**, не равное выбору движка:
|
||
- SurfaceView (аппаратный видеослой) нужен для HDR, 4K, чересстрочного видео и частот 24/25/50 (ТВ сам переключает частоту панели);
|
||
- TextureView (через GPU) даёт более естественную SDR-картинку ≤1080p 30/60 к/с на Xiaomi: обходит постобработку ТВ.
|
||
5. **Dolby Vision**: DV-декодер с базовым слоем HDR10 работает, ТВ включает режим DV. Если DV-декодера нет или дисплей не DV — играть базовый слой (HEVC/AV1/AVC) через свой `MediaCodecSelector`.
|
||
6. **Звук**: passthrough всего, что ТВ принимает (AC3, E-AC3, JOC, TrueHD, DTS, DTS-HD, AC4, MAT — декодирует Dolby MS12 телевизора). Остальное — стерео PCM. Для устройств без passthrough — ffmpeg-аудиодекодер в media3.
|
||
7. **Сеть**: дома видео нужно брать с LAN-адреса сервера по HTTP, а не через HTTPS-hairpin на reverse proxy. Иначе 4K-ремукс на 60+ Мбит/с уходит в подзагрузку каждые 5–8 с.
|
||
8. **Память**: на 32-bit ТВ с кучей 192 МБ ExoPlayer по умолчанию копит до ~145 МБ буфера → OOM. Нужны `largeHeap` и ограничение буфера.
|
||
|
||
---
|
||
|
||
## 1. Исходные условия
|
||
|
||
### 1.1 Тестовое устройство
|
||
|
||
| Параметр | Значение |
|
||
|---|---|
|
||
| ТВ | Xiaomi TV S Pro Mini LED 75 (`MiTV-MOEU1`), Google TV, Android 14 / API 34 |
|
||
| SoC | MediaTek MT9689 (`ro.hardware=mt5896`), 4×Cortex-A73, GPU Mali (слабый) |
|
||
| ABI | **только `armeabi-v7a`**, 32-bit userspace. APK обязан содержать 32-bit библиотеки |
|
||
| RAM | 2,4 ГБ, свободно ~870 МБ. Java-куча 192 МБ, с `largeHeap` 512 МБ |
|
||
| Дисплей | приложения рисуются в 1920×1080, видеоплоскость 4K. Android видит один режим 3840×2160@60. HDR: DV, HDR10, HDR10+, HLG |
|
||
| Видеодекодеры (рантайм) | AVC (включая **High10**), HEVC, VP9, AV1 — до 8K; MPEG-2, VC-1, MPEG-4 — до 1080p (MPEG-4 до 1344×1152). DV: P4, P5, P8, P9, P10. **P7 нет** |
|
||
| Звук | Dolby MS12. Passthrough на встроенные динамики: AC3, E-AC3, E-AC3 JOC, TrueHD, DTS, DTS-HD, AC4, Dolby MAT |
|
||
|
||
### 1.2 Сервер и медиатека
|
||
|
||
- Jellyfin 12.2, `jellyfin-sdk-kotlin` 1.9.2.
|
||
- Медиатека: 12 671 файл. MKV / AVI / MP4 = 6924 / 4207 / 1524. MPEG-4 ASP (DivX/Xvid) — 4182, H.264 Hi10P — 151, HEVC HDR — 42, DV P8 — 46, MPEG-2 (в т. ч. чересстрочный) — 14, ASS-субтитров — 2475. Подробно: [`research/library-profile.md`](research/library-profile.md).
|
||
- Сервер доступен по публичному адресу (`https://jellyfin.example.com`) через openresty (HTTP/1.1) и в LAN по `http://<адрес-сервера-в-LAN>:8096`.
|
||
|
||
### 1.3 Версии библиотек
|
||
|
||
| Библиотека | Версия | Зачем |
|
||
|---|---|---|
|
||
| `androidx.media3` | 1.11.1 | основной движок |
|
||
| `org.videolan.android:libvlc-all` | 3.7.7 (внутри libVLC 3.0.25) | запасной движок |
|
||
| `io.github.peerless2012:ass-media` | 0.5.1 | ASS/SSA в ExoPlayer через libass |
|
||
| `org.jellyfin.media3:media3-ffmpeg-decoder` | 1.9.0+1 | программное аудио (TrueHD, DTS…) в ExoPlayer, если нет passthrough |
|
||
| OkHttp | 5.5.0 | локальный прокси для libVLC |
|
||
|
||
---
|
||
|
||
## 2. Архитектура
|
||
|
||
### 2.1 Модули и интерфейсы
|
||
|
||
```
|
||
playback/api PlaybackEngine, EngineProvider, Verdict/Loss, DeviceCapabilities, Codecs
|
||
+ интерфейсы-возможности: VideoPathSwitchable, DolbyVisionSwitchable,
|
||
SubtitleStyleable, AudioDelayCapable, SubtitleDelayCapable
|
||
playback/core DeviceCapabilitiesProbe, EngineSelector, PlaybackSession, PlaybackPreferences
|
||
playback/engine-exo ExoEngineProvider, ExoEngine, исправленные экстракторы
|
||
playback/engine-vlc VlcEngineProvider, VlcEngine, LoopbackProxy, RecentBytesCache
|
||
```
|
||
|
||
- **`EngineProvider`** — точка подключения движка. Регистрируется в модуле Koin своего движка (`bind EngineProvider::class`; до 2026-10-25 — Hilt `@IntoSet`). Новый движок (mpv, libVLC 4) — это новый модуль, ядро не меняется. Методы:
|
||
- `evaluate(source, selection, device) → Verdict` — может ли движок сыграть файл напрямую и чего это будет стоить;
|
||
- `deviceProfile(device)` — свой DeviceProfile для сервера;
|
||
- `create(device)` — экземпляр движка.
|
||
- **`Verdict`** = `Direct` | `Degraded(losses)` | `No(reasons)`.
|
||
- **`PlaybackEngine`** — узкий интерфейс: `load`, `play`, `pause`, `seekTo`, выбор дорожек, `isSeekable`, `state`/`events`/`stats` (Flow), `attachVideo(FrameLayout)`, `release`. Остальные возможности — отдельными интерфейсами, которые проверяются через `as?`.
|
||
- Дорожки адресуются **индексами Jellyfin `MediaStream`**. Каждый движок сам сопоставляет их со своими треками. Так выбор дорожки переживает смену движка.
|
||
|
||
Почему не «движок как media3 `Player`»: в клиентах, которые так сделали (Findroid, Wholphin), обёртка над mpv полна `TODO()` и `UnsupportedOperationException`. Если нужен MediaSession, ставится один адаптер `SimpleBasePlayer` поверх своего интерфейса.
|
||
|
||
### 2.2 Жизненный цикл воспроизведения (`PlaybackSession`)
|
||
|
||
1. Выбор движка: `EngineSelector.rank(...)` — список движков по возрастанию суммарного веса потерь.
|
||
2. `POST /Items/{id}/PlaybackInfo` с DeviceProfile **выбранного движка**. Флаги: `enableDirectPlay/DirectStream/Transcoding = true`, `allowVideoStreamCopy/AudioStreamCopy = true`, `alwaysBurnInSubtitleWhenTranscoding = false`, выбранные индексы аудио и субтитров, `mediaSourceId`, `startTimeTicks`.
|
||
3. Если `supportsDirectPlay` — URL `/Videos/{id}/stream?static=true&mediaSourceId&playSessionId&deviceId&tag`. Иначе — `transcodingUrl`. Причины транскода (`TranscodeReasons` в URL) пишутся в лог и показываются в UI.
|
||
4. Внешние субтитры (`deliveryMethod = External`) передаются движку отдельными URL.
|
||
5. Запуск движка, отчёт `Playing`, `Progress` каждые 10 с, в конце `Stopped`.
|
||
6. Сторожа (§7): первый кадр, застой. По ошибке — следующий движок из ранжирования с той же позиции. Если упали все — транскод.
|
||
7. После `Stopped` вызывается `DELETE /Videos/ActiveEncodings` (чтобы сервер не держал ffmpeg) и событие «данные просмотра изменились» для UI.
|
||
|
||
---
|
||
|
||
## 3. Выбор движка
|
||
|
||
### 3.1 Веса потерь (`Loss`)
|
||
|
||
| Потеря | Вес | Когда |
|
||
|---|---|---|
|
||
| `HDR_LOST` | 100 | HDR-файл, а движок или дисплей HDR не выведут |
|
||
| `SOFTWARE_DECODE_HEAVY` | 40 | программно >1080p, 10-bit или >30 к/с при потоке > 1280×720×50 пикс/с |
|
||
| `DV_TO_BASE_LAYER` | 30 | DV будет сыгран как HDR10/HLG базового слоя |
|
||
| `BROKEN_AUDIO_TIMING` | 30 | известная поломка синхронизации звука (сейчас не используется: AC3 в AVI исправлен, §5.5) |
|
||
| `DVD_COMBING` | 30 | чересстрочный MPEG-PS (DVD) в ExoPlayer: «гребёнка» на видеослое ТВ |
|
||
| `HDR_UNVERIFIED` | 25 | HDR через VLC с аппаратным декодером: формально выводится, не проверено |
|
||
| `SOFTWARE_DECODE` | 15 | программное декодирование ≤1080p |
|
||
| `NO_DEINTERLACE` | 5 | чересстрочное в ExoPlayer: деинтерлейсит видеослой ТВ, поэтому вес малый |
|
||
|
||
- `Direct` = 0, `No` = 10 000 (такой движок ещё может играть серверный транскод).
|
||
- При равенстве выигрывает приоритет: Exo = 0, VLC = 10.
|
||
|
||
### 3.2 Правила ExoPlayer (`ExoEngineProvider.evaluate`)
|
||
|
||
- **Контейнеры:** mkv, webm, mp4, m4v, mov, ts, mpegts, avi, mpeg, ogg, flv, 3gp. Остальное (M2TS, ASF/WMV, ISO, BDMV, VIDEO_TS) → `No`.
|
||
- **Видео:**
|
||
- нет MIME для кодека → `No`;
|
||
- H.264 10-bit без аппаратного `AVCProfileHigh10` → `No` (media3 не умеет программно);
|
||
- нет аппаратного декодера под разрешение, но программный есть → `SOFTWARE_DECODE_HEAVY`, иначе `No`.
|
||
- **HDR/DV** (`evaluateRange`):
|
||
- DV-декодер «есть», только если `MediaCodecList` показывает профиль **и** дисплей сообщает `HDR_TYPE_DOLBY_VISION`;
|
||
- P5 (`DOVI`, без совместимого слоя) без DV-декодера → `No`;
|
||
- P7 (`DOVI_WITH_EL…`) без DV-декодера → `No`: media3 не откатывается на HEVC;
|
||
- P8.x / P9 / P10 без DV-декодера → `DV_TO_BASE_LAYER`, и если дисплей не умеет HDR10/HLG — ещё `HDR_LOST`;
|
||
- HDR10 / HDR10+ / HLG без поддержки дисплеем → `HDR_LOST`.
|
||
- **Чересстрочное:** `NO_DEINTERLACE`. Если к тому же контейнер mpeg/mpg/vob/mpegps — ещё и `DVD_COMBING` (§5.8).
|
||
- **Аудио:** passthrough, если кодек есть в `device.passthroughEncodings`; иначе аппаратный или системный декодер; иначе ffmpeg-расширение; иначе `No`.
|
||
- **Субтитры:** внешние PGS / VobSub / DVB → `No` (media3 их как внешние не умеет).
|
||
|
||
### 3.3 Правила libVLC (`VlcEngineProvider.evaluate`)
|
||
|
||
- Контейнеры и кодеки не ограничиваются: libVLC через ffmpeg открывает практически всё.
|
||
- Аппаратный декодер VLC используется **только для >1080p** (§6.4), поэтому всё ≤1080p считается программным: `SOFTWARE_DECODE` или `SOFTWARE_DECODE_HEAVY` (>1080p, 10-bit, или >30 к/с с учётом удвоения деинтерлейсом при потоке > 1280×720×50 пикс/с).
|
||
- DV → `DV_TO_BASE_LAYER`, P5 → ещё и `HDR_LOST`. HDR при аппаратном декодере → `HDR_UNVERIFIED`, при программном → `HDR_LOST`.
|
||
|
||
### 3.4 Что получается на практике (авто)
|
||
|
||
| Файл | Движок | Вывод |
|
||
|---|---|---|
|
||
| HEVC/AV1 4K HDR10/HDR10+/HLG, TrueHD Atmos, ASS | Exo | видеослой, passthrough |
|
||
| DV P8.1, DV + HDR10+, DV P8.4 (HLG) | Exo | видеослой, DV-декодер, ТВ в режиме Dolby Vision |
|
||
| H.264/HEVC SDR ≤1080p, 30/60 к/с | Exo | через GPU (только Xiaomi, §5.2) |
|
||
| SDR 23,976/24/25/50 к/с, SDR 4K | Exo | видеослой |
|
||
| AVI DivX/Xvid (+ AC3) | Exo | аппаратно, AC3 через исправленный экстрактор |
|
||
| H.264 Hi10P | Exo | аппаратно (на этом ТВ есть High10) |
|
||
| MPEG-2 чересстрочный в MKV | Exo | видеослой (ТВ деинтерлейсит) |
|
||
| MPEG-2 чересстрочный в VOB/MPEG-PS (DVD) | **VLC** | yadif |
|
||
| M2TS, WMV, MKV-zlib, внешние PGS/VobSub | **VLC** | — |
|
||
| Ничего не подошло / упали оба | транскод | HLS |
|
||
|
||
### 3.5 Пересчёт при смене дорожки
|
||
|
||
Когда пользователь меняет аудио или субтитры, ранжирование пересчитывается (`PlaybackSession.onSelectionChanged`). В авторежиме сессия переходит на движок, который стал **строго лучше**, кроме уже упавших в этой сессии. Если движок выбран вручную, он не меняется.
|
||
|
||
---
|
||
|
||
## 4. Jellyfin: детали протокола
|
||
|
||
- **Авторизация в 12.x:** `X-Emby-Token` **отклоняется (401)**. Работает только заголовок `Authorization: MediaBrowser Token="…", Client=…, Device=…, DeviceId=…, Version=…`. Там, где заголовок не передать (URL для libVLC), — query `ApiKey=`.
|
||
- `/Videos/{id}/stream?static=true` сервер отдаёт даже без токена (особенность Jellyfin).
|
||
- **SDK `jellyfin-sdk-kotlin` читает тело ответа в вызывающем потоке** → `NetworkOnMainThreadException`. Все вызовы SDK — на `Dispatchers.IO`. SDK требует slf4j в рантайме.
|
||
- **DeviceProfile отдельный для каждого движка** (`buildDeviceProfile` из SDK):
|
||
- Exo: перечислены контейнеры и кодеки, которые реально есть на устройстве; `codecProfile` для hevc/av1/vp9 с `VIDEO_RANGE_TYPE inCollection` (список HDR-типов, включая DV-варианты, только если есть DV-декодер; P7 — только если есть `DvheDtb`); для h264 без High10 — `VIDEO_BIT_DEPTH ≤ 8`. Транскод — HLS fMP4 (`mp4`), hevc/h264, aac/ac3/eac3. Субтитры — EMBED (srt, ass, pgs, dvdsub, dvbsub, vtt…) и EXTERNAL (srt, ass, vtt, ttml).
|
||
- VLC: `directPlayProfile` без ограничений, транскод — HLS `ts`, субтитры — всё, включая sup/idx/sub.
|
||
- `maxStreamingBitrate` = `maxStaticBitrate` = 400 Мбит/с, чтобы сервер не транскодировал по битрейту.
|
||
- **ASS по умолчанию — главная причина транскода у других клиентов**: если клиент не рисует ASS, сервер прожигает их в картинку (полный транскод 4K HDR с тонмаппингом → тусклая картинка). ASS нужно рендерить на клиенте и не включать `alwaysBurnInSubtitleWhenTranscoding`.
|
||
- **`/Videos/ActiveEncodings`** удалять после остановки транскода.
|
||
- **Встроенная обложка** (mjpeg/png как видеопоток, 499 файлов в медиатеке): «видеодорожка» — первый поток `Video` с кодеком не из mjpeg/png/bmp/gif/webp. Обложку пропускать и при сопоставлении индексов Jellyfin с номерами треков движка: Jellyfin нумерует потоки глобально (индекс ffprobe), движки — по типу.
|
||
- **Сервер помнит дорожки прошлого просмотра** (`UserData.AudioStreamIndex/SubtitleStreamIndex`, если в профиле включено «Запоминать выбор»). Для таких фильмов языковые настройки пользователя не действуют.
|
||
- **Транскод DV-файла в fMP4 ломает ExoPlayer** (`FragmentedMp4Extractor: Top bit not zero`). Не исправлено, см. §10.
|
||
|
||
### 4.1 Сеть: LAN-адрес для видео
|
||
|
||
- **Симптом:** 4K-ремукс (66 Мбит/с) в Exo уходил в подзагрузку каждые 5–8 с.
|
||
- **Причина:** ТВ ходил на публичный адрес `https://jellyfin.example.com`. Split DNS резолвил его в LAN-адрес, но запрос шёл через reverse proxy с TLS на слабом CPU ТВ. Exo получал 40–115 Мбит/с, прямой `http://<адрес-сервера-в-LAN>:8096` даёт ~240 Мбит/с.
|
||
- **Решение:**
|
||
- поиск сервера в LAN: UDP broadcast на порт 7359, сообщение `Who is JellyfinServer?`. Берётся IP отправителя и порт 8096, проверяется `/System/Info/Public` → тот же `Id`;
|
||
- поле `LocalAddress` сервера нельзя брать как есть: за прокси сервер отдаёт в нём публичное имя;
|
||
- найденный LAN-адрес запоминается в аккаунте. Один `OkHttpClient` с перехватчиком переписывает запросы на публичный адрес в LAN-адрес. Если LAN не ответил за 1,5 с — запрос уходит на публичный адрес, и запускается новый поиск;
|
||
- ключи кэша картинок строятся на публичном адресе, чтобы не меняться дома и вне дома.
|
||
- **Итог:** 0 подзагрузок. **Минус:** токен в URL видео идёт по LAN открытым HTTP.
|
||
|
||
---
|
||
|
||
## 5. ExoPlayer (media3 1.11.1)
|
||
|
||
### 5.1 Настройки плеера (`ExoEngine`)
|
||
|
||
- `DefaultRenderersFactory`:
|
||
- `setEnableDecoderFallback(true)`;
|
||
- `setExtensionRendererMode(EXTENSION_RENDERER_MODE_ON)` — ffmpeg-аудио **после** MediaCodec, чтобы аппаратные декодеры и passthrough были главнее;
|
||
- свой `MediaCodecSelector` (§5.3).
|
||
- `DefaultTrackSelector`:
|
||
- `setTunnelingEnabled(false)` — tunneling ломает часть сценариев и не понадобился;
|
||
- `setAllowInvalidateSelectionsOnRendererCapabilitiesChange(true)`.
|
||
- **`DefaultLoadControl.setTargetBufferBytes` = ¼ кучи, не больше 64 МБ** + `setPrioritizeTimeOverSizeThresholds(false)`. По умолчанию media3 копит до ~145 МБ видео: при куче 192 МБ это OOM и тормоза от GC (§8). 64 МБ — это ~8 с 4K-ремукса 60 Мбит/с, по LAN хватает.
|
||
- В манифесте `android:largeHeap="true"`.
|
||
- **Ложные отказы `Tracks.Group.isSupported`**: без `allowExceedsCapabilities = true` дорожка помечается неподдерживаемой. Пример: HEVC Main10 L5.1 High tier 55 Мбит/с media3 считает `EXCEEDS_CAPABILITIES`, а декодер играет его корректно. Декодеры MTK заявляют возможности ниже реальных.
|
||
- Собственные экстракторы подключаются через свою `ExtractorsFactory` (`ExoExtractorsFactory`) в `DefaultMediaSourceFactory`.
|
||
|
||
### 5.2 Вывод видео: SurfaceView или TextureView
|
||
|
||
Это самое неочевидное открытие проекта.
|
||
|
||
- **SurfaceView = аппаратный видеослой.**
|
||
- Только через него работает HDR/DV и выводится 4K: UI-поверхность приложения всего 1920×1080.
|
||
- ТВ сам переключает частоту панели под видео (23,976 → 119,88 Гц, 25 → 100 Гц), хотя Android видит единственный режим 60 Гц. Рывков 24p нет.
|
||
- ТВ деинтерлейсит чересстрочное видео.
|
||
- **Но** на видеослой ТВ накладывает свою постобработку (резкость, контраст MediaTek PQ). Сжатое SDR-видео выглядит «жёстче», артефакты сжатия заметнее. Метаданные цвета ExoPlayer читает правильно (BT.709 limited), дело не в них.
|
||
- **TextureView = через GPU**, как обычная графика (так выводит VLC).
|
||
- Постобработки ТВ нет, SDR-картинка «мягче и естественнее» — проверено на глаз A/B-сравнением.
|
||
- Нет HDR, нет 4K (UI 1080p), нет деинтерлейса, частота UI 60 Гц → 3:2 judder на 24p и неровно на 25p.
|
||
- 4K HDR через GPU тормозит.
|
||
- **Правило AUTO (`ExoEngine`):** через GPU идёт только SDR, не чересстрочное, ≤1920×1088, с частотой, которая ровно делится в 60 Гц (`fitsUiRefreshRate`: 30, 60, 59,94). Всё остальное — видеослой. И только на ТВ Xiaomi (`Build.MANUFACTURER == "Xiaomi"` и `UI_MODE_TYPE_TELEVISION`); на других устройствах всегда видеослой: постобработка — особенность этого ТВ.
|
||
- Режим вывода хранится **в экземпляре движка**, а не статически. При переходе на другой движок внутри одного воспроизведения он переносится. Новый фильм начинается с AUTO или с настройки пользователя. Ошибка, которую мы сделали: статический режим «через GPU» перетекал на следующие файлы, и 4K шёл через GPU.
|
||
- Пользователь может переключить режим вручную (Авто / Видеослой ТВ / Через GPU) в «Информации о потоке → Дополнительно» и в настройках.
|
||
|
||
### 5.3 Dolby Vision
|
||
|
||
- media3 распознаёт DV в MKV (`BlockAdditionMapping` с `dvcC`/`dvvC`) и в MP4 и выбирает декодер `video/dolby-vision`.
|
||
- **Свой `MediaCodecSelector`.** Если он для `video/dolby-vision` возвращает пустой список, media3 через `getAlternativeCodecMimeType` берёт декодер базового слоя (HEVC/AVC/AV1). Это самый простой способ «выключить DV» и играть P8/P9/P10 как HDR10/HLG/SDR. P5 базового слоя не имеет, поэтому для него DV-декодер нужен всегда (`requiresDolbyVisionDecoder`).
|
||
- **На тестовом ТВ DV-декодер `c2.mtk.dvhe.st.decoder` работает:** ТВ показывает значок Dolby Vision, DispService пишет `Hdr Dolby`. Поэтому DV-декодер включён по умолчанию, если дисплей сообщает `HDR_TYPE_DOLBY_VISION`.
|
||
- Сначала картинка через DV-декодер казалась «тусклее», чем базовый слой HDR10. Оказалось, это режим изображения Dolby Vision в настройках ТВ, а не поломка. При сравнении учитывать режимы изображения ТВ.
|
||
- **DV + HDR10+** (известный баг media3 на MediaTek, #1895) на этом ТВ не воспроизвёлся: играет и через DV-декодер, и базовым слоем.
|
||
- **DV P7 (BD-remux)**: на этом ТВ в рантайме декодера нет. media3 откат на HEVC для P7 явно запрещает (PR #3160 не смержен). Нужно переписать формат дорожки на HEVC (декодер HEVC игнорирует NAL 62/63) — в медиатеке P7 нет, вслепую не делали. До тех пор P7 без DV-декодера → VLC (базовый слой).
|
||
- Выбор «DV-декодер / базовый слой» — переключатель в UI и настройках, показывается только для DV-файлов и только если у ТВ есть DV-декодер и DV-экран.
|
||
|
||
### 5.4 HDR10 / HDR10+ / HLG
|
||
|
||
- Работает «из коробки» через SurfaceView: статические метаданные идут в `KEY_HDR_STATIC_INFO`, HDR10+ для HEVC/AV1 — внутри потока. Встроенный ТВ включает HDR-режим сам по метаданным декодера.
|
||
- Через TextureView HDR не выводится никогда — поэтому HDR всегда на видеослое.
|
||
- `screencap` HDR/DV-видеослой не захватывает (чёрное). SDR — захватывает и через GPU, и (для AVC) через видеослой. Оценку HDR-картинки даёт только человек.
|
||
|
||
### 5.5 Исправленные экстракторы media3
|
||
|
||
Все подключаются через `ExoExtractorsFactory` и проверены на реальных файлах. При обновлении media3 их нужно перепроверять.
|
||
|
||
| Проблема | Симптом | Причина | Исправление |
|
||
|---|---|---|---|
|
||
| **MKV с цепочкой SeekHead** (после mkvpropedit/MakeMKV) | файл «неперематываемый», seek откатывает к началу | первый `SeekHead` ссылается только на второй `SeekHead` в конце файла, а уже тот — на `Cues`. `MatroskaExtractor` по цепочке не идёт → `SeekMap.Unseekable` | `SeekHeadChainExtractor`: через peek читает первую SeekHead; если в ней только ссылка на SeekHead — `RESULT_SEEK` к хвостовой, берёт позицию `Cues`, возвращается на 0 и на лету подменяет в первой SeekHead ID и позицию (`PatchingInput`). Обычные файлы не получают лишних запросов |
|
||
| **AC3/E-AC3/DTS в AVI** | рывки, постоянная буферизация, 173 пропуска за 16 с | AVI-куски по 1280 байт не совпадают с кадрами AC3 (1024 байта). `AviExtractor` отдаёт кусок как кадр, аудиосинк считает 32 мс вместо 40 → `UnexpectedDiscontinuityException` | `AviAudioFramingExtractor`: режет поток на настоящие кадры. Время кадра = время первого куска после старта/перемотки + байты × длительность кадра / размер кадра (время куска у `AviExtractor` в начале файла расходится на 0,5 с) |
|
||
| **MPEG-2 в VOB/MPEG-PS** | ~1–1,5 к/с, при этом счётчик пропусков молчит | `PsExtractor` даёт PES без PTS время **0**, а не «неизвестно»; `H262Reader` достраивает времена «предыдущее + кадр» в порядке декодирования. 14 из 15 кадров GOP получали 0, рендерер их **пропускал как «до старта»** (skipped, не dropped) | `Mpeg2TimestampFixExtractor`: читает `temporal_reference` из заголовка картинки и ставит время `ключевой + (tr − tr ключевого) × длительность кадра`. Частота кадров — из `frame_rate_code` в CSD |
|
||
| **DVD private stream 1 (0xBD)** | «квакающий» звук через passthrough | `PsExtractor` отдаёт 0xBD в `Ac3Reader` как есть: с 4-байтовым заголовком подпотока и со всеми подпотоками (субтитры, DTS, LPCM, вторая AC3) | Форк `PsExtractor`/`PsBinarySearchSeeker`/`PsDurationReader` (Apache 2.0, `engine-exo/src/main/java/.../exo/ps/`): `DvdPsExtractor` срезает заголовок подпотока, каждому AC3-подпотоку (0x80–0x87) даёт свой `Ac3Reader` и дорожку, ищет дорожки в окне 1 МБ. PES без PTS → `C.TIME_UNSET` |
|
||
|
||
Урок: **для диагностики нужен счётчик «кадров/с на экране» и «пропущено декодером»** (`DecoderCounters.skippedOutputBufferCount`), а не только «пропущено кадров» (dropped). Проблему MPEG-PS поймал именно он.
|
||
|
||
### 5.6 ASS/SSA
|
||
|
||
- `ass-media` 0.5.1 (libass), `AssRenderType.OVERLAY_OPEN_GL`, кэш 32 МБ, рендер не больше 1080p.
|
||
- `AssSubtitleView` кладётся **внутрь `AspectRatioFrameLayout`**, чтобы раскладка шла относительно кадра, а не экрана.
|
||
- `AssMatroskaExtractor` библиотеки нужно обернуть в `SeekHeadChainExtractor`, иначе теряется исправление перемотки. Внешние ASS — через `setSubtitleParserFactory`.
|
||
- Библиотека собрана под media3 1.8.0 и лезет рефлексией в приватные поля `MatroskaExtractor` (`extractorOutput`, `subtitleSample`): при обновлении media3 проверять, при R8 нужны keep-правила.
|
||
- Проверено: 720p60 со стилизованными ASS — 60 к/с, 0 пропусков. После этого ASS-файлы остаются в Exo, VLC для них не нужен.
|
||
|
||
### 5.7 Звук
|
||
|
||
- **Passthrough** определяется через `AudioTrack.isDirectPlaybackSupported` для каждого `ENCODING_*` (`DeviceCapabilitiesProbe`). На встроенных динамиках Xiaomi passthrough проходит для AC3, E-AC3, JOC, TrueHD, DTS, DTS-HD, AC4, MAT — ТВ декодирует их сам (Dolby MS12). Исследование ожидало, что TrueHD и DTS-HD «из приложения не пройдут», — на этом ТВ проходят.
|
||
- **ffmpeg-аудио** (`media3-ffmpeg-decoder`) нужен для устройств без passthrough (телефоны, саундбары без декодера). С media3 1.11.1 работает (TrueHD и DTS декодировались при принудительном PREFER). APK вырос на ~40 МБ → нужны ABI-сплиты.
|
||
- Многоканальный PCM ExoPlayer сам направляет правильно (в отличие от VLC, §6.3).
|
||
|
||
### 5.8 Чересстрочное видео
|
||
|
||
- MPEG-2 декодирует аппаратный `c2.mtk.mpeg2.decoder`, а на видеослое ТВ деинтерлейсит сам (DispService: `interlace 1, scanType 1`). Через GPU видна «гребёнка», поэтому чересстрочное в AUTO всегда идёт на видеослой.
|
||
- **Исключение — DVD (MPEG-PS) с нижним полем первым**: гребёнка в движении остаётся и на видеослое. MKV с верхним полем первым — чисто. Jellyfin порядок полей не сообщает, поэтому правило сделано по контейнеру (`DVD_COMBING` → VLC с yadif). Точнее было бы читать `top_field_first` из picture coding extension.
|
||
|
||
### 5.9 Частота кадров (24p/25p)
|
||
|
||
- На этом ТВ ничего делать не нужно, если видео идёт через видеослой: ТВ сам переключает панель (проверено по DispService `PanelTiming`/`outFmt`).
|
||
- Через GPU — 60 Гц UI, отсюда правило AUTO из §5.2.
|
||
- На ТВ-приставках с HDMI нужно самим ставить `preferredDisplayModeId` и ждать `onDisplayChanged`: media3 сам умеет только бесшовную смену через `Surface.setFrameRate` (см. исследование [`research/media3.md`](research/media3.md) §3).
|
||
|
||
### 5.10 Стиль субтитров и диагностика
|
||
|
||
- Стиль текстовых субтитров (размер, цвет, фон) — через `SubtitleView` на лету. Пока виден OSD, субтитры поднимаются над ним.
|
||
- `AnalyticsListener` для статистики: декодер, кадры/с на экране, пропущено, пропущено декодером, срывы звука (`onAudioUnderrun`), число буферизаций (после первых 3 с от загрузки или перемотки).
|
||
|
||
---
|
||
|
||
## 6. libVLC (3.7.7 / libVLC 3.0.25)
|
||
|
||
### 6.1 Опции
|
||
|
||
```
|
||
--network-caching=3000
|
||
--audio-time-stretch
|
||
--no-sub-autodetect-file
|
||
--deinterlace=-1 # авто: только для кадров, помеченных interlaced
|
||
--deinterlace-mode=yadif
|
||
-v # -vv для отладки: в logcat видны полные HTTP-заголовки
|
||
```
|
||
|
||
Плюс опции freetype для стиля субтитров (§6.6).
|
||
|
||
### 6.2 Сеть: локальный прокси (`LoopbackProxy`) — обязательно
|
||
|
||
**Проблема.** libVLC ходит по HTTPS через GnuTLS. Через reverse proxy (openresty, HTTP/1.1):
|
||
- после Range-запроса к концу MKV (чтение индекса) — «connection failed»;
|
||
- перемотка к концу файла ждёт ~10 с;
|
||
- иногда prefetch уходит в бесконечный цикл: 97 % CPU, >1 ГБ памяти;
|
||
- `--http-reconnect` делает только хуже;
|
||
- напрямую по `http://<адрес-сервера-в-LAN>:8096`, без reverse proxy, зацикливание не воспроизводилось.
|
||
|
||
Рассматривались и другие пути: HTTP/2 на reverse proxy, старый http-модуль VLC.
|
||
|
||
Произвольных HTTP-заголовков libVLC не умеет, поэтому токен можно передать только в URL.
|
||
|
||
**Решение — HTTP-прокси внутри приложения на OkHttp.**
|
||
- VLC ходит только на `http://127.0.0.1:<port>/<random-key>/<путь сервера>?<query без ApiKey>`.
|
||
- Прокси повторяет запрос через OkHttp (общий пул соединений) и ставит токен заголовком `Authorization`.
|
||
- Путь сервера сохраняется, поэтому относительные URL в HLS-плейлистах тоже идут через прокси. Внешние субтитры (`addSlave`) — тоже.
|
||
- Каждый ответ с `Connection: close`: одно соединение на запрос, закрытие сокета отменяет запрос к серверу.
|
||
- Сокет явно на `127.0.0.1`: `InetAddress.getLoopbackAddress()` на Android даёт `::1` → Connection refused.
|
||
- `VlcEngine.release()` первым делом закрывает прокси, тогда libVLC освобождается за ~36 мс.
|
||
- Бонус: TLS, пользовательские CA и `network_security_config` работают как у остального приложения.
|
||
- Запись `http stream: connection failed` в логе VLC после этого — штатная (закрытие соединения).
|
||
|
||
**Результат:** перемотка по сети ~0,1 с, зависаний нет.
|
||
|
||
### 6.3 Кэш недавних байт (`RecentBytesCache`)
|
||
|
||
- Фильтр `prefetch` VLC 3.0 выбрасывает прочитанное, как только заполнится буфер 16 МБ (`modules/stream_filter/prefetch.c`). Любой шаг назад становится новым Range-запросом с повторной закачкой ~16–20 МБ.
|
||
- Демуксер AVI (OpenDML) делает такой шаг каждые ~1,5 с. Файл 2 Мбит/с без кэша качался на 40–80 Мбит/с при 191 % CPU.
|
||
- Кэш в прокси: 32 МБ на движок, блоки по 256 КБ. С ним с сервера идёт только новое (≈ битрейт файла), CPU ~20 %.
|
||
- `If-Range`/`If-Unmodified-Since` от VLC в пути через кэш игнорируются — безопасно, пока файл не меняется во время просмотра.
|
||
|
||
### 6.4 Аппаратное декодирование
|
||
|
||
- Модуль `mediacodec` VLC при старте опрашивает все декодеры. На MediaTek каждый `getCapabilitiesForType` бросает исключения, и **опрос занимает ~45 с**.
|
||
- Поэтому `setHWDecoderEnabled(true)` только для видео больше 1080p (4K там программно не потянуть). Всё ≤1080p VLC декодирует программно.
|
||
- Программно на 4×A73 (32-bit) VLC тянет 1080p 24–30 к/с и SD 50 к/с (деинтерлейс DVD). 720p60 и 1080i50 (yadif удваивает кадры) — с пропусками. Отсюда правило `SOFTWARE_DECODE_HEAVY` в §3.3.
|
||
|
||
### 6.5 Звук
|
||
|
||
- **Многоканальный PCM на этом ТВ беззвучен.** Устройство `pcm` (по умолчанию в libvlc-android) отдаёт 6-канальный PCM. Android направляет его в DIRECT-выход (`AUDIO_OUTPUT_FLAG_DIRECT|DEEP_BUFFER`): трек создаётся и удаляется, `Total writes: 0`. Без звука были AC3 5.1 и HE-AAC 5.1.
|
||
- Решение (`VlcEngine.applyAudioOutput`, пересчитывается при смене дорожки):
|
||
- кодек есть в `passthroughEncodings` → `forceAudioDigitalEncodings(int[])`, passthrough, выход OFFLOAD, ТВ декодирует сам;
|
||
- иначе → `setAudioOutputDevice("stereo")` — сведение в 2 канала через обычный микшер.
|
||
- Устройства модуля `android_audiotrack`: `stereo` (до 2 каналов), `pcm` (до 8), `encoded[:mask]` (passthrough).
|
||
- **При транскоде** выходной аудиокодек нужно брать из `AudioCodec` в URL транскода. Ошибка, которую мы сделали: VLC включал passthrough исходного TrueHD, а сервер отдавал AAC.
|
||
- Минус: многоканальный не-passthrough звук (AAC/FLAC/Opus 5.1) сводится в стерео. На системе 5.1 по HDMI это потеря — выбирать `pcm`/`stereo` по реальному выходу.
|
||
|
||
### 6.6 Субтитры
|
||
|
||
- ASS, PGS, VobSub, DVB VLC рисует сам.
|
||
- Стиль текстовых субтитров задаётся только **опциями freetype при создании `LibVLC`**: `--freetype-rel-fontsize`, `--freetype-color`, `--freetype-background-opacity`, `--freetype-shadow-opacity`, `--freetype-outline-thickness`. Поэтому `LibVLC` создаётся лениво, а смена стиля при включённых субтитрах перезапускает VLC с той же позиции.
|
||
- **Внешние субтитры:** слейв-дорожка появляется позже первого `ESAdded`. Выбор нужно повторять на каждом `ESAdded`, пока нужная дорожка не появится (`pendingSubtitle`), иначе внешние субтитры не включаются.
|
||
|
||
### 6.7 Перемотка в MPEG-PS
|
||
|
||
- VLC перематывает MPEG-PS по среднему битрейту: старт с 30:00 попадал на 26:25.
|
||
- `VlcEngine.checkSeek`: через 1,5 с после перемотки или старта сравнивает, куда попали, и перематывает повторно с поправкой (до 3 раз, допуск 5 с). 30:00 → 30:02 за 2 поправки.
|
||
|
||
### 6.8 Жизненный цикл
|
||
|
||
- `MediaPlayer.release()` и `LibVLC.release()` **блокируют на зависшем потоке** → освобождать только в фоновом потоке.
|
||
- Свой `LibVLC` на каждое воспроизведение.
|
||
- `VLCVideoLayout` создавать с контекстом **Activity**, иначе видео выводится в нативном размере в углу экрана.
|
||
- Буфер для таймлайна libVLC не сообщает.
|
||
|
||
---
|
||
|
||
## 7. Сессия: fallback и сторожа (`PlaybackSession`)
|
||
|
||
- **Сторож первого кадра:** нет кадра за 60 с → ошибка `NO_VIDEO_OUTPUT` → следующий движок.
|
||
- **Сторож застоя** (тик 1 с):
|
||
- движок в READY и `playWhenReady`, а позиция не растёт ≥ 8 с, **или** за 10 с пропущено > 25 % кадров → `STALLED` → следующий движок;
|
||
- 5 с после перемотки не судим;
|
||
- долгая буферизация застоем **не** считается: это сеть, другой движок быстрее не скачает;
|
||
- если движок выбран вручную — только запись в лог.
|
||
- Проверено: AVI с AC3 до исправления экстрактора — переход exo → vlc на 611 с; ложных срабатываний при перемотках нет.
|
||
- **Неперематываемый файл:** `PlaybackEngine.isSeekable` (в Exo: `currentTimeline.isEmpty || isCurrentMediaItemSeekable`). При seek в таком файле сессия сразу запускает следующий движок с целевой позиции.
|
||
- **Позиция до первого кадра.** Пока движок не показал первый кадр, его позиция равна 0 (или отсчитывается от начала транскода). Для передачи другому движку брать позицию, с которой движок запускали. Иначе быстрое переключение или fallback начинает фильм сначала.
|
||
- **Поколения.** Каждый запуск движка увеличивает `generation`. События и сторожа старого движка сверяют поколение и игнорируются.
|
||
- При переходе на другой движок переносятся режим вывода и выбор DV.
|
||
- Если не справился ни один движок — транскод (`enableDirectPlay = false`).
|
||
- Отчёт `Stopped` уходит через `GlobalScope`. При убийстве процесса сессия на сервере «висит» — некритично.
|
||
|
||
---
|
||
|
||
## 8. Память
|
||
|
||
- Куча Java на ТВ — 192 МБ. ExoPlayer `DefaultLoadControl` по умолчанию копит до ~145 МБ (в дампе 2222 блока по 64 КБ). Возле лимита GC молотит, плеер тормозит, потом `OutOfMemoryError` (в том числе внутри JNI VLC).
|
||
- Исправление: `largeHeap="true"` (512 МБ) + буфер ¼ кучи, но не больше 64 МБ. После: 10 кругов переключения движков на 4K AV1 без падений, куча ~78 МБ.
|
||
- Свой экземпляр движка на воспроизведение, освобождение старого до создания нового.
|
||
- PGS в media3 декодирует все дорожки, даже невыбранные (#2667) — следить за памятью на файлах с десятком PGS.
|
||
|
||
---
|
||
|
||
## 9. Особенности телевизора MediaTek (Xiaomi MT9689)
|
||
|
||
| Особенность | Что делать |
|
||
|---|---|
|
||
| **«Засор» видеоконвейера.** Если процесс убит (`am force-stop`, вылет по OOM) во время воспроизведения, поток остаётся висеть в DispService (`srcT[0]`). После этого любой 4K HEVC получает от `c2.mtk.hevc.decoder` `NO_MEMORY` (0xfffffff4) — и в Exo, и в VLC — до перезагрузки ТВ | Не допускать вылетов (§8). При штатном выходе поток освобождается. **Если 4K вдруг перестал декодироваться и в логе `NO_MEMORY` — сначала перезагрузить ТВ.** Тестировать только со штатным выходом |
|
||
| **Застрявший звук.** После `force-stop` посреди AC3 passthrough аудио-HAL Xiaomi продолжал играть звук и после закрытия приложения | Закрывать плеер «Назад», force-stop только после этого |
|
||
| ТВ иногда целиком работает втрое медленнее (UI, декодеры) до перезагрузки; признаки — большой аптайм, занятая подкачка | Перед серьёзными замерами перезагружать ТВ |
|
||
| Постобработка изображения на видеослое | §5.2: SDR ≤1080p через TextureView |
|
||
| Многоканальный PCM беззвучен | §6.5: passthrough или стерео |
|
||
| `MediaCodecList` отличается от `media_codecs.xml` (P7 в XML есть, в рантайме нет) | Опрашивать только рантайм |
|
||
| VC-1 зарегистрирован как `video/VC1` (регистр), media3 ищет `video/wvc1` | Сравнивать MIME без учёта регистра; VC-1 в Exo только после явной проверки |
|
||
| HEVC заявлен ниже реального (L5.1 High tier → `EXCEEDS_CAPABILITIES`) | `allowExceedsCapabilities` и не доверять пределам |
|
||
| AVC High10 есть аппаратно | Проверять `AVCProfileHigh10` в профилях декодера, а не считать Hi10P программным |
|
||
| `c2.mtk.mpeg4.decoder` — максимум 1344×1152 | DivX 1080p — не аппаратно |
|
||
| Частота панели переключается сама только на видеослое | §5.9 |
|
||
| Порт беспроводной отладки меняется после перезагрузки | Классический `adb connect <ip-тв>:5555` тоже был открыт |
|
||
|
||
### 9.1 Как опрашивать устройство (`DeviceCapabilitiesProbe`)
|
||
|
||
- `MediaCodecList(REGULAR_CODECS)`: декодеры, аппаратный ли (`isHardwareAccelerated`), профили (`CodecProfileLevel`), пределы размеров (`VideoCapabilities.isSizeSupported`).
|
||
- HDR-типы дисплея (DV, HDR10, HDR10+, HLG): на API 34+ из режима дисплея, раньше — `Display.hdrCapabilities.supportedHdrTypes`.
|
||
- Passthrough: `AudioTrack.isDirectPlaybackSupported(AudioFormat(encoding, 48000, 7.1 / 5.1 / стерео), attrs)` для AC3, E-AC3, E-AC3 JOC, TrueHD, DTS, DTS-HD, AC4, MAT.
|
||
- Результат кэшируется на запуск (и при смене дисплея).
|
||
|
||
---
|
||
|
||
## 10. Что осталось нерешённым
|
||
|
||
- **Транскод DV-файла в ExoPlayer падает**: `FragmentedMp4Extractor: Top bit not zero: -2048` на HLS fMP4 от сервера; повтор тоже падает, VLC не пробуется. SDR HEVC транскодом играет. Затрагивает выбор качества ниже оригинала для DV-файлов. Вероятные пути: транскод в `ts` вместо fMP4 для DV или fallback на VLC при ошибке транскода.
|
||
- **DV P7** без DV-декодера: переписывать формат на HEVC в Exo (нет тестовых файлов).
|
||
- **SDR 4K** идёт через видеослой с постобработкой ТВ (GPU 4K не тянет). Можно поискать способ отключить PQ у Xiaomi.
|
||
- **Порядок полей** определяется по контейнеру, а не по потоку.
|
||
- Многоканальный не-passthrough звук в VLC сводится в стерео.
|
||
- Сервер с самоподписанным сертификатом не проверен (настройка доверия пользовательским CA есть).
|
||
- Буфер на таймлайне только у Exo.
|
||
- Exo → VLC при неперематываемом файле: обратно на Exo не возвращаемся, нажатия во время переключения теряются.
|
||
|
||
---
|
||
|
||
## 11. Как тестировали
|
||
|
||
### 11.1 Режим обхода и варианты (только debug-сборка)
|
||
|
||
- **Варианты** — цифрами с пульта или из меню:
|
||
- `0` — авто;
|
||
- `1` — Exo, видеослой (DV как базовый слой);
|
||
- `2` — Exo через GPU;
|
||
- `3` — VLC;
|
||
- `4` — Exo, видеослой, DV-декодер;
|
||
- `9` — транскод.
|
||
|
||
Значок в углу: «N ДВИЖОК · вывод», «авто/вручную», время, пропуски. Так можно сравнивать один и тот же момент фильма в разных режимах.
|
||
- **Режим обхода** — список из 38 файлов, жадно покрывающих 155 признаков медиатеки: контейнер, кодек/битность, разрешение, HDR-тип, fps, interlaced, аудиокодек/каналы, субтитры, плюс пары «контейнер × кодек», «кодек × HDR». Генератор — `tools/tour/build_tour.py` (по API сервера). Файлы играются с середины с заранее выбранными дорожками:
|
||
- ↑ — следующий вариант;
|
||
- ↓ — следующий файл (запуск через 2 с после последнего нажатия, чтобы быстрое листание не рвало плеер);
|
||
- ←/→ — перемотка.
|
||
- Обход проходили вручную и оценивали глазами и ушами. Почти все находки §5–§9 пришли из двух таких прогонов.
|
||
|
||
### 11.2 Диагностика
|
||
|
||
- Панель «Информация о потоке»: режим (DirectPlay/транскод и причина по-русски), контейнер, видео, частота, битрейт, аудио, субтитры, движок, декодер, вывод звука, буфер, пропуски, кадров/с на экране, пропущено декодером, срывы звука, буферизации, строка прокси VLC.
|
||
- `tools/tv/env.sh` (`source` перед работой):
|
||
|
||
| Функция | Что делает |
|
||
|---|---|
|
||
| `play <id> [exo\|vlc]` | запустить воспроизведение через intent |
|
||
| `k KEY…` | нажатия пульта |
|
||
| `shot name` | скриншот |
|
||
| `plog` | журнал `PlaybackSession` (выбор движка, причины, fallback) |
|
||
| `sess` | что играет по данным сервера: позиция, PlayMethod |
|
||
| `aout` | реально ли звук уходит на выход (`dumpsys media.audio_flinger`: `Total writes`) |
|
||
| `vproxy` | журнал прокси VLC: диапазоны, попадания в кэш, сколько скачано |
|
||
|
||
- Полезные системные команды:
|
||
- `adb shell dumpsys media.audio_flinger` — выходы, флаги DIRECT/OFFLOAD, `Total writes`;
|
||
- DispService в logcat — `Hdr Dolby`, `PanelTiming`, `interlace`, `srcT[…]` (висящие потоки);
|
||
- `dumpsys display` — HDR-типы и режимы;
|
||
- `adb logcat -v raw -s VLC` с `-vv` — HTTP-заголовки libVLC.
|
||
- Отладочный запуск: `adb shell am start -S -n <pkg>/.MainActivity --es item <id> [--es engine exo|vlc] [--ez play true] [--el start <ms>]`.
|
||
|
||
### 11.3 Итог тестов (проверено на глаз и на слух)
|
||
|
||
| Что | Результат |
|
||
|---|---|
|
||
| Фильм HEVC 4K HDR10 55 Мбит/с, TrueHD Atmos, ASS forced | Exo, видеослой: «отлично», 0 пропусков (после перехода на LAN-адрес) |
|
||
| Фильм DV P8.1 + HDR10+, AC3 | Exo, DV-декодер: значок DV, «цвета хорошие» |
|
||
| SDR ≤1080p 30/60 к/с | Exo через GPU «намного лучше», чем видеослой |
|
||
| 24p/25p | видеослой: «нормально и плавно» |
|
||
| AVI Xvid + AC3 | Exo после исправления экстрактора, 0 разрывов |
|
||
| DVD 576i (VOB) | VLC (yadif); Exo после исправлений тоже играет, но с гребёнкой |
|
||
| Звук в VLC (AC3 passthrough, AAC → стерео) | «звук теперь везде норм» |
|
||
| MKV с цепочкой SeekHead | Exo перематывает |
|
||
| ASS 720p60 | Exo, 60 к/с, 0 пропусков |
|
||
|
||
---
|
||
|
||
## 12. Чек-лист для своего приложения
|
||
|
||
1. Два движка за одним узким интерфейсом. Дорожки адресовать индексами сервера. Свой DeviceProfile на движок.
|
||
2. Опрашивать устройство в рантайме: `MediaCodecList`, `HdrCapabilities`, `isDirectPlaybackSupported`. Не доверять `media_codecs.xml` и заявленным пределам (`allowExceedsCapabilities`).
|
||
3. Выбор движка по весам потерь; HDR дороже всего. Пересчитывать при смене дорожки.
|
||
4. Сторожа первого кадра и застоя; fallback с позиции запуска, если кадра ещё не было; транскод последним.
|
||
5. ExoPlayer: ограничить буфер (¼ кучи, ≤64 МБ), `largeHeap`, decoder fallback, ffmpeg-аудио после MediaCodec, tunneling выключен.
|
||
6. Свой `MediaCodecSelector` для DV (пустой список → базовый слой). DV-декодер — только при DV-дисплее.
|
||
7. HDR, 4K, чересстрочное и 24/25/50 к/с — всегда SurfaceView. SDR ≤1080p 30/60 к/с — по устройству (на Xiaomi через TextureView).
|
||
8. Исправленные экстракторы: SeekHead-цепочка в MKV, AC3 в AVI, MPEG-2 PTS в PS, DVD private stream.
|
||
9. ASS через `ass-media` в `AspectRatioFrameLayout`, без серверного прожига.
|
||
10. libVLC только через локальный HTTP-прокси с кэшем недавних байт; токен — заголовком из прокси; сокет на `127.0.0.1`.
|
||
11. libVLC: аппаратное декодирование только >1080p на MediaTek; звук — passthrough или `stereo`, никогда многоканальный `pcm` на ТВ; release в фоне; `VLCVideoLayout` с Activity-контекстом; повтор выбора внешних субтитров на каждом `ESAdded`; поправка перемотки в MPEG-PS.
|
||
12. Видео и картинки дома — с LAN-адреса сервера (UDP 7359), а не через HTTPS reverse proxy.
|
||
13. Авторизация Jellyfin 12 — только `Authorization: MediaBrowser …` или `ApiKey`; вызовы SDK — на IO.
|
||
14. В диагностике обязательно «кадров/с на экране» и «пропущено декодером», а не только dropped.
|
||
15. Тестировать на реальном ТВ, по обходу, покрывающему признаки медиатеки, со штатным выходом из плеера. HDR оценивает только человек: `screencap` его не видит.
|