feat: Quality change in player (transcoded stream)
This commit is contained in:
parent
ab090a01d7
commit
8139119c35
20 changed files with 388 additions and 157 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -16,36 +16,10 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"attributes": [],
|
"attributes": [],
|
||||||
"versionCode": 8,
|
"versionCode": 9,
|
||||||
"versionName": "0.14.2",
|
"versionName": "0.14.2",
|
||||||
"outputFile": "ananas-v0.14.2-libre-armeabi-v7a.apk"
|
"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",
|
"type": "ONE_OF_MANY",
|
||||||
"filters": [
|
"filters": [
|
||||||
|
@ -55,9 +29,35 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"attributes": [],
|
"attributes": [],
|
||||||
"versionCode": 8,
|
"versionCode": 9,
|
||||||
"versionName": "0.14.2",
|
"versionName": "0.14.2",
|
||||||
"outputFile": "ananas-v0.14.2-libre-x86_64.apk"
|
"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",
|
"elementType": "File",
|
||||||
|
@ -67,9 +67,9 @@
|
||||||
"maxApi": 30,
|
"maxApi": 30,
|
||||||
"baselineProfiles": [
|
"baselineProfiles": [
|
||||||
"baselineProfiles/1/ananas-v0.14.2-libre-armeabi-v7a.dm",
|
"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.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,
|
"maxApi": 2147483647,
|
||||||
"baselineProfiles": [
|
"baselineProfiles": [
|
||||||
"baselineProfiles/0/ananas-v0.14.2-libre-armeabi-v7a.dm",
|
"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.dm",
|
||||||
"baselineProfiles/0/ananas-v0.14.2-libre-x86_64.dm"
|
"baselineProfiles/0/ananas-v0.14.2-libre-arm64-v8a.dm"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.nomadics9.ananas
|
package com.nomadics9.ananas
|
||||||
|
|
||||||
import android.app.AlertDialog
|
|
||||||
import android.app.AppOpsManager
|
import android.app.AppOpsManager
|
||||||
import android.app.PictureInPictureParams
|
import android.app.PictureInPictureParams
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -25,15 +24,18 @@ import android.widget.ImageView
|
||||||
import android.widget.Space
|
import android.widget.Space
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.ui.DefaultTimeBar
|
import androidx.media3.ui.DefaultTimeBar
|
||||||
import androidx.media3.ui.PlayerControlView
|
import androidx.media3.ui.PlayerControlView
|
||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
import androidx.navigation.navArgs
|
import androidx.navigation.navArgs
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import com.nomadics9.ananas.databinding.ActivityPlayerBinding
|
import com.nomadics9.ananas.databinding.ActivityPlayerBinding
|
||||||
import com.nomadics9.ananas.dialogs.SpeedSelectionDialogFragment
|
import com.nomadics9.ananas.dialogs.SpeedSelectionDialogFragment
|
||||||
|
@ -88,7 +90,7 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
binding = ActivityPlayerBinding.inflate(layoutInflater)
|
binding = ActivityPlayerBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
val changeQualityButton: Button = findViewById(R.id.btnChangeQuality)
|
val changeQualityButton: ImageButton = findViewById(R.id.btnChangeQuality)
|
||||||
changeQualityButton.setOnClickListener {
|
changeQualityButton.setOnClickListener {
|
||||||
showQualitySelectionDialog()
|
showQualitySelectionDialog()
|
||||||
}
|
}
|
||||||
|
@ -426,9 +428,15 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showQualitySelectionDialog() {
|
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")
|
.setTitle("Select Video Quality")
|
||||||
.setItems(qualities) { _, which ->
|
.setItems(qualities) { _, which ->
|
||||||
val selectedQuality = qualities[which]
|
val selectedQuality = qualities[which]
|
||||||
|
@ -438,6 +446,9 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
override fun onPictureInPictureModeChanged(
|
override fun onPictureInPictureModeChanged(
|
||||||
isInPictureInPictureMode: Boolean,
|
isInPictureInPictureMode: Boolean,
|
||||||
newConfig: Configuration,
|
newConfig: Configuration,
|
||||||
|
|
|
@ -8,29 +8,32 @@
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Select Quality" />
|
android:text="Select Quality"
|
||||||
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/btnQuality1080"
|
android:id="@+id/btnQuality1080"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="1080p" />
|
android:text="1080p" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/btnQuality720"
|
android:id="@+id/btnQuality720"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="720p" />
|
android:text="720p"
|
||||||
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/btnQuality480"
|
android:id="@+id/btnQuality480"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="480p" />
|
android:text="480p"
|
||||||
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/btnQuality360"
|
android:id="@+id/btnQuality360"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="360p" />
|
android:text="360p"
|
||||||
|
/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
@ -73,6 +73,23 @@
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1" />
|
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
|
<ImageButton
|
||||||
android:id="@+id/btn_pip"
|
android:id="@+id/btn_pip"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|
|
@ -90,12 +90,6 @@
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btnChangeQuality"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Change Quality" />
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/btn_skip_intro"
|
android:id="@+id/btn_skip_intro"
|
||||||
style="@style/Widget.Material3.Button.Icon"
|
style="@style/Widget.Material3.Button.Icon"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import org.gradle.api.JavaVersion
|
import org.gradle.api.JavaVersion
|
||||||
|
|
||||||
object Versions {
|
object Versions {
|
||||||
const val appCode = 8
|
const val appCode = 9
|
||||||
const val appName = "0.14.2"
|
const val appName = "0.14.2"
|
||||||
|
|
||||||
const val compileSdk = 34
|
const val compileSdk = 34
|
||||||
|
|
35
core/src/main/res/drawable/ic_quality.xml
Normal file
35
core/src/main/res/drawable/ic_quality.xml
Normal 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>
|
|
@ -10,7 +10,7 @@
|
||||||
app:key="pref_player_mpv"
|
app:key="pref_player_mpv"
|
||||||
app:summary="@string/mpv_player_summary"
|
app:summary="@string/mpv_player_summary"
|
||||||
app:title="@string/mpv_player"
|
app:title="@string/mpv_player"
|
||||||
app:defaultValue="true"/>
|
app:defaultValue="false"/>
|
||||||
<ListPreference
|
<ListPreference
|
||||||
app:defaultValue="mediacodec"
|
app:defaultValue="mediacodec"
|
||||||
app:dependency="pref_player_mpv"
|
app:dependency="pref_player_mpv"
|
||||||
|
|
|
@ -565,12 +565,13 @@ class JellyfinRepositoryImpl(
|
||||||
return jellyfinApi.userId!!
|
return jellyfinApi.userId!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int?, Int?> {
|
override fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int?, Int?> {
|
||||||
return when (transcodeResolution) {
|
return when (transcodeResolution) {
|
||||||
1080 -> 14616000 to 384000
|
1080 -> 8000000 to 384000 // Adjusted for 1080p
|
||||||
720 -> 7616000 to 384000
|
720 -> 2000000 to 384000 // Adjusted for 720p
|
||||||
480 -> 2616000 to 384000
|
480 -> 1000000 to 384000 // Adjusted for 480p
|
||||||
360 -> 292000 to 128000
|
360 -> 800000 to 128000 // Adjusted for 360p
|
||||||
else -> null to null
|
else -> null to null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ androidx-work = "2.9.0"
|
||||||
coil = "2.6.0"
|
coil = "2.6.0"
|
||||||
hilt = "2.51.1"
|
hilt = "2.51.1"
|
||||||
compose-destinations = "1.10.2"
|
compose-destinations = "1.10.2"
|
||||||
jellyfin = "1.5.0-beta.4"
|
jellyfin = "1.5.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
kotlin = "2.0.0"
|
kotlin = "2.0.0"
|
||||||
kotlinx-serialization = "1.7.0"
|
kotlinx-serialization = "1.7.0"
|
||||||
|
|
|
@ -19,7 +19,6 @@ import androidx.media3.common.TrackSelectionParameters
|
||||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import com.nomadics9.ananas.AppPreferences
|
import com.nomadics9.ananas.AppPreferences
|
||||||
import com.nomadics9.ananas.api.JellyfinApi
|
import com.nomadics9.ananas.api.JellyfinApi
|
||||||
import com.nomadics9.ananas.models.FindroidSegment
|
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.mpv.MPVPlayer
|
||||||
import com.nomadics9.ananas.player.video.R
|
import com.nomadics9.ananas.player.video.R
|
||||||
import com.nomadics9.ananas.repository.JellyfinRepository
|
import com.nomadics9.ananas.repository.JellyfinRepository
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
@ -40,12 +40,18 @@ import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
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.ClientCapabilitiesDto
|
||||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||||
import org.jellyfin.sdk.model.api.DirectPlayProfile
|
import org.jellyfin.sdk.model.api.DirectPlayProfile
|
||||||
import org.jellyfin.sdk.model.api.DlnaProfileType
|
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.MediaStreamProtocol
|
||||||
|
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||||
import org.jellyfin.sdk.model.api.PlaybackInfoDto
|
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.SubtitleDeliveryMethod
|
||||||
import org.jellyfin.sdk.model.api.SubtitleProfile
|
import org.jellyfin.sdk.model.api.SubtitleProfile
|
||||||
import org.jellyfin.sdk.model.api.TranscodeSeekInfo
|
import org.jellyfin.sdk.model.api.TranscodeSeekInfo
|
||||||
|
@ -66,6 +72,7 @@ constructor(
|
||||||
private val savedStateHandle: SavedStateHandle,
|
private val savedStateHandle: SavedStateHandle,
|
||||||
) : ViewModel(), Player.Listener {
|
) : ViewModel(), Player.Listener {
|
||||||
val player: Player
|
val player: Player
|
||||||
|
private var originalHeight: Int = 0
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(
|
private val _uiState = MutableStateFlow(
|
||||||
UiState(
|
UiState(
|
||||||
|
@ -186,6 +193,7 @@ constructor(
|
||||||
.setSubtitleConfigurations(mediaSubtitles)
|
.setSubtitleConfigurations(mediaSubtitles)
|
||||||
.build()
|
.build()
|
||||||
mediaItems.add(mediaItem)
|
mediaItems.add(mediaItem)
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
|
@ -262,7 +270,8 @@ constructor(
|
||||||
val itemId = UUID.fromString(currentMediaItem.mediaId)
|
val itemId = UUID.fromString(currentMediaItem.mediaId)
|
||||||
val seconds = player.currentPosition / 1000.0
|
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) }
|
_uiState.update { it.copy(currentSegment = currentSegment) }
|
||||||
Timber.tag("SegmentInfo").d("currentSegment: %s", currentSegment)
|
Timber.tag("SegmentInfo").d("currentSegment: %s", currentSegment)
|
||||||
|
|
||||||
|
@ -286,7 +295,8 @@ constructor(
|
||||||
try {
|
try {
|
||||||
items.first { it.itemId.toString() == player.currentMediaItem?.mediaId }
|
items.first { it.itemId.toString() == player.currentMediaItem?.mediaId }
|
||||||
.let { item ->
|
.let { item ->
|
||||||
val itemTitle = if (item.parentIndexNumber != null && item.indexNumber != null) {
|
val itemTitle =
|
||||||
|
if (item.parentIndexNumber != null && item.indexNumber != null) {
|
||||||
if (item.indexNumberEnd == null) {
|
if (item.indexNumberEnd == null) {
|
||||||
"S${item.parentIndexNumber}:E${item.indexNumber} - ${item.name}"
|
"S${item.parentIndexNumber}:E${item.indexNumber} - ${item.name}"
|
||||||
} else {
|
} else {
|
||||||
|
@ -322,13 +332,16 @@ constructor(
|
||||||
ExoPlayer.STATE_IDLE -> {
|
ExoPlayer.STATE_IDLE -> {
|
||||||
stateString = "ExoPlayer.STATE_IDLE -"
|
stateString = "ExoPlayer.STATE_IDLE -"
|
||||||
}
|
}
|
||||||
|
|
||||||
ExoPlayer.STATE_BUFFERING -> {
|
ExoPlayer.STATE_BUFFERING -> {
|
||||||
stateString = "ExoPlayer.STATE_BUFFERING -"
|
stateString = "ExoPlayer.STATE_BUFFERING -"
|
||||||
}
|
}
|
||||||
|
|
||||||
ExoPlayer.STATE_READY -> {
|
ExoPlayer.STATE_READY -> {
|
||||||
stateString = "ExoPlayer.STATE_READY -"
|
stateString = "ExoPlayer.STATE_READY -"
|
||||||
_uiState.update { it.copy(fileLoaded = true) }
|
_uiState.update { it.copy(fileLoaded = true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
ExoPlayer.STATE_ENDED -> {
|
ExoPlayer.STATE_ENDED -> {
|
||||||
stateString = "ExoPlayer.STATE_ENDED -"
|
stateString = "ExoPlayer.STATE_ENDED -"
|
||||||
eventsChannel.trySend(PlayerEvents.NavigateBack)
|
eventsChannel.trySend(PlayerEvents.NavigateBack)
|
||||||
|
@ -356,7 +369,10 @@ constructor(
|
||||||
player.trackSelectionParameters = player.trackSelectionParameters
|
player.trackSelectionParameters = player.trackSelectionParameters
|
||||||
.buildUpon()
|
.buildUpon()
|
||||||
.setOverrideForType(
|
.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)
|
.setTrackTypeDisabled(trackType, false)
|
||||||
.build()
|
.build()
|
||||||
|
@ -373,7 +389,10 @@ constructor(
|
||||||
Timber.d("Trickplay Resolution: ${trickplayInfo.width}")
|
Timber.d("Trickplay Resolution: ${trickplayInfo.width}")
|
||||||
|
|
||||||
withContext(Dispatchers.Default) {
|
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>()
|
val bitmaps = mutableListOf<Bitmap>()
|
||||||
|
|
||||||
for (i in 0..maxIndex) {
|
for (i in 0..maxIndex) {
|
||||||
|
@ -385,18 +404,32 @@ constructor(
|
||||||
val fullBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
|
val fullBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
|
||||||
for (offsetY in 0..<trickplayInfo.height * trickplayInfo.tileHeight step trickplayInfo.height) {
|
for (offsetY in 0..<trickplayInfo.height * trickplayInfo.tileHeight step trickplayInfo.height) {
|
||||||
for (offsetX in 0..<trickplayInfo.width * trickplayInfo.tileWidth step trickplayInfo.width) {
|
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)
|
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
|
* Get chapters of current item
|
||||||
|
*
|
||||||
* @return list of [PlayerChapter]
|
* @return list of [PlayerChapter]
|
||||||
*/
|
*/
|
||||||
private fun getChapters(): List<PlayerChapter>? {
|
private fun getChapters(): List<PlayerChapter>? {
|
||||||
|
@ -405,6 +438,7 @@ constructor(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the index of the current chapter
|
* Get the index of the current chapter
|
||||||
|
*
|
||||||
* @return the index of the current chapter
|
* @return the index of the current chapter
|
||||||
*/
|
*/
|
||||||
private fun getCurrentChapterIndex(): Int? {
|
private fun getCurrentChapterIndex(): Int? {
|
||||||
|
@ -421,6 +455,7 @@ constructor(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the index of the next chapter
|
* Get the index of the next chapter
|
||||||
|
*
|
||||||
* @return the index of the next chapter
|
* @return the index of the next chapter
|
||||||
*/
|
*/
|
||||||
private fun getNextChapterIndex(): Int? {
|
private fun getNextChapterIndex(): Int? {
|
||||||
|
@ -431,8 +466,10 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the index of the previous chapter.
|
* Get the index of the previous chapter. Only use this for seeking as it
|
||||||
* 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
|
* 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
|
* @return the index of the previous chapter
|
||||||
*/
|
*/
|
||||||
private fun getPreviousChapterIndex(): Int? {
|
private fun getPreviousChapterIndex(): Int? {
|
||||||
|
@ -448,11 +485,13 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isFirstChapter(): Boolean? = getChapters()?.let { getCurrentChapterIndex() == 0 }
|
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
|
* 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
|
* @return the [PlayerChapter] which has been sought to
|
||||||
*/
|
*/
|
||||||
private fun seekToChapter(chapterIndex: Int): PlayerChapter? {
|
private fun seekToChapter(chapterIndex: Int): PlayerChapter? {
|
||||||
|
@ -463,6 +502,7 @@ constructor(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seek to the next chapter
|
* Seek to the next chapter
|
||||||
|
*
|
||||||
* @return the [PlayerChapter] which has been sought to
|
* @return the [PlayerChapter] which has been sought to
|
||||||
*/
|
*/
|
||||||
fun seekToNextChapter(): PlayerChapter? {
|
fun seekToNextChapter(): PlayerChapter? {
|
||||||
|
@ -470,8 +510,9 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seek to the previous chapter
|
* Seek to the previous chapter Will seek to start of current chapter if
|
||||||
* Will seek to start of current chapter if player position is more than 5 seconds past start of chapter
|
* player position is more than 5 seconds past start of chapter
|
||||||
|
*
|
||||||
* @return the [PlayerChapter] which has been sought to
|
* @return the [PlayerChapter] which has been sought to
|
||||||
*/
|
*/
|
||||||
fun seekToPreviousChapter(): PlayerChapter? {
|
fun seekToPreviousChapter(): PlayerChapter? {
|
||||||
|
@ -486,37 +527,38 @@ constructor(
|
||||||
private fun getTranscodeResolutions(preferredQuality: String): Int {
|
private fun getTranscodeResolutions(preferredQuality: String): Int {
|
||||||
return when (preferredQuality) {
|
return when (preferredQuality) {
|
||||||
"1080p" -> 1080
|
"1080p" -> 1080
|
||||||
"720p" -> 720
|
"720p - 2Mbps" -> 720
|
||||||
"480p" -> 480
|
"480p - 1Mbps" -> 480
|
||||||
"360p" -> 360
|
"360p - 800kbps" -> 360
|
||||||
else -> 1080
|
"Auto" -> 1
|
||||||
|
else -> 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeVideoQuality(quality: String) {
|
fun changeVideoQuality(quality: String) {
|
||||||
val mediaId = player.currentMediaItem?.mediaId ?: return
|
val mediaId = player.currentMediaItem?.mediaId ?: return
|
||||||
val itemId = UUID.fromString(mediaId)
|
val itemId = UUID.fromString(mediaId)
|
||||||
//val playerItem = playerItemMap[itemId] ?: return
|
|
||||||
val currentItem = items.firstOrNull { it.itemId.toString() == mediaId } ?: return
|
val currentItem = items.firstOrNull { it.itemId.toString() == mediaId } ?: return
|
||||||
val currentPosition = player.currentPosition
|
val currentPosition = player.currentPosition
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val transcodingResolution = getTranscodeResolutions(quality)
|
val transcodingResolution = getTranscodeResolutions(quality)
|
||||||
val (videoBitRate, audioBitRate) = jellyfinRepository.getVideoTranscodeBitRate(transcodingResolution)
|
val (videoBitRate, audioBitRate) = jellyfinRepository.getVideoTranscodeBitRate(
|
||||||
if (transcodingResolution != null) {
|
transcodingResolution
|
||||||
|
)
|
||||||
val deviceProfile = ClientCapabilitiesDto(
|
val deviceProfile = ClientCapabilitiesDto(
|
||||||
supportedCommands = emptyList(),
|
supportedCommands = emptyList(),
|
||||||
playableMediaTypes = emptyList(),
|
playableMediaTypes = emptyList(),
|
||||||
supportsMediaControl = true,
|
supportsMediaControl = true,
|
||||||
supportsPersistentIdentifier = true,
|
supportsPersistentIdentifier = true,
|
||||||
deviceProfile = DeviceProfile(
|
deviceProfile = DeviceProfile(
|
||||||
name = "Ananas User",
|
name = "AnanasUser",
|
||||||
id = jellyfinRepository.getUserId().toString(),
|
id = jellyfinRepository.getUserId().toString(),
|
||||||
maxStaticBitrate = 1_000_000_000,
|
maxStaticBitrate = videoBitRate,
|
||||||
maxStreamingBitrate = 1_000_000_000,
|
maxStreamingBitrate = videoBitRate,
|
||||||
codecProfiles = emptyList(),
|
codecProfiles = emptyList(),
|
||||||
containerProfiles = emptyList(),
|
containerProfiles = listOf(),
|
||||||
directPlayProfiles = listOf(
|
directPlayProfiles = listOf(
|
||||||
DirectPlayProfile(type = DlnaProfileType.VIDEO),
|
DirectPlayProfile(type = DlnaProfileType.VIDEO),
|
||||||
DirectPlayProfile(type = DlnaProfileType.AUDIO),
|
DirectPlayProfile(type = DlnaProfileType.AUDIO),
|
||||||
|
@ -524,19 +566,33 @@ constructor(
|
||||||
transcodingProfiles = listOf(
|
transcodingProfiles = listOf(
|
||||||
TranscodingProfile(
|
TranscodingProfile(
|
||||||
container = "ts",
|
container = "ts",
|
||||||
|
context = EncodingContext.STREAMING,
|
||||||
protocol = MediaStreamProtocol.HLS,
|
protocol = MediaStreamProtocol.HLS,
|
||||||
audioCodec = "aac",
|
audioCodec = "aac,ac3,eac3",
|
||||||
videoCodec = "hevc",
|
videoCodec = "hevc,h264",
|
||||||
type = DlnaProfileType.VIDEO,
|
type = DlnaProfileType.VIDEO,
|
||||||
conditions = emptyList(),
|
conditions = listOf(
|
||||||
|
ProfileCondition(
|
||||||
|
condition = ProfileConditionType.LESS_THAN_EQUAL,
|
||||||
|
property = ProfileConditionValue.VIDEO_BITRATE,
|
||||||
|
value = "8000000",
|
||||||
|
isRequired = true,
|
||||||
|
)
|
||||||
|
),
|
||||||
copyTimestamps = true,
|
copyTimestamps = true,
|
||||||
enableSubtitlesInManifest = true,
|
enableSubtitlesInManifest = true,
|
||||||
transcodeSeekInfo = TranscodeSeekInfo.AUTO,
|
transcodeSeekInfo = TranscodeSeekInfo.AUTO,
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
subtitleProfiles = listOf(
|
subtitleProfiles = listOf(
|
||||||
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
|
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
|
||||||
SubtitleProfile("ass", 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(
|
PlaybackInfoDto(
|
||||||
userId = jellyfinApi.userId!!,
|
userId = jellyfinApi.userId!!,
|
||||||
enableTranscoding = true,
|
enableTranscoding = true,
|
||||||
enableDirectPlay = true,
|
enableDirectPlay = false,
|
||||||
enableDirectStream = true,
|
enableDirectStream = true,
|
||||||
autoOpenLiveStream = true,
|
autoOpenLiveStream = true,
|
||||||
deviceProfile = deviceProfile.deviceProfile,
|
deviceProfile = deviceProfile.deviceProfile,
|
||||||
|
@ -554,48 +610,162 @@ constructor(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
val playSessionId = playbackInfo.content.playSessionId
|
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 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()
|
||||||
|
|
||||||
|
// val allSubtitles = embeddedSubtitles.apply { addAll(mediaSubtitles) }
|
||||||
|
|
||||||
// URL METHOD
|
|
||||||
val baseUrl = jellyfinApi.api.baseUrl
|
val baseUrl = jellyfinApi.api.baseUrl
|
||||||
val cleanBaseUrl = baseUrl?.removePrefix("http://")?.removePrefix("https://")
|
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")
|
.scheme("https")
|
||||||
.authority(cleanBaseUrl)
|
.authority(cleanBaseUrl)
|
||||||
//.appendQueryParameter("ffmpegTranscoding", "true")
|
|
||||||
.appendQueryParameter("maxVideoBitrate", videoBitRate.toString())
|
|
||||||
.appendQueryParameter("TranscodeReasons", "ContainerBitrateExceedsLimit")
|
|
||||||
.appendQueryParameter("static", "false")
|
|
||||||
.appendQueryParameter("maxHeight", videoBitRate.toString())
|
|
||||||
.appendQueryParameter("PlaySessionId", playSessionId)
|
|
||||||
.build()
|
.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()
|
val mediaItemBuilder = MediaItem.Builder()
|
||||||
.setMediaId(currentItem.itemId.toString())
|
.setMediaId(currentItem.itemId.toString())
|
||||||
.setUri(newUri)
|
if (transcodingResolution == 1080) {
|
||||||
|
mediaItemBuilder.setUri(staticUrl)
|
||||||
|
} else {
|
||||||
|
mediaItemBuilder.setUri(newUri)
|
||||||
|
}
|
||||||
.setMediaMetadata(
|
.setMediaMetadata(
|
||||||
MediaMetadata.Builder()
|
MediaMetadata.Builder()
|
||||||
.setTitle(currentItem.name)
|
.setTitle(currentItem.name)
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
//.setSubtitleConfigurations(player.currentMediaItem!!.subtitleConfigurations)
|
.setSubtitleConfigurations(mediaSubtitles)
|
||||||
|
|
||||||
player.setMediaItem(mediaItemBuilder.build())
|
player.setMediaItem(mediaItemBuilder.build())
|
||||||
player.prepare()
|
player.prepare()
|
||||||
player.seekTo(currentPosition)
|
player.seekTo(currentPosition)
|
||||||
player.play()
|
player.play()
|
||||||
|
|
||||||
|
val originalHeight = mediaSource.mediaStreams
|
||||||
|
?.firstOrNull { it.type == MediaStreamType.VIDEO }?.height ?: -1
|
||||||
|
// Store the original height
|
||||||
|
this@PlayerActivityViewModel.originalHeight = originalHeight
|
||||||
|
|
||||||
//isQualityChangeInProgress = true
|
//isQualityChangeInProgress = true
|
||||||
}else if (transcodingResolution == 1080) {
|
|
||||||
jellyfinRepository.getStreamUrl(itemId, currentItem.mediaSourceId)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getOriginalHeight(): Int {
|
||||||
|
return originalHeight
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sealed interface PlayerEvents {
|
sealed interface PlayerEvents {
|
||||||
data object NavigateBack : PlayerEvents
|
data object NavigateBack : PlayerEvents
|
||||||
data class IsPlayingChanged(val isPlaying: Boolean) : PlayerEvents
|
data class IsPlayingChanged(val isPlaying: Boolean) : PlayerEvents
|
||||||
|
|
|
@ -90,7 +90,7 @@ constructor(
|
||||||
Constants.PREF_PLAYER_SEEK_FORWARD_INC,
|
Constants.PREF_PLAYER_SEEK_FORWARD_INC,
|
||||||
DEFAULT_SEEK_FORWARD_INCREMENT_MS.toString(),
|
DEFAULT_SEEK_FORWARD_INCREMENT_MS.toString(),
|
||||||
)!!.toLongOrNull() ?: DEFAULT_SEEK_FORWARD_INCREMENT_MS
|
)!!.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 playerMpvHwdec get() = sharedPreferences.getString(Constants.PREF_PLAYER_MPV_HWDEC, "mediacodec")!!
|
||||||
val playerMpvVo get() = sharedPreferences.getString(Constants.PREF_PLAYER_MPV_VO, "gpu-next")!!
|
val playerMpvVo get() = sharedPreferences.getString(Constants.PREF_PLAYER_MPV_VO, "gpu-next")!!
|
||||||
val playerMpvAo get() = sharedPreferences.getString(Constants.PREF_PLAYER_MPV_AO, "audiotrack")!!
|
val playerMpvAo get() = sharedPreferences.getString(Constants.PREF_PLAYER_MPV_AO, "audiotrack")!!
|
||||||
|
|
Loading…
Reference in a new issue