From c01ed644b21e938c8bba054f5da2dd3d273ecb07 Mon Sep 17 00:00:00 2001 From: Jarne Demeulemeester Date: Sat, 22 Jun 2024 18:21:24 +0200 Subject: [PATCH] feat: native 10.9 trickplay (#763) * feat: native trickplay TODO: update downloaded trickplay data * chore: fix tv build * fix: set dispatcher on image loading to remove flicker * feat: download trickplay data * refactor: simplify trickplay info by only loading a single resolution * refactor: follow jellyfin naming of trickplay --- .../dev/jdtech/jellyfin/PlayerActivity.kt | 6 +- .../jellyfin/utils/PreviewScrubListener.kt | 13 +- .../dev/jdtech/jellyfin/ui/dummy/Episodes.kt | 1 + .../dev/jdtech/jellyfin/ui/dummy/Movies.kt | 1 + .../jdtech/jellyfin/utils/DownloaderImpl.kt | 68 +- core/src/main/res/values-b+es+419/strings.xml | 2 - core/src/main/res/values-bg/strings.xml | 2 - core/src/main/res/values-de/strings.xml | 2 - core/src/main/res/values-es-rMX/strings.xml | 2 - core/src/main/res/values-es/strings.xml | 2 - core/src/main/res/values-fr/strings.xml | 2 - core/src/main/res/values-hu/strings.xml | 2 - core/src/main/res/values-it/strings.xml | 2 - core/src/main/res/values-iw/strings.xml | 2 - core/src/main/res/values-ko/strings.xml | 2 - core/src/main/res/values-nl/strings.xml | 2 - core/src/main/res/values-pl/strings.xml | 2 - core/src/main/res/values-pt-rBR/strings.xml | 2 - core/src/main/res/values-pt/strings.xml | 2 - core/src/main/res/values-ro/strings.xml | 1 - core/src/main/res/values-ru/strings.xml | 2 - core/src/main/res/values-sk/strings.xml | 2 - core/src/main/res/values-sl/strings.xml | 1 - core/src/main/res/values-sv/strings.xml | 2 - core/src/main/res/values-uk/strings.xml | 2 - core/src/main/res/values-vi/strings.xml | 2 - core/src/main/res/values-zh-rCN/strings.xml | 2 - core/src/main/res/values-zh-rTW/strings.xml | 2 - core/src/main/res/values/strings.xml | 4 +- .../main/res/xml/fragment_settings_player.xml | 6 +- .../5.json | 873 ++++++++++++++++++ .../dev/jdtech/jellyfin/api/JellyfinApi.kt | 2 + .../jellyfin/database/ServerDatabase.kt | 12 +- .../jellyfin/database/ServerDatabaseDao.kt | 17 +- .../jdtech/jellyfin/models/FindroidEpisode.kt | 12 +- .../jdtech/jellyfin/models/FindroidMovie.kt | 10 + .../jdtech/jellyfin/models/FindroidSources.kt | 1 + .../jellyfin/models/FindroidTrickplayInfo.kt | 37 + .../models/FindroidTrickplayInfoDto.kt | 41 + .../jellyfin/models/TrickPlayManifest.kt | 19 - .../jellyfin/models/TrickPlayManifestDto.kt | 21 - .../jellyfin/repository/JellyfinRepository.kt | 5 +- .../repository/JellyfinRepositoryImpl.kt | 50 +- .../JellyfinRepositoryOfflineImpl.kt | 21 +- .../dev/jdtech/jellyfin/models/PlayerItem.kt | 1 + .../dev/jdtech/jellyfin/models/Trickplay.kt | 8 + .../jdtech/jellyfin/models/TrickplayInfo.kt | 15 + .../dev/jdtech/jellyfin/utils/bif/BifData.kt | 11 - .../jellyfin/utils/bif/BifIndexEntry.kt | 3 - .../dev/jdtech/jellyfin/utils/bif/BifUtil.kt | 77 -- .../jellyfin/utils/bif/Indexed8Array.kt | 37 - .../viewmodels/PlayerActivityViewModel.kt | 57 +- .../jellyfin/viewmodels/PlayerViewModel.kt | 21 +- .../dev/jdtech/jellyfin/AppPreferences.kt | 2 +- .../java/dev/jdtech/jellyfin/Constants.kt | 2 +- 55 files changed, 1145 insertions(+), 353 deletions(-) create mode 100644 data/schemas/dev.jdtech.jellyfin.database.ServerDatabase/5.json create mode 100644 data/src/main/java/dev/jdtech/jellyfin/models/FindroidTrickplayInfo.kt create mode 100644 data/src/main/java/dev/jdtech/jellyfin/models/FindroidTrickplayInfoDto.kt delete mode 100644 data/src/main/java/dev/jdtech/jellyfin/models/TrickPlayManifest.kt delete mode 100644 data/src/main/java/dev/jdtech/jellyfin/models/TrickPlayManifestDto.kt create mode 100644 player/core/src/main/java/dev/jdtech/jellyfin/models/Trickplay.kt create mode 100644 player/core/src/main/java/dev/jdtech/jellyfin/models/TrickplayInfo.kt delete mode 100644 player/core/src/main/java/dev/jdtech/jellyfin/utils/bif/BifData.kt delete mode 100644 player/core/src/main/java/dev/jdtech/jellyfin/utils/bif/BifIndexEntry.kt delete mode 100644 player/core/src/main/java/dev/jdtech/jellyfin/utils/bif/BifUtil.kt delete mode 100644 player/core/src/main/java/dev/jdtech/jellyfin/utils/bif/Indexed8Array.kt diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index b5a03750..e21c79b3 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -141,9 +141,9 @@ class PlayerActivity : BasePlayerActivity() { } } - // Trick Play + // Trickplay previewScrubListener?.let { - it.currentTrickPlay = currentTrickPlay + it.currentTrickplay = currentTrickplay } // Chapters @@ -259,7 +259,7 @@ class PlayerActivity : BasePlayerActivity() { val timeBar = binding.playerView.findViewById(R.id.exo_progress) timeBar.setAdMarkerColor(Color.WHITE) - if (appPreferences.playerTrickPlay) { + if (appPreferences.playerTrickplay) { val imagePreview = binding.playerView.findViewById(R.id.image_preview) previewScrubListener = PreviewScrubListener( imagePreview, diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/utils/PreviewScrubListener.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/utils/PreviewScrubListener.kt index bfe26e63..153f528b 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/utils/PreviewScrubListener.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/utils/PreviewScrubListener.kt @@ -8,8 +8,8 @@ import androidx.media3.common.Player import androidx.media3.ui.TimeBar import coil.load import coil.transform.RoundedCornersTransformation -import dev.jdtech.jellyfin.utils.bif.BifData -import dev.jdtech.jellyfin.utils.bif.BifUtil +import dev.jdtech.jellyfin.models.Trickplay +import kotlinx.coroutines.Dispatchers import timber.log.Timber class PreviewScrubListener( @@ -17,14 +17,14 @@ class PreviewScrubListener( private val timeBarView: View, private val player: Player, ) : TimeBar.OnScrubListener { - var currentTrickPlay: BifData? = null + var currentTrickplay: Trickplay? = null private val roundedCorners = RoundedCornersTransformation(10f) private var currentBitMap: Bitmap? = null override fun onScrubStart(timeBar: TimeBar, position: Long) { Timber.d("Scrubbing started at $position") - if (currentTrickPlay == null) { + if (currentTrickplay == null) { return } @@ -35,8 +35,8 @@ class PreviewScrubListener( override fun onScrubMove(timeBar: TimeBar, position: Long) { Timber.d("Scrubbing to $position") - val currentBifData = currentTrickPlay ?: return - val image = BifUtil.getTrickPlayFrame(position.toInt(), currentBifData) ?: return + val trickplay = currentTrickplay ?: return + val image = trickplay.images[position.div(trickplay.interval).toInt()] val parent = scrubbingPreview.parent as ViewGroup @@ -57,6 +57,7 @@ class PreviewScrubListener( if (currentBitMap != image) { scrubbingPreview.load(image) { + dispatcher(Dispatchers.Main.immediate) transformations(roundedCorners) } currentBitMap = image diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/dummy/Episodes.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/dummy/Episodes.kt index 8775f291..cd35d0a1 100644 --- a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/dummy/Episodes.kt +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/dummy/Episodes.kt @@ -56,6 +56,7 @@ val dummyEpisode = FindroidEpisode( communityRating = 9.2f, images = FindroidImages(), chapters = null, + trickplayInfo = null, ) val dummyEpisodes = listOf( diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/dummy/Movies.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/dummy/Movies.kt index f6598e39..4817b95b 100644 --- a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/dummy/Movies.kt +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/dummy/Movies.kt @@ -56,6 +56,7 @@ val dummyMovie = FindroidMovie( trailer = "https://www.youtube.com/watch?v=puKWa8hrvA8", images = FindroidImages(), chapters = null, + trickplayInfo = null, ) val dummyMovies = listOf( diff --git a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt index 0b670493..9b0d2090 100644 --- a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt +++ b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt @@ -13,7 +13,8 @@ import dev.jdtech.jellyfin.models.FindroidEpisode import dev.jdtech.jellyfin.models.FindroidItem import dev.jdtech.jellyfin.models.FindroidMovie import dev.jdtech.jellyfin.models.FindroidSource -import dev.jdtech.jellyfin.models.TrickPlayManifest +import dev.jdtech.jellyfin.models.FindroidSources +import dev.jdtech.jellyfin.models.FindroidTrickplayInfo import dev.jdtech.jellyfin.models.UiText import dev.jdtech.jellyfin.models.toFindroidEpisodeDto import dev.jdtech.jellyfin.models.toFindroidMediaStreamDto @@ -21,13 +22,14 @@ import dev.jdtech.jellyfin.models.toFindroidMovieDto import dev.jdtech.jellyfin.models.toFindroidSeasonDto import dev.jdtech.jellyfin.models.toFindroidShowDto import dev.jdtech.jellyfin.models.toFindroidSourceDto +import dev.jdtech.jellyfin.models.toFindroidTrickplayInfoDto import dev.jdtech.jellyfin.models.toFindroidUserDataDto import dev.jdtech.jellyfin.models.toIntroDto -import dev.jdtech.jellyfin.models.toTrickPlayManifestDto import dev.jdtech.jellyfin.repository.JellyfinRepository import java.io.File import java.util.UUID import kotlin.Exception +import kotlin.math.ceil import dev.jdtech.jellyfin.core.R as CoreR class DownloaderImpl( @@ -46,12 +48,8 @@ class DownloaderImpl( try { val source = jellyfinRepository.getMediaSources(item.id, true).first { it.id == sourceId } val intro = jellyfinRepository.getIntroTimestamps(item.id) - val trickPlayManifest = jellyfinRepository.getTrickPlayManifest(item.id) - val trickPlayData = if (trickPlayManifest != null) { - jellyfinRepository.getTrickPlayData( - item.id, - trickPlayManifest.widthResolutions.max(), - ) + val trickplayInfo = if (item is FindroidSources) { + item.trickplayInfo?.get(sourceId) } else { null } @@ -78,12 +76,12 @@ class DownloaderImpl( database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty())) database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId())) downloadExternalMediaStreams(item, source, storageIndex) + if (trickplayInfo != null) { + downloadTrickplayData(item.id, sourceId, trickplayInfo) + } if (intro != null) { database.insertIntro(intro.toIntroDto(item.id)) } - if (trickPlayManifest != null && trickPlayData != null) { - downloadTrickPlay(item, trickPlayManifest, trickPlayData) - } val request = DownloadManager.Request(source.path.toUri()) .setTitle(item.name) .setAllowedOverMetered(appPreferences.downloadOverMobileData) @@ -107,12 +105,12 @@ class DownloaderImpl( database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty())) database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId())) downloadExternalMediaStreams(item, source, storageIndex) + if (trickplayInfo != null) { + downloadTrickplayData(item.id, sourceId, trickplayInfo) + } if (intro != null) { database.insertIntro(intro.toIntroDto(item.id)) } - if (trickPlayManifest != null && trickPlayData != null) { - downloadTrickPlay(item, trickPlayManifest, trickPlayData) - } val request = DownloadManager.Request(source.path.toUri()) .setTitle(item.name) .setAllowedOverMetered(appPreferences.downloadOverMobileData) @@ -175,8 +173,7 @@ class DownloaderImpl( database.deleteIntro(item.id) - database.deleteTrickPlayManifest(item.id) - File(context.filesDir, "trickplay/${item.id}.bif").delete() + File(context.filesDir, "trickplay/${item.id}").deleteRecursively() } override suspend fun getProgress(downloadId: Long?): Pair { @@ -233,14 +230,37 @@ class DownloaderImpl( } } - private fun downloadTrickPlay( - item: FindroidItem, - trickPlayManifest: TrickPlayManifest, - byteArray: ByteArray, + private suspend fun downloadTrickplayData( + itemId: UUID, + sourceId: String, + trickplayInfo: FindroidTrickplayInfo, ) { - database.insertTrickPlayManifest(trickPlayManifest.toTrickPlayManifestDto(item.id)) - File(context.filesDir, "trickplay").mkdirs() - val file = File(context.filesDir, "trickplay/${item.id}.bif") - file.writeBytes(byteArray) + val maxIndex = ceil(trickplayInfo.thumbnailCount.toDouble().div(trickplayInfo.tileWidth * trickplayInfo.tileHeight)).toInt() + val byteArrays = mutableListOf() + for (i in 0..maxIndex) { + jellyfinRepository.getTrickplayData( + itemId, + trickplayInfo.width, + i, + )?.let { byteArray -> + byteArrays.add(byteArray) + } + } + saveTrickplayData(itemId, sourceId, trickplayInfo, byteArrays) + } + + private fun saveTrickplayData( + itemId: UUID, + sourceId: String, + trickplayInfo: FindroidTrickplayInfo, + byteArrays: List, + ) { + val basePath = "trickplay/$itemId/$sourceId" + database.insertTrickplayInfo(trickplayInfo.toFindroidTrickplayInfoDto(sourceId)) + File(context.filesDir, basePath).mkdirs() + for ((i, byteArray) in byteArrays.withIndex()) { + val file = File(context.filesDir, "$basePath/$i") + file.writeBytes(byteArray) + } } } diff --git a/core/src/main/res/values-b+es+419/strings.xml b/core/src/main/res/values-b+es+419/strings.xml index 379b1570..7e9cebcf 100644 --- a/core/src/main/res/values-b+es+419/strings.xml +++ b/core/src/main/res/values-b+es+419/strings.xml @@ -138,7 +138,6 @@ Requiere que el complemento Intro Skipper de ConfusedPolarBear esté instalado en el servidor %1$d-%2$d. %3$s T%1$d:E%2$d-%3$d - %4$s - Requiere que el complemento Jellyscrub de nicknsy esté instalado en el servidor Mostrar información detallada de audio, video y subtítulos Modo desconectado Icono de modo desconectado @@ -161,7 +160,6 @@ Gesto de búsqueda Deslizar horizontalmente para buscar adelante o atrás Indicador de descargado - Miniaturas (Trick Play) %1$s (%2$d MB libres) Preparando descarga Cancelar descarga diff --git a/core/src/main/res/values-bg/strings.xml b/core/src/main/res/values-bg/strings.xml index cab09c09..96cacc2d 100644 --- a/core/src/main/res/values-bg/strings.xml +++ b/core/src/main/res/values-bg/strings.xml @@ -142,7 +142,6 @@ Мрежа Лимит на сокета (ms) Хардуерно декодиране - Изисква Jellyscrub на nicknsy да бъде инсталиран на сървъра Добави адрес Аудио Видео @@ -161,7 +160,6 @@ Изберете версия Тъмна Пропускане на интрота - Trick Play Адреси Добави адрес на сървър Жест за приближаване (zoom) diff --git a/core/src/main/res/values-de/strings.xml b/core/src/main/res/values-de/strings.xml index 7c2e24ca..d3af014e 100644 --- a/core/src/main/res/values-de/strings.xml +++ b/core/src/main/res/values-de/strings.xml @@ -136,8 +136,6 @@ Hardware-Dekodierung Videoausgang Audioausgang - Trickspiel - Erfordert die Installation des Jellyscrub-Plugins von nicknsy auf dem Server CC Anzeigen von detaillierten Infos zu Ton/Audio, Video und Untertiteln Video diff --git a/core/src/main/res/values-es-rMX/strings.xml b/core/src/main/res/values-es-rMX/strings.xml index 3f6eb4d6..abd46361 100644 --- a/core/src/main/res/values-es-rMX/strings.xml +++ b/core/src/main/res/values-es-rMX/strings.xml @@ -149,14 +149,12 @@ Gesto de búsqueda Deslice horizontalmente para buscar hacia adelante o hacia atrás Idioma de la aplicación - Requiere que el complemento Jellyscrub de nicknsy esté instalado en el servidor Cancelar descarga ¿Quiere cancelar la descarga\? Subtítulos CC temporáneo Indicador de descargado - Miniaturas (Trick Play) Preparando la descarga %1$s (%2$d MB libres) Éste elemento requiere %1$s de almacenamiento pero solo hay disponible %2$s diff --git a/core/src/main/res/values-es/strings.xml b/core/src/main/res/values-es/strings.xml index d185a4bf..e0e9396c 100644 --- a/core/src/main/res/values-es/strings.xml +++ b/core/src/main/res/values-es/strings.xml @@ -147,8 +147,6 @@ temporáneo Muestra información detallada de audio, video y subtítulos Mostrar info extra - Miniaturas (Trick Play) - Requiere que el plugin Jellyscrub de nicknsy esté instalado en el servidor Modo sin conexión Icono de Modo sin conexión Conectarse diff --git a/core/src/main/res/values-fr/strings.xml b/core/src/main/res/values-fr/strings.xml index 2a5b78a9..f12287a9 100644 --- a/core/src/main/res/values-fr/strings.xml +++ b/core/src/main/res/values-fr/strings.xml @@ -142,7 +142,6 @@ Ajouter une adresse Ajouter l\'adresse d\'un serveur Ajouter - Nécessite que le plugin Jellyscrub de Nicknsy soit installé sur le serveur temporaire CC Déplacement du curseur de lecture @@ -166,7 +165,6 @@ Aucune connection au serveur Jellyfin, pour regarder hors-ligne activer ce mode Êtes-vous sûr de vouloir arrêter le téléchargement \? Langue de l\'application - Lecture Spéciale Mode hors-ligne Préparation du téléchargement Indicateur de téléchargements diff --git a/core/src/main/res/values-hu/strings.xml b/core/src/main/res/values-hu/strings.xml index 590a5dd1..1fdbfd64 100644 --- a/core/src/main/res/values-hu/strings.xml +++ b/core/src/main/res/values-hu/strings.xml @@ -137,7 +137,6 @@ Könyvtárak Biztosan el akarod távolítani a következő felhasználót: %1$s Letöltött indikátor - nicknsy Jellyscrub bővítményének telepítve kell legyen a szerveren %1$d-%2$d. %3$s S%1$d:E%2$d-%3$d - %4$s Váltás online módra @@ -170,7 +169,6 @@ Felirat Szerver címének eltávolítása Biztosan el akarod távolítani a %1$s szervert - Trükkös játék Gesztus keresése Kép a képben Kép a képben otthoni gesztus diff --git a/core/src/main/res/values-it/strings.xml b/core/src/main/res/values-it/strings.xml index a320a594..c992a47f 100644 --- a/core/src/main/res/values-it/strings.xml +++ b/core/src/main/res/values-it/strings.xml @@ -147,8 +147,6 @@ Mostra più informazioni Tema scuro AMOLED Usa il tema AMOLED con lo sfondo nero - Anteprima - Richiede il plugin Jellyscrub di nicknsy installato sul server Dimensione Utilizzando Findroid accetti l\'informativa sulla privacy che afferma che non raccogliamo alcun dato %1$d-%2$d. %3$s diff --git a/core/src/main/res/values-iw/strings.xml b/core/src/main/res/values-iw/strings.xml index af8dd55c..2ffdbc37 100644 --- a/core/src/main/res/values-iw/strings.xml +++ b/core/src/main/res/values-iw/strings.xml @@ -147,7 +147,6 @@ צבוט כדי להציג את הוידאו במסך מלא גודל קפיצה אחורה (מילי שניות) השתמש בצבעים דינמיים של Material You (זמין רק בגרסת אנדרואיד 12 ומעלה) - דורש תוסף Jellyscrub של nicknsy מותקן על השרת גודל סמל מצב לא מקוון פריט זה דורש %1$s של מקום פנוי אבל רק %2$s זמינים @@ -171,7 +170,6 @@ מחוון הורדה הסר כתובת שרת האם אתה בטוח שברצונך להסיר את כתובת השרת %1$s - Trick Play תמונה-בתוך-תמונה השתמש בכפתור הבית או במחווה כדי להכנס למצב תמונה-בתוך-תמונה כאשר הוידאו פועל \ No newline at end of file diff --git a/core/src/main/res/values-ko/strings.xml b/core/src/main/res/values-ko/strings.xml index a0af933f..80303955 100644 --- a/core/src/main/res/values-ko/strings.xml +++ b/core/src/main/res/values-ko/strings.xml @@ -136,8 +136,6 @@ Quick Connect Intro Skipper 기능 서버에 ConfusedPolarBear의 Intro Skipper 플러그인이 설치되어 있어야 합니다. - nicknsy\'s Jellyscrub 플러그인이 서버에 설치되어 있어야 합니다 - Trick Play 내부 %1$s (%2$d MB 사용 가능) 외부 diff --git a/core/src/main/res/values-nl/strings.xml b/core/src/main/res/values-nl/strings.xml index 4132b7f9..dd664fcd 100644 --- a/core/src/main/res/values-nl/strings.xml +++ b/core/src/main/res/values-nl/strings.xml @@ -161,14 +161,12 @@ AMOLED donker thema Gebruik AMOLED thema met een compleet zwarte achtergrond Fout tijdens het downloaden - Verzoek voor nicknsy\'s Jellyscrub te installeren op de server tijdelijk Intro Overslaan CC %1$d-%2$d. %3$s S%1$d:E%2$d-%3$d - %4$s Gedownloade indicator - Trick Play Zoek gebaar Geen gebruikers gevonden Scherm-in-scherm diff --git a/core/src/main/res/values-pl/strings.xml b/core/src/main/res/values-pl/strings.xml index 9c2066b8..1a63caa5 100644 --- a/core/src/main/res/values-pl/strings.xml +++ b/core/src/main/res/values-pl/strings.xml @@ -147,8 +147,6 @@ Wyświetla szczegółowe informacje o audio, wideo i napisach Ciemny motyw AMOLED Użyj motywu AMOLED z czystym czarnym tłem - Trick Play - Wymaga zainstalowania na serwerze wtyczki Nicknsy\'s Jellyscrub %1$d-%2$d. %3$s S%1$d:E%2$d-%3$d - %4$s Tryb offline diff --git a/core/src/main/res/values-pt-rBR/strings.xml b/core/src/main/res/values-pt-rBR/strings.xml index defb1f50..de56b64c 100644 --- a/core/src/main/res/values-pt-rBR/strings.xml +++ b/core/src/main/res/values-pt-rBR/strings.xml @@ -147,8 +147,6 @@ Use o tema AMOLED com um fundo preto Gesto de buscar Deslize horizontalmente para buscar para frente ou para trás - Requer que o plug-in Jellyscrub do nicknsy seja instalado no servidor - Miniatura de pré-visualização Ícone do modo offline Este item requer %1$s de armazenamento livre, mas apenas %2$s está disponível Tem certeza de que deseja cancelar o download\? diff --git a/core/src/main/res/values-pt/strings.xml b/core/src/main/res/values-pt/strings.xml index c1e824ec..17c5e2ee 100644 --- a/core/src/main/res/values-pt/strings.xml +++ b/core/src/main/res/values-pt/strings.xml @@ -125,14 +125,12 @@ Saida de video Saída de áudio Adicionar endereço - Jogo de truque %1$d. %2$s S%1$d:E%2$d - %3$s Tem certeza de que deseja remover o endereço do servidor %1$s %1$d minutos Capitão de introdução Requer que o plugin Confused Polar Bears Intro Skipper esteja instalado no servidor - Requer que o plugin Jellyscrub do Nicknsy esteja instalado no servidor Adicionar endereço do servidor Procure gesto Deslize horizontalmente para avançar ou retroceder diff --git a/core/src/main/res/values-ro/strings.xml b/core/src/main/res/values-ro/strings.xml index fa6adc89..6dbf5d3f 100644 --- a/core/src/main/res/values-ro/strings.xml +++ b/core/src/main/res/values-ro/strings.xml @@ -80,7 +80,6 @@ Ieșire video Ieșire audio Necesită ca pluginul IntroSkipper de ConfusedPolarBear să fie instalat pe server - Necesită ca pluginul Jellyscrub de nicknsy să fie instalat pe server Adrese Adaugă o adresă Adaugă o adresă de server diff --git a/core/src/main/res/values-ru/strings.xml b/core/src/main/res/values-ru/strings.xml index 9f9f6207..732548a9 100644 --- a/core/src/main/res/values-ru/strings.xml +++ b/core/src/main/res/values-ru/strings.xml @@ -136,8 +136,6 @@ Время ожидания запроса (мс) Время ожидания соединения (мс) Время ожидания сокета (мс) - Требуется, чтобы на сервере был установлен плагин Jellyscrub от nicknsy - Миниатюры предпросмотра (Trick Play) Субтитры Показывает дополнительную информацию о Аудио, Видео и Субтитрах Показывать дополнительную информацию diff --git a/core/src/main/res/values-sk/strings.xml b/core/src/main/res/values-sk/strings.xml index b44c710a..4e6fee3e 100644 --- a/core/src/main/res/values-sk/strings.xml +++ b/core/src/main/res/values-sk/strings.xml @@ -147,8 +147,6 @@ dočasné AMOLED temný motív Použiť AMOLED motív s úplne čiernym pozadím - Miniatúry (Trick Play) - Potrebuje aby bol na serveri nainštalovaný Jellyscrub plugin od nicknsy %1$d:%2$d. %3$s S%1$d:E%2$d-%3$d - %4$s Offline Režim diff --git a/core/src/main/res/values-sl/strings.xml b/core/src/main/res/values-sl/strings.xml index eb762f8b..7e490843 100644 --- a/core/src/main/res/values-sl/strings.xml +++ b/core/src/main/res/values-sl/strings.xml @@ -139,7 +139,6 @@ [%1$s] %2$s (%3$s) Časovna omejitev povezave (ms) Ali ste prepričani, da želite preklicati prenos\? - Zahteva namestitev vtičnika nicknsy Jellyscrub na strežniku Velikost Način brez povezave izhod iz načina brez povezave diff --git a/core/src/main/res/values-sv/strings.xml b/core/src/main/res/values-sv/strings.xml index 12a1d938..6e4feba5 100644 --- a/core/src/main/res/values-sv/strings.xml +++ b/core/src/main/res/values-sv/strings.xml @@ -93,7 +93,6 @@ Lägg till användare Videooutput Ljudoutput - Trickspel Adresser Lägg till adress Lägg till serveradress @@ -146,7 +145,6 @@ Hårdvaruavkodning Introskippare Kräver ConfusedPolarBears Intro Skipper-plugin installerat på servern - Kräver nicknsys Jellyscrub-plugin installerat på servern Serverversionen är ej aktuell: %1$s. Vänligen uppdatera din server Serverversion stöds ej: %1$s Vänligen uppdatera din server \ No newline at end of file diff --git a/core/src/main/res/values-uk/strings.xml b/core/src/main/res/values-uk/strings.xml index 59850ace..69ae746b 100644 --- a/core/src/main/res/values-uk/strings.xml +++ b/core/src/main/res/values-uk/strings.xml @@ -100,8 +100,6 @@ Виведення аудіо Intro Skipper (Пропуск інтро) Потребує встановлення на сервері плагіна Intro Skipper від ConfusedPolarBear - Мініатюри попереднього перегляду (Trick Play) - Потрібно встановити на сервері плагін Jellyscrub від nicknsy Адреси Додати адресу Додати адресу сервера diff --git a/core/src/main/res/values-vi/strings.xml b/core/src/main/res/values-vi/strings.xml index 40cb1350..0c243597 100644 --- a/core/src/main/res/values-vi/strings.xml +++ b/core/src/main/res/values-vi/strings.xml @@ -156,9 +156,7 @@ tạm Hiện thị thêm thông tin Video - Tua mượt mà Hiển thị thêm các thông tin về âm thanh, video và phụ đề - Yêu cầu phần mở rộng Jellyscrub của nicknsy đã được cài đặt trên máy chủ %1$s (%2$d MB trống) Chủ đề màu tối (AMOLED) Sử dụng chủ đề cho màn hình AMOLED với nền màu đen tuyệt đối diff --git a/core/src/main/res/values-zh-rCN/strings.xml b/core/src/main/res/values-zh-rCN/strings.xml index 81cd8ca5..4f5fcba0 100644 --- a/core/src/main/res/values-zh-rCN/strings.xml +++ b/core/src/main/res/values-zh-rCN/strings.xml @@ -145,8 +145,6 @@ 显示额外信息 显示关于音频、视频和字幕的详细信息 temp - 跳转预览 - 需要在服务器上安装 nicknsy 的 Jellyscrub 插件 AMOLED 深色模式 使用带有纯黑背景的 AMOLED 主题 上线 diff --git a/core/src/main/res/values-zh-rTW/strings.xml b/core/src/main/res/values-zh-rTW/strings.xml index c6487364..da1d9c6e 100644 --- a/core/src/main/res/values-zh-rTW/strings.xml +++ b/core/src/main/res/values-zh-rTW/strings.xml @@ -138,7 +138,6 @@ 添加 %1$d-%2$d. %3$s S%1$d:E%2$d-%3$d - %4$s - 需要在服務器上安裝 nicknsy 的 Jellyscrub 插件 下載時出錯 大小 未連接到 Jellyfin 服務器,要離線觀看請啟用離線模式 @@ -173,7 +172,6 @@ 影片播放時使用主頁按鈕或手勢進入畫中畫 刪除伺服器位址 您確定要刪除伺服器位址嗎%1$s - 跳轉預覽 臨時文件 未找到伺服器 未找到相應的用戶 diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 5fba35ff..b00c4f89 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -146,8 +146,8 @@ Audio output Intro Skipper Requires ConfusedPolarBear\'s Intro Skipper plugin to be installed on the server - Trick Play - Requires nicknsy\'s Jellyscrub plugin to be installed on the server + Trickplay + Display preview images while scrubbing Chapter markers Display chapter markers on the timebar Addresses diff --git a/core/src/main/res/xml/fragment_settings_player.xml b/core/src/main/res/xml/fragment_settings_player.xml index d70eef5b..d9336467 100644 --- a/core/src/main/res/xml/fragment_settings_player.xml +++ b/core/src/main/res/xml/fragment_settings_player.xml @@ -98,9 +98,9 @@ @@ -270,4 +261,10 @@ interface ServerDatabaseDao { @Query("SELECT * FROM episodes WHERE serverId = :serverId AND name LIKE '%' || :name || '%'") fun searchEpisodes(serverId: String, name: String): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertTrickplayInfo(trickplayInfoDto: FindroidTrickplayInfoDto) + + @Query("SELECT * FROM trickplayInfos WHERE sourceId = :sourceId") + fun getTrickplayInfo(sourceId: String): FindroidTrickplayInfoDto? } diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidEpisode.kt b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidEpisode.kt index 6517de5b..2783e441 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidEpisode.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidEpisode.kt @@ -32,6 +32,7 @@ data class FindroidEpisode( val missing: Boolean = false, override val images: FindroidImages, override val chapters: List?, + override val trickplayInfo: Map?, ) : FindroidItem, FindroidSources suspend fun BaseItemDto.toFindroidEpisode( @@ -67,6 +68,7 @@ suspend fun BaseItemDto.toFindroidEpisode( missing = locationType == LocationType.VIRTUAL, images = toFindroidImages(jellyfinRepository), chapters = toFindroidChapters(), + trickplayInfo = trickplay?.mapValues { it.value[it.value.keys.max()]!!.toFindroidTrickplayInfo() }, ) } catch (_: NullPointerException) { null @@ -75,6 +77,13 @@ suspend fun BaseItemDto.toFindroidEpisode( fun FindroidEpisodeDto.toFindroidEpisode(database: ServerDatabaseDao, userId: UUID): FindroidEpisode { val userData = database.getUserDataOrCreateNew(id, userId) + val sources = database.getSources(id).map { it.toFindroidSource(database) } + val trickplayInfos = mutableMapOf() + for (source in sources) { + database.getTrickplayInfo(source.id)?.toFindroidTrickplayInfo()?.let { + trickplayInfos[source.id] = it + } + } return FindroidEpisode( id = id, name = name, @@ -83,7 +92,7 @@ fun FindroidEpisodeDto.toFindroidEpisode(database: ServerDatabaseDao, userId: UU indexNumber = indexNumber, indexNumberEnd = indexNumberEnd, parentIndexNumber = parentIndexNumber, - sources = database.getSources(id).map { it.toFindroidSource(database) }, + sources = sources, played = userData.played, favorite = userData.favorite, canPlay = true, @@ -97,5 +106,6 @@ fun FindroidEpisodeDto.toFindroidEpisode(database: ServerDatabaseDao, userId: UU communityRating = communityRating, images = FindroidImages(), chapters = chapters, + trickplayInfo = trickplayInfos, ) } diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidMovie.kt b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidMovie.kt index 2c841835..c28d9c1b 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidMovie.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidMovie.kt @@ -32,6 +32,7 @@ data class FindroidMovie( override val unplayedItemCount: Int? = null, override val images: FindroidImages, override val chapters: List?, + override val trickplayInfo: Map?, ) : FindroidItem, FindroidSources suspend fun BaseItemDto.toFindroidMovie( @@ -66,11 +67,19 @@ suspend fun BaseItemDto.toFindroidMovie( trailer = remoteTrailers?.getOrNull(0)?.url, images = toFindroidImages(jellyfinRepository), chapters = toFindroidChapters(), + trickplayInfo = trickplay?.mapValues { it.value[it.value.keys.max()]!!.toFindroidTrickplayInfo() }, ) } fun FindroidMovieDto.toFindroidMovie(database: ServerDatabaseDao, userId: UUID): FindroidMovie { val userData = database.getUserDataOrCreateNew(id, userId) + val sources = database.getSources(id).map { it.toFindroidSource(database) } + val trickplayInfos = mutableMapOf() + for (source in sources) { + database.getTrickplayInfo(source.id)?.toFindroidTrickplayInfo()?.let { + trickplayInfos[source.id] = it + } + } return FindroidMovie( id = id, name = name, @@ -94,5 +103,6 @@ fun FindroidMovieDto.toFindroidMovie(database: ServerDatabaseDao, userId: UUID): trailer = null, images = FindroidImages(), chapters = chapters, + trickplayInfo = trickplayInfos, ) } diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidSources.kt b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidSources.kt index ca14f752..e013f670 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidSources.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidSources.kt @@ -3,4 +3,5 @@ package dev.jdtech.jellyfin.models interface FindroidSources { val sources: List val runtimeTicks: Long + val trickplayInfo: Map? } diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidTrickplayInfo.kt b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidTrickplayInfo.kt new file mode 100644 index 00000000..326565b5 --- /dev/null +++ b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidTrickplayInfo.kt @@ -0,0 +1,37 @@ +package dev.jdtech.jellyfin.models + +import org.jellyfin.sdk.model.api.TrickplayInfo + +data class FindroidTrickplayInfo( + val width: Int, + val height: Int, + val tileWidth: Int, + val tileHeight: Int, + val thumbnailCount: Int, + val interval: Int, + val bandwidth: Int, +) + +fun TrickplayInfo.toFindroidTrickplayInfo(): FindroidTrickplayInfo { + return FindroidTrickplayInfo( + width = width, + height = height, + tileWidth = tileWidth, + tileHeight = tileHeight, + thumbnailCount = thumbnailCount, + interval = interval, + bandwidth = bandwidth, + ) +} + +fun FindroidTrickplayInfoDto.toFindroidTrickplayInfo(): FindroidTrickplayInfo { + return FindroidTrickplayInfo( + width = width, + height = height, + tileWidth = tileWidth, + tileHeight = tileHeight, + thumbnailCount = thumbnailCount, + interval = interval, + bandwidth = bandwidth, + ) +} diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidTrickplayInfoDto.kt b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidTrickplayInfoDto.kt new file mode 100644 index 00000000..50219908 --- /dev/null +++ b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidTrickplayInfoDto.kt @@ -0,0 +1,41 @@ +package dev.jdtech.jellyfin.models + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey + +@Entity( + tableName = "trickplayInfos", + foreignKeys = [ + ForeignKey( + entity = FindroidSourceDto::class, + parentColumns = arrayOf("id"), + childColumns = arrayOf("sourceId"), + onDelete = ForeignKey.CASCADE, + ), + ], +) +data class FindroidTrickplayInfoDto( + @PrimaryKey + val sourceId: String, + val width: Int, + val height: Int, + val tileWidth: Int, + val tileHeight: Int, + val thumbnailCount: Int, + val interval: Int, + val bandwidth: Int, +) + +fun FindroidTrickplayInfo.toFindroidTrickplayInfoDto(sourceId: String): FindroidTrickplayInfoDto { + return FindroidTrickplayInfoDto( + sourceId = sourceId, + width = width, + height = height, + tileWidth = tileWidth, + tileHeight = tileHeight, + thumbnailCount = thumbnailCount, + interval = interval, + bandwidth = bandwidth, + ) +} diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/TrickPlayManifest.kt b/data/src/main/java/dev/jdtech/jellyfin/models/TrickPlayManifest.kt deleted file mode 100644 index 3533fd13..00000000 --- a/data/src/main/java/dev/jdtech/jellyfin/models/TrickPlayManifest.kt +++ /dev/null @@ -1,19 +0,0 @@ -package dev.jdtech.jellyfin.models - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class TrickPlayManifest( - @SerialName("Version") - val version: String, - @SerialName("WidthResolutions") - val widthResolutions: List, -) - -fun TrickPlayManifestDto.toTrickPlayManifest(): TrickPlayManifest { - return TrickPlayManifest( - version = version, - widthResolutions = listOf(resolution), - ) -} diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/TrickPlayManifestDto.kt b/data/src/main/java/dev/jdtech/jellyfin/models/TrickPlayManifestDto.kt deleted file mode 100644 index 85ed9b17..00000000 --- a/data/src/main/java/dev/jdtech/jellyfin/models/TrickPlayManifestDto.kt +++ /dev/null @@ -1,21 +0,0 @@ -package dev.jdtech.jellyfin.models - -import androidx.room.Entity -import androidx.room.PrimaryKey -import java.util.UUID - -@Entity(tableName = "trickPlayManifests") -data class TrickPlayManifestDto( - @PrimaryKey - val itemId: UUID, - val version: String, - val resolution: Int, -) - -fun TrickPlayManifest.toTrickPlayManifestDto(itemId: UUID): TrickPlayManifestDto { - return TrickPlayManifestDto( - itemId = itemId, - version = version, - resolution = widthResolutions.max(), - ) -} diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt index 8b902f55..e2f117a3 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt @@ -10,7 +10,6 @@ import dev.jdtech.jellyfin.models.FindroidShow import dev.jdtech.jellyfin.models.FindroidSource import dev.jdtech.jellyfin.models.Intro import dev.jdtech.jellyfin.models.SortBy -import dev.jdtech.jellyfin.models.TrickPlayManifest import kotlinx.coroutines.flow.Flow import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind @@ -86,9 +85,7 @@ interface JellyfinRepository { suspend fun getIntroTimestamps(itemId: UUID): Intro? - suspend fun getTrickPlayManifest(itemId: UUID): TrickPlayManifest? - - suspend fun getTrickPlayData(itemId: UUID, width: Int): ByteArray? + suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray? suspend fun postCapabilities() diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index e0270bf7..f1b2c2d3 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -16,7 +16,6 @@ import dev.jdtech.jellyfin.models.FindroidShow import dev.jdtech.jellyfin.models.FindroidSource import dev.jdtech.jellyfin.models.Intro import dev.jdtech.jellyfin.models.SortBy -import dev.jdtech.jellyfin.models.TrickPlayManifest import dev.jdtech.jellyfin.models.toFindroidCollection import dev.jdtech.jellyfin.models.toFindroidEpisode import dev.jdtech.jellyfin.models.toFindroidItem @@ -25,9 +24,7 @@ import dev.jdtech.jellyfin.models.toFindroidSeason import dev.jdtech.jellyfin.models.toFindroidShow import dev.jdtech.jellyfin.models.toFindroidSource import dev.jdtech.jellyfin.models.toIntro -import dev.jdtech.jellyfin.models.toTrickPlayManifest -import io.ktor.util.cio.toByteArray -import io.ktor.utils.io.ByteReadChannel +import io.ktor.util.toByteArray import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext @@ -361,46 +358,17 @@ class JellyfinRepositoryImpl( } } - override suspend fun getTrickPlayManifest(itemId: UUID): TrickPlayManifest? = + override suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray? = withContext(Dispatchers.IO) { - val trickPlayManifest = database.getTrickPlayManifest(itemId) - if (trickPlayManifest != null) { - return@withContext trickPlayManifest.toTrickPlayManifest() - } - // https://github.com/nicknsy/jellyscrub/blob/main/Nick.Plugin.Jellyscrub/Api/TrickplayController.cs - val pathParameters = mutableMapOf() - pathParameters["itemId"] = itemId - try { - return@withContext jellyfinApi.api.get( - "/Trickplay/{itemId}/GetManifest", - pathParameters, - ).content - } catch (e: Exception) { - return@withContext null - } - } + try { + val sources = File(context.filesDir, "trickplay/$itemId").listFiles() + if (sources != null) { + return@withContext File(sources.first(), index.toString()).readBytes() + } + } catch (_: Exception) { } - override suspend fun getTrickPlayData(itemId: UUID, width: Int): ByteArray? = - withContext(Dispatchers.IO) { - val trickPlayManifest = database.getTrickPlayManifest(itemId) - if (trickPlayManifest != null) { - return@withContext File( - context.filesDir, - "trickplay/$itemId.bif", - ).readBytes() - } - - // https://github.com/nicknsy/jellyscrub/blob/main/Nick.Plugin.Jellyscrub/Api/TrickplayController.cs - val pathParameters = mutableMapOf() - pathParameters["itemId"] = itemId - pathParameters["width"] = width - - try { - return@withContext jellyfinApi.api.get( - "/Trickplay/{itemId}/{width}/GetBIF", - pathParameters, - ).content.toByteArray() + return@withContext jellyfinApi.trickplayApi.getTrickplayTileImage(itemId, width, index).content.toByteArray() } catch (e: Exception) { return@withContext null } diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt index 2fb4a399..0a78ec47 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt @@ -14,14 +14,12 @@ import dev.jdtech.jellyfin.models.FindroidShow import dev.jdtech.jellyfin.models.FindroidSource import dev.jdtech.jellyfin.models.Intro import dev.jdtech.jellyfin.models.SortBy -import dev.jdtech.jellyfin.models.TrickPlayManifest import dev.jdtech.jellyfin.models.toFindroidEpisode import dev.jdtech.jellyfin.models.toFindroidMovie import dev.jdtech.jellyfin.models.toFindroidSeason import dev.jdtech.jellyfin.models.toFindroidShow import dev.jdtech.jellyfin.models.toFindroidSource import dev.jdtech.jellyfin.models.toIntro -import dev.jdtech.jellyfin.models.toTrickPlayManifest import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext @@ -184,21 +182,14 @@ class JellyfinRepositoryOfflineImpl( database.getIntro(itemId)?.toIntro() } - override suspend fun getTrickPlayManifest(itemId: UUID): TrickPlayManifest? = + override suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray? = withContext(Dispatchers.IO) { - database.getTrickPlayManifest(itemId)?.toTrickPlayManifest() - } - - override suspend fun getTrickPlayData(itemId: UUID, width: Int): ByteArray? = - withContext(Dispatchers.IO) { - val trickPlayManifest = database.getTrickPlayManifest(itemId) - if (trickPlayManifest != null) { - return@withContext File( - context.filesDir, - "trickplay/$itemId.bif", - ).readBytes() + try { + val sources = File(context.filesDir, "trickplay/$itemId").listFiles() ?: return@withContext null + File(sources.first(), index.toString()).readBytes() + } catch (e: Exception) { + null } - null } override suspend fun postCapabilities() {} diff --git a/player/core/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt b/player/core/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt index bf4e1e4d..370c1b33 100644 --- a/player/core/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt +++ b/player/core/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt @@ -16,4 +16,5 @@ data class PlayerItem( val indexNumberEnd: Int? = null, val externalSubtitles: List = emptyList(), val chapters: List? = null, + val trickplayInfo: TrickplayInfo? = null, ) : Parcelable diff --git a/player/core/src/main/java/dev/jdtech/jellyfin/models/Trickplay.kt b/player/core/src/main/java/dev/jdtech/jellyfin/models/Trickplay.kt new file mode 100644 index 00000000..2a5700d7 --- /dev/null +++ b/player/core/src/main/java/dev/jdtech/jellyfin/models/Trickplay.kt @@ -0,0 +1,8 @@ +package dev.jdtech.jellyfin.models + +import android.graphics.Bitmap + +data class Trickplay( + val interval: Int, + val images: List, +) diff --git a/player/core/src/main/java/dev/jdtech/jellyfin/models/TrickplayInfo.kt b/player/core/src/main/java/dev/jdtech/jellyfin/models/TrickplayInfo.kt new file mode 100644 index 00000000..e38cd1b4 --- /dev/null +++ b/player/core/src/main/java/dev/jdtech/jellyfin/models/TrickplayInfo.kt @@ -0,0 +1,15 @@ +package dev.jdtech.jellyfin.models + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class TrickplayInfo( + val width: Int, + val height: Int, + val tileWidth: Int, + val tileHeight: Int, + val thumbnailCount: Int, + val interval: Int, + val bandwidth: Int, +) : Parcelable diff --git a/player/core/src/main/java/dev/jdtech/jellyfin/utils/bif/BifData.kt b/player/core/src/main/java/dev/jdtech/jellyfin/utils/bif/BifData.kt deleted file mode 100644 index 0db720b8..00000000 --- a/player/core/src/main/java/dev/jdtech/jellyfin/utils/bif/BifData.kt +++ /dev/null @@ -1,11 +0,0 @@ -package dev.jdtech.jellyfin.utils.bif - -import android.graphics.Bitmap - -data class BifData( - val version: Int, - val timestampMultiplier: Int, - val imageCount: Int, - val images: Map, - val imageWidth: Int, -) diff --git a/player/core/src/main/java/dev/jdtech/jellyfin/utils/bif/BifIndexEntry.kt b/player/core/src/main/java/dev/jdtech/jellyfin/utils/bif/BifIndexEntry.kt deleted file mode 100644 index 292b1e30..00000000 --- a/player/core/src/main/java/dev/jdtech/jellyfin/utils/bif/BifIndexEntry.kt +++ /dev/null @@ -1,3 +0,0 @@ -package dev.jdtech.jellyfin.utils.bif - -data class BifIndexEntry(val timestamp: Int, val offset: Int) diff --git a/player/core/src/main/java/dev/jdtech/jellyfin/utils/bif/BifUtil.kt b/player/core/src/main/java/dev/jdtech/jellyfin/utils/bif/BifUtil.kt deleted file mode 100644 index 36fdd072..00000000 --- a/player/core/src/main/java/dev/jdtech/jellyfin/utils/bif/BifUtil.kt +++ /dev/null @@ -1,77 +0,0 @@ -package dev.jdtech.jellyfin.utils.bif - -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import timber.log.Timber -import java.nio.ByteBuffer - -object BifUtil { - - /* https://github.com/nicknsy/jellyscrub/blob/main/Nick.Plugin.Jellyscrub/Api/trickplay.js */ - private val BIF_MAGIC_NUMBERS = byteArrayOf(0x89.toByte(), 0x42.toByte(), 0x49.toByte(), 0x46.toByte(), 0x0D.toByte(), 0x0A.toByte(), 0x1A.toByte(), 0x0A.toByte()) - private const val SUPPORTED_BIF_VERSION = 0 - - fun trickPlayDecode(array: ByteArray, width: Int): BifData? { - val data = Indexed8Array(array) - - Timber.d("BIF file size: ${data.limit()}") - - for (b in BIF_MAGIC_NUMBERS) { - if (data.read() != b) { - Timber.d("Attempted to read invalid bif file.") - return null - } - } - - val bifVersion = data.readInt32() - if (bifVersion != SUPPORTED_BIF_VERSION) { - Timber.d("Client only supports BIF v$SUPPORTED_BIF_VERSION but file is v$bifVersion") - return null - } - - Timber.d("BIF version: $bifVersion") - - val bifImgCount = data.readInt32() - if (bifImgCount <= 0) { - Timber.d("BIF file contains no images.") - return null - } - - Timber.d("BIF image count: $bifImgCount") - - var timestampMultiplier = data.readInt32() - if (timestampMultiplier == 0) timestampMultiplier = 1000 - - data.addPosition(44) // Reserved - - val bifIndex = mutableListOf() - for (i in 0 until bifImgCount) { - bifIndex.add(BifIndexEntry(data.readInt32(), data.readInt32())) - } - - val bifImages = mutableMapOf() - for (i in bifIndex.indices) { - val indexEntry = bifIndex[i] - val timestamp = indexEntry.timestamp - val offset = indexEntry.offset - val nextOffset = bifIndex.getOrNull(i + 1)?.offset ?: data.limit() - - val imageBuffer = ByteBuffer.wrap(data.array(), offset, nextOffset - offset).order(data.order()) - val imageBytes = ByteArray(imageBuffer.remaining()) - imageBuffer.get(imageBytes) - - val bmp = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) - bifImages[timestamp] = bmp - } - - return BifData(bifVersion, timestampMultiplier, bifImgCount, bifImages, width) - } - - fun getTrickPlayFrame(playerTimestamp: Int, data: BifData): Bitmap? { - val multiplier = data.timestampMultiplier - val images = data.images - - val frame = playerTimestamp / multiplier - return images[frame] - } -} diff --git a/player/core/src/main/java/dev/jdtech/jellyfin/utils/bif/Indexed8Array.kt b/player/core/src/main/java/dev/jdtech/jellyfin/utils/bif/Indexed8Array.kt deleted file mode 100644 index a41c015f..00000000 --- a/player/core/src/main/java/dev/jdtech/jellyfin/utils/bif/Indexed8Array.kt +++ /dev/null @@ -1,37 +0,0 @@ -package dev.jdtech.jellyfin.utils.bif - -import java.nio.ByteOrder - -class Indexed8Array(private val array: ByteArray) { - - private var readIndex = 0 - - fun read(): Byte { - return array[readIndex++] - } - - fun addPosition(amount: Int) { - readIndex += amount - } - - fun readInt32(): Int { - val b1 = read().toInt().and(0xFF) - val b2 = read().toInt().and(0xFF) - val b3 = read().toInt().and(0xFF) - val b4 = read().toInt().and(0xFF) - - return b1 or (b2 shl 8) or (b3 shl 16) or (b4 shl 24) - } - - fun array(): ByteArray { - return array - } - - fun limit(): Int { - return array.size - } - - fun order(): ByteOrder { - return ByteOrder.BIG_ENDIAN - } -} diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt index ceac6b22..37b1ed42 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -1,6 +1,8 @@ package dev.jdtech.jellyfin.viewmodels import android.app.Application +import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.os.Handler import android.os.Looper import androidx.lifecycle.SavedStateHandle @@ -21,12 +23,12 @@ import dev.jdtech.jellyfin.AppPreferences import dev.jdtech.jellyfin.models.Intro import dev.jdtech.jellyfin.models.PlayerChapter import dev.jdtech.jellyfin.models.PlayerItem +import dev.jdtech.jellyfin.models.Trickplay import dev.jdtech.jellyfin.mpv.MPVPlayer import dev.jdtech.jellyfin.player.video.R import dev.jdtech.jellyfin.repository.JellyfinRepository -import dev.jdtech.jellyfin.utils.bif.BifData -import dev.jdtech.jellyfin.utils.bif.BifUtil import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay @@ -35,9 +37,11 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import java.util.UUID import javax.inject.Inject +import kotlin.math.ceil @HiltViewModel class PlayerActivityViewModel @@ -54,7 +58,7 @@ constructor( UiState( currentItemTitle = "", currentIntro = null, - currentTrickPlay = null, + currentTrickplay = null, currentChapters = null, fileLoaded = false, ), @@ -66,12 +70,10 @@ constructor( private val intros: MutableMap = mutableMapOf() - private val trickPlays: MutableMap = mutableMapOf() - data class UiState( val currentItemTitle: String, val currentIntro: Intro?, - val currentTrickPlay: BifData?, + val currentTrickplay: Trickplay?, val currentChapters: List?, val fileLoaded: Boolean, ) @@ -208,7 +210,7 @@ constructor( } } - _uiState.update { it.copy(currentTrickPlay = null) } + _uiState.update { it.copy(currentTrickplay = null) } playWhenReady = false playbackPosition = 0L currentMediaItemIndex = 0 @@ -277,8 +279,8 @@ constructor( jellyfinRepository.postPlaybackStart(item.itemId) - if (appPreferences.playerTrickPlay) { - getTrickPlay(item.itemId) + if (appPreferences.playerTrickplay) { + getTrickplay(item) } } } catch (e: Exception) { @@ -339,28 +341,31 @@ constructor( playbackSpeed = speed } - private suspend fun getTrickPlay(itemId: UUID) { - if (trickPlays[itemId] != null) return - jellyfinRepository.getTrickPlayManifest(itemId) - ?.let { trickPlayManifest -> - val widthResolution = - trickPlayManifest.widthResolutions.max() - Timber.d("Trickplay Resolution: $widthResolution") + private suspend fun getTrickplay(item: PlayerItem) { + val trickplayInfo = item.trickplayInfo ?: return + Timber.d("Trickplay Resolution: ${trickplayInfo.width}") - jellyfinRepository.getTrickPlayData( - itemId, - widthResolution, + withContext(Dispatchers.Default) { + val maxIndex = ceil(trickplayInfo.thumbnailCount.toDouble().div(trickplayInfo.tileWidth * trickplayInfo.tileHeight)).toInt() + val bitmaps = mutableListOf() + + for (i in 0..maxIndex) { + jellyfinRepository.getTrickplayData( + item.itemId, + trickplayInfo.width, + i, )?.let { byteArray -> - val trickPlayData = - BifUtil.trickPlayDecode(byteArray, widthResolution) - - trickPlayData?.let { bifData -> - Timber.d("Trickplay Images: ${bifData.imageCount}") - trickPlays[itemId] = bifData - _uiState.update { it.copy(currentTrickPlay = trickPlays[itemId]) } + val fullBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) + for (offsetY in 0.. { + this.trickplayInfo?.get(mediaSource.id)?.let { + TrickplayInfo( + width = it.width, + height = it.height, + tileWidth = it.tileWidth, + tileHeight = it.tileHeight, + thumbnailCount = it.thumbnailCount, + interval = it.interval, + bandwidth = it.bandwidth, + ) + } + } + else -> null + } return PlayerItem( name = name, itemId = id, @@ -162,6 +180,7 @@ class PlayerViewModel @Inject internal constructor( indexNumberEnd = if (this is FindroidEpisode) indexNumberEnd else null, externalSubtitles = externalSubtitles, chapters = chapters.toPlayerChapters(), + trickplayInfo = trickplayInfo, ) } diff --git a/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt b/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt index a2da8085..eb7e9dca 100644 --- a/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt +++ b/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt @@ -78,7 +78,7 @@ constructor( val playerMpvVo get() = sharedPreferences.getString(Constants.PREF_PLAYER_MPV_VO, "gpu-next")!! val playerMpvAo get() = sharedPreferences.getString(Constants.PREF_PLAYER_MPV_AO, "audiotrack")!! val playerIntroSkipper get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_INTRO_SKIPPER, true) - val playerTrickPlay get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_TRICK_PLAY, true) + val playerTrickplay get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_TRICKPLAY, true) val showChapterMarkers get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_CHAPTER_MARKERS, true) val playerPipGesture get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_PIP_GESTURE, false) diff --git a/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt b/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt index 2006ac38..cca99608 100644 --- a/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt +++ b/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt @@ -27,7 +27,7 @@ object Constants { const val PREF_PLAYER_MPV_VO = "pref_player_mpv_vo" const val PREF_PLAYER_MPV_AO = "pref_player_mpv_ao" const val PREF_PLAYER_INTRO_SKIPPER = "pref_player_intro_skipper" - const val PREF_PLAYER_TRICK_PLAY = "pref_player_trick_play" + const val PREF_PLAYER_TRICKPLAY = "pref_player_trickplay" const val PREF_PLAYER_CHAPTER_MARKERS = "pref_player_chapter_markers" const val PREF_PLAYER_PIP_GESTURE = "pref_player_picture_in_picture_gesture" const val PREF_AUDIO_LANGUAGE = "pref_audio_language"