1694 lines
402 KiB
Markdown
1694 lines
402 KiB
Markdown
# Исследование: нативный Kotlin-клиент Jellyfin для Android и Android TV на libVLC
|
||
|
||
Дата сбора: 2026-10-11. Документ собран из шести параллельных исследований (веб-поиск, чтение исходников, Maven/GitHub API). Все ссылки проверялись на доступность; непроверенные помечены «(не проверено)».
|
||
|
||
## Содержание
|
||
|
||
1. [Jellyfin API, официальные SDK, протокол](#часть-1-jellyfin-api-официальные-sdk-и-протокольная-документация)
|
||
2. [Существующие open-source клиенты: что заимствовать](#часть-2-обзор-open-source-jellyfin-клиентов-и-смежных-проектов)
|
||
3. [libVLC на Android: артефакты, API, субтитры, стриминг, лицензии](#часть-3-libvlc-на-android-как-движок-воспроизведения)
|
||
4. [Android-платформа: Compose for TV, медиа-интеграция, дистрибуция](#часть-4-android-платформа)
|
||
5. [Доменный слой воспроизведения: DeviceProfile, транскодирование, субтитры, изображения, тестовые медиа](#часть-5-доменный-слой-воспроизведения)
|
||
6. [Инструментарий разработки: сборка, эмуляторы Android TV с пультом, просмотр UI](#часть-6-инструментарий-разработки)
|
||
7. [Гибридный движок воспроизведения, тестовый ТВ, план прототипа](#часть-7-гибридный-движок-воспроизведения-дополнение-от-2026-10-12)
|
||
|
||
## Ключевые выводы (сводка)
|
||
|
||
- **Сервер и SDK.** Актуальный сервер — Jellyfin 12.2 (октябрь 2026, схема версий сменилась с 10.x на 12.x). Официальный `jellyfin-sdk-kotlin` 1.9.2 (LGPL-3.0, Ktor) покрывает API, discovery и WebSocket; использовать его как есть. С 12.0 legacy-авторизация выключена: только заголовок `Authorization: MediaBrowser ...` и query-параметр `ApiKey` там, где заголовок невозможен (URL для libVLC).
|
||
- **libVLC.** Артефакт `org.videolan.android:libvlc-all:3.7.7` (обновляется ежемесячно; 4.0 только EAP). Нативные библиотеки 40–63 МБ на ABI, поэтому только AAB или ABI-splits. Произвольных HTTP-заголовков нет, TLS через GnuTLS (нужен обработчик `Dialog`), HLS-seek слабый, Dolby Vision без RPU. Сильные стороны: ASS/PGS/VobSub рендерит сам, passthrough, любые контейнеры.
|
||
- **Референсы.** Ни один живой Kotlin-клиент не использует libVLC как основной движок; официальный Android TV клиент удалил его в 2024 из-за плохого управления жизненным циклом и размера APK. Лучшие образцы: Swiftfin (DeviceProfile для VLC), jellyfin-androidtv (архитектура playback-модулей, профиль под Android-железо), Findroid (`BasePlayer` поверх стороннего движка), Wholphin (Compose for TV без Leanback).
|
||
- **UI для TV.** Leanback официально deprecated, для нового проекта только Compose for TV (`androidx.tv:tv-material:1.1.0`, stable). TV-специфичные lazy-списки удалены, используются обычные `LazyRow/LazyColumn` с `BringIntoViewSpec`. libVLC можно обернуть в media3 `SimpleBasePlayer`/`BasePlayer` и отдать в `MediaSession` для Now Playing, PiP и медиакнопок; готовые шаблоны есть в jellyfin-androidtv и Findroid.
|
||
- **Лицензия.** Бинарный libVLC де-факто GPLv3-совместим, поэтому для приложения брать GPL-2.0-or-later или GPL-3.0, не GPL-2.0-only. Имя «Jellyfin» и логотип в названии и иконке использовать нельзя.
|
||
- **Инструменты.** Android Studio 2026.2.1, AGP 9.4, Gradle 9.8, Kotlin 2.4, Compose for TV `tv-material 1.1.0`. Android TV эмулятор управляется стрелками клавиатуры и `adb shell input keyevent`; рецепт запуска из командной строки в части 6.
|
||
|
||
---
|
||
|
||
# Часть 1. Jellyfin API, официальные SDK и протокольная документация
|
||
|
||
_Исходный заголовок: Jellyfin: API сервера, официальные SDK и протокольная документация — отчёт для нового Kotlin-клиента (Android + Android TV, libVLC)
|
||
|
||
Дата исследования: 2026-10-11. Все URL ниже были проверены (HTTP 200 / содержимое прочитано), кроме помеченных «(не проверено)».
|
||
|
||
## 0. Самое важное — контекст версий на октябрь 2026
|
||
|
||
| Что | Версия / дата | Источник |
|
||
|---|---|---|
|
||
| Jellyfin Server **12.2** (текущий stable) | 05.10.2026 | [GitHub release v12.2](https://github.com/jellyfin/jellyfin/releases/tag/v12.2) |
|
||
| Jellyfin 12.1 | 15.09.2026 | [GitHub release v12.1](https://github.com/jellyfin/jellyfin/releases/tag/v12.1) |
|
||
| Jellyfin **12.0** (бывший «10.12», смена схемы версий) | 07.09.2026 | [Release post 12.0](https://jellyfin.org/posts/jellyfin-release-12.0/) |
|
||
| Jellyfin 10.11.0 | 19.10.2025 | [Release post 10.11.0](https://jellyfin.org/posts/jellyfin-release-10.11.0/) |
|
||
| Jellyfin 10.10.0 | 26.10.2024 | [Release post 10.10.0](https://jellyfin.org/posts/jellyfin-release-10.10.0/) |
|
||
| Jellyfin 10.9.0 | 11.05.2024 | [Release post 10.9.0](https://jellyfin.org/posts/jellyfin-release-10.9.0/) |
|
||
| jellyfin-sdk-kotlin **1.9.2** (API 12.2.0, min server 12.0.0) | 06.10.2026 | [Releases](https://github.com/jellyfin/jellyfin-sdk-kotlin/releases), Maven Central metadata |
|
||
| jellyfin-sdk-kotlin 1.8.12 (ветка для 10.11.x; min 10.11.0) | 26.07.2026 | там же |
|
||
|
||
Ключевые следствия для нового клиента:
|
||
1. **Legacy-авторизация выключена по умолчанию с 12.0** (`X-Emby-Authorization`, `X-Emby-Token`, `X-MediaBrowser-Token`, query `api_key`, префиксы `/emby/*` и `/mediabrowser/*` удалены). Нужно использовать только `Authorization: MediaBrowser ...` и query-параметр `ApiKey` (с большой буквы) там, где заголовок невозможен (например, URL для libVLC).
|
||
2. HLS-эндпоинты (`master.m3u8` и др.) с 12.0 **скрыты из OpenAPI** — их нет в сгенерированном SDK; нужно использовать `MediaSourceInfo.TranscodingUrl` из ответа `PlaybackInfo`.
|
||
3. SDK 1.9.x несовместим с серверами ≤10.11; если хотите поддерживать 10.11 — придётся собирать на 1.8.x или писать свой клиент поверх OpenAPI.
|
||
4. В SDK 1.9 (под API 12) **переименованы классы операций** относительно старых примеров: `PlayStateApi.reportPlayback*` → `SessionApi.reportPlayback*`, `ItemsApi` → `LibraryApi` (`getItems`, `getResumeItems`, `getLatestMedia`), `TvShowsApi` → `ShowApi`, `VideosApi` → `VideoApi`, `UserViewsApi` → `UserViewApi`, `DynamicHlsApi` отсутствует. Проверено по Dokka 1.9.x.
|
||
|
||
---
|
||
|
||
## 1. Официальная документация API, аутентификация, OpenAPI
|
||
|
||
**Важно:** на jellyfin.org **нет** отдельного раздела «API docs» (`/docs/general/server/api/` → 404). Официальные источники по API — это: интерактивный ReDoc на api.jellyfin.org, сам OpenAPI JSON, документация SDK (kotlin-sdk.jellyfin.org / typescript-sdk.jellyfin.org), release-посты и gist мейнтейнера SDK Niels van Velzen.
|
||
|
||
- [api.jellyfin.org (ReDoc)](https://api.jellyfin.org/) — официальный интерактивный справочник API (рендерится из OpenAPI JS'ом, поэтому curl и простая загрузка страницы видят только заголовок страницы). Ссылка на него стоит в официальном contributing-гайде как «API documentation».
|
||
- [OpenAPI JSON stable](https://api.jellyfin.org/openapi/jellyfin-openapi-stable.json) — актуальная спецификация stable-сервера (сейчас 12.2.0, ~1.9 МБ, 294 пути). **Gotcha:** с CDN api.jellyfin.org скачивание у меня обрывалось на 14 КБ; надёжнее брать с зеркала ниже.
|
||
- [OpenAPI JSON unstable](https://api.jellyfin.org/openapi/jellyfin-openapi-unstable.json) — спецификация master-ветки.
|
||
- [Архив OpenAPI по версиям](https://repo.jellyfin.org/files/openapi/stable/) — файлы `jellyfin-openapi-<версия>.json` для каждого релиза: от 10.9.0 через 10.10.7, 10.11.0…10.11.11 до 12.0, 12.1, 12.2 (например, [jellyfin-openapi-12.2.json](https://repo.jellyfin.org/files/openapi/stable/jellyfin-openapi-12.2.json), [jellyfin-openapi-10.11.0.json](https://repo.jellyfin.org/files/openapi/stable/jellyfin-openapi-10.11.0.json)). Именно это «версионирование API по релизам сервера»: отдельного номера версии API нет, `info.version` = версии сервера. Удобно для diff'ов между версиями (см. раздел 7). Есть и каталог [unstable](https://repo.jellyfin.org/files/openapi/unstable/).
|
||
- [Gist Niels van Velzen «Jellyfin authorization»](https://gist.github.com/nielsvanvelzen/ea047d9028f676185832e51ffaf12a6f) — де-факто официальное описание схемы авторизации (на него ссылается release-пост 10.11). Схема `MediaBrowser`, параметры через запятую, значения в двойных кавычках и URL-encoded, ключи регистрозависимые, неизвестные ключи игнорируются. Перечисляет устаревшие методы (query `api_key`, заголовки `X-Emby-Token`, `X-MediaBrowser-Token`, `X-Emby-Authorization`) и подтверждает их отключение в 12.0. Рекомендует хешировать имя пользователя в `DeviceId`.
|
||
|
||
Формат заголовка (из gist + исходника SDK `AuthorizationHeaderBuilder.kt`):
|
||
```
|
||
Authorization: MediaBrowser Client="MyApp", Device="Pixel 8", DeviceId="<уникальный id>", Version="1.0.0", Token="<accessToken>"
|
||
```
|
||
До логина — тот же заголовок без `Token`. Для API-ключа достаточно `Authorization: MediaBrowser Token="<key>"`. В OpenAPI это security scheme `CustomAuthentication` (apiKey, header `Authorization`).
|
||
|
||
- [Исходник серверной проверки авторизации `AuthorizationContext.cs`](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs) — «источник истины»: порядок поиска токена: заголовок `Authorization` → (только при `EnableLegacyAuthorization`) `X-Emby-Token`, `X-MediaBrowser-Token` → query `ApiKey` (всегда) → (legacy) query `api_key`; схема `Emby` принимается только в legacy-режиме. Проверено grep'ом по master.
|
||
- [SDK: AuthorizationHeaderBuilder.kt](https://github.com/jellyfin/jellyfin-sdk-kotlin/blob/master/jellyfin-api/src/commonMain/kotlin/org/jellyfin/sdk/api/client/util/AuthorizationHeaderBuilder.kt) — эталонная реализация сборки заголовка (`Client`, `Version`, `DeviceId`, `Device`, `Token`; `encodeURLPart` для значений).
|
||
- [Issue #15962 «Default EnableLegacyAuthorization=false breaks Jellyseerr»](https://github.com/jellyfin/jellyfin/issues/15962) — позиция мейнтейнеров: депрекация объявлена в 2023, в 10.11 добавлен opt-in флаг для тестирования, в 12.0 стало opt-out, в следующем мажоре legacy-опция будет удалена совсем; «откатывать не планируем». Временный обход для админов — `EnableLegacyAuthorization` в `system.xml` (см. также [форум-тред](https://forum.jellyfin.org/printthread.php?tid=15085)).
|
||
- [Help Net Security о 12.0](https://www.helpnetsecurity.com/2026/09/08/jellyfin-12-0-security-fixes/) — новостная заметка про отключение legacy-логинов и security-фиксы 12.0 (вторичный источник).
|
||
|
||
**DeviceId / идентификация клиента**
|
||
- Сервер хранит **один access token на `DeviceId`**: если на одном устройстве залогинены два пользователя Jellyfin с одинаковым `DeviceId`, второй логин инвалидирует первый. Рекомендация (gist + сообщество): `DeviceId = hash(installationId + username)` или отдельный id на пользователя.
|
||
- `Client`/`Version`/`Device` показываются в дашборде сервера (Devices, Sessions) и влияют на «Playback reporting».
|
||
- В SDK это `ClientInfo(name, version)` и `DeviceInfo(id, name)` ([Dokka ClientInfo](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-model/org.jellyfin.sdk.model/-client-info/index.html), [Dokka DeviceInfo](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-model/org.jellyfin.sdk.model/-device-info/index.html)); с 1.9.0 в `DeviceInfo` можно задать язык API (сервер локализует ответы; на сервере с 12.0 поддерживается `Accept-Language`).
|
||
- Эндпоинты устройств: `GET /Devices`, `GET /Devices/Info`, `GET/POST /Devices/Options`; разлогин — `POST /Sessions/Logout`; текущий пользователь — `GET /Users/Me`; публичный список пользователей для экрана логина — `GET /Users/Public`.
|
||
|
||
**Логин по паролю:** `POST /Users/AuthenticateByName` с телом `{"Username": "...", "Pw": "..."}` (пользователь без пароля — пустая строка) → `AuthenticationResult {User, SessionInfo, AccessToken, ServerId}`; 401 при неверных данных.
|
||
|
||
**Quick Connect** (все пути проверены по OpenAPI 12.2):
|
||
- `GET /QuickConnect/Enabled` → bool.
|
||
- `POST /QuickConnect/Initiate` → `QuickConnectResult {Authenticated, Secret, Code, DeviceId, DeviceName, AppName, AppVersion, DateAdded}`. **`GET /QuickConnect/Initiate` удалён в 12.0** — только POST.
|
||
- `GET /QuickConnect/Connect?secret=...` — опрос состояния (SDK рекомендует раз в ~5 с).
|
||
- `POST /Users/AuthenticateWithQuickConnect` с телом `{"Secret": "..."}` → `AuthenticationResult`.
|
||
- `POST /QuickConnect/Authorize?code=...&userId=...` — вызывается с уже залогиненного устройства (если хотите, чтобы ваш клиент мог *подтверждать* чужие коды).
|
||
- [Kotlin SDK guide: Authentication](https://kotlin-sdk.jellyfin.org/guide/authentication.html) — готовые сниппеты для `authenticateUserByName`, Quick Connect, `api.update(accessToken=...)`.
|
||
- [Docs: Quick Connect (пользовательская)](https://jellyfin.org/docs/general/server/quick-connect/) — поведение фичи, код из 6 символов, включён по умолчанию, список клиентов, умеющих логиниться/авторизовывать.
|
||
|
||
**Неофициальные зеркала (с осторожностью):**
|
||
- [mintlify.wiki: jellyfin/api/authentication/overview](https://mintlify.wiki/jellyfin/jellyfin/api/authentication/overview) — AI-сгенерированное «зеркало» документации, не официальное; примеры curl корректны, но детали (например, про `api_key`) местами противоречивы.
|
||
|
||
---
|
||
|
||
## 2. jellyfin-sdk-kotlin и legacy-репозиторий
|
||
|
||
- [github.com/jellyfin/jellyfin-sdk-kotlin](https://github.com/jellyfin/jellyfin-sdk-kotlin) — официальный Kotlin SDK (JVM + Android), сгенерирован из OpenAPI сервера. Лицензия **LGPL-3.0** (совместимо с GPL-приложением при обычном линковании). ~2190 коммитов, активно поддерживается (релизы в сентябре–октябре 2026).
|
||
- **Важно про `jellyfin-apiclient-java`:** [github.com/jellyfin/jellyfin-apiclient-java](https://github.com/jellyfin/jellyfin-apiclient-java) теперь **редиректит на jellyfin-sdk-kotlin** — это тот же репозиторий, переименованный; отдельного deprecated Java-клиента больше нет. Старое Java-API было окончательно убрано в SDK v1.7 («Java helper utilities removed», см. миграцию v1.7).
|
||
- [kotlin-sdk.jellyfin.org — Getting started](https://kotlin-sdk.jellyfin.org/guide/getting-started.html) — Maven Central, `implementation("org.jellyfin.sdk:jellyfin-core:$sdkVersion")`, DSL `createJellyfin { clientInfo = ClientInfo(name, version); context = androidContext }`, `jellyfin.createApi(baseUrl, accessToken, clientInfo, deviceInfo, httpClientOptions)`. Указано: Java 8+, «Android 4.4+» (устарело — см. ниже), desugaring для Android < 8. Snapshot-репозиторий `master-SNAPSHOT` на `https://s01.oss.sonatype.org/content/repositories/snapshots/` (работоспособность snapshot-репо не проверена). В разделе «Projects using the SDK» перечислены Findroid и Homedia — можно добавить свой проект PR'ом.
|
||
- [Kotlin SDK guide: WebSockets](https://kotlin-sdk.jellyfin.org/guide/websockets.html) — см. раздел 5.
|
||
- [Dokka (API reference)](https://kotlin-sdk.jellyfin.org/dokka/) — полный справочник по модулям.
|
||
- [Releases](https://github.com/jellyfin/jellyfin-sdk-kotlin/releases) и [v1.9.0](https://github.com/jellyfin/jellyfin-sdk-kotlin/releases/tag/v1.9.0): 1.9.0 (08.09.2026) — под Jellyfin 12.0, **minSdk 21, compileSdk 37**, Kotlin 2.4.20, OkHttp 5.5.0, «no longer compatible with Jellyfin 10.11 and earlier»; 1.9.1 — фикс отмены WebSocket-подписок; 1.9.2 (06.10.2026) — OpenAPI 12.2.0. 1.8.12 (26.07.2026) — бэкпорт для 10.11.x.
|
||
- Maven Central: группа `org.jellyfin.sdk`, артефакты (проверено по [repo1.maven.org](https://repo1.maven.org/maven2/org/jellyfin/sdk/)): `jellyfin-core` (+ `-android`, `-jvm`), `jellyfin-api` (+ `-jvm`), `jellyfin-api-okhttp`, `jellyfin-model` (+ `-jvm`), `jellyfin-platform-android`, и устаревший `jellyfin-api-ktor` (последняя 1.7.1). `maven-metadata.xml` для `jellyfin-core`: latest/release = **1.9.2**.
|
||
- **Сетевой стек — НЕ Ktor.** [Migrate to v1.7](https://kotlin-sdk.jellyfin.org/migration/v1.7.html): OkHttp 4 стал дефолтом, SDK перестал управлять потоками (вызовы нельзя делать на main thread), `User-Agent` формируется из `ClientInfo`; [Migrate to v1.8](https://kotlin-sdk.jellyfin.org/migration/v1.8.html): пакет `jellyfin-api-ktor` удалён. В 1.9 — OkHttp 5.5. Это удобно: тот же OkHttp можно переиспользовать для картинок (Coil) и при желании настраивать TLS.
|
||
- Версии зависимостей (из [gradle/libs.versions.toml](https://github.com/jellyfin/jellyfin-sdk-kotlin/blob/master/gradle/libs.versions.toml) на master): kotlin 2.4.20, kotlinx-coroutines 1.11.0, kotlinx-serialization 1.11.0, okhttp 5.5.0, koin 4.2.2, kotlin-logging 8.0.4, slf4j 2.0.19.
|
||
- [HttpClientOptions.kt](https://github.com/jellyfin/jellyfin-sdk-kotlin/blob/master/jellyfin-api/src/commonMain/kotlin/org/jellyfin/sdk/api/client/HttpClientOptions.kt): `followRedirects=true`, `requestTimeout=30s`, `connectTimeout=6s`, `socketTimeout=30s`, `socketReconnectPolicy=ExponentialDelayReconnect()`. Явной настройки доверенных сертификатов в опциях нет (не проверено глубже) — для self-signed придётся либо Android Network Security Config, либо смотреть, как подменить OkHttp-клиент.
|
||
- [Jellyfin.kt](https://github.com/jellyfin/jellyfin-sdk-kotlin/blob/master/jellyfin-core/src/commonMain/kotlin/org/jellyfin/sdk/Jellyfin.kt): `Jellyfin.minimumVersion = ServerVersion(12,0,0,0)`, `Jellyfin.apiVersion` = версия OpenAPI, по которой сгенерирован SDK.
|
||
- Пакет расширений [org.jellyfin.sdk.api.client.extensions](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-api/org.jellyfin.sdk.api.client.extensions/index.html): свойства `api.libraryApi`, `api.mediaInfoApi`, `api.sessionApi`, `api.userDataApi`, `api.showApi`, `api.userViewApi`, `api.videoApi`, `api.audioApi`, `api.imageApi`, `api.subtitleApi`, `api.trickPlayApi`, `api.mediaSegmentApi`, `api.lyricApi`, `api.liveTvApi`, `api.syncPlayApi`, `api.quickConnectApi`, `api.searchApi`, `api.systemApi`, `api.userApi` и т. д.; функции `authenticateUserByName`, `authenticateWithQuickConnect`, generic `api.get/post/delete<T>(pathTemplate, …)` для эндпоинтов, которых нет в генерации (например, HLS).
|
||
- Список всех классов операций 1.9: [org.jellyfin.sdk.api.operations](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-api/org.jellyfin.sdk.api.operations/index.html) (46 классов: `LibraryApi`, `MediaInfoApi`, `SessionApi`, `ShowApi`, `UserViewApi`, `VideoApi`, `AudioApi`, `ImageApi`, `SubtitleApi`, `TrickPlayApi`, `MediaSegmentApi`, `LyricApi`, `LiveTvApi`, `SyncPlayApi`, `QuickConnectApi`, `AuthenticationApi`, `UserApi`, `UserDataApi`, `SearchApi`, `SystemApi`, `PlaylistApi`, `CollectionApi`, `PersonApi`, `GenreApi`, `StudioApi`, …).
|
||
- Полезные Dokka-страницы (проверены): [MediaInfoApi.getPostedPlaybackInfo](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-api/org.jellyfin.sdk.api.operations/-media-info-api/get-posted-playback-info.html), [SessionApi.reportPlaybackProgress](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-api/org.jellyfin.sdk.api.operations/-session-api/report-playback-progress.html), [VideoApi.getVideoStreamUrl](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-api/org.jellyfin.sdk.api.operations/-video-api/get-video-stream-url.html), [AudioApi.getUniversalAudioStreamUrl](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-api/org.jellyfin.sdk.api.operations/-audio-api/get-universal-audio-stream-url.html), [ImageApi.getItemImageUrl](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-api/org.jellyfin.sdk.api.operations/-image-api/get-item-image-url.html), [SubtitleApi.getSubtitleUrl](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-api/org.jellyfin.sdk.api.operations/-subtitle-api/get-subtitle-url.html), [TrickPlayApi.getTrickplayTileImageUrl](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-api/org.jellyfin.sdk.api.operations/-trick-play-api/get-trickplay-tile-image-url.html), [MediaSegmentApi.getItemSegments](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-api/org.jellyfin.sdk.api.operations/-media-segment-api/get-item-segments.html), [LibraryApi.getItems](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-api/org.jellyfin.sdk.api.operations/-library-api/get-items.html), [LibraryApi.getResumeItems](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-api/org.jellyfin.sdk.api.operations/-library-api/get-resume-items.html), [LibraryApi.getLatestMedia](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-api/org.jellyfin.sdk.api.operations/-library-api/get-latest-media.html), [ShowApi.getNextUp](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-api/org.jellyfin.sdk.api.operations/-show-api/get-next-up.html), [UserViewApi.getUserViews](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-api/org.jellyfin.sdk.api.operations/-user-view-api/get-user-views.html), [SearchApi.getSearchHints](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-api/org.jellyfin.sdk.api.operations/-search-api/get-search-hints.html), [SessionApi.postFullCapabilities](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-api/org.jellyfin.sdk.api.operations/-session-api/post-full-capabilities.html), [SyncPlayApi.syncPlayJoinGroup](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-api/org.jellyfin.sdk.api.operations/-sync-play-api/sync-play-join-group.html), [LyricApi.getLyrics](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-api/org.jellyfin.sdk.api.operations/-lyric-api/get-lyrics.html), [SystemApi.getPublicSystemInfo](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-api/org.jellyfin.sdk.api.operations/-system-api/get-public-system-info.html).
|
||
- DSL для DeviceProfile: [org.jellyfin.sdk.model.deviceprofile](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-model/org.jellyfin.sdk.model.deviceprofile/index.html) — `buildDeviceProfile { directPlayProfile {…}; transcodingProfile {…}; codecProfile {…}; subtitleProfile(...) }`, `buildUpon`.
|
||
- Тики: [org.jellyfin.sdk.model.extensions](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-model/org.jellyfin.sdk.model.extensions/index.html) — `Long.ticks: Duration`, `Duration.inWholeTicks`.
|
||
- Discovery (jellyfin-core): [org.jellyfin.sdk.discovery](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-core/org.jellyfin.sdk.discovery/index.html), [DiscoveryService](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-core/org.jellyfin.sdk.discovery/-discovery-service/index.html) — `discoverLocalServers(timeout, maxServers): Flow<ServerDiscoveryInfo>`, `getAddressCandidates(input)`, `getRecommendedServers(input|servers, minimumScore)`. Детали в разделе 6. Отдельной страницы guide по discovery на сайте нет (404); пример есть в Getting started (`jellyfin.discovery.getRecommendedServers(candidates, RecommendedServerInfoScore.GOOD)`).
|
||
- Пример проекта: [samples/kotlin-cli](https://github.com/jellyfin/jellyfin-sdk-kotlin/tree/master/samples/kotlin-cli) — CLI с командами discovery, observe (websocket) и т. п.
|
||
- Эталонные потребители SDK: [jellyfin-androidtv](https://github.com/jellyfin/jellyfin-androidtv) (официальный TV-клиент на SDK), [jellyfin-android](https://github.com/jellyfin/jellyfin-android), [Findroid](https://github.com/jarnedemeulemeester/findroid) (сторонний нативный Kotlin-клиент на SDK, GPL).
|
||
- Для сравнения — [jellyfin-sdk-typescript](https://github.com/jellyfin/jellyfin-sdk-typescript) / [typescript-sdk.jellyfin.org](https://typescript-sdk.jellyfin.org/): таблица совместимости SDK↔сервер (1.0.0 → 12.x, 0.12/0.13 → 10.11.x, 0.11 → 10.10.x, 0.9/0.10 → 10.9.x), тот же паттерн ClientInfo/DeviceInfo/discovery/auth.
|
||
|
||
---
|
||
|
||
## 3. Воспроизведение (PlaybackInfo, DeviceProfile, стримы, прогресс, субтитры, trickplay, UserData)
|
||
|
||
Единицы времени: **1 tick = 100 нс; 1 с = 10 000 000 ticks**. Все `*Ticks` в API (`RunTimeTicks`, `PositionTicks`, `StartTimeTicks`, `StartPositionTicks`, `PlaybackPositionTicks`, `StartTicks/EndTicks`) — в этих единицах. libVLC оперирует миллисекундами → `ms * 10_000`.
|
||
|
||
### PlaybackInfo и DeviceProfile
|
||
- `POST /Items/{itemId}/PlaybackInfo` (query: `userId`, `maxStreamingBitrate`, `startTimeTicks`, `audioStreamIndex`, `subtitleStreamIndex`, `maxAudioChannels`, `mediaSourceId`, `liveStreamId`, `autoOpenLiveStream`, `enableDirectPlay`, `enableDirectStream`, `enableTranscoding`, `allowVideoStreamCopy`, `allowAudioStreamCopy`; тело `PlaybackInfoDto` с теми же полями + `DeviceProfile`, `AlwaysBurnInSubtitleWhenTranscoding`). Ответ `PlaybackInfoResponse { MediaSources: [MediaSourceInfo], PlaySessionId, ErrorCode }`. Есть и `GET`-вариант без профиля (бесполезен для решения о транскодировании).
|
||
- `MediaSourceInfo` (проверено по спеке 12.2): `Id`, `Container`, `Protocol`, `Path`, `Size`, `Bitrate`, `RunTimeTicks`, `SupportsDirectPlay`, `SupportsDirectStream`, `SupportsTranscoding`, `TranscodingUrl`, `TranscodingSubProtocol` (`http`|`hls`), `TranscodingContainer`, `MediaStreams`, `MediaAttachments`, `DefaultAudioStreamIndex`, `DefaultSubtitleStreamIndex`, `RequiredHttpHeaders`, `IsInfiniteStream`, `LiveStreamId`, `RequiresOpening/OpenToken/RequiresClosing` (Live TV), `HasSegments` (есть media segments), `ETag`.
|
||
- Логика выбора: сервер сравнивает `DeviceProfile` с ffprobe-данными файла: контейнер+кодеки проходят `DirectPlayProfiles` и `CodecProfiles` → **DirectPlay** (`/Videos/{id}/stream?static=true`); видео проходит, но контейнер/аудио/субтитры нет → **DirectStream** (ремукс, `TranscodingUrl` c `allowVideoStreamCopy`); иначе **Transcode** (`TranscodingUrl`, обычно HLS). Enum `PlayMethod`: `DirectPlay | DirectStream | Transcode` — его нужно сообщать в прогрессе.
|
||
- `DeviceProfile` поля: `Name`, `Id`, `MaxStreamingBitrate`, `MaxStaticBitrate`, `MusicStreamingTranscodingBitrate`, `MaxStaticMusicBitrate`, `DirectPlayProfiles[] {Container, AudioCodec, VideoCodec, Type(Audio|Video|Photo|Subtitle|Lyric)}`, `TranscodingProfiles[] {Container, Type, VideoCodec, AudioCodec, Protocol(http|hls), Context(Streaming|Static), MaxAudioChannels, MinSegments, SegmentLength, BreakOnNonKeyFrames, CopyTimestamps, EnableSubtitlesInManifest, Conditions, EnableAudioVbrEncoding, …}`, `ContainerProfiles`, `CodecProfiles[] {Type(Video|VideoAudio|Audio), Codec, Container, SubContainer, Conditions, ApplyConditions}`, `SubtitleProfiles[] {Format, Method, Language, Container, DidlMode}`. Условия (`ProfileConditionValue`): `Width, Height, VideoBitDepth, VideoBitrate, VideoFramerate, VideoLevel, VideoProfile, AudioChannels, AudioBitrate, AudioSampleRate, AudioBitDepth, RefFrames, IsAnamorphic, IsInterlaced, IsSecondaryAudio, VideoCodecTag, IsAvc, VideoRangeType, NumAudioStreams, NumVideoStreams, NumStreams (10.11+), VideoRotation (12.x)`. Схема DeviceProfile одинакова в 10.10.7 / 10.11.11 / 12.2.0 — можно писать один профиль.
|
||
- Для libVLC: он умеет почти всё (MKV/MP4/TS/AVI, H.264/HEVC/VP9/AV1 через MediaCodec или софт, AC3/EAC3/DTS/TrueHD/AAC/FLAC/Opus, PGS/ASS/SRT/VTT), поэтому агрессивный DirectPlay-профиль + HLS/TS fallback (как у официальных клиентов) — разумная стартовая точка. Опирайтесь на реальные профили:
|
||
- [jellyfin-androidtv: util/profile/deviceProfile.kt](https://github.com/jellyfin/jellyfin-androidtv/blob/master/app/src/main/java/org/jellyfin/androidtv/util/profile/deviceProfile.kt) — `buildDeviceProfile { … }` с несколькими `transcodingProfile` (TS+HLS, MP4+HLS), `directPlayProfile`, множеством `codecProfile` (HDR-range, AV1/HEVC условия), `subtitleProfile(VTT/WEBVTT, embedded=true, hls=true, external=true)`; рядом `deviceProfileReport.kt` и `codec/` с проверкой MediaCodec-возможностей.
|
||
- [jellyfin-android: DeviceProfileBuilder.kt](https://github.com/jellyfin/jellyfin-android/blob/master/app/src/main/java/org/jellyfin/mobile/player/deviceprofile/DeviceProfileBuilder.kt) — профиль для ExoPlayer + отдельный `getExternalPlayerProfile()` (для внешних плееров, по сути «играй всё, maxStreamingBitrate = Int.MAX_VALUE», субтитры EMBED + EXTERNAL) — ближайший аналог того, что нужно для libVLC.
|
||
- [jellyfin-android: MediaSourceResolver.kt](https://github.com/jellyfin/jellyfin-android/blob/master/app/src/main/java/org/jellyfin/mobile/player/source/MediaSourceResolver.kt) — как официальный клиент выбирает MediaSource/PlayMethod из ответа PlaybackInfo.
|
||
- [jellyfin-web: browserDeviceProfile.js](https://github.com/jellyfin/jellyfin-web/blob/master/src/scripts/browserDeviceProfile.js) и [playbackmanager.js](https://github.com/jellyfin/jellyfin-web/blob/master/src/components/playback/playbackmanager.js) — эталон полного playback-цикла (PlaybackInfo → выбор → отчёты → стоп), включая периодичность прогресса и обработку `TranscodingUrl`.
|
||
- [Docs: Codec Support](https://jellyfin.org/docs/general/clients/codec-support/) — таблицы кодеков/контейнеров по клиентам (Android / Android TV) и принцип «Direct Play всё, DirectStream при неподдерживаемом контейнере/аудио/субтитрах, Transcode при неподдерживаемом видео».
|
||
- [Docs: Transcoding](https://jellyfin.org/docs/general/post-install/transcoding/) — серверная сторона (HW accel), полезно для понимания причин транскодирования.
|
||
|
||
### URL потоков
|
||
- Direct play: `GET /Videos/{itemId}/stream` или `/Videos/{itemId}/stream.{container}` с `static=true&mediaSourceId=...&deviceId=...` (+ `tag`/ETag). Полный список query-параметров (проверено по спеке): `container, static, params, tag, deviceProfileId, playSessionId, segmentContainer, segmentLength, minSegments, mediaSourceId, deviceId, audioCodec, videoCodec, enableAutoStreamCopy, allowVideoStreamCopy, allowAudioStreamCopy, audioSampleRate, maxAudioBitDepth, audioBitRate, audioChannels, maxAudioChannels, profile, level, framerate, maxFramerate, copyTimestamps, startTimeTicks, width, height, maxWidth, maxHeight, videoBitRate, subtitleStreamIndex, subtitleMethod, maxRefFrames, maxVideoBitDepth, requireAvc, deInterlace, requireNonAnamorphic, transcodingMaxAudioChannels, cpuCoreLimit, liveStreamId, enableMpegtsM2TsMode, subtitleCodec, transcodeReasons, audioStreamIndex, videoStreamIndex, context, streamOptions, enableAudioVbrEncoding`.
|
||
- Transcode/DirectStream: берите готовый `MediaSourceInfo.TranscodingUrl` (относительный путь `/videos/{id}/master.m3u8?...` или `/Videos/{id}/stream?...`, уже содержит `PlaySessionId`, `DeviceId`, `MediaSourceId`, индексы потоков и ключ `ApiKey`; с 10.11 генерируемые URL используют `ApiKey` вместо `api_key`). Склеивайте с `baseUrl`.
|
||
- HLS-маршруты существуют, но с 12.0 **скрыты из OpenAPI** (`[ApiExplorerSettings(IgnoreApi = true)]` в [DynamicHlsController.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/DynamicHlsController.cs)): `GET /Videos/{itemId}/master.m3u8`, `/Videos/{itemId}/main.m3u8`, `/Videos/{itemId}/live.m3u8`, `/Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}`, аналогично `/Audio/{itemId}/master.m3u8`. Релиз-пост 12.0 прямо говорит: «endpoints not in the OpenAPI specification … should not be used» — т. е. формировать их вручную не рекомендуется, используйте `TranscodingUrl`.
|
||
- **Gotcha для libVLC:** плееру нужен URL без возможности выставить заголовок `Authorization` → используйте query `ApiKey=<token>` (не legacy `api_key`!). libVLC 3 позволяет задать только User-Agent/referrer-подобные опции, не произвольные заголовки (общее знание, не проверялось в этом исследовании).
|
||
- Музыка: `GET /Audio/{itemId}/universal` (`container[]`, `audioCodec`, `maxStreamingBitrate`, `transcodingContainer`, `transcodingProtocol`, `maxAudioSampleRate`, `enableRedirection`, …) — сервер сам решает direct/transcode без PlaybackInfo. Скачивание: `GET /Items/{itemId}/Download`.
|
||
- Trailers/Intros: `GET /Items/{itemId}/Intros` (pre-roll), `RemoteTrailers` в `BaseItemDto`.
|
||
|
||
### Отчёты о воспроизведении (сессии)
|
||
- `POST /Sessions/Playing` — тело `PlaybackStartInfo`; `POST /Sessions/Playing/Progress` — `PlaybackProgressInfo`; `POST /Sessions/Playing/Stopped` — `PlaybackStopInfo`; `POST /Sessions/Playing/Ping?playSessionId=` — keep-alive транскодера. Поля Start/Progress: `ItemId`, `MediaSourceId`, `PlaySessionId`, `PositionTicks`, `IsPaused`, `IsMuted`, `VolumeLevel`, `CanSeek`, `PlayMethod`, `AudioStreamIndex`, `SubtitleStreamIndex`, `RepeatMode`, `PlaybackOrder`, `NowPlayingQueue`, `PlaylistItemId`, `LiveStreamId`, `SessionId`, `PlaybackStartTimeTicks`, `AspectRatio`, `Brightness`. Stop: `ItemId`, `MediaSourceId`, `PlaySessionId`, `PositionTicks`, `Failed`, `NextMediaType`, `LiveStreamId`. Исходник: [PlaystateController.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/PlaystateController.cs).
|
||
- **Старые `POST/DELETE /PlayingItems/{itemId}[/Progress]` (OnPlayback*) депрекированы в 10.11 и удалены из спеки в 12.0** — не используйте. Также `POST/DELETE /Users/{userId}/PlayedItems/{id}` помечены `Obsolete` в коде; актуальные: `POST/DELETE /UserPlayedItems/{itemId}` (`?userId&datePlayed`), `POST/DELETE /UserFavoriteItems/{itemId}`, `GET/POST /UserItems/{itemId}/UserData`, `POST/DELETE /UserItems/{itemId}/Rating`.
|
||
- Периодичность прогресса: веб-клиент шлёт прогресс примерно раз в 10 с и при событиях pause/seek/stream switch (значение 10 с — по памяти, не проверено в этом исследовании; смотрите `playbackmanager.js`). Без прогресса сервер через таймаут считает сессию мёртвой и убивает транскодер — отсюда `Ping`.
|
||
- **Resume / UserData:** `BaseItemDto.UserData: UserItemDataDto {PlaybackPositionTicks, PlayedPercentage, Played, PlayCount, IsFavorite, Likes, LastPlayedDate, UnplayedItemCount, Rating, Key, ItemId}`. Список «Продолжить просмотр»: `GET /UserItems/Resume?userId=&limit=&mediaTypes=Video&fields=…&excludeActiveSessions=` (пути `/Users/{userId}/Items/Resume`, `/Users/{userId}/Items/Latest` в спеках 10.10.7+ уже отсутствуют — используйте `userId` как query).
|
||
- Для «кнопки Resume» нужны `fields=MediaSources,Chapters,Trickplay,MediaStreams` и `enableUserData=true` в запросах.
|
||
|
||
### Субтитры
|
||
- `SubtitleDeliveryMethod` enum: `Encode` (вжечь), `Embed` (в контейнере, плеер читает сам), `External` (отдельный URL), `Hls` (в манифесте), `Drop`. В `DeviceProfile.SubtitleProfiles` объявляете, какие форматы и каким методом умеете (например, для libVLC: `srt/subrip, ass, ssa, vtt, pgssub, dvdsub, dvbsub` как `Embed`; `srt, ass, ssa, vtt` как `External`).
|
||
- Внешний поток: `GET /Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}` и `GET /Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks}/Stream.{format}` (query: `copyTimestamps`, `addVttTimeMap`, `endPositionTicks`). HLS-плейлист субтитров: `GET /Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8`. Загрузка/удаление: `POST /Videos/{itemId}/Subtitles`, `DELETE /Videos/{itemId}/Subtitles/{index}`; поиск онлайн: `GET /Items/{itemId}/RemoteSearch/Subtitles/{language}`, `POST .../Subtitles/{subtitleId}`. Исходник: [SubtitleController.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/SubtitleController.cs). В ответе PlaybackInfo каждый `MediaStream` типа Subtitle несёт `DeliveryMethod`/`DeliveryUrl`/`IsExternal` (имена этих полей MediaStream не перепроверял по спеке — «не проверено»), что позволяет не собирать URL вручную. В 12.2 графические субтитры отдаются как raw-файлы с Range-поддержкой.
|
||
- Для libVLC внешние субтитры удобно подключать через `Media.addSlave(Subtitle, url)`.
|
||
|
||
### Trickplay (с 10.9) и главы
|
||
- Данные: `BaseItemDto.Trickplay` (нужен `fields=Trickplay`) — словарь `mediaSourceId → width → TrickplayInfoDto {Width, Height, TileWidth, TileHeight, ThumbnailCount, Interval (мс), Bandwidth}` ([Dokka TrickplayInfoDto](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-model/org.jellyfin.sdk.model.api/-trickplay-info-dto/index.html)).
|
||
- Картинки-тайлы: `GET /Videos/{itemId}/Trickplay/{width}/{index}.jpg?mediaSourceId=`; HLS image playlist: `GET /Videos/{itemId}/Trickplay/{width}/tiles.m3u8`. Исходник: [TrickplayController.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/TrickplayController.cs). Индекс тайла = `floor(positionMs / Interval) / (TileWidth*TileHeight)`, позиция внутри — остаток.
|
||
- Главы: `BaseItemDto.Chapters[] ChapterInfo {StartPositionTicks, Name, ImageTag, ImagePath, ImageDateModified}` (`fields=Chapters`); картинка главы — `GET /Items/{itemId}/Images/Chapter/{chapterIndex}?tag=`.
|
||
|
||
### Media Segments (intro/outro skip, с 10.10)
|
||
- `GET /MediaSegments/{itemId}?includeSegmentTypes=Intro&includeSegmentTypes=Outro` → `QueryResult<MediaSegmentDto {Id, ItemId, Type, StartTicks, EndTicks}>`; `MediaSegmentType`: `Unknown, Commercial, Preview, Recap, Outro, Intro`. `MediaSourceInfo.HasSegments` подсказывает, стоит ли вообще запрашивать. Исходник: [MediaSegmentsController.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/MediaSegmentsController.cs). Сегменты создают плагины (Chapter Segments Provider, Intro Skipper); сервер лишь хранит.
|
||
- [Docs: Media Segments](https://jellyfin.org/docs/general/server/metadata/media-segments) — описание фичи для пользователей/админов, типы сегментов.
|
||
- Эталон поведения в клиенте: [jellyfin-androidtv ui/playback/segment](https://github.com/jellyfin/jellyfin-androidtv/tree/master/app/src/main/java/org/jellyfin/androidtv/ui/playback/segment) ([MediaSegmentRepository.kt](https://github.com/jellyfin/jellyfin-androidtv/blob/master/app/src/main/java/org/jellyfin/androidtv/ui/playback/segment/MediaSegmentRepository.kt)): действия per type (Skip / Ask to skip / Nothing), минимум 1 с для автоскипа, 3 с для кнопки, автоскрытие кнопки через 8 с.
|
||
|
||
---
|
||
|
||
## 4. API библиотеки (browsing)
|
||
|
||
Всё проверено по OpenAPI 12.2.0:
|
||
- `GET /UserViews?userId=&includeExternalContent=&presetViews=&includeHidden=` — корневые «библиотеки» пользователя (`CollectionType`: movies, tvshows, music, books, …). `GET /UserViews/GroupingOptions`.
|
||
- `GET /Items` — универсальный запрос. Ключевые параметры: `userId, parentId, recursive, includeItemTypes[] (BaseItemKind: Movie, Series, Season, Episode, MusicAlbum, Audio, BoxSet, Playlist, …), excludeItemTypes, searchTerm, sortBy[] (ItemSortBy: SortName, DateCreated, PremiereDate, DatePlayed, CommunityRating, Random, Runtime, ProductionYear, SeriesSortName, …), sortOrder[] (Ascending|Descending), filters[] (ItemFilter: IsUnplayed, IsPlayed, IsFavorite, IsResumable, IsFolder, …), fields[] (ItemFields: Overview, Genres, People, MediaSources, MediaStreams, Chapters, Trickplay, ChildCount, DateCreated, ProviderIds, PrimaryImageAspectRatio, Path, …), startIndex, limit, enableTotalRecordCount, enableImages, imageTypeLimit, enableImageTypes[], enableUserData, genres/genreIds, studios/studioIds, years, tags, officialRatings, person/personIds/personTypes, artistIds/albumArtistIds, ids, isPlayed, isFavorite, minCommunityRating, hasSubtitles, isHd, is4K, nameStartsWith/nameStartsWithOrGreater/nameLessThan (алфавитная навигация), collapseBoxSetItems, audioLanguages, subtitleLanguages, mediaTypes`. **Gotcha 12.0:** `recursive` теперь применяется автоматически при фильтрах, но только если задан `includeItemTypes` — результаты могут отличаться от 10.11.
|
||
- `GET /Items/{itemId}?userId=` — карточка; `POST /Items/{itemId}` — обновление метаданных; `DELETE`.
|
||
- Сериалы: `GET /Shows/NextUp?userId=&seriesId=&parentId=&limit=&enableResumable=&enableRewatching=&nextUpDateCutoff=&fields=`; `GET /Shows/{seriesId}/Seasons`, `GET /Shows/{seriesId}/Episodes?seasonId=&userId=&fields=`; похожие: `GET /Items/{itemId}/Similar` (также `/Movies/{id}/Similar`, `/Shows/{id}/Similar`; в 10.11 добавлены фильтр по тегам и случайная сортировка).
|
||
- Новинки: `GET /Items/Latest?userId=&parentId=&includeItemTypes=&limit=&groupItems=&fields=`. Продолжить: `GET /UserItems/Resume` (см. выше). Рекомендации: `GET /Items/Suggestions`. Счётчики: `GET /Items/Counts`. Фильтры для UI: `GET /Items/Filters`, `GET /Items/Filters2` (жанры/теги/годы/рейтинги для выбранной библиотеки).
|
||
- Поиск: `GET /Search/Hints?searchTerm=&userId=&includeItemTypes=&limit=&includePeople/Media/Genres/Studios/Artists` (быстрые подсказки, `SearchHint`) **или** `GET /Items?searchTerm=&recursive=true&includeItemTypes=Movie,Series,Episode&fields=…` (полные `BaseItemDto`, то, что делает веб-клиент).
|
||
- Коллекции (BoxSet): `POST /Collections?name=&ids=&parentId=&isLocked=`, `POST/DELETE /Collections/{collectionId}/Items?ids=`, содержимое — `GET /Items?parentId={boxSetId}`; `GET /Items/{itemId}/Collections` (новое в 12.x: в каких коллекциях состоит элемент).
|
||
- Плейлисты: `POST /Playlists` (тело `CreatePlaylistDto`), `GET/POST /Playlists/{playlistId}`, `GET/POST/DELETE /Playlists/{playlistId}/Items`, `POST .../Items/{itemId}/Move/{newIndex}`, доступы `GET/POST/DELETE /Playlists/{playlistId}/Users[/{userId}]`. В 12.0 плейлисты/коллекции вынесены из родительского элемента в отдельное хранилище (быстрее для больших списков).
|
||
- Люди/жанры/студии/годы: `GET /Persons?searchTerm=&personTypes=Actor&appearsInItemId=`, `GET /Persons/{name}`; `GET /Genres`, `GET /Genres/{genreName}`; `GET /Studios`, `GET /Studios/{name}`; `GET /Years`, `GET /Years/{year}`; музыка: `GET /Artists`, `/Artists/AlbumArtists`, `/MusicGenres/{genreName}` — **помечены deprecated в 12.0** (рекомендация релиза: `GetPersons`/`GetGenre`/`GetItems` с типами). Фильтрация по ним — через `/Items?genreIds=…`, `personIds=…`, `studioIds=…`.
|
||
- Картинки: `GET /Items/{itemId}/Images/{imageType}[/{imageIndex}]` с query `tag, maxWidth, maxHeight, width, height, fillWidth, fillHeight, quality, format (jpg|png|webp|…), blur, backgroundColor, foregroundLayer, percentPlayed, unplayedCount` (+ `HEAD`). Также «длинный» путь `/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}`. `ImageType`: `Primary, Art, Backdrop, Banner, Logo, Thumb, Disc, Box, Screenshot, Menu, Chapter, BoxRear, Profile`. Теги берите из `BaseItemDto.ImageTags{type→tag}`, `BackdropImageTags[]`, `ParentBackdropItemId/ParentBackdropImageTags`, `SeriesPrimaryImageTag`, `AlbumPrimaryImageTag`, `ParentLogoItemId/ParentLogoImageTag`, `ParentThumbItemId/ParentThumbImageTag` — `tag` даёт корректное кеширование/инвалидацию. **BlurHash:** `BaseItemDto.ImageBlurHashes {ImageType → {tag → blurhash}}` ([Dokka](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-model/org.jellyfin.sdk.model.api/-base-item-dto/image-blur-hashes.html)). Картинки людей/жанров/студий: `/Persons/{name}/Images/{imageType}`, `/Genres/{name}/Images/{imageType}`, `/Studios/{name}/Images/{imageType}`, `/Artists/{name}/Images/{imageType}/{imageIndex}`. **Gotcha 12.0:** сервер больше не апскейлит картинку выше исходного разрешения.
|
||
- Брендинг сервера для экрана логина: `GET /Branding/Configuration` (LoginDisclaimer, SplashscreenEnabled), `GET /Branding/Splashscreen`, `GET /Branding/Css`.
|
||
- Live TV (для полноты): `GET /LiveTv/Channels`, `/LiveTv/Channels/{channelId}`, `GET/POST /LiveTv/Programs`, `/LiveTv/Programs/Recommended`, запись/стрим: `/LiveTv/LiveRecordings/{recordingId}/stream`, `/LiveTv/LiveStreamFiles/{streamId}/stream.{container}`; для живого эфира PlaybackInfo с `autoOpenLiveStream=true` возвращает `LiveStreamId`, который надо закрывать (`POST /LiveStreams/Close`, не перепроверял путь). 12.0: Live TV больше не возвращает недостижимые server-local URL.
|
||
- Lyrics (10.9+): `GET /Audio/{itemId}/Lyrics` (`LyricDto`, с 10.11 — ELRC-cues по словам), `GET /Audio/{itemId}/RemoteSearch/Lyrics`, `GET /Providers/Lyrics/{lyricId}`; флаг `BaseItemDto.HasLyrics`.
|
||
|
||
---
|
||
|
||
## 5. Realtime: WebSocket и удалённое управление
|
||
|
||
- Эндпоинт: `ws(s)://<host>/socket` (константа `SOCKET_URL = "/socket"` в [DefaultSocketApi.kt](https://github.com/jellyfin/jellyfin-sdk-kotlin/blob/master/jellyfin-api/src/commonMain/kotlin/org/jellyfin/sdk/api/sockets/DefaultSocketApi.kt)). Авторизация — query-параметры (`ApiKey` + id устройства; при отключённом legacy сервер отвечает «Token is required», если прислать `api_key` — см. форум-тред выше). В OpenAPI самого `/socket` нет, но все схемы сообщений описаны (`WebSocketMessage`, `InboundWebSocketMessage`, `OutboundWebSocketMessage` и ~40 конкретных типов).
|
||
- Формат сообщения: JSON `{ "MessageType": "<SessionMessageType>", "MessageId": "<guid>", "Data": … }`. Enum `SessionMessageType` (12.2): `ForceKeepAlive, GeneralCommand, UserDataChanged, Sessions, Play, SyncPlayCommand, SyncPlayGroupUpdate, Playstate, RestartRequired, ServerShuttingDown, ServerRestarting, LibraryChanged, UserDeleted, UserUpdated, SeriesTimerCreated, TimerCreated, SeriesTimerCancelled, TimerCancelled, RefreshProgress, ScheduledTaskEnded, PackageInstallationCancelled/Failed/Completed, PackageInstalling, PackageUninstalled, ActivityLogEntry, ScheduledTasksInfo, ActivityLogEntryStart/Stop, SessionsStart/Stop, ScheduledTasksInfoStart/Stop, KeepAlive` ([Dokka SessionMessageType](https://kotlin-sdk.jellyfin.org/dokka/jellyfin-model/org.jellyfin.sdk.model.api/-session-message-type/index.html)).
|
||
- **KeepAlive:** сервер после подключения присылает `ForceKeepAlive` с `Data` = таймаут в секундах; клиент обязан периодически слать `{"MessageType":"KeepAlive"}` (`InboundKeepAliveMessage`), иначе соединение закроется. В Kotlin SDK это делает `DefaultSocketApi` автоматически, плюс реконнект по `SocketReconnectPolicy.ExponentialDelayReconnect`.
|
||
- **Подписки:** периодические данные надо явно запросить: `SessionsStart` с `Data: "initialDelayMs,intervalMs"` (например `"0,1500"`), `SessionsStop`; аналогично `ScheduledTasksInfoStart/Stop`, `ActivityLogEntryStart/Stop`. SDK шлёт start/stop сам при появлении/исчезновении подписчиков (интервал 1 с).
|
||
- События, важные для клиента: `UserDataChanged` (`UserDataChangeInfo {UserId, UserDataList[]}` — обновлять прогресс/избранное в UI), `LibraryChanged` (`LibraryUpdateInfo {ItemsAdded, ItemsUpdated, ItemsRemoved, FoldersAddedTo, FoldersRemovedFrom, CollectionFolders, IsEmpty}` — инвалидировать кэш), `ServerRestarting` / `ServerShuttingDown` / `RestartRequired` (показать баннер, переподключаться; HTTP во время рестарта отвечает **503 с `Retry-After`** — введено в 10.11), `RefreshProgress`, `UserUpdated/UserDeleted`.
|
||
- **Удалённое управление («Play to» / Cast на ваш клиент):** чтобы ваш клиент появился в списке целей у других клиентов, после логина отправьте возможности: `POST /Sessions/Capabilities?playableMediaTypes=Video,Audio&supportedCommands=…&supportsMediaControl=true&supportsPersistentIdentifier=true` или `POST /Sessions/Capabilities/Full` с телом `ClientCapabilitiesDto {PlayableMediaTypes, SupportedCommands[GeneralCommandType], SupportsMediaControl, SupportsPersistentIdentifier, DeviceProfile, AppStoreUrl, IconUrl}`. Далее по WebSocket приходят: `Play` (`PlayRequest {ItemIds, StartPositionTicks, PlayCommand: PlayNow|PlayNext|PlayLast|PlayInstantMix|PlayShuffle, MediaSourceId, AudioStreamIndex, SubtitleStreamIndex, StartIndex, ControllingUserId}`), `Playstate` (`PlaystateRequest {Command: Stop|Pause|Unpause|NextTrack|PreviousTrack|Seek|Rewind|FastForward|PlayPause, SeekPositionTicks}`), `GeneralCommand` (`GeneralCommandType`: `SetVolume, Mute/Unmute/ToggleMute, SetAudioStreamIndex, SetSubtitleStreamIndex, DisplayMessage, DisplayContent, GoHome, GoToSearch, Back, Select, MoveUp/Down/Left/Right, SetRepeatMode, SetShuffleQueue, SetPlaybackOrder, SetMaxStreamingBitrate, PlayTrailers, ToggleFullscreen, ToggleOsd, SendString, SendKey, …` с `Arguments` map). SDK даёт `api.webSocket.subscribeGeneralCommand(...)`, `subscribePlayStateCommands(...)`, `subscribeSyncPlayCommands(...)`, `subscribe<PlayMessage>()`.
|
||
- **Управление другими сессиями (ваш клиент как пульт):** `GET /Sessions?controllableByUserId=&activeWithinSeconds=`; `POST /Sessions/{sessionId}/Playing?playCommand=PlayNow&itemIds=…&startPositionTicks=…&mediaSourceId=&audioStreamIndex=&subtitleStreamIndex=&startIndex=`; `POST /Sessions/{sessionId}/Playing/{command}` (`PlaystateCommand`, `?seekPositionTicks=`); `POST /Sessions/{sessionId}/Command/{command}` или `POST /Sessions/{sessionId}/Command` (тело `GeneralCommand`); `POST /Sessions/{sessionId}/Message` (`MessageCommand {Header, Text, TimeoutMs}`); `POST /Sessions/{sessionId}/Viewing` («сейчас просматривает»); `POST/DELETE /Sessions/{sessionId}/User/{userId}`. Исходник: [SessionController.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/SessionController.cs). С 10.11 не-админ может подписываться на свои сессии.
|
||
- Заметки релизов: 12.0 — «WebSockets have moved to SDK subscriptions, duplicate `/socket` connections fixed»; 12.1 — закрытие сокета больше не роняет request handler.
|
||
|
||
---
|
||
|
||
## 6. Обнаружение серверов в LAN, публичная информация, мульти-сервер, reverse proxy / TLS
|
||
|
||
- **UDP discovery:** broadcast на порт **7359/UDP** строки `who is JellyfinServer?`; сервер отвечает JSON `ServerDiscoveryInfo { Address, Id, Name, EndpointAddress }` (модель: [ServerDiscoveryInfo.cs](https://github.com/jellyfin/jellyfin/blob/master/MediaBrowser.Model/ApiClient/ServerDiscoveryInfo.cs); схема есть и в OpenAPI). Эталон клиента — [LocalServerDiscovery.kt](https://github.com/jellyfin/jellyfin-sdk-kotlin/blob/master/jellyfin-core/src/jvmCommonMain/kotlin/org/jellyfin/sdk/discovery/LocalServerDiscovery.kt): `DISCOVERY_MESSAGE = "who is JellyfinServer?"`, `DISCOVERY_PORT = 7359`, буфер 1024 байта, таймаут 500 мс, максимум 15 серверов, `DatagramSocket` с broadcast. Работает только внутри подсети; на Android нужен `WifiManager.MulticastLock` для надёжного приёма (общая практика, не проверено в SDK).
|
||
- [Docs: Networking](https://jellyfin.org/docs/general/post-install/networking/) — порты 8096/TCP (HTTP), 8920/TCP (HTTPS, по умолчанию выключен), 7359/UDP (discovery, не настраивается), 1900/UDP (DLNA — отдельный плагин), замечание о «Known Proxies» и рекомендация терминировать HTTPS на reverse proxy.
|
||
- **Публичная информация без авторизации:** `GET /System/Info/Public` → `PublicSystemInfo {LocalAddress, ServerName, Version, ProductName, OperatingSystem, Id, StartupWizardCompleted}` (используйте `Id` как ключ сервера в вашем хранилище, `Version` — для проверки совместимости); `GET /System/Ping` (строка «Jellyfin Server»); `GET /System/Endpoint` (`IsLocal`, `IsInNetwork` — сервер сообщает, считает ли он вас локальным); `GET /Branding/Configuration`; `GET /Users/Public`. Полный `GET /System/Info` требует авторизации.
|
||
- **«Умный» подбор адреса (SDK):** [AddressCandidateHelper.kt](https://github.com/jellyfin/jellyfin-sdk-kotlin/blob/master/jellyfin-core/src/jvmCommonMain/kotlin/org/jellyfin/sdk/discovery/AddressCandidateHelper.kt) — из пользовательского ввода (`192.168.1.5`, `host`, `https://host/jellyfin`) строит кандидаты: добавляет `http://`/`https://`, порты 8096/8920, сохраняет base path; [RecommendedServerDiscovery.kt](https://github.com/jellyfin/jellyfin-sdk-kotlin/blob/master/jellyfin-core/src/commonMain/kotlin/org/jellyfin/sdk/discovery/RecommendedServerDiscovery.kt) — параллельно дёргает `/System/Info/Public`, измеряет `responseTime`, присваивает `RecommendedServerInfoScore` GREAT/GOOD/OK/BAD по версии (`< minimumServerVersion` → OK с issue `UnsupportedServerVersion`; устаревшая, но поддерживаемая → GOOD; HTTPS предпочтительнее HTTP) и возвращает `RecommendedServerInfo {address, responseTime, score, issues, systemInfo}`. Эта логика стоит копирования даже без SDK.
|
||
- **Мульти-сервер / мульти-пользователь:** в API нет понятия «аккаунта» — клиент сам хранит список `(serverId, baseUrl[, альтернативные URL LAN/WAN], userId, accessToken, deviceId)`. `AuthenticationResult.ServerId` и `PublicSystemInfo.Id` одинаковы — используйте для дедупликации одного сервера, найденного по разным адресам (так делают официальные клиенты). Для нескольких пользователей на одном устройстве используйте разные `DeviceId` (один токен на DeviceId). Токены не истекают сами, но могут быть отозваны в дашборде (с 12.1 отзыв устройства разлогинивает сессии) — на 401 показывать повторный логин.
|
||
- [Docs: Reverse Proxy](https://jellyfin.org/docs/general/post-install/networking/reverse-proxy/) — требования, влияющие на клиент: прокси должен пропускать **WebSocket** (`/socket`), корректно пробрасывать `X-Forwarded-For/Proto/Host` (иначе сервер считает всех «удалёнными» и может резать битрейт/доступ), поддержка base URL (например `https://example.com/jellyfin` — ваш клиент обязан уметь путь в baseUrl), HTTP/3 опционален. Все остальные гайды (Caddy/Nginx/Traefik/HAProxy/Apache) — подразделы этой страницы.
|
||
- **Self-signed сертификаты:** Jellyfin сам может слушать 8920 с PKCS#12-сертификатом, но документация настойчиво рекомендует TLS на прокси. На Android 7+ пользовательские CA не доверяются приложениям по умолчанию — нужен `network_security_config.xml` с `<certificates src="user"/>` и/или собственный trust manager в OkHttp (SDK использует OkHttp; как подменить `OkHttpClient` в SDK 1.9 — не проверено). Также учитывайте cleartext HTTP (`usesCleartextTraffic`) — большинство домашних серверов работают по http://.
|
||
- Поведение при старте/рестарте сервера: HTTP **503 + `Retry-After` (секунды)** — клиенту следует ретраить, а не показывать «сервер недоступен» (release notes 10.11).
|
||
|
||
---
|
||
|
||
## 7. Прочие полезные документы: фичи, клиенты, сообщество, release notes и ломающие изменения
|
||
|
||
**Фичи**
|
||
- [Docs: Media Segments](https://jellyfin.org/docs/general/server/metadata/media-segments) — см. раздел 3.
|
||
- [Docs: Quick Connect](https://jellyfin.org/docs/general/server/quick-connect/) — см. раздел 1.
|
||
- [Docs: Codec Support](https://jellyfin.org/docs/general/clients/codec-support/) — таблицы по клиентам.
|
||
- [Docs: Live TV](https://jellyfin.org/docs/general/server/live-tv/) и [Live TV Setup Guide](https://jellyfin.org/docs/general/server/live-tv/setup-guide/) — тюнеры (HDHomeRun, M3U), гайды (XMLTV, Schedules Direct); клиентской части там нет, API — см. раздел 4.
|
||
- [Docs: Music (lyrics)](https://jellyfin.org/docs/general/server/media/music/) — форматы `.lrc/.elrc/.txt`, синхронизированные/несинхронизированные тексты.
|
||
- [Docs: Books](https://jellyfin.org/docs/general/server/media/books/) — в 12.0 книги/комиксы стали нативными (плагин Bookshelf депрекирован).
|
||
- Отдельных страниц документации по **SyncPlay** и **Trickplay** на jellyfin.org нет (проверил несколько путей — 404). Источники: OpenAPI (тег SyncPlay: `POST /SyncPlay/New, Join, Leave, Ping, Ready, Buffering, Pause, Unpause, Seek, Stop, NextItem, PreviousItem, Queue, SetNewQueue, SetPlaylistItem, MovePlaylistItem, RemoveFromPlaylist, SetRepeatMode, SetShuffleMode, SetIgnoreWait`, `GET /SyncPlay/List`, `GET /SyncPlay/{id}` (10.11+)), WebSocket-сообщения `SyncPlayCommand`/`SyncPlayGroupUpdate`, серверный [SyncPlayController.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/SyncPlayController.cs) и эталонная реализация в веб-клиенте [jellyfin-web/src/plugins/syncPlay](https://github.com/jellyfin/jellyfin-web/tree/master/src/plugins/syncPlay) (синхронизация часов через `GET /GetUtcTime` и `SyncPlay/Ping`). Известные грабли: сервер ждёт `Ready` от всех участников до 30 с и ставит группу на паузу ([issue #18121](https://github.com/jellyfin/jellyfin/issues/18121) — упомянут в поиске, содержимое не читал).
|
||
- Trickplay: единственная «документация» — [release post 10.9.0](https://jellyfin.org/posts/jellyfin-release-10.9.0/) + контроллер/схемы (раздел 3). В 10.10 добавлено извлечение только по keyframes; в 10.11 trickplay-данные переехали в отдельную подпапку и чистятся при пересканировании.
|
||
|
||
**Клиенты и требования к листингу**
|
||
- [Downloads → Clients](https://jellyfin.org/downloads/clients/) — только официальные клиенты (Android, Android TV/Fire TV, Desktop/JMP, Kodi/JellyCon, iOS, Roku, WebOS, Tizen, Xbox) с вкладками Recommended/All.
|
||
- [Docs: Clients](https://jellyfin.org/docs/general/clients/) — **критерии для попадания стороннего клиента в официальный список**: Jellyfin как first-class (настраиваемый URL), FOSS-лицензия из списка FSF, ≥1 года активной разработки или заметный интерес, хотя бы одна уникальная фича относительно уже перечисленных для платформы, соблюдение community standards и branding; подача — PR в файл данных клиентов в репозитории jellyfin.org (точный путь файла не проверял).
|
||
- [Docs: Branding](https://jellyfin.org/docs/general/contributing/branding/) — **важно для именования вашего приложения**: нельзя называть продукт «Jellyfin …», избегать паттернов «Jelly[слово]» и «[слово]fin», нельзя использовать логотип без разрешения; можно писать «X, a Jellyfin client» и использовать цветовой градиент при уникальной форме логотипа. Ассеты — [jellyfin-ux](https://github.com/jellyfin/jellyfin-ux).
|
||
- [awesome-jellyfin CLIENTS.md](https://github.com/awesome-jellyfin/awesome-jellyfin/blob/main/CLIENTS.md) — большой список сторонних клиентов. Android: Findroid, Fladder, Jellyflix, AFinity, JellyCine, Linthra, Mellow, Moonfin, Coppelia, Finamp (музыка), Jellify, Fintunes, JellyBook и др.; Android TV: Elefin, Wholphin, Void, Fladder, Moonfin, Ghosten-Player, AfuseKt TV, Plezy, VidHub, Yamby. Полезно для анализа конкурентов и поиска «уникальной фичи» (libVLC-клиентов среди них почти нет).
|
||
- [Findroid](https://github.com/jarnedemeulemeester/findroid), [Streamyfin](https://github.com/streamyfin/streamyfin) — крупнейшие сторонние клиенты (Kotlin+SDK и React Native соответственно), полезны как референс поведения.
|
||
|
||
**Контрибьюция и сообщество**
|
||
- [Docs: Contributing](https://jellyfin.org/docs/general/contributing/) и [Development](https://jellyfin.org/docs/general/contributing/development/) — fork/feature-branch/PR, обсуждение крупных идей в [jellyfin-meta Discussions](https://github.com/jellyfin/jellyfin-meta/discussions), ссылки на style guides и API docs.
|
||
- [Contact](https://jellyfin.org/contact/) — Matrix (предпочтительно): `#jellyfin:matrix.org`, `#jellyfin-dev:matrix.org`, **`#jellyfin-dev-client:matrix.org`** и **`#jellyfin-dev-android:matrix.org`** (прямо про сторонние/Android-клиенты), `#jellyfin-troubleshooting`, Space `#jellyfinorg:matrix.org`; Discord-мост `https://discord.gg/zHBxVSXdBV`; форум [forum.jellyfin.org](https://forum.jellyfin.org) (раздел [Development](https://forum.jellyfin.org/f-development)); фичреквесты [features.jellyfin.org](https://features.jellyfin.org) (HEAD вернул 405 — сайт отвечает только на GET, ссылка рабочая).
|
||
- README SDK ссылается на Matrix-комнату `+jellyfin-android-dev:matrix.org` (space для Android-разработки).
|
||
|
||
**Release notes и ломающие изменения API (10.9 → 12.2)**
|
||
- [10.9.0 (2024-05-11)](https://jellyfin.org/posts/jellyfin-release-10.9.0/): Trickplay; корректные HTTP-коды у всех эндпоинтов; удалён EasyPassword (PIN) — остался Quick Connect; DLNA вынесен в плагин; плейлисты приватны по умолчанию.
|
||
- [10.10.0 (2024-10-26)](https://jellyfin.org/posts/jellyfin-release-10.10.0/): Media Segments (хранение на сервере, провайдеры — плагины); keyframe-only trickplay; выбор провайдеров лирики; удалены «network paths» в библиотеках (сломало Jellyfin4Kodi); клиенты в основном совместимы.
|
||
- [10.11.0 (2025-10-19)](https://jellyfin.org/posts/jellyfin-release-10.11.0/) + [GitHub v10.11.0](https://github.com/jellyfin/jellyfin/releases/tag/v10.11.0): депрекированы `OnPlaybackStart/Progress/Stopped` → `ReportPlayback*` (`/Sessions/Playing*`); добавлен серверный флаг для отключения legacy-авторизации (для тестирования клиентов); **503 + `Retry-After`** при старте/рестарте; `GET /System/Info/Storage`, Backup API, `GET /SyncPlay/{id}`; `ratingScore` для возрастных рейтингов; ELRC-лирика; `parentPrimaryImageItemId` стал Guid; явные дефолты enum'ов в моделях; генерируемые URL используют `ApiKey`; библиотека переехала на EF Core (`jellyfin.db`). Diff OpenAPI 10.10.7→10.11.11, который я сделал локально: удалён только `GET /System/WakeOnLanInfo`; добавлены `/Backup*`, `/SyncPlay/{id}`, `/System/Configuration/Branding`, `/System/Info/Storage`.
|
||
- [12.0 (2026-09-07)](https://jellyfin.org/posts/jellyfin-release-12.0/) + [GitHub v12.0](https://github.com/jellyfin/jellyfin/releases/tag/v12.0): смена версионирования (10.12 → 12.0; клиенты, парсящие версии, должны это учитывать); **legacy-авторизация выключена по умолчанию, в т. ч. миграцией на существующих серверах**; удалены префиксы `/emby/*`, `/mediabrowser/*`; удалены `GET /QuickConnect/Initiate` (только POST), `POST /Users/{userId}/EasyPassword`, `GET /Items/{itemId}/CriticReviews`, `GET /Environment/NetworkShares`, `POST /System/MediaEncoder/Path`, `GET /LiveTv/Recordings/Groups/{groupId}`; HLS-контроллеры скрыты из OpenAPI; Swashbuckle v10 (SDK надо регенерировать); `GetItems` асинхронный и меняет семантику `recursive`; `ItemByName`-ответы урезаны, люди дедуплицированы; `UserDto.HasPassword` бесполезен; `Accept-Language`; имена пользователей case-insensitive; картинки не апскейлятся; obsolete (пока работают): `GetTrailers`, `GetArtists/GetAlbumArtists`, `GetArtistByName`, `GetMusicGenre`, instant-mix по жанру, Startup-роуты. Политика: «не используйте эндпоинты/параметры, которых нет в OpenAPI или помеченные obsolete; депрекация держится один мажорный цикл». Мой локальный diff OpenAPI 10.11.11→12.2.0 (25 удалённых операций): все HLS (`/Videos/{id}/master.m3u8|main.m3u8|live.m3u8|hls/*|hls1/*`, `/Audio/{id}/master.m3u8|main.m3u8|hls/*|hls1/*`), `POST/DELETE /PlayingItems/{itemId}`, `POST /PlayingItems/{itemId}/Progress`, `GET /Artists/InstantMix`, `GET /MusicGenres`, `GET /LiveTv/Recordings/Groups`, `GET /LiveTv/Recordings/Series`, `GET /Items/{itemId}/CriticReviews`, `GET /Environment/NetworkShares`, `GET /Tmdb/ClientConfiguration`, `DELETE /Videos/ActiveEncodings`; добавлен `GET /Items/{itemId}/Collections`.
|
||
- [12.1 (2026-09-15)](https://github.com/jellyfin/jellyfin/releases/tag/v12.1): только фиксы — отзыв устройства разлогинивает сессии, forced-субтитры перекрывают запомненные, trickplay берёт правильный видеопоток, `/UserViews` не течёт по памяти, `Filters2` возвращает теги, фикс WebSocket.
|
||
- [12.2 (2026-10-05)](https://github.com/jellyfin/jellyfin/releases/tag/v12.2): фиксы — Dolby Vision p8 → тег `hvc1`, графические субтитры с Range, исправлено имя типа `BaseItemKind.Channel` (проверьте, если матчите строки типов), Next Up при повторном просмотре, фильтры по людям.
|
||
- [State of the Fin 2026-01-06](https://jellyfin.org/posts/state-of-the-fin-2026-01-06) — анонс обсуждения смены версии на 12.0, новый «Experimental» layout веба по умолчанию, тема через CSS-переменные.
|
||
|
||
---
|
||
|
||
## 8. Сторонние гайды / статьи («как написать клиент Jellyfin»)
|
||
|
||
Качественных полноценных туториалов мало; большинство материалов — справочники SDK. Что реально полезно:
|
||
- [Gist nielsvanvelzen — Jellyfin authorization](https://gist.github.com/nielsvanvelzen/ea047d9028f676185832e51ffaf12a6f) — лучший короткий текст по авторизации от мейнтейнера SDK (см. раздел 1).
|
||
- [Gist kylehowells — «Jellyfin Video Playback.md»](https://gist.github.com/kylehowells/74f538c766a244a3666319860a937030) — неформальная заметка: пример тела `POST /Items/{id}/PlaybackInfo?UserId=` с DeviceProfile, пример ответа с `MediaSources`, `TranscodingUrl` (`/videos/<id>/master.m3u8?...`), direct-play `/Videos/{mediaSourceId}/stream.mp4`, внешние субтитры `/Videos/<id>/Subtitles/5/0/Stream.vtt`. JSON в примерах «рукописный» (не парсится), версия сервера не указана, но как ориентир по потоку — ок. Последняя активность 06.2025.
|
||
- [Kotlin SDK Getting started](https://kotlin-sdk.jellyfin.org/guide/getting-started.html) / [Authentication](https://kotlin-sdk.jellyfin.org/guide/authentication.html) / [WebSockets](https://kotlin-sdk.jellyfin.org/guide/websockets.html) — официальные мини-туториалы; вместе с `samples/kotlin-cli` это фактически «how to write a client».
|
||
- [typescript-sdk.jellyfin.org](https://typescript-sdk.jellyfin.org/) — README с пошаговым сценарием: ClientInfo/DeviceInfo → `getAddressCandidates`/`getRecommendedServers` → `createApi` → `getPublicSystemInfo` → `authenticateUserByName` → `getUserViews` → logout. Хороший чеклист для порта.
|
||
- [mintlify.wiki Jellyfin API «overview»](https://mintlify.wiki/jellyfin/jellyfin/api/authentication/overview) — AI-сгенерированная документация; curl/JS/Python примеры логина корректны, но не считайте её официальной.
|
||
- [Jellywatch: What's New in 10.10/10.11](https://jellywatch.app/blog/jellyfin-10-10-10-11-upgrade-guide-new-features-2026), [Jellywatch: 12.0 release](https://jellywatch.app/blog/jellyfin-12-release-everything-you-need-to-know) — сторонний блог; обзорно, местами путаница «10.12 vs 12.0», проверяйте по официальным постам (не проверено содержание детально).
|
||
- [deepwiki: jellyfin-meta — OpenAPI endpoint domains & playback lifecycle](https://deepwiki.com/jellyfin/jellyfin-meta/3.2-openapi-specification:-endpoint-domains-and-playback-lifecycle) — AI-сгенерированная wiki по репозиторию; при двух попытках вернула 429, содержимое не проверено.
|
||
- [PR mia-cx/pendia #91](https://github.com/mia-cx/pendia/pull/91) — сторонний сервер, реализующий Jellyfin API (PlaybackInfo → DeviceProfile → direct/remux/transcode, `/Sessions/Playing*`); интересен как «обратная» документация того, что ожидают клиенты (октябрь 2026; содержимое PR не читал подробно).
|
||
- Сообщество Dart: [dart_jellyfin docs](https://pub.dev/documentation/dart_jellyfin/0.2.0/) — неофициальная библиотека с описанием Quick Connect-потока и замечанием про один токен на DeviceId.
|
||
|
||
---
|
||
|
||
## Краткий чеклист практических «граблей» для вашего клиента
|
||
1. Только `Authorization: MediaBrowser Client=…, Device=…, DeviceId=…, Version=…, Token=…`; в URL для libVLC — `?ApiKey=`. Никаких `X-Emby-*`/`api_key`.
|
||
2. `DeviceId` уникален на пару (установка, пользователь); иначе логины вытесняют друг друга.
|
||
3. Решение о способе воспроизведения принимает сервер по `DeviceProfile` в `POST /Items/{id}/PlaybackInfo`; используйте `TranscodingUrl` как есть, `PlaySessionId` — во всех отчётах и в `Ping`.
|
||
4. Все времена — тики по 100 нс; `PositionTicks = ms * 10_000`.
|
||
5. Отчёты: `POST /Sessions/Playing`, `/Progress` (периодически + при pause/seek), `/Stopped` (обязательно, иначе сессия «висит» и транскодер не освобождается).
|
||
6. Запрашивайте `fields=` явно (`MediaSources, MediaStreams, Chapters, Trickplay, Overview, Genres, People, …`) — по умолчанию `BaseItemDto` неполный.
|
||
7. Используйте `tag` в URL картинок и `ImageBlurHashes` для плейсхолдеров.
|
||
8. WebSocket `/socket`: отвечать `KeepAlive` на `ForceKeepAlive`, отправить `/Sessions/Capabilities(/Full)`, чтобы принимать `Play`/`Playstate`/`GeneralCommand`.
|
||
9. Обрабатывайте 503 + `Retry-After` и `ServerRestarting`.
|
||
10. Поддержка base path в URL сервера (`https://host/jellyfin`), cleartext HTTP, пользовательские CA (network security config).
|
||
11. Название приложения — не «Jellyfin…», не «Jelly*», не «*fin» (branding guide), иначе в официальный список клиентов не попадёте.
|
||
12. Для совместимости с 10.11 и 12.x одновременно SDK не подходит (1.8 ↔ 1.9 ломают друг друга); либо целиться только в 12.x с SDK 1.9, либо писать тонкий клиент по OpenAPI и держать совместимость самостоятельно (разница в путях между 10.11.11 и 12.2 — см. список выше).
|
||
|
||
Для анализа использовались OpenAPI-спецификации 10.10.7 / 10.11.11 / 12.2.0 и выдержки исходников SDK (в репозиторий не входят).
|
||
---
|
||
|
||
# Часть 2. Обзор open-source Jellyfin-клиентов и смежных проектов
|
||
|
||
Дата среза: 2026-10-11. Звёзды/даты последнего push взяты из GitHub API в этот день (если не указано иное). Всё, что не удалось подтвердить напрямую, помечено «(не проверено)».
|
||
|
||
## 2.0 Главные выводы
|
||
|
||
1. **Ни один живой Kotlin-клиент Jellyfin не использует libVLC как основной движок.** Официальный `jellyfin-androidtv` удалил libVLC в июне 2024 (PR #3639, релиз 0.17). Единственный актуальный Kotlin-клиент с libVLC-бэкендом — **JellyPlay** (raulshma/jellyplay, 31★, бета, `libvlc-all 3.7.5`), где VLC — третий «запасной» движок после ExoPlayer и libmpv. Все остальные крупные native-клиенты (Findroid, Wholphin, AFinity, Void, JellyCine) выбрали пару **media3/ExoPlayer + libmpv**.
|
||
2. **Лучший референс по DeviceProfile для VLC-движка — Swiftfin** (`Shared/Objects/VideoPlayerType/VideoPlayerType+VLC.swift`, `Shared/Extensions/JellyfinAPI/DeviceProfile.swift`): полные списки direct-play/transcoding/subtitle/codec-профилей для libVLC, режимы совместимости, пользовательские профили. Swiftfin в 2026 перешёл с VLCKit на **SwiftVLC (libVLC 4.0)** и добавил экспериментальный **MPV**-бэкенд (MPVUI).
|
||
3. **Лучший референс по DeviceProfile для Android (MediaCodec-зондирование, HDR, passthrough, known-defects по моделям)** — `jellyfin-androidtv/app/src/main/java/org/jellyfin/androidtv/util/profile/` (использует DSL `buildDeviceProfile` из jellyfin-sdk-kotlin) и `jellyfin-android/.../player/deviceprofile/DeviceProfileBuilder.kt`.
|
||
4. **Уроки удаления libVLC из jellyfin-androidtv**: (а) устаревший артефакт `libvlc-all 3.5.1` (2022) с незакрытым CVE — F-Droid поднял вопрос (issue #3623); (б) «плохая» реализация JNI-биндингов: объекты не всегда уничтожались → утечки и краши при use-after-destroy; (в) смешанный код ExoPlayer+VLC в одном `VideoManager.java` → проблемы с памятью; (г) размер APK 107 МБ → 24 МБ после удаления. Сейчас на Maven Central есть `org.videolan.android:libvlc-all` **3.7.7** (ветка 3.x) и **4.0.0-eap29** (обновлено 2026-10-01), т.е. причина (а) снята.
|
||
5. Пользователи требовали libVLC ради **ASS/SSA и PGS субтитров, DTS-HD/TrueHD, Dolby Vision в MKV**, которые ExoPlayer не тянул. Сегодня официальный клиент закрывает ASS через `io.github.peerless2012:ass-media` (libass для media3) — это альтернатива, которую стоит знать.
|
||
6. Брендинг: название «Jellyfin» и логотип — зарегистрированные товарные знаки Jellyfin, Inc.; сторонним приложениям нельзя называться «Jellyfin X», нежелательны «Jelly[word]»/«[word]fin», логотип (и его повороты/вставки) запрещён без разрешения; можно использовать градиент цветов и писать «клиент для Jellyfin» в описании.
|
||
|
||
---
|
||
|
||
## 2.1 Официальные проекты Jellyfin
|
||
|
||
### jellyfin-androidtv — официальный Android TV-клиент
|
||
| | |
|
||
|---|---|
|
||
| URL | https://github.com/jellyfin/jellyfin-androidtv |
|
||
| Лицензия | GPL-2.0 |
|
||
| Стек | Kotlin (565 .kt / 36 .java файлов на master), Koin DI, Coil 3, jellyfin-sdk-kotlin 1.8.12 |
|
||
| Плеер | media3/ExoPlayer 1.11.1 + `org.jellyfin.media3:media3-ffmpeg-decoder` (аудио DTS/TrueHD и т.п.) + `io.github.peerless2012:ass-media` 0.5.1 (libass). Внешние плееры через Intent: VLC, mpv, MX Player, Vimu (`ui/playback/external/*ExternalPlayerApi.kt`) |
|
||
| Цель | только TV (Android TV, Shield, Fire TV), minSdk 23 (с 0.20) |
|
||
| UI | гибрид: Leanback 1.2.0 + Compose 1.12.1 (в 0.20 настройки и MainActivity переведены на Compose; 36 XML-layout ещё остались) |
|
||
| Статус | активно: 4.6k★, push 2026-10-08; стабильная 0.19.10 (авг 2026), 0.20.0-beta.3 (24 сен 2026), требует сервер 10.11 |
|
||
|
||
**История libVLC (проверено по PR/issue):**
|
||
- До 0.16.x включительно в `app/.../ui/playback/VideoManager.java` жили оба движка: ExoPlayer и `org.videolan.libvlc.LibVLC/MediaPlayer` (артефакт `org.videolan.android:libvlc-all:3.5.1`). VLC инициализировался опциями `--network-caching`, `--audio-time-stretch`, `--avcodec-skiploopfilter`, `--android-display-chroma`, `--vout=android-opaque,android-display`, опциональный `--video-filter=deinterlace`; субтитры рисовались во второй `SurfaceView` (`setSubtitlesView`), выбор дорожек через `getSpuTracks()/setSpuTrack()`, задержка через `setSpuDelay`. Полезно как список граблей, а не как образец.
|
||
- 2023-02-23 PR #2534 «Mark LibVLC as experimental»: «много крашей, потому что наша реализация просто плохая».
|
||
- Discussion #2546 (2023-02): мейнтейнер nielsvanvelzen — JNI-биндинги «не всегда корректно уничтожаются» → утечки/краши; старый код playback «end-of-life»; новый playback-модуль стартует только с ExoPlayer; libVLC/mpv «возможно позже, но с нуля, с тестами».
|
||
- 2024-06-02 PR #3639 «Remove LibVLC» (релиз 0.17.0), закрывает issue #3623 «CVE-2023-5217 — update libvlc» (поднято из-за F-Droid): последняя стабильная libvlc-jni 3.5.1 от августа 2022, незакрытая CVE; «смешение ExoPlayer и LibVLC могло вызывать проблемы с памятью»; артефакты сборки 107 МБ → 24 МБ. Мейнтейнер: «LibVLC может вернуться, но возможно выберем mpv или FFmpeg».
|
||
- Issue #3937 (авг 2024, «вернуть выбор плеера») закрыт как not planned. Форумный тред «SOLVED: Bring back libvlc» — совет ставить 0.16.11 либо внешний VLC/mpv.
|
||
- Issue #3888: после удаления гибридные DV/HDR10-файлы дают чёрный экран в ExoPlayer — closed/not planned.
|
||
|
||
**Что заимствовать:**
|
||
- Архитектуру playback-модулей: `playback/core` (PlaybackManager, PlayerBackend-абстракция, очередь с QueueSupplier/OrderIndexProvider, MediaStreamResolver, TimedEventService), `playback/jellyfin` (JellyfinMediaStreamResolver, PlaySessionService + socket-репортинг, MediaSegmentService, lyrics), `playback/media3/exoplayer` (ExoPlayerBackend, `support/*` — отчёт о поддержке формата, `mapping/*` — маппинг кодеков) и `playback/media3/session`. Это готовый чертёж, куда вместо ExoPlayerBackend подставляется VlcBackend.
|
||
- `util/profile/deviceProfile.kt` + `MediaCodecCapabilitiesTest`, `codec/*CodecCapabilities.kt`, `audioPassthrough.kt`, `KnownDefects.kt` (чёрные списки моделей Fire TV с багом DoVi/HDR10+, Shield с нераспознанным DV Profile 7). Профиль учитывает downmix, bitstream AC3/EAC3/DTS/TrueHD (auto-детект passthrough), `assDirectPlay`/`pgsDirectPlay`, пользовательские AVC/HEVC level override, принудительное включение/выключение HDR-форматов, два transcoding-профиля HLS (TS и fMP4) с `enableSubtitlesInManifest`.
|
||
- Внешние плееры: `VlcExternalPlayerApi.kt` реализует интенты VLC (`subtitles_location`, `position`, коды результата) — полезно для fallback «открыть в VLC».
|
||
- Боли из issues: TV-навигация и фокус на Leanback тяжело поддерживать (поэтому идёт миграция на Compose), нестабильность DV/HDR на Fire TV, отсутствие субтитров нужных форматов в ExoPlayer.
|
||
|
||
### jellyfin-android — официальный мобильный клиент
|
||
| | |
|
||
|---|---|
|
||
| URL | https://github.com/jellyfin/jellyfin-android |
|
||
| Лицензия | GPL-2.0 |
|
||
| Стек | Kotlin, WebView-обёртка jellyfin-web + нативный плеер; Koin 4.2.2, Compose 1.12.1 (частично), jellyfin-sdk 1.8.12 |
|
||
| Плеер | media3/ExoPlayer 1.11.1 (+hls, +cast, +ffmpeg-decoder), Chromecast только в proprietary-flavor |
|
||
| Цель | телефон/планшет |
|
||
| Статус | 2.8k★, push 2026-10-09, активен |
|
||
|
||
**Что заимствовать:** `app/src/main/java/org/jellyfin/mobile/player/deviceprofile/DeviceProfileBuilder.kt` — профиль строится динамически по `MediaCodecList`: таблица контейнер→допустимые видео/аудио-кодеки, `CodecProfile` c `VIDEO_PROFILE` из реальных возможностей декодера, набор `FORCED_AUDIO_CODECS` (кодеки от FFmpeg-расширения, которые MediaCodecList не объявляет), отдельный `getExternalPlayerProfile()` (всё direct-play, без transcoding — ровно то, что нужно для VLC-режима «играй всё сам»), профили субтитров EMBED/EXTERNAL с опцией `exoPlayerDirectPlayAss`. Также `player/source/MediaSourceResolver.kt`, `ExternalSubtitleStream.kt`, `QueueManager`, `TrickplayHelper`, `mediasegments/`, `PlayerService`/MediaSession.
|
||
|
||
### jellyfin-sdk-kotlin
|
||
| | |
|
||
|---|---|
|
||
| URL | https://github.com/jellyfin/jellyfin-sdk-kotlin (docs: https://kotlin-sdk.jellyfin.org) |
|
||
| Лицензия | LGPL-3.0 |
|
||
| Стек | Kotlin, Android + JVM; модули `jellyfin-core`, `jellyfin-api`, `jellyfin-model`, `jellyfin-api-okhttp`; код генерируется из `openapi.json` |
|
||
| Версия | **1.9.2** на Maven Central (обновлено 2026-10-06); клиенты в основном на 1.8.12 (10.11 SDK) |
|
||
| Статус | 125★, push 2026-10-08 |
|
||
|
||
**Что заимствовать:** использовать как есть. Есть DSL `org.jellyfin.sdk.model.deviceprofile.buildDeviceProfile { directPlayProfile {…}; transcodingProfile {…}; codecProfile { conditions { … } } }` (видно в jellyfin-androidtv), server discovery, socket API. Binary-compatibility validator — пример дисциплины для публичного API модуля плеера.
|
||
|
||
### Swiftfin (iOS/tvOS) — главный референс по VLC-профилям
|
||
| | |
|
||
|---|---|
|
||
| URL | https://github.com/jellyfin/Swiftfin ; docs: https://github.com/jellyfin/Swiftfin/blob/main/Documentation/players.md |
|
||
| Лицензия | MPL-2.0 |
|
||
| Стек | Swift, SwiftUI, jellyfin-sdk-swift 3.3.0 |
|
||
| Плеер | три бэкенда: `VideoPlayerType.avPlayer` (native), `.vlc` — **SwiftVLC 1.0.0 (harflabs/SwiftVLC, libVLC 4.0, MIT)**, `.mpv` — **MPVUI 0.5.0 (LePips/MPVUI)**, пока за флагом `Experimental.mpvPlayer`. Прокси: `Shared/Objects/MediaPlayerManager/MediaPlayerProxy/MediaPlayerProxy+{VLC,MPV,AVPlayer}.swift` |
|
||
| Цель | iOS 18+, tvOS 26+ |
|
||
| Статус | 4.2k★, push 2026-10-10, активен |
|
||
|
||
**Что заимствовать (конкретные файлы):**
|
||
- `Shared/Objects/VideoPlayerType/VideoPlayerType+VLC.swift`: `_vlcDirectPlayProfiles` (30 аудио- и ~25 видео-кодеков, AV1 только при `PlaybackCapabilities.supportsAV1`), `_vlcTranscodingProfiles` (HLS, `isBreakOnNonKeyFrames`, `maxAudioChannels 8`, `minSegments 2`, контейнер mp4; **отдельный профиль для VC1**, чтобы обойти серверное ограничение `_supportedHlsVideoCodecs`), `_vlcSubtitleProfiles` (embed: ass, ssa, pgssub, dvbsub, dvdsub, subrip, vtt, ttml, mov_text, sami, teletext…; external: subrip, ass, ssa, …; encode/burn-in: dvbsub, dvdsub, pgssub, vtt, xsub — т.е. внешние bitmap-субтитры VLC не грузит, их прожигают), `_vlcCodecProfiles` (условия `videoRangeType` для h264/hevc/av1/vp9, `isAnamorphic`, `isInterlaced`, список HDR-режимов `vlcHDRProfiles`: HLG/HDR10/HDR10+/DoVi-with-*).
|
||
- `Shared/Extensions/JellyfinAPI/DeviceProfile.swift`: `DeviceProfile.build(for:compatibilityMode:maxBitrate:maxResolution:)` — режимы `auto / mostCompatible / directPlay / custom`, принудительный burn-in субтитров (`forceSubtitleBurnIn`), методы `canPlay(...)` для проверки direct-play локально; `PlaybackDeviceProfile.swift` — пользовательские профили в UI (`CustomDeviceProfilesView`).
|
||
- `MediaPlayerProxy+VLC.swift`: выбор дорожек по индексу `MediaStream.index` ↔ `player.audioTracks/subtitleTracks`, `setAudioDelay`/`setSubtitleDelay`, протоколы `MediaPlayerSubtitleConfigurable`/`MediaPlayerOffsetConfigurable`; `TextSubtitlePresentation.swift` — overlay текстовых субтитров поверх MPV (у VLC субтитры рисует сам движок).
|
||
- Документ players.md фиксирует ограничения VLC: нет TLS 1.3 в libvlc, нет DTS-HD/TrueHD/MLP, нет PiP, external-display только зеркалом, повышенное энергопотребление при software-decoding, DV Profile 5 не поддерживается, HDR→SDR tonemapping «точность цвета может плавать».
|
||
|
||
### jellyfin-web / jellyfin-vue
|
||
- **jellyfin-web** — https://github.com/jellyfin/jellyfin-web, GPL-2.0, JS/TS (webpack+vite), 3.9k★, push 2026-10-11, v13.0.0. Плеер — HTML5 `<video>` + `hls.js 1.6.16`, ASS через `@jellyfin/libass-wasm 4.2.4`; профиль устройства — `src/scripts/browserDeviceProfile.js`. Полезен как «эталонная» логика выбора direct-play/transcode и как источник семантики полей DeviceProfile.
|
||
- **jellyfin-vue** — https://github.com/jellyfin/jellyfin-vue, GPL-3.0, Vue/Vite, 1.7k★, push 2026-10-11; «экспериментальный, стабильных релизов нет». `hls.js 1.7.3` в `packages/frontend/package.json`; libass/JASSUB — (не проверено).
|
||
|
||
### jellyfin-desktop (бывш. jellyfin-media-player) и jellyfin-mpv-shim
|
||
- **jellyfin-media-player → переименован в jellyfin-desktop** (старый URL редиректит): https://github.com/jellyfin/jellyfin-desktop, GPL-2.0, Qt WebEngine + libmpv, ~5.9k★. jellyfin-web в WebEngine + нативный mpv; пользовательский `mpv.conf`. Заимствовать: идею «веб-UI + нативный движок» не стоит, но полезны настройки passthrough/HW-decoding.
|
||
- **jellyfin-mpv-shim** — https://github.com/jellyfin/jellyfin-mpv-shim, Python, 2.5k★, push 2026-10-11. Cast-target + полноценный клиент; запоминание аудио/субтитров на сериал, задержка субтитров, SyncPlay (хрупкий), passthrough через `audio_mode hdmi/optical`, шейдер-паки. Заимствовать: UX «запомнить выбранные дорожки для всего сериала».
|
||
|
||
### jellyfin-roku, jellyfin-kodi / JellyCon, jellyfin-ios (ex-expo)
|
||
- **jellyfin-roku** — https://github.com/jellyfin/jellyfin-roku, GPL-2.0, BrighterScript, 236★, push 2026-10-10; нативный Roku Video node; есть trickplay, media segments, загрузка субтитров. Полезен как ещё одна TV-реализация D-pad-навигации, но стек не переносим.
|
||
- **jellyfin-kodi** — https://github.com/jellyfin/jellyfin-kodi, GPL-3.0, Python, 1.1k★, push 2026-10-11 — синхронизация библиотеки в БД Kodi (+ динамические списки); **JellyCon** — https://github.com/jellyfin/jellycon, GPL-2.0, Python, 559★ — чисто динамический браузинг. Плеер — внутренний Kodi (ffmpeg). Интересны как пример «offline-first» кэша метаданных.
|
||
- **jellyfin-expo** → редирект на **jellyfin-ios**: https://github.com/jellyfin/jellyfin-ios, MPL-2.0, Expo/React Native, 603★, push 2026-10-10 — web-wrapper для iOS с нативными расширениями.
|
||
|
||
---
|
||
|
||
## 2.2 Сторонние нативные Android-клиенты (Kotlin)
|
||
|
||
### Findroid
|
||
| | |
|
||
|---|---|
|
||
| URL | https://github.com/jarnedemeulemeester/findroid |
|
||
| Лицензия | GPL-3.0 |
|
||
| Стек | Kotlin, Compose + Material 3 (v1.0.0 «Compose» 30.12.2025), Hilt 2.60, Room 3.0.3, Paging, jellyfin-sdk 1.8.12 |
|
||
| Плеер | media3/ExoPlayer 1.11.1 + `org.jellyfin.media3:media3-ffmpeg-decoder`; **libmpv** через собственный артефакт `dev.jdtech.mpv:libmpv 1.0.0` (репо https://github.com/jarnedemeulemeester/libmpv-android, MIT, на базе mpv-android); только direct play, транскодинга нет |
|
||
| Цель | телефон; **в репо есть модуль `app/tv` (55 файлов, `androidx.tv:tv-material 1.1.0`)**, но README по-прежнему пишет «Android TV — planned» |
|
||
| Статус | 4.3k★, push 2026-10-10; v1.1.0 (18.07.2026): редактирование `mpv.conf`/`input.conf` из настроек, системные шрифты для mpv; v1.0.0 требует сервер 10.11 (Media Segments API) |
|
||
|
||
**Что заимствовать:** модульная структура `app/{phone,tv}`, `core`, `data`, `player/{core,local}`, `setup`, `settings`, `modes/film`. Главное — `player/local/src/main/java/dev/jdtech/jellyfin/player/local/mpv/MPVPlayer.kt`: **реализация `androidx.media3.common.Player` (через `BasePlayer`) поверх стороннего движка** с `ListenerSet`, audio focus, duck, `Commands`; тот же приём стоит повторить для libVLC (`VlcPlayer : BasePlayer`), чтобы переиспользовать media3-session, PlayerView-подобные контролы и уведомления. Также: offline downloads с локальными картинками, trickplay, Media Segments skip/auto-skip, `MpvCleanupWorker`. Боли из README: «SSA/ASS в ExoPlayer — ограниченная стилизация (google/ExoPlayer#8435)», отсюда и mpv.
|
||
|
||
### Wholphin
|
||
| | |
|
||
|---|---|
|
||
| URL | https://github.com/damontecres/Wholphin |
|
||
| Лицензия | GPL-2.0 |
|
||
| Стек | Kotlin, single-Activity MVVM, **Compose (BOM 2026.09) + `androidx.tv:tv-foundation/tv-material`**, Navigation 3, Hilt, Room, DataStore(proto), Coil, OkHttp, jellyfin-sdk 1.7.1 |
|
||
| Плеер | media3/ExoPlayer 1.11.1 (+dash, +hls, `media3-ui-compose`, `media3-effect`) + собственные расширения `com.github.damontecres.wholphin.media3:decoder-ffmpeg/decoder-av1` + **libmpv** (`wholphin-mpv`, с `wholphin-mpv-stub`, чтобы собираться без нативов) + `ass-media` 0.5.1 (libass) |
|
||
| Цель | только TV (Android TV/Fire TV, minSdk 23), сервер 10.10/10.11 |
|
||
| Статус | 3.0k★, push 2026-10-11, очень активен; «не форк официального клиента, UI в духе Plex» |
|
||
|
||
**Что заимствовать:** эталон современного **Compose-for-TV** клиента без Leanback: кастомный home с перестановкой рядов, Plex-подобные контролы плеера под D-pad, trickplay, Live TV/DVR, PIN-профили, screensaver. По их README они **адаптировали код direct-play DeviceProfile из официального клиента** — удобная «вырезка». `DEVELOPMENT.md` описывает слоистые настройки (proto DataStore / Room / key-value) и схему experimental-флагов. Паттерн «stub-модуль для нативного движка» (`wholphin-mpv-stub`) идеально переносится на libVLC (сборка без 60-мегабайтного .aar для CI/превью).
|
||
|
||
### JellyPlay — единственный живой Kotlin-клиент с libVLC
|
||
| | |
|
||
|---|---|
|
||
| URL | https://github.com/raulshma/jellyplay ; docs: `docs/player-engines.md` |
|
||
| Лицензия | GPL-3.0 (в LICENSE; GitHub API показывает NOASSERTION) |
|
||
| Стек | Kotlin Multiplatform + Compose Multiplatform; Android — Compose/Material 3 + Compose for TV; Koin 4.2.2; ~25 feature-модулей в `shared/feature/*`, `shared/core/player-contract` с engine-agnostic интерфейсом `MediaEngine`; jellyfin-sdk 1.8.12 |
|
||
| Плеер | **три движка на выбор per device**: media3/ExoPlayer 1.10.1 (+ffmpeg-decoder), libmpv (`io.github.abdallahmehiz:mpv-android-lib 0.1.12`), **libVLC `org.videolan.android:libvlc-all 3.7.5`**; desktop — libmpv через JNA |
|
||
| Цель | телефон + Android TV/Fire TV + Windows preview |
|
||
| Статус | 31★, 1 203 коммита, push 2026-10-11, «Active Beta» |
|
||
|
||
**Что заимствовать:** `docs/player-engines.md` — честная таблица компромиссов, в т.ч. **минусы libVLC на Android по их опыту**: нет runtime-аудиоэффектов (только стартовые `--audio-filter`), ограниченная стилизация субтитров (нет ASS override, цветов, border-style), **нет нативной поддержки trickplay sprite-sheets**, «реже Android-специфичные обновления», LL-HLS хуже, нет mini-player; плюсы — широчайшая поддержка контейнеров, HW-декодирование на большинстве чипов, passthrough и audio-delay, фильтры яркость/контраст, встроенный renderer/cast. Также: сохранение выбора дорожек при смене движка/транскода, WorkManager-загрузки с Range-resume, SyncPlay с коррекцией скорости. Минус как референса: мало пользователей, один открытый issue — реальная обкатка слабая.
|
||
|
||
### AFinity
|
||
- https://github.com/MakD/AFinity — GPL-3.0, Kotlin, Compose/M3, MVVM+Repository, Hilt, Room, Retrofit+SDK, Tink для секретов; **libmpv (libass для ASS) + ExoPlayer с FFmpeg-декодером**, переключаемые. Телефон/планшет, **Android 15+**. 243★, push 2026-10-10. Заимствовать: offline-home, download manager, SyncPlay, Chromecast, Live TV EPG; боль — release-сборка без `key.properties` подписывается debug-ключом.
|
||
|
||
### Void for Jellyfin
|
||
- https://github.com/hritwikjohri/Void-for-jellyfin — GPL-3.0, Kotlin, Compose/M3, Clean Architecture, Hilt, Room, WorkManager; плеер — **mpv через `dev.marcelsoftware.mpvcompose:mpv-compose`**, fallback media3 1.9.0. minSdk 26, телефон. ~474★ (по странице), ранняя альфа. Полезен как компактный пример mpv-in-Compose.
|
||
|
||
### JellyCine
|
||
- https://github.com/sureshfizzy/JellyCine — GPL-3.0, Kotlin 2.3 Multiplatform (Android + iOS в разработке), Compose/M3, Hilt+KSP, Ktor; плеер — **кастомный форк mpv + fallback media3**; HDR10/HDR10+/DV, passthrough TrueHD/DTS-HD; поддержка **Jellyfin и Emby** с авто-разрешением эндпоинтов; **модуль `tv/` на Leanback/D-pad** отдельно от `phone/`. 109★, push 2026-10-10. Заимствовать: разделение phone/tv с общим `core/` (shared player), Emby-совместимость.
|
||
|
||
### Mellow
|
||
- https://github.com/Malinskiy/mellow — Apache-2.0, Kotlin, Compose/M3, media3; offline-first музыкальный плеер с Android Auto, `MediaLibraryService`, screenshot-тесты на Robolectric (168 кейсов), in-app обновления с проверкой SHA-256 и ключа подписи. 26★, push 2026-10-07. Заимствовать: тестовую инфраструктуру и безопасный self-update.
|
||
|
||
### Форки официального Android TV клиента
|
||
- **JellyTV** — https://github.com/d8f6/JellyTV, GPL-2.0, форк jellyfin-androidtv с целью «вернуть libVLC по умолчанию и убрать телеметрию»; **все цели не выполнены**, 0★, последний push 2025-02-21 — мёртв, но подтверждает спрос.
|
||
- **CookeInCode/jellyfin-androidtv** (форум Jellyfin, 2024) — UI-урезанный форк с другим package name; мейнтейнер советовал апстримить. (не проверено состояние)
|
||
- **Moonfin**: ранее Kotlin-форк `Moonfin-Client/AndroidTV-FireTV` (~624★ по сторонним агрегаторам) — **репозиторий сейчас 404**; организация https://github.com/Moonfin-Client перешла на **Moonfin-Core** (Flutter/Dart, GPL-2.0+, 821★, push 2026-10-11) — единая кодовая база для Android TV/Apple TV/мобильных/десктопа/веб, Jellyfin 10.8+ и Emby 4.8+, mpv (`mpv.conf` в wiki; движок явно не назван — не проверено), «premium»-фичи через серверный плагин Moonbase, DV Profile 7 конвертация на лету на Apple/Android TV, OTA-обновления.
|
||
|
||
### Komorebi (не Jellyfin, но libVLC + Compose for TV)
|
||
- https://github.com/BeerEgg2001/Komorebi — MIT, Kotlin, **Compose for TV (TV Material3)**, Hilt, media3 (с FFmpeg-расширением) как основной движок и **libVLC для SMB/файловых библиотек**; клиент японского DTV (KonomiTV/EDCB/EPGStation). 118★, push 2026-10-08. Ценно: кастомный `FocusTicketManager` для стабильного фокуса под пультом, JNI-рендер ARIB-субтитров; боль — **libVLC сильно раздувает APK, Universal-сборка не ставится на слабые устройства → отдельные ABI-сборки; Fire TV — 32-битная ОС, arm64 не ставится**.
|
||
|
||
---
|
||
|
||
## 2.3 Кроссплатформенные клиенты (React Native / Flutter)
|
||
|
||
### Streamyfin
|
||
| | |
|
||
|---|---|
|
||
| URL | https://github.com/streamyfin/streamyfin |
|
||
| Лицензия | MPL-2.0 |
|
||
| Стек | Expo 57 / React Native (react-native-tvos 0.86), TypeScript, Bun, модули в `modules/` |
|
||
| Плеер | сейчас **MPV (MPVKit на Apple, нативный mpv-модуль `modules/mpv-player`)** как основной; с 0.55.0 (04.10.2026) — «полностью нативный плеер»: SwiftUI на Apple, **Jetpack Compose на Android/Android TV**, плюс opt-in **ExoPlayer-движок на Android TV** (`modules/exoplayer-player`, HDR10/HDR10+, DV P5, Atmos passthrough); KSPlayer как опция на iOS. До 0.51.0 (янв 2026) основным был **VLC** (релиз 0.51 говорит об «improved native VLC integration» на Apple); использование libVLC на Android в старых версиях — (не проверено) |
|
||
| Цель | iOS, tvOS, Android, Android TV |
|
||
| Статус | 5.3k★, push 2026-10-11, самый популярный сторонний клиент |
|
||
|
||
**Что заимствовать:** эволюция движков VLC → MPV → нативный Compose+ExoPlayer на TV — показатель, что команда ушла от libVLC (причины публично не раскрыты, кроме «лучше HW-декодирование/PiP»). Полезны: выбор дорожек «по идентичности, а не позиции», матчинг языков с регионом (pt-BR vs pt-PT), стилизация субтитров, поиск/скачивание субтитров в плеере, серверный плагин для синхронизации настроек, авторизация через кастомные заголовки (Cloudflare Zero Trust). Боли: загрузки через HLS→ffmpeg в реальном времени (медленно), Chromecast без субтитров.
|
||
|
||
### Fladder
|
||
- https://github.com/DonutWare/Fladder — GPL-3.0, Flutter/Dart, Android + **Android TV** + desktop + web/Docker; 2.6k★, push 2026-09-18. Плеер не назван в README (media_kit/mpv — не проверено); в числе пользователей `libass-android` (по README peerless2012). Фичи: direct/transcode/offline, Media Segments, trickplay, CBZ/CBR, Seerr. Боль: «улучшить D-pad-навигацию на не-TV» в планах, Chromecast не готов.
|
||
|
||
### Finamp (музыка)
|
||
- https://github.com/finamp-app/finamp (старый URL UnicornsOnLSD/finamp редиректит) — MPL-2.0, Flutter, 4.3k★, push 2026-10-11; Android/iOS/desktop; офлайн-загрузки, транскодинг, gapless, ReplayGain, Android Auto/CarPlay, отчёт прослушиваний офлайн (Playback Reporting plugin). Библиотека аудио (just_audio?) — (не проверено).
|
||
|
||
### Jellyflix
|
||
- https://github.com/jellyflix-app/jellyflix — GPL-3.0, Flutter, 466★, push 2026-09-17; mobile+desktop; офлайн-загрузки, HDR tonemapping; на Linux требует libmpv → движок mpv (вывод по зависимостям, media_kit — не проверено).
|
||
|
||
### Plezy (Plex + Jellyfin + Emby)
|
||
- https://github.com/edde746/plezy — GPL-3.0, Flutter; desktop/mobile/**Android TV, Apple TV**; ~2.1k★ (сторонний агрегатор). **Android: ExoPlayer по умолчанию, mpv опционально (нужен для шейдеров), ASS через libass-android; desktop — mpv (форк MPVKit); iOS/tvOS — mpv недоступен**. Watch Together, Quick Connect, мультисервер. Заимствовать: подход «ExoPlayer default + тяжёлый движок opt-in».
|
||
|
||
### Музыкальные RN/Java
|
||
- **Fintunes** — https://github.com/leinelissen/jellyfin-audio-player, MIT, TypeScript/RN, 1.4k★, push 2026-07-17 (react-native-track-player — не проверено).
|
||
- **Jellify** — https://github.com/Jellify-Music/App, MIT, RN, 1.8k★, push 2026-10-05.
|
||
- **Gelli** — https://github.com/dkanada/gelli, GPL-3.0, Java, 534★, **архив с 2023-06**.
|
||
|
||
---
|
||
|
||
## 2.4 Закрытые / коммерческие и прочие упомянутые
|
||
|
||
- **Symfonium** — проприетарный, offline-first музыкальный клиент (Jellyfin/Emby/Plex/Kodi), есть экспериментальная сборка под Android TV (support.symfonium.app). Движок видео — (не проверено).
|
||
- **Yatse** — проприетарный пульт/клиент (Jellyfin, Kodi, Plex, Emby) с Chromecast; тот же автор, что Symfonium. Код закрыт — заимствовать нечего.
|
||
- **JellyWatch TV** — закрытый freemium Android TV-клиент для Emby и Jellyfin (jellywatch.app; тема на emby.media). Исходников нет.
|
||
- **Infuse** (Firecore) — закрытый; VidHub, Hills, AfuseKt, **Yamby** — закрытые (так помечены в awesome-jellyfin). **Kinoplex**, **JellyCompose/«Jellyfin Compose»**, **lxwiq/JellyFish (KMP)**, **VoidStream-FireTV** — репозитории не найдены (404 / нет в поиске).
|
||
- **Emby**: репозиторий `MediaBrowser/MediaBrowser.AndroidTv` (когда-то открытый, 2015) сейчас **404**; текущие Emby-клиенты закрыты. На форуме Emby: встроенный плеер — ExoPlayer, VLC предлагается как внешний.
|
||
- **Plex-клиенты на libVLC** — открытых не найдено; Plezy использует ExoPlayer/mpv.
|
||
- **awesome-jellyfin/CLIENTS.md** дополнительно перечисляет для Android: Coppelia, Ghosten-Player (AGPL, Dart, 3.3k★ — универсальный плеер, Jellyfin-поддержка не проверена), JellyBook, JellyBox, Linthra, Litefin (Tizen/webOS), Breezyfin (webOS), JellyRock (Roku), Kofin PVR (Kodi).
|
||
|
||
---
|
||
|
||
## 2.5 Плееры и библиотеки-референсы для интеграции libVLC
|
||
|
||
- **VLC for Android** — https://github.com/videolan/vlc-android, GPL-2.0+ (libVLC — LGPL-2.1; README: «Android-библиотеки делают приложение де-факто GPLv3»), Kotlin, 4.1k★, push 2026-10-07; Android TV входит (UI — Leanback, не проверено). Разработка в GitLab code.videolan.org (там же `libvlcjni`, `libvlc-android-samples`). Единственный полноценный пример продакшен-использования libVLC на TV: вывод через `VLCVideoLayout`, передача `MediaPlayer` между Activity и сервисом, passthrough SPDIF/HDMI, HDR→SDR tonemapping, 8K.
|
||
- **Артефакт** `org.videolan.android:libvlc-all` на Maven Central: ветка 3.x — **3.7.7** (3.7.0…3.7.7 за 2025–26), ветка 4.x — **4.0.0-eap29** (metadata lastUpdated 2026-10-01). Выбор версии: 3.7.x стабильна (lunarr/vlc-player и JellyPlay сидят на 3.7.5), 4.0 EAP даёт новый API треков/рендереров (как SwiftVLC), но пока pre-release.
|
||
- **lunarr-app/vlc-player** — https://github.com/lunarr-app/vlc-player, MIT, Kotlin+Swift, RN New Architecture; оборачивает **libvlc-all 3.7.5 / MobileVLCKit 3.7.3 / TVVLCKit 3.7.3**; API: `onTracks`, `selectSubtitleTrack(-1)`, `setSubtitleFile`, subtitle/audio delay, `VLCHardwareDecoder {Automatic, Disabled, DecodeOnly, Full}`, эквалайзер, snapshot, фоновое аудио; Android TV поддержан, minSdk 24. Компактный и свежий (push 2026-09-02) Kotlin-пример корректного управления libVLC — стоит прочитать нативную часть.
|
||
- **mpv-android** — https://github.com/mpv-android/mpv-android, MIT, Kotlin, 3.6k★; **libmpv-android (Findroid)** — MIT; **mpv-android-lib (AbdallahMehiz)** — используется JellyPlay; альтернативная ветка решений, если libVLC не вытянет ASS.
|
||
- **libass-android / ass-media** — https://github.com/peerless2012/libass-android, MIT, 89★: `lib_ass_media` — расширение media3 для рендера ASS через libass; используется jellyfin-androidtv, Fladder, AfuseKt, FongMi/TV. Для VLC-движка не нужен (VLC рисует ASS сам), но полезен как fallback-путь на ExoPlayer.
|
||
- **jellyfin media3-ffmpeg-decoder** (`org.jellyfin.media3:media3-ffmpeg-decoder 1.9.0+1`, репо jellyfin/jellyfin-androidx-media — не проверено напрямую) — готовые FFmpeg-аудиодекодеры для ExoPlayer-fallback.
|
||
- **Just (Video) Player** — https://github.com/moneytoo/Player, Unlicense, Java, 2.7k★, ExoPlayer; эталон минималистичных жестов/контролов.
|
||
- **Nova Video Player** — https://github.com/nova-video-player/aos-AVP, Apache-2.0, 4.7k★; движок — собственный `avos` на FFmpeg (+ dav1d), **не libVLC**; Jellyfin не поддерживает (SMB/UPnP, скрейпинг TMDb/Trakt). Интересен Leanback-UI для TV и torrent/UPnP, но как референс плеера для нас — слабо.
|
||
|
||
---
|
||
|
||
## 2.6 Каталоги клиентов и брендинг
|
||
|
||
- Официальный список: https://jellyfin.org/downloads/clients/all (Swiftfin помечен как «uses VLC»); критерии включения сторонних клиентов — https://jellyfin.org/docs/general/clients/ : Jellyfin как «first-class», настраиваемый URL сервера, **FSF-свободная лицензия**, соблюдение community standards и branding guidelines, ≥1 год активной разработки (или значимый интерес), **хотя бы одна уникальная фича в публичном релизе**; удаление — за неподдержку последней стабильной версии сервера, поломки, нарушение правил.
|
||
- **awesome-jellyfin** — https://github.com/awesome-jellyfin/awesome-jellyfin (CC0-1.0, 9.6k★, push 2026-10-02), файл `CLIENTS.md` — таблицы по платформам с флагами OSS/Free/Paid; раздел Android TV: Plezy, VidHub (closed), Void (beta), Wholphin, Yamby (closed), Moonfin.
|
||
- **Брендинг** — https://jellyfin.org/docs/general/contributing/branding (и /docs/project/branding): «Jellyfin» зарезервировано за проектом и официальными клиентами «Jellyfin for X»; сторонним — **не использовать имя напрямую, избегать паттернов «Jelly[word]» и «[word]fin»**, допустимо «Floostream, a Jellyfin client» в подзаголовке; **логотип (скруглённый треугольник в треугольнике) нельзя** использовать как свой, поворачивать или встраивать; можно взять градиент цветов при уникальной форме. Товарные знаки (имя и логотип) — Jellyfin, Inc. (Канада, США, ЕС, Китай). Ассеты — https://github.com/jellyfin/jellyfin-ux (CC-BY-SA-4.0, 59★, push 2026-10-08): логотипы PNG/SVG, иконки для Android mobile/TV, iOS, Roku, Tizen, web-favicons, плагинные thumbnails.
|
||
|
||
---
|
||
|
||
## 2.7 Сводка «что брать откуда»
|
||
|
||
| Задача | Источник |
|
||
|---|---|
|
||
| Абстракция плеера/очередь/репортинг сессии | `jellyfin-androidtv/playback/{core,jellyfin}` (PlayerBackend, QueueSupplier, PlaySessionService, TimedEventService) |
|
||
| Реализация стороннего движка как `media3.Player` | `findroid/player/local/.../mpv/MPVPlayer.kt` (BasePlayer) — повторить для libVLC |
|
||
| DeviceProfile для VLC (direct/transcode/subtitle/codec-условия, HDR-режимы, VC1-костыль, burn-in bitmap-субтитров) | `Swiftfin/Shared/Objects/VideoPlayerType/VideoPlayerType+VLC.swift`, `.../DeviceProfile.swift` |
|
||
| DeviceProfile для Android-железа (MediaCodec-зондирование, уровни AVC/HEVC, passthrough-детект, чёрные списки моделей) | `jellyfin-androidtv/app/.../util/profile/*`, `jellyfin-android/.../deviceprofile/DeviceProfileBuilder.kt` (+ `getExternalPlayerProfile()`) |
|
||
| DSL для сборки профиля | jellyfin-sdk-kotlin `buildDeviceProfile {}` (1.9.2) |
|
||
| Compose for TV: фокус, home-ряды, контролы под пульт | Wholphin (tv-foundation/tv-material, Navigation 3), Komorebi (`FocusTicketManager`), JellyCine (`tv/`) |
|
||
| Stub-модуль для тяжёлого нативного движка | Wholphin `wholphin-mpv-stub` |
|
||
| Опыт libVLC на Android (ограничения, ABI-сплиты) | JellyPlay `docs/player-engines.md`, Komorebi README, старый `VideoManager.java` (jellyfin-androidtv v0.16.11) — как антипример |
|
||
| Управление libVLC из Kotlin (треки, delay, HW-decoder режимы) | lunarr-app/vlc-player (Android-часть), videolan/vlc-android |
|
||
| Внешний плеер как fallback (интенты VLC/mpv/MX) | `jellyfin-androidtv/ui/playback/external/*` |
|
||
| Офлайн-загрузки | Findroid, JellyPlay (WorkManager + Range-resume), AFinity |
|
||
| Политика имени/иконки | jellyfin.org branding + jellyfin-ux |
|
||
|
||
**Практические предостережения для проекта на libVLC (из найденного):** держать libVLC в отдельном Gradle-модуле за интерфейсом и обязательно иметь ExoPlayer-fallback (все клиенты, вернувшиеся к VLC-вопросу, делали так); строго управлять жизненным циклом `LibVLC`/`MediaPlayer` (главная причина крашей в jellyfin-androidtv); планировать ABI-split сборки (libVLC ≈ +60–80 МБ в universal APK; Fire TV требует armeabi-v7a); для trickplay/медиасегментов/HDR-метаданных рассчитывать на собственную реализацию поверх позиционных событий VLC; для субтитров — VLC рендерит ASS/PGS сам, но внешние bitmap-субтитры придётся прожигать на сервере (как у Swiftfin); учесть отсутствие TLS 1.3 в libvlc 3.x (Swiftfin players.md).
|
||
|
||
### Ключевые источники
|
||
- https://github.com/jellyfin/jellyfin-androidtv/pull/3639 , https://github.com/jellyfin/jellyfin-androidtv/pull/2534 , https://github.com/jellyfin/jellyfin-androidtv/discussions/2546 , https://github.com/jellyfin/jellyfin-androidtv/issues/3937 , https://forum.jellyfin.org/t-solved-bring-back-libvlc , https://github.com/jellyfin/jellyfin-androidtv/releases/tag/v0.20.0-beta.1
|
||
- https://github.com/jellyfin/Swiftfin/blob/main/Documentation/players.md , https://github.com/harflabs/SwiftVLC , https://github.com/LePips/MPVUI (не проверено отдельно)
|
||
- https://github.com/raulshma/jellyplay/blob/main/docs/player-engines.md
|
||
- https://github.com/damontecres/Wholphin/blob/main/DEVELOPMENT.md
|
||
- https://repo1.maven.org/maven2/org/videolan/android/libvlc-all/maven-metadata.xml , https://repo1.maven.org/maven2/org/jellyfin/sdk/jellyfin-core/maven-metadata.xml
|
||
- https://jellyfin.org/docs/general/contributing/branding , https://jellyfin.org/docs/general/clients/ , https://github.com/awesome-jellyfin/awesome-jellyfin/blob/main/CLIENTS.md
|
||
|
||
---
|
||
|
||
# Часть 3. libVLC на Android как движок воспроизведения
|
||
|
||
Методика: все URL ниже проверены (HTTP 200 через curl/загрузку страницы или через GitLab/Maven API), если не помечено «(не проверено)». code.videolan.org закрыт анти-бот-прокси Anubis для «браузерных» запросов, поэтому содержимое читалось через GitLab API/raw. Исходники libvlcjni (ветки `master` и `libvlcjni-3.x`), VLC Android (`VLCOptions.kt`, `VideoPlayerActivity.kt`, `PlaybackService.kt`, `PlayerController.kt`), VLC 3.0.x (`modules/access/http/access.c`, `audiotrack.c`, `mediacodec.c`, `adaptive.cpp`, `gnutls.c`, `freetype.c`, `libvlc-module.c`) и AAR `libvlc-all-3.7.7` скачаны и просмотрены напрямую.
|
||
|
||
---
|
||
|
||
## 3.1 Официальные источники, артефакты, версии
|
||
|
||
### Репозитории
|
||
- [https://code.videolan.org/videolan/vlc-android](https://code.videolan.org/videolan/vlc-android) — официальное приложение VLC for Android / Android TV / ChromeOS (последняя активность 2026-10-10). Модули: `application/` (приложение), `buildsystem/`, `libvlc/` (gradle-модуль, VLC клонируется в `vlc/`), `medialibrary/`. С 2022 г. сам JNI-биндинг вынесен в отдельный репозиторий libvlcjni (см. ниже), а `vlc-android` берёт `libvlc-all` из Maven.
|
||
- [https://github.com/videolan/vlc-android](https://github.com/videolan/vlc-android) — GitHub-зеркало приложения (работает, `raw.githubusercontent.com` отдаёт файлы). **Зеркал `videolan/libvlcjni` и `videolan/libvlc-android-samples` на GitHub нет** (404) — только на code.videolan.org.
|
||
- [https://code.videolan.org/videolan/libvlcjni](https://code.videolan.org/videolan/libvlcjni) — «JNI wrapper for java interaction for libVLC, mostly for Android» (создан 2022-03-22, активность 2026-09-30). Отсюда публикуется `org.videolan.android:libvlc-all`. Ветки:
|
||
- `master` — API **libVLC 4.0** (`libvlcVersion = '4.0.0-eap29'`, `minSdk = 21`, NDK `28.2.13676358`). Исходники Java лежат в `libvlc/src/main/java/org/videolan/libvlc/`.
|
||
- `libvlcjni-3.x` — API **libVLC 3.x** (`libvlcVersion = '3.7.7'`, `minSdk = 17`). Старая раскладка: `libvlc/src/org/videolan/libvlc/`. Дерево ветки: [https://code.videolan.org/videolan/libvlcjni/-/tree/libvlcjni-3.x](https://code.videolan.org/videolan/libvlcjni/-/tree/libvlcjni-3.x).
|
||
- Лицензия репозитория — LGPLv2 (`libvlc/COPYING.LIB`). README упоминает официальный Discord: `https://discord.gg/3h3K3JF`.
|
||
- [https://code.videolan.org/videolan/libvlc-android-samples](https://code.videolan.org/videolan/libvlc-android-samples) — официальные примеры (последняя активность 2025-03-18, ветки `master`, `4.0`, `vulkan`). Состав: `java_sample/` (JavaActivity.java + VLCVideoLayout), `native_sample/` (NDK/C через libvlc.h), `assets/bbb.m4v`, `buildlibvlc.sh`. **Kotlin-примера нет**, README нет. Лицензия примеров — BSD-подобная. В `java_sample/build.gradle`: `minSdkVersion 17`, `compileSdkVersion 33`.
|
||
- [https://code.videolan.org/videolan/medialibrary](https://code.videolan.org/videolan/medialibrary) — медиатека VLC (Maven `org.videolan.android:medialibrary-all`), для Jellyfin-клиента не нужна.
|
||
|
||
### Wiki / документация
|
||
- [https://wiki.videolan.org/LibVLC/](https://wiki.videolan.org/LibVLC/) — обзор LibVLC: LGPLv2, список биндингов, ссылка на Doxygen и на libvlc-android-samples; рекомендация использовать только LGPL-плагины, если приложение не GPL.
|
||
- [https://wiki.videolan.org/AndroidCompile/](https://wiki.videolan.org/AndroidCompile/) — сборка VLC/libVLC для Android (`buildsystem/compile.sh -l -a <ABI> -r` → AAR в `libvlc/build/outputs/aar/`). **Страница устарела** (NDK r21); актуальные инструкции — README libvlcjni.
|
||
- Страницы `LibVLC_Android` на wiki **не существуют** (404). Есть категория [https://wiki.videolan.org/Android/](https://wiki.videolan.org/Android/) и [https://wiki.videolan.org/Android_Player_Intents/](https://wiki.videolan.org/Android_Player_Intents/) (интенты для запуска внешнего VLC — пригодится для fallback «открыть в VLC»).
|
||
- [https://wiki.videolan.org/LibVLC_Tutorial/](https://wiki.videolan.org/LibVLC_Tutorial/) — базовый туториал по C-API (понятия libvlc_instance/media/media_player, те же, что в Java-обёртке).
|
||
- [https://wiki.videolan.org/VLC_command-line_help/](https://wiki.videolan.org/VLC_command-line_help/) — полный `vlc -H` (для 4.0.0-dev); удобно для поиска имён опций.
|
||
- [https://wiki.videolan.org/Documentation:Modules/](https://wiki.videolan.org/Documentation:Modules/) — описание модулей и их опций.
|
||
- Doxygen C-API: [https://videolan.videolan.me/vlc-3.0/](https://videolan.videolan.me/vlc-3.0/) (libVLC 3.0, см. [group__libvlc.html](https://videolan.videolan.me/vlc-3.0/group__libvlc.html)) и [https://videolan.videolan.me/vlc/](https://videolan.videolan.me/vlc/) (VLC 4 master). Старые адреса `videolan.org/developers/vlc/doc/doxygen/...` — 404. Java-классы libvlcjni — тонкие обёртки над `libvlc_media_player_*`, поэтому семантика берётся из Doxygen; Javadoc отдельно не публикуется — читать исходники.
|
||
|
||
### Maven Central: `org.videolan.android:libvlc-all`
|
||
- [https://central.sonatype.com/artifact/org.videolan.android/libvlc-all](https://central.sonatype.com/artifact/org.videolan.android/libvlc-all) — страница артефакта; [maven-metadata.xml](https://repo1.maven.org/maven2/org/videolan/android/libvlc-all/maven-metadata.xml) — полный список версий. В группе `org.videolan.android` три артефакта: `libvlc-all`, `medialibrary-all`, `remote-access`. **Отдельных per-ABI артефактов на Maven нет** — только «all» (4 ABI).
|
||
- **Последняя стабильная 3.x: `3.7.7`** (AAR опубликован 2026-10-01, 92.6 МБ). История 3.7.x в 2026: 3.7.0 (03), 3.7.1, 3.7.2, 3.7.3/3.7.4 (06), 3.7.5 (07), 3.7.6 (09), 3.7.7 (10). Ветка 3.6.x закончилась на 3.6.5 (2025-10-02).
|
||
- **Пре-релизы 4.0: `4.0.0-eap29`** (2026-07-08, 108.9 МБ) — последний. Maven помечает его как `latest`/`release`, но это EAP на нестабильном VLC 4.
|
||
- POM: `<name>LibVLC-Android</name>`, лицензия `GNU Lesser General Public License, version 2.1`. Транзитивные зависимости: `androidx.annotation:annotation`, `androidx.legacy:legacy-support-v4:1.0.0` (+ `kotlin-stdlib` у eap29).
|
||
|
||
```kotlin
|
||
// build.gradle.kts (модуль плеера)
|
||
dependencies {
|
||
implementation("org.videolan.android:libvlc-all:3.7.7") // стабильный libVLC 3.x API
|
||
// implementation("org.videolan.android:libvlc-all:4.0.0-eap29") // другой (4.x) API, см. 3.7
|
||
}
|
||
```
|
||
|
||
### ABI, размер, minSdk
|
||
- Содержимое `libvlc-all-3.7.7.aar` (измерено): `classes.jar`, `R.txt`, assets (lua/hrtfs), `jni/<abi>/{libvlc.so, libvlcjni.so, libc++_shared.so}`. Несжатые размеры нативных библиотек: **arm64-v8a 55.3 МБ, armeabi-v7a 40.4 МБ, x86 50.8 МБ, x86_64 63.5 МБ**. При публикации через AAB Play раздаст один ABI (+~40–55 МБ на устройстве); при универсальном APK — ~210 МБ `.so` до сжатия. Обязательно использовать AAB или `splits { abi { enable = true } }`, а для Android TV — как минимум `armeabi-v7a` + `arm64-v8a` (многие ТВ-боксы 32-битные).
|
||
- minSdk: `libvlc-all` 3.x → **17**; 4.0 EAP → **21** (libvlcjni), а приложение VLC для VLC4 подняло minSdk до **23** в феврале 2026 ([коммит «Bump VLC 4 api min to 23»](https://mailman.videolan.org/pipermail/android/2026-February/023564.html)).
|
||
- Проверка совместимости CPU перед init: `VLCUtil.hasCompatibleCPU(context)` (так делает `VLCInstance.kt` в VLC Android).
|
||
|
||
---
|
||
|
||
## 3.2 Java/Kotlin API (libvlcjni)
|
||
|
||
Пути исходников (ветка 3.x: `libvlc/src/org/videolan/libvlc/…`; master/4.0: `libvlc/src/main/java/org/videolan/libvlc/…`):
|
||
- [MediaPlayer.java (3.x)](https://code.videolan.org/videolan/libvlcjni/-/blob/libvlcjni-3.x/libvlc/src/org/videolan/libvlc/MediaPlayer.java), `Media.java`, `LibVLC.java`, `MediaList.java`, `RendererDiscoverer.java`, `RendererItem.java`, `MediaDiscoverer.java`, `Dialog.java`, `AWindow.java`, `VideoHelper.java`; `interfaces/IVLCVout.java`, `interfaces/IMedia.java`, `interfaces/ILibVLC.java`; `util/VLCVideoLayout.java`, `util/DisplayManager.java`, `util/HWDecoderUtil.java` (3.x), `util/VLCUtil.java`, `util/MediaBrowser.java`, `util/AndroidUtil.java`. Пакет `media/` — обёртки, имитирующие `android.media.MediaPlayer`/`VideoView`; `stubs/` — заглушки для JVM-тестов.
|
||
|
||
### `LibVLC`
|
||
- `LibVLC(Context, List<String> options)` / `LibVLC(Context)`; `static version()`, `majorVersion()` (3 или 4 — удобно для условной логики опций), `setUserAgent(String name, String http)` (задаёт и HTTP User-Agent); `release()`.
|
||
- Опции экземпляра задаются как строки `--xxx` (глобально) — то же, что аргументы `vlc`. Опции медиа — `Media.addOption(":xxx")` (с двоеточием). Подтверждённые в исходниках VLC 3.0.x имена: `--network-caching=<ms>`, `--live-caching`, `--file-caching`, `--clock-jitter`, `--clock-synchro`, `--audio-time-stretch`/`--no-audio-time-stretch`, `--vout`, `--aout`, `--deinterlace={-1,0,1}` и `--deinterlace-mode=<yadif,…>`, `--sub-track=<n>`, `--sub-file=<path>`, `--input-slave=<mrl>`, `--sub-autodetect-file`, `--sub-language`/`--audio-language`, `--audio-desync=<ms>`, `--preferred-resolution`, `--input-fast-seek`, `--drop-late-frames`, `--skip-frames`, `--spdif`, `--stereo-mode`, `--audio-filter`, `--video-filter`, `--video-title-show`, `--verbose`/`-vvv`.
|
||
- **Внимание**: `--avcodec-hw` относится к модулю `avcodec` (VA-API/DXVA на десктопе). На Android аппаратное декодирование — отдельные модули `mediacodec_ndk`/`mediacodec_jni` (см. 3.4), включаются через `:codec=` на уровне медиа, а не через `--avcodec-hw`.
|
||
|
||
Что реально передаёт VLC for Android (из [VLCOptions.kt](https://github.com/videolan/vlc-android/blob/master/application/resources/src/main/java/org/videolan/resources/VLCOptions.kt)):
|
||
```kotlin
|
||
// фрагмент логики VLCOptions.libOptions (упрощённо, значения из настроек приложения)
|
||
val options = arrayListOf<String>()
|
||
options += if (timeStretching) "--audio-time-stretch" else "--no-audio-time-stretch"
|
||
options += "--avcodec-skiploopfilter"; options += "$deblocking"
|
||
options += "--avcodec-skip-frame"; options += if (frameSkip) "2" else "0"
|
||
options += "--avcodec-skip-idct"; options += if (frameSkip) "2" else "0"
|
||
options += "--subsdec-encoding"; options += subtitlesEncoding
|
||
options += "--stats"
|
||
if (networkCaching > 0) options += "--network-caching=$networkCaching" // 0..60000 мс
|
||
options += "--audio-resampler"; options += "soxr"
|
||
options += "--audiotrack-session-id=$audiotrackSessionId"
|
||
if (isVLC4()) options += "--sub-text-scale=${1600 / freetypeRelFontsize.toFloat()}"
|
||
else options += "--freetype-rel-fontsize=$freetypeRelFontsize"
|
||
if (freetypeBold) options += "--freetype-bold"
|
||
options += "--freetype-color=$freetypeColor"; options += "--freetype-opacity=$opacity"
|
||
options += "--freetype-background-opacity=0"
|
||
options += "--freetype-shadow-opacity=…"; options += "--freetype-outline-thickness=…"
|
||
when (opengl) { 1 -> options += "--vout=gles2,none"; 0 -> options += "--vout=android_display,none" }
|
||
options += "--keystore"; options += if (isMarshMallowOrLater) "file_crypt,none" else "file_plaintext,none"
|
||
options += "--keystore-file"; options += File(dir, "keystore").path
|
||
options += "--sout-chromecast-audio-passthrough"; options += "--sout-chromecast-conversion-quality=2"
|
||
options += "--sout-keep"
|
||
options += "--preferred-resolution=-1"
|
||
// на уровне Media (VLCOptions.setMediaOptions):
|
||
media.addOption(":no-mediacodec-dr"); media.addOption(":no-omxil-dr") // режим «decoding acceleration» (без direct rendering)
|
||
media.addOption(":no-video"); media.addOption(":start-paused"); media.addOption(":sub-language=none")
|
||
```
|
||
Инициализация в приложении — `VLCInstance.kt`: `VLCUtil.hasCompatibleCPU(ctx)` → `LibVLC(ctx, VLCOptions.libOptions)`.
|
||
|
||
### `Media` / `IMedia`
|
||
- Конструкторы: `Media(ILibVLC, Uri)`, `Media(ILibVLC, String path)`, `Media(ILibVLC, AssetFileDescriptor)`, `Media(ILibVLC, FileDescriptor)`.
|
||
- `addOption(String ":opt")`, `setHWDecoderEnabled(boolean enabled, boolean force)`, `setDefaultMediaPlayerOptions()`, `parse()/parseAsync(flags, timeout)`, `getTracks()`, `getMeta(id)`, `getDuration()`, `subItems()`, `getStats()`, `setEventListener(IMedia.EventListener)`.
|
||
- События `IMedia.Event`: `MetaChanged=0`, `SubItemAdded=1`, `DurationChanged=2`, `ParsedChanged=3`, `SubItemTreeAdded=6` (**`Media.Event.Buffering` не существует** — буферизация приходит только через `MediaPlayer.Event.Buffering`).
|
||
- Слейвы: `IMedia.Slave(int type, int priority /*0..4*/, String uri)`, типы `IMedia.Slave.Type.Subtitle=0`, `Audio=1`; `media.addSlave(slave)`, `clearSlaves()`, `getSlaves()`.
|
||
- HW-декодирование в 3.x (`Media.setHWDecoderEnabled`): по `HWDecoderUtil.getDecoderFromDevice()` добавляет `:codec=mediacodec_ndk,all` (на API<21 — `mediacodec_jni`; при `force` ещё `iomx,`), иначе `:codec=all`; плюс, если не заданы, `:file-caching=1500` и `:network-caching=1500`. В 4.x метод добавляет только `:no-hw-dec` при `enabled=false` (HW включён по умолчанию).
|
||
|
||
### `MediaPlayer` (3.x; отличия 4.x — ниже)
|
||
- Создание: `MediaPlayer(ILibVLC)` / `MediaPlayer(IMedia)`; `setMedia(IMedia)`, `play()`, `pause()`, `stop()`, `isPlaying()`, `isSeekable()`, `getLength()`, `getTime()`/`setTime(long ms, boolean fast)`, `getPosition()`/`setPosition(float, boolean fast)`, `setRate(float)`/`getRate()`, `getVolume()/setVolume(int)`, `release()`.
|
||
- Треки (3.x): `getVideoTracks()/setVideoTrack(int id)`, `getAudioTracks()/getAudioTrack()/setAudioTrack(int id)`, `getSpuTracks()/getSpuTrack()/setSpuTrack(int id)` (возвращают `TrackDescription[] {id, name}`; `-1` = выключить), `setVideoTrackEnabled(boolean)` (для фонового аудио), `getAudioDelay()/setAudioDelay(long)`, `getSpuDelay()/setSpuDelay(long)` (задержки в микросекундах), `setEqualizer(MediaPlayer.Equalizer)`.
|
||
- Главы/тайтлы: `getTitles(): Title[]`, `getChapters(int title): Chapter[]`, `getTitle()/setTitle(int)`, `getChapter()/setChapter(int)`, `navigate(int)` (меню DVD/BD), `setVideoTitleDisplay(position, timeout)`.
|
||
- Видео: `attachViews(VLCVideoLayout, DisplayManager?, boolean subtitles, boolean textureView)`, `detachViews()`, `updateVideoSurfaces()`, `setVideoScale(ScaleType)` (SURFACE_BEST_FIT/FIT_SCREEN/FILL/16_9/4_3/ORIGINAL…), `setScale(float)`, `setAspectRatio(String "16:9")`, `getVLCVout(): IVLCVout`, `updateViewpoint(...)` (360°), `setRenderer(RendererItem)`.
|
||
- Аудио: `setAudioOutput("audiotrack"|"opensles"|…)`, `setAudioOutputDevice(String id)`, `setAudioDigitalOutputEnabled(boolean)`, `forceAudioDigitalEncodings(int[] AudioFormat.ENCODING_*)`, `canDoPassthrough()`.
|
||
- Слейвы на лету: `addSlave(int type, Uri uri, boolean select)` / `addSlave(type, String path, select)`.
|
||
- Запись: `record(String directory, boolean enable)`; телетекст: `setTeletext()`.
|
||
- События `MediaPlayer.Event` (константы): `MediaChanged=0x100, Opening=0x102, Buffering=0x103, Playing=0x104, Paused=0x105, Stopped=0x106, EndReached=0x109, EncounteredError=0x10a, TimeChanged=0x10b, PositionChanged=0x10c, SeekableChanged=0x10d, PausableChanged=0x10e, LengthChanged=0x111, Vout=0x112, ESAdded=0x114, ESDeleted=0x115, ESSelected=0x116, RecordChanged=0x11e`. Поля события: `getTimeChanged()`, `getLengthChanged()`, `getPositionChanged()`, `getVoutCount()`, `getEsChangedType()`/`getEsChangedID()`, `getPausable()`, `getSeekable()`, `getBuffering()` (проценты 0–100f). Слушатель: `setEventListener(MediaPlayer.EventListener)` (события доставляются в главный поток через Handler).
|
||
- **4.x (master) — изменения API треков**: вместо int-индексов — строковые id: `selectTrack(String id)`, `selectTracks(int type, String ids)`, `unselectTrackType(int type)`, `getSelectedVideoTrack()`, `getAllAudioTracks()/getAllSpuTracks()`, `setSpuTrack(String)`, `setVideoTrack(String)`, `setDisplayFit()`, `setUseOrientationFromBounds()`; `Media.setHWDecoderEnabled` сводится к `:no-hw-dec`.
|
||
|
||
### Отображение: `VLCVideoLayout`, `IVLCVout`, `DisplayManager`
|
||
- `VLCVideoLayout extends FrameLayout` — инфлейтит `R.layout.vlc_video_layout` (SurfaceView/TextureView для видео + отдельная поверхность субтитров). Javadoc: «Use it preferably with MediaPlayer.attachViews()».
|
||
- `attachViews(layout, dm, subtitles, textureView)`: `subtitles=true` создаёт отдельную subtitles-surface (нужна для рендера субтитров поверх видео при `android_display`), `textureView=true` — TextureView вместо SurfaceView (TextureView нужен для анимаций/трансформаций/прозрачности, но дороже и без secure/HDR-путей; VLC Android по умолчанию SurfaceView: `mediaPlayer.attachViews(it, displayManager, true, false)`).
|
||
- Низкоуровневый `IVLCVout`: `setVideoView(SurfaceView|TextureView)`, `setVideoSurface(Surface, SurfaceHolder)`/`(SurfaceTexture)`, `setSubtitlesView(...)`, `setSubtitlesSurface(...)`, `attachViews(OnNewVideoLayoutListener)`, `detachViews()`, `areViewsAttached()`, `setWindowSize(w, h)` (сообщить libVLC размер окна — важно при ресайзе/повороте/PiP), `addCallback(Callback{onSurfacesCreated/onSurfacesDestroyed})`. `OnNewVideoLayoutListener.onNewVideoLayout(vout, width, height, visibleWidth, visibleHeight, sarNum, sarDen)` даёт реальные размеры кадра для ручного расчёта letterbox. Старый Jellyfin ATV использовал именно `mVlcPlayer.getVLCVout().attachViews(this)` с собственным SurfaceView.
|
||
- Поворот/ресайз: при использовании `attachViews(VLCVideoLayout…)` достаточно вызвать `mediaPlayer.updateVideoSurfaces()` в `onConfigurationChanged`/при смене layout (так делает `VideoPlayerActivity.kt`).
|
||
- `DisplayManager(Activity, LiveData<RendererItem>?, boolean textureView, boolean cloneMode, boolean benchmark)` — переключение между основным дисплеем, `Presentation` на вторичном дисплее и удалённым рендерером. Для простого клиента можно передавать `null`.
|
||
- Picture-in-Picture — обычный Android API; VLC Android берёт размеры выбранной видеодорожки и вызывает `enterPictureInPictureMode(PictureInPictureParams.Builder().setAspectRatio(Rational(w,h)).build())`, а при изменении размера — `updateVideoSurfaces()`.
|
||
- Фоновое аудио: VLC Android держит `MediaPlayer` в `PlaybackService : MediaBrowserServiceCompat` с `MediaSessionCompat` и при уходе видео в фон вызывает `setVideoTrackEnabled(false)` / `detachViews()`, при возврате — наоборот. Для нового приложения: foreground service `mediaPlayback` + media3 `MediaSession` поверх собственного `Player`-адаптера или `MediaSessionCompat`.
|
||
|
||
### Chromecast через libVLC: `RendererDiscoverer`
|
||
- `RendererDiscoverer.list(ILibVLC): Description[]` (имена модулей обнаружения, напр. `microdns_renderer`), `RendererDiscoverer(libvlc, name)`, `start()/stop()`, события `ItemAdded=0x502`/`ItemDeleted=0x503` → `event.getItem(): RendererItem` (`name`, `type`, `iconUrl`, `flags`; нужно `retain()/release()`), затем `mediaPlayer.setRenderer(item)` — libVLC сам транскодирует/стримит на Chromecast (`sout-chromecast-*` опции выше). Это реализация VLC Android («Cast to…»), не Google Cast SDK, т. е. плеер на телефоне продолжает работать как источник; для Jellyfin обычно лучше нативный Cast receiver, но вариант есть.
|
||
- `MediaDiscoverer(libvlc, name)` — обнаружение SMB/UPnP, для Jellyfin не нужен.
|
||
|
||
### Диалоги (TLS/логин)
|
||
- `Dialog.setCallbacks(ILibVLC, Dialog.Callbacks)` с `onDisplay(ErrorMessage|LoginDialog|QuestionDialog|ProgressDialog)`, `onCanceled`, `onProgressUpdate`; `LoginDialog.postLogin(user, pass, store)`, `QuestionDialog.postAction(int)`. Через `QuestionDialog` libVLC задаёт вопрос «Insecure site / accept certificate» для недоверенных TLS-сертификатов (см. 3.4).
|
||
|
||
### Авторитетный пример (official `java_sample/JavaActivity.java`, переведён на Kotlin)
|
||
```kotlin
|
||
class PlayerActivity : AppCompatActivity() {
|
||
private val useTextureView = false
|
||
private val enableSubtitles = true
|
||
private lateinit var libVLC: LibVLC
|
||
private lateinit var mediaPlayer: MediaPlayer
|
||
private lateinit var videoLayout: VLCVideoLayout
|
||
|
||
override fun onCreate(savedInstanceState: Bundle?) {
|
||
super.onCreate(savedInstanceState)
|
||
setContentView(R.layout.activity_main)
|
||
libVLC = LibVLC(this, arrayListOf("-vvv", "--network-caching=1500"))
|
||
mediaPlayer = MediaPlayer(libVLC)
|
||
videoLayout = findViewById(R.id.video_layout)
|
||
}
|
||
override fun onStart() {
|
||
super.onStart()
|
||
mediaPlayer.attachViews(videoLayout, null, enableSubtitles, useTextureView)
|
||
val media = Media(libVLC, Uri.parse("https://jellyfin.example/Videos/$itemId/stream?static=true&ApiKey=$token"))
|
||
media.setHWDecoderEnabled(true, false) // 3.x: :codec=mediacodec_ndk,all
|
||
media.addSlave(IMedia.Slave(IMedia.Slave.Type.Subtitle, 4,
|
||
"https://jellyfin.example/Videos/$itemId/$sourceId/Subtitles/$index/0/Stream.srt?ApiKey=$token"))
|
||
mediaPlayer.media = media; media.release()
|
||
mediaPlayer.setEventListener { ev ->
|
||
when (ev.type) {
|
||
MediaPlayer.Event.Buffering -> showBuffer(ev.buffering) // 0..100f
|
||
MediaPlayer.Event.TimeChanged -> onTime(ev.timeChanged)
|
||
MediaPlayer.Event.LengthChanged -> onLength(ev.lengthChanged)
|
||
MediaPlayer.Event.ESAdded, MediaPlayer.Event.ESSelected -> refreshTracks() // getSpuTracks()/getAudioTracks()
|
||
MediaPlayer.Event.EndReached, MediaPlayer.Event.EncounteredError -> finishPlayback()
|
||
}
|
||
}
|
||
mediaPlayer.play()
|
||
}
|
||
override fun onStop() { super.onStop(); mediaPlayer.stop(); mediaPlayer.detachViews() }
|
||
override fun onDestroy() { super.onDestroy(); mediaPlayer.release(); libVLC.release() }
|
||
}
|
||
```
|
||
Compose: официальных примеров `AndroidView { VLCVideoLayout }` не найдено. Паттерн тривиален: `AndroidView(factory = { VLCVideoLayout(it) }, modifier = Modifier.fillMaxSize(), onRelease = { player.detachViews() })` и `attachViews` в `factory`/`LaunchedEffect` — рекомендация, не проверенный официальный код.
|
||
|
||
### HTTP-заголовки / авторизация Jellyfin
|
||
- В модуле HTTP VLC 3.0.x (`modules/access/http/access.c`) есть только: `http-user-agent`, `http-referrer`, `http-forward-cookies` (default true), `http-continuous`; в старом `modules/access/http.c` — `http-reconnect`. **Произвольных заголовков (Authorization, X-Emby-Token) нет**: MR [«http: add support for custom HTTP headers» !7495](https://code.videolan.org/videolan/vlc/-/merge_requests/7495) — закрыт, не смержен; форумные ответы — «Not possible» ([пример](https://forum.videolan.org/viewtopic.php?p=498927)).
|
||
- Поэтому для Jellyfin токен передаётся **query-параметром**: `ApiKey=<token>` — константа `ApiClient.QUERY_ACCESS_TOKEN = "ApiKey"` в jellyfin-sdk-kotlin (проверено в исходнике `ApiClient.kt`). Сервер также принимает устаревший `api_key` (не проверено). User-Agent можно задать `LibVLC.setUserAgent(appName, httpUa)` или `:http-user-agent=…`.
|
||
|
||
---
|
||
|
||
## 3.3 Субтитры
|
||
|
||
- Внешние SRT/ASS/VTT: `Media.addSlave(IMedia.Slave(Type.Subtitle, priority, uri))` до `play()` или `MediaPlayer.addSlave(Type.Subtitle, uri, select=true)` во время воспроизведения; URL по HTTP(S) поддерживаются. VLC Android: `PlayerController.addSubtitleTrack(uri, select)`.
|
||
- Встроенные дорожки: `getSpuTracks()`/`setSpuTrack(id)` (3.x) / `selectTrack(id)` (4.x); `-1`/`unselectTrackType(Text)` — выключить. Автозагрузка одноимённых файлов отключается `:sub-language=none`.
|
||
- Задержка: `setSpuDelay(µs)`/`getSpuDelay()`; аудио — `setAudioDelay(µs)`.
|
||
- Стиль текстовых субтитров — опции модуля `freetype`: `--freetype-rel-fontsize` (VLC 3; меньше = крупнее), `--sub-text-scale` (VLC 4; VLC Android переводит как `1600/relfontsize`), `--freetype-bold`, `--freetype-color`, `--freetype-opacity`, `--freetype-background-color/-opacity`, `--freetype-shadow-color/-opacity`, `--freetype-outline-thickness/-color/-opacity`, `--freetype-fontsize`, `--freetype-font`. Кодировка: `--subsdec-encoding`. Это опции экземпляра LibVLC — чтобы менять размер «на лету», нужно пересоздать LibVLC (VLC Android так и делает: `VLCInstance.restart()`).
|
||
- ASS/SSA: рендерится libass внутри libVLC с полной поддержкой стилей/позиционирования/karaoke — главное преимущество перед ExoPlayer, у которого SSA/ASS только «базовая» ([форум Jellyfin](https://forum.jellyfin.org/t-solved-subtitle-limit-android-tv?pid=47233)).
|
||
- Битмап-субтитры PGS/VobSub/DVB: libVLC декодирует и накладывает сам, независимо от контейнера и способа доставки — в т. ч. в прямом воспроизведении без burn-in на сервере. У media3 PGS/DVB парсеры есть (в контейнере MKV/TS), VobSub внешний не поддерживается ([supported-formats](https://developer.android.com/media/media3/exoplayer/supported-formats)).
|
||
- Известные проблемы HLS + субтитры: внешние субтитры не показываются при HLS-воспроизведении — [форум, 2024](https://forum.videolan.org/viewtopic.php?p=546788); WebVTT в HLS ограничено ([форум](https://forum.videolan.org/viewtopic.php?p=521307)); встроенный WebVTT — вторая строка не отображается, 3.6.5 ([vlc#29230](https://code.videolan.org/videolan/vlc/-/issues/29230)); «No embedded subtitles when playing HDR video» — открыт ([vlc-android#2347](https://code.videolan.org/videolan/vlc-android/-/work_items/2347)). Практический вывод: при транскодировании в HLS просить сервер отдавать субтитры отдельным файлом или burn-in, при direct play — слейвы и встроенные треки работают хорошо.
|
||
|
||
---
|
||
|
||
## 3.4 Стриминг
|
||
|
||
### HTTP(S), TLS, самоподписанные сертификаты
|
||
- TLS в libVLC Android делает **GnuTLS** (`modules/misc/gnutls.c`), а не Android-стек. Опции: `--gnutls-system-trust` (default true), `--gnutls-dir-trust=<dir>`, `--gnutls-priorities`. Системное хранилище Android читается, но «User»-сертификаты на новых версиях Android не подхватываются — открытый баг [vlc-android#3005](https://code.videolan.org/videolan/vlc-android/-/work_items/3005).
|
||
- Отключить проверку опцией нельзя; на недоверенный сертификат libVLC поднимает `Dialog.QuestionDialog` («Insecure site»), который приложение должно показать и ответить `postAction()` ([vlc-android#2673](https://code.videolan.org/videolan/vlc-android/-/work_items/2673), открыт). История: [#665](https://code.videolan.org/videolan/vlc-android/-/work_items/665), [#1642](https://code.videolan.org/videolan/vlc-android/-/work_items/1642), [#2193](https://code.videolan.org/videolan/vlc-android/-/work_items/2193), форум [«HTTP/TLS on libVLC Android»](https://forum.videolan.org/viewtopic.php?p=447307). Для Jellyfin-клиента: отдельно валидировать сертификат в OkHttp (API) и обязательно реализовать `Dialog.Callbacks` для libVLC.
|
||
- Переподключение: `--http-reconnect` (старый модуль), `--http-continuous`; куки: `--http-forward-cookies` (включены по умолчанию).
|
||
|
||
### HLS и seeking
|
||
- HLS/DASH — модуль `adaptive`: опции `--adaptive-logic={predictive,nearoptimal,rate,fixedrate,lowest,highest}`, `--adaptive-maxwidth/-maxheight`, `--adaptive-bw`, `--adaptive-livedelay`, `--adaptive-maxbuffer`, `--adaptive-lowlatency`, `--adaptive-use-access`.
|
||
- Direct play по статическому HTTP с Range — seek через Range работает штатно (`setTime/setPosition`, `fast=true` = key-frame seek, `--input-fast-seek`). Для HLS от транскодера Jellyfin плейлист «растёт» по мере транскодирования: `SeekableChanged` может прийти `false`, а перемотка внутри сегмента может откатываться к началу сегмента ([vlc#26564](https://code.videolan.org/videolan/vlc/-/issues/26564)); жалобы на Android — [форум 2015](https://forum.videolan.org/viewtopic.php?p=429161), [2019](https://forum.videolan.org/viewtopic.php?p=492115), [LibVLCSharp PR 361](https://github.com/videolan/libvlcsharp/pull/361). Стандартная тактика Jellyfin-клиентов: при seek в транскодируемом потоке перезапрашивать `PlaybackInfo`/новый HLS-URL с `StartTimeTicks`, а не полагаться на внутрипоточный seek.
|
||
- Буферизация: `--network-caching=<ms>` (VLC Android: 0–60000, libVLC 3.x при HW-декодировании выставляет 1500), `--live-caching`, `--file-caching`, `--clock-jitter`, `--clock-synchro`. Прогресс — `MediaPlayer.Event.Buffering` + `event.getBuffering()` (0–100f).
|
||
|
||
### Аппаратное декодирование (MediaCodec), тоннелирование
|
||
- Модули `mediacodec_ndk` (NDK AMediaCodec, API≥21) и `mediacodec_jni` (через JNI, старые устройства) — `modules/codec/omxil/mediacodec.c`; оба имеют `set_capability("video decoder", 0)` — «Only enabled via commandline arguments», т. е. включаются через `:codec=mediacodec_ndk,…` (это и делает `Media.setHWDecoderEnabled` в 3.x). Опции: `--mediacodec-dr` (direct rendering в Surface, default true), `--mediacodec-audio` (default false), `--mediacodec-tunneled-playback` (default false). Режимы VLC Android: Disabled → `:codec=all`; «Decoding acceleration» → HW-декодер + `:no-mediacodec-dr :no-omxil-dr` (кадры копируются и выводятся через `gles2`/`android_display`, можно накладывать фильтры/субтитры в самом vout); «Full» → direct rendering (минимальная нагрузка, zero-copy). В 4.x: HW включён по умолчанию, выключается `:no-hw-dec`.
|
||
- Vout: `--vout=android_display,none` (прямой вывод в Surface) или `--vout=gles2,none` (OpenGL ES 2: нужен для 360°, фильтров, тонмаппинга); `--android-display-chroma`.
|
||
- Аудиовыход: по умолчанию в текущем vlc-android — `aaudio`, альтернативы `audiotrack`, `opensles` (`setAudioOutput()`).
|
||
|
||
### Passthrough (Dolby/DTS/TrueHD) на Android TV
|
||
- README libvlcjni/vlc-android: «Audio passthrough with SPDIF and HDMI, including for Audio HD codecs, like DD+, TrueHD or DTS-HD». Реализация — aout `audiotrack`: проверяет `AudioFormat.ENCODING_AC3/E_AC3/DOLBY_TRUEHD/DTS/DTS_HD`, устройство `encoded`. Javadoc `MediaPlayer.setAudioOutput`: «Starting Android 21, passthrough is enabled for encodings supported by the device/audio system».
|
||
- API: `setAudioDigitalOutputEnabled(true)` («Works only with AudioTrack AudioOutput»), `forceAudioDigitalEncodings(intArrayOf(AudioFormat.ENCODING_E_AC3, …))`, `setAudioOutputDevice("hdmi"|"encoded:…")`, `canDoPassthrough()`; VLC Android применяет `setAudioDigitalOutputEnabled(...)` и `setAudioOutput(getAout())` при каждом новом медиа. Старый Jellyfin ATV делал: `setAudioOutput("opensles_android")` + `setAudioOutputDevice("hdmi")` либо `setAudioDigitalOutputEnabled(true)`.
|
||
- Известные проблемы: TrueHD/DTS не проходят на ряде устройств (Fire TV Cube — [vlc#27670](https://code.videolan.org/videolan/vlc/-/issues/27670); Sony Android TV — [форум](https://forum.videolan.org/viewtopic.php?p=471323); Homatics TrueHD — [форум](https://forum.videolan.org/viewtopic.php?p=532343)); запрос «passthrough per codec» — [vlc#18112](https://code.videolan.org/videolan/vlc/-/issues/18112); DTS:X как DTS-HD на Shield — [vlc#26978](https://code.videolan.org/videolan/vlc/-/issues/26978). Выбор per-codec через `forceAudioDigitalEncodings` уже позволяет сделать UI вида «E-AC3: passthrough, DTS: decode».
|
||
|
||
### HDR / Dolby Vision
|
||
- Заявлено: «Support for HDR, including tonemapping for SDR streams». Реально через `mediacodec` + direct rendering HDR10 выводится устройством; через `gles2` путь — тонмаппинг в SDR. Баги: [vlc-android#1605](https://code.videolan.org/videolan/vlc-android/-/work_items/1605) (закрыт); «washed-out colors when playing HDR» — [#2999](https://code.videolan.org/videolan/vlc-android/-/work_items/2999) (открыт); «Grey bars on HDR content on Bravia» — [#943](https://code.videolan.org/videolan/vlc-android/-/work_items/943); «wrong color playing 10bit DV on 8bit display» — [#3069](https://code.videolan.org/videolan/vlc-android/-/work_items/3069) (открыт); 4K AV1 freeze на Fire TV Cube — [#3358](https://code.videolan.org/videolan/vlc-android/-/work_items/3358) (2026, открыт). Dolby Vision: отдельного DV-пути (RPU/профили 5/7/8, `video/dolby-vision` MIME) в libVLC нет — DV-файлы декодируются как HEVC и либо падают в HDR10-fallback (профили 7/8.1), либо дают зелёно-фиолетовую картинку (профиль 5). Документация Android: гарантированно HDR только у tunneled-декодеров ([source.android.com/docs/core/display/hdr](https://source.android.com/docs/core/display/hdr)). Для DV-библиотек ExoPlayer с нативным `video/dolby-vision` декодером пока лучше.
|
||
|
||
### Частота обновления (не задача VLC)
|
||
- libVLC не переключает режим дисплея. Делать самим по [Android «Frame rate» guide](https://developer.android.com/media/optimize/performance/frame-rate): `Surface.setFrameRate(fps, FRAME_RATE_COMPATIBILITY_FIXED_SOURCE, CHANGE_FRAME_RATE_ALWAYS)` (API 30+) или `WindowManager.LayoutParams.preferredDisplayModeId` по `Display.getSupportedModes()` — последнее рекомендовано именно для Android TV, с проверкой `DisplayManager.getMatchContentFrameRateUserPreference()` и ожиданием `onDisplayChanged` ~2 с перед стартом. FPS дорожки берётся из Jellyfin `MediaStream.RealFrameRate` или `Media.getTracks()` (`VideoTrack.frameRateNum/Den`).
|
||
|
||
---
|
||
|
||
## 3.5 Примеры и референсные реализации
|
||
|
||
- [libvlc-android-samples](https://code.videolan.org/videolan/libvlc-android-samples) — `java_sample/src/main/java/org/videolan/javasample/JavaActivity.java` (81 строка), `native_sample/jni/main.c` — libvlc C API. Kotlin/Compose-примера нет.
|
||
- VLC for Android как референс:
|
||
- [VideoPlayerActivity.kt](https://github.com/videolan/vlc-android/blob/master/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt) (~2700 строк) — attach/detach, PiP, DisplayManager, задержки аудио/субтитров, ресайз, Bluetooth-задержка.
|
||
- [PlaybackService.kt](https://github.com/videolan/vlc-android/blob/master/application/vlc-android/src/org/videolan/vlc/PlaybackService.kt) — `MediaBrowserServiceCompat` + `MediaSessionCompat`, `MediaPlayer.EventListener`, рендереры, эквалайзер.
|
||
- `application/vlc-android/src/org/videolan/vlc/media/PlayerController.kt` — тонкая обёртка над `MediaPlayer` (треки, passthrough, слейвы, rate, события) — лучший шаблон «Player-адаптера».
|
||
- [VLCOptions.kt](https://github.com/videolan/vlc-android/blob/master/application/resources/src/main/java/org/videolan/resources/VLCOptions.kt), `VLCInstance.kt` — все опции и инициализация.
|
||
- Старый Jellyfin Android TV с libVLC: последняя версия с libVLC — **v0.16.11**, класс [`VideoManager.java`](https://github.com/jellyfin/jellyfin-androidtv/blob/v0.16.11/app/src/main/java/org/jellyfin/androidtv/ui/playback/VideoManager.java) (1093 строки; одновременно ExoPlayer и libVLC). Опции: `--network-caching=<buffer>`, `--audio-time-stretch`, `--avcodec-skiploopfilter 1`, `--avcodec-skip-frame 0`, `--avcodec-skip-idct 0`, `--android-display-chroma RV32`, `--audio-resampler soxr`, `-vv`. Удалено в 0.17 — [PR #3639](https://github.com/jellyfin/jellyfin-androidtv/pull/3639). Жалобы после удаления (MPEG-2 Live TV, ASS, HDR/DV) — [форум Jellyfin](https://forum.jellyfin.org/printthread.php?tid=7841), [t-default-player-option-gone](https://forum.jellyfin.org/t-default-player-option-gone). Важно: сейчас libvlc-all обновляется ежемесячно (3.7.x), так что аргумент «не обновляется 2 года» устарел.
|
||
- Другие OSS: [Nova Video Player](https://github.com/nova-video-player/aos-AVP) — Apache-2.0, **не libVLC** (собственный `avos` на ffmpeg); [mpv-android](https://github.com/mpv-android/mpv-android) — MIT, «not a library/module (AAR)»; [LibVLCSharp issue 425](https://code.videolan.org/videolan/LibVLCSharp/-/issues/425) — обсуждение `MediaPlayerElement` для Android.
|
||
- Статьи/блоги по libVLC + Kotlin/Compose: качественных англоязычных не найдено; лучшие «документы» — исходники выше.
|
||
|
||
---
|
||
|
||
## 3.6 Лицензирование
|
||
|
||
- libVLC/libvlccore — **LGPL-2.1+** с 21.12.2011 ([пресс-релиз](https://www.videolan.org/press/lgpl-libvlc.html)); приложение VLC остаётся GPLv2+. Общая страница [videolan.org/legal.html](https://www.videolan.org/legal.html): торговые марки VLC нельзя использовать для продвижения не-open-source ПО; патенты — позиция, что во Франции/ЕС ПО не патентуется.
|
||
- `libvlcjni` — LGPLv2; POM `libvlc-all` — LGPL-2.1. Приложение VLC for Android — **GPLv2+**, «Android libraries make this, de facto, a GPLv3 application» (README).
|
||
- **Ключевой нюанс — contribs внутри `libvlc.so`.** `buildsystem/build-libvlc.sh` (libvlcjni) имеет флаг `--license {g|l|a}`: `g` — «GPLv3 (default)» (contribs с GPL-компонентами + `--enable-live555 --enable-dvdread`), `l` — «LGPLv3 + ad-clauses», `a` — «LGPLv2 + ad-clauses». Какой флаг использует CI при публикации в Maven — проверить не удалось **(не проверено)**; по умолчанию скрипта — GPL-сборка, т. е. **разумно считать бинарный `libvlc-all` де-факто GPLv3-совместимым артефактом, а не чистым LGPL**. Для нашего GPL-приложения это не проблема; проблемой было бы для проприетарного/MPL-клиента.
|
||
- Следствия для GPL-клиента: GPL-3.0 совместим с LGPL-2.1+ и GPLv3-contribs; **GPL-2.0-only** несовместим с GPLv3-компонентами (gnutls ≥3, dvdread/live555) → выбирать **GPL-2.0-or-later** или GPL-3.0 (так же поступает VLC). Нужно включить тексты LGPL-2.1/GPL и уведомления контрибов (ffmpeg, libass, dav1d, gnutls и т. д.) в экран «О программе»; ссылку на исходники libvlcjni/vlc.
|
||
- Патенты/кодеки: VideoLAN не покупает патентные лицензии; для open-source распространения через Play это общепринятая практика (VLC сам в Play: [download-android.html](https://www.videolan.org/vlc/download-android.html)). Риск (как и у любого ffmpeg-плеера) несёт издатель.
|
||
- Имя «VLC» и логотип — торговые марки VideoLAN, в названии/бренде приложения не использовать.
|
||
|
||
---
|
||
|
||
## 3.7 Сравнение и известные проблемы; статус libVLC 4
|
||
|
||
| Критерий | libVLC 3.7.x | media3 / ExoPlayer | libmpv (mpv-android) |
|
||
|---|---|---|---|
|
||
| Кодеки | всё через ffmpeg + MediaCodec; MPEG-2/VC-1/AC-3/DTS/TrueHD софтом | только MediaCodec (+ ffmpeg-расширение надо собирать самим) | ffmpeg + MediaCodec |
|
||
| Субтитры | libass (полный ASS), PGS/VobSub/DVB/CC, внешние слейвы | SRT/VTT/TTML/SSA (базовый стиль), PGS/DVB парсеры, нет VobSub | libass, всё что ffmpeg |
|
||
| HDR/DV | HDR10 через mediacodec DR; DV — без RPU, ряд открытых багов | лучший DV/HDR путь (`video/dolby-vision`, tunneling) | HDR есть, DV ограниченно |
|
||
| Passthrough | есть (AudioTrack encoded, TrueHD/DTS-HD), устройство-зависимо | есть (`DefaultAudioSink`) | есть |
|
||
| HLS seek/субтитры | слабое место (см. 3.3–3.4) | сильная сторона | среднее |
|
||
| DRM | нет (не нужно) | Widevine | нет |
|
||
| Размер | +40–63 МБ `.so` на ABI | ~5–10 МБ | ~20–30 МБ, нет готового AAR |
|
||
| Интеграция | готовый AAR, Java API, активно обновляется | Kotlin-first, Compose-дружелюбен | нужна своя сборка JNI |
|
||
|
||
- Практический вывод для Jellyfin-клиента: libVLC как **основной движок для direct play** (ASS/PGS, MPEG-2, DTS/TrueHD passthrough, любые контейнеры) и ExoPlayer как альтернатива/fallback для HLS-транскодов и DV — именно та схема, от которой Jellyfin ATV отказался из-за стоимости поддержки двух бэкендов; закладывать абстракцию `Player` с самого начала.
|
||
- Трекеры: [vlc-android issues](https://code.videolan.org/videolan/vlc-android/-/issues), `https://code.videolan.org/videolan/libvlcjni/-/issues` (биндинг), `https://code.videolan.org/videolan/vlc/-/issues` (ядро). Релевантные открытые: #3005 (TLS user certs), #2673 (accept cert), #2999/#943/#3069 (HDR/DV), #2347 (субтитры при HDR), #3358 (AV1 4K Fire TV), #2589 (Chromecast options).
|
||
- **libVLC 4.0 статус (октябрь 2026):** стабильного релиза VLC 4.0 нет — на [videolan.org/news.html](https://www.videolan.org/news.html) за 2025–2026 только VLC 3.0.23 (2026-01-08), 3.0.24 (2026-09-22), VLC for Android 3.6.0 (2025-01-13) и 3.7.0 (2026-02-25). Android-артефакты 4.x: `libvlc-all:4.0.0-eap29` (2026-07-08); vlc-android ведёт параллельную линию `forceVlc4` с тегами `3.80.0-alpha01…alpha06` (не проверено). Изменения 4.x API для Android: строковые id треков (`selectTrack`), `:no-hw-dec`, `--sub-text-scale` вместо `--freetype-rel-fontsize`, minSdk 21→23, новый clock/aout. Рекомендация: стартовать на 3.7.x, изолировав работу с треками/опциями за адаптером, чтобы переход на 4.0 был локальным.
|
||
|
||
---
|
||
|
||
## 3.8 Каналы коммуникации
|
||
|
||
- Форум: [https://forum.videolan.org/viewforum.php?f=35](https://forum.videolan.org/viewforum.php?f=35) — раздел для VLC for Android (название «VLC for Android and Chrome OS» — не проверено). Общий форум: [forum.videolan.org](https://forum.videolan.org/).
|
||
- Списки рассылки: [android@videolan.org — «Development of VLC for Android»](https://mailman.videolan.org/listinfo/android) (туда же идут коммит-уведомления vlc-android/libvlcjni — удобно следить за bump'ами версий), [vlc-devel](https://mailman.videolan.org/listinfo/vlc-devel) (ядро/libVLC).
|
||
- IRC: `#videolan` на Libera.Chat, веб-клиент [videolan.org/webirc](https://www.videolan.org/webirc/); страница поддержки [videolan.org/support](https://www.videolan.org/support/). Discord VideoLAN: `https://discord.gg/3h3K3JF`.
|
||
- Баг-репорты и MR — только на code.videolan.org; GitHub-зеркало vlc-android PR не принимает.
|
||
|
||
---
|
||
|
||
### Краткая сводка для принятия решения
|
||
1. Артефакт: `org.videolan.android:libvlc-all:3.7.7` (LGPL-2.1 POM, де-факто GPLv3-сборка контрибов — для GPL-приложения ок), minSdk 17, ~40–63 МБ `.so` на ABI → только AAB/ABI-splits.
|
||
2. API стабильно и покрывает всё нужное: треки, слейвы-субтитры, задержки, главы, passthrough (`setAudioDigitalOutputEnabled`/`forceAudioDigitalEncodings`), Chromecast-рендеринг, события буферизации.
|
||
3. Ограничения, которые надо закрывать на стороне приложения: нет произвольных HTTP-заголовков (токен в `ApiKey=` query), TLS через GnuTLS (нужен обработчик `Dialog`), HLS-seek/субтитры в HLS слабые (перезапрос транскода с `StartTimeTicks`), Dolby Vision без RPU, refresh-rate switching — своими руками через `Display.Mode`/`setFrameRate`.
|
||
4. 4.0 — только EAP; проектировать адаптер с учётом смены API треков.
|
||
|
||
---
|
||
|
||
# Часть 4. Android-платформа
|
||
|
||
_Исходный заголовок: Android-платформа для нативного Kotlin-клиента Jellyfin (phone + Android TV, libVLC) — отчёт по исследованию_
|
||
|
||
Дата: 2026-10-11. Все URL ниже открывались и проверялись (кроме помеченных «(не проверено)»). Версии библиотек взяты из официальных release-страниц, `maven-metadata.xml` на Maven Central / Google Maven и из актуальных version-catalog'ов Findroid и jellyfin-androidtv (оба обновляются Renovate'ом и на момент проверки сидят на самых свежих версиях).
|
||
|
||
---
|
||
|
||
## 0. Главные выводы (TL;DR)
|
||
|
||
1. **Leanback официально deprecated** («This artifact and its classes are deprecated. Use Compose for TV instead» на странице релизов). Для нового проекта в 2026 — только **Compose for TV** (`androidx.tv:tv-material:1.1.0`, stable с 2026-05-06). TV-специфичные lazy-списки (`TvLazyColumn/Row/Grid`) удалены; используются обычные `LazyColumn/LazyRow` из `androidx.compose.foundation` ≥1.7 + `BringIntoViewSpec` вместо `pivotOffsets`. `ImmersiveList` удалён (есть сэмпл), `Carousel`, `TabRow`, `NavigationDrawer`, `ListItem`, `Card*`, `Button*`, `WideButton` — stable.
|
||
2. **libVLC + media3 `MediaSession` реально:** jellyfin-androidtv уже оборачивает свой backend-независимый `PlaybackManager` в `SimpleBasePlayer` (`playback/media3/session/.../MediaSessionPlayer.kt`: `getState()`, `handleSetPlayWhenReady`, `handlePrepare`, `handleStop`, `handleSeek`, `handleSetPlaybackParameters`, `handleSetRepeatMode`, `handleSetDeviceVolume`, `handleRelease`…) и отдаёт его в `MediaSession.Builder(context, player)`. Findroid оборачивает libmpv в `BasePlayer` (`MPVPlayer : BasePlayer(), MPVLib.EventObserver, AudioManager.OnAudioFocusChangeListener`). Это готовые шаблоны для «libVLC → media3 Player → MediaSession/Now Playing/PiP/медиакнопки».
|
||
3. **Один код на phone+TV — модель Findroid**: `:app:phone` (Material3 + material3-adaptive) и `:app:tv` (tv-material) поверх общих `:core`, `:data`, `:player:core`, `:player:local`, `:setup`, `:settings`, `:modes:film`; Hilt, Navigation 3, Room 3, Paging 3, WorkManager, Coil 3. jellyfin-androidtv — Koin + гибрид Leanback/Compose + Navigation3-ui.
|
||
4. **F-Droid принимает prebuilt `org.videolan.android:libvlc-all` с Maven Central**: в актуальном рецепте VLC 3.7.1 F-Droid больше не собирает libvlc через `compile.sh`, а вырезает модули `:libvlcjni`/`:medialibrary` из `settings.gradle`, т.е. берёт AAR из Maven Central (Maven Central — в allowlist политики включения). Findroid аналогично тянет prebuilt `dev.jdtech.mpv:libmpv` и при этом публикуется в F-Droid с reproducible builds (`Binaries:` + `AllowedAPKSigningKeys`).
|
||
5. **Актуальный стек (октябрь 2026):** Kotlin 2.4.21, AGP 9.4.x (Gradle 9.6, JDK 17+), Compose BOM 2026.09.00 (ui/foundation 1.12.1, material3 1.4.0), tv-material 1.1.0, media3 1.11.1, Navigation 3 1.2.0 (Navigation Compose 2.10.2 — «maintenance mode»), Room 3.0.3 (`androidx.room3`) / Room 2.8.5, Paging 3.5.2, WorkManager 2.12.0, DataStore 1.2.1, Lifecycle 2.11.0, Hilt 2.60.1 + androidx.hilt 1.4.0, Koin 4.2.2, Coil 3.6.3, Ktor 3.6.0, OkHttp 5.5.0, kotlinx.coroutines 1.11.0, kotlinx.serialization 1.11.0, KSP 2.3.12, jellyfin-sdk-kotlin 1.9.2, libvlc-all 3.7.7 (stable, 2026-10-01) / 4.0.0-eap29.
|
||
6. **Google Play: TV требует target API ≥34, с 1 августа 2026 — 64-bit + 16 KB page size**; F-Droid/IzzyOnDroid — лимит 30 MB на IzzyOnDroid (per-ABI split), reproducible builds желательны; libVLC 3.6.2 имел проблему с 16 KB alignment (баг-репорт), проверять `zipalign -P 16` на текущих 3.7.x.
|
||
7. **Google TV «Continue watching»**: legacy Watch Next (TvProvider) продолжает работать на Android TV и на Google TV (с сертификацией), но «все новые интеграции Continue Watching должны идти через Engage SDK» (allowlist/EAP, g.co/tv/vda, лимит 5 сущностей). Для open-source клиента реалистично реализовать TvProvider Watch Next + каналы (как jellyfin-androidtv `LeanbackChannelWorker`), а Engage — опционально.
|
||
|
||
---
|
||
|
||
## 1. Официальная документация Android TV
|
||
|
||
### Качество и манифест
|
||
- [TV app quality guidelines](https://developer.android.com/docs/quality-guidelines/tv-app-quality) — чек-лист ревью Play (Tier 3 «TV Ready» — минимум). Ключевые пункты: **TV-ML** (`ACTION_MAIN` + `CATEGORY_LEANBACK_LAUNCHER`), **TV-MT** (`android.hardware.touchscreen` и прочее железо `required=false`), **TV-LB** (баннер 320×180 px xhdpi + иконка ≥160×160), **TV-BN** (баннер содержит имя), **TV-DP** (вся функциональность доступна с D-pad), **TV-DM** (не зависеть от Menu), **TV-DB** (Back ведёт к Home), **TV-LO/TV-OV** (landscape, без обрезки по overscan), **TV-PC** (центр D-pad = play/pause, лево/право = перемотка), **TV-PP** (`KEYCODE_MEDIA_PLAY_PAUSE`), **TV-NP** (Now Playing card только для фонового аудио; видео должно ставиться на паузу при уходе), **TV-VC** (Tier 2: голосовые команды через `MediaSession`), **TV-BU/BY/BA** (Ambient Mode: блокировать только при активном видео), **TV-ME** (лимиты памяти на low-RAM), **TV-G6** (с 1 августа 2026 — 32/64-bit + 16 KB страницы), **TV-PN/TV-EC/TV-ER** (Watch Next / Engage SDK), PiP **TV-IC…TV-IX**.
|
||
- [Get started with TV apps (manифест, баннер, эмулятор)](https://developer.android.com/training/tv/start/start) — обязательные элементы манифеста:
|
||
```xml
|
||
<uses-feature android:name="android.software.leanback" android:required="false"/> <!-- true только для TV-only -->
|
||
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
|
||
<application android:banner="@drawable/banner" ...> <!-- 320x180 px xhdpi, текст внутри картинки, по баннеру на локаль -->
|
||
<activity ...>
|
||
<intent-filter>
|
||
<action android:name="android.intent.action.MAIN"/>
|
||
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
|
||
</intent-filter>
|
||
</activity>
|
||
```
|
||
Также: `android:colorPrimary` для анимации запуска; кастомный SplashScreen API на TV (Android 12+) не поддерживается. Раздел про запуск на TV-эмуляторе (Device Manager → категория TV).
|
||
- [Handle TV hardware](https://developer.android.com/training/tv/get-started/hardware) — рекомендованный способ определить TV: `packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)`. Таблица «неподдерживаемого» на TV железа (touchscreen, faketouch, telephony, camera, nfc, gps, microphone, sensors, screen.portrait) — всё объявлять `required="false"`, плюс `android.hardware.wifi` (есть Ethernet-only ТВ). Разрешения, неявно требующие железо (`RECORD_AUDIO` → microphone, `ACCESS_WIFI_STATE` → wifi) — тоже закрывать `uses-feature required=false`. `<meta-data android:name="android.software.leanback.supports_touch">` для pointer-пультов. Останавливать воспроизведение в `onStop()` (ТВ в «выключенном» режиме продолжает играть звук).
|
||
- [Manage TV controllers](https://developer.android.com/training/tv/get-started/controllers) — D-pad обязателен; `android:configChanges="keyboard|keyboardHidden|navigation"` чтобы не пересоздавать Activity при отключении BT-пульта; таблица маппинга `KEYCODE_BUTTON_A/SELECT/ENTER/DPAD_CENTER` → Select, `BUTTON_B/BACK` → Back, `DPAD_RIGHT/BUTTON_R2` → FF, `DPAD_LEFT/BUTTON_L2` → RW; не перехватывать `KEYCODE_MEDIA_*`, если есть `MediaSession` (система сама диспатчит в callback). Back никогда не toggle.
|
||
- [TV navigation (D-pad, фокус, Back)](https://developer.android.com/training/tv/start/navigation) — принципы: всегда есть сфокусированный элемент; индикация фокуса одной схемой; `nextFocusUp/Down/Left/Right` только при необходимости; Back не должен спрашивать подтверждение выхода; не рисовать экранную кнопку Back; `OnBackPressedDispatcher` для кастомного поведения; splash не в back stack.
|
||
- [Game controller input](https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input) — `KeyEvent` для кнопок, `MotionEvent` для осей; D-pad геймпада приходит как `AXIS_HAT_X/Y` **или** `KEYCODE_DPAD_*` — обрабатывать оба; dead zone через `MotionRange.getFlat()`; `InputDevice.supportsSource(SOURCE_GAMEPAD/DPAD/JOYSTICK)`.
|
||
|
||
### Воспроизведение («Build TV playback apps»)
|
||
- [Build TV playback apps (оглавление раздела)](https://developer.android.com/training/tv/playback) — подразделы: Compose for TV (setup, layouts, catalog browser, details, scrollable layouts, migrate from Leanback), Ambient mode, Playback controls, Media session, Now Playing card, Audio capabilities, Adjust display settings/frame rate, Memory. TV Input Framework нужен **не** клиенту Jellyfin (только для Live-TV-провайдеров и DRM-превью на домашнем экране).
|
||
- [Playback controls on TV](https://developer.android.com/training/tv/playback/controls) — стандарт: центр = play/pause (+показать контролы), короткий Right/Left = перемотка на N сек, удержание = scrub, Up/Down = «peek» информации без паузы.
|
||
- [Implement a media session](https://developer.android.com/training/tv/playback/media-session) — framework `MediaSession`: `FLAG_HANDLES_MEDIA_BUTTONS|FLAG_HANDLES_TRANSPORT_CONTROLS`, `setActive(true)` после получения audio focus, `PlaybackState` с доступными `ACTION_*`, `MediaMetadata` (DISPLAY_TITLE/SUBTITLE/ICON_URI + legacy TITLE/ARTIST + маленький bitmap). (Для нового кода — media3 `MediaSession`, см. §5.)
|
||
- [Background playback in a Now Playing card](https://developer.android.com/training/tv/playback/now-playing) — карточка появляется при активной сессии + audio focus; обязателен `setSessionActivity(PendingIntent)` на экран с контролами; снимать `setActive(false)` сразу при стопе или через 5–30 мин паузы; **видео не должно использовать Now Playing** (только аудио/музыка или PiP).
|
||
- [Ambient mode](https://developer.android.com/training/tv/playback/ambient-mode) — `FLAG_KEEP_SCREEN_ON` только во время активного видео, снимать на паузе; Energy Saver предотвратить нельзя.
|
||
- [Optimize memory usage](https://developer.android.com/training/tv/playback/memory) — `ActivityManager.isLowRamDevice()`; на 1 GB-устройствах цель: anon+swap <160 MB, graphics 30–40 MB, максимум 280 MB суммарно; не грузить картинки больше разрешения UI (на 720p UI даунскейлить 1080p-постеры); hardware bitmaps; медиабуферы 40–60 MB (1 GB/1080p) — 100–120 MB (2 GB/4K); освобождать плеер в `onStop`; WorkManager с `setRequiresDeviceIdle(true)` на low-RAM; baseline profiles для холодного старта.
|
||
- [Match content frame rate (TV)](https://developer.android.com/training/tv/playback/framerate) и общий гайд [Frame rate API](https://developer.android.com/media/optimize/performance/frame-rate) — см. §5.
|
||
- [Audio capabilities (TV)](https://developer.android.com/training/tv/playback/audio-capabilities) — см. §5.
|
||
- [PiP on TV / multitasking](https://developer.android.com/training/tv/get-started/multitasking) — см. §5 (важно: на TV PiP для медиа-воспроизведения **не разрешён** по категориям `com.google.android.tv.pip.category`).
|
||
|
||
### Watch Next / каналы / Google TV
|
||
- [Add programs to the Watch Next channel](https://developer.android.com/training/tv/discovery/watch-next-add-programs) — `androidx.tvprovider:tvprovider:1.1.0`, `WatchNextProgram.Builder().setType(TYPE_MOVIE/TYPE_TV_EPISODE).setWatchNextType(WATCH_NEXT_TYPE_CONTINUE/NEXT/NEW/WATCHLIST).setLastEngagementTimeUtcMillis().setLastPlaybackPositionMillis().setDurationMillis().setPosterArtUri().setIntentUri().setInternalProviderId()` → `contentResolver.insert(TvContractCompat.WatchNextPrograms.CONTENT_URI, …)`; обновление через `TvContractCompat.buildWatchNextProgramUri(id)`; обрабатывать `ACTION_WATCH_NEXT_PROGRAM_BROWSABLE_DISABLED` (пользователь удалил — не вставлять снова).
|
||
- [Watch Next program attributes](https://developer.android.com/training/tv/discovery/watch-next-programs) — обязательны: Watch Next type, last engagement time; для `CONTINUE` — ещё last playback position и duration; `InternalProviderId` рекомендуется.
|
||
- [Video program attributes (постеры)](https://developer.android.com/training/tv/discovery/video-programs) — обязательные поля программ: channelId, title, intentUri, posterArtUri, posterArtAspectRatio; размеры: `ASPECT_RATIO_16_9` 272×153 dp, `ASPECT_RATIO_2_3` 102×153 dp, `ASPECT_RATIO_MOVIE_POSTER` (1:1.441) 106×153 dp.
|
||
- [Watch Next guidelines for app developers](https://developer.android.com/training/tv/discovery/guidelines-app-developers) — только фильмы/эпизоды; «начато» = >3 %/2 мин (фильм) или >2 мин (эпизод); «закончено» = титры (или <3 мин до конца); публиковать при выходе из приложения или паузе >5 мин, латентность ≤5 с; не более одного эпизода на сериал; удалять по завершении.
|
||
- [Channels on the home screen (preview channels)](https://developer.android.com/training/tv/discovery/recommendations-channel) — Android 8+; `Channel.Builder().setType(TYPE_PREVIEW)`, лого 80×80 dp, `TvContractCompat.requestChannelBrowsable()` для дефолтного канала, `ACTION_REQUEST_CHANNEL_BROWSABLE` для остальных (из foreground Activity), ресивер `android.media.tv.action.INITIALIZE_PROGRAMS`; нельзя WHERE-клаузы в запросах к провайдеру; нужен `WRITE_EPG_DATA`. Пример в проде: jellyfin-androidtv `app/src/main/java/org/jellyfin/androidtv/integration/LeanbackChannelWorker.kt` (CoroutineWorker раз в час, каналы latest_media/my_media/next_up/latest_movies/latest_episodes + Watch Next; проверяет `FEATURE_LEANBACK` и наличие провайдера `TvContractCompat.AUTHORITY`).
|
||
- [Display video previews](https://developer.android.com/training/tv/discovery/preview-videos) — `setPreviewVideoUri()` (публичный HTTPS, без DRM) или `TvInputService` для DRM — для Jellyfin не нужно.
|
||
- [Recommendations row (Android ≤7.1)](https://developer.android.com/training/tv/discovery/recommendations-row) — только для API ≤25; на API 26+ обязательны каналы. Для нового приложения (minSdk ≥26 на TV) можно игнорировать.
|
||
- [Google TV для разработчиков](https://developer.android.com/training/tv/get-started/google-tv) — всё Android TV работает на Google TV без отдельной сборки; Watch Next на Google TV требует сертификации интеграции; определить Google TV: `packageManager.hasSystemFeature("com.google.android.feature.AMATI_EXPERIENCE")`; media actions feed — только для ограниченного круга партнёров.
|
||
- [Engage SDK](https://developer.android.com/guide/playcore/engage), [Engage SDK for TV: Continue Watching (client)](https://developer.android.com/guide/playcore/engage/tv/continue-watching/client), [Engage SDK FAQ](https://developer.android.com/guide/playcore/engage/faq) — `AppEngagePublishClient.publishContinuationCluster()` (upsert всего списка, ≤10 элементов, FAQ: лимит 5 на партнёра; постеры 16:9 ≥200 px; `PlatformSpecificUri` для TV и Android; `setSyncAcrossDevices` с явным согласием; `deleteClusters(DELETE_REASON_USER_LOG_OUT)`). FAQ: «Engage будет обратно совместим на всех Android TV с Watch Next API… существующая интеграция продолжит работать», «все новые интеграции Continue Watching должны использовать Engage», доступ — phased/EAP с allowlist (g.co/tv/vda), обязателен скриншот verification app при сабмите.
|
||
- Статус TvProvider: `androidx.tvprovider:tvprovider:1.1.0` (май 2025) активно сопровождается — [release notes](https://developer.android.com/jetpack/androidx/releases/tvprovider).
|
||
|
||
### Дизайн (10-foot UI)
|
||
- [TV design (обзор)](https://developer.android.com/design/ui/tv) — Foundations / Style / Layout / Components, Figma-киты ([figma.com/@tv](https://www.figma.com/@tv)).
|
||
- [Design for TV (foundations)](https://developer.android.com/design/ui/tv/guides/foundations/design-for-tv) — 10-foot UI (зритель ~3 м, крупный текст, мало чтения), D-pad, общее устройство (приватность профилей).
|
||
- [Layouts (safe area, сетка)](https://developer.android.com/design/ui/tv/guides/styles/layouts) — проектировать на канве **960×540 dp** (16:9), ассеты под 1080p; overscan-отступы ≈5 % (48 dp по бокам / 27 dp сверху-снизу, в разделе сетки — 58/28 dp; документ сам противоречив, большинство современных ТВ overscan не имеют); сетка 12 колонок × 52 dp, gutter 20 dp, поля 58 dp; ширины карточек 844/412/268/196 dp для 1–4 в ряд; оставлять зазор под увеличение сфокусированной карточки.
|
||
- [Typography](https://developer.android.com/design/ui/tv/guides/styles/typography) — 15 стилей (Display/Headline/Title/Body/Label), Roboto по умолчанию; точные sp на странице только картинкой (не проверено текстом).
|
||
|
||
### Reference-приложения и эмулятор
|
||
- [android/tv-samples](https://github.com/android/tv-samples) — `JetStreamCompose` (полное стриминг-приложение на Compose for TV: TabRow, Carousel, ImmersiveList-паттерн, карточки, Macrobenchmark + baseline profile; catalog: tv-material 1.0.0, Hilt, Navigation Compose, media3-ui-compose, Coil 2), `TvMaterialCatalog` (каталог компонентов tv-material), `ReferenceAppKotlin` (Leanback + ExoPlayer + MediaSessionConnector + каналы/Watch Next + Cast Connect; Leanback-based, но единственный официальный пример полной интеграции Watch Next/каналов), `ClassicsKotlin`, `Leanback`, `LeanbackShowcase`, `AccessibilityDemo`.
|
||
- [android/tv-codelabs](https://github.com/android/tv-codelabs) + [Codelab «Introduction to Compose for TV»](https://developer.android.com/codelabs/compose-for-tv-introduction) — каталог (LazyColumn/LazyRow + Carousel + Coil AsyncImage) и экран деталей.
|
||
- [android/uamp (Universal Android Music Player)](https://github.com/android/uamp) — **архивирован 9 января 2026, deprecated**; как референс для TV/MediaSession больше не использовать. Актуальная замена по теме Compose + media3 на нескольких форм-факторах — [Jetcaster](https://github.com/android/compose-samples/tree/main/Jetcaster) (модули `mobile`, `tv`, `wear`, `core/data|domain|designsystem`; `tv` использует `androidx.tv:tv-material`, Navigation Compose, Hilt, `media3-exoplayer/session/ui-compose`).
|
||
- Эмулятор без устройства: Device Manager → категория TV; доступные системные образы (проверено по индексу SDK): `system-images;android-33|34|36;android-tv;{arm64-v8a,x86[,x86_64]}` и `system-images;android-33|34|36;google-tv;…`, плюс `system-images;android-36;google-tv-ps16k;{arm64-v8a,x86_64}` (Google TV с 16 KB страницами) — [Create and manage virtual devices](https://developer.android.com/tools/devices). Начиная с Android 13 на TV работает Wireless debugging (`adb pair`), для старых — `adb tcpip 5555` + `adb connect ip:5555` — [adb docs](https://developer.android.com/tools/adb).
|
||
|
||
---
|
||
|
||
## 2. Compose for TV
|
||
|
||
### Библиотеки и статус
|
||
- [Compose for TV release notes](https://developer.android.com/jetpack/androidx/releases/tv) — `androidx.tv:tv-material:1.1.0` (stable, 2026-05-06; 1.1.0-alpha01 чинила фокус в Carousel; в 1.1.0-beta01 deprecated `NativePaint`/`asFrameworkPaint()` → `Paint.nativePaint`, нужен KGP ≥2.0) и `androidx.tv:tv-foundation:1.0.0` (stable, 2026-05-06). История: `TvLazyColumn/TvLazyRow/TvLazyVerticalGrid/TvLazyHorizontalGrid` deprecated в 1.0.0-alpha11 (июль 2024) и **удалены** в 1.0.0-alpha12 (янв 2025); `ImmersiveList` **удалён** в tv-material 1.0.0-beta01 (есть сэмпл: [ImmersiveListSamples.kt в cs.android.com](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:tv/samples/src/main/java/androidx/tv/samples/ImmersiveListSamples.kt)); в beta01 стабилизированы `MaterialTheme`, `ColorScheme/Typography/Shapes`, `Border`, `Glow`, `Surface`, `Text`, `Icon`, `Button/OutlinedButton/IconButton/OutlinedIconButton/WideButton`, `Card*` (`StandardCardContainer`, `WideCardContainer`), `ListItem/DenseListItem`, `Tab/TabRow`, `NavigationDrawer/ModalNavigationDrawer/NavigationDrawerItem`, `Checkbox/RadioButton/Switch`; `Carousel` — часть функций «dropped for beta из-за экспериментальных API».
|
||
Зависимости:
|
||
```kotlin
|
||
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
|
||
implementation("androidx.tv:tv-material:1.1.0") // вместо androidx.compose.material3 на TV
|
||
implementation("androidx.tv:tv-foundation:1.0.0") // опционально
|
||
```
|
||
- [Compose for TV: setup](https://developer.android.com/training/tv/playback/compose) — использовать `androidx.tv.material3` вместо `androidx.compose.material3` на TV (свой `MaterialTheme`, не смешивать); minSdk 21.
|
||
- [Create scrollable layouts for TV](https://developer.android.com/training/tv/playback/compose/lists) — обычные `LazyRow/LazyColumn` (Foundation ≥1.7) уже умеют focus-driven scroll; таблица миграции `TvLazyRow→LazyRow`, `pivotOffsets→BringIntoViewSpec` через `LocalBringIntoViewSpec`; код `PositionFocusedItemInLazyLayout(parentFraction=0.3f)`; для вложенного списка — отключить, подставив пустой `object : BringIntoViewSpec {}`.
|
||
- [BringIntoViewSpec introduction (Paramount Tech, 03.2025)](https://paramount.tech/blog/2025/03/03/jetpack-compose-bringintoviewspec-intro.html) — дефолт `LocalBringIntoViewSpec` сам выбирает `PivotBringIntoViewSpec` при `FEATURE_LEANBACK`; ловушка: в конце списка pivot не достигается — добавлять `contentPadding` в конец, равный ширине вьюпорта.
|
||
- [Migrate from Leanback to Compose](https://developer.android.com/training/tv/playback/leanback/migrate-to-compose) — экран за экраном, Leanback и Compose сосуществуют, цель — single-activity.
|
||
- [Leanback release notes](https://developer.android.com/jetpack/androidx/releases/leanback) — **«This artifact and its classes are deprecated. Use Compose for TV instead»**; `androidx.leanback:leanback:1.2.0` stable (есть 1.3.0-alpha02, июнь 2026 — только багфиксы).
|
||
|
||
**Leanback vs Compose for TV для нового проекта 2026:** Leanback — deprecated, Fragment/RecyclerView-ориентирован, фиксированные шаблоны (`BrowseSupportFragment`, `DetailsSupportFragment`, `PlaybackSupportFragment`, `GuidedStepFragment`), нет аналога в Compose «из коробки», но зато зрелая фокус-логика. Compose for TV — рекомендация Google, общий код с phone, но фокус-менеджмент надо строить самому (`focusRestorer`, `focusGroup`, `BringIntoViewSpec`), и часть вещей (immersive list, guided step) — руками. Вывод: Compose for TV; Leanback только как источник UX-паттернов. jellyfin-androidtv до сих пор на Leanback 1.2.0 + Compose foundation (гибрид), Findroid TV — чистый Compose (tv-material 1.1.0 + Navigation 3).
|
||
|
||
### Фокус в Compose (гайды, которые реально существуют)
|
||
- [Focus in Compose (обзор + ссылки на 11 подгайдов)](https://developer.android.com/develop/ui/compose/touch-input/focus) — подстраницы: `/focus/focus-target`, `/focus/focus-traversal`, `/focus/focus-in-text-fields`, `/focus/focus-group`, `/focus/focus-restoration`, `/focus/focused-state`, `/focus/react-to-focus`, `/focus/request-focus`, `/focus/move-focus`, `/focus/capture-and-release-focus`, `/focus/testing-focus`.
|
||
- [Restore focus](https://developer.android.com/develop/ui/compose/touch-input/focus/focus-restoration) — `LazyColumn/LazyRow/LazyVerticalGrid` уже focus group → достаточно `Modifier.focusRestorer()`; для `Row/Column` нужен `focusGroup().focusRestorer()`; fallback `focusRestorer(firstItemRequester)` (лямбда-вариант deprecated); для `NavigationRail/TabRow` — `focusProperties { onEnter = { selectedRequester.requestFocus() } }`; переход между экранами — `FocusRequester.saveFocusedChild()` перед навигацией и `restoreFocusedChild()` при возврате.
|
||
- [Group focus targets](https://developer.android.com/develop/ui/compose/touch-input/focus/focus-group) — `focusGroup()`, `focusProperties { onEnter / onExit }`, `cancelFocusChange()`, `requestedFocusDirection`.
|
||
- [Programmatically request focus](https://developer.android.com/develop/ui/compose/touch-input/focus/change-focus-behavior) — `FocusRequester` + `LaunchedEffect(Unit){ requestFocus() }`; **ловушка**: не вызывать `requestFocus()` в теле composable и до attach модификатора (исключение).
|
||
- [React to focus](https://developer.android.com/develop/ui/compose/touch-input/focus/react-to-focus) — `onFocusChanged { it.isFocused / it.hasFocus / it.isCaptured }`; ловушка: `isFocused` у контейнера = false, когда сфокусирован ребёнок — для подсветки контейнера использовать `hasFocus`; состояние держать в `remember { mutableStateOf }`.
|
||
- [focusRestorer reference](https://developer.android.com/reference/kotlin/androidx/compose/ui/focus/focusRestorer.modifier).
|
||
- Обработка клавиш: `Modifier.onPreviewKeyEvent { it.key == Key.DirectionLeft && it.type == KeyEventType.KeyDown }` (capture-фаза) / `onKeyEvent`; Back — `BackHandler` из `androidx.activity.compose` (и `NavigationBackHandler` в Nav3 для predictive back). Ключевые `Key.DirectionUp/Down/Left/Right/DirectionCenter`, `Key.MediaPlayPause`, `Key.Back`.
|
||
- Тесты фокуса: [Test focus navigation](https://developer.android.com/develop/ui/compose/touch-input/focus/testing-focus) — см. §8.
|
||
|
||
---
|
||
|
||
## 3. Одна кодовая база phone + TV
|
||
|
||
### Детекция формы устройства
|
||
- `PackageManager.FEATURE_LEANBACK` — официальная рекомендация ([Handle TV hardware](https://developer.android.com/training/tv/get-started/hardware)). `UiModeManager.currentModeType == UI_MODE_TYPE_TELEVISION` — старая рекомендация, всё ещё встречается (например в IMA SDK samples); система выставляет TV-режим при `FEATURE_TELEVISION` **или** `FEATURE_LEANBACK`, поэтому сигналы почти совпадают. На практике: для выбора UI-ветки — `FEATURE_LEANBACK`; для «есть ли тач» — `FEATURE_TOUCHSCREEN`; Google TV — `com.google.android.feature.AMATI_EXPERIENCE`; Fire TV — `Build.MANUFACTURER == "Amazon"` + `amazon.hardware.fire_tv` (не проверено).
|
||
- [Window size classes](https://developer.android.com/develop/ui/compose/layouts/adaptive/use-window-size-classes) — `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true).windowSizeClass` из `androidx.compose.material3.adaptive:adaptive:1.3.0` (stable); брейкпоинты Compact <600 / Medium <840 / Expanded <1200 / Large <1600 / XL ≥1600 dp. **Для TV не подходят как признак устройства**: 1080p-ТВ = 960×540 dp (Expanded width, Medium height), т.е. выглядит как планшет в ландшафте; док прямо говорит «size classes are not device types». Размерные классы — для адаптивности внутри phone/tablet-UI, а TV-ветка выбирается по `FEATURE_LEANBACK`.
|
||
|
||
### Как организуют код существующие клиенты
|
||
- **Findroid** ([repo](https://github.com/jarnedemeulemeester/findroid), GPL-3.0) — `settings.gradle.kts`: `:app:phone`, `:app:tv`, `:core`, `:data`, `:player:core`, `:player:local`, `:setup`, `:modes:film`, `:settings`. TV-модуль: отдельный `MainActivity` (`ComponentActivity` + `setContent { FindroidTheme { NavigationRoot(...) } }`), манифест с `android.software.leanback required=true`, `touchscreen required=false`, `android:banner`, `LEANBACK_LAUNCHER`; зависимости TV-модуля — `tv-material`, `navigation3-runtime/ui`, `lifecycle-viewmodel-navigation3`, `savedstate-compose`, `paging-compose`, `hilt-lifecycle-viewmodel-compose`, `coil-compose/network-okhttp/cache-control/svg`, `media3-exoplayer/ui/session`, `media3-ffmpeg-decoder` (org.jellyfin.media3). Phone-модуль — `material3` 1.4.0 + `material3-adaptive-navigation-suite`. Плеер общий (`:player:local` — ExoPlayer и libmpv через `dev.jdtech.mpv:libmpv:1.0.0`, `MPVPlayer : BasePlayer()`), данные общие (`:data`, Room 3), скачивание в `:core` (`Downloader`/`DownloaderImpl`). Т.е. **две app-модули с разным UI поверх общих core/data/player** — самый прямой образец для вашего проекта (замените mpv на libVLC).
|
||
- **jellyfin-androidtv** ([repo](https://github.com/jellyfin/jellyfin-androidtv), GPL-2.0) — модули `:app`, `:design`, `:playback:core`, `:playback:jellyfin`, `:playback:media3:exoplayer`, `:playback:media3:session`, `:preference`. Koin 4.2.2 (`koin-android`, `koin-androidx-compose`, `koin-androidx-workmanager`), Leanback 1.2.0 + `compose-foundation` 1.12.1 + `fragment-compose` + `navigation3-ui` 1.1.7, media3 1.11.1 (+ `media3-datasource-okhttp`, `media3-exoplayer-hls`, ffmpeg-decoder, `io.github.peerless2012:ass-media` для ASS-субтитров), Coil 3.6.3, tvprovider 1.1.0, WorkManager, jellyfin-sdk 1.8.12, Kotlin 2.4.20, AGP 9.4.1, minSdk 23, compileSdk 37, targetSdk 36. Архитектурно важно: `playback:core` — собственная абстракция плеера (`PlaybackManager` + backends), а `playback:media3:session` — адаптер в media3 `SimpleBasePlayer` → `MediaSession`.
|
||
- **Streamyfin** ([repo](https://github.com/streamyfin/streamyfin), MPL-2.0) — Expo/React Native, MPV на всех платформах, есть TV-таргет (`:tv` скрипты), загрузки через FFmpeg/HLS; как образец архитектуры для нативного Kotlin — не релевантен, но подтверждает тренд «общая логика + отдельная TV-оболочка».
|
||
- Google-сэмпл с разделением по форм-факторам: [Jetcaster](https://github.com/android/compose-samples/tree/main/Jetcaster) (`mobile`/`tv`/`wear` + `core/*`).
|
||
|
||
### Ввод: пульт vs тач vs геймпад
|
||
- Общие composable'ы должны работать от `clickable`/`onClick` (D-pad center вызывает `onClick`), а TV-специфика (увеличение карточки по фокусу, `Glow`/`Border` из tv-material, `focusRestorer`) живёт в TV-модуле. Медиаклавиши (`KEYCODE_MEDIA_*`) — через `MediaSession`, не через `onKeyEvent`. Геймпад: см. §1 (D-pad может приходить как `AXIS_HAT_*`). Pointer-пульты/тач на ТВ — `android.software.leanback.supports_touch`.
|
||
|
||
---
|
||
|
||
## 4. Рекомендуемый стек (октябрь 2026) — версии проверены
|
||
|
||
| Компонент | Версия (stable) | Источник |
|
||
|---|---|---|
|
||
| Kotlin | **2.4.21** (2026-10-08; 2.4.0 — 2026-06-03; есть 2.5.0-Beta1) | [kotlinlang.org/docs/releases](https://kotlinlang.org/docs/releases.html) |
|
||
| AGP / Gradle / JDK | **AGP 9.4.0** (Gradle 9.6.0, JDK 17, Build-Tools 36, NDK 28.2); Findroid и jellyfin-androidtv на 9.4.1 | [AGP release notes](https://developer.android.com/build/releases/gradle-plugin) |
|
||
| Android Studio | Rabbit 1 (2026.2.1) | [studio/releases](https://developer.android.com/studio/releases) |
|
||
| Compose BOM | **2026.09.00** → ui/foundation/runtime 1.12.1, material3 1.4.0 | [BOM mapping](https://developer.android.com/develop/ui/compose/bom/bom-mapping) |
|
||
| Compose for TV | tv-material **1.1.0**, tv-foundation 1.0.0 | [releases/tv](https://developer.android.com/jetpack/androidx/releases/tv) |
|
||
| material3-adaptive | 1.3.0 (phone-ветка) | Google Maven |
|
||
| activity-compose | 1.13.0 | Google Maven |
|
||
| Lifecycle | **2.11.0** (viewmodel-navigation3, `rememberViewModelStoreOwner`) | [releases/lifecycle](https://developer.android.com/jetpack/androidx/releases/lifecycle) |
|
||
| Navigation 3 | **1.2.0** (2026-09-23; `navigation3-runtime` KMP, `navigation3-ui`; 1.3.0-alpha02) | [releases/navigation3](https://developer.android.com/jetpack/androidx/releases/navigation3) |
|
||
| Navigation Compose (2.x) | 2.10.2, **«maintenance mode, only critical fixes»** | [releases/navigation](https://developer.android.com/jetpack/androidx/releases/navigation) |
|
||
| media3 | **1.11.1** (2026-09-10) | [releases/media3](https://developer.android.com/jetpack/androidx/releases/media3) |
|
||
| Room | Room 3 **`androidx.room3:room3-runtime:3.0.3`** (KSP-only, suspend/Flow-first, SQLiteDriver) или Room 2 **2.8.5** | [releases/room3](https://developer.android.com/jetpack/androidx/releases/room3), [releases/room](https://developer.android.com/jetpack/androidx/releases/room) |
|
||
| Paging | **3.5.2** (`paging-compose`) | [releases/paging](https://developer.android.com/jetpack/androidx/releases/paging) |
|
||
| WorkManager | **2.12.0** (minSdk 24, compileSdk ≥33) | [releases/work](https://developer.android.com/jetpack/androidx/releases/work) |
|
||
| DataStore | **1.2.1** (1.3.0-alpha11) | [releases/datastore](https://developer.android.com/jetpack/androidx/releases/datastore) |
|
||
| Hilt | Dagger/Hilt **2.60.1** (Kotlin 2.3.21; KSP через `ksp("com.google.dagger:hilt-compiler")`), androidx.hilt **1.4.0** (`hilt-lifecycle-viewmodel-compose`, `hilt-navigation-compose`, `hilt-work`; требует AGP ≥9.2 из-за compileSdk 37) | [dagger releases](https://github.com/google/dagger/releases) |
|
||
| Koin | **4.2.2** (`koin-android`, `koin-androidx-compose`, `koin-androidx-workmanager`, `koin-compose-navigation3`) | [koin releases](https://github.com/InsertKoinIO/koin/releases) |
|
||
| Coil | **3.6.3** (`io.coil-kt.coil3:coil-compose`, `coil-network-okhttp`, `coil-network-cache-control`, `coil-svg`, `coil-gif`) | [coil releases](https://github.com/coil-kt/coil/releases) |
|
||
| Ktor | **3.6.0** (2026-09-17) | [ktor releases](https://ktor.io/docs/releases.html) |
|
||
| OkHttp | 5.5.0 | Maven Central |
|
||
| kotlinx.coroutines | **1.11.0** | [releases](https://github.com/Kotlin/kotlinx.coroutines/releases) |
|
||
| kotlinx.serialization | **1.11.0** (1.12.0-RC) | [releases](https://github.com/Kotlin/kotlinx.serialization/releases) |
|
||
| KSP | 2.3.12 | Maven Central |
|
||
| jellyfin-sdk-kotlin | **`org.jellyfin.sdk:jellyfin-core:1.9.2`** (2026-10-06, OpenAPI 12.2, минимум сервер 12.0; 1.8.12 — последняя для серверов 10.11) | [GitHub releases](https://github.com/jellyfin/jellyfin-sdk-kotlin/releases) |
|
||
| libVLC | **`org.videolan.android:libvlc-all:3.7.7`** (2026-10-01), 4.0.0-eap29 (2026-07-08); `medialibrary-all` 0.13.24 | Maven Central metadata |
|
||
| profileinstaller / macrobenchmark | 1.4.1 / 1.5.0 | Google Maven |
|
||
|
||
Пояснения и ссылки:
|
||
- **DI**: Findroid — Hilt (+KSP), jellyfin-androidtv — Koin. Для многомодульного проекта с двумя app-модулями оба работают; Hilt даёт compile-time проверку и `hilt-work`/`hilt-lifecycle-viewmodel-compose`; Koin проще в KMP/мультимодульности и уже имеет `koin-compose-navigation3`. Любой из них — вопрос вкуса; совместимость с Navigation 3 есть у обоих (`lifecycle-viewmodel-navigation3` + `hiltViewModel()`/`koinViewModel()`).
|
||
- **Навигация**: для нового Compose-проекта — **Navigation 3** (`NavDisplay`, `entryProvider`, `SceneStrategy`, `NavigationBackHandler`, deep links, Result API). Оба Jellyfin-клиента уже на Nav3.
|
||
- **Сеть/SDK**: jellyfin-sdk-kotlin с v1.7 по умолчанию на **OkHttp** (Ktor-пакет `jellyfin-api-ktor` объявлен удаляемым) — [migration v1.7](https://kotlin-sdk.jellyfin.org/migration/v1.7.html) (не проверено текстом, из поисковой выдачи); [Getting started](https://kotlin-sdk.jellyfin.org/guide/getting-started.html): `createJellyfin { clientInfo = ClientInfo(name, version); context = androidContext }`, `jellyfin.createApi(baseUrl, accessToken, deviceInfo, httpClientOptions)`, API через `api.userApi.authenticateUserByName(...)`, WebSocket/discovery через `Flow`; Android ≥4.4, в 1.9 — minSdk 21, compileSdk 37; SDK содержит `buildDeviceProfile {}` DSL (`org.jellyfin.sdk.model.deviceprofile`). Следствие: отдельный Ktor-клиент не нужен — один `OkHttpClient` для SDK, Coil и загрузок. Если всё же Ktor — на Android движок `ktor-client-okhttp` (HTTP/2, общий пул с OkHttp), CIO не поддерживает HTTP/2.
|
||
- **Coil 3 с заголовками Jellyfin**: [Coil network docs](https://coil-kt.github.io/coil/network/) — per-request `ImageRequest.Builder().httpHeaders(NetworkHeaders.Builder().set("Authorization","MediaBrowser Token=…").build())`, глобально — `OkHttpNetworkFetcherFactory(callFactory = { okHttpClient.newBuilder().addNetworkInterceptor(...).build() })`; по умолчанию Coil игнорирует `Cache-Control` (всегда пишет в дисковый кэш) — `coil-network-cache-control` для уважения заголовков (Findroid подключает).
|
||
- **BlurHash**: Coil 3 не имеет встроенного декодера. Варианты: [woltapp/blurhash](https://github.com/woltapp/blurhash) (референсная Kotlin-реализация в репо, **не публикуется** как Maven-артефакт — копировать файл), [vanniktech/blurhash](https://github.com/vanniktech/blurhash) (`com.vanniktech:blurhash:0.3.0`, KMP, MIT, encode+decode в `Bitmap`; релиз 2024-03), `io.github.brys0:blurhash` (AGSL/SKSL-шейдер, 0.0-4-ALPHA — не для прода), `com.github.skydoves:landscapist-coil3:2.13.2` (обёртка Coil3 + `landscapist-placeholder`, но BlurHash там нет — не проверено). Практика: декодировать BlurHash (из `ImageBlurHashes` в `BaseItemDto`) в маленький `Bitmap` → `placeholder(BitmapDrawable)` или рисовать `ImageBitmap` под `AsyncImage`.
|
||
- **Baseline Profiles**: [обзор](https://developer.android.com/topic/performance/baselineprofiles/overview) — плагин `androidx.baselineprofile` + `androidx.benchmark:benchmark-macro-junit4:1.5.0` + `androidx.profileinstaller:profileinstaller:1.4.1`; `./gradlew app:generateBaselineProfile`; ~30 % ускорение исполнения, Startup Profiles +15 %; профиль должен быть <1.5 MB; **для TV-боксов со слабым CPU особенно важно**, но учесть: вне Play (F-Droid/APK) профиль применяется не при установке, а после фонового dexopt (обычно «за ночь»); JetStreamCompose содержит готовый пример (`benchmark` модуль + `baseline-prof.txt`). TV-гайд по памяти тоже рекомендует baseline profiles.
|
||
- **R8**: [Shrink code](https://developer.android.com/build/shrink-code) — `isMinifyEnabled = true`, `isShrinkResources = true`, только `proguard-android-optimize.txt` (`proguard-android.txt` удалён в AGP 9.0); full mode по умолчанию с AGP 8; AGP 9.3 — новый `optimization { enable = true }` и keep-rules в `src/<variant>/keepRules/*.keep`; keep-правила для kotlinx.serialization и libVLC JNI (`org.videolan.libvlc.**` — не проверено, брать из README libvlc).
|
||
- **Version catalogs**: [Migrate to catalogs](https://developer.android.com/build/migrate-to-catalogs) — `gradle/libs.versions.toml` (`[versions]/[libraries]/[plugins]`, `alias(libs.plugins.…)`); оба Jellyfin-клиента используют каталоги + `TYPESAFE_PROJECT_ACCESSORS`.
|
||
- **KMP**: не нужен (Room 3/Nav3/Lifecycle KMP-совместимы, но целей кроме Android нет; libVLC-биндинги — Android-only).
|
||
- **Play target API**: с 31.08.2026 новые приложения/обновления — target 36 (телефоны), **Android TV — ≥34**; продление до 01.11.2026 — [target-sdk requirements](https://developer.android.com/google/play/requirements/target-sdk).
|
||
|
||
---
|
||
|
||
## 5. Медиа-интеграция на Android (без ExoPlayer как движка)
|
||
|
||
### media3 MediaSession поверх libVLC
|
||
- [The Player interface (media3)](https://developer.android.com/media/media3/session/player) — `MediaSession` требует `Player`; для своего движка — `SimpleBasePlayer(looper)`: обязателен `getState()` (`State.Builder().setAvailableCommands().setPlaybackState().setPlayWhenReady().setPlaylist().setContentPositionMs().setIsLoading()…`), затем `handle*` только для объявленных команд (`handlePrepare`, `handleSetPlayWhenReady`, `handleSeek`, `handleStop`, `handleRelease`, `handleSetMediaItems`, `handleSetVolume`, `handleSetPlaybackParameters`, `handleSetVideoOutput`, `handleSetTrackSelectionParameters`…), каждый возвращает `ListenableFuture`; `invalidateState()` при изменениях; все вызовы — на треде `Looper`. `ForwardingSimpleBasePlayer` — для декорирования существующего `Player`, не для своего движка ([androidx/media#2181](https://github.com/androidx/media/issues/2181): мейнтейнер подтвердил, что «реализация Player поверх другой инфраструктуры — это ровно то, для чего SimpleBasePlayer»; референс-кода Google не дал). Reference: [SimpleBasePlayer](https://developer.android.com/reference/kotlin/androidx/media3/common/SimpleBasePlayer).
|
||
- **Рабочие примеры в open source**:
|
||
- jellyfin-androidtv `playback/media3/session/src/main/kotlin/MediaSessionPlayer.kt` — `class MediaSessionPlayer(looper, manager: PlaybackManager) : SimpleBasePlayer(looper)` с `getState()` и `handleSetPlayWhenReady/handlePrepare/handleStop/handleSeek/handleSetPlaybackParameters/handleSetShuffleModeEnabled/handleSetRepeatMode/handleIncrease|Decrease|SetDeviceVolume/handleSetDeviceMuted/handleRelease`; `MediaSessionService.kt` — `MediaSession.Builder(androidContext, player)`. Бэкенд-независимый — идеальный шаблон для libVLC.
|
||
- Findroid `player/local/src/main/java/dev/jdtech/jellyfin/player/local/mpv/MPVPlayer.kt` — `class MPVPlayer(...) : BasePlayer(), MPVLib.EventObserver, AudioManager.OnAudioFocusChangeListener` (полная реализация `Player` поверх libmpv, включая `Timeline`, трек-листы, audio focus через `AudioFocusRequestCompat`/`AudioManagerCompat` из media3).
|
||
- VLC for Android ([PlaybackService.kt](https://github.com/videolan/vlc-android/blob/master/application/vlc-android/src/org/videolan/vlc/PlaybackService.kt)) и [mpv-android](https://github.com/mpv-android/mpv-android) используют **legacy `MediaSessionCompat` + `MediaBrowserServiceCompat` + `androidx.media.session.MediaButtonReceiver`** — рабочий, но устаревший путь (`androidx.media` 1.8.0 помечен superseded media3).
|
||
- [Background playback (MediaSessionService)](https://developer.android.com/media/media3/session/background-playback) — `MediaSessionService`/`MediaLibraryService`, `onGetSession()`, `onDestroy()` → `player.release(); session.release()`; манифест:
|
||
```xml
|
||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||
<service android:name=".PlaybackService" android:foregroundServiceType="mediaPlayback" android:exported="true">
|
||
<intent-filter><action android:name="androidx.media3.session.MediaSessionService"/></intent-filter>
|
||
</service>
|
||
```
|
||
Уведомление `MediaStyle` создаётся автоматически из `MediaItem.mediaMetadata` (с API 33 System UI берёт данные из сессии, `MediaNotification.Provider` не действует); сервис сам выходит из foreground через 10 мин паузы; `onTaskRemoved` → `pauseAllPlayersAndStopSelf()`; `MediaButtonReceiver` нужен только для playback resumption (тогда обязателен `onPlaybackResumption()`). media3 1.11: строгий threading (геттеры с чужого потока бросают `IllegalStateException`), `onConnectAsync()` вместо `onConnect`, недоверенные контроллеры по умолчанию read-only. Для **видео** на TV фоновый сервис не нужен (TV-NP), но `MediaSession` нужна для медиакнопок/голоса; для **музыки** из Jellyfin — `MediaSessionService` обязателен.
|
||
- [MediaController](https://developer.android.com/media/media3/session/control-playback) — UI общается с плеером через `MediaController.Builder(context, sessionToken).buildAsync()`.
|
||
- [media3-ui-compose](https://developer.android.com/media/media3/ui/compose) — `PlayerSurface`, `ContentFrame`, state-holders (`PlayPauseButtonState`, `ProgressStateWithTickInterval`, `MuteButtonState`…) работают с любым `Player`; `media3-ui-compose-material3` даёт готовые кнопки. С libVLC поверхность всё равно даётся через `VLCVideoLayout`/`SurfaceView` в `AndroidView` (**только SurfaceView для HDR**, `TextureView` на Android 13+ тонмапит HDR→SDR).
|
||
- [Audio focus](https://developer.android.com/media/optimize/audio-focus) — свой плеер обязан сам делать `AudioFocusRequest.Builder(AUDIOFOCUS_GAIN).setAudioAttributes(USAGE_MEDIA/CONTENT_TYPE_MOVIE).setOnAudioFocusChangeListener()`, реагировать на `LOSS/LOSS_TRANSIENT/LOSS_TRANSIENT_CAN_DUCK/GAIN`; Android 12+ система сама делает fade-out; **target 35+: запрос фокуса возможен только из top-app или foreground service**. ExoPlayer делает это сам через `setAudioAttributes(attrs, handleAudioFocus=true)` — для libVLC копировать логику из Findroid `MPVPlayer`.
|
||
- [Foreground service types](https://developer.android.com/develop/background-work/services/fgs/service-types) — `mediaPlayback` (без runtime-пререквизитов; с target 35 нельзя стартовать из `BOOT_COMPLETED`), `dataSync` (загрузки; таймауты, рекомендуют альтернативы — WorkManager/DownloadManager).
|
||
|
||
### Picture-in-Picture
|
||
- [PiP (общий гайд)](https://developer.android.com/develop/ui/views/picture-in-picture) — `android:supportsPictureInPicture="true"`, `configChanges="screenSize|smallestScreenSize|screenLayout|orientation"`, `launchMode="singleTask"`; Android 12+: `PictureInPictureParams.Builder().setAspectRatio().setSourceRectHint().setAutoEnterEnabled(true).setSeamlessResizeEnabled(true)`, вызывать `setPictureInPictureParams` заранее и при смене aspect; `androidx.activity` `trackPipAnimationHintView`; до 12 — `onUserLeaveHint { enterPictureInPictureMode(params) }`; контролы play/pause/next/prev появляются автоматически при активной `MediaSession`, свои — `RemoteAction` (≤ `getMaxNumPictureInPictureActions()`); паузить в `onStop`, не в `onPause`; `onPictureInPictureUiStateChanged` (Android 15) для скрытия оверлеев; проверять `FEATURE_PICTURE_IN_PICTURE` (low-RAM может не иметь).
|
||
- [PiP на TV / multitasking](https://developer.android.com/training/tv/get-started/multitasking) — PiP на TV только с Android 14 (API 34); **обязательна meta-data `com.google.android.tv.pip.category`** со значениями `communication|smartHome|health|ticker`, и «usage types outside these categories, **including any playback of media content, aren't allowed in PiP on TV**» → для видеоклиента PiP на TV фактически запрещён; на телефоне — да. Keep-clear API (`setPreferKeepClear`, `setPreferKeepClearRects` для Compose) — чтобы чужое PiP-окно не перекрывало важные элементы вашего fullscreen-UI.
|
||
|
||
### Частота кадров (24p и т.д.)
|
||
- [Frame rate API](https://developer.android.com/media/optimize/performance/frame-rate) — `Surface.setFrameRate(fps, FRAME_RATE_COMPATIBILITY_FIXED_SOURCE, CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS|CHANGE_FRAME_RATE_ALWAYS)` (API 30+, третий параметр с API 31; также `SurfaceControl.Transaction.setFrameRate`, NDK `ANativeWindow_setFrameRate`); передавать точную частоту (23.976, 29.97); для фильмов — `CHANGE_FRAME_RATE_ALWAYS` **только** если пользователь включил «Match content frame rate» (`DisplayManager.getMatchContentFrameRateUserPreference() == MATCH_CONTENT_FRAMERATE_ALWAYS`), при non-seamless переключении показать UI и подождать `onDisplayChanged` + ~2 с; сброс `setFrameRate(0f, …)` на паузе/конце; не вызывать часто. Legacy-путь `WindowManager.LayoutParams.preferredDisplayModeId` (+ `Display.getSupportedModes()`, `Display.Mode.getAlternativeRefreshRates()` API 31) — когда нужна смена разрешения или «тяжёлый modeset на Android TV» (док прямо называет этот кейс); `preferredRefreshRate` игнорируется для поверхностей с `setFrameRate`. Важно для libVLC: `setFrameRate` **не действует**, если `Surface` идёт напрямую в MediaCodec, а не в композитор (примечание в reference `Surface`) — значит на TV практичнее `preferredDisplayModeId` по `MediaInfo` потока (как делает jellyfin-androidtv для refresh-rate switching). TV-страница: [Match content frame rate](https://developer.android.com/training/tv/playback/framerate) (там же — Android 16 `MediaQualityManager`/picture profiles «Filmmaker mode»).
|
||
|
||
### HDR / Dolby Vision
|
||
- [HDR video playback (developer guide)](https://developer.android.com/media/grow/hdr-playback) — `display.hdrCapabilities.supportedHdrTypes` (`HDR_TYPE_HDR10/HLG/DOLBY_VISION/HDR10_PLUS`), с Android 13 HLG10 — минимум; без ExoPlayer: `MediaCodecList.findDecoderForFormat(format с KEY_PROFILE = HEVCProfileMain10HDR10 / AV1ProfileMain10 …)`; только `SurfaceView`.
|
||
- [AOSP: HDR video playback](https://source.android.com/docs/core/display/hdr) — профили `HEVCProfileMain10HDR10(+)`, `VP9Profile2HDR(10Plus)`, Dolby Vision через MIME `video/dolby-vision` + `DolbyVisionProfileDvheStn/Dtr/St…`; **только tunneled-декодеры гарантируют HDR**; `CodecCapabilities.isFormatSupported` не учитывает требование tunneled; `KEY_HDR_STATIC_INFO`/`KEY_HDR10_PLUS_INFO`. Reference: [Display.HdrCapabilities](https://developer.android.com/reference/android/view/Display.HdrCapabilities), [Display.Mode](https://developer.android.com/reference/android/view/Display.Mode), [MediaCodecInfo.CodecCapabilities](https://developer.android.com/reference/android/media/MediaCodecInfo.CodecCapabilities) (`isFeatureSupported(FEATURE_TunneledPlayback/SecurePlayback)`, `profileLevels`, `videoCapabilities.areSizeAndRateSupported(w,h,fps)`, `MediaCodecInfo.isHardwareAccelerated()` API 29). HDR/DV passthrough на уровне ОС: libVLC сам отдаёт поток в MediaCodec (`mediacodec_ndk`) — приложение лишь выбирает, что direct-play'ить, через DeviceProfile.
|
||
|
||
### Аудио-возможности и passthrough
|
||
- [Audio capabilities (TV)](https://developer.android.com/training/tv/playback/audio-capabilities) — API 33+: `AudioManager.getDirectPlaybackSupport(AudioFormat(E_AC3, 5.1, 48 kHz), AudioAttributes(USAGE_MEDIA)) != DIRECT_PLAYBACK_NOT_SUPPORTED`, `getDirectProfilesForAttributes()` (только текущий маршрут, без транскодинга фреймворком); API 29+: `AudioTrack.isDirectPlaybackSupported()` (до API 33 учитывает все выходы, в т.ч. неактивные — ложные срабатывания при BT); API 23/31: `AudioDeviceInfo.getEncodings()/getAudioProfiles()` через `AudioTrack.getRoutedDevice()`; слушать `AudioRouting.OnRoutingChangedListener`/`AudioDeviceCallback`/`ACTION_HDMI_AUDIO_PLUG`; Bluetooth — только PCM. Системная настройка Android TV «Surround sound: Auto/Manual/Never» влияет на результаты (в документе не описана — не проверено).
|
||
- **Готовая реализация DeviceProfile из возможностей устройства** (jellyfin-androidtv): `app/src/main/java/org/jellyfin/androidtv/util/profile/deviceProfile.kt` (`buildDeviceProfile {}` из SDK: `maxStreamingBitrate`, DirectPlay/Transcoding/Codec/Subtitle-профили, `VideoRangeType` исключения для DOVI/HDR10/HDR10+ по результатам тестов), `MediaCodecCapabilitiesTest.kt` (`MediaCodecList(REGULAR_CODECS)`; `supportsHevc/Main10/DolbyVision/DolbyVisionEL/HDR10/HDR10Plus`, `supportsAV1*`, `supportsAVCHigh10`, уровни, `getMaxResolution(mime)`, флаг софт-кодеков), `codec/{Av1,Avc,Hevc}CodecCapabilities.kt`, `codec/MediaCodecQuery.kt`, `codec/audioPassthrough.kt` (`isPassthroughAudioAvailable(context, mimetype)` через `AudioFormat` stereo/5.1), `KnownDefects.kt` (обход прошивочных багов). Это лучший открытый образец «MediaCodecList → Jellyfin DeviceProfile»; для libVLC те же проверки MediaCodec остаются валидными (libVLC использует те же аппаратные декодеры), плюс список софт-декодируемого FFmpeg'ом libVLC.
|
||
|
||
---
|
||
|
||
## 6. Загрузки / офлайн
|
||
|
||
- [Data and file storage overview](https://developer.android.com/training/data-storage) — для офлайн-кэша видео: **app-specific external storage** `context.getExternalFilesDirs(null)[i]` / `getExternalFilesDir(DIRECTORY_MOVIES)` — без разрешений (scoped storage с API 29), удаляется при деинсталляции, может быть на съёмной SD (проверять `Environment.getExternalStorageState(dir) == MEDIA_MOUNTED`); shared storage/MediaStore — только если файлы должны быть видны другим приложениям (для GPL-клиента скорее нет). `MANAGE_EXTERNAL_STORAGE` не нужен.
|
||
- [DownloadManager](https://developer.android.com/reference/android/app/DownloadManager) — системный сервис: очередь, ретраи, продолжение после смерти процесса, уведомления; `Request.setDestinationUri(file-uri в app-specific dir)` / `setDestinationInExternalFilesDir`, `addRequestHeader("Authorization", "MediaBrowser Token=…")`, `setAllowedOverMetered`, `setNotificationVisibility`; прогресс через `query(Query().setFilterById(id))` + `COLUMN_BYTES_DOWNLOADED_SO_FAR/COLUMN_TOTAL_SIZE_BYTES`; `ACTION_DOWNLOAD_COMPLETE`; ограничения — нет pause/resume API, только заголовки для авторизации, нет контроля над параллелизмом/бэкоффом.
|
||
- **Как сделано в Findroid** (`core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt`): интерфейс `Downloader { downloadItem(item, sourceId, storageIndex); cancelDownload; deleteItem; getProgress(downloadId) }`; видео и внешние потоки субтитров — через **`android.app.DownloadManager`** (`Request(source.path.toUri()).setDestinationUri(path)` в `context.getExternalFilesDirs(null)[storageIndex]`, `VISIBILITY_VISIBLE_NOTIFY_COMPLETED` для основного файла и `VISIBILITY_HIDDEN` для сабов), прогресс опросом `DownloadManager.Query`; картинки — через **WorkManager** (`workManager.enqueue(downloadImagesRequest)`); метаданные офлайн-библиотеки — в Room (`data/.../repository/JellyfinRepositoryOfflineImpl.kt` — отдельная реализация репозитория для офлайн-режима). Направление «WorkManager + OkHttp» даёт pause/resume (Range), лимит параллельности и контроль ошибок, но требует собственного foreground-уведомления (`dataSync` тип с таймаутами на Android 15) — компромисс Findroid (DownloadManager для тяжёлых файлов) практичен для первого релиза.
|
||
- Шифрование не требуется (ваше решение), но на TV стоит учесть: внешние диски по USB на Android TV доступны как `getExternalFilesDirs()` только если смонтированы как adoptable/portable (не проверено).
|
||
|
||
---
|
||
|
||
## 7. Дистрибуция и лицензирование
|
||
|
||
### Google Play
|
||
- GPL-приложения в Play разрешены де-факто: VLC (`org.videolan.vlc`), Jellyfin for Android TV (`org.jellyfin.androidtv`, GPL-2.0, в README есть бейдж Google Play), Findroid (GPL-3.0, Google Play + Amazon Appstore + F-Droid + IzzyOnDroid), Kodi. Отдельной политики Play про GPL нет; конфликт GPLv3 с условиями стора исторически касался Apple App Store, не Play. Страницы Play напрямую (play.google.com) открыть не удалось (таймаут) — (не проверено).
|
||
- [Distribute to Android TV](https://developer.android.com/training/tv/publishing/distribute) — соответствие TV quality guidelines; **с 1 августа 2026 — 64-bit + 16 KB**; тот же package name, что и у мобильной версии (единый AAB, отдельный TV-трек по желанию); листинг: ≥1 TV-скриншот, **TV banner** и упоминание «Android TV» в описании; opt-in: Play Console → Setup → Advanced settings → Form factors → Add release type → Android TV; после публикации Google ревьюит по TV-критериям и присылает письмо с замечаниями (типично: D-pad недоступность элементов, отсутствие баннера/LEANBACK_LAUNCHER, тач-зависимость).
|
||
- [Graphic assets (Play Console help)](https://support.google.com/googleplay/android-developer/answer/9866151) — **TV banner для листинга: 1280×720 px, JPEG/24-bit PNG без альфы** (не путать с in-app `android:banner` 320×180 xhdpi); TV-скриншоты: JPEG/PNG, 320–3840 px, рекомендовано 1920×1080 16:9; иконка 512×512 PNG; feature graphic 1024×500.
|
||
- [16 KB page sizes](https://developer.android.com/guide/practices/page-sizes) — требование Play для target 35+ (с 01.11.2025; с 01.02.2027 без поддержки нельзя выпускать обновления); проверка: `check_elf_alignment.sh app.apk`, `zipalign -v -c -P 16 4 app.apk`, `llvm-objdump -p lib.so | grep LOAD` (align 2**14); AGP ≥8.5.1 + NDK ≥r28; эмулятор `google-tv-ps16k`; `adb shell getconf PAGE_SIZE` → 16384. **libVLC**: баг-репорт [vlc-android#3242](https://code.videolan.org/videolan/vlc-android/-/issues/3242) (страница закрыта анти-бот-защитой, содержимое из поисковой выдачи — не проверено) сообщал о невыровненных `libvlc.so/libvlcjni.so/libc++_shared.so` в 3.6.2; исправление упоминалось в контексте 3.6.5+ — обязательно проверить 3.7.7 скриптом выше.
|
||
- Подпись: `apksigner`, v2/v3 схема, один release-ключ для Play/GitHub/F-Droid (reproducible) — ключ нельзя менять, иначе обновления между источниками ломаются.
|
||
|
||
### F-Droid
|
||
- [Inclusion Policy](https://f-droid.org/docs/Inclusion_Policy/) — FLOSS-лицензия; Google Play Services/Firebase/Crashlytics/проприетарные SDK **строго запрещены** (нужен `libre`-flavor без них); prebuilt-бинарники допустимы, если свободно лицензированы и из доверенных источников: Debian main (предпочтительно), **Maven Central, Google Maven**, JitPack, OSS Sonatype и др.; Anti-Features обязательно декларировать.
|
||
- [Reproducible Builds](https://f-droid.org/docs/Reproducible_Builds/) — если сборка F-Droid побайтно совпадает с вашим APK, публикуется APK **с вашей подписью** (`Binaries:` + `AllowedAPKSigningKeys`); типичные проблемы: timestamps, build paths в native-коде, версии NDK/JDK, `baseline.prof`, PNG crunch, R8-недетерминизм, `apksigner` ≥35.0.0-rc1 (использовать 34), `vcsInfo`.
|
||
- **libVLC в F-Droid — фактическое положение дел** (из [metadata/org.videolan.vlc.yml](https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/org.videolan.vlc.yml)): VLC 3.7.1 (versionCode 13070108) собирается F-Droid'ом так: `sed -i -e "/':libvlcjni/d" -e "/':medialibrary/d" ../../settings.gradle` — т.е. **libVLC и medialibrary берутся как prebuilt AAR с Maven Central**, а не компилируются (`compile.sh` использовался до 3.5.x). AntiFeatures нет, сборки per-ABI. Следовательно, `org.videolan.android:libvlc-all` из Maven Central для F-Droid приемлем (VideoLAN — апстрим, лицензия LGPL). Аналогично Findroid ([metadata/dev.jdtech.jellyfin.yml](https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/dev.jdtech.jellyfin.yml)) тянет prebuilt `dev.jdtech.mpv:libmpv` (собран из исходников в [jarnedemeulemeester/libmpv-android](https://github.com/jarnedemeulemeester/libmpv-android), MIT) и публикуется reproducible (`Binaries: …findroid-v%v-libre-arm64-v8a.apk`, flavor `libre`, только arm64 из-за размера). Nova Video Player ([metadata/org.courville.nova.yml](https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/org.courville.nova.yml)) — собирает ffmpeg/dav1d/openssl из исходников внутри своего репо, AntiFeature `NonFreeNet` (TMDB). jellyfin-androidtv в F-Droid ([metadata](https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/org.jellyfin.androidtv.yml)) — reproducible с `AllowedAPKSigningKeys`, prebuilt `org.jellyfin.media3:media3-ffmpeg-decoder` с Maven Central. Обсуждение политики по AAR: [форум F-Droid «Can applications integrate pre-compiled AAR libraries?»](https://forum.f-droid.org/t/policy-clarification-can-applications-integrate-pre-compiled-aar-libraries/33624) — формального запрета нет, предпочтение сборке из исходников, Maven Central в allowlist, желательны provenance/SBOM.
|
||
- Практические требования к репо для F-Droid: тегированные релизы, версия в читаемом месте (`UpdateCheckMode: Tags`), fastlane-метаданные (`fastlane/metadata/android/*`), `libre` flavor без Play-зависимостей, детерминированная сборка.
|
||
|
||
### IzzyOnDroid, GitHub Releases, Obtainium
|
||
- [IzzyOnDroid App Inclusion Policy](https://izzyondroid.org/docs/general/AppInclusionPolicy/) — FLOSS, APK из тегированных GitHub/GitLab/Codeberg-релизов (новые приложения только так), подпись release-ключом, без `debuggable/testOnly`, без трекеров/рекламы, **лимит ~30 MB на APK** (держат до 3 версий) — для libVLC-приложения нужны **per-ABI split APK** (fat APK с 4 ABI libVLC в 30 MB не влезет).
|
||
- [Obtainium](https://github.com/ImranR98/Obtainium) (GPL-3.0) — обновления напрямую из GitHub Releases/GitLab/Codeberg/F-Droid/IzzyOnDroid; поддерживает deep-link «Add to Obtainium» (см. wiki проекта); Streamyfin публикует так. Для TV: Obtainium на Android TV устанавливается side-load'ом (не проверено).
|
||
|
||
### Amazon Fire TV
|
||
- [Fire TV vs Android TV development](https://developer.amazon.com/docs/fire-tv/differences-from-android-tv-development.html) — Fire TV уважает `LEANBACK_LAUNCHER`, частично поддерживает Leanback (кроме `SearchFragment`); **нет Google Play Services/Firebase**, голосовой поиск — через Alexa; аудиофокус требует `MediaButtonReceiver` в манифесте; Fire TV имеет кнопки RW/FF/Menu.
|
||
- [Fire OS overview (версии)](https://developer.amazon.com/docs/fire-tv/fire-os-overview.html) — Fire OS 5 = Android 5.1 (22), 6 = 7.1 (25), 7 = Android 9 (28), 8 = Android 10/11 (29/30; Stick 4K Max 2nd gen, Omni QLED…), 14 = Android 12–14 (31–34; телевизоры 2026), 16 = Android 15/16; новые стики 2026 — **Vega OS** (не Android, свой SDK — ваше приложение туда не попадёт). Следствие: minSdk 25–28 покрывает парк Fire TV; PiP на Fire TV нет (API<34 у большинства).
|
||
- Домашний экран Fire TV: нет Google TV-каналов/Watch Next; «Continue Watching» на Fire TV — через Amazon Content Personalization/watch activity (документация в разделе Vega: [intro-to-vega-content-personalization](https://developer.amazon.com/docs/vega/0.24/intro-to-vega-content-personalization.html); для классических Android-приложений Fire TV — отдельный Fire TV Launcher Integration (не проверено)).
|
||
- [Appstore details: image assets](https://developer.amazon.com/docs/app-submission/appstore-details.html) — Fire TV: иконка **1280×720 PNG без прозрачности** (safe area 882×448), скриншоты 3–10 шт 1920×1080, фон 1920×1080 (safe area 1214×830); планшеты: 114×114 и 512×512 PNG.
|
||
- [Connect to Fire TV through ADB](https://developer.amazon.com/docs/fire-tv/connecting-adb-to-device.html) — Settings → My Fire TV → Developer Options (7 нажатий на «About» при необходимости) → ADB Debugging + Apps from Unknown Sources; `adb connect <ip>:5555`.
|
||
|
||
### Иконки и баннеры (сводка)
|
||
- In-app: `android:banner` 320×180 px (xhdpi), текст в картинке, по баннеру на язык; launcher icon ≥160×160 xhdpi (adaptive icon как обычно).
|
||
- Play листинг: TV banner 1280×720; скриншоты TV 1920×1080; иконка 512×512; feature graphic 1024×500.
|
||
- Amazon: иконка Fire TV 1280×720 PNG; фон 1920×1080.
|
||
|
||
---
|
||
|
||
## 8. Тестирование
|
||
|
||
- **Эмулятор**: образы `android-tv` и `google-tv` для API 33/34/36 (+ `google-tv-ps16k` API 36 для 16 KB) — создаются через Device Manager (категория TV, профили «Television 1080p/4K»). Для low-RAM-тестов — AVD с 1–1.5 GB RAM и `isLowRamDevice`-поведением (ro.config.low_ram — не проверено).
|
||
- **Реальный ТВ**: Android 13+ — Wireless debugging (`adb pair ip:port`), иначе Developer options → USB/Network debugging и `adb connect ip:5555` ([adb](https://developer.android.com/tools/adb)); Fire TV — см. выше. Полезно: `adb shell input keyevent KEYCODE_DPAD_RIGHT`, `adb shell dumpsys media_session`, `adb shell dumpsys SurfaceFlinger | grep -i "refresh"` (не проверено).
|
||
- **Compose UI-тесты с D-pad**: [Test focus navigation](https://developer.android.com/develop/ui/compose/touch-input/focus/testing-focus) — `createComposeRule()`, задать стартовый фокус `performClick()`/`requestFocus()`, затем `node.performKeyInput { pressKey(Key.DirectionDown) }` и `target.assertIsFocused()`/`assertIsNotFocused()`; `withKeyDown(Key.ShiftLeft) { pressKey(Key.Tab) }`; события внутри одного `performKeyInput` батчатся без рекомпозиции между ними — разделять на несколько вызовов. Зависимости: `androidx.compose.ui:ui-test-junit4`, `debugImplementation ui-test-manifest` ([Compose testing](https://developer.android.com/develop/ui/compose/testing)); `performKeyInput` в актуальном reference без `@ExperimentalTestApi` (раньше была) — [composables.com reference](https://composables.com/docs/androidx.compose.ui/ui-test/functions/performKeyInput). Для end-to-end на TV — Macrobenchmark/UIAutomator (`pressDPadRight()`), как в JetStream `benchmark`.
|
||
- **Jellyfin для разработки**: публичный демо-сервер [https://demo.jellyfin.org/stable](https://demo.jellyfin.org/stable) (ссылка «Try the Demo» на jellyfin.org), логин `demo`, пароль пустой ([подтверждение модератора на форуме](https://forum.jellyfin.org/t-jellyfin-demo-credentials)); демо периодически сбрасывается, транскодинг может быть ограничен. Локально — Docker: [Jellyfin in a container](https://jellyfin.org/docs/general/installation/container/):
|
||
```bash
|
||
docker run -d --name jellyfin -p 8096:8096/tcp -p 7359:7359/udp \
|
||
-v /path/config:/config -v /path/cache:/cache \
|
||
--mount type=bind,source=/path/media,target=/media \
|
||
--restart=unless-stopped jellyfin/jellyfin # или ghcr.io/jellyfin/jellyfin
|
||
```
|
||
(7359/udp — автодискавери, которое использует `jellyfin-sdk` `discovery`; HW-транскодинг — проброс `/dev/dri`). Тестовый контент — свободные фильмы Blender: [Big Buck Bunny (480p/720p/1080p, mov/avi/ogg)](https://download.blender.org/peach/bigbuckbunny_movies/) + Sintel/Tears of Steel (не проверено); для HDR/DV/24p/passthrough-кейсов нужны собственные образцы (например, Dolby/Kodi sample-клипы — не проверено). Эмулятор Android TV API 33+ находит сервер в хост-сети через `10.0.2.2:8096`.
|
||
- Для TV-ревью перед публикацией — пройти чек-лист TV-DP/TV-OV/TV-PC из §1 на эмуляторе только с клавишами стрелок/Enter/Esc.
|
||
|
||
---
|
||
|
||
## Пробелы и непроверенное
|
||
- Статус 16 KB-выравнивания в `libvlc-all:3.7.7` — проверять локально (`zipalign -P 16`), первоисточник (issue 3242 на code.videolan.org) закрыт антибот-защитой.
|
||
- Содержимое `kotlin-sdk.jellyfin.org/migration/v1.7.html` (OkHttp по умолчанию) — из поисковой выдачи, страница не открывалась напрямую.
|
||
- Точные sp-значения TV-типографики (на странице — только изображение).
|
||
- Google Play страницы приложений (VLC, Jellyfin ATV) — не открылись по таймауту; факт наличия подтверждён бейджами в README проектов.
|
||
- Отдельного Fire TV API «Continue Watching» для классических Android-приложений (вне Vega) — не найдено/не проверено.
|
||
- Android Studio Rabbit 1: какой AGP в комплекте — не указано на странице релизов (AGP 9.4.0 — текущий stable по странице AGP; оба Jellyfin-клиента уже на 9.4.1).
|
||
|
||
---
|
||
|
||
# Часть 5. Доменный слой воспроизведения
|
||
|
||
_Исходный заголовок: Jellyfin: доменный слой воспроизведения — DeviceProfile, транскодирование, субтитры/аудио, изображения, UX, экосистема, тестовые медиа
|
||
|
||
Дата исследования: 2026-10-11. Все ссылки ниже проверены (HTTP 200 или прочитаны), кроме помеченных «(не проверено)». Исходники клиентов/сервера скачаны с raw.githubusercontent.com и прочитаны локально; цитаты идентификаторов — дословные.
|
||
|
||
## 0. Контекст версий (важно для планирования)
|
||
|
||
- Актуальный сервер: **Jellyfin 12.2** (релиз 05.10.2026) — [GitHub releases/latest](https://github.com/jellyfin/jellyfin/releases/latest). Публичный демо-сервер тоже на 12.2.0 (`GET https://demo.jellyfin.org/stable/System/Info/Public` → `"Version":"12.2.0"`).
|
||
- [Jellyfin 12.0 (07.09.2026)](https://jellyfin.org/posts/jellyfin-release-12.0) — префикс «10.» отброшен (10.12 → 12.0). Критично для клиентов: удалены legacy-префиксы `/emby/` и `/mediabrowser/`; устаревшие методы авторизации отключены по умолчанию; **удалён `GET /QuickConnect/Initiate` (только POST)**; `GetItems` применяет `recursive` при фильтрах; множественные версии эпизодов (resume привязан к версии); плейлисты построчно; настройки субтитров — per-library, добавлена запись VobSub, внешние субтитры могут вшиваться в MKS при транскоде; трикплей подхватывается, а не регенерируется; HLS: вариант `dvh1` для Dolby Vision P5; HLG tone-mapping по BT.2446B; FFmpeg 8.1; добавлена опция DeviceProfile для Android TV-боксов, не умеющих повёрнутое видео (ср. `VideoRotation`/`VideoRotationNotSupported` ниже). Сервер теперь отдаёт версию `12.0.0` — проверьте парсеры версий.
|
||
- [Jellyfin 10.11.0 (19.10.2025)](https://jellyfin.org/posts/jellyfin-release-10.11.0/) — EF Core база; **`OnPlaybackStart/Progress/Stopped` помечены deprecated → использовать `ReportPlaybackStart/Progress/Stopped`** (`POST /Sessions/Playing[...]`); рейтинги переехали в `ratingScore`; сервер может отвечать `503` при старте/остановке — уважать `Retry-After`; media segment providers настраиваются per-library; лирика получила word-level cues; FFmpeg 7.1; HDR10+ детект и выборочное удаление DV/HDR10+ метаданных.
|
||
- [Jellyfin 10.10.0](https://jellyfin.org/posts/jellyfin-release-10.10.0/) — появились Media Segments (нужен плагин-провайдер), keyframe-only trickplay, software tonemapping HDR10/HLG/DV, DV Profile 10 (предварительно), AC-4.
|
||
- [Jellyfin 10.9.0](https://jellyfin.org/posts/jellyfin-release-10.9.0/) — трикплей встроен в сервер («requires explicit client support»), HLS с Opus/FLAC/ALAC, DTS/TrueHD enforcement, нормализация громкости, лирика, DV remux, AV1 encoding, PIN-логин удалён (Quick Connect остался), DLNA вынесен в плагин.
|
||
- OpenAPI: [api.jellyfin.org](https://api.jellyfin.org/) (ReDoc), JSON стабильной спеки — [jellyfin-openapi-stable.json](https://api.jellyfin.org/openapi/jellyfin-openapi-stable.json). Kotlin SDK: [jellyfin-sdk-kotlin](https://github.com/jellyfin/jellyfin-sdk-kotlin).
|
||
|
||
---
|
||
|
||
## 1. DeviceProfile — глубокий разбор
|
||
|
||
### 1.1. Модель на сервере (точные поля и enum'ы)
|
||
|
||
- [MediaBrowser.Model/Dlna/DeviceProfile.cs](https://github.com/jellyfin/jellyfin/blob/master/MediaBrowser.Model/Dlna/DeviceProfile.cs) — поля: `Name`, `Id`, `MaxStreamingBitrate` (default `8000000`), `MaxStaticBitrate` (default `8000000`), `MusicStreamingTranscodingBitrate` (default `128000`), `MaxStaticMusicBitrate` (`8000000`), `DirectPlayProfiles[]`, `TranscodingProfiles[]`, `ContainerProfiles[]`, `CodecProfiles[]`, `SubtitleProfiles[]`. Дефолты низкие — клиент обязан задать свои.
|
||
- `DirectPlayProfile`: `Container` (CSV, пустая строка = любой), `AudioCodec`, `VideoCodec`, `Type` (`DlnaProfileType`: Audio/Video/Photo/Subtitle/Lyric).
|
||
- `TranscodingProfile`: `Container`, `Type`, `VideoCodec`, `AudioCodec`, `Protocol` (`MediaStreamProtocol`: `http`=0, `hls`=1), `EstimateContentLength`, `EnableMpegtsM2TsMode`, `TranscodeSeekInfo` (`Auto`/`Bytes`), `CopyTimestamps`, `Context` (`EncodingContext`: `Streaming`=0, `Static`=1 — Static используется для скачивания/оффлайна), `EnableSubtitlesInManifest`, `MaxAudioChannels` (строка!), `MinSegments`, `SegmentLength`, `BreakOnNonKeyFrames`, `Conditions[]`, `EnableAudioVbrEncoding` (default true). Порядок кодеков в `VideoCodec`/`AudioCodec` = приоритет.
|
||
- `ContainerProfile`: `Type`, `Conditions[]`, `Container`, `SubContainer`.
|
||
- `CodecProfile`: `Type` (`CodecType`: Video / VideoAudio / Audio), `Conditions[]`, `ApplyConditions[]` (условия, при которых профиль вообще применяется), `Codec`, `Container`, `SubContainer`.
|
||
- `ProfileCondition`: `Condition` (`ProfileConditionType`: `Equals`=0, `NotEquals`=1, `LessThanEqual`=2, `GreaterThanEqual`=3, `EqualsAny`=4), `Property`, `Value`, `IsRequired`.
|
||
- `ProfileConditionValue` ([файл](https://github.com/jellyfin/jellyfin/blob/master/MediaBrowser.Model/Dlna/ProfileConditionValue.cs)): `AudioChannels, AudioBitrate, AudioProfile, Width, Height, Has64BitOffsets, PacketLength, VideoBitDepth, VideoBitrate, VideoFramerate, VideoLevel, VideoProfile, VideoTimestamp, IsAnamorphic, RefFrames, NumAudioStreams, NumVideoStreams, IsSecondaryAudio, VideoCodecTag, IsAvc, IsInterlaced, AudioSampleRate, AudioBitDepth, VideoRangeType, NumStreams, VideoRotation`.
|
||
- `SubtitleProfile`: `Format`, `Method`, `DidlMode`, `Language`, `Container`.
|
||
- [SubtitleDeliveryMethod.cs](https://github.com/jellyfin/jellyfin/blob/master/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs): `Encode = 0` (burn-in при транскоде), `Embed = 1` (остаются в контейнере, клиент сам рендерит), `External = 2` (отдельный HTTP-поток `/Subtitles/.../Stream.{fmt}`), `Hls = 3` (отдельная WebVTT-дорожка в HLS-манифесте, только при `PlayMethod.Transcode`), `Drop = 4`.
|
||
- `VideoRangeType` ([файл](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Data/Enums/VideoRangeType.cs)): `Unknown, SDR, HDR10, HLG, DOVI, DOVIWithHDR10, DOVIWithHLG, DOVIWithSDR, DOVIWithEL, DOVIWithHDR10Plus, DOVIWithELHDR10Plus, DOVIInvalid, HDR10Plus`. `DOVI` = «чистый» Profile 5 (IPTPQc2), который без DV-декодера даёт зелёно-фиолетовую картинку; `DOVIWithHDR10` = P8.1, `DOVIWithEL` = P7 и т.п.
|
||
- `TranscodeReason` ([файл](https://github.com/jellyfin/jellyfin/blob/master/MediaBrowser.Model/Session/TranscodeReason.cs), flags, сервер отдаёт в `MediaSourceInfo.TranscodeReasons`): `ContainerNotSupported, VideoCodecNotSupported, AudioCodecNotSupported, SubtitleCodecNotSupported, AudioIsExternal, SecondaryAudioNotSupported, StreamCountExceedsLimit, VideoProfileNotSupported, VideoRangeTypeNotSupported, VideoCodecTagNotSupported, VideoLevelNotSupported, VideoResolutionNotSupported, VideoBitDepthNotSupported, VideoFramerateNotSupported, VideoRotationNotSupported, RefFramesNotSupported, AnamorphicVideoNotSupported, InterlacedVideoNotSupported, AudioChannelsNotSupported, AudioProfileNotSupported, AudioSampleRateNotSupported, AudioBitDepthNotSupported, ContainerBitrateExceedsLimit, VideoBitrateNotSupported, AudioBitrateNotSupported, UnknownVideoStreamInfo, UnknownAudioStreamInfo, DirectPlayError`. Swiftfin держит локализованные названия всех причин в [TranscodeReason.swift](https://github.com/jellyfin/Swiftfin/blob/main/Shared/Extensions/JellyfinAPI/TranscodeReason.swift); jellyfin-web показывает их в оверлее статистики ([playerstats.js](https://github.com/jellyfin/jellyfin-web/blob/master/src/components/playerstats/playerstats.js), `session.TranscodingInfo.TranscodeReasons`).
|
||
|
||
### 1.2. Как сервер решает (StreamBuilder)
|
||
|
||
- [MediaBrowser.Model/Dlna/StreamBuilder.cs](https://github.com/jellyfin/jellyfin/blob/master/MediaBrowser.Model/Dlna/StreamBuilder.cs) — ключевые факты:
|
||
- Жёсткие белые списки HLS независимо от профиля: `_supportedHlsVideoCodecs = ["h264", "hevc", "vp9", "av1"]`, `_supportedHlsAudioCodecsTs = ["aac", "ac3", "eac3", "mp3"]`, `_supportedHlsAudioCodecsMp4 = ["aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dts", "truehd"]`. Итог: контейнер `mp4` (fMP4 HLS) даёт больше passthrough-аудио (DTS/TrueHD/FLAC/Opus), чем `ts`. Кодек вне списка → `VideoCodecNotSupported`/`AudioCodecNotSupported` даже если вы его объявили. Swiftfin из-за этого заводит отдельный TranscodingProfile для VC1, чтобы получить direct stream (комментарий в коде: «Necessary to avoid server-imposed HLS video restrictions in StreamBuilder _supportedHlsVideoCodecs»).
|
||
- Комментарий в коде: «As of 10.10 codebase, SupportsDirectPlay is always true because the MediaSourceInfo initializes this key as true». Решение direct play / direct stream / transcode: если `transcodeReasons == 0` и битрейт не превышает `MaxStreamingBitrate` → DirectPlay; иначе если `SupportsDirectStream` и причины ⊆ `DirectStreamReasons` (= аудио-причины | `ContainerNotSupported` | `VideoCodecTagNotSupported`) → DirectStream (remux/перекодирование только аудио); превышение битрейта → `ContainerBitrateExceedsLimit` → transcode. `UseMostCompatibleTranscodingProfile` (поле MediaSourceInfo, обычно для Live TV) заставляет брать «самый совместимый» профиль.
|
||
- Выбор доставки субтитров (`GetSubtitleProfile`): (1) если можно «embed» (не transcode, либо transcode не по HLS и контейнер поддерживает) — ищется `Embed`-профиль с тем же `Format` и совпадением текст/графика (`MediaStream.IsTextFormat`), затем `Embed` с конвертируемым текстовым форматом (`SupportsSubtitleConversionTo`; **из `ass` конвертировать нельзя**); (2) иначе `External`/`Hls`-профиль без конверсии, потом с конверсией (`Hls` принимается только при `PlayMethod.Transcode`); (3) иначе fallback `Method = Encode` (burn-in). Любой `SubtitleProfile.Method`, кроме `Drop/External/Embed`, добавляет `SubtitleCodecNotSupported` в причины — именно так выбор ASS/PGS с `Encode` принуждает транскод.
|
||
- `IsTextFormat` ([MediaStream.cs](https://github.com/jellyfin/jellyfin/blob/master/MediaBrowser.Model/Entities/MediaStream.cs)): графическими считаются `pgs*`, `dvdsub`, `vobsub`, `dvbsub`, `sup`, `sub` (кроме `microdvd`), всё остальное — текст.
|
||
|
||
### 1.3. Профили реальных клиентов (точные файлы)
|
||
|
||
**Jellyfin Android TV (ExoPlayer/Media3)**
|
||
- [app/src/main/java/org/jellyfin/androidtv/util/profile/deviceProfile.kt](https://github.com/jellyfin/jellyfin-androidtv/blob/master/app/src/main/java/org/jellyfin/androidtv/util/profile/deviceProfile.kt) — строится через DSL `buildDeviceProfile {}` из Kotlin SDK. Содержимое: `name = "AndroidTV-Default"`; `maxStaticBitrate = maxStreamingBitrate = maxBitrate` (из настроек, Mbit→bit, «0» запрещён); три TranscodingProfile: Video `TS`+`HLS` (`audioCodec` из `aac, ac3, eac3, mp3`), Video `MP4`+`HLS` (`aac, ac3, eac3, mp3, alac, flac, opus, dts`), оба `copyTimestamps = false`, `enableSubtitlesInManifest = true`, `videoCodec = [hevc?, h264]`; Audio `TS`+`HLS` AAC. DirectPlayProfile Video: контейнеры `ASF, HLS, M4V, MKV, MOV, MP4, OGM, OGV, TS, VOB, WEBM, WMV, XVID`, видео `AV1, H264, HEVC, MPEG, MPEG2VIDEO, VC1, VP8, VP9`, аудио — отфильтрованный список (passthrough-кодеки AC3/EAC3/DTS/TRUEHD включаются только если разрешены). CodecProfile'ы: H264 `VIDEO_PROFILE inCollection [high, main, baseline, constrained baseline, (high 10)]`, `VIDEO_LEVEL <= level` с `applyConditions` по профилю, `REF_FRAMES <= 12` при `WIDTH >= 1200` и `<= 4` при `WIDTH >= 1900`; HEVC main/main 10 + level; AV1/VC1 через трюк `VIDEO_PROFILE equals "none"` для полного исключения; `WIDTH/HEIGHT <=` максимального разрешения из MediaCodec; HDR-исключения через `VIDEO_RANGE_TYPE notEquals "A|B|C"` + `applyConditions inCollection` (в коде есть пояснение, что `notEquals` всегда проваливает ConditionProcessor, поэтому через `applyConditions` профиль активируется только для неподдерживаемых диапазонов); `CodecType.VIDEO_AUDIO`: `AUDIO_CHANNELS <= 2` при даунмиксе, иначе `8`. Субтитры: `VTT/WEBVTT` — `Embed+Hls+External` (комментарий: сервер в HLS умеет только WebVTT, остальные текстовые конвертировались бы в VTT — нежелательно); `SRT/SUBRIP/TTML` — `Embed+External`; `DVBSUB/DVDSUB/IDX` — `Embed+Encode`; `PGS/PGSSUB` — `Embed` только если `pgsDirectPlay`, иначе `Encode`; `ASS/SSA` — `Encode` + `Embed/External` только если `assDirectPlay` (через libass-расширение).
|
||
- [util/profile/codec/audioPassthrough.kt](https://github.com/jellyfin/jellyfin-androidtv/blob/master/app/src/main/java/org/jellyfin/androidtv/util/profile/codec/audioPassthrough.kt) — проверка `AudioCapabilities.isPassthroughPlaybackSupported` для AC3/EAC3/DTS/TrueHD; режимы `BitstreamAudioMode.AUTO/ENABLE/DISABLE` на каждый формат; `AudioBehavior.DIRECT_STREAM / DOWNMIX_TO_STEREO`.
|
||
- [util/profile/KnownDefects.kt](https://github.com/jellyfin/jellyfin-androidtv/blob/master/app/src/main/java/org/jellyfin/androidtv/util/profile/KnownDefects.kt) — список устройств с багами (Fire TV 4K: DoVi+HDR10+; Shield 2019: незаявленный DV Profile 7). Хороший пример «quirks»-таблицы.
|
||
- [constant/QualityProfiles.kt](https://github.com/jellyfin/jellyfin-androidtv/blob/master/app/src/main/java/org/jellyfin/androidtv/constant/QualityProfiles.kt) — ступени качества: `200,180,140,120,110,100,90,80,70,60,50,40,30,20,15,10,5,3,2,1 Mbit; 0.72, 0.42` (720/420 kbit).
|
||
- История libVLC: исторический профиль [app/src/main/java/org/jellyfin/androidtv/util/profile/LibVlcProfile.kt @ v0.16.11](https://github.com/jellyfin/jellyfin-androidtv/blob/v0.16.11/app/src/main/java/org/jellyfin/androidtv/util/profile/LibVlcProfile.kt) (`name = "AndroidTV-libVLC"`, `maxStreamingBitrate = 20_000_000`; TranscodingProfile `MKV`/H264/aac,mp3 с `copyTimestamps = true` (не HLS!); DirectPlay: почти все контейнеры (`M4V, 3GP, TS, MPEGTS, MOV, XVID, VOB, MKV, WMV, ASF, OGM, OGV, M2V, AVI, MPG, MPEG, MP4, WEBM, WTV`) и аудио (`AAC, MP3, MP2, AC3, EAC3, WMA, WMAV2, DCA, DTS, PCM, PCM_S16LE, PCM_S24LE, OPUS, FLAC, TRUEHD, AAC_LATM`); ContainerProfile AVI `VideoCodecTag NotEquals xvid`; субтитры все `Embed` + `SRT External`). Рядом — [ExoPlayerProfile.kt](https://github.com/jellyfin/jellyfin-androidtv/blob/v0.16.11/app/src/main/java/org/jellyfin/androidtv/util/profile/ExoPlayerProfile.kt) и [ProfileHelper.kt](https://github.com/jellyfin/jellyfin-androidtv/blob/v0.16.11/app/src/main/java/org/jellyfin/androidtv/util/profile/ProfileHelper.kt) той же версии.
|
||
- Удаление libVLC: [PR #3639 «Remove LibVLC»](https://github.com/jellyfin/jellyfin-androidtv/pull/3639) (nielsvanvelzen, merged 02.06.2024, milestone v0.17.0). Причины: последний стабильный libvlc-jni 3.5.1 (авг. 2022), непатченный CVE ([issue #3623 «CVE-2023-5217 - update libvlc»](https://github.com/jellyfin/jellyfin-androidtv/issues/3623), F-Droid), смешанный код двух плееров (утечки памяти, производительность), размер артефакта ~107 MB → 24 MB, новая playback-архитектура не планирует LibVLC-бэкенд. Последствия в форуме: [«Live TV no longer works on AndroidTV client without transcoding»](https://forum.jellyfin.org/t-live-tv-no-longer-works-on-androidtv-client-without-transcoding) (MPEG-2 с тюнеров ExoPlayer без софт-декодера не тянет), [«bring back libvlc»](https://forum.jellyfin.org/t-solved-bring-back-libvlc). Это прямой аргумент «за» ваш libVLC-клиент: софт-декод MPEG-2/VC-1/DTS-HD/TrueHD, ASS через libass, два трека субтитров, offset субтитров/аудио.
|
||
|
||
**Jellyfin Android (mobile, ExoPlayer + web)**
|
||
- [app/src/main/java/org/jellyfin/mobile/player/deviceprofile/DeviceProfileBuilder.kt](https://github.com/jellyfin/jellyfin-android/blob/master/app/src/main/java/org/jellyfin/mobile/player/deviceprofile/DeviceProfileBuilder.kt) — константы `MAX_STREAMING_BITRATE = 120000000`, `MAX_STATIC_BITRATE = 100000000`, `MAX_MUSIC_TRANSCODING_BITRATE = 384000`; контейнеры `mp4, fmp4, webm, mkv, mp3, ogg, wav, mpegts, flv, aac, flac, 3gp`, списки кодеков фильтруются по `MediaCodecList`, но `FORCED_AUDIO_CODECS` (PCM, alac, aac, ac3, eac3, dts, mlp, truehd) включаются всегда; TranscodingProfile `ts`/`h264`/`mp1,mp2,mp3,aac,ac3,eac3,dts,mlp,truehd`/`HLS` (+ `mkv` HLS, + audio `mp3` HTTP); CodecProfile `EQUALS_ANY VIDEO_PROFILE` из профилей устройства; субтитры `EXO_EMBEDDED_SUBTITLES = dvbsub, pgssub, srt, subrip, ttml`, `EXO_EXTERNAL_SUBTITLES = srt, subrip, ttml, vtt, webvtt`, `SUBTITLES_SSA = ssa, ass` добавляются при `exoPlayerDirectPlayAss`. **Профиль для внешнего плеера** `getExternalPlayerProfile()`: `DirectPlayProfile(type = VIDEO, container = "")` + AUDIO, пустые transcoding/codec-профили, все `EXTERNAL_PLAYER_SUBTITLES` как Embed+External, `maxStreamingBitrate = maxStaticBitrate = musicStreamingTranscodingBitrate = Int.MAX_VALUE` — фактически «играй всё».
|
||
- Интервал отчётов: [utils/Constants.kt](https://github.com/jellyfin/jellyfin-android/blob/master/app/src/main/java/org/jellyfin/mobile/utils/Constants.kt) `PLAYER_TIME_UPDATE_RATE = 10000L`, `CHAPTER_MARKING_UPDATE_DELAY = 1000L`, `SKIP_MEDIA_SEGMENT_UPDATE_DELAY = 1000L`; логика в [player/PlayerViewModel.kt](https://github.com/jellyfin/jellyfin-android/blob/master/app/src/main/java/org/jellyfin/mobile/player/PlayerViewModel.kt).
|
||
|
||
**Swiftfin (iOS/tvOS; VLCKit, MPV, AVPlayer)**
|
||
- Сборка: [Shared/Extensions/JellyfinAPI/DeviceProfile.swift](https://github.com/jellyfin/Swiftfin/blob/main/Shared/Extensions/JellyfinAPI/DeviceProfile.swift) — `DeviceProfile.build(for: videoPlayer, compatibilityMode:, maxBitrate:, maxResolution:)`; режимы `PlaybackCompatibility`: `.auto` (профили плеера), `.mostCompatible` (только `h264+aac` в `mp4`, [PlaybackCompatibility+Video.swift](https://github.com/jellyfin/Swiftfin/blob/main/Shared/Objects/PlaybackCompatibility/PlaybackCompatibility+Video.swift)), `.directPlay` (`forcedDirectPlayProfile = DirectPlayProfile(type: .video)` без ограничений = «всё direct play»), `.custom` (пользовательские профили, [PlaybackDeviceProfile.swift](https://github.com/jellyfin/Swiftfin/blob/main/Shared/Objects/PlaybackDeviceProfile.swift) → `CustomDeviceProfile{type, useAsTranscodingProfile, audio, video, container}`). `forceSubtitleBurnIn` → все `SubtitleFormat.allCases` с `.encode`. `maxBitrate` пишется сразу в `maxStaticBitrate`, `maxStreamingBitrate`, `musicStreamingTranscodingBitrate`.
|
||
- VLC-профиль: [Shared/Objects/VideoPlayerType/VideoPlayerType+VLC.swift](https://github.com/jellyfin/Swiftfin/blob/main/Shared/Objects/VideoPlayerType/VideoPlayerType+VLC.swift) — DirectPlay без ограничения контейнера; аудио `aac, ac3, alac, amr_nb, amr_wb, dts, eac3, flac, mp1, mp2, mp3, nellymoser, opus, pcm_alaw, pcm_bluray, pcm_dvd, pcm_mulaw, pcm_s16be, pcm_s16le, pcm_s24be, pcm_s24le, pcm_u8, speex, vorbis, wavpack, wmalossless, wmapro, wmav1, wmav2`; видео `av1 (если HW), dirac, dv, ffv1, flv1, h261, h263, h264, hevc, mjpeg, mpeg1video, mpeg2video, mpeg4, msmpeg4v1/v2/v3, prores, theora, vc1, vp8, vp9, wmv1, wmv2, wmv3`; TranscodingProfile `HLS`, container `mp4`, `isBreakOnNonKeyFrames: true`, `maxAudioChannels: "8"`, `minSegments: 2`, аудио `aac, ac3, alac, dts, eac3, flac, mp1, mp2, mp3, opus, vorbis`, видео `av1?, h263, h264, hevc, mjpeg, mpeg1video, mpeg2video, mpeg4, vp9`; отдельный профиль для `vc1`. Субтитры: `Embed` — `ass, cc_dec, dvbsub, dvdsub, libzvbi_teletextdec, mov_text, mpl2, pgssub, pjs, realtext, sami, ssa, subrip, subviewer, subviewer1, text, ttml, vplayer, vtt, xsub`; `External` — текстовые (`subrip` первым: «Unmatched text subtitles (ex: VTT) are converted to the first option (subrip)»); `Encode` — `dvbsub, dvdsub, pgssub, vtt, xsub`. CodecProfile'ы: `isAnamorphic != true`, `isInterlaced != true`, `videoRangeType equalsAny` (SDR/DOVIWithSDR всегда; HDR-типы при `hdrEnabled`), H264 `videoLevel <= 80`, HEVC `<= 175`.
|
||
- AVPlayer-профиль: [VideoPlayerType+AVPlayer.swift](https://github.com/jellyfin/Swiftfin/blob/main/Shared/Objects/VideoPlayerType/VideoPlayerType+AVPlayer.swift) — контейнеры `mp4/m4v`, `mov`, `3gp/3g2`, `avi(mjpeg)`; HEVC требует `videoCodecTag equalsAny hvc1, dvh1` (isRequired) и `videoFramerate <= 60`; субтитры: `Embed` только `cc_dec, ttml`, `Encode` графические, `Hls` для `vtt`; `enableSubtitlesInManifest: true`. Общие условия — [VideoPlayerType+Shared.swift](https://github.com/jellyfin/Swiftfin/blob/main/Shared/Objects/VideoPlayerType/VideoPlayerType+Shared.swift). Форматы — [SubtitleFormat.swift](https://github.com/jellyfin/Swiftfin/blob/main/Shared/Objects/MediaComponents/SubtitleFormat.swift) (`isText`, расширения: pgssub→`sup`, dvdsub→`sub`, mov_text→`tx3g`...).
|
||
- Битрейты: [PlaybackBitrate.swift](https://github.com/jellyfin/Swiftfin/blob/main/Shared/Objects/PlaybackBitrate/PlaybackBitrate.swift) (`auto=0, max=360 Mbps, 120, 80, 60, 40, 20, 15, 10, 8, 6, 4, 3, 1.5, 0.72, 0.42`, отдельный список для аудио), [PlaybackBitrateTestSize.swift](https://github.com/jellyfin/Swiftfin/blob/main/Shared/Objects/PlaybackBitrate/PlaybackBitrateTestSize.swift) (размер теста `1, 2.5, 5, 7.5, 10 MB`).
|
||
|
||
**Findroid (mpv, Android)** — [data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt](https://github.com/jarnedemeulemeester/findroid/blob/main/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt): `DeviceProfile(name = "Direct play all", maxStaticBitrate = 1_000_000_000, maxStreamingBitrate = 1_000_000_000, codecProfiles = emptyList(), containerProfiles = emptyList(), directPlayProfiles = emptyList(), transcodingProfiles = emptyList(), subtitleProfiles = [srt EXTERNAL, ass EXTERNAL])` — т.е. mpv играет всё, сервер транскод не предлагает (пустые transcodingProfiles). Stream URL — `videosApi.getVideoStreamUrl(itemId, static = true, mediaSourceId)`; отчёты всегда `PlayMethod.DIRECT_PLAY`; локальные правила «просмотрено»: `<10%` → сброс, `>90%` → played. В дереве репо есть модуль `app/tv` (TV-UI в разработке).
|
||
|
||
**Streamyfin (React Native; mpv + ExoPlayer на TV)** — каталог [utils/profiles/](https://github.com/streamyfin/streamyfin/tree/develop/utils/profiles): [native.ts](https://github.com/streamyfin/streamyfin/blob/develop/utils/profiles/native.ts) (режимы аудио `auto/stereo/5.1/passthrough` → `MaxAudioChannels` `"6"/"2"/"6"/"8"`; mpv-аудио `aac,mp3,flac,opus,vorbis,ac3,eac3,dts,truehd,wma`; ExoPlayer-профиль с `org.jellyfin.media3:media3-ffmpeg-decoder`; гейт `NotEquals VideoRangeType DOVI` блокирует только P5, если нет DV-декодера; AV1 только при HW), [subtitles.ts](https://github.com/streamyfin/streamyfin/blob/develop/utils/profiles/subtitles.ts) (графические `dvdsub, idx, pgs, pgssub, teletext, vobsub, xsub` → `Encode`; текстовые → `External`; **намеренно не объявляют `webvtt` для External**, иначе сервер перегоняет .vtt через свой VttWriter с нестандартным `Region:` и теряет реплики — их issue #1892), [getStreamUrl.ts](https://github.com/streamyfin/streamyfin/blob/develop/utils/jellyfin/media/getStreamUrl.ts) (если есть `TranscodingUrl` — используют его, при `subtitleStreamIndex === -1` заменяют `SubtitleMethod=Encode` → `SubtitleMethod=Hls`; иначе `/Videos/{itemId}/stream?static=true&container=...&mediaSourceId=...&playSessionId=...`; для `BaseItemKind.Program` делают PlaybackInfo по `ChannelId` c `autoOpenLiveStream: true`; для удалённых `IsRemote && Protocol == "Http"` играют `Path` напрямую).
|
||
|
||
**jellyfin-web (браузер; эталон)** — [src/scripts/browserDeviceProfile.js](https://github.com/jellyfin/jellyfin-web/blob/master/src/scripts/browserDeviceProfile.js): `MaxStreamingBitrate = getMaxBitrate()` (120000000), `MaxStaticBitrate: 100000000`, `MusicStreamingTranscodingBitrate: Math.min(bitrateSetting, 384000)`; HLS `Container: enableFmp4Hls ? 'mp4' : 'ts'`, `MinSegments: '2'` на iOS/macOS, `BreakOnNonKeyFrames`; аудио HTTP `aac, mp3, opus, wav`, `Context: 'Static'` для оффлайна; ContainerProfile Tizen `NumStreams LessThanEqual 32`; `AudioProfile NotEquals HE-AAC`; субтитры: `vtt External`, `ass/ssa External` (рендер JASSUB/libass-wasm), `pgssub External` + `vobsub External Container: 'mks'` при `subtitlerenderpgs` (клиентский рендер через `libbitsub`), настройка `subtitleburnin` = `all` / `allcomplexformats` / `onlyimageformats`. Логика выбора рендерера — [src/plugins/htmlVideoPlayer/plugin.js](https://github.com/jellyfin/jellyfin-web/blob/master/src/plugins/htmlVideoPlayer/plugin.js) (`NATIVE_UNSUPPORTED_SUBTITLE_CODECS = ['ssa', 'ass', 'pgssub', 'dvdsub', 'vobsub']`, `DeliveryMethod === 'Encode'` → индекс `-1`, `alwaysBurnInSubtitleWhenTranscoding`).
|
||
|
||
**jellyfin-mpv-shim** — профиль в [jellyfin_mpv_shim/utils.py](https://github.com/jellyfin/jellyfin-mpv-shim/blob/master/jellyfin_mpv_shim/utils.py): `"DirectPlayProfiles": [{"Type": "Video"}, {"Type": "Audio"}, {"Type": "Photo"}]` (всё), субтитры `srt/ass/sub/ssa/smi` External+Embed, `pgssub/dvdsub/dvbsub/pgs` только Embed (комментарий: «Jellyfin currently refuses to serve these subtitle types as external» — учтите, что в 12.2 графические субтитры стали отдаваться как raw-файлы с range-запросами); опциональные CodecProfile-переключатели: `transcode_hi10p` → `VideoBitDepth LessThanEqual 8`, `transcode_dolby_vision` → `VideoRangeType NotEquals DOVI`, `transcode_hdr` → `VideoRangeType Equals SDR`, `transcode_hevc`; `always_transcode` → `DirectPlayProfiles = []`. (`video_profile.py` там — это шейдерные профили mpv, не DeviceProfile.)
|
||
|
||
**jellyfin-kodi** — [jellyfin_kodi/helper/playutils.py](https://github.com/jellyfin/jellyfin-kodi/blob/master/jellyfin_kodi/helper/playutils.py) `get_device_profile()`: `"MusicStreamingTranscodingBitrate": 1280000`, `"TimelineOffsetSeconds": 5`, TranscodingProfile Video `"Container": "m3u8"` + `MaxAudioChannels` из настроек, DirectPlay `{"Type":"Video","VideoCodec":...}`, `{"Type":"Audio"}`, `{"Type":"Photo"}`; субтитры `srt/ass/sub/ssa/smi/pgssub/dvdsub/pgs` External+Embed; `transcodeHi10P` → `VideoBitDepth <= 8`; `transcode_h265_rext` → `VideoProfile EqualsAny main|main 10`; для `TvChannel` вставляется профиль `ts`/`h264`/`mp3,aac`/`hls`/`MaxAudioChannels "2"`/`MinSegments "1"`/`BreakOnNonKeyFrames`.
|
||
|
||
**Kotlin SDK DSL** (рекомендую использовать): [jellyfin-model/.../deviceprofile/DeviceProfileBuilder.kt](https://github.com/jellyfin/jellyfin-sdk-kotlin/blob/master/jellyfin-model/src/commonMain/kotlin/org/jellyfin/sdk/model/deviceprofile/DeviceProfileBuilder.kt), [DeviceProfileBuilderDsl.kt](https://github.com/jellyfin/jellyfin-sdk-kotlin/blob/master/jellyfin-model/src/commonMain/kotlin/org/jellyfin/sdk/model/deviceprofile/DeviceProfileBuilderDsl.kt) — `buildDeviceProfile { transcodingProfile {...}; directPlayProfile {...}; codecProfile { conditions {...}; applyConditions {...} }; subtitleProfile(format, method) }` с инфиксами `equals / notEquals / inCollection / lowerThanOrEquals / greaterThanOrEquals`.
|
||
|
||
### 1.4. «VLC играет всё» vs ExoPlayer — как выглядит профиль и подводные камни
|
||
|
||
Профиль libVLC-клиента (по образцу Swiftfin-VLC, Findroid, mpv-shim, старого `LibVlcProfile`):
|
||
- DirectPlay: `DirectPlayProfile(type=VIDEO, container=null/"" , videoCodec=null, audioCodec=null)` + такой же AUDIO — либо явные длинные списки (как Swiftfin), чтобы всё же исключить то, что устройство реально не тянет (AV1/HEVC 10-bit софтом на слабом SoC, 4K60 без HW).
|
||
- Ограничения через CodecProfile, а не через отсутствие кодека: `WIDTH/HEIGHT`, `VIDEO_BITRATE`, `VIDEO_RANGE_TYPE` (DV P5 без HW-декодера → транскод/тонмап; HDR на SDR-панели — решать по `Display.getHdrCapabilities()`), `VIDEO_BIT_DEPTH` (Hi10P H.264 только софтом), `IS_INTERLACED`, `VideoRotation` (новая опция 12.0).
|
||
- TranscodingProfile всё равно нужен: Video `HLS` + `mp4` (fMP4) с `h264,hevc,(av1)` и аудио `aac,ac3,eac3,mp3,alac,flac,opus,dts,truehd` — иначе при превышении `MaxStreamingBitrate`/пользовательского лимита сервер не сможет ничего предложить (`NoCompatibleStream`). `ts` даёт меньше аудио-кодеков (см. белые списки). Ставьте `BreakOnNonKeyFrames=true`, `MinSegments=2`, `MaxAudioChannels` по режиму (2/6/8), `EnableSubtitlesInManifest=true`, если хотите HLS-субтитры.
|
||
- Битрейт: `MaxStreamingBitrate` = выбранное пользователем качество (Auto → результат `/Playback/BitrateTest`; LAN → очень большое число); `MaxStaticBitrate` — для `static=true`/скачивания; `MusicStreamingTranscodingBitrate` — для `/Audio/{id}/universal` (web ограничивает 384 kbps). Сервер дополнительно режет по `UserPolicy.RemoteClientBitrateLimit` и `ServerConfiguration.RemoteClientBitrateLimit` для внешних адресов.
|
||
- Субтитры с libVLC: текст (`srt, subrip, ass, ssa, vtt, ttml, mov_text, microdvd, sami...`) — `Embed` + `External` (ASS внешним файлом VLC отрисует через libass, если доступны шрифты/attachments; при External сервер отдаёт исходный `.ass`, конверсия из ASS невозможна); графические (`pgssub, dvdsub, dvbsub, xsub`) — `Embed` (VLC декодирует сам) и `Encode` как fallback для транскода. При транскоде через HLS «embed» невозможен (`CanConsiderEmbedSubtitle`), поэтому для выбранной PGS/ASS при транскоде сервер выберет `Encode` (burn-in) — это ожидаемо и так делают все клиенты; для текстовых при транскоде лучше `Hls`/`External`.
|
||
- Аудио: при passthrough (`AudioCapabilities`/`AudioManager` сообщает поддержку AC3/EAC3/DTS/TrueHD через HDMI) — объявлять кодеки и `AUDIO_CHANNELS <= 8`; при даунмиксе — `<= 2` и исключать `truehd/dts/mlp` из direct play, чтобы сервер перегнал в AAC/EAC3 (так делает Android TV). TrueHD/Atmos и DTS-HD MA libVLC декодирует софтом в PCM (как mpv/ExoPlayer+ffmpeg), но биттстрим через Android AudioTrack для TrueHD практически нет — для Atmos нужен passthrough TrueHD/EAC3-JOC.
|
||
- `SupportsDirectStream=false` (например, некоторые Live TV/плагинные источники) — тогда только DirectPlay или полный транскод. `IsInfiniteStream`, `RequiresOpening/OpenToken/LiveStreamId`, `RequiresClosing` — Live TV.
|
||
- Удалите `SubtitleCodecNotSupported`-ловушку: если объявить формат только с `Encode`, выбор такой дорожки всегда вызовет транскод видео.
|
||
- Streamyfin-грабли с `webvtt`-алиасом (см. выше) и Swiftfin-комментарий о конверсии «в первый External-формат» — порядок SubtitleProfile'ов важен.
|
||
- `MaxAudioChannels` — строка; `Container` в DirectPlayProfile сравнивается с `MediaSourceInfo.Container` (у MKV — `mkv`, у TS — `ts`/`mpegts`), пустое = любое.
|
||
|
||
---
|
||
|
||
## 2. Поток воспроизведения (как у реальных клиентов)
|
||
|
||
1. **PlaybackInfo** — `POST /Items/{itemId}/PlaybackInfo` ([MediaInfoController.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/MediaInfoController.cs)), query-параметры помечены `ParameterObsolete` — всё передаётся телом `PlaybackInfoDto`: `UserId, MaxStreamingBitrate, StartTimeTicks, AudioStreamIndex, SubtitleStreamIndex, MaxAudioChannels, MediaSourceId, LiveStreamId, DeviceProfile, EnableDirectPlay, EnableDirectStream, EnableTranscoding, AllowVideoStreamCopy, AllowAudioStreamCopy, AutoOpenLiveStream, AlwaysBurnInSubtitleWhenTranscoding`. Ответ `PlaybackInfoResponse { MediaSources[], PlaySessionId, ErrorCode }`, `PlaybackErrorCode`: `NotAllowed`, `NoCompatibleStream`, `RateLimitExceeded` (это не API rate limit, а лимит `UserPolicy.MaxActiveSessions`/битрейт).
|
||
2. **MediaSourceInfo** ([файл](https://github.com/jellyfin/jellyfin/blob/master/MediaBrowser.Model/Dto/MediaSourceInfo.cs)) — ключевые поля: `Id, Protocol (File/Http/...), Container, Size, Bitrate, RunTimeTicks, SupportsDirectPlay, SupportsDirectStream, SupportsTranscoding, IsInfiniteStream, IsRemote, Path, TranscodingUrl, TranscodingSubProtocol (http/hls), TranscodingContainer, TranscodeReasons, DefaultAudioStreamIndex, DefaultSubtitleStreamIndex, DefaultAudioIndexSource, MediaStreams[], MediaAttachments[], RequiredHttpHeaders, RequiresOpening, OpenToken, LiveStreamId, RequiresClosing, BufferMs, HasSegments, UseMostCompatibleTranscodingProfile, FallbackMaxStreamingBitrate, VideoType, IsoType, Video3DFormat`.
|
||
3. **Выбор URL** (так делают web/Streamyfin/Android):
|
||
- есть `TranscodingUrl` → `baseUrl + TranscodingUrl` (это уже HLS `/Videos/{id}/master.m3u8?...` с `PlaySessionId`, `DeviceId`, `api_key`, `MediaSourceId`, `VideoCodec`, `AudioCodec`, `SubtitleMethod`, `TranscodeReasons=...`); при direct stream сервер тоже отдаёт `TranscodingUrl` (remux в HLS, `PlayMethod.DirectStream`);
|
||
- иначе direct play: `GET /Videos/{itemId}/stream?static=true&mediaSourceId=...&playSessionId=...&deviceId=...&api_key=...` или `/Videos/{itemId}/stream.{container}` ([VideosController.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/VideosController.cs)); у Kotlin SDK — `videosApi.getVideoStreamUrl(itemId, static = true, mediaSourceId = ...)`;
|
||
- HLS-эндпоинты ([DynamicHlsController.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/DynamicHlsController.cs)): `/Videos/{itemId}/master.m3u8`, `/main.m3u8`, `/live.m3u8`, сегменты `/Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}`; параметр `subtitleMethod` (`SubtitleDeliveryMethod`, по умолчанию `External`). Legacy `/Videos/{itemId}/hls/{playlistId}/stream.m3u8` в [HlsSegmentController.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/HlsSegmentController.cs). В 12.0 HLS-контроллеры скрыты из OpenAPI-спеки (эндпоинты работают).
|
||
- аудио: `GET /Audio/{itemId}/universal` ([UniversalAudioController.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/UniversalAudioController.cs)) — сервер сам решает direct/transcode по `Container`, `AudioCodec`, `MaxStreamingBitrate`, `TranscodingContainer`, `TranscodingProtocol`; `/Audio/{itemId}/master.m3u8`.
|
||
4. **Live TV**: `POST /LiveStreams/Open` (`openToken, playSessionId, maxStreamingBitrate, startTimeTicks, audioStreamIndex, subtitleStreamIndex, maxAudioChannels, itemId, enableDirectPlay, enableDirectStream, alwaysBurnInSubtitleWhenTranscoding` + тело с DeviceProfile) и `POST /LiveStreams/Close`; проще — `AutoOpenLiveStream=true` в PlaybackInfo.
|
||
5. **Отчёты** ([PlaystateController.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/PlaystateController.cs)): `POST /Sessions/Playing` (`PlaybackStartInfo`), `POST /Sessions/Playing/Progress` (`PlaybackProgressInfo`: `CanSeek, Item, ItemId, SessionId, MediaSourceId, AudioStreamIndex, SubtitleStreamIndex, IsPaused, IsMuted, PositionTicks, PlaybackStartTimeTicks, VolumeLevel, Brightness, AspectRatio, PlayMethod, LiveStreamId, PlaySessionId, RepeatMode, PlaybackOrder, NowPlayingQueue[], PlaylistItemId`), `POST /Sessions/Playing/Stopped` (`PlaybackStopInfo`: `... PositionTicks, LiveStreamId, PlaySessionId, Failed, NextMediaType, PlaylistItemId, NowPlayingQueue`), `POST /Sessions/Playing/Ping?playSessionId=` (держит транскод живым). Старые `POST/DELETE /PlayingItems/{itemId}` и `/Users/{userId}/PlayingItems/...` — deprecated (10.11). `PlayMethod`: `Transcode=0, DirectStream=1, DirectPlay=2`. Обязательно передавать `PlaySessionId` — по нему сервер связывает транскод и сессию.
|
||
- Интервалы у клиентов: jellyfin-web — `setInterval(..., 10000)` ([playbackmanager.js](https://github.com/jellyfin/jellyfin-web/blob/master/src/components/playback/playbackmanager.js)); jellyfin-android — 10 с; Android TV legacy `PlaybackController.java`: `PROGRESS_REPORTING_INTERVAL = 3 s`, `PROGRESS_REPORTING_PAUSE_INTERVAL = 15 s` ([файл](https://github.com/jellyfin/jellyfin-androidtv/blob/master/app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackController.java)); новая архитектура ATV — событийная [playback/jellyfin/.../playsession/PlaySessionService.kt](https://github.com/jellyfin/jellyfin-androidtv/blob/master/playback/jellyfin/src/main/kotlin/playsession/PlaySessionService.kt) (start на PLAYING, progress на PAUSED, stop на STOPPED/ERROR, `playMethod` из `MediaConversionMethod`, очередь `peekNext(15)` в `nowPlayingQueue`); Swiftfin — [MediaProgressObserver.swift](https://github.com/jellyfin/Swiftfin/blob/main/Shared/Objects/MediaPlayerManager/MediaProgressObserver.swift) с `PokeIntervalTimer` (точный интервал не извлечён — не проверено). Также после каждой паузы/сика/смены дорожки клиенты шлют progress немедленно.
|
||
6. **Остановка транскода**: `DELETE /Videos/ActiveEncodings?deviceId=...&playSessionId=...` (оба `Required`). Web вызывает `apiClient.stopActiveEncodings(playSessionId)` перед сменой потока и при стопе.
|
||
7. **Bitrate test**: `GET /Playback/BitrateTest?size=` (`Range(1, 100_000_000)`, default `102400` байт). Логика web — [src/utils/bitrateTest.ts](https://github.com/jellyfin/jellyfin-web/blob/master/src/utils/bitrateTest.ts): таймаут 5 с, кэш результата 1 ч, `LAN_BITRATE = 140000000`, учёт `navigator.connection.downlinkMax * 0.7`; замер от `HEADERS_RECEIVED` до `onload`. Swiftfin тестирует 1–10 MB. «Auto» = результат теста, «Max»/ручное значение → `MaxStreamingBitrate`; смена качества на лету = новый PlaybackInfo с `StartTimeTicks` + stop старого транскода (`changeStreamToUrl` в playbackmanager).
|
||
8. **Next Up / автоплей**: `GET /Shows/NextUp` ([TvShowsController.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/TvShowsController.cs)) с `userId, startIndex, limit, fields, seriesId, parentId, enableImages, imageTypeLimit, enableImageTypes, enableUserData, nextUpDateCutoff, enableTotalRecordCount, enableResumable (default true), enableRewatching (default false)`. Web на главной: `limit 24/15`, `fields: PrimaryImageAspectRatio, DateCreated, Path, MediaSourceCount`, `enableResumable: false`, `nextUpDateCutoff` из настройки ([sections/nextUp.ts](https://github.com/jellyfin/jellyfin-web/blob/master/src/components/homesections/sections/nextUp.ts)); после эпизода web вызывает `getNextUpEpisodes({ SeriesId, UserId })` и автоплеит, если `user.Configuration.EnableNextEpisodeAutoPlay` (или тип не видео). Android TV: `NextUpBehavior = EXTENDED / MINIMAL / DISABLED`, «Still watching?» `StillWatchingBehavior = SHORT / DEFAULT / LONG / VERY_LONG / DISABLED` ([файлы](https://github.com/jellyfin/jellyfin-androidtv/tree/master/app/src/main/java/org/jellyfin/androidtv/preference/constant)).
|
||
9. **Resume / «просмотрено»**: пороги на сервере — [ServerConfiguration.cs](https://github.com/jellyfin/jellyfin/blob/master/MediaBrowser.Model/Configuration/ServerConfiguration.cs): `MinResumePct = 5`, `MaxResumePct = 90`, `MinResumeDurationSeconds = 300`, `MinAudiobookResume = 5`, `MaxAudiobookResume = 5` — сервер сам при `Stopped` обнуляет позицию (<5 % или короче 5 мин) или помечает Played (>90 %); клиенту достаточно честно слать `PositionTicks`. `UserData.PlaybackPositionTicks > 0` → показывать диалог «Продолжить с …/Сначала» (Android TV ещё вычитает `resumeSubtractDuration`). Ручные отметки: `POST /UserPlayedItems/{itemId}?datePlayed=`, `DELETE /UserPlayedItems/{itemId}`; `GET /UserItems/Resume` (`mediaTypes, includeItemTypes, excludeActiveSessions, limit...`), `POST /UserItems/{itemId}/UserData`; избранное `POST/DELETE /UserFavoriteItems/{itemId}`.
|
||
10. **Media Segments (10.10+)**: `GET /MediaSegments/{itemId}?includeSegmentTypes=Intro,Outro,...` ([MediaSegmentsController.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/MediaSegmentsController.cs)), `MediaSegmentDto { Id, ItemId, Type, StartTicks, EndTicks }`, `MediaSegmentType`: `Unknown=0, Commercial=1, Preview=2, Recap=3, Outro=4, Intro=5`. Сигнал наличия — `MediaSourceInfo.HasSegments`. Документация: [Media segments](https://jellyfin.org/docs/general/server/metadata/media-segments) (нужен плагин-провайдер, задача `Media segment scan`; официальный [jellyfin-plugin-chapter-segments](https://github.com/jellyfin/jellyfin-plugin-chapter-segments) делает сегменты из названий глав). Реализации: web — [mediaSegmentManager.ts](https://github.com/jellyfin/jellyfin-web/blob/master/src/apps/legacy/features/playback/utils/mediaSegmentManager.ts) + [mediaSegmentAction.ts](https://github.com/jellyfin/jellyfin-web/blob/master/src/apps/legacy/features/playback/constants/mediaSegmentAction.ts) (`None / AskToSkip / Skip`; запрашивает только типы с действием; Skip игнорируется для сегментов <1 с и если пользователь сам отмотал назад внутрь сегмента; AskToSkip — <3 с; без `EndTicks` → `nextTrack()`), кнопка [skipsegment.ts](https://github.com/jellyfin/jellyfin-web/blob/master/src/components/playback/skipsegment.ts); Android TV — [MediaSegmentRepository.kt](https://github.com/jellyfin/jellyfin-androidtv/blob/master/app/src/main/java/org/jellyfin/androidtv/ui/playback/segment/MediaSegmentRepository.kt) (`MediaSegmentAction.NOTHING / SKIP / ASK_TO_SKIP`, `SkipMinDuration = 1s`, `AskToSkipMinDuration = 3s`, `AskToSkipAutoHideDuration = 8s`, поддерживаемые типы INTRO, OUTRO, PREVIEW, RECAP, COMMERCIAL, настройки хранятся строкой `Intro=SKIP,Outro=ASK_TO_SKIP`); Findroid — [FindroidSegment.kt](https://github.com/jarnedemeulemeester/findroid/blob/main/data/src/main/java/dev/jdtech/jellyfin/models/FindroidSegment.kt) + `PlayerViewModel` (кнопка исчезает за 100 мс до конца, автоскип с режимом «всегда/в PiP», кэш сегментов для оффлайна); Streamyfin — [utils/segments.ts](https://github.com/streamyfin/streamyfin/blob/develop/utils/segments.ts), [hooks/useMediaSegments.ts](https://github.com/streamyfin/streamyfin/blob/develop/hooks/useMediaSegments.ts) (приоритет перекрытий `Commercial > Recap > Intro > Preview > Outro`, задержка первого автоскипа 1,5 с из-за транскода, «титры до конца −5 с» → countdown следующего эпизода).
|
||
11. **Intro Skipper (legacy API)**: оригинальная документация — [ConfusedPolarBear/intro-skipper docs/api.md](https://github.com/ConfusedPolarBear/intro-skipper/blob/master/docs/api.md): `GET /Episode/{ItemId}/IntroTimestamps` (= `/v1`), ответ `{ EpisodeId, Valid, IntroStart, IntroEnd, ShowSkipPromptAt, HideSkipPromptAt }` (секунды), `404` если интро нет. Форки добавили `GET /Episode/{id}/Timestamps` с полями `Introduction / Credits / Recap / Preview / Commercial { Start, End, Valid }` (так его читает Streamyfin как fallback). Актуальный плагин — [intro-skipper/intro-skipper](https://github.com/intro-skipper/intro-skipper): требует **Jellyfin 12.0+ и jellyfin-ffmpeg 7.1.3-1+**, манифест `https://intro-skipper.org/manifest.json`, «As of Jellyfin 10.10, Intro Skipper does NOT modify the UI», публикует сегменты в нативный MediaSegments (wiki: «Jellyfin 12 clients use the native MediaSegments API», плагинная БД — источник истины, таблица MediaSegments — зеркало). Вывод: новому клиенту достаточно Media Segments API; legacy `/Episode/.../IntroTimestamps` — только для старых серверов 10.8/10.9.
|
||
12. **Trickplay**: `GET /Videos/{itemId}/Trickplay/{width}/{index}.jpg?mediaSourceId=` и `GET /Videos/{itemId}/Trickplay/{width}/tiles.m3u8` ([TrickplayController.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/TrickplayController.cs)); метаданные — `BaseItemDto.Trickplay: Dictionary<mediaSourceId, Dictionary<width, TrickplayInfoDto>>` (нужен `ItemFields.Trickplay`), `TrickplayInfoDto { Width, Height, TileWidth, TileHeight, ThumbnailCount, Interval (мс), Bandwidth }`. Дефолты сервера ([TrickplayOptions.cs](https://github.com/jellyfin/jellyfin/blob/master/MediaBrowser.Model/Configuration/TrickplayOptions.cs)): `Interval = 10000`, `WidthResolutions = [320]`, `TileWidth = TileHeight = 10`, `Qscale = 4`, `JpegQuality = 90`, `EnableKeyFrameOnlyExtraction`, `EnableHwAcceleration/EnableHwEncoding`. Расчёт листа/тайла: `tileIndex = floor(positionMs / Interval)`, лист = `tileIndex / (TileWidth*TileHeight)`, в листе — `x = (i % TileWidth) * Width`, `y = (i / TileWidth) * Height` (см. [Streamyfin utils/trickplay.ts](https://github.com/streamyfin/streamyfin/blob/develop/utils/trickplay.ts): ключ `Trickplay[mediaSourceId]`, для альтернативных версий считать по `ThumbnailCount`). Findroid кэширует тайлы в `filesDir/trickplay/{itemId}`.
|
||
13. **Главы**: `BaseItemDto.Chapters: ChapterInfo[] { StartPositionTicks, Name, ImagePath, ImageDateModified, ImageTag }` (нужен `ItemFields.Chapters`); картинка — `GET /Items/{itemId}/Images/Chapter/{imageIndex}?tag=`.
|
||
14. **Capabilities**: `POST /Sessions/Capabilities` (query) или `POST /Sessions/Capabilities/Full` с `ClientCapabilitiesDto { PlayableMediaTypes[], SupportedCommands[], SupportsMediaControl, SupportsPersistentIdentifier, DeviceProfile, AppStoreUrl, IconUrl }` — чтобы сессия управлялась другими клиентами (remote control, SyncPlay); Findroid делает `postCapabilities` при старте.
|
||
|
||
---
|
||
|
||
## 3. Субтитры и аудио
|
||
|
||
### Субтитры
|
||
- Форматы, которые сервер понимает: текстовые `srt/subrip, ass, ssa, vtt/webvtt, ttml, mov_text (tx3g), microdvd, sami/smi, mpl2, pjs, realtext, subviewer, text, vplayer, scc, cc_dec (EIA-608/708)`; графические `pgssub/pgs (sup), dvdsub/vobsub (idx+sub), dvbsub, xsub, teletext (libzvbi_teletextdec)`. Официальная таблица: [Codec Support](https://jellyfin.org/docs/general/clients/codec-support/) — ASS/SSA и PGS только в MKV (не в MP4/TS), SRT/WebVTT/VobSub/MP4TT/PGSSUB/EIA-608/708 перечислены. Отдельной страницы «subtitles» в документации нет (проверено по дереву репо jellyfin.org).
|
||
- Поля `MediaStream` ([MediaStream.cs](https://github.com/jellyfin/jellyfin/blob/master/MediaBrowser.Model/Entities/MediaStream.cs)): `Index, Type (Audio/Video/Subtitle/EmbeddedImage/Data/Lyric), Codec, CodecTag, Language, Title, DisplayTitle, IsDefault, IsForced, IsHearingImpaired, IsExternal, IsExternalUrl, IsTextSubtitleStream, SupportsExternalStream, DeliveryMethod, DeliveryUrl, Path, Channels, ChannelLayout, Profile, Level, BitDepth, RefFrames, IsInterlaced, IsAVC, VideoRangeType, DvProfile, DvLevel, LocalizedDefault/Forced/External/HearingImpaired`. После PlaybackInfo сервер проставляет `DeliveryMethod` и `DeliveryUrl` каждой субтитровой дорожке — используйте их, а не собирайте URL сами (mpv-shim советует для названий брать `DisplayTitle`).
|
||
- Внешний поток ([SubtitleController.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/SubtitleController.cs)): `GET /Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}` и `GET /Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks}/Stream.{format}` (query: `endPositionTicks, copyTimestamps=false, addVttTimeMap=false`; `format=js` → json); HLS-плейлист субтитров `GET /Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8?segmentLength=`; графические (pgs/vobsub) отдаются как сырой файл (12.2: с поддержкой range-запросов). Загрузка пользователем: `POST /Videos/{itemId}/Subtitles` (`UploadSubtitleDto`), удаление `DELETE /Videos/{itemId}/Subtitles/{index}`. Шрифты для ASS: `GET /FallbackFont/Fonts`, `GET /FallbackFont/Fonts/{name}` (сервер: `EncodingOptions.EnableFallbackFont`, `FallbackFontPath`); вложенные шрифты MKV — `MediaSourceInfo.MediaAttachments` + `GET /Videos/{videoId}/{mediaSourceId}/Attachments/{index}` (не проверялось в этом исследовании, но используется web).
|
||
- Поиск/скачивание (нужен плагин [jellyfin-plugin-opensubtitles](https://github.com/jellyfin/jellyfin-plugin-opensubtitles), документация [Open Subtitles](https://jellyfin.org/docs/general/server/plugins/open-subtitles/) — требуется аккаунт OpenSubtitles.com в настройках плагина): `GET /Items/{itemId}/RemoteSearch/Subtitles/{language}?isPerfectMatch=` → `RemoteSubtitleInfo[]`, `POST /Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}` (скачать на сервер), `GET /Providers/Subtitles/Subtitles/{subtitleId}`. Разрешение пользователя: `EnableSubtitleManagement` (`PermissionKind`). Библиотечные опции (`LibraryOptions`): `SubtitleFetcherOrder, SubtitleDownloadLanguages, SkipSubtitlesIfEmbeddedSubtitlesPresent, SkipSubtitlesIfAudioTrackMatches, RequirePerfectSubtitleMatch, SaveSubtitlesWithMedia, AllowEmbeddedSubtitles`.
|
||
- Предпочтения пользователя ([UserConfiguration.cs](https://github.com/jellyfin/jellyfin/blob/master/MediaBrowser.Model/Configuration/UserConfiguration.cs)): `AudioLanguagePreference`, `PlayDefaultAudioTrack`, `SubtitleLanguagePreference`, `SubtitleMode` (`SubtitlePlaybackMode`: `Default=0, Always=1, OnlyForced=2, None=3, Smart=4`), `RememberAudioSelections`, `RememberSubtitleSelections`, `EnableNextEpisodeAutoPlay`, `DisplayMissingEpisodes`, `HidePlayedInLatest`, `LatestItemsExcludes`, `MyMediaExcludes`, `OrderedViews`, `GroupedFolders`, `CastReceiverId`. Сервер уже применяет их, выставляя `DefaultAudioStreamIndex`/`DefaultSubtitleStreamIndex` в MediaSourceInfo (`Smart` = субтитры на предпочитаемом языке, только если аудио не на нём). `PlaybackInfoDto.AlwaysBurnInSubtitleWhenTranscoding` — соответствующая пользовательская настройка web.
|
||
- Рендер на клиенте: web — ASS через `@jellyfin/libass-wasm` (SubtitlesOctopus, `libassMemoryLimit: 40`), PGS/VobSub через `libbitsub.PgsRenderer` (стриминг, range-запросы, `prefetchWindow`), настройка `subtitlerenderpgs`. Для libVLC всё это внутри плеера; внешнее ASS подключайте через `Media.addSlave(Type.Subtitle, url)`.
|
||
|
||
### Аудио
|
||
- Passthrough vs транскод: ATV — объявляет AC3/EAC3/DTS/TrueHD только при подтверждённом passthrough (`AudioCapabilities.getCapabilities(...).isPassthroughPlaybackSupported`), иначе сервер перегонит в AAC/AC3 внутри HLS; при `DOWNMIX_TO_STEREO` direct play оставляют только `AAC, MP2, MP3` и `AUDIO_CHANNELS <= 2`. Streamyfin: TrueHD/DTS-HD декодирует mpv/ffmpeg в PCM, режим аудио меняет только `MaxAudioChannels`. HLS `ts` не пропустит DTS/TrueHD/FLAC — только `mp4`. Сервер-сайд даунмикс: [Stereo Downmix](https://jellyfin.org/docs/general/post-install/transcoding/downmix/) — алгоритмы `None, Dave750, NightmodeDialogue, RFC7845, AC-4` (`EncodingOptions.DownMixStereoAlgorithm`), `DownMixAudioBoost` (0.5–3, default 2).
|
||
- Нормализация громкости (10.9+): `BaseItemDto.NormalizationGain` и `AlbumNormalizationGain` (float, dB); рассчитывается задачей [AudioNormalizationTask.cs](https://github.com/jellyfin/jellyfin/blob/master/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs) через `ffmpeg -af ebur128=framelog=verbose` (парсит `I: ... LUFS`) для библиотек с `LibraryOptions.EnableLUFSScan`; Android TV применяет gain с целевой громкостью **-23 LUFS** ([normalizationGainElement.kt](https://github.com/jellyfin/jellyfin-androidtv/blob/master/playback/core/src/main/kotlin/mediastream/normalizationGainElement.kt)); Finamp тоже применяет; по словам автора Symfonium, album gain полноценно появится позже ([обсуждение](https://support.symfonium.app/t/jellyfin-replay-normalization-gain/14084)). В libVLC — `MediaPlayer.setVolume` не годится для dB; используйте аудиофильтр `gain` или `--gain`/`audio-filter=gain` при создании LibVLC (не проверено на практике).
|
||
- Live TV & DVR ([LiveTvController.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/LiveTvController.cs), документация [Live Broadcast](https://jellyfin.org/docs/general/server/live-tv/) — тюнеры HDHomeRun и M3U, гиды Schedules Direct/XMLTV): `GET /LiveTv/Info`, `/LiveTv/Channels`, `/LiveTv/Channels/{channelId}`, `/LiveTv/Programs` (+`POST /LiveTv/Programs` для сложных фильтров), `/LiveTv/Programs/Recommended`, `/LiveTv/GuideInfo`, `/LiveTv/Recordings`, `/LiveTv/Recordings/{recordingId}`, `/LiveTv/Recordings/Folders`, `/LiveTv/Timers`, `/LiveTv/Timers/Defaults?programId=`, `POST /LiveTv/Timers`, `/LiveTv/SeriesTimers`, `GET /LiveTv/LiveRecordings/{recordingId}/stream`, `GET /LiveTv/LiveStreamFiles/{streamId}/stream.{container}`. Воспроизведение канала — обычный PlaybackInfo по `LiveTvChannel` c `AutoOpenLiveStream`; тип элемента `Program` → играть `ChannelId`. Разрешения `EnableLiveTvAccess`, `EnableLiveTvManagement`.
|
||
- Музыка: лирика `GET /Audio/{itemId}/Lyrics` → `LyricDto { Metadata, Lyrics: LyricLine[] (Text, Start ticks, с 10.11 — word cues) }`, `POST /Audio/{itemId}/Lyrics`, `DELETE`, `GET /Audio/{itemId}/RemoteSearch/Lyrics`, `POST /Audio/{itemId}/RemoteSearch/Lyrics/{lyricId}`, `GET /Providers/Lyrics/{lyricId}`; флаг `BaseItemDto.HasLyrics`; файлы `.lrc/.elrc/.txt` рядом с треком ([Music docs](https://jellyfin.org/docs/general/server/media/music/)). Instant Mix ([InstantMixController.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/InstantMixController.cs)): `GET /Items/{itemId}/InstantMix`, `/Songs/{itemId}/InstantMix`, `/Albums/{itemId}/InstantMix`, `/Artists/{itemId}/InstantMix`, `/MusicGenres/{name}/InstantMix`, `/Playlists/{itemId}/InstantMix`. Gapless: серверного API нет — это задача плеера (предзагрузка следующего `Media` в libVLC через `MediaList`/второй `MediaPlayer`); при транскоде через `/Audio/{id}/universal` gapless не гарантирован (не проверено).
|
||
|
||
---
|
||
|
||
## 4. Изображения и метаданные
|
||
|
||
- `ImageType` ([ImageType.cs](https://github.com/jellyfin/jellyfin/blob/master/MediaBrowser.Model/Entities/ImageType.cs)): `Primary=0, Art=1, Backdrop=2, Banner=3, Logo=4, Thumb=5, Disc=6, Box=7, Screenshot=8, Menu=9, Chapter=10, BoxRear=11, Profile=12`.
|
||
- Эндпоинты ([ImageController.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/ImageController.cs)): `GET /Items/{itemId}/Images/{imageType}` и `/{imageIndex}` с query `maxWidth, maxHeight, width, height, quality, fillWidth, fillHeight, tag, format (ImageFormat: Bmp/Gif/Jpg/Png/Webp/Svg), percentPlayed, unplayedCount, blur, backgroundColor, foregroundLayer, imageIndex`; `GET /Items/{itemId}/Images` (список `ImageInfo`); `GET /Persons/{name}/Images/{imageType}`, `/Artists/{name}/Images/...`, `/Genres/{name}/...`, `/Studios/{name}/...`, `/MusicGenres/{name}/...`; аватар `GET /UserImage?userId=`/`/Users/{userId}/Images/Primary`; `GET /Branding/Splashscreen`. Семантика: `maxWidth/maxHeight` — вписать с сохранением пропорций; `width/height` — точный размер; `fillWidth/fillHeight` — заполнение с кропом (web использует `fillWidth/fillHeight` для постеров, `maxWidth` для бэкдропов); `quality` — JPEG/WebP качество; `tag` — кэш-бастинг (из `ImageTags[type]`/`BackdropImageTags[i]`); `blur` — размытие на сервере; `percentPlayed`/`unplayedCount` — наложение индикаторов сервером (web предпочитает рисовать сам).
|
||
- Поля `BaseItemDto` ([BaseItemDto.cs](https://github.com/jellyfin/jellyfin/blob/master/MediaBrowser.Model/Dto/BaseItemDto.cs)): `ImageTags: Dictionary<ImageType,string>`, `BackdropImageTags[]`, `ScreenshotImageTags[]`, `ImageBlurHashes: Dictionary<ImageType, Dictionary<tag, blurhash>>`, `PrimaryImageAspectRatio`, `ParentBackdropItemId` + `ParentBackdropImageTags[]`, `ParentLogoItemId/ParentLogoImageTag`, `ParentThumbItemId/ParentThumbImageTag`, `ParentArtItemId/ParentArtImageTag`, `ParentPrimaryImageItemId/ParentPrimaryImageTag`, `SeriesPrimaryImageTag`, `SeriesThumbImageTag`, `AlbumPrimaryImageTag`, `ChannelPrimaryImageTag`, `SeriesId/SeriesName/SeasonId/SeasonName`, `ParentId`, `ParentIndexNumber/IndexNumber`, `RunTimeTicks`, `CumulativeRunTimeTicks`, `PartCount`, `LocalTrailerCount`, `HasSubtitles`, `HasLyrics`, `Status`, `AirsAfter/BeforeSeasonNumber`, `PremiereDate`, `Chapters`, `Trickplay`, `MediaSources`, `MediaStreams`, `CollectionType`, `NormalizationGain`, `UserData`. Правило для эпизода: `Primary` — кадр эпизода, бэкдроп/лого обычно у сериала → брать через `ParentBackdropItemId` + `ParentBackdropImageTags`, постер сезона/сериала — `SeriesPrimaryImageTag` (itemId = `SeriesId`). Сериал без собственного `Thumb` → `ParentThumbItemId`.
|
||
- `UserItemDataDto`: `Rating, PlayedPercentage, UnplayedItemCount, PlaybackPositionTicks, PlayCount, IsFavorite, Likes, LastPlayedDate, Played, Key, ItemId`.
|
||
- `ItemFields` ([ItemFields.cs](https://github.com/jellyfin/jellyfin/blob/master/MediaBrowser.Model/Querying/ItemFields.cs)): `AirTime, CanDelete, CanDownload, ChannelInfo, Chapters, Trickplay, ChildCount, CumulativeRunTimeTicks, CustomRating, DateCreated, DateLastMediaAdded, DisplayPreferencesId, Etag, ExternalUrls, Genres, ItemCounts, MediaSourceCount, MediaSources, OriginalTitle, Overview, ParentId, Path, People, PlayAccess, ProductionLocations, ProviderIds, PrimaryImageAspectRatio, RecursiveItemCount, Settings, SeriesStudio, SortName, SpecialEpisodeNumbers, Studios, Taglines, Tags, RemoteTrailers, MediaStreams, SeasonUserData, DateLastRefreshed, DateLastSaved, RefreshState, ChannelImage, EnableMediaSourceDisplay, Width, Height, ExtraIds, LocalTrailerCount, IsHD, SpecialFeatureCount`. Практика web: для лент — `PrimaryImageAspectRatio` (+ `DateCreated, Path, MediaSourceCount`) с `imageTypeLimit: 1` и `enableImageTypes`; для деталей — `Overview, Genres, People, Studios, Chapters, MediaSources, MediaStreams, Trickplay, Tags, ExternalUrls, ProviderIds, RemoteTrailers, Taglines, DateCreated, CanDownload, CanDelete, CustomRating`.
|
||
- `BaseItemKind` ([файл](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Data/Enums/BaseItemKind.cs)): `AggregateFolder, Audio, AudioBook, BasePluginFolder, Book, BoxSet, Channel, ChannelFolderItem, CollectionFolder, Episode, Folder, Genre, ManualPlaylistsFolder, Movie, LiveTvChannel, LiveTvProgram, MusicAlbum, MusicArtist, MusicGenre, MusicVideo, Person, Photo, PhotoAlbum, Playlist, PlaylistsFolder, Program, Recording, Season, Series, Studio, Trailer, TvChannel, TvProgram, UserRootFolder, UserView, Video, Year`.
|
||
- `CollectionType` ([файл](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Data/Enums/CollectionType.cs), сериализуется в lowercase): `unknown, movies, tvshows, music, musicvideos, trailers, homevideos, boxsets, books, photos, livetv, playlists, folders` + внутренние виды `tvshowseries, tvgenres, tvlatest, tvnextup, tvresume, tvfavoriteseries, tvfavoriteepisodes, movielatest, movieresume, moviemovies, moviecollection, moviefavorites, moviegenres...`. Библиотеки — `GET /UserViews?userId=` (элементы `CollectionFolder` с `CollectionType`), `GET /Items/Latest?parentId=&limit=&includeItemTypes=&isPlayed=&groupItems=true`, `GET /Items?parentId=&recursive=&includeItemTypes=&sortBy=&fields=...`, `GET /Shows/{seriesId}/Seasons`, `GET /Shows/{seriesId}/Episodes?seasonId=`, `GET /Shows/Upcoming`, `GET /Items/{itemId}/Intros`, `/LocalTrailers`, `/SpecialFeatures`. Персоны — тип `Person`, картинка `Persons/{name}/Images/Primary?tag=` (в `People[]` есть `PrimaryImageTag`, `Id`, `Role`, `Type`).
|
||
|
||
---
|
||
|
||
## 5. UX-референсы
|
||
|
||
- [jellyfin/jellyfin-ux](https://github.com/jellyfin/jellyfin-ux) — брендинг/логотипы/иконки (CC-BY-SA-4.0), папки `branding/`, `logos/`, `plugins/`; Figma не публикуется. Правила — [Branding guidelines](https://jellyfin.org/docs/general/contributing/branding): имя «Jellyfin» и шаблоны «Jelly[word]»/«[word]fin» не использовать; разрешено «X, a Jellyfin client»; логотип (треугольник в треугольнике) без разрешения не брать, градиент — можно. Дизайн-обсуждения — Matrix `#jellyfin-ui-ux`.
|
||
- Структура главной в официальном web — [src/components/homesections/homesections.js](https://github.com/jellyfin/jellyfin-web/blob/master/src/components/homesections/homesections.js) и [constants/homeSectionType.ts](https://github.com/jellyfin/jellyfin-web/blob/master/src/constants/homeSectionType.ts): `SmallLibraryTiles` («My Media»), `LibraryButtons`, `Resume` («Continue Watching», `GET /UserItems/Resume` с `mediaTypes: [Video]`, limit 12/5, `fields: PrimaryImageAspectRatio`), `ResumeAudio`, `ResumeBook`, `NextUp`, `LatestMedia` (по одной секции на библиотеку, `/Items/Latest?parentId=`; limit 16, tvshows 5, music 9/30, иначе 8; учитывает `LatestItemsExcludes`/`HidePlayedInLatest`), `LiveTv`, `ActiveRecordings`. Порядок секций — из DisplayPreferences пользователя (`GET /DisplayPreferences/usersettings?client=emby`, ключи `homesection0..6`).
|
||
- Android TV: [Design for TV](https://developer.android.com/design/ui/tv) (Foundations/Style/Layout/Components; компоненты Buttons, Cards, Featured carousel, Immersive list, Lists, Navigation drawer, Tabs; ссылка на Android TV Figma kit — сам Figma-файл отдаёт 403 анонимно, не проверено), [Compose for TV](https://developer.android.com/training/tv/playback/compose) (`androidx.tv:tv-material:1.0.0`, свой `MaterialTheme`; не смешивать с mobile Material3; сэмплы [android/tv-samples](https://github.com/android/tv-samples) — JetStreamCompose, TvMaterialCatalog), релизы [androidx.tv](https://developer.android.com/jetpack/androidx/releases/tv), [TV app quality](https://developer.android.com/docs/quality-guidelines/tv-app-quality) (TV-DP d-pad, TV-PC/TV-PP play-pause, TV-OV overscan, TV-LB баннер 320×180, TV-ML `LEANBACK_LAUNCHER`, TV-NP media session, TV-BU ambient mode, TV-AX captions/high contrast, TV-AO выбор аудиоканалов, TV-FR frame-rate matching), [TV accessibility](https://developer.android.com/training/tv/accessibility) (`fontScale`, `isAudioDescriptionRequested`, `getKeyCodeForKeyLocation`, TalkBack, системные стили субтитров через `CaptioningManager`).
|
||
- Сравнения клиентов: независимых обзоров мало; материалы [jellywatch.app](https://jellywatch.app/blog/best-jellyfin-client-android-tv-2026) принадлежат конкурирующему клиенту — предвзяты (не проверено по сути). Факты: Findroid и Streamyfin — mobile-first (у Streamyfin есть TV-режим на ExoPlayer, у Findroid — модуль `app/tv`); официальный ATV — Leanback → Compose-миграция; из форума видно спрос на функции, потерянные с libVLC (двойные субтитры, offset аудио/субтитров, MPEG-2 Live TV без транскода).
|
||
|
||
---
|
||
|
||
## 6. Сообщество и экосистема
|
||
|
||
- Контакты — [jellyfin.org/contact](https://jellyfin.org/contact): Matrix Space `#jellyfinorg:matrix.org` (комнаты `#jellyfin`, `#jellyfin-announce`, `#jellyfin-troubleshooting`, dev: `#jellyfin-dev`, `#jellyfin-dev-client`, `#jellyfin-dev-android`, `#jellyfin-dev-ios`, `#jellyfin-dev-roku`, `#jellyfin-dev-python`, `#jellyfin-documentation`, `#jellyfin-ui-ux`, `#jellyfin-hwa-tool`), Discord `https://discord.gg/zHBxVSXdBV` (мост в Matrix), Mastodon `@jellyfin@mastodon.online`.
|
||
- Форум — [forum.jellyfin.org](https://forum.jellyfin.org/): [Client Development](https://forum.jellyfin.org/f-client-development) (подфорумы Android, Android TV, Roku...), [Server Development](https://forum.jellyfin.org/f-server-development), [Plugin Development](https://forum.jellyfin.org/f-plugin-development), [Feature Requests](https://forum.jellyfin.org/f-feature-requests), Project Announcements. Старый трекер идей features.jellyfin.org (Fider) отвечает, но контент не читался (не проверено); актуальные предложения — форум и [GitHub Discussions](https://github.com/jellyfin/jellyfin/discussions) (категории Announcements, Feature Proposals, General, Polls, Q&A, Show and tell). r/jellyfin существует, но недоступен для проверки из этой среды (не проверено).
|
||
- Листинг стороннего клиента — [Clients docs](https://jellyfin.org/docs/general/clients/): PR в [src/data/clients.ts](https://github.com/jellyfin/jellyfin.org/blob/master/src/data/clients.ts) (`Client { id, name, description, smallDescription?, clientType: Official|OfficialBeta|ThirdParty, deviceTypes: Desktop|Mobile|TV, platforms, primaryLinks, secondaryLinks?, recommended? }`). Требования: Jellyfin как «first-class citizen» с настраиваемым URL; соблюдение [Community Standards](https://jellyfin.org/docs/general/community-standards/) и брендинга; FOSS-лицензия из списка FSF; ≥1 года активной разработки или заметный интерес; хотя бы одна уникальная функция в публичном релизе. Сейчас на [downloads/clients/all](https://jellyfin.org/downloads/clients/all) из сторонних Android — Findroid, Finamp, Streamyfin; **сторонних Android TV-клиентов в списке нет** — ниша для вашего проекта.
|
||
- Ограничений API для сторонних клиентов нет: клиенты объявляют себя через заголовок `Authorization: MediaBrowser Client="...", Device="...", DeviceId="...", Version="...", Token="..."`; rate limiting на уровне API сервер не вводит (есть только `UserPolicy.MaxActiveSessions`, `LoginAttemptsBeforeLockout`, битрейт-лимиты). Политика: [Contributing / development](https://jellyfin.org/docs/general/contributing/development) и `docs/general/contributing/llm-policies.md` в репо jellyfin.org (политика по LLM-коду для PR в официальные репо).
|
||
- Плагины, важные клиенту: [Intro Skipper](https://github.com/intro-skipper/intro-skipper) (сегменты), [Chapter Segments Provider](https://github.com/jellyfin/jellyfin-plugin-chapter-segments), [OpenSubtitles](https://github.com/jellyfin/jellyfin-plugin-opensubtitles), [Playback Reporting](https://github.com/jellyfin/jellyfin-plugin-playbackreporting) (статистика, клиентского API не заявляет; ваши отчёты Progress/Stopped — его источник данных), Trickplay — встроен с 10.9 (плагин больше не нужен).
|
||
- Серверные настройки, влияющие на клиент: `EncodingOptions` ([файл](https://github.com/jellyfin/jellyfin/blob/master/MediaBrowser.Model/Configuration/EncodingOptions.cs)) — `HardwareAccelerationType`, `EnableHardwareEncoding`, `AllowHevcEncoding`, `AllowAv1Encoding` (без них транскод всегда в H.264 — объявляйте h264 в TranscodingProfile первым/обязательно), `EnableTonemapping`/`EnableVppTonemapping`/`EnableVideoToolboxTonemapping`, `EnableThrottling`/`ThrottleDelaySeconds`, `EnableSegmentDeletion`/`SegmentKeepSeconds` (нельзя далеко сикать назад в уже удалённые сегменты — делайте новый PlaybackInfo со `StartTimeTicks`), `EnableSubtitleExtraction`, `EnableFallbackFont`, `DownMixStereoAlgorithm`, `HlsAudioSeekStrategy`, `EnableAudioVbr`. Документация: [Transcoding](https://jellyfin.org/docs/general/post-install/transcoding/) («Direct Play / Remux / Direct Stream / Transcode», тонмаппинг HDR→SDR, разрешение нельзя задать вручную), [Hardware Acceleration](https://jellyfin.org/docs/general/post-install/transcoding/hardware-acceleration/).
|
||
- Права пользователя (`UserPolicy` / `PermissionKind`): `EnableMediaPlayback`, `EnableAudioPlaybackTranscoding`, `EnableVideoPlaybackTranscoding`, `EnablePlaybackRemuxing` («Allow video playback that requires conversion without re-encoding»), `ForceRemoteSourceTranscoding`, `EnableRemoteAccess`, `RemoteClientBitrateLimit` («Internet streaming bitrate limit (Mbps)»), `EnableContentDownloading`, `EnableSyncTranscoding`, `EnableMediaConversion`, `EnableLiveTvAccess/Management`, `EnableAllFolders/EnabledFolders`, `EnableAllDevices/EnabledDevices`, `EnableAllChannels`, `EnableSharedDeviceControl`, `EnableRemoteControlOfOtherUsers`, `EnablePublicSharing`, `EnableCollectionManagement`, `EnableSubtitleManagement`, `EnableLyricManagement`, `SyncPlayAccess`, `MaxActiveSessions`, `IsHidden`, `IsDisabled`, родительский контроль `MaxParentalRating`, `MaxParentalSubRating`, `BlockUnratedItems[]`, `BlockedTags[]`, `AllowedTags[]`, `AccessSchedules[]`. Документация: [Adding & Managing Users](https://jellyfin.org/docs/general/server/users/adding-managing-users). При отказе сервер вернёт `PlaybackErrorCode.NotAllowed` или 403 — показывайте понятное сообщение.
|
||
- Quick Connect ([docs](https://jellyfin.org/docs/general/server/quick-connect), [QuickConnectController.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/QuickConnectController.cs)): `GET /QuickConnect/Enabled`, `POST /QuickConnect/Initiate` → `{ Secret, Code(6 символов), Authenticated, DateAdded }`, опрос `GET /QuickConnect/Connect?secret=`, затем `POST /Users/AuthenticateWithQuickConnect` с `Secret`; авторизация с другого устройства — `POST /QuickConnect/Authorize?code=&userId=`. Включается в Dashboard → General («Enable Quick Connect on this server», по умолчанию включено). `GET /QuickConnect/Initiate` удалён в 12.0.
|
||
- Плагины/клиенты для сравнения архитектуры: [jellyfin-media-player (Jellyfin Desktop)](https://github.com/jellyfin/jellyfin-media-player) — Qt WebEngine + libmpv (passthrough, HW-декод, `mpv.conf`), [Finamp](https://github.com/jmshrv/finamp) (музыка, применяет NormalizationGain).
|
||
|
||
---
|
||
|
||
## 7. Тестовые медиа
|
||
|
||
- [repo.jellyfin.org/test-videos](http://repo.jellyfin.org/test-videos/) — **официальный Jellyfin test suite** (CC BY-SA, автор gnattu): `SDR/{AVC, AV1, HEVC 8bit, HEVC 10bit}`, `HDR/HDR10/{AV1, HEVC}`, `HDR/Dolby Vision/` (P5, P8.1, P8.4); имена вида `Test Jellyfin 1080p AV1 10bit 3M.mp4`, `Test Jellyfin 4K DV P8.1.mp4`; 1080p/4K/8K, 3–150 Mbps, с `.md5/.sha1/.sha256`. HLG нет, аудио не описано.
|
||
- [repo.jellyfin.org/archive/jellyfish/](https://repo.jellyfin.org/archive/jellyfish/) — зеркало классических Jellyfish bitrate-тестов (помечено DEPRECATED): 56 MKV `jellyfish-{3..400}-mbps-{hd|4k-uhd}-{h264|hevc|hevc-10bit}.mkv`; оригинальный jell.yfish.us не работает.
|
||
- Blender open movies (CC-BY): Big Buck Bunny — [download.blender.org/peach/bigbuckbunny_movies/](https://download.blender.org/peach/bigbuckbunny_movies/) (MP4 320×180, M4V 640×360, MOV H.264 480/720/1080p, AVI stereo/surround 480/720/1080p, OGG Theora 480/720/1080p — удобно для MPEG-4 ASP/AC3 5.1/Theora+Vorbis); Sintel — [download.blender.org/durian/movies/](https://download.blender.org/durian/movies/) (`Sintel.2010.1080p.mkv`, 720p/4K MKV, `sintel_4k.mov`, Blu-ray ISO, FLAC m+e); Tears of Steel — [download.blender.org/demo/movies/ToS/](https://download.blender.org/demo/movies/ToS/) (4K MOV, 1080p MOV/WebM, 720p MOV, `tears_of_steel_surround.webm`, `Surround-TOS_DVDSURROUND-Dolby 5.1.ac3`, `subtitles/`), сайт [mango.blender.org/download](https://mango.blender.org/download/).
|
||
- [ietf-wg-cellar/matroska-test-files](https://github.com/ietf-wg-cellar/matroska-test-files) — `test_files/test1..8.mkv`: MPEG-4 Part 2 + MP3; нестандартный TimecodeScale/AR; header stripping (test3 — проверка демуксера); «live»-запись без Duration и Cues; **test5 — несколько аудио (AAC + HE-AAC commentary) и 7 субтитровых дорожек** (EN/FR/DE/HU/ES/IT/JA) — идеален для UI выбора дорожек; test6 без Cues (сик по кластерам); test7 с мусорными элементами; test8 с пропуском аудио.
|
||
- [test-videos.co.uk](https://test-videos.co.uk/) — отвечает 200, набор H.264/H.265/VP9/AV1 в MP4/MKV/WebM разных битрейтов (содержимое страницы не проверено).
|
||
- Kodi wiki [Samples](https://kodi.wiki/view/Samples) / [Test files](https://kodi.wiki/view/Test_files) — крупнейший каталог (Dolby Vision P5/P7/P8/P10, HDR10+, LG/Samsung демо, TrueHD/Atmos/DTS:X passthrough-клипы, PGS/VobSub, 3D, interlaced, VC-1), но страницы отдают 403 автоматическим клиентам — открывать в браузере (не проверено). Аналогично [4kmedia.org](https://4kmedia.org/) (HDR10/HLG/DV демо-клипы, 403 для нас — не проверено) и Dolby Developer «Dolby Vision streams» (DNS не разрешился из среды — не проверено).
|
||
- [demo.jellyfin.org/stable/web/](https://demo.jellyfin.org/stable/web/) — публичный демо-сервер (сейчас 12.2.0), логин `demo` без пароля (подтверждено модератором на форуме: [thread](https://forum.jellyfin.org/t-jellyfin-demo-credentials)); подходит для smoke-тестов API и UI, но транскод там ограничен.
|
||
|
||
---
|
||
|
||
## Краткие выводы для libVLC-клиента
|
||
|
||
1. Профиль: широкие DirectPlay-списки (по образцу Swiftfin-VLC) + CodecProfile-ограничения по разрешению/битрейту/`VideoRangeType`/`VideoBitDepth`, обязательный HLS-`mp4` TranscodingProfile с `h264,hevc` и расширенным аудио; `MaxStreamingBitrate` из настройки/`BitrateTest`.
|
||
2. Субтитры: текстовые `Embed+External`, графические `Embed` (+`Encode` fallback); ASS внешним файлом только если доверяете libass и шрифтам; никогда не объявлять формат только с `Encode`.
|
||
3. Аудио: решать passthrough через `AudioManager`/`AudioCapabilities`, при даунмиксе ограничивать `AUDIO_CHANNELS<=2`; применять `NormalizationGain` (цель −23 LUFS).
|
||
4. Сегменты/трикплей/главы — через нативные API 10.10+/12; legacy Intro Skipper — только как fallback.
|
||
5. Отчёты: `POST /Sessions/Playing*` каждые ~10 с (3 с, как у ATV, тоже приемлемо) с `PlaySessionId`, `PlayMethod`, индексами дорожек; `DELETE /Videos/ActiveEncodings` при стопе/смене качества; `Ping` при долгой паузе транскода.
|
||
6. Ниша: сторонних Android TV-клиентов в официальном списке нет; требования к листингу — год разработки/интерес + уникальная функция (двойные субтитры, offset, MPEG-2/VC-1 софт-декод, DTS-HD/TrueHD — всё то, что потеряли с удалением libVLC из официального ATV).
|
||
---
|
||
|
||
# Часть 6. Инструментарий разработки
|
||
|
||
_Исходный заголовок: Инструментарий разработки: Kotlin Android-клиент Jellyfin (phone + Android TV, libVLC) на Linux
|
||
|
||
Дата проверки: 2026-10-11. Все URL ниже открывались и проверялись (кроме помеченных «(не проверено)»). Версии — текущие стабильные на дату проверки.
|
||
|
||
---
|
||
|
||
## 0. Сводка актуальных версий (проверено)
|
||
|
||
| Компонент | Версия | Источник |
|
||
|---|---|---|
|
||
| Android Studio (stable) | **Rabbit 1 \| 2026.2.1** (build 2026.2.1.8), вышла 1 октября 2026; в canary — Rabbit 2 | https://developer.android.com/studio/releases , https://androidstudio.googleblog.com/ |
|
||
| AGP (stable) | **9.4.0** (сентябрь 2026): Gradle ≥ 9.6.0, JDK 17 (min и default), Build Tools 36.0.0, NDK 28.2.13676358, max API 37 | https://developer.android.com/build/releases/gradle-plugin |
|
||
| Gradle | **9.8.1** (7 октября 2026); ветка 8.x — 8.14.6 | https://gradle.org/releases/ |
|
||
| Kotlin | **2.4.21** (8 октября 2026); 2.4.0 — июнь 2026 | https://kotlinlang.org/docs/releases.html |
|
||
| KSP | **2.3.12** (9 сентября 2026); требует AGP ≥ 8.12.0 | https://github.com/google/ksp/releases |
|
||
| Hilt / Dagger | **2.60.1** | https://github.com/google/dagger/releases , https://dagger.dev/hilt/gradle-setup.html |
|
||
| Room | **2.8.5** (сентябрь 2026), Gradle-плагин `androidx.room` | https://developer.android.com/jetpack/androidx/releases/room |
|
||
| Compose for TV | `androidx.tv:tv-material:1.1.0`, `androidx.tv:tv-foundation:1.0.0` (stable, май 2026); Compose BOM в примерах Google — `2026.09.00` | https://developer.android.com/jetpack/androidx/releases/tv , https://developer.android.com/training/tv/playback/compose |
|
||
| Android Emulator | **37.2.12** (1 октября 2026) | https://developer.android.com/studio/releases/emulator |
|
||
| cmdline-tools (Linux) | build 15859902 (short 22.0): `https://dl.google.com/android/repository/commandlinetools-linux-15859902_latest.zip` | https://developer.android.com/studio/install |
|
||
| Android CLI (`android`) | 1.0.16500706 (октябрь 2026); первый релиз 0.7 — апрель 2026 | https://developer.android.com/tools/agents/android-cli/release-notes |
|
||
| scrcpy | **v5.0.1** | https://github.com/Genymobile/scrcpy |
|
||
| LeakCanary | 2.14 | https://square.github.io/leakcanary/getting_started/ |
|
||
| Chucker | 4.2.0 | https://github.com/ChuckerTeam/chucker |
|
||
|
||
Таблица совместимости Android Studio ↔ AGP (с официальной страницы релизов): Rabbit 1 (2026.2.1) и Quail 4 (2026.1.4) поддерживают AGP 7.1–9.4; Quail 2/3 — до 9.3; Quail 1 / Panda 4 — до 9.2; Panda 1 — до 9.0. Минимальные версии для API 37: Panda 3 Patch 1 + AGP 9.1.1; для API 36: Meerkat Patch 1 + AGP 8.9.1.
|
||
|
||
Матрица AGP 9.x ↔ Gradle ↔ KGP (из release notes каждой версии):
|
||
|
||
| AGP | Gradle min | JDK | KGP min | Релиз |
|
||
|---|---|---|---|---|
|
||
| 9.0.x | 9.1.0 | 17 | 2.2.10 (встроенный Kotlin) | январь 2026 |
|
||
| 9.1.x | 9.3.1 | 17 | 2.2.10 | апрель 2026 |
|
||
| 9.2.x | 9.4.1 | 17 | 2.2.10 | апрель 2026 |
|
||
| 9.3.x | 9.5.0 | 17 | не указан | июль 2026 |
|
||
| 9.4.0 | 9.6.0 | 17 | не указан | сентябрь 2026 |
|
||
|
||
Важно про AGP 9: **built-in Kotlin** включён по умолчанию — плагин `org.jetbrains.kotlin.android` больше не применяется (его надо удалить), `kotlin-kapt` несовместим (переходить на KSP или временно `com.android.legacy-kapt`), `android.kotlinOptions {}` мигрирует в `kotlin { compilerOptions {} }`, новый DSL (`android.newDsl=true`), `variantFilter` → `androidComponents.beforeVariants`. AGP 10 (середина 2026+) уберёт опции отката. Подробно:
|
||
- [AGP 9.0 release notes](https://developer.android.com/build/releases/agp-9-0-0-release-notes) — список поведенческих изменений, таблица совместимости.
|
||
- [Migrate to built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin) — что удалить, как задать compilerOptions, опт-аут `android.builtInKotlin=false` + `android.newDsl=false`.
|
||
- [Compose compiler Gradle plugin](https://kotlinlang.org/docs/compose-compiler-migration-guide.html) — с Kotlin 2.0 компилятор Compose живёт в репозитории Kotlin, плагин `org.jetbrains.kotlin.plugin.compose` версионируется вместе с Kotlin (в каталоге: `version.ref = "kotlin"`); при AGP ≥ 9.0 плагин `org.jetbrains.kotlin.android` не нужен; `composeOptions.kotlinCompilerExtensionVersion` больше не используется.
|
||
|
||
---
|
||
|
||
## 1. Build toolchain
|
||
|
||
### 1.1 Android Studio на Linux
|
||
- [Install Android Studio](https://developer.android.com/studio/install) — официальный способ: tar.gz (`android-studio-rabbit1-linux.tar.gz`), распаковать в `/usr/local/` (для пользователя) или `/opt/` (для всех), запуск `android-studio/bin/studio`; далее `Tools > Create Desktop Entry`. Для 64-битной Ubuntu документация всё ещё просит 32-битные библиотеки: `sudo apt-get install libc6:i386 libncurses5:i386 libstdc++6:i386 lib32z1 libbz2-1.0:i386`.
|
||
- Системные требования Linux (та же страница): любой 64-битный дистрибутив с GNOME/KDE/Unity, **glibc ≥ 2.31**; RAM: 8 ГБ только Studio / **16 ГБ Studio + эмулятор**, рекомендовано 32 ГБ; диск 8/16 ГБ, рекомендован SSD 32+ ГБ; CPU с виртуализацией (Intel VT-x / AMD-V, включена в BIOS), микроархитектура после 2017; GPU для эмулятора — 4 ГБ VRAM (рекомендовано 8 ГБ); экран 1280×800 (рек. 1920×1080). ARM-Linux не поддерживается. Intel N/U-серии «не рекомендуются».
|
||
- [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) — официально умеет ставить и обновлять «Google's Android Studio releases and Canary builds». Для пользователя JetBrains IDE это самый удобный путь (параллельно Stable и Canary).
|
||
- Snap (`sudo snap install android-studio --classic`, пакет Snapcrafters) и Flathub (`flatpak install flathub com.google.AndroidStudio`, неофициальный community-пакет) существуют, но preview-сборок в них нет и они отстают от tarball — (ссылки на страницы snapcraft/flathub не проверялись, команды — из сторонних гайдов).
|
||
- Релизы/каналы: [Android Studio blog](https://androidstudio.googleblog.com/) — хронология: Rabbit 1 stable 01.10.2026, Rabbit 2 Canary 4 08.10.2026, Quail 4 Patch 1 18.09.2026. Фичи Rabbit 2 Canary (Compose Preview Screenshot Testing через test suites, ресайз окна Running Devices, BYOA-агенты, Journeys, Parallel sync с Gradle 9.4) — [Preview release notes](https://developer.android.com/studio/preview/features).
|
||
|
||
### 1.2 KVM / ускорение эмулятора на Linux
|
||
- [Configure hardware acceleration](https://developer.android.com/studio/run/emulator-acceleration) — проверка: `egrep -c '(vmx|svm)' /proc/cpuinfo` (>0), `sudo apt-get install cpu-checker && sudo kvm-ok`; установка `sudo apt-get install qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils`; проверка эмулятором `emulator -accel-check` (ожидается `KVM (version 12) is installed and usable`). Эмулятор с KVM нельзя запускать внутри другой ВМ (VirtualBox/VMware/Docker). GPU-режимы: `-gpu auto|host|software|lavapipe|swiftshader|swangle`; `swiftshader_indirect` объявлен deprecated в эмуляторе 36.4.9, `mesa` — deprecated; на Linux `-gpu software` теперь выбирает Lavapipe. Пользователя нужно добавить в группу `kvm` (в доке — «specific user permissions», конкретика в инструкции Ubuntu KVM).
|
||
|
||
### 1.3 Только командная строка
|
||
- [sdkmanager](https://developer.android.com/tools/sdkmanager) — расположение `android_sdk/cmdline-tools/latest/bin/sdkmanager` (после распаковки zip нужно вручную создать подпапку `latest` и перенести туда `bin`, `lib`, `NOTICE.txt`, `source.properties`). Команды: `sdkmanager --list`, `sdkmanager "platform-tools" "platforms;android-36"`, `sdkmanager "build-tools;36.0.0"`, `sdkmanager --licenses`, опции `--sdk_root=`, `--channel=0..3`, `--update`, `--uninstall`. **Внимание:** страница теперь помечает `sdkmanager` как deprecated в пользу Android CLI (`android sdk install|list|update|remove`).
|
||
- [avdmanager](https://developer.android.com/tools/avdmanager) — `avdmanager create avd -n name -k "system-images;android-25;google_apis;x86" [-c size] [-f] [-p path]`, `avdmanager list device|target|avd`, `delete avd -n`. Тоже помечен deprecated (→ `android emulator create|remove`). Флаг `-d/--device` для выбора hardware-профиля на онлайн-странице не описан, но присутствует в `avdmanager create avd --help` (не проверено по онлайн-доке).
|
||
- [Emulator command line](https://developer.android.com/studio/run/emulator-commandline) — `emulator -avd NAME` / `emulator @NAME`, `-list-avds`, `-no-snapshot` (полностью отключить Quick Boot), `-no-snapshot-load`, `-no-snapshot-save`, `-gpu MODE`, `-no-window`, `-memory 1536..8192`, `-no-audio`/`-noaudio`, `-no-boot-anim`, `-wipe-data`, `-netdelay`, `-accel auto|on|off`, `emulator -help-all`. `-skin`/`-skindir` помечены deprecated. Страница тоже отмечает, что `emulator` deprecated в пользу `android emulator create|start|stop|list`.
|
||
- **Android CLI** (новый официальный инструмент Google, бинарник `android`):
|
||
- [Обзор/скачивание](https://developer.android.com/tools/agents) и [Overview](https://developer.android.com/tools/agents/android-cli/overview) — установка со страницы загрузки (в release notes упоминаются `apt-get`, `winget`, `homebrew`), `android update`, `android init`; группы команд `android sdk`, `android emulator`, `android device remote`, `android screen capture`, `android layout`, `android studio render-compose-preview` и др.
|
||
- [android sdk install](https://developer.android.com/tools/agents/android-cli/commands/sdk_install) — синтаксис `android sdk install [--beta] [--canary] [--force] <package>[@<version>]`, пути через слеш: `android sdk install emulator platforms/android-34`; список — `android sdk list --all`.
|
||
- [android emulator create](https://developer.android.com/tools/agents/android-cli/commands/emulator_create) — `android emulator create --list-profiles`, `android emulator create <profile>` (дефолт `medium_phone`). Выбор образа/TV-профиля на странице не описан — проверяйте `--list-profiles`.
|
||
- [android emulator start](https://developer.android.com/tools/agents/android-cli/commands/emulator_start) — `android emulator start [--cold] [--headless] <device>`; команда блокируется до окончания загрузки.
|
||
- [Release notes](https://developer.android.com/tools/agents/android-cli/release-notes) — 1.0.16500706 (октябрь 2026); `android emulator` на Windows отключён; TV-профили в заметках не упоминаются.
|
||
- Сборка из терминала: `./gradlew assembleDebug`, `./gradlew installDebug`, `./gradlew :app:assembleRelease`, `./gradlew connectedCheck` (инструментальные тесты на подключённом устройстве/эмуляторе). Это стандартные задачи AGP (примеры `./gradlew assembleDebug` / `connectedCheck` встречаются в README jellyfin-androidtv CI и android-emulator-runner).
|
||
|
||
### 1.4 JDK
|
||
- [Java versions in Android builds](https://developer.android.com/build/jdks) — три разных JDK: JBR (запускает IDE; рекомендуется не трогать, не задавать `STUDIO_JDK`), **Gradle JDK** (из терминала берётся `JAVA_HOME`/`java` в PATH; в Studio — `Settings > Build, Execution, Deployment > Build Tools > Gradle > Gradle JDK`, рекомендован макрос `GRADLE_LOCAL_JAVA_HOME`; с Panda 1 новые проекты используют Gradle Daemon JVM criteria), **toolchain JDK** (компиляция). Рекомендуется всегда задавать toolchain: `java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }`; `kotlinOptions.jvmTarget` нужен только при Kotlin < 2.2.
|
||
- Требование AGP 9.x: **JDK 17** (минимум и default). Для терминальной сборки на Ubuntu достаточно Temurin 17 или 21 (Gradle 9.8 работает на 17+); Android Studio bundled JBR на середину 2026 — 21 (из обсуждений на platform.jetbrains.com, официально версия JBR в release notes не указана — не проверено). Практичный выбор: Temurin 21 как `JAVA_HOME` + toolchain 17 в проекте.
|
||
|
||
### 1.5 Gradle: каталоги версий, кэш конфигурации, производительность
|
||
- [Version catalogs](https://docs.gradle.org/current/userguide/version_catalogs.html) — `gradle/libs.versions.toml`, секции `[versions]`, `[libraries]`, `[bundles]`, `[plugins]`; доступ `libs.xxx`, `alias(libs.plugins.xxx)`.
|
||
- [Configuration cache](https://docs.gradle.org/current/userguide/configuration_cache.html) — `org.gradle.configuration-cache=true`; предпочтительный режим с Gradle 9.0, станет default в Gradle 10; IDE sync его не использует.
|
||
- [Optimize your build speed](https://developer.android.com/build/optimize-your-build) — пример `org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g`; `org.gradle.configuration-cache=true`; kapt → KSP («kapt significantly slower»); non-transitive R (default с AGP 8.0); `resourceConfigurations("en","xxhdpi")` для dev-флейвора; статичные версии зависимостей; `android.enableJetifier=false`. `org.gradle.caching=true` и `org.gradle.parallel=true` — стандартные свойства Gradle (на этой странице не упомянуты).
|
||
|
||
### 1.6 KSP для Hilt/Room
|
||
- [Hilt Gradle setup](https://dagger.dev/hilt/gradle-setup.html) — плагины `com.google.devtools.ksp` и `com.google.dagger.hilt.android` (2.60.1), зависимости `implementation("com.google.dagger:hilt-android:2.60.1")`, `ksp("com.google.dagger:hilt-compiler:2.60.1")`, для тестов `kspAndroidTest`/`kspTest`.
|
||
- [Room releases](https://developer.android.com/jetpack/androidx/releases/room) — `ksp("androidx.room:room-compiler:2.8.5")`, плагин `id("androidx.room")` + `room { schemaDirectory("$projectDir/schemas") }`.
|
||
- Совместимость KSP 2.3.12 с Kotlin 2.4.21 на странице релиза явно не заявлена (не проверено) — KSP2 с 2025 года декаплирован от версии компилятора, но при обновлении Kotlin стоит сверяться с release notes KSP.
|
||
|
||
### 1.7 Варианты сборки phone vs TV, подпись, R8
|
||
- [Configure build variants](https://developer.android.com/build/build-variants) — `flavorDimensions += "..."`, `productFlavors { create("tv") { dimension = ... } }`, source sets `src/<flavor>/`, отключение вариантов через `androidComponents { beforeVariants { } }`. **Рекомендация для Jellyfin-клиента:** один `applicationId` и один APK/AAB с `LEANBACK_LAUNCHER` + обычным `LAUNCHER` (так делают и Google в [Get started with TV apps](https://developer.android.com/training/tv/start/start): `uses-feature android.software.leanback required=false`, `android.hardware.touchscreen required=false`, баннер `android:banner` 320×180 px xhdpi с текстом внутри, отдельная TV-activity или одна activity с обеими категориями). Флейворы имеют смысл только если нужны разные `applicationId`/магазины (например, отдельная сборка для Amazon Appstore без GMS).
|
||
- [Sign your app](https://developer.android.com/studio/publish/app-signing) — `keystore.properties` вне VCS, `signingConfigs { create("release") { ... } }`, `keytool -export -rfc -keystore ... -alias ... -file upload_certificate.pem`, Play App Signing обязателен для новых приложений, CLI — `apksigner`.
|
||
- [Shrink/optimize (R8)](https://developer.android.com/build/shrink-code) — AGP < 9.3: `isMinifyEnabled = true; isShrinkResources = true; proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), ...)` (`proguard-android.txt` с AGP 9.1 не поддерживается); AGP ≥ 9.3: `release { optimization { enable = true } }` и keep-правила в `src/<variant>/keepRules/*.keep`; задача `./gradlew :app:analyzeReleaseR8Config` (R8 Configuration Analyzer). Для libVLC потребуются consumer-правила библиотеки (libvlc-android их поставляет — не проверено).
|
||
|
||
### 1.8 adb и TV-устройства
|
||
- [Android Debug Bridge](https://developer.android.com/tools/adb) — wireless debugging: Android 11+ для телефонов, **для TV и Wear OS — Android 13+ (API 33)**; `adb pair ipaddr:port` (код из «Pair using pairing code»), затем автоподключение по mDNS; диагностика `adb server-status`, `adb mdns track-services --proto-text`; adb 37.0.0 + Android 17 добавляют «adb Wi-Fi 2.0». Legacy-способ (работает и на старых TV): по USB `adb tcpip 5555`, отключить кабель, `adb connect <ip>:5555`, `adb devices`.
|
||
- [Get started with TV apps — физическое устройство](https://developer.android.com/training/tv/start/start) — включение Developer options на Android TV: `Settings > Device > About > Build` (нажать несколько раз до «You are now a developer!»), затем `Settings > Preferences > Developer options > Debugging > USB debugging`. На Google TV (Chromecast with Google TV / Google TV Streamer) путь `Settings > System > About > Android TV OS build` ×7 — по сторонним гайдам (allaboutchromecast.com, aftvnews.com), официальной страницы Google не найдено (не проверено).
|
||
- [Fire TV: Connect to Fire TV through ADB](https://developer.amazon.com/docs/fire-tv/connecting-adb-to-device.html) — `Settings > My Fire TV > Developer Options > ADB Debugging` (+ Apps from Unknown Sources); на части прошивок Developer Options скрыты: `Settings > My Fire TV > About > Your TV` ×7; IP брать из `About > Network`; `adb connect <ip>:5555` (порт 5555–5585); подтвердить «Always allow from this computer».
|
||
- Отправка клавиш (keycodes подтверждены по [keycodes.h в AOSP](https://android.googlesource.com/platform/frameworks/native/+/refs/heads/main/include/android/keycodes.h)): DPAD_UP=19, DOWN=20, LEFT=21, RIGHT=22, DPAD_CENTER=23, BACK=4, HOME=3, MENU=82, MEDIA_PLAY_PAUSE=85, MEDIA_REWIND=89, MEDIA_FAST_FORWARD=90, SEARCH=84, TV=170, CAPTIONS=175, SETTINGS=176, TV_INPUT=178, ASSIST=219.
|
||
|
||
```bash
|
||
adb connect <ip-тв>:5555
|
||
adb -s <ip-тв>:5555 shell input keyevent KEYCODE_DPAD_CENTER
|
||
adb shell input keyevent KEYCODE_DPAD_RIGHT
|
||
adb shell input keyevent KEYCODE_MEDIA_PLAY_PAUSE
|
||
adb shell input keyevent KEYCODE_BACK
|
||
adb shell input keyevent 85 # то же, что KEYCODE_MEDIA_PLAY_PAUSE
|
||
adb shell input text "hello"
|
||
adb shell am start -n org.example.app/.MainActivity
|
||
adb install -r app/build/outputs/apk/debug/app-debug.apk
|
||
```
|
||
(Команда `adb shell input keyevent` — стандартный системный бинарник `input`; на странице ADB-доки она не описана, но её поведение общеизвестно.)
|
||
|
||
---
|
||
|
||
## 2. Android TV эмуляторы
|
||
|
||
### 2.1 Hardware-профили и системные образы (проверено по официальным репозиториям SDK)
|
||
- Device definitions для TV (файл [tv.xml в sdklib](https://android.googlesource.com/platform/tools/base/+/refs/heads/mirror-goog-studio-main/sdklib/src/main/resources/com/android/sdklib/devices/tv.xml)): **`tv_4k` «Television (4K)» 3840×2160 xxxhdpi**, **`tv_1080p` «Television (1080p)» 1920×1080 xhdpi**, **`tv_720p` «Television (720p)» 1280×720 tvdpi**; у всех nav = `dpad`, tag `android-tv`.
|
||
- [Create and manage virtual devices](https://developer.android.com/studio/run/managing-avds) — в Device Manager Device Type = Android TV / Google TV; «Navigation Style: D-pad» — события от внешнего пульта идентичны D-pad-событиям; для Android TV аппаратная клавиатура в конфиге AVD отключена, камеры нет. Образы с Play Store (release-key) не дают `adb root`; для root берите AOSP-образы.
|
||
- Репозиторий образов **Google TV** ([sys-img2-1.xml](https://dl.google.com/android/repository/sys-img/google-tv/sys-img2-1.xml), канал stable):
|
||
- API **36**: `system-images;android-36;google-tv;x86_64`, `...;google-tv;arm64-v8a`, `...;google-tv;x86`, плюс 16 KB page-size варианты `system-images;android-36;google-tv-ps16k;x86_64` и `...;google-tv-ps16k;arm64-v8a`.
|
||
- API 34: `system-images;android-34;google-tv;x86` и `...;arm64-v8a` (**x86_64 нет**).
|
||
- API 33: `...;android-33;google-tv;x86`, `arm64-v8a`. API 31: `x86`, `arm64-v8a`. API 30: `x86`.
|
||
- Репозиторий образов **Android TV (AOSP)** ([sys-img2-1.xml](https://dl.google.com/android/repository/sys-img/android-tv/sys-img2-1.xml)): API 36 — `system-images;android-36;android-tv;x86_64`, `arm64-v8a`, `x86`; API 34/33/31 — только `x86` и `arm64-v8a`; API 21–30 — `x86` (и armeabi-v7a для 21/23).
|
||
- **Выводы:** TV-образов для API **35 не существует**; единственные x86_64 TV-образы — API 36 (Android 16). Для API 34 и ниже на x86_64-хосте используйте 32-битные `x86`-образы (работают под KVM). arm64-образы на x86-хосте работать не будут. Google TV образы требуют принятия лицензии `android-googletv-license`.
|
||
|
||
### 2.2 Управление TV-эмулятором
|
||
- [Extended controls](https://developer.android.com/studio/run/emulator-extended-controls) — раздел **Directional Pad** появляется, если hardware-профиль включает D-pad (у TV-профилей — да). Полный список горячих клавиш — внутри эмулятора: **F1** (Help pane) в standalone-режиме; в embedded-режиме — `Settings > Keymap > Running Devices`. Чтобы шорткаты работали, в General должно быть «Send keyboard shortcuts: Emulator controls».
|
||
- [Run apps on the Android Emulator](https://developer.android.com/studio/run/emulator) — Ctrl+M симулирует Menu; требования эмулятора 16 ГБ RAM / 16 ГБ диска.
|
||
- Практическая раскладка (официальной таблицы для TV в доке нет — по опыту, не проверено): стрелки = DPAD_*, Enter = DPAD_CENTER, Esc = BACK, Home = HOME, Ctrl+M = MENU; Play/Pause и медиаклавиши с клавиатуры не маппятся — используйте `adb shell input keyevent KEYCODE_MEDIA_PLAY_PAUSE` или панель D-pad в Extended controls.
|
||
- Посылать клавиши в эмулятор: `adb -e shell input keyevent KEYCODE_DPAD_DOWN` (`-e` выбирает единственный эмулятор).
|
||
|
||
### 2.3 Производительность, ограничения
|
||
- KVM + `-gpu host` (или `auto`) — лучшая производительность на Linux ([acceleration docs](https://developer.android.com/studio/run/emulator-acceleration)); `-no-snapshot` для чистых стартов; `-memory 4096`; TV-профиль 4K тяжёлый — для разработки UI используйте `tv_1080p`.
|
||
- Release notes эмулятора ([ссылка](https://developer.android.com/studio/releases/emulator)): 36.4.10 — фикс краша TV-эмулятора при hover; 36.4.9 — Lavapipe как software-рендер по умолчанию, `-gpu software`; 36.5.10 — новый сетевой стек (AVD видят друг друга — удобно для теста discovery Jellyfin по UDP 7359); 37.1.11 — Vulkan-расширения для API 37. Про аппаратное декодирование видео упоминаний нет; единственное упоминание — опция CUDA-декода VPX (`ANDROID_EMU_MEDIA_DECODER_CUDA_VPX=1`).
|
||
- Ограничения для видеоплеера (общие знания, не проверено по официальной доке): в эмуляторе нет аппаратных кодеков SoC — MediaCodec предоставляет только программные `c2.android.*`/`OMX.google.*` декодеры, поэтому libVLC с `--codec=mediacodec_ndk` получит software-путь, HEVC 10-bit/4K будет тормозить; HDR (HDR10/DV) не воспроизводится как HDR; audio passthrough (AC3/DTS/TrueHD bitstream) и HDMI CEC проверить нельзя; тональность Dolby Vision профилей — только на железе. Тестируйте транскодинг-решения Jellyfin на реальном устройстве.
|
||
|
||
### 2.4 Headless / CI
|
||
- `emulator -avd tv36 -no-window -no-audio -no-boot-anim -no-snapshot -gpu swiftshader` (на новых версиях `-gpu software`), затем `adb wait-for-device` и polling `getprop sys.boot_completed`.
|
||
- [ReactiveCircus/android-emulator-runner](https://github.com/ReactiveCircus/android-emulator-runner) — `target` официально принимает **`android-tv` и `google-tv`**, `arch: x86_64|x86|arm64-v8a`, `profile` (например `tv_1080p` — имя из `avdmanager list device`), `api-level: 36`, `emulator-options` (default `-no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim`), `script: ./gradlew connectedCheck`. Перед шагом на Linux включить KVM udev-правилом (см. раздел 6).
|
||
- [GitHub changelog: hardware-accelerated Android virtualization](https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/) — KVM доступен на 2-vCPU Linux-раннерах GitHub; udev-правило `KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"`.
|
||
|
||
### 2.5 Альтернативные эмуляторы
|
||
- **Genymotion** — [Desktop docs](https://docs.genymotion.com/desktop/) и [список шаблонов](https://rozz.genymotion.com/pages/which-devices-templates-are-available-with-genymotion-deskto.html) — только телефоны/планшеты (и Automotive-туториалы); **TV-шаблонов нет**; SaaS-образы до Android 16.
|
||
- **Waydroid** — [docs.waydro.id](https://docs.waydro.id/) — контейнер с LineageOS (Android 13), **официальных TV-образов нет**; существуют community-сборки: [supechicken/waydroid-androidtv-builds](https://github.com/supechicken/waydroid-androidtv-builds) (Android TV образы с VA-API/GApps/Widevine L3 — не проверено на работоспособность). Для разработки под D-pad малопригоден, но может подойти для проверки libVLC с VA-API.
|
||
- **Android-x86 / Bliss OS** — TV/leanback-сборок для PC в официальных каналах не найдено; Android-x86 «dormant», Bliss OS — desktop-ориентированные ISO (не проверено).
|
||
- **Cuttlefish** — [Get started](https://source.android.com/docs/setup/create/cuttlefish-use); в [AndroidProducts.mk](https://android.googlesource.com/device/google/cuttlefish/+/refs/heads/main/AndroidProducts.mk) есть TV-таргеты **`aosp_cf_x86_64_tv`** и `aosp_cf_x86_tv` (lunch `aosp_cf_x86_64_tv-trunk_staging-userdebug`). Готовые сборки — на ci.android.com (нужно выбрать этот таргет); запуск `launch_cvd`, просмотр через WebRTC на localhost:8443, adb работает. Это AOSP без GMS — полезно для CI/проверки чистого Android TV, но без Google TV launcher.
|
||
- **Firebase Test Lab** ([каталог устройств](https://firebase.google.com/docs/test-lab/android/available-testing-devices)) — виртуальные `GoogleTvEmulator/30` и `AmatiTvEmulator/29` помечены deprecated (удаление планировалось 2025-05-31), замена `GoogleTv.arm/31` — «available soon». Список: `gcloud firebase test android models list`.
|
||
- **Fire TV** — [How Fire TV differs from Android TV](https://developer.amazon.com/public/solutions/devices/fire-tv/docs/amazon-fire-tv-differences-from-android-tv-development): «**Fire TV doesn't support emulators for testing**» — только физическое устройство по ADB; нет Google Play services (Firebase с GMS не работает), Leanback частично (без SearchFragment), пульт имеет те же D-pad/Select/Back/Play-Pause + Rewind/FF/Menu. Vega OS (новая платформа Fire TV на базе не-Android) имеет свой Vega Virtual Device — к Android-клиенту не относится.
|
||
|
||
### 2.6 Дешёвое тестовое железо и управление с телефона/ПК
|
||
- Google TV Streamer (4K) — [store.google.com](https://store.google.com/us/product/google_tv_streamer?hl=en-US) (страница есть; спецификации не распарсились — не проверено); актуальные прошивки: [Chromecast & Google TV Streamer firmware versions](https://support.google.com/chromecast/answer/7124014?hl=en) (Streamer — build UTTK.260317.003, Chromecast HD — UTTC.250917.004). Это эталон Google TV UX; Chromecast with Google TV (HD/4K) — ещё дешевле на вторичном рынке.
|
||
- NVIDIA SHIELD TV / SHIELD TV Pro — [nvidia.com/shield](https://www.nvidia.com/en-us/shield/) (Pro — $299); лучший декодер/passthrough для проверки AC3/DTS/TrueHD и Dolby Vision — классическое устройство для тестов медиаплееров.
|
||
- Xiaomi TV Stick 4K / Mi Box S — mi.com отдал 403 для автоматического запроса (ссылка не проверена); дешёвый Android TV-девайс с распространённым Amlogic SoC.
|
||
- Fire TV Stick (4K/4K Max) — самый дешёвый способ проверить Fire OS; adb по сети (см. 1.8).
|
||
- Телефон как пульт: [Use your phone as the remote for your Chromecast or Google TV Streamer](https://support.google.com/chromecast/answer/11221499?hl=en) — встроено в Google TV app (Android ≥ 4.27.8.93) и Google Home; D-pad/свайп, клавиатура, громкость.
|
||
- Протокол пульта из кода/скриптов: [tronikos/androidtvremote2](https://github.com/tronikos/androidtvremote2) — Python-библиотека «Android TV Remote protocol v2» (тот же протокол, что в Google TV app), без ADB, pairing по коду, отправка keycodes; PyPI 0.2.1, есть в Ubuntu как `python-androidtvremote2`. Удобно для автоматизации сценариев навигации на реальной приставке.
|
||
- scrcpy — см. раздел 3; для TV-приставок по `adb connect` это самый удобный «монитор + пульт» на Linux-десктопе.
|
||
|
||
---
|
||
|
||
## 3. Телефонные эмуляторы и реальные устройства
|
||
- Device Manager / AVD ([managing-avds](https://developer.android.com/studio/run/managing-avds)) — профили Pixel (`android emulator create --list-profiles` / `avdmanager list device`), вкладка Recommended для образов; foldable/tablet имеет смысл только для адаптивного UI телефона (Pixel Fold / Pixel Tablet профили есть в Paparazzi и AVD).
|
||
- [Run apps on a hardware device](https://developer.android.com/studio/run/device) — пара по Wi-Fi через QR/код (`Pair Devices Using Wi-Fi`), device mirroring в окне Running Devices, настройки `Settings > Tools > Device Mirroring` (включая проброс звука «Redirect audio from local devices»).
|
||
- **scrcpy**:
|
||
- [Genymobile/scrcpy](https://github.com/Genymobile/scrcpy) — v5.0.1; Linux/Win/mac; `--keyboard=uhid` (`-K`) — физическая клавиатура, `--otg`, аудио (Android 11+), запись `--record=file.mp4`.
|
||
- [Linux install](https://github.com/Genymobile/scrcpy/blob/master/doc/linux.md) — пакет в Ubuntu/Debian и snap помечены **obsolete**; рекомендуется статический релиз с GitHub или `install_release.sh` (apt-зависимости ffmpeg, SDL3, libusb, adb, meson/ninja).
|
||
- [Connection (TCP/IP)](https://github.com/Genymobile/scrcpy/blob/master/doc/connection.md) — `scrcpy --tcpip` (автоматически переводит USB-устройство в TCP/IP), `scrcpy --tcpip=<ip-тв>` (порт 5555 по умолчанию), `-s ip:port`, `-e` (единственное TCP/IP-устройство), `-d` (USB).
|
||
- [Shortcuts](https://github.com/Genymobile/scrcpy/blob/master/doc/shortcuts.md) — MOD (левый Alt/Super): MOD+h Home, MOD+b Back, MOD+s App switch, MOD+m Menu, MOD+↑/↓ громкость, MOD+p Power, MOD+f fullscreen, MOD+o выключить экран (продолжая зеркалить), MOD+Shift+v вставить текст. Обычные стрелки и Enter прокидываются на устройство как клавиши — на Android TV это DPAD_*/DPAD_CENTER, поэтому scrcpy работает как полноценный пульт (маппинг стрелок по опыту; в shortcuts.md не задокументирован).
|
||
|
||
```bash
|
||
adb connect <ip-тв>:5555
|
||
scrcpy -e --max-size 1280 --no-audio # зеркало TV-приставки с управлением стрелками
|
||
scrcpy --tcpip=<ip-тв> --record=tv-session.mp4
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Смотреть UI без полного эмулятора
|
||
|
||
### 4.1 Compose @Preview
|
||
- [Preview your UI with composable previews](https://developer.android.com/develop/ui/compose/tooling/previews) — параметры `device` (`"id:pixel_4"` или `spec:width=...,height=...,dpi=...`), `uiMode` (константы `Configuration.UI_*`), `showSystemUi`, `fontScale`, `locale`, `widthDp/heightDp`; multipreview-шаблоны `@PreviewScreenSizes`, `@PreviewFontScales`, `@PreviewLightDark`, `@PreviewDynamicColors` (ui-tooling-preview ≥ 1.6.0-alpha01) и собственные аннотации; **Interactive mode** (кнопка на превью — клики/ввод/анимации в песочнице); **Run Preview** разворачивает превью как Activity на устройстве; `LocalInspectionMode`; `@PreviewParameter`.
|
||
- [Devices reference](https://developer.android.com/reference/kotlin/androidx/compose/ui/tooling/preview/Devices) — объект с предопределёнными устройствами (DEFAULT, DESKTOP, FOLDABLE, AUTOMOTIVE_1024p, Nexus/Pixel… — страница рендерится динамически, полный список и точные строки `TV_720p`/`TV_1080p`/`TV_4K` через fetch подтвердить не удалось — **(не проверено)**). Гарантированно работающая форма — ID из tv.xml: `@Preview(device = "id:tv_1080p")`, `"id:tv_720p"`, `"id:tv_4k"` (формат `id:` подтверждён докой, ID — в tv.xml). Рекомендуемая TV-мультипревью:
|
||
|
||
```kotlin
|
||
@Preview(name = "TV 1080p", device = "id:tv_1080p", showSystemUi = true,
|
||
uiMode = Configuration.UI_MODE_TYPE_TELEVISION or Configuration.UI_MODE_NIGHT_YES)
|
||
@Preview(name = "TV 720p", device = "id:tv_720p", showSystemUi = true,
|
||
uiMode = Configuration.UI_MODE_TYPE_TELEVISION or Configuration.UI_MODE_NIGHT_YES)
|
||
annotation class TvPreviews
|
||
```
|
||
- Для Compose for TV ([Use Jetpack Compose on Android TV](https://developer.android.com/training/tv/playback/compose)) — `androidx.tv:tv-material` используется **вместо** `androidx.compose.material3`; смешивать нельзя (разные `MaterialTheme`); сэмплы `android/tv-samples` (JetStream, TV Material Catalog), codelab «Introduction to Compose for TV».
|
||
- Live Edit — на странице превью описано как «live updates to your composable preview as you make your edits»; Live Edit также включается в Layout Inspector.
|
||
- Android CLI: `android studio render-compose-preview` — рендер превью из терминала (команда есть в списке Android CLI; синтаксис не проверен).
|
||
|
||
### 4.2 Layout Inspector и Running Devices
|
||
- [Layout Inspector](https://developer.android.com/studio/debug/layout-inspector) — embedded в Running Devices (кнопка Toggle Layout Inspector), счётчики рекомпозиций, Deep Inspect, снапшоты, overlay-картинка, Live Edit; поддерживаются эмуляторы и физические устройства через mirroring; CLI — `android layout`.
|
||
- Running Devices / device mirroring ([Run apps on a hardware device](https://developer.android.com/studio/run/device)) — зеркало любого устройства с USB/wireless debugging прямо в IDE; TV-устройства отдельно не упомянуты, но mirroring работает через adb-стрим, поэтому Android TV по `adb connect` зеркалится так же (по опыту, не проверено официально).
|
||
- [Android Device Streaming](https://developer.android.com/studio/run/android-device-streaming) — удалённые реальные Pixel 9/9a/Pro/Pro XL/9 Pro Fold и партнёрские лаборатории (Samsung, Xiaomi, OPPO, OnePlus, vivo, Transsion); бесплатные минуты на Spark-плане; **TV-устройств нет**; CLI `android device remote`.
|
||
|
||
### 4.3 Screenshot-тесты для TV-лейаутов
|
||
- [Compose Preview Screenshot Testing](https://developer.android.com/studio/preview/compose-screenshot-testing) — плагин `com.android.compose.screenshot` 0.0.1-alpha16, `android.experimental.enableScreenshotTest=true`, source set `screenshotTest`, аннотация `@PreviewTest`, задачи `./gradlew updateDebugScreenshotTest` / `validateDebugScreenshotTest`, отчёт `build/reports/screenshotTest/preview/<variant>/index.html`. Стандалон-плагин **deprecated с AGP 9.5.0-alpha03**.
|
||
- [Screenshot testing with AGP test suites](https://developer.android.com/studio/preview/compose-screenshot-testing-with-testsuites) — новый путь: AGP ≥ 9.5.0-alpha03, Rabbit 1 Canary 4+, `android.experimental.testSuiteSupport=true`, `testOptions { screenshotTests.create("screenshotTest") { engineVersion = "0.0.1-alpha16"; targetVariants.add("debug") } }`, задачи `update{Suite}{Target}{Variant}TestSuite` / `test{Suite}...TestSuite`. Поскольку движок рендерит `@Preview` с параметром `device`, TV-превью (`id:tv_1080p`) становятся регрессионными скриншотами (прямо в доке TV не упомянут).
|
||
- [Roborazzi](https://github.com/takahirom/roborazzi) — JVM/Robolectric (`@GraphicsMode(NATIVE)`), плагин `io.github.takahirom.roborazzi`, задачи `recordRoborazziDebug` / `compareRoborazziDebug` / `verifyRoborazziDebug`, автогенерация тестов из `@Preview` (`generateComposePreviewRobolectricTests`), конфигурация устройства через `@Config(qualifiers = ...)`. TV-квалификатор: Robolectric разбирает строку по правилам Android-ресурсов ([Robolectric device configuration](https://robolectric.org/device-configuration/)), а UI-mode `television` — валидный квалификатор ресурсов с API 13 ([Providing resources](https://developer.android.com/guide/topics/resources/providing-resources)), поэтому ожидается рабочей строка `"w960dp-h540dp-land-xhdpi-television"` (1920×1080 px при xhdpi) — поддержка `television` именно в Robolectric не проверена.
|
||
- [Paparazzi](https://github.com/cashapp/paparazzi) — 2.0.0-alpha05.1, без эмулятора (layoutlib), `recordPaparazziDebug` / `verifyPaparazziDebug`; в [DeviceConfig.kt](https://raw.githubusercontent.com/cashapp/paparazzi/master/paparazzi/src/main/java/app/cash/paparazzi/DeviceConfig.kt) **нет TV-пресетов** — нужно создать `DeviceConfig(screenWidth = 1920, screenHeight = 1080, density = Density.XHIGH, uiMode = UiMode.TELEVISION, orientation = LANDSCAPE)` (поля `uiMode`/`orientation` — не проверены по исходнику, но пресеты показывают настраиваемые width/height/density).
|
||
|
||
### 4.4 UI-тесты с D-pad
|
||
- [Test focus navigation (Compose)](https://developer.android.com/develop/ui/compose/touch-input/focus/testing-focus) — `node.performKeyInput { pressKey(Key.DirectionDown) }`, `assertIsFocused()` / `assertIsNotFocused()`, `withKeyDown(Key.ShiftLeft) { pressKey(Key.Tab) }`; тестовая среда по умолчанию в touch mode — для TV переключайте input mode (см. ссылку на default input mode в доке). Поддерживаются `Key.DirectionUp/Down/Left/Right`, `Key.DirectionCenter`.
|
||
- [Compose testing APIs](https://developer.android.com/develop/ui/compose/testing/apis) — `performKeyPress(keyEvent)`, matchers `hasTestTag`, `assertIsDisplayed`.
|
||
- Espresso для View-based экранов: `ViewActions.pressKey(KeyEvent.KEYCODE_DPAD_RIGHT)` (стандартный API Espresso; страница не проверялась).
|
||
|
||
---
|
||
|
||
## 5. Прочие инструменты разработки
|
||
|
||
### 5.1 IntelliJ IDEA vs Android Studio
|
||
- [IntelliJ IDEA Help: Android](https://www.jetbrains.com/help/idea/android.html) — Android-поддержка **не входит в комплект** IDEA (unbundled с 2023.3); ставятся плагины **Android** и **Android Design Tools** из Marketplace (Community-редакция тоже подходит).
|
||
- [Compose previews (Kotlin Multiplatform docs)](https://kotlinlang.org/docs/multiplatform/compose-previews.html) — превью `androidx.compose.ui.tooling.preview.Preview` рендерятся и в Android Studio, и в IntelliJ IDEA (с плагином Kotlin Multiplatform); **но AGP 9.0 поддерживается в стабильной Android Studio и «пока не поддерживается» в IntelliJ IDEA** (обещали Q1 2026; на дату проверки community-отчёты в Slack kotlinlang — IDEA 2026.1 всё ещё не работает с AGP 9.x превью). Практика: для проекта на AGP 9.4 **нужна Android Studio** — Compose Preview, Layout Inspector, Running Devices, Device Manager/AVD, Logcat, Image Asset wizard и App Inspection там доступны полноценно; IDEA пригодна для редактирования/рефакторинга, а сборка/запуск — через Gradle/CLI. Ставьте Android Studio через JetBrains Toolbox рядом с IDEA — настройки/кеймапы переносятся.
|
||
|
||
### 5.2 Инспекция трафика Jellyfin API
|
||
- jellyfin-sdk-kotlin: [репозиторий](https://github.com/jellyfin/jellyfin-sdk-kotlin), [документация](https://kotlin-sdk.jellyfin.org/), [Getting started](https://kotlin-sdk.jellyfin.org/guide/getting-started) — координаты `org.jellyfin.sdk:jellyfin-core`, `createJellyfin { clientInfo = ClientInfo(...); context = androidContext }`. В `jellyfin-api/build.gradle.kts` на JVM объявлена **только зависимость OkHttp** ([файл](https://github.com/jellyfin/jellyfin-sdk-kotlin/blob/master/jellyfin-api/build.gradle.kts)) — Ktor-клиента там нет. Значит, подходят OkHttp-инструменты (Chucker, `HttpLoggingInterceptor`), если SDK даёт передать свой `OkHttpClient`/interceptor — способ передачи не проверен в доке.
|
||
- [Chucker](https://github.com/ChuckerTeam/chucker) — 4.2.0, `debugImplementation("com.github.chuckerteam.chucker:library:4.2.0")` + `releaseImplementation(...:library-no-op:4.2.0)`, `OkHttpClient.Builder().addInterceptor(ChuckerInterceptor(context))`; **интеграции с Ktor нет**. На TV UI Chucker (уведомления + Activity с тач-UI) неудобен — лучше `adb logcat` + HttpLoggingInterceptor или прокси.
|
||
- [Ktor client Logging](https://ktor.io/docs/client-logging.html) — на случай собственного Ktor-слоя: `io.ktor:ktor-client-logging`, `install(Logging) { level = LogLevel.HEADERS; sanitizeHeader { it == "Authorization" } }`, на Android — `org.slf4j:slf4j-android` (Ktor 3.6.0).
|
||
- [mitmproxy: install system-trusted CA on Android](https://docs.mitmproxy.org/stable/howto/install-system-trusted-ca-android/) — приложения с targetSdk ≥ 24 игнорируют пользовательские CA; на эмуляторе: `emulator -avd NAME -writable-system`, `adb root`, (API ≥ 29) `adb shell avbctl disable-verification && adb reboot && adb root`, `adb remount`, `openssl x509 -inform PEM -subject_hash_old -in mitmproxy-ca-cert.cer | head -1` → `<hash>.0` → `adb push <hash>.0 /system/etc/security/cacerts && adb shell chmod 644 ...`; для Play-образов — Magisk-модуль. Проще для своего приложения: [Network security config](https://developer.android.com/privacy-and-security/security-config) с `<debug-overrides><trust-anchors><certificates src="user"/></trust-anchors></debug-overrides>` — доверять пользовательским CA только в debuggable-сборках, затем на устройстве/эмуляторе поставить CA через mitm.it ([mitmproxy certificates](https://docs.mitmproxy.org/stable/concepts/certificates/)).
|
||
- HTTP Toolkit — https://httptoolkit.com/android/ (сайт отдал 403 для автоматического запроса — ссылка не проверена): ADB-настройка, автоматическая инъекция системного сертификата в эмулятор/root-устройство.
|
||
- [LeakCanary](https://square.github.io/leakcanary/getting_started/) — `debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")`; важно для плеера (утечки Activity/Surface при выходе из playback).
|
||
|
||
### 5.3 Jellyfin-сервер для разработки и медиаинструменты
|
||
- [Jellyfin in a container](https://jellyfin.org/docs/general/installation/container/) — образ `jellyfin/jellyfin` (также `ghcr.io/jellyfin/jellyfin`), порты `8096/tcp` (HTTP/web), `7359/udp` (auto-discovery), volumes `/config`, `/cache`, `/media` (можно read-only), опционально `/dev/dri` для HW-транскодинга, `JELLYFIN_PublishedServerUrl`. Пример compose:
|
||
|
||
```yaml
|
||
services:
|
||
jellyfin:
|
||
image: jellyfin/jellyfin
|
||
container_name: jellyfin
|
||
ports:
|
||
- 8096:8096/tcp
|
||
- 7359:7359/udp
|
||
volumes:
|
||
- ./config:/config
|
||
- ./cache:/cache
|
||
- type: bind
|
||
source: /path/to/media
|
||
target: /media
|
||
read_only: true
|
||
devices:
|
||
- /dev/dri:/dev/dri # опционально, VA-API/QSV транскодинг
|
||
restart: unless-stopped
|
||
```
|
||
- Демо-сервер: ссылка «Try the Demo» на [jellyfin.org](https://jellyfin.org/) ведёт на **https://demo.jellyfin.org/stable** (страница открывается); логин по сообщениям модераторов форума — пользователь `demo` без пароля (2023; не проверено).
|
||
- [Movies naming](https://jellyfin.org/docs/general/server/media/movies/) — `Movie Name (2030) [imdbid-tt0000000]/Movie Name (2030) [imdbid-tt0000000].mkv` для тестовой библиотеки.
|
||
- [Troubleshooting](https://jellyfin.org/docs/general/administration/troubleshooting/) и [Configuration](https://jellyfin.org/docs/general/administration/configuration/) — логи в `<Data Directory>/log` (Linux: `~/.local/share/jellyfin/log`, в Docker — внутри `/config`), debug-уровень через `logging.json` (`Serilog.MinimumLevel.Default = "Debug"`), при проблемах транскодинга «first check the ffmpeg logs» (файлы FFmpeg.Transcode-*.log в той же папке — имя не проверено по доке). Причина транскодирования: в Dashboard у активной сессии (иконка «i») показываются Direct Play / Direct Stream / Transcode и причина; в логе ищите `TranscodeReason`/`TranscodeReasons` (ContainerNotSupported, AudioCodecNotSupported, VideoCodecNotSupported, SubtitleCodecNotSupported и т.п.) — по форуму, официальной страницы с перечнем не найдено. Для клиента ключ — корректный `DeviceProfile` в `PlaybackInfo`, тогда сервер пишет причины в ответе `PlaybackInfoResponse.MediaSources[].TranscodingReasons` (поле из OpenAPI Jellyfin — не проверено в этом исследовании).
|
||
- [ffprobe](https://ffmpeg.org/ffprobe.html) — `ffprobe -show_streams -show_format -output_format json INPUT` для проверки кодеков/профилей/HDR-метаданных тестовых файлов.
|
||
- [MediaInfo](https://mediaarea.net/en/MediaInfo) — пакеты для Ubuntu/Debian/Fedora/Arch, Flatpak/Snap; удобно для Dolby Vision профилей и аудиодорожек.
|
||
- VLC desktop — https://www.videolan.org/vlc/ (не проверено в этом исследовании) — эталон для сравнения поведения libVLC (те же модули демукса/декодирования).
|
||
|
||
---
|
||
|
||
## 6. CI/CD и релизы
|
||
- [android-actions/setup-android](https://github.com/android-actions/setup-android) — `@v4`, inputs `packages` (default `platform-tools`), `cmdline-tools-version` (default 15859902), `accept-android-sdk-licenses`; на Ubuntu-раннерах GitHub SDK уже предустановлен, экшен докачивает только недостающее.
|
||
- [gradle/actions/setup-gradle](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md) — `@v6`, кеширование Gradle User Home по умолчанию (запись только с default-ветки, `cache-read-only`), `dependency-graph`, build scans. Не комбинировать с `actions/setup-java` `cache: gradle`.
|
||
- [actions/setup-java](https://github.com/actions/setup-java) — `@v6`, `distribution: temurin`, `java-version: 17`.
|
||
- [ReactiveCircus/android-emulator-runner](https://github.com/ReactiveCircus/android-emulator-runner) — TV-таргеты поддерживаются (`target: google-tv` / `android-tv`). Пример:
|
||
|
||
```yaml
|
||
jobs:
|
||
tv-tests:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v6
|
||
- uses: actions/setup-java@v6
|
||
with: { distribution: temurin, java-version: 17 }
|
||
- uses: gradle/actions/setup-gradle@v6
|
||
- name: Enable KVM
|
||
run: |
|
||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||
sudo udevadm control --reload-rules
|
||
sudo udevadm trigger --name-match=kvm
|
||
- uses: reactivecircus/android-emulator-runner@v2
|
||
with:
|
||
api-level: 36
|
||
target: google-tv
|
||
arch: x86_64
|
||
profile: tv_1080p
|
||
script: ./gradlew connectedDebugAndroidTest
|
||
```
|
||
- Живой пример у Jellyfin: [jellyfin-androidtv/.github/workflows](https://github.com/jellyfin/jellyfin-androidtv/tree/master/.github/workflows) — `app-build.yaml` (ubuntu-26.04, `actions/setup-java@v6` temurin с версией из `.tools-versions`, `gradle/actions/setup-gradle@v6`, `./gradlew assembleDebug`, upload APK), отдельные `app-lint`, `app-test`, `app-publish`, `gradlew-validate`.
|
||
- Подпись в CI: keystore в base64 в GitHub Secret → `echo "$KEYSTORE_B64" | base64 -d > release.jks`, пароли через env в `keystore.properties`/`signingConfigs` (схема из [app-signing](https://developer.android.com/studio/publish/app-signing)).
|
||
- [fastlane supply](https://docs.fastlane.tools/actions/supply/) — `fastlane supply init`, `fastlane supply --aab app.aab --track internal`, service-account JSON (`validate_play_store_json_key`), структура `fastlane/metadata/android/<locale>/{title.txt,short_description.txt,full_description.txt,changelogs/<versionCode>.txt,images/{icon.png,featureGraphic.png,tvBanner.png,phoneScreenshots/,tvScreenshots/}}`.
|
||
- [F-Droid: descriptions, graphics, screenshots](https://f-droid.org/docs/All_About_Descriptions_Graphics_and_Screenshots/) — F-Droid читает ту же fastlane-структуру (`fastlane/metadata/android/` или `metadata/`), `changelogs/<versionCode>.txt` (имя — ровно versionCode), `images/tvScreenshots/`, `images/tvBanner.png`; альтернатива Triple-T (`src/main/play/`). [Inclusion How-To](https://f-droid.org/docs/Inclusion_How-To/) — RFP → `fdroiddata` merge request с `metadata/<appid>.yml`, автообновление по тегам, reproducible builds рекомендованы.
|
||
- [Obtainium](https://github.com/ImranR98/Obtainium) — обновления прямо из GitHub Releases/F-Droid/IzzyOnDroid, deep-link/бейдж для README (детали в wiki); про Android TV на странице не сказано (не проверено). GitHub Releases с APK (универсальный + per-ABI) — базовый канал для сайдлоада на TV.
|
||
|
||
---
|
||
|
||
## 7. Дизайн-инструменты
|
||
- [Design for TV](https://developer.android.com/design/ui/tv) — официальные гайды (buttons, cards, featured carousel, immersive list, lists, navigation drawer, tabs; стиль: цвет, типографика, motion, theming) и ссылка на Figma-сообщество **https://www.figma.com/@tv** (Figma отдаёт 403 для автоматического запроса — содержимое профиля не проверено).
|
||
- [Compose for TV](https://developer.android.com/training/tv/playback/compose) и [androidx.tv releases](https://developer.android.com/jetpack/androidx/releases/tv) — реализация тех же компонентов в коде (`tv-material 1.1.0`).
|
||
- [jellyfin-ux](https://github.com/jellyfin/jellyfin-ux) — логотипы (SVG/PNG), `branding/` с ассетами для Android mobile/Android TV (включая store-листинг и баннеры), лицензия CC-BY-SA-4.0. [Branding guide](https://jellyfin.org/docs/general/contributing/branding/) — **сторонним клиентам нельзя называться «Jellyfin …»** и использовать логотип без разрешения; допускается «Jellyfin client» в описании и использование фирменного градиента на собственном уникальном логотипе; не поощряются имена вида «Jelly*»/«*fin».
|
||
- [Material Theme Builder](https://material-foundation.github.io/material-theme-builder/) — страница существует (генерация M3 цветовой схемы; экспорт в Compose/Kotlin — не проверено).
|
||
- [Image Asset Studio](https://developer.android.com/studio/write/create-app-icons) — `res > New > Image Asset`: adaptive/legacy launcher icons, notification icons, Play Store 512×512; **генератора TV-баннера нет** — баннер 320×180 px (xhdpi, с текстом внутри, локализованный) рисуется вручную и кладётся в `res/drawable-xhdpi/banner.png`, подключается `android:banner` ([TV app start](https://developer.android.com/training/tv/start/start)).
|
||
|
||
---
|
||
|
||
## Рецепт: Android TV эмулятор на Linux из командной строки (все пакеты и флаги проверены по официальным источникам)
|
||
|
||
```bash
|
||
# 0. Предпосылки: KVM
|
||
egrep -c '(vmx|svm)' /proc/cpuinfo # > 0
|
||
sudo apt-get install -y qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils cpu-checker
|
||
sudo kvm-ok # "KVM acceleration can be used"
|
||
sudo usermod -aG kvm "$USER" # перелогиниться
|
||
|
||
# 1. JDK + command-line tools
|
||
sudo apt-get install -y temurin-17-jdk || sudo apt-get install -y openjdk-17-jdk # любой JDK 17+
|
||
export ANDROID_HOME="$HOME/Android/Sdk"
|
||
mkdir -p "$ANDROID_HOME/cmdline-tools"
|
||
cd /tmp && curl -LO https://dl.google.com/android/repository/commandlinetools-linux-15859902_latest.zip
|
||
unzip -q commandlinetools-linux-15859902_latest.zip -d /tmp/cmdtools
|
||
mv /tmp/cmdtools/cmdline-tools "$ANDROID_HOME/cmdline-tools/latest"
|
||
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$PATH"
|
||
|
||
# 2. Пакеты SDK (Google TV, Android 16 / API 36, x86_64)
|
||
yes | sdkmanager --licenses
|
||
sdkmanager --install "platform-tools" "emulator" "platforms;android-36" "build-tools;36.0.0" \
|
||
"system-images;android-36;google-tv;x86_64"
|
||
# альтернативы: "system-images;android-36;android-tv;x86_64" (AOSP TV),
|
||
# "system-images;android-34;google-tv;x86" (API 34 есть только в 32-bit x86 / arm64)
|
||
|
||
# 3. AVD с TV-профилем 1080p
|
||
echo no | avdmanager create avd -n tv36 -k "system-images;android-36;google-tv;x86_64" -d tv_1080p
|
||
# (-d/--device см. `avdmanager create avd --help`; профили: `avdmanager list device` -> tv_720p / tv_1080p / tv_4k)
|
||
|
||
# 4. Запуск
|
||
emulator -accel-check
|
||
emulator -avd tv36 -gpu host -memory 4096 -no-snapshot -no-boot-anim &
|
||
# headless/CI: emulator -avd tv36 -no-window -no-audio -no-boot-anim -no-snapshot -gpu software &
|
||
adb wait-for-device
|
||
until [ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ]; do sleep 2; done
|
||
|
||
# 5. Установка и управление "пультом"
|
||
./gradlew installDebug
|
||
adb -e shell am start -n org.example.jellyfinclient/.MainActivity
|
||
adb -e shell input keyevent KEYCODE_DPAD_DOWN
|
||
adb -e shell input keyevent KEYCODE_DPAD_RIGHT
|
||
adb -e shell input keyevent KEYCODE_DPAD_CENTER
|
||
adb -e shell input keyevent KEYCODE_MEDIA_PLAY_PAUSE
|
||
adb -e shell input keyevent KEYCODE_BACK
|
||
adb -e shell input keyevent KEYCODE_HOME
|
||
adb -e exec-out screencap -p > tv.png # скриншот
|
||
scrcpy -e # зеркало с клавиатурой как D-pad (стрелки/Enter/Esc)
|
||
|
||
# Эквивалент через новый Android CLI (если установлен):
|
||
# android sdk install emulator platforms/android-36
|
||
# android emulator create --list-profiles # найти TV-профиль
|
||
# android emulator start --headless <avd>
|
||
```
|
||
|
||
Примечания к рецепту: имена пакетов `system-images;android-36;google-tv;x86_64`, `...;android-tv;x86_64`, `platforms;android-36`, `build-tools;36.0.0`, `emulator`, `platform-tools` — из официальных манифестов/доков; ID профилей `tv_720p/tv_1080p/tv_4k` — из sdklib `tv.xml`; флаги `emulator` — из страницы command-line; `-accel-check` и установка KVM — из страницы acceleration. Единственное непроверенное по онлайн-документации — флаг `-d` у `avdmanager create avd` (присутствует в `--help` самого инструмента). Клавиатурная раскладка эмулятора (стрелки/Enter/Esc) — по опыту; официальный список открывается по F1 в окне эмулятора.
|
||
|
||
---
|
||
|
||
## Ключевые выводы для проекта
|
||
1. Стек на октябрь 2026: Android Studio Rabbit 1 (2026.2.1) + AGP 9.4.0 + Gradle 9.6–9.8 + Kotlin 2.4.21 + KSP 2.3.12 + Hilt 2.60.1 + Room 2.8.5 + Compose BOM 2026.09.00 + tv-material 1.1.0; JDK 17 как toolchain. При AGP 9 убрать `org.jetbrains.kotlin.android` и kapt.
|
||
2. TV-образы: x86_64 только для API 36 (Google TV / Android TV); API 35 TV-образов нет; API 34/33/31 — 32-bit x86 или arm64. Эмулятор не покрывает HW-декодирование/HDR/passthrough — нужен хотя бы один реальный девайс (Google TV Streamer или Chromecast with Google TV как эталон UX, SHIELD для аудио/видео-форматов, Fire TV Stick — Fire OS без эмулятора).
|
||
3. Для пользователя JetBrains: IDEA с AGP 9.x сейчас не рендерит Compose-превью — ставьте Android Studio через JetBrains Toolbox; для «посмотреть UI» без эмулятора — `@Preview(device = "id:tv_1080p", uiMode = UI_MODE_TYPE_TELEVISION)` + Interactive mode + screenshot-тесты (Compose Preview Screenshot Testing / Roborazzi).
|
||
4. Пульт в разработке: клавиатура эмулятора, `adb shell input keyevent`, scrcpy по `adb connect` к приставке, Google TV app на телефоне, androidtvremote2 для скриптов.
|
||
5. jellyfin-sdk-kotlin на JVM/Android использует OkHttp → Chucker/HttpLoggingInterceptor применимы; для прокси — `debug-overrides` в network security config + mitmproxy.
|
||
6. CI: `reactivecircus/android-emulator-runner` поддерживает `target: google-tv`/`android-tv` на ubuntu-раннерах с KVM; метаданные для Play/F-Droid в единой структуре `fastlane/metadata/android/` (есть `tvScreenshots`, `tvBanner`); помните об ограничениях брендинга Jellyfin на имя и логотип.
|
||
---
|
||
|
||
# Часть 7. Гибридный движок воспроизведения (дополнение от 2026-10-12)
|
||
|
||
Цель проекта уточнена: не «клиент на libVLC», а **клиент, который воспроизводит всё возможное без серверного транскода и с корректным HDR/Dolby Vision**. Полные отчёты: [media3.md](media3.md), [engines.md](engines.md), [device-xiaomi.md](device-xiaomi.md); сырые дампы тестового ТВ — [tv-dump/](tv-dump/).
|
||
|
||
## 7.1 Принятые решения
|
||
|
||
| Вопрос | Решение |
|
||
|---|---|
|
||
| Движок | Гибрид: **media3/ExoPlayer — основной** (аппаратный MediaCodec, HDR10/HDR10+/HLG/DV, passthrough), **libVLC 3.7.7 — запасной** (программный ffmpeg для того, что не тянет железо/media3). mpv и libVLC 4 — возможные будущие модули. |
|
||
| Расширяемость | Свой узкий интерфейс `PlaybackEngine` + capability-интерфейсы (AudioDelay, SubtitleDelay, Passthrough, SubtitleStyle…); каждый движок — отдельный Gradle-модуль, регистрируется через Hilt `@IntoSet`; один адаптер `SimpleBasePlayer` в ядре для MediaSession/UI. Треки адресуются индексами Jellyfin `MediaStream`. |
|
||
| Выбор движка | Автоматический, **до** `PlaybackInfo`: данные файла (контейнер, кодек, профиль, битность, `VideoRangeType`, DV-профиль, субтитры, аудио) × возможности устройства (`MediaCodecList`, HDR-типы дисплея, `AudioCapabilities`, `RendererCapabilities` media3). Порядок: ручной выбор → media3 Direct → вариант с наименьшими потерями (HDR важнее всего) → серверный транскод. DeviceProfile — свой для каждого движка. |
|
||
| Runtime-fallback | Как в Plezy: при ошибке декодера / зависании / 0 кадров — один переход на другой движок с сохранением позиции и треков; если упал и он — транскод. Ни один Jellyfin-клиент этого не делает — это ключевое отличие проекта. |
|
||
| Сервер / SDK | Только Jellyfin 12.x, `jellyfin-sdk-kotlin` 1.9.x. |
|
||
| Платформа | Сначала Android TV, общие модули для будущего phone-UI. |
|
||
| DI / лицензия | Hilt (позже заменён на Koin); GPL-3.0-or-later; собственное название без Jellyfin-бренда. |
|
||
| Сборки | `armeabi-v7a` обязателен (тестовый ТВ — 32-bit userspace), сплиты по ABI; flavors `full` (Exo+VLC) / `lite` (только Exo). Dynamic Feature Delivery не подходит (sideload/F-Droid). |
|
||
|
||
## 7.2 media3: что играет и что нет (кратко)
|
||
|
||
- **Да:** HDR10/HDR10+/HLG, DV в MKV и MP4 (`dvcC`/`dvvC`), passthrough AC3/E-AC3/JOC/TrueHD/DTS/DTS-HD, ASS через `io.github.peerless2012:ass-media` (libass), PGS/VobSub/DVB внутри MKV, AVI. Вывод только через `SurfaceView` (Compose `PlayerSurface` с `SURFACE_TYPE_SURFACE_VIEW`).
|
||
- **Нет (→ libVLC):** M2TS, ASF/WMV, ISO/VIDEO_TS/BDMV, MKV с zlib, ordered chapters; H.264 Hi10P; VC-1/MPEG-2/MPEG-4 ASP без HW-декодера; TrueHD/PGS/VC-1 в TS; внешние `.sup`/`.idx`. FFmpeg-расширение media3 — только аудио (`org.jellyfin.media3:media3-ffmpeg-decoder`).
|
||
- **Риски:** DV P7 без DV-декодера не имеет фолбэка на HEVC (нужен свой `MediaCodecSelector` или `DOVI_WITH_EL` как неподдерживаемый); DV P8.1+HDR10+ виснет на MediaTek (#1895); падения HEVC-декодера MTK посреди потока не ловятся `setEnableDecoderFallback`; смену частоты/HDR-режима дисплея делать самим (`preferredDisplayModeId` + ожидание `onDisplayChanged`); все PGS-дорожки декодируются даже невыбранные (#2667, память). Tunneling — опция, по умолчанию выкл.
|
||
- jellyfin-androidtv объявляет в direct play ASF/WMV/VOB, которые media3 не открывает, — вероятный источник ошибок официального клиента.
|
||
|
||
## 7.3 libVLC vs libmpv как запасной
|
||
|
||
libVLC 3.7.7 выбран: официальный артефакт VideoLAN с регулярными релизами, стабильный Java API, libass/PGS/VobSub внутри, passthrough, выравнивание 16 KB есть. libmpv (`dev.jdtech.mpv:libmpv`) меньше (≈25 МБ против 55 МБ несжатых `.so` на arm64), но артефакт неофициальный, а HDR в стоковом mpv-android уходит в SDR через GLES. Для VLC при `useLegacyPackaging=true` размер ≈ +24 МБ.
|
||
|
||
## 7.4 Тестовый ТВ: Xiaomi TV S Pro Mini LED 75 (снято через adb 2026-10-12)
|
||
|
||
| Параметр | Значение |
|
||
|---|---|
|
||
| Модель / ОС | `MiTV-MOEU1` (`lalaland`), Android 14 / API 34, `leanback_only` |
|
||
| SoC | MediaTek **MT9689** (`ro.hardware=mt5896`, платформа `merak`) |
|
||
| ABI | **только `armeabi-v7a`** (32-bit userspace) |
|
||
| RAM | 2,4 ГБ видно системе; `MemAvailable` ≈ 870 МБ (MemFree ≈ 60–240 МБ вводит в заблуждение — остальное кэш), swap 600 МБ почти занят — бюджет памяти умеренно жёсткий; хранилище 22 ГБ свободно |
|
||
| UI / дисплей | приложения рисуются в 1920×1080 (320 dpi), видеоплоскость 4K; Android видит **один режим 3840×2160@60** — переключение частоты через Android API недоступно; есть Xiaomi-настройка `mitv_vrr_frame_rate_enable_23` (не исследовано) |
|
||
| HDR дисплея | DV, HDR10, HLG, HDR10+ |
|
||
| HW-видео (`media_codecs-mt5896.xml`) | AVC/HEVC/VP9/AV1 до 8K; MPEG-2, VC-1, WMV3, MPEG-4, H.263, RealVideo, AVS/AVS2 до 1080p; DV-декодеры `dvav.per/pen`, `dvhe.der/den/dtr/stn/dth/dtb/st`, `dvav.se`, `dvav1` — т.е. профили 1–10, **включая P7 (`dvhe.dtb`)** |
|
||
| Аудио | Dolby MS12; выходы заявляют AC3, E-AC3, E-AC3-JOC, TrueHD, DTS, DTS-HD; сейчас активен только `speaker` |
|
||
|
||
Расхождения с веб-исследованием: исследование по открытым источникам предполагало отсутствие P7 и VC-1 в железе — дамп устройства это опровергает (наличие в XML ≠ гарантированная работа через MediaCodecList; проверить в прототипе).
|
||
|
||
Известные проблемы этого семейства (проверить на устройстве): jellyfin-androidtv #5825 — на Xiaomi S Pro (кит. HyperOS) media3-клиенты не включают DV, а Just Player включает; stutter `c2.mtk.hevc.decoder` (media3 #3347); вечная буферизация при AC-3 passthrough после быстрых seek на MTK audio HAL (#3407); искажённый E-AC3 passthrough на Xiaomi (jellyfin #5366); MTK DV-декодеру нужен 4-байтный start code для NAL 62/63 (квирк Kodi).
|
||
|
||
## 7.5 Что проверить первым прототипом
|
||
|
||
1. DV P5, P8.1, P7 (MEL/FEL), P8.1+HDR10+ через media3 — включается ли DV-режим ТВ (кейс #5825).
|
||
2. HDR10 / HDR10+ / HLG; возврат дисплея из HDR после выхода.
|
||
3. 24p-контент: есть ли judder при фиксированных 60 Гц; что делает `Surface.setFrameRate`.
|
||
4. VC-1 и MPEG-2 через media3+MediaCodec vs libVLC; H.264 Hi10P через libVLC (хватит ли CPU).
|
||
5. ASS со шрифтами (ass-media vs libVLC), PGS, VobSub.
|
||
6. Звук: динамики ТВ и (если есть) eARC — passthrough E-AC3/JOC, TrueHD, DTS-HD; быстрые seek при passthrough.
|
||
7. M2TS / WMV / ISO через libVLC; runtime-переход Exo → VLC при сбое.
|
||
8. Потребление памяти обоих движков на 2,4 ГБ RAM.
|