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": [],
|
||||
"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"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
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: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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,37 +527,38 @@ 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(),
|
||||
supportsMediaControl = true,
|
||||
supportsPersistentIdentifier = true,
|
||||
deviceProfile = DeviceProfile(
|
||||
name = "Ananas User",
|
||||
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,48 +610,162 @@ 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
|
||||
data class IsPlayingChanged(val isPlaying: Boolean) : PlayerEvents
|
||||
|
|
|
@ -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")!!
|
||||
|
|
Loading…
Reference in a new issue