Added the option to select the stream resolution

This commit is contained in:
e2fo2l 2023-02-03 00:12:08 +01:00 committed by Sean Greenawalt
parent 16dd40d489
commit 0fb3402e39
9 changed files with 159 additions and 15 deletions

View file

@ -37,4 +37,20 @@
<string-array name="mpv_gpu_api">
<item>opengl</item>
</string-array>
</resources>
<string-array name="video_quality">
<item>Original</item>
<item>4K</item>
<item>1080p</item>
<item>720p</item>
<item>480p</item>
<item>360p</item>
</string-array>
<string-array name="video_quality_labels">
<item>@string/quality_original</item>
<item>4K</item>
<item>1080p</item>
<item>720p</item>
<item>480p</item>
<item>360p</item>
</string-array>
</resources>

View file

@ -177,4 +177,7 @@
<string name="cancel_download_message">Are you sure you want to cancel the download?</string>
<string name="stop_download">Stop download</string>
<string name="privacy_policy_notice">By using Findroid you agree with the <a href='https://raw.githubusercontent.com/jarnedemeulemeester/findroid/main/PRIVACY'>Privacy Policy</a> which states that we do not collect any data</string>
<string name="quality">Quality</string>
<string name="preferred_quality">%s\nAny setting other than Original might require transcoding.</string>
<string name="quality_original">Original</string>
</resources>

View file

@ -1,5 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<DropDownPreference
app:defaultValue="Original"
app:entries="@array/video_quality_labels"
app:entryValues="@array/video_quality"
app:key="pref_player_preferred_quality"
app:summary="@string/preferred_quality"
app:title="@string/quality" />
<SwitchPreference
app:key="pref_player_display_extended_title"
app:summary="@string/display_extended_title_summary"
app:title="@string/display_extended_title" />
<Preference
app:key="pref_player_subtitles"
app:summary="@string/subtitles_summary"
@ -99,7 +112,7 @@
app:summary="@string/pref_player_intro_skipper_summary"
app:title="@string/pref_player_intro_skipper"
app:widgetLayout="@layout/preference_material3_switch" />
<SwitchPreferenceCompat
app:defaultValue="true"
app:key="pref_player_trick_play"
@ -113,5 +126,5 @@
app:title="@string/picture_in_picture_gesture"
app:summary="@string/picture_in_picture_gesture_summary" />
</PreferenceCategory>
</PreferenceScreen>

View file

@ -84,6 +84,8 @@ interface JellyfinRepository {
suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String): String
suspend fun getHlsPlaylistUrl(itemId: UUID, mediaSourceId: String, transcodeResolution: Int?): String
suspend fun getIntroTimestamps(itemId: UUID): Intro?
suspend fun getTrickPlayManifest(itemId: UUID): TrickPlayManifest?

View file

