feat: Quality change in player (transcoded stream)

This commit is contained in:
nomadics9 2024-07-14 21:54:21 +03:00
parent ab090a01d7
commit 8139119c35
20 changed files with 388 additions and 157 deletions

View file

@ -16,36 +16,10 @@
}
],
"attributes": [],
"versionCode": 8,
"versionCode": 9,
"versionName": "0.14.2",
"outputFile": "ananas-v0.14.2-libre-armeabi-v7a.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "arm64-v8a"
}
],
"attributes": [],
"versionCode": 8,
"versionName": "0.14.2",
"outputFile": "ananas-v0.14.2-libre-arm64-v8a.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "x86"
}
],
"attributes": [],
"versionCode": 8,
"versionName": "0.14.2",
"outputFile": "ananas-v0.14.2-libre-x86.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
@ -55,9 +29,35 @@
}
],
"attributes": [],
"versionCode": 8,
"versionCode": 9,
"versionName": "0.14.2",
"outputFile": "ananas-v0.14.2-libre-x86_64.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "x86"
}
],
"attributes": [],
"versionCode": 9,
"versionName": "0.14.2",
"outputFile": "ananas-v0.14.2-libre-x86.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "arm64-v8a"
}
],
"attributes": [],
"versionCode": 9,
"versionName": "0.14.2",
"outputFile": "ananas-v0.14.2-libre-arm64-v8a.apk"
}
],
"elementType": "File",
@ -67,9 +67,9 @@
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/ananas-v0.14.2-libre-armeabi-v7a.dm",
"baselineProfiles/1/ananas-v0.14.2-libre-arm64-v8a.dm",
"baselineProfiles/1/ananas-v0.14.2-libre-x86_64.dm",
"baselineProfiles/1/ananas-v0.14.2-libre-x86.dm",
"baselineProfiles/1/ananas-v0.14.2-libre-x86_64.dm"
"baselineProfiles/1/ananas-v0.14.2-libre-arm64-v8a.dm"
]
},
{
@ -77,9 +77,9 @@
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/ananas-v0.14.2-libre-armeabi-v7a.dm",
"baselineProfiles/0/ananas-v0.14.2-libre-arm64-v8a.dm",
"baselineProfiles/0/ananas-v0.14.2-libre-x86_64.dm",
"baselineProfiles/0/ananas-v0.14.2-libre-x86.dm",
"baselineProfiles/0/ananas-v0.14.2-libre-x86_64.dm"
"baselineProfiles/0/ananas-v0.14.2-libre-arm64-v8a.dm"
]
}
],

View file

