From b05e45ea9dca022b9fb86bf101723257aedcaae9 Mon Sep 17 00:00:00 2001 From: e2fo2l Date: Fri, 3 Feb 2023 00:12:08 +0100 Subject: [PATCH 1/6] Added the option to select the stream resolution --- core/src/main/res/values/string_arrays.xml | 18 +++++- core/src/main/res/values/strings.xml | 5 +- .../main/res/xml/fragment_settings_player.xml | 8 +++ .../jellyfin/repository/JellyfinRepository.kt | 2 + .../repository/JellyfinRepositoryImpl.kt | 59 ++++++++++++++++++- .../viewmodels/PlayerActivityViewModel.kt | 18 +++++- .../jellyfin/viewmodels/PlayerViewModel.kt | 7 ++- .../dev/jdtech/jellyfin/AppPreferences.kt | 5 ++ .../java/dev/jdtech/jellyfin/Constants.kt | 1 + 9 files changed, 116 insertions(+), 7 deletions(-) diff --git a/core/src/main/res/values/string_arrays.xml b/core/src/main/res/values/string_arrays.xml index bc768c8a5f..9fca4af57d 100644 --- a/core/src/main/res/values/string_arrays.xml +++ b/core/src/main/res/values/string_arrays.xml @@ -37,4 +37,20 @@ opengl - \ No newline at end of file + + Original + 4K + 1080p + 720p + 480p + 360p + + + @string/quality_original + 4K + 1080p + 720p + 480p + 360p + + diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index eb357ae077..05c6b639a8 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -151,4 +151,7 @@ Add server address Add Quick Connect - \ No newline at end of file + Quality + %s\nAny setting other than Original might require transcoding. + Original + diff --git a/core/src/main/res/xml/fragment_settings_player.xml b/core/src/main/res/xml/fragment_settings_player.xml index 88a47c129b..d7ac798010 100644 --- a/core/src/main/res/xml/fragment_settings_player.xml +++ b/core/src/main/res/xml/fragment_settings_player.xml @@ -1,5 +1,13 @@ + + () + override suspend fun getUserViews(): List = withContext(Dispatchers.IO) { jellyfinApi.viewsApi.getUserViews(jellyfinApi.userId!!).content.items.orEmpty() } @@ -168,7 +172,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep override suspend fun getMediaSources(itemId: UUID): List = withContext(Dispatchers.IO) { - jellyfinApi.mediaInfoApi.getPostedPlaybackInfo( + val playbackInfo = jellyfinApi.mediaInfoApi.getPostedPlaybackInfo( itemId, PlaybackInfoDto( userId = jellyfinApi.userId!!, @@ -204,7 +208,9 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep ), maxStreamingBitrate = 1_000_000_000, ) - ).content.mediaSources + ).content + playSessionIds[itemId] = playbackInfo.playSessionId + playbackInfo.mediaSources } override suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String): String = @@ -221,6 +227,55 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep } } + private fun getVideoTranscodeBitRate(transcodeResolution: Int?): Pair { + return when (transcodeResolution) { + 2160 -> 59616000 to 384000 + 1080 -> 14616000 to 384000 + 720 -> 7616000 to 384000 + 480 -> 2616000 to 384000 + 360 -> 292000 to 128000 + + else -> null to null + } + } + + override suspend fun getHlsPlaylistUrl( + itemId: UUID, + mediaSourceId: String, + transcodeResolution: Int? + ): String = + withContext(Dispatchers.IO) { + try { + val (videoBitRate, audioBitRate) = getVideoTranscodeBitRate(transcodeResolution) + if(videoBitRate == null || audioBitRate == null) { + jellyfinApi.api.dynamicHlsApi.getVariantHlsVideoPlaylistUrl( + itemId, + static = true, + mediaSourceId = mediaSourceId, + playSessionId = playSessionIds[itemId] // playSessionId is required to update the transcoding resolution + ) + } + else { + jellyfinApi.api.dynamicHlsApi.getVariantHlsVideoPlaylistUrl( + itemId, + static = false, + mediaSourceId = mediaSourceId, + playSessionId = playSessionIds[itemId], + videoCodec = "h264", + audioCodec = "aac", + videoBitRate = videoBitRate, + audioBitRate = audioBitRate, + maxHeight = transcodeResolution, + subtitleMethod = SubtitleDeliveryMethod.EXTERNAL, + transcodeReasons = "ContainerBitrateExceedsLimit", + ) + } + } catch (e: Exception) { + Timber.e(e) + "" + } + } + override suspend fun getIntroTimestamps(itemId: UUID): Intro? = withContext(Dispatchers.IO) { // https://github.com/ConfusedPolarBear/intro-skipper/blob/master/docs/api.md 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 bd66aa6206..ac0b7b89d4 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 @@ -105,6 +105,18 @@ constructor( } } + private fun getTranscodeResolution(preferredQuality: String): Int? { + return when (preferredQuality) { + "4K" -> 2160 + "1080p" -> 1080 + "720p" -> 720 + "480p" -> 480 + "360p" -> 360 + + else -> null + } + } + fun initializePlayer( items: Array ) { @@ -115,9 +127,13 @@ constructor( val mediaItems: MutableList = mutableListOf() try { for (item in items) { + val transcodeResolution = getTranscodeResolution(appPreferences.playerPreferredQuality) val streamUrl = when { item.mediaSourceUri.isNotEmpty() -> item.mediaSourceUri - else -> jellyfinRepository.getStreamUrl(item.itemId, item.mediaSourceId) + else -> when (transcodeResolution) { + null -> jellyfinRepository.getStreamUrl(item.itemId, item.mediaSourceId) + else -> jellyfinRepository.getHlsPlaylistUrl(item.itemId, item.mediaSourceId, transcodeResolution) + } } val mediaSubtitles = item.externalSubtitles.map { externalSubtitle -> MediaItem.SubtitleConfiguration.Builder(externalSubtitle.uri) diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt index d0cfab0144..e698608adf 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.media3.common.MimeTypes import dagger.hilt.android.lifecycle.HiltViewModel +import dev.jdtech.jellyfin.AppPreferences import dev.jdtech.jellyfin.player.video.R import dev.jdtech.jellyfin.database.DownloadDatabaseDao import dev.jdtech.jellyfin.models.ExternalSubtitle @@ -30,7 +31,8 @@ import timber.log.Timber class PlayerViewModel @Inject internal constructor( private val application: Application, private val repository: JellyfinRepository, - private val downloadDatabase: DownloadDatabaseDao + private val downloadDatabase: DownloadDatabaseDao, + private val appPreferences: AppPreferences ) : ViewModel() { private val playerItems = MutableSharedFlow( @@ -177,7 +179,8 @@ class PlayerViewModel @Inject internal constructor( val mediaSource = repository.getMediaSources(id)[mediaSourceIndex] val externalSubtitles = mutableListOf() for (mediaStream in mediaSource.mediaStreams!!) { - if (mediaStream.isExternal && mediaStream.type == MediaStreamType.SUBTITLE && !mediaStream.deliveryUrl.isNullOrBlank()) { + if ((appPreferences.playerPreferredQuality != "Original" || mediaStream.isExternal) // When transcoding, subtitles aren't embedded, so we add them externally + && mediaStream.type == MediaStreamType.SUBTITLE && !mediaStream.deliveryUrl.isNullOrBlank()) { // Temp fix for vtt // Jellyfin returns a srt stream when it should return vtt stream. diff --git a/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt b/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt index 52b2ef64fd..4bd31d71f6 100644 --- a/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt +++ b/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt @@ -26,6 +26,11 @@ constructor( val dynamicColors get() = sharedPreferences.getBoolean(Constants.PREF_DYNAMIC_COLORS, true) // Player + val playerPreferredQuality: String get() = sharedPreferences.getString( + Constants.PREF_PLAYER_PREFERRED_QUALITY, + "Original" + )!! + val displayExtendedTitle get() = sharedPreferences.getBoolean(Constants.PREF_DISPLAY_EXTENDED_TITLE, false) val playerGestures get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES, true) diff --git a/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt b/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt index 25dbc83faf..56ff572652 100644 --- a/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt +++ b/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt @@ -9,6 +9,7 @@ object Constants { // pref const val PREF_CURRENT_SERVER = "pref_current_server" + const val PREF_PLAYER_PREFERRED_QUALITY = "pref_player_preferred_quality" const val PREF_DISPLAY_EXTENDED_TITLE = "pref_player_display_extended_title" const val PREF_PLAYER_GESTURES = "pref_player_gestures" const val PREF_PLAYER_GESTURES_VB = "pref_player_gestures_vb" From 8e477db6e2cd8948a74216a957258c595569baac Mon Sep 17 00:00:00 2001 From: e2fo2l Date: Tue, 7 Feb 2023 22:02:01 +0100 Subject: [PATCH 2/6] Changed DropDownPreference to ListPreference --- core/src/main/res/xml/fragment_settings_player.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/src/main/res/xml/fragment_settings_player.xml b/core/src/main/res/xml/fragment_settings_player.xml index 527dfc5209..eafbd59e82 100644 --- a/core/src/main/res/xml/fragment_settings_player.xml +++ b/core/src/main/res/xml/fragment_settings_player.xml @@ -1,5 +1,13 @@ + + Date: Mon, 13 Feb 2023 09:41:55 +0100 Subject: [PATCH 3/6] Fix double import for PlayerViewModel --- .../main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt index 1056eea4e0..4e12229f37 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt @@ -8,7 +8,6 @@ import androidx.lifecycle.viewModelScope import androidx.media3.common.MimeTypes import dagger.hilt.android.lifecycle.HiltViewModel import dev.jdtech.jellyfin.AppPreferences -import dev.jdtech.jellyfin.player.video.R import dev.jdtech.jellyfin.database.DownloadDatabaseDao import dev.jdtech.jellyfin.models.ExternalSubtitle import dev.jdtech.jellyfin.models.PlayerItem From 476be0d25706b73c7b0fd59aca914eefeb661af4 Mon Sep 17 00:00:00 2001 From: Efflam <92077690+e2fo2l@users.noreply.github.com> Date: Mon, 13 Feb 2023 10:02:49 +0100 Subject: [PATCH 4/6] Fix linting for JellyfinRepositoryImpl --- .../jellyfin/repository/JellyfinRepositoryImpl.kt | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) 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 8ead6ed307..108dc46bf6 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -10,7 +10,6 @@ import java.util.UUID import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext -import org.jellyfin.sdk.api.client.exception.InvalidStatusException import org.jellyfin.sdk.api.client.extensions.dynamicHlsApi import org.jellyfin.sdk.api.client.extensions.get import org.jellyfin.sdk.model.api.BaseItemDto @@ -231,10 +230,9 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep return when (transcodeResolution) { 2160 -> 59616000 to 384000 1080 -> 14616000 to 384000 - 720 -> 7616000 to 384000 - 480 -> 2616000 to 384000 - 360 -> 292000 to 128000 - + 720 -> 7616000 to 384000 + 480 -> 2616000 to 384000 + 360 -> 292000 to 128000 else -> null to null } } @@ -247,15 +245,14 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep withContext(Dispatchers.IO) { try { val (videoBitRate, audioBitRate) = getVideoTranscodeBitRate(transcodeResolution) - if(videoBitRate == null || audioBitRate == null) { + if (videoBitRate == null || audioBitRate == null) { jellyfinApi.api.dynamicHlsApi.getVariantHlsVideoPlaylistUrl( itemId, static = true, mediaSourceId = mediaSourceId, playSessionId = playSessionIds[itemId] // playSessionId is required to update the transcoding resolution ) - } - else { + } else { jellyfinApi.api.dynamicHlsApi.getVariantHlsVideoPlaylistUrl( itemId, static = false, From 89d1d9e51a5e931f6caeed1661fbb43da71aa518 Mon Sep 17 00:00:00 2001 From: Efflam <92077690+e2fo2l@users.noreply.github.com> Date: Mon, 13 Feb 2023 10:12:52 +0100 Subject: [PATCH 5/6] Fix linting for PlayerViewModel --- .../java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt index 4e12229f37..2098e05502 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt @@ -179,8 +179,11 @@ class PlayerViewModel @Inject internal constructor( val mediaSource = repository.getMediaSources(id)[mediaSourceIndex] val externalSubtitles = mutableListOf() for (mediaStream in mediaSource.mediaStreams!!) { - if ((appPreferences.playerPreferredQuality != "Original" || mediaStream.isExternal) // When transcoding, subtitles aren't embedded, so we add them externally - && mediaStream.type == MediaStreamType.SUBTITLE && !mediaStream.deliveryUrl.isNullOrBlank()) { + // When transcoding, subtitles aren't embedded, so we add them externally + if ((appPreferences.playerPreferredQuality != "Original" || mediaStream.isExternal) && + mediaStream.type == MediaStreamType.SUBTITLE && + !mediaStream.deliveryUrl.isNullOrBlank() + ) { // Temp fix for vtt // Jellyfin returns a srt stream when it should return vtt stream. From 16fbda6cfe31bf6e17e97bdf1c2e0ccad45ca61f Mon Sep 17 00:00:00 2001 From: Efflam <92077690+e2fo2l@users.noreply.github.com> Date: Mon, 13 Feb 2023 10:19:30 +0100 Subject: [PATCH 6/6] Fix linting for PlayerViewModel (if closing bracket indentation) --- .../main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt index 2098e05502..7a109a1f0b 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt @@ -183,7 +183,7 @@ class PlayerViewModel @Inject internal constructor( if ((appPreferences.playerPreferredQuality != "Original" || mediaStream.isExternal) && mediaStream.type == MediaStreamType.SUBTITLE && !mediaStream.deliveryUrl.isNullOrBlank() - ) { + ) { // Temp fix for vtt // Jellyfin returns a srt stream when it should return vtt stream.