@ -31,6 +31,8 @@ import io.ktor.utils.io.ByteReadChannel
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
import org.jellyfin.sdk.model.api.BaseItemKind
@ -61,6 +63,8 @@ class JellyfinRepositoryImpl(
jellyfinApi.systemApi.getPublicSystemInfo().content
}
private val playSessionIds = mutableMapOf<UUID, String?>()
override suspend fun getUserViews(): List<BaseItemDto> = withContext(Dispatchers.IO) {
jellyfinApi.viewsApi.getUserViews(jellyfinApi.userId!!).content.items.orEmpty()
}
@ -350,6 +354,55 @@ class JellyfinRepositoryImpl(
}
}
private fun getVideoTranscodeBitRate(transcodeResolution: Int?): Pair<Int?, Int?> {
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) {
val intro = database.getIntro(itemId)?.toIntro()

View file

@ -123,6 +123,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<PlayerItem>,
) {
@ -134,6 +146,14 @@ constructor(
try {
for (item in items) {
val streamUrl = item.mediaSourceUri
val transcodeResolution = getTranscodeResolution(appPreferences.playerPreferredQuality)
val streamUrl = when {
item.mediaSourceUri.isNotEmpty() -> item.mediaSourceUri
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)
.setLabel(externalSubtitle.title.ifBlank { application.getString(R.string.external) })

View file

@ -6,6 +6,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.models.ExternalSubtitle
import dev.jdtech.jellyfin.models.FindroidEpisode
import dev.jdtech.jellyfin.models.FindroidItem
@ -19,6 +20,7 @@ import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.ItemFields
import org.jellyfin.sdk.model.api.MediaProtocol
import org.jellyfin.sdk.model.api.MediaStreamType
import timber.log.Timber
import javax.inject.Inject
@ -26,6 +28,7 @@ import javax.inject.Inject
@HiltViewModel
class PlayerViewModel @Inject internal constructor(
private val repository: JellyfinRepository,
private val appPreferences: AppPreferences
) : ViewModel() {
private val playerItems = MutableSharedFlow<PlayerItemState>(
@ -144,7 +147,8 @@ class PlayerViewModel @Inject internal constructor(
}
val externalSubtitles = mediaSource.mediaStreams
.filter { mediaStream ->
mediaStream.isExternal && mediaStream.type == MediaStreamType.SUBTITLE && !mediaStream.path.isNullOrBlank()
(appPreferences.playerPreferredQuality != "Original" || mediaStream.isExternal)
&& mediaStream.type == MediaStreamType.SUBTITLE && !mediaStream.path.isNullOrBlank()
}
.map { mediaStream ->
// Temp fix for vtt
@ -166,17 +170,41 @@ class PlayerViewModel @Inject internal constructor(
},
)
}
return PlayerItem(
name = name,
itemId = id,
mediaSourceId = mediaSource.id,
mediaSourceUri = mediaSource.path,
playbackPosition = playbackPosition,
parentIndexNumber = if (this is FindroidEpisode) parentIndexNumber else null,
indexNumber = if (this is FindroidEpisode) indexNumber else null,
indexNumberEnd = if (this is FindroidEpisode) indexNumberEnd else null,
externalSubtitles = externalSubtitles,
)
return when (mediaSource.protocol) {
MediaProtocol.FILE -> PlayerItem(
name = name,
itemId = id,
mediaSourceId = mediaSource.id,
mediaSourceUri = mediaSource.path,
playbackPosition = playbackPosition,
parentIndexNumber = if (this is FindroidEpisode) parentIndexNumber else null,
indexNumber = if (this is FindroidEpisode) indexNumber else null,
indexNumberEnd = if (this is FindroidEpisode) indexNumberEnd else null,
externalSubtitles = externalSubtitles
)
MediaProtocol.HTTP -> PlayerItem(
name = name,
itemId = id,
mediaSourceId = mediaSource.id,
mediaSourceUri = mediaSource.path,
playbackPosition = playbackPosition,
parentIndexNumber = if (this is FindroidEpisode) parentIndexNumber else null,
indexNumber = if (this is FindroidEpisode) indexNumber else null,
indexNumberEnd = if (this is FindroidEpisode) indexNumberEnd else null,
externalSubtitles = externalSubtitles
)
else -> PlayerItem(
name = name,
itemId = id,
mediaSourceId = mediaSource.id,
mediaSourceUri = mediaSource.path,
playbackPosition = playbackPosition,
parentIndexNumber = if (this is FindroidEpisode) parentIndexNumber else null,
indexNumber = if (this is FindroidEpisode) indexNumber else null,
indexNumberEnd = if (this is FindroidEpisode) indexNumberEnd else null,
externalSubtitles = externalSubtitles,
)
}
}
sealed class PlayerItemState

View file

@ -43,6 +43,13 @@ constructor(
}
// 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)
val playerGesturesVB get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES_VB, true)
val playerGesturesZoom get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES_ZOOM, true)

View file

@ -12,6 +12,8 @@ object Constants {
// pref
const val PREF_CURRENT_SERVER = "pref_current_server"
const val PREF_OFFLINE_MODE = "pref_offline_mode"
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"
const val PREF_PLAYER_GESTURES_ZOOM = "pref_player_gestures_zoom"