@ -1,6 +1,5 @@
package com.nomadics9.ananas
import android.app.AlertDialog
import android.app.AppOpsManager
import android.app.PictureInPictureParams
import android.content.Context
@ -25,15 +24,18 @@ import android.widget.ImageView
import android.widget.Space
import android.widget.TextView
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.media3.common.C
import androidx.media3.common.Player
import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.PlayerControlView
import androidx.media3.ui.PlayerView
import androidx.navigation.navArgs
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import com.nomadics9.ananas.databinding.ActivityPlayerBinding
import com.nomadics9.ananas.dialogs.SpeedSelectionDialogFragment
@ -88,7 +90,7 @@ class PlayerActivity : BasePlayerActivity() {
binding = ActivityPlayerBinding.inflate(layoutInflater)
setContentView(binding.root)
val changeQualityButton: Button = findViewById(R.id.btnChangeQuality)
val changeQualityButton: ImageButton = findViewById(R.id.btnChangeQuality)
changeQualityButton.setOnClickListener {
showQualitySelectionDialog()
}
@ -426,9 +428,15 @@ class PlayerActivity : BasePlayerActivity() {
}
private fun showQualitySelectionDialog() {
val qualities = arrayOf("1080p", "720p", "480p", "360p")
val height = viewModel.getOriginalHeight()
AlertDialog.Builder(this)
val qualities = when (height) {
0 -> arrayOf("Auto", "Original - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps")
in 1001..1999 -> arrayOf("Auto", "Original (1080p) - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps")
in 2000..3000 -> arrayOf("Auto", "Original (4K) - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps")
else -> arrayOf("Auto", "Original - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps")
}
MaterialAlertDialogBuilder(this)
.setTitle("Select Video Quality")
.setItems(qualities) { _, which ->
val selectedQuality = qualities[which]
@ -438,6 +446,9 @@ class PlayerActivity : BasePlayerActivity() {
}
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration,

View file

@ -8,29 +8,32 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Select Quality" />
android:text="Select Quality"
/>
<Button
android:id="@+id/btnQuality1080"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1080p" />
<Button
android:id="@+id/btnQuality720"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="720p" />
android:text="720p"
/>
<Button
android:id="@+id/btnQuality480"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="480p" />
android:text="480p"
/>
<Button
android:id="@+id/btnQuality360"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="360p" />
android:text="360p"
/>
</LinearLayout>

View file

@ -73,6 +73,23 @@
android:layout_height="0dp"
android:layout_weight="1" />
<ImageButton
android:id="@+id/btnChangeQuality"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@drawable/transparent_circle_background"
android:contentDescription="Quality"
android:padding="16dp"
android:src="@drawable/ic_quality"
android:layout_gravity="end"
app:tint="@android:color/white"
/>
<Space
android:layout_width="16dp"
android:layout_height="0dp"
android:layout_weight="1" />
<ImageButton
android:id="@+id/btn_pip"
android:layout_width="wrap_content"

View file

@ -90,12 +90,6 @@
android:visibility="gone"
tools:visibility="visible" />
<Button
android:id="@+id/btnChangeQuality"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Change Quality" />
<Button
android:id="@+id/btn_skip_intro"
style="@style/Widget.Material3.Button.Icon"

View file

@ -1,7 +1,7 @@
import org.gradle.api.JavaVersion
object Versions {
const val appCode = 8
const val appCode = 9
const val appName = "0.14.2"
const val compileSdk = 34

View file

@ -0,0 +1,35 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:pathData="M10,7.75a0.75,0.75 0,0 1,1.142 -0.638l3.664,2.249a0.75,0.75 0,0 1,0 1.278l-3.664,2.25a0.75,0.75 0,0 1,-1.142 -0.64z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M12,17v4"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M8,21h8"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M4,3L20,3A2,2 0,0 1,22 5L22,15A2,2 0,0 1,20 17L4,17A2,2 0,0 1,2 15L2,5A2,2 0,0 1,4 3z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
</vector>

View file

@ -10,7 +10,7 @@
app:key="pref_player_mpv"
app:summary="@string/mpv_player_summary"
app:title="@string/mpv_player"
app:defaultValue="true"/>
app:defaultValue="false"/>
<ListPreference
app:defaultValue="mediacodec"
app:dependency="pref_player_mpv"

View file

@ -565,12 +565,13 @@ class JellyfinRepositoryImpl(
return jellyfinApi.userId!!
}
override fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int?, Int?> {
return when (transcodeResolution) {
1080 -> 14616000 to 384000
720 -> 7616000 to 384000
480 -> 2616000 to 384000
360 -> 292000 to 128000
1080 -> 8000000 to 384000 // Adjusted for 1080p
720 -> 2000000 to 384000 // Adjusted for 720p
480 -> 1000000 to 384000 // Adjusted for 480p
360 -> 800000 to 128000 // Adjusted for 360p
else -> null to null
}
}

View file

@ -28,7 +28,7 @@ androidx-work = "2.9.0"
coil = "2.6.0"
hilt = "2.51.1"
compose-destinations = "1.10.2"
jellyfin = "1.5.0-beta.4"
jellyfin = "1.5.0"
junit = "4.13.2"
kotlin = "2.0.0"
kotlinx-serialization = "1.7.0"

View file

@ -19,7 +19,6 @@ import androidx.media3.common.TrackSelectionParameters
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import dagger.hilt.android.lifecycle.HiltViewModel
import com.nomadics9.ananas.AppPreferences
import com.nomadics9.ananas.api.JellyfinApi
import com.nomadics9.ananas.models.FindroidSegment
@ -29,6 +28,7 @@ import com.nomadics9.ananas.models.Trickplay
import com.nomadics9.ananas.mpv.MPVPlayer
import com.nomadics9.ananas.player.video.R
import com.nomadics9.ananas.repository.JellyfinRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@ -40,12 +40,18 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.extensions.hlsSegmentApi
import org.jellyfin.sdk.model.api.ClientCapabilitiesDto
import org.jellyfin.sdk.model.api.DeviceProfile
import org.jellyfin.sdk.model.api.DirectPlayProfile
import org.jellyfin.sdk.model.api.DlnaProfileType
import org.jellyfin.sdk.model.api.EncodingContext
import org.jellyfin.sdk.model.api.MediaStreamProtocol
import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.api.PlaybackInfoDto
import org.jellyfin.sdk.model.api.ProfileCondition
import org.jellyfin.sdk.model.api.ProfileConditionType
import org.jellyfin.sdk.model.api.ProfileConditionValue
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
import org.jellyfin.sdk.model.api.SubtitleProfile
import org.jellyfin.sdk.model.api.TranscodeSeekInfo
@ -66,6 +72,7 @@ constructor(
private val savedStateHandle: SavedStateHandle,
) : ViewModel(), Player.Listener {
val player: Player
private var originalHeight: Int = 0
private val _uiState = MutableStateFlow(
UiState(
@ -186,6 +193,7 @@ constructor(
.setSubtitleConfigurations(mediaSubtitles)
.build()
mediaItems.add(mediaItem)
}
} catch (e: Exception) {
Timber.e(e)
@ -262,7 +270,8 @@ constructor(
val itemId = UUID.fromString(currentMediaItem.mediaId)
val seconds = player.currentPosition / 1000.0
val currentSegment = segments[itemId]?.find { segment -> seconds in segment.startTime..<segment.endTime }
val currentSegment =
segments[itemId]?.find { segment -> seconds in segment.startTime..<segment.endTime }
_uiState.update { it.copy(currentSegment = currentSegment) }
Timber.tag("SegmentInfo").d("currentSegment: %s", currentSegment)
@ -286,7 +295,8 @@ constructor(
try {
items.first { it.itemId.toString() == player.currentMediaItem?.mediaId }
.let { item ->
val itemTitle = if (item.parentIndexNumber != null && item.indexNumber != null) {
val itemTitle =
if (item.parentIndexNumber != null && item.indexNumber != null) {
if (item.indexNumberEnd == null) {
"S${item.parentIndexNumber}:E${item.indexNumber} - ${item.name}"
} else {
@ -322,13 +332,16 @@ constructor(
ExoPlayer.STATE_IDLE -> {
stateString = "ExoPlayer.STATE_IDLE -"
}
ExoPlayer.STATE_BUFFERING -> {
stateString = "ExoPlayer.STATE_BUFFERING -"
}
ExoPlayer.STATE_READY -> {
stateString = "ExoPlayer.STATE_READY -"
_uiState.update { it.copy(fileLoaded = true) }
}
ExoPlayer.STATE_ENDED -> {
stateString = "ExoPlayer.STATE_ENDED -"
eventsChannel.trySend(PlayerEvents.NavigateBack)
@ -356,7 +369,10 @@ constructor(
player.trackSelectionParameters = player.trackSelectionParameters
.buildUpon()
.setOverrideForType(
TrackSelectionOverride(player.currentTracks.groups.filter { it.type == trackType && it.isSupported }[index].mediaTrackGroup, 0),
TrackSelectionOverride(
player.currentTracks.groups.filter { it.type == trackType && it.isSupported }[index].mediaTrackGroup,
0
),
)
.setTrackTypeDisabled(trackType, false)
.build()
@ -373,7 +389,10 @@ constructor(
Timber.d("Trickplay Resolution: ${trickplayInfo.width}")
withContext(Dispatchers.Default) {
val maxIndex = ceil(trickplayInfo.thumbnailCount.toDouble().div(trickplayInfo.tileWidth * trickplayInfo.tileHeight)).toInt()
val maxIndex = ceil(
trickplayInfo.thumbnailCount.toDouble()
.div(trickplayInfo.tileWidth * trickplayInfo.tileHeight)
).toInt()
val bitmaps = mutableListOf<Bitmap>()
for (i in 0..maxIndex) {
@ -385,18 +404,32 @@ constructor(
val fullBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
for (offsetY in 0..<trickplayInfo.height * trickplayInfo.tileHeight step trickplayInfo.height) {
for (offsetX in 0..<trickplayInfo.width * trickplayInfo.tileWidth step trickplayInfo.width) {
val bitmap = Bitmap.createBitmap(fullBitmap, offsetX, offsetY, trickplayInfo.width, trickplayInfo.height)
val bitmap = Bitmap.createBitmap(
fullBitmap,
offsetX,
offsetY,
trickplayInfo.width,
trickplayInfo.height
)
bitmaps.add(bitmap)
}
}
}
}
_uiState.update { it.copy(currentTrickplay = Trickplay(trickplayInfo.interval, bitmaps)) }
_uiState.update {
it.copy(
currentTrickplay = Trickplay(
trickplayInfo.interval,
bitmaps
)
)
}
}
}
/**
* Get chapters of current item
*
* @return list of [PlayerChapter]
*/
private fun getChapters(): List<PlayerChapter>? {
@ -405,6 +438,7 @@ constructor(
/**
* Get the index of the current chapter
*
* @return the index of the current chapter
*/
private fun getCurrentChapterIndex(): Int? {
@ -421,6 +455,7 @@ constructor(
/**
* Get the index of the next chapter
*
* @return the index of the next chapter
*/
private fun getNextChapterIndex(): Int? {
@ -431,8 +466,10 @@ constructor(
}
/**
* Get the index of the previous chapter.
* Only use this for seeking as it will return the current chapter when player position is more than 5 seconds past the start of the chapter
* Get the index of the previous chapter. Only use this for seeking as it
* will return the current chapter when player position is more than 5
* seconds past the start of the chapter
*
* @return the index of the previous chapter
*/
private fun getPreviousChapterIndex(): Int? {
@ -448,11 +485,13 @@ constructor(
}
fun isFirstChapter(): Boolean? = getChapters()?.let { getCurrentChapterIndex() == 0 }
fun isLastChapter(): Boolean? = getChapters()?.let { chapters -> getCurrentChapterIndex() == chapters.size - 1 }
fun isLastChapter(): Boolean? =
getChapters()?.let { chapters -> getCurrentChapterIndex() == chapters.size - 1 }
/**
* Seek to chapter
* @param [chapterIndex] the index of the chapter to seek to
*
* @param chapterIndex the index of the chapter to seek to
* @return the [PlayerChapter] which has been sought to
*/
private fun seekToChapter(chapterIndex: Int): PlayerChapter? {
@ -463,6 +502,7 @@ constructor(
/**
* Seek to the next chapter
*
* @return the [PlayerChapter] which has been sought to
*/
fun seekToNextChapter(): PlayerChapter? {
@ -470,8 +510,9 @@ constructor(
}
/**
* Seek to the previous chapter
* Will seek to start of current chapter if player position is more than 5 seconds past start of chapter
* Seek to the previous chapter Will seek to start of current chapter if
* player position is more than 5 seconds past start of chapter
*
* @return the [PlayerChapter] which has been sought to
*/
fun seekToPreviousChapter(): PlayerChapter? {
@ -486,25 +527,26 @@ constructor(
private fun getTranscodeResolutions(preferredQuality: String): Int {
return when (preferredQuality) {
"1080p" -> 1080
"720p" -> 720
"480p" -> 480
"360p" -> 360
else -> 1080
"720p - 2Mbps" -> 720
"480p - 1Mbps" -> 480
"360p - 800kbps" -> 360
"Auto" -> 1
else -> 1
}
}
fun changeVideoQuality(quality: String) {
val mediaId = player.currentMediaItem?.mediaId ?: return
val itemId = UUID.fromString(mediaId)
//val playerItem = playerItemMap[itemId] ?: return
val currentItem = items.firstOrNull { it.itemId.toString() == mediaId } ?: return
val currentPosition = player.currentPosition
viewModelScope.launch {
try {
val transcodingResolution = getTranscodeResolutions(quality)
val (videoBitRate, audioBitRate) = jellyfinRepository.getVideoTranscodeBitRate(transcodingResolution)
if (transcodingResolution != null) {
val (videoBitRate, audioBitRate) = jellyfinRepository.getVideoTranscodeBitRate(
transcodingResolution
)
val deviceProfile = ClientCapabilitiesDto(
supportedCommands = emptyList(),
playableMediaTypes = emptyList(),
@ -513,10 +555,10 @@ constructor(
deviceProfile = DeviceProfile(
name = "AnanasUser",
id = jellyfinRepository.getUserId().toString(),
maxStaticBitrate = 1_000_000_000,
maxStreamingBitrate = 1_000_000_000,
maxStaticBitrate = videoBitRate,
maxStreamingBitrate = videoBitRate,
codecProfiles = emptyList(),
containerProfiles = emptyList(),
containerProfiles = listOf(),
directPlayProfiles = listOf(
DirectPlayProfile(type = DlnaProfileType.VIDEO),
DirectPlayProfile(type = DlnaProfileType.AUDIO),
@ -524,19 +566,33 @@ constructor(
transcodingProfiles = listOf(
TranscodingProfile(
container = "ts",
context = EncodingContext.STREAMING,
protocol = MediaStreamProtocol.HLS,
audioCodec = "aac",
videoCodec = "hevc",
audioCodec = "aac,ac3,eac3",
videoCodec = "hevc,h264",
type = DlnaProfileType.VIDEO,
conditions = emptyList(),
conditions = listOf(
ProfileCondition(
condition = ProfileConditionType.LESS_THAN_EQUAL,
property = ProfileConditionValue.VIDEO_BITRATE,
value = "8000000",
isRequired = true,
)
),
copyTimestamps = true,
enableSubtitlesInManifest = true,
transcodeSeekInfo = TranscodeSeekInfo.AUTO,
)
),
),
subtitleProfiles = listOf(
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("sub", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("vtt", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("ssa", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("pgs", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("dvb_teletext", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("dvd_subtitle", SubtitleDeliveryMethod.EXTERNAL)
),
)
)
@ -546,7 +602,7 @@ constructor(
PlaybackInfoDto(
userId = jellyfinApi.userId!!,
enableTranscoding = true,
enableDirectPlay = true,
enableDirectPlay = false,
enableDirectStream = true,
autoOpenLiveStream = true,
deviceProfile = deviceProfile.deviceProfile,
@ -554,47 +610,161 @@ constructor(
),
)
val playSessionId = playbackInfo.content.playSessionId
val getDeviceId =
jellyfinApi.devicesApi.getDeviceOptions(jellyfinApi.userId.toString())
val deviceId = getDeviceId.content.deviceId
if (playSessionId != null) {
jellyfinApi.api.hlsSegmentApi.stopEncodingProcess(
deviceId,
playSessionId
)
}
val mediaSource = playbackInfo.content.mediaSources.firstOrNull()
val transcodingUrl = mediaSource?.transcodingUrl
if (mediaSource == null) {
Timber.e("Media source is null")
} else {
Timber.d("Media source found: $mediaSource")
}
val transcodingUrl = mediaSource!!.transcodingUrl
val mediaSubtitles = currentItem.externalSubtitles.map { externalSubtitle ->
MediaItem.SubtitleConfiguration.Builder(externalSubtitle.uri)
.setLabel(externalSubtitle.title.ifBlank { application.getString(R.string.external) })
.setMimeType(externalSubtitle.mimeType)
.build()
}
// Timber.tag("MediaStreams").d("Media Streams: %s", mediaSource?.mediaStreams)
//TODO: Embedded sub support
// val embeddedSubtitles = mediaSource?.mediaStreams
// ?.filter { it.type == MediaStreamType.SUBTITLE && !it.isExternal }
// ?.map { mediaStream ->
// MediaItem.SubtitleConfiguration.Builder(Uri.parse(mediaStream.deliveryUrl!!))
// .setMimeType(
// when (mediaStream.codec) {
// "subrip" -> MimeTypes.APPLICATION_SUBRIP
// "webvtt" -> MimeTypes.APPLICATION_SUBRIP
// "ass" -> MimeTypes.TEXT_SSA
// else -> MimeTypes.TEXT_UNKNOWN
// }
// )
// .setLanguage(mediaStream.language ?: "und")
// .setLabel(mediaStream.title ?: "Embedded Subtitle")
// .build()
// }
// ?.toMutableList() ?: mutableListOf()
// URL METHOD
// val allSubtitles = embeddedSubtitles.apply { addAll(mediaSubtitles) }
val baseUrl = jellyfinApi.api.baseUrl
val cleanBaseUrl = baseUrl?.removePrefix("http://")?.removePrefix("https://")
val newUri = Uri.parse(transcodingUrl).buildUpon()
val staticUrl = jellyfinApi.videosApi.getVideoStreamUrl(
itemId,
static = true,
playSessionId = playSessionId,
deviceId = deviceId,
mediaSourceId = currentItem.mediaSourceId,
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL
)
val uri =
Uri.parse(transcodingUrl).buildUpon()
.scheme("https")
.authority(cleanBaseUrl)
//.appendQueryParameter("ffmpegTranscoding", "true")
.appendQueryParameter("maxVideoBitrate", videoBitRate.toString())
.appendQueryParameter("TranscodeReasons", "ContainerBitrateExceedsLimit")
.appendQueryParameter("static", "false")
.appendQueryParameter("maxHeight", videoBitRate.toString())
.appendQueryParameter("PlaySessionId", playSessionId)
.build()
fun Uri.Builder.setOrReplaceQueryParameter(
name: String,
value: String
): Uri.Builder {
val currentQueryParams = this.build().queryParameterNames
// Create a new builder for the URI
val newBuilder = Uri.parse(this.build().toString()).buildUpon()
// Track if the parameter was replaced
var parameterReplaced = false
// Re-add all parameters
currentQueryParams.forEach { param ->
val paramValue = this.build().getQueryParameter(param)
if (param == name) {
// Replace the parameter value
parameterReplaced = true
newBuilder.appendQueryParameter(name, value)
} else {
// Append the existing parameter
newBuilder.appendQueryParameter(param, paramValue)
}
}
// Append the new parameter only if it wasn't replaced
if (!parameterReplaced) {
newBuilder.appendQueryParameter(name, value)
}
return newBuilder
}
val uriBuilder = uri.buildUpon()
//.setOrReplaceQueryParameter("PlaySessionId", playSessionId!!)
if (transcodingResolution == 1) {
uriBuilder.setOrReplaceQueryParameter("EnableAdaptiveBitrateStreaming", "true")
uriBuilder.setOrReplaceQueryParameter("Static", "false")
uriBuilder.appendQueryParameter("MaxVideoHeight","1080" )
} else if (transcodingResolution == 720 || transcodingResolution == 480 || transcodingResolution == 360) {
uriBuilder.setOrReplaceQueryParameter(
"MaxVideoBitRate",
videoBitRate.toString()
)
uriBuilder.setOrReplaceQueryParameter("VideoBitrate", videoBitRate.toString())
uriBuilder.setOrReplaceQueryParameter("AudioBitrate", audioBitRate.toString())
uriBuilder.setOrReplaceQueryParameter("Static", "false")
uriBuilder.appendQueryParameter(
"MaxVideoHeight",
transcodingResolution.toString()
)
uriBuilder.appendQueryParameter("subtitleMethod", "External")
}
val newUri = uriBuilder.build()
Timber.e("URI IS %s", newUri)
val mediaItemBuilder = MediaItem.Builder()
.setMediaId(currentItem.itemId.toString())
.setUri(newUri)
if (transcodingResolution == 1080) {
mediaItemBuilder.setUri(staticUrl)
} else {
mediaItemBuilder.setUri(newUri)
}
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle(currentItem.name)
.build(),
)
//.setSubtitleConfigurations(player.currentMediaItem!!.subtitleConfigurations)
.setSubtitleConfigurations(mediaSubtitles)
player.setMediaItem(mediaItemBuilder.build())
player.prepare()
player.seekTo(currentPosition)
player.play()
val originalHeight = mediaSource.mediaStreams
?.firstOrNull { it.type == MediaStreamType.VIDEO }?.height ?: -1
// Store the original height
this@PlayerActivityViewModel.originalHeight = originalHeight
//isQualityChangeInProgress = true
}else if (transcodingResolution == 1080) {
jellyfinRepository.getStreamUrl(itemId, currentItem.mediaSourceId)
}
} catch (e: Exception) {
Timber.e(e)
}
}
}
fun getOriginalHeight(): Int {
return originalHeight
}
}
sealed interface PlayerEvents {
data object NavigateBack : PlayerEvents

View file

@ -90,7 +90,7 @@ constructor(
Constants.PREF_PLAYER_SEEK_FORWARD_INC,
DEFAULT_SEEK_FORWARD_INCREMENT_MS.toString(),
)!!.toLongOrNull() ?: DEFAULT_SEEK_FORWARD_INCREMENT_MS
val playerMpv get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_MPV, true)
val playerMpv get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_MPV, false)
val playerMpvHwdec get() = sharedPreferences.getString(Constants.PREF_PLAYER_MPV_HWDEC, "mediacodec")!!
val playerMpvVo get() = sharedPreferences.getString(Constants.PREF_PLAYER_MPV_VO, "gpu-next")!!
val playerMpvAo get() = sharedPreferences.getString(Constants.PREF_PLAYER_MPV_AO, "audiotrack")!!