feat: Quality change in player
This commit is contained in:
parent
2253780903
commit
ab090a01d7
7 changed files with 204 additions and 0 deletions
|
@ -1,5 +1,6 @@
|
||||||
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
|
||||||
|
@ -86,6 +87,12 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
|
|
||||||
binding = ActivityPlayerBinding.inflate(layoutInflater)
|
binding = ActivityPlayerBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
val changeQualityButton: Button = findViewById(R.id.btnChangeQuality)
|
||||||
|
changeQualityButton.setOnClickListener {
|
||||||
|
showQualitySelectionDialog()
|
||||||
|
}
|
||||||
|
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
|
||||||
binding.playerView.player = viewModel.player
|
binding.playerView.player = viewModel.player
|
||||||
|
@ -418,6 +425,19 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
} catch (_: IllegalArgumentException) { }
|
} catch (_: IllegalArgumentException) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showQualitySelectionDialog() {
|
||||||
|
val qualities = arrayOf("1080p", "720p", "480p", "360p")
|
||||||
|
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle("Select Video Quality")
|
||||||
|
.setItems(qualities) { _, which ->
|
||||||
|
val selectedQuality = qualities[which]
|
||||||
|
viewModel.changeVideoQuality(selectedQuality)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onPictureInPictureModeChanged(
|
override fun onPictureInPictureModeChanged(
|
||||||
isInPictureInPictureMode: Boolean,
|
isInPictureInPictureMode: Boolean,
|
||||||
newConfig: Configuration,
|
newConfig: Configuration,
|
||||||
|
|
36
app/phone/src/main/res/layout/dialog_quality_selection.xml
Normal file
36
app/phone/src/main/res/layout/dialog_quality_selection.xml
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
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" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnQuality480"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="480p" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnQuality360"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="360p" />
|
||||||
|
</LinearLayout>
|
|
@ -10,6 +10,7 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="center">
|
android:layout_gravity="center">
|
||||||
|
|
||||||
|
|
||||||
<!-- Video surface will be inserted as the first child of the content frame. -->
|
<!-- Video surface will be inserted as the first child of the content frame. -->
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
@ -89,6 +90,12 @@
|
||||||
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"
|
||||||
|
|
|
@ -112,4 +112,6 @@ interface JellyfinRepository {
|
||||||
suspend fun getDownloads(): List<FindroidItem>
|
suspend fun getDownloads(): List<FindroidItem>
|
||||||
|
|
||||||
fun getUserId(): UUID
|
fun getUserId(): UUID
|
||||||
|
|
||||||
|
fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int?, Int?>
|
||||||
}
|
}
|
||||||
|
|
|
@ -564,4 +564,14 @@ class JellyfinRepositoryImpl(
|
||||||
override fun getUserId(): UUID {
|
override fun getUserId(): UUID {
|
||||||
return jellyfinApi.userId!!
|
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
|
||||||
|
else -> null to null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -285,4 +285,8 @@ class JellyfinRepositoryOfflineImpl(
|
||||||
override fun getUserId(): UUID {
|
override fun getUserId(): UUID {
|
||||||
return jellyfinApi.userId!!
|
return jellyfinApi.userId!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int?, Int?> {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.nomadics9.ananas.viewmodels
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
@ -20,6 +21,7 @@ import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
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.models.FindroidSegment
|
import com.nomadics9.ananas.models.FindroidSegment
|
||||||
import com.nomadics9.ananas.models.PlayerChapter
|
import com.nomadics9.ananas.models.PlayerChapter
|
||||||
import com.nomadics9.ananas.models.PlayerItem
|
import com.nomadics9.ananas.models.PlayerItem
|
||||||
|
@ -38,6 +40,16 @@ 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.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.MediaStreamProtocol
|
||||||
|
import org.jellyfin.sdk.model.api.PlaybackInfoDto
|
||||||
|
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
|
||||||
|
import org.jellyfin.sdk.model.api.SubtitleProfile
|
||||||
|
import org.jellyfin.sdk.model.api.TranscodeSeekInfo
|
||||||
|
import org.jellyfin.sdk.model.api.TranscodingProfile
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -49,6 +61,7 @@ class PlayerActivityViewModel
|
||||||
constructor(
|
constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val jellyfinRepository: JellyfinRepository,
|
private val jellyfinRepository: JellyfinRepository,
|
||||||
|
private val jellyfinApi: JellyfinApi,
|
||||||
private val appPreferences: AppPreferences,
|
private val appPreferences: AppPreferences,
|
||||||
private val savedStateHandle: SavedStateHandle,
|
private val savedStateHandle: SavedStateHandle,
|
||||||
) : ViewModel(), Player.Listener {
|
) : ViewModel(), Player.Listener {
|
||||||
|
@ -469,6 +482,118 @@ constructor(
|
||||||
super.onIsPlayingChanged(isPlaying)
|
super.onIsPlayingChanged(isPlaying)
|
||||||
eventsChannel.trySend(PlayerEvents.IsPlayingChanged(isPlaying))
|
eventsChannel.trySend(PlayerEvents.IsPlayingChanged(isPlaying))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getTranscodeResolutions(preferredQuality: String): Int {
|
||||||
|
return when (preferredQuality) {
|
||||||
|
"1080p" -> 1080
|
||||||
|
"720p" -> 720
|
||||||
|
"480p" -> 480
|
||||||
|
"360p" -> 360
|
||||||
|
else -> 1080
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 deviceProfile = ClientCapabilitiesDto(
|
||||||
|
supportedCommands = emptyList(),
|
||||||
|
playableMediaTypes = emptyList(),
|
||||||
|
supportsMediaControl = true,
|
||||||
|
supportsPersistentIdentifier = true,
|
||||||
|
deviceProfile = DeviceProfile(
|
||||||
|
name = "Ananas User",
|
||||||
|
id = jellyfinRepository.getUserId().toString(),
|
||||||
|
maxStaticBitrate = 1_000_000_000,
|
||||||
|
maxStreamingBitrate = 1_000_000_000,
|
||||||
|
codecProfiles = emptyList(),
|
||||||
|
containerProfiles = emptyList(),
|
||||||
|
directPlayProfiles = listOf(
|
||||||
|
DirectPlayProfile(type = DlnaProfileType.VIDEO),
|
||||||
|
DirectPlayProfile(type = DlnaProfileType.AUDIO),
|
||||||
|
),
|
||||||
|
transcodingProfiles = listOf(
|
||||||
|
TranscodingProfile(
|
||||||
|
container = "ts",
|
||||||
|
protocol = MediaStreamProtocol.HLS,
|
||||||
|
audioCodec = "aac",
|
||||||
|
videoCodec = "hevc",
|
||||||
|
type = DlnaProfileType.VIDEO,
|
||||||
|
conditions = emptyList(),
|
||||||
|
copyTimestamps = true,
|
||||||
|
enableSubtitlesInManifest = true,
|
||||||
|
transcodeSeekInfo = TranscodeSeekInfo.AUTO,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
subtitleProfiles = listOf(
|
||||||
|
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
|
||||||
|
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val playbackInfo =
|
||||||
|
jellyfinApi.mediaInfoApi.getPostedPlaybackInfo(
|
||||||
|
itemId,
|
||||||
|
PlaybackInfoDto(
|
||||||
|
userId = jellyfinApi.userId!!,
|
||||||
|
enableTranscoding = true,
|
||||||
|
enableDirectPlay = true,
|
||||||
|
enableDirectStream = true,
|
||||||
|
autoOpenLiveStream = true,
|
||||||
|
deviceProfile = deviceProfile.deviceProfile,
|
||||||
|
maxStreamingBitrate = videoBitRate,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val playSessionId = playbackInfo.content.playSessionId
|
||||||
|
val mediaSource = playbackInfo.content.mediaSources.firstOrNull()
|
||||||
|
val transcodingUrl = mediaSource?.transcodingUrl
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// URL METHOD
|
||||||
|
val baseUrl = jellyfinApi.api.baseUrl
|
||||||
|
val cleanBaseUrl = baseUrl?.removePrefix("http://")?.removePrefix("https://")
|
||||||
|
val newUri = 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()
|
||||||
|
val mediaItemBuilder = MediaItem.Builder()
|
||||||
|
.setMediaId(currentItem.itemId.toString())
|
||||||
|
.setUri(newUri)
|
||||||
|
.setMediaMetadata(
|
||||||
|
MediaMetadata.Builder()
|
||||||
|
.setTitle(currentItem.name)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
//.setSubtitleConfigurations(player.currentMediaItem!!.subtitleConfigurations)
|
||||||
|
player.setMediaItem(mediaItemBuilder.build())
|
||||||
|
player.prepare()
|
||||||
|
player.seekTo(currentPosition)
|
||||||
|
player.play()
|
||||||
|
//isQualityChangeInProgress = true
|
||||||
|
}else if (transcodingResolution == 1080) {
|
||||||
|
jellyfinRepository.getStreamUrl(itemId, currentItem.mediaSourceId)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface PlayerEvents {
|
sealed interface PlayerEvents {
|
||||||
|
|
Loading…
Reference in a new issue