bugfixes: getDeviceId() / code: New Enum VideoQuality

This commit is contained in:
nomadics9 2024-07-20 06:31:53 +03:00
parent 36dd8480e1
commit 4e8ee15d0a
8 changed files with 1450 additions and 1172 deletions

View file

@ -24,19 +24,16 @@ 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
import com.nomadics9.ananas.dialogs.TrackSelectionDialogFragment
@ -45,6 +42,7 @@ import com.nomadics9.ananas.utils.PlayerGestureHelper
import com.nomadics9.ananas.utils.PreviewScrubListener
import com.nomadics9.ananas.viewmodels.PlayerActivityViewModel
import com.nomadics9.ananas.viewmodels.PlayerEvents
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -54,7 +52,6 @@ var isControlsLocked: Boolean = false
@AndroidEntryPoint
class PlayerActivity : BasePlayerActivity() {
@Inject
lateinit var appPreferences: AppPreferences
@ -115,7 +112,8 @@ class PlayerActivity : BasePlayerActivity() {
configureInsets(lockedControls)
if (appPreferences.playerGestures) {
playerGestureHelper = PlayerGestureHelper(
playerGestureHelper =
PlayerGestureHelper(
appPreferences,
this,
binding.playerView,
@ -155,7 +153,12 @@ class PlayerActivity : BasePlayerActivity() {
skipButton.text =
getString(CoreR.string.skip_intro_button)
skipButton.isVisible =
!isInPictureInPictureMode && !buttonPressed && (showSkip == true || (binding.playerView.isControllerFullyVisible && currentSegment?.skip == true))
!isInPictureInPictureMode &&
!buttonPressed &&
(
showSkip == true ||
(binding.playerView.isControllerFullyVisible && currentSegment?.skip == true)
)
watchCreditsButton.isVisible = false
}
@ -167,7 +170,10 @@ class PlayerActivity : BasePlayerActivity() {
getString(CoreR.string.skip_credit_button_last)
}
skipButton.isVisible =
!isInPictureInPictureMode && !buttonPressed && currentSegment?.skip == true && !binding.playerView.isControllerFullyVisible
!isInPictureInPictureMode &&
!buttonPressed &&
currentSegment?.skip == true &&
!binding.playerView.isControllerFullyVisible
watchCreditsButton.isVisible = skipButton.isVisible
}
@ -181,12 +187,15 @@ class PlayerActivity : BasePlayerActivity() {
when (currentSegment?.type) {
"intro" -> {
skipButton.isVisible =
!buttonPressed && (showSkip == true || (visibility == View.VISIBLE && currentSegment?.skip == true))
!buttonPressed &&
(showSkip == true || (visibility == View.VISIBLE && currentSegment?.skip == true))
}
"credit" -> {
skipButton.isVisible =
!buttonPressed && currentSegment?.skip == true && visibility == View.GONE
!buttonPressed &&
currentSegment?.skip == true &&
visibility == View.GONE
watchCreditsButton.isVisible = skipButton.isVisible
}
}
@ -268,7 +277,8 @@ class PlayerActivity : BasePlayerActivity() {
if (appPreferences.playerPipGesture) {
try {
setPictureInPictureParams(pipParams(event.isPlaying))
} catch (_: IllegalArgumentException) { }
} catch (_: IllegalArgumentException) {
}
}
}
}
@ -346,7 +356,8 @@ class PlayerActivity : BasePlayerActivity() {
if (appPreferences.playerTrickplay) {
val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview)
previewScrubListener = PreviewScrubListener(
previewScrubListener =
PreviewScrubListener(
imagePreview,
timeBar,
viewModel.player,
@ -381,14 +392,16 @@ class PlayerActivity : BasePlayerActivity() {
private fun pipParams(enableAutoEnter: Boolean = viewModel.player.isPlaying): PictureInPictureParams {
val displayAspectRatio = Rational(binding.playerView.width, binding.playerView.height)
val aspectRatio = binding.playerView.player?.videoSize?.let {
val aspectRatio =
binding.playerView.player?.videoSize?.let {
Rational(
it.width.coerceAtMost((it.height * 2.39f).toInt()),
it.height.coerceAtMost((it.width * 2.39f).toInt()),
)
}
val sourceRectHint = if (displayAspectRatio < aspectRatio!!) {
val sourceRectHint =
if (displayAspectRatio < aspectRatio!!) {
val space = ((binding.playerView.height - (binding.playerView.width.toFloat() / aspectRatio.toFloat())) / 2).toInt()
Rect(
0,
@ -406,7 +419,9 @@ class PlayerActivity : BasePlayerActivity() {
)
}
val builder = PictureInPictureParams.Builder()
val builder =
PictureInPictureParams
.Builder()
.setAspectRatio(aspectRatio)
.setSourceRectHint(sourceRectHint)
@ -424,30 +439,51 @@ class PlayerActivity : BasePlayerActivity() {
try {
enterPictureInPictureMode(pipParams())
} catch (_: IllegalArgumentException) { }
} catch (_: IllegalArgumentException) {
}
}
private fun showQualitySelectionDialog() {
val height = viewModel.getOriginalHeight()
val qualityEntries = resources.getStringArray(CoreR.array.quality_entries).toList()
val qualityValues = resources.getStringArray(CoreR.array.quality_values).toList()
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")
// Map entries to values
val qualityMap = qualityEntries.zip(qualityValues).toMap()
val qualities: List<String> =
when (height) {
0 -> qualityEntries
in 1001..1999 ->
listOf(
qualityEntries[0],
"${qualityEntries[1]} (1080p)",
qualityEntries[2],
qualityEntries[3],
qualityEntries[4],
qualityEntries[5],
)
in 2000..3000 ->
listOf(
qualityEntries[0],
"${qualityEntries[1]} (4K)",
qualityEntries[2],
qualityEntries[3],
qualityEntries[4],
qualityEntries[5],
)
else -> qualityEntries
}
MaterialAlertDialogBuilder(this)
.setTitle("Select Video Quality")
.setItems(qualities) { _, which ->
val selectedQuality = qualities[which]
viewModel.changeVideoQuality(selectedQuality)
.setItems(qualities.toTypedArray()) { _, which ->
val selectedQualityEntry = qualities[which]
val selectedQualityValue =
qualityMap.entries.find { it.key.contains(selectedQualityEntry.split(" ")[0]) }?.value ?: selectedQualityEntry
viewModel.changeVideoQuality(selectedQualityValue)
}.show()
}
.show()
}
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
@ -463,7 +499,8 @@ class PlayerActivity : BasePlayerActivity() {
playerGestureHelper?.updateZoomMode(false)
// Brightness mode Auto
window.attributes = window.attributes.apply {
window.attributes =
window.attributes.apply {
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
}
}
@ -472,11 +509,14 @@ class PlayerActivity : BasePlayerActivity() {
playerGestureHelper?.updateZoomMode(wasZoom)
// Override auto brightness
window.attributes = window.attributes.apply {
screenBrightness = if (appPreferences.playerBrightnessRemember) {
window.attributes =
window.attributes.apply {
screenBrightness =
if (appPreferences.playerBrightnessRemember) {
appPreferences.playerBrightness
} else {
Settings.System.getInt(
Settings.System
.getInt(
contentResolver,
Settings.System.SCREEN_BRIGHTNESS,
).toFloat() / 255

View file

@ -17,6 +17,7 @@ import com.nomadics9.ananas.models.FindroidSource
import com.nomadics9.ananas.models.FindroidSources
import com.nomadics9.ananas.models.FindroidTrickplayInfo
import com.nomadics9.ananas.models.UiText
import com.nomadics9.ananas.models.VideoQuality
import com.nomadics9.ananas.models.toFindroidEpisodeDto
import com.nomadics9.ananas.models.toFindroidMediaStreamDto
import com.nomadics9.ananas.models.toFindroidMovieDto
@ -50,13 +51,13 @@ class DownloaderImpl(
storageIndex: Int,
): Pair<Long, UiText?> {
try {
Timber.d("Downloading item: ${item.id} with sourceId: $sourceId")
val source =
jellyfinRepository.getMediaSources(item.id, true).first { it.id == sourceId }
val segments = jellyfinRepository.getSegmentsTimestamps(item.id)
val trickplayInfo = if (item is FindroidSources) {
val trickplayInfo =
if (item is FindroidSources) {
item.trickplayInfo?.get(sourceId)
} else {
null
@ -96,9 +97,13 @@ class DownloaderImpl(
return Pair(
-1,
if (e.message != null) UiText.DynamicString(e.message!!) else UiText.StringResource(
CoreR.string.unknown_error
if (e.message != null) {
UiText.DynamicString(e.message!!)
} else {
UiText.StringResource(
CoreR.string.unknown_error,
)
},
)
}
}
@ -110,7 +115,7 @@ class DownloaderImpl(
trickplayInfo: FindroidTrickplayInfo?,
segments: List<FindroidSegment>?,
path: Uri,
quality: String
quality: String,
): Pair<Long, UiText?> {
val transcodingUrl = getTranscodedUrl(item.id, quality)
when (item) {
@ -126,7 +131,9 @@ class DownloaderImpl(
if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
}
val request = DownloadManager.Request(transcodingUrl)
val request =
DownloadManager
.Request(transcodingUrl)
.setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
@ -139,7 +146,8 @@ class DownloaderImpl(
is FindroidEpisode -> {
database.insertShow(
jellyfinRepository.getShow(item.seriesId)
jellyfinRepository
.getShow(item.seriesId)
.toFindroidShowDto(appPreferences.currentServer!!),
)
database.insertSeason(
@ -156,7 +164,9 @@ class DownloaderImpl(
if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
}
val request = DownloadManager.Request(transcodingUrl)
val request =
DownloadManager
.Request(transcodingUrl)
.setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
@ -190,7 +200,9 @@ class DownloaderImpl(
if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
}
val request = DownloadManager.Request(source.path.toUri())
val request =
DownloadManager
.Request(source.path.toUri())
.setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
@ -203,7 +215,8 @@ class DownloaderImpl(
is FindroidEpisode -> {
database.insertShow(
jellyfinRepository.getShow(item.seriesId)
jellyfinRepository
.getShow(item.seriesId)
.toFindroidShowDto(appPreferences.currentServer!!),
)
database.insertSeason(
@ -219,7 +232,9 @@ class DownloaderImpl(
if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
}
val request = DownloadManager.Request(source.path.toUri())
val request =
DownloadManager
.Request(source.path.toUri())
.setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
@ -233,15 +248,20 @@ class DownloaderImpl(
return Pair(-1, null)
}
override suspend fun cancelDownload(item: FindroidItem, source: FindroidSource) {
override suspend fun cancelDownload(
item: FindroidItem,
source: FindroidSource,
) {
if (source.downloadId != null) {
downloadManager.remove(source.downloadId!!)
}
deleteItem(item, source)
}
override suspend fun deleteItem(item: FindroidItem, source: FindroidSource) {
override suspend fun deleteItem(
item: FindroidItem,
source: FindroidSource,
) {
when (item) {
is FindroidMovie -> {
database.deleteMovie(item.id)
@ -283,11 +303,14 @@ class DownloaderImpl(
if (downloadId == null) {
return Pair(downloadStatus, progress)
}
val query = DownloadManager.Query()
val query =
DownloadManager
.Query()
.setFilterById(downloadId)
val cursor = downloadManager.query(query)
if (cursor.moveToFirst()) {
downloadStatus = cursor.getInt(
downloadStatus =
cursor.getInt(
cursor.getColumnIndexOrThrow(
DownloadManager.COLUMN_STATUS,
),
@ -320,20 +343,23 @@ class DownloaderImpl(
val storageLocation = context.getExternalFilesDirs(null)[storageIndex]
for (mediaStream in source.mediaStreams.filter { it.isExternal }) {
val id = UUID.randomUUID()
val streamPath = Uri.fromFile(
val streamPath =
Uri.fromFile(
File(
storageLocation,
"downloads/${item.id}.${source.id}.$id.download"
)
"downloads/${item.id}.${source.id}.$id.download",
),
)
database.insertMediaStream(
mediaStream.toFindroidMediaStreamDto(
id,
source.id,
streamPath.path.orEmpty()
streamPath.path.orEmpty(),
),
)
)
val request = DownloadManager.Request(Uri.parse(mediaStream.path))
val request =
DownloadManager
.Request(Uri.parse(mediaStream.path))
.setTitle(mediaStream.title)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
@ -347,7 +373,7 @@ class DownloaderImpl(
private fun downloadEmbeddedMediaStreams(
item: FindroidItem,
source: FindroidSource,
storageIndex: Int = 0
storageIndex: Int = 0,
) {
val storageLocation = context.getExternalFilesDirs(null)[storageIndex]
val subtitleStreams = source.mediaStreams.filter { !it.isExternal && it.type == MediaStreamType.SUBTITLE && it.path != null }
@ -357,20 +383,23 @@ class DownloaderImpl(
deliveryUrl = deliveryUrl.replace("Stream.srt", "Stream.vtt")
}
val id = UUID.randomUUID()
val streamPath = Uri.fromFile(
val streamPath =
Uri.fromFile(
File(
storageLocation,
"downloads/${item.id}.${source.id}.$id.download"
)
"downloads/${item.id}.${source.id}.$id.download",
),
)
database.insertMediaStream(
mediaStream.toFindroidMediaStreamDto(
id,
source.id,
streamPath.path.orEmpty()
streamPath.path.orEmpty(),
),
)
)
val request = DownloadManager.Request(Uri.parse(deliveryUrl))
val request =
DownloadManager
.Request(Uri.parse(deliveryUrl))
.setTitle(mediaStream.title)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
@ -382,19 +411,21 @@ class DownloaderImpl(
}
}
private suspend fun downloadTrickplayData(
itemId: UUID,
sourceId: String,
trickplayInfo: FindroidTrickplayInfo,
) {
val maxIndex = ceil(
trickplayInfo.thumbnailCount.toDouble()
.div(trickplayInfo.tileWidth * trickplayInfo.tileHeight)
val maxIndex =
ceil(
trickplayInfo.thumbnailCount
.toDouble()
.div(trickplayInfo.tileWidth * trickplayInfo.tileHeight),
).toInt()
val byteArrays = mutableListOf<ByteArray>()
for (i in 0..maxIndex) {
jellyfinRepository.getTrickplayData(
jellyfinRepository
.getTrickplayData(
itemId,
trickplayInfo.width,
i,
@ -420,52 +451,46 @@ class DownloaderImpl(
}
}
private suspend fun getTranscodedUrl(itemId: UUID, quality: String): Uri? {
val maxBitrate = when (quality) {
"720p" -> 2000000 // 2 Mbps
"480p" -> 1000000 // 1 Mbps
"360p" -> 800000 // 800Kbps
else -> 2000000 // Default to 2 Mbps if not specified
}
private suspend fun getTranscodedUrl(
itemId: UUID,
quality: String,
): Uri? {
val videoQuality = VideoQuality.fromString(quality)!!
return try {
val deviceProfile = jellyfinRepository.buildDeviceProfile(maxBitrate,"mkv", EncodingContext.STATIC)
val playbackInfo = jellyfinRepository.getPostedPlaybackInfo(itemId,false,deviceProfile,maxBitrate)
val mediaSourceId = playbackInfo.content.mediaSources.firstOrNull()?.id!!
val deviceProfile =
jellyfinRepository.buildDeviceProfile(
VideoQuality.getBitrate(videoQuality),
"mkv",
EncodingContext.STATIC,
)
val playbackInfo =
jellyfinRepository.getPostedPlaybackInfo(
itemId,
false,
deviceProfile,
VideoQuality.getBitrate(videoQuality),
)
val mediaSourceId =
playbackInfo.content.mediaSources
.firstOrNull()
?.id!!
val playSessionId = playbackInfo.content.playSessionId!!
val deviceId = jellyfinRepository.getDeviceId()
val downloadUrl = jellyfinRepository.getVideoStreambyContainerUrl(itemId, deviceId, mediaSourceId, playSessionId, maxBitrate, "ts")
val downloadUrl =
jellyfinRepository.getVideoStreambyContainerUrl(
itemId,
deviceId,
mediaSourceId,
playSessionId,
VideoQuality.getBitrate(videoQuality),
VideoQuality.getQualityInt(videoQuality),
"mkv",
)
val transcodeUri = buildTranscodeUri(downloadUrl, maxBitrate, quality)
Timber.d("Constructed Transcode URL: $transcodeUri")
transcodeUri
return downloadUrl.toUri()
} catch (e: Exception) {
Timber.e(e)
null
}
}
private fun buildTranscodeUri(
transcodingUrl: String,
maxBitrate: Int,
quality: String
): Uri {
val resolution = when (quality) {
"720p" -> "720"
"480p" -> "480"
"360p" -> "360"
else -> "720"
}
return Uri.parse(transcodingUrl).buildUpon()
.appendQueryParameter("MaxVideoHeight", resolution)
.appendQueryParameter("MaxVideoBitRate", maxBitrate.toString())
.appendQueryParameter("subtitleMethod", "External")
//.appendQueryParameter("api_key", apiKey)
.build()
}
}

View file

@ -26,13 +26,17 @@
<item>opensles</item>
</string-array>
<string-array name="quality_entries">
<item>Auto</item>
<item>Original</item>
<item>1080p - 8Mbps</item>
<item>720p - 2Mbps</item>
<item>480p - 1Mbps</item>
<item>360p - 800Kbps</item>
</string-array>
<string-array name="quality_values">
<item>Auto</item>
<item>Original</item>
<item>1080p</item>
<item>720p</item>
<item>480p</item>
<item>360p</item>

View file

@ -0,0 +1,25 @@
package com.nomadics9.ananas.models
enum class VideoQuality(
val bitrate: Int,
val qualityString: String,
val qualityInt: Int,
) {
PAuto(1, "Auto", 1080),
POriginal(1000000000, "Original", 1080),
P1080(8000000, "1080p", 1080),
P720(2000000, "720p", 720),
P480(1000000, "480p", 480),
P360(700000, "360p", 360),
;
companion object {
fun fromString(quality: String): VideoQuality? = entries.find { it.qualityString == quality }
fun getBitrate(quality: VideoQuality): Int = quality.bitrate
fun getQualityString(quality: VideoQuality): String = quality.qualityString
fun getQualityInt(quality: VideoQuality): Int = quality.qualityInt
}
}

View file

@ -14,8 +14,6 @@ import kotlinx.coroutines.flow.Flow
import org.jellyfin.sdk.api.client.Response
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.DeviceInfo
import org.jellyfin.sdk.model.api.DeviceInfoQueryResult
import org.jellyfin.sdk.model.api.DeviceProfile
import org.jellyfin.sdk.model.api.EncodingContext
import org.jellyfin.sdk.model.api.ItemFields
@ -31,7 +29,9 @@ interface JellyfinRepository {
suspend fun getUserViews(): List<BaseItemDto>
suspend fun getItem(itemId: UUID): BaseItemDto
suspend fun getEpisode(itemId: UUID): FindroidEpisode
suspend fun getMovie(itemId: UUID): FindroidMovie
suspend fun getShow(itemId: UUID): FindroidShow
@ -72,7 +72,10 @@ interface JellyfinRepository {
suspend fun getLatestMedia(parentId: UUID): List<FindroidItem>
suspend fun getSeasons(seriesId: UUID, offline: Boolean = false): List<FindroidSeason>
suspend fun getSeasons(
seriesId: UUID,
offline: Boolean = false,
): List<FindroidSeason>
suspend fun getNextUp(seriesId: UUID? = null): List<FindroidEpisode>
@ -85,21 +88,40 @@ interface JellyfinRepository {
offline: Boolean = false,
): List<FindroidEpisode>
suspend fun getMediaSources(itemId: UUID, includePath: Boolean = false): List<FindroidSource>
suspend fun getMediaSources(
itemId: UUID,
includePath: Boolean = false,
): List<FindroidSource>
suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String, playSessionId: String? = null): String
suspend fun getStreamUrl(
itemId: UUID,
mediaSourceId: String,
playSessionId: String? = null,
): String
suspend fun getSegmentsTimestamps(itemId: UUID): List<FindroidSegment>?
suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray?
suspend fun getTrickplayData(
itemId: UUID,
width: Int,
index: Int,
): ByteArray?
suspend fun postCapabilities()
suspend fun postPlaybackStart(itemId: UUID)
suspend fun postPlaybackStop(itemId: UUID, positionTicks: Long, playedPercentage: Int)
suspend fun postPlaybackStop(
itemId: UUID,
positionTicks: Long,
playedPercentage: Int,
)
suspend fun postPlaybackProgress(itemId: UUID, positionTicks: Long, isPaused: Boolean)
suspend fun postPlaybackProgress(
itemId: UUID,
positionTicks: Long,
isPaused: Boolean,
)
suspend fun markAsFavorite(itemId: UUID)
@ -121,15 +143,36 @@ interface JellyfinRepository {
suspend fun getDeviceId(): String
suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int, Int>
suspend fun buildDeviceProfile(
maxBitrate: Int,
container: String,
context: EncodingContext,
): DeviceProfile
suspend fun buildDeviceProfile(maxBitrate: Int, container: String, context: EncodingContext): DeviceProfile
suspend fun getVideoStreambyContainerUrl(
itemId: UUID,
deviceId: String,
mediaSourceId: String,
playSessionId: String,
videoBitrate: Int,
maxHeight: Int,
container: String,
): String
suspend fun getVideoStreambyContainerUrl(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int, container: String): String
suspend fun getTranscodedVideoStream(
itemId: UUID,
deviceId: String,
mediaSourceId: String,
playSessionId: String,
videoBitrate: Int,
): String
suspend fun getTranscodedVideoStream(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int): String
suspend fun getPostedPlaybackInfo(itemId: UUID, enableDirectStream: Boolean, deviceProfile: DeviceProfile ,maxBitrate: Int): Response<PlaybackInfoResponse>
suspend fun getPostedPlaybackInfo(
itemId: UUID,
enableDirectStream: Boolean,
deviceProfile: DeviceProfile,
maxBitrate: Int,
): Response<PlaybackInfoResponse>
suspend fun stopEncodingProcess(playSessionId: String)
}

View file

@ -17,6 +17,7 @@ import com.nomadics9.ananas.models.FindroidSegments
import com.nomadics9.ananas.models.FindroidShow
import com.nomadics9.ananas.models.FindroidSource
import com.nomadics9.ananas.models.SortBy
import com.nomadics9.ananas.models.VideoQuality
import com.nomadics9.ananas.models.toFindroidCollection
import com.nomadics9.ananas.models.toFindroidEpisode
import com.nomadics9.ananas.models.toFindroidItem
@ -36,7 +37,6 @@ import org.jellyfin.sdk.api.client.extensions.hlsSegmentApi
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ClientCapabilitiesDto
import org.jellyfin.sdk.model.api.DeviceInfoQueryResult
import org.jellyfin.sdk.model.api.DeviceOptionsDto
import org.jellyfin.sdk.model.api.DeviceProfile
import org.jellyfin.sdk.model.api.DirectPlayProfile
@ -57,7 +57,6 @@ import org.jellyfin.sdk.model.api.PublicSystemInfo
import org.jellyfin.sdk.model.api.SortOrder
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
import org.jellyfin.sdk.model.api.SubtitleProfile
import org.jellyfin.sdk.model.api.TranscodeReason
import org.jellyfin.sdk.model.api.TranscodeSeekInfo
import org.jellyfin.sdk.model.api.TranscodingProfile
import org.jellyfin.sdk.model.api.UserConfiguration
@ -71,53 +70,68 @@ class JellyfinRepositoryImpl(
private val database: ServerDatabaseDao,
private val appPreferences: AppPreferences,
) : JellyfinRepository {
override suspend fun getPublicSystemInfo(): PublicSystemInfo = withContext(Dispatchers.IO) {
override suspend fun getPublicSystemInfo(): PublicSystemInfo =
withContext(Dispatchers.IO) {
jellyfinApi.systemApi.getPublicSystemInfo().content
}
override suspend fun getUserViews(): List<BaseItemDto> = withContext(Dispatchers.IO) {
jellyfinApi.viewsApi.getUserViews(jellyfinApi.userId!!).content.items.orEmpty()
override suspend fun getUserViews(): List<BaseItemDto> =
withContext(Dispatchers.IO) {
jellyfinApi.viewsApi
.getUserViews(jellyfinApi.userId!!)
.content.items
.orEmpty()
}
override suspend fun getItem(itemId: UUID): BaseItemDto = withContext(Dispatchers.IO) {
override suspend fun getItem(itemId: UUID): BaseItemDto =
withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getItem(itemId, jellyfinApi.userId!!).content
}
override suspend fun getEpisode(itemId: UUID): FindroidEpisode =
withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getItem(
jellyfinApi.userLibraryApi
.getItem(
itemId,
jellyfinApi.userId!!,
).content.toFindroidEpisode(this@JellyfinRepositoryImpl, database)!!
).content
.toFindroidEpisode(this@JellyfinRepositoryImpl, database)!!
}
override suspend fun getMovie(itemId: UUID): FindroidMovie =
withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getItem(
jellyfinApi.userLibraryApi
.getItem(
itemId,
jellyfinApi.userId!!,
).content.toFindroidMovie(this@JellyfinRepositoryImpl, database)
).content
.toFindroidMovie(this@JellyfinRepositoryImpl, database)
}
override suspend fun getShow(itemId: UUID): FindroidShow =
withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getItem(
jellyfinApi.userLibraryApi
.getItem(
itemId,
jellyfinApi.userId!!,
).content.toFindroidShow(this@JellyfinRepositoryImpl)
).content
.toFindroidShow(this@JellyfinRepositoryImpl)
}
override suspend fun getSeason(itemId: UUID): FindroidSeason =
withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getItem(
jellyfinApi.userLibraryApi
.getItem(
itemId,
jellyfinApi.userId!!,
).content.toFindroidSeason(this@JellyfinRepositoryImpl)
).content
.toFindroidSeason(this@JellyfinRepositoryImpl)
}
override suspend fun getLibraries(): List<FindroidCollection> =
withContext(Dispatchers.IO) {
jellyfinApi.itemsApi.getItems(
jellyfinApi.itemsApi
.getItems(
jellyfinApi.userId!!,
).content.items
.orEmpty()
@ -134,7 +148,8 @@ class JellyfinRepositoryImpl(
limit: Int?,
): List<FindroidItem> =
withContext(Dispatchers.IO) {
jellyfinApi.itemsApi.getItems(
jellyfinApi.itemsApi
.getItems(
jellyfinApi.userId!!,
parentId = parentId,
includeItemTypes = includeTypes,
@ -154,9 +169,10 @@ class JellyfinRepositoryImpl(
recursive: Boolean,
sortBy: SortBy,
sortOrder: SortOrder,
): Flow<PagingData<FindroidItem>> {
return Pager(
config = PagingConfig(
): Flow<PagingData<FindroidItem>> =
Pager(
config =
PagingConfig(
pageSize = 10,
maxSize = 100,
enablePlaceholders = false,
@ -172,14 +188,15 @@ class JellyfinRepositoryImpl(
)
},
).flow
}
override suspend fun getPersonItems(
personIds: List<UUID>,
includeTypes: List<BaseItemKind>?,
recursive: Boolean,
): List<FindroidItem> = withContext(Dispatchers.IO) {
jellyfinApi.itemsApi.getItems(
): List<FindroidItem> =
withContext(Dispatchers.IO) {
jellyfinApi.itemsApi
.getItems(
jellyfinApi.userId!!,
personIds = personIds,
includeItemTypes = includeTypes,
@ -193,10 +210,12 @@ class JellyfinRepositoryImpl(
override suspend fun getFavoriteItems(): List<FindroidItem> =
withContext(Dispatchers.IO) {
jellyfinApi.itemsApi.getItems(
jellyfinApi.itemsApi
.getItems(
jellyfinApi.userId!!,
filters = listOf(ItemFilter.IS_FAVORITE),
includeItemTypes = listOf(
includeItemTypes =
listOf(
BaseItemKind.MOVIE,
BaseItemKind.SERIES,
BaseItemKind.EPISODE,
@ -209,10 +228,12 @@ class JellyfinRepositoryImpl(
override suspend fun getSearchItems(searchQuery: String): List<FindroidItem> =
withContext(Dispatchers.IO) {
jellyfinApi.itemsApi.getItems(
jellyfinApi.itemsApi
.getItems(
jellyfinApi.userId!!,
searchTerm = searchQuery,
includeItemTypes = listOf(
includeItemTypes =
listOf(
BaseItemKind.MOVIE,
BaseItemKind.SERIES,
BaseItemKind.EPISODE,
@ -224,12 +245,15 @@ class JellyfinRepositoryImpl(
}
override suspend fun getResumeItems(): List<FindroidItem> {
val items = withContext(Dispatchers.IO) {
jellyfinApi.itemsApi.getResumeItems(
val items =
withContext(Dispatchers.IO) {
jellyfinApi.itemsApi
.getResumeItems(
jellyfinApi.userId!!,
limit = 12,
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE),
).content.items.orEmpty()
).content.items
.orEmpty()
}
return items.mapNotNull {
it.toFindroidItem(this, database)
@ -237,8 +261,10 @@ class JellyfinRepositoryImpl(
}
override suspend fun getLatestMedia(parentId: UUID): List<FindroidItem> {
val items = withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getLatestMedia(
val items =
withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi
.getLatestMedia(
jellyfinApi.userId!!,
parentId = parentId,
limit = 16,
@ -249,10 +275,15 @@ class JellyfinRepositoryImpl(
}
}
override suspend fun getSeasons(seriesId: UUID, offline: Boolean): List<FindroidSeason> =
override suspend fun getSeasons(
seriesId: UUID,
offline: Boolean,
): List<FindroidSeason> =
withContext(Dispatchers.IO) {
if (!offline) {
jellyfinApi.showsApi.getSeasons(seriesId, jellyfinApi.userId!!).content.items
jellyfinApi.showsApi
.getSeasons(seriesId, jellyfinApi.userId!!)
.content.items
.orEmpty()
.map { it.toFindroidSeason(this@JellyfinRepositoryImpl) }
} else {
@ -262,7 +293,8 @@ class JellyfinRepositoryImpl(
override suspend fun getNextUp(seriesId: UUID?): List<FindroidEpisode> =
withContext(Dispatchers.IO) {
jellyfinApi.showsApi.getNextUp(
jellyfinApi.showsApi
.getNextUp(
jellyfinApi.userId!!,
limit = 24,
seriesId = seriesId,
@ -282,7 +314,8 @@ class JellyfinRepositoryImpl(
): List<FindroidEpisode> =
withContext(Dispatchers.IO) {
if (!offline) {
jellyfinApi.showsApi.getEpisodes(
jellyfinApi.showsApi
.getEpisodes(
seriesId,
jellyfinApi.userId!!,
seasonId = seasonId,
@ -297,33 +330,41 @@ class JellyfinRepositoryImpl(
}
}
override suspend fun getMediaSources(itemId: UUID, includePath: Boolean): List<FindroidSource> =
override suspend fun getMediaSources(
itemId: UUID,
includePath: Boolean,
): List<FindroidSource> =
withContext(Dispatchers.IO) {
val sources = mutableListOf<FindroidSource>()
sources.addAll(
jellyfinApi.mediaInfoApi.getPostedPlaybackInfo(
jellyfinApi.mediaInfoApi
.getPostedPlaybackInfo(
itemId,
PlaybackInfoDto(
userId = jellyfinApi.userId!!,
deviceProfile = DeviceProfile(
deviceProfile =
DeviceProfile(
name = "Direct play all",
maxStaticBitrate = 1_000_000_000,
maxStreamingBitrate = 1_000_000_000,
codecProfiles = emptyList(),
containerProfiles = emptyList(),
directPlayProfiles = listOf(
directPlayProfiles =
listOf(
DirectPlayProfile(type = DlnaProfileType.VIDEO),
DirectPlayProfile(type = DlnaProfileType.AUDIO),
),
transcodingProfiles = emptyList(),
subtitleProfiles = listOf(
subtitleProfiles =
listOf(
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
),
),
maxStreamingBitrate = 1_000_000_000,
),
).content.mediaSources.map {
).content.mediaSources
.map {
it.toFindroidSource(
this@JellyfinRepositoryImpl,
itemId,
@ -337,24 +378,30 @@ class JellyfinRepositoryImpl(
sources
}
override suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String, playSessionId: String?): String =
override suspend fun getStreamUrl(
itemId: UUID,
mediaSourceId: String,
playSessionId: String?,
): String =
withContext(Dispatchers.IO) {
// val deviceId = getDeviceId()
try {
val url = if (playSessionId != null) {
val url =
if (playSessionId != null) {
jellyfinApi.videosApi.getVideoStreamUrl(
itemId,
static = true,
mediaSourceId = mediaSourceId,
playSessionId = playSessionId,
deviceId = getDeviceId(),
context = EncodingContext.STATIC
// deviceId = deviceId,
context = EncodingContext.STREAMING,
)
} else {
jellyfinApi.videosApi.getVideoStreamUrl(
itemId,
static = true,
mediaSourceId = mediaSourceId,
deviceId = getDeviceId(),
// deviceId = deviceId,
)
}
url
@ -377,12 +424,15 @@ class JellyfinRepositoryImpl(
pathParameters["itemId"] = itemId
try {
val segmentToConvert = jellyfinApi.api.get<FindroidSegments>(
val segmentToConvert =
jellyfinApi.api
.get<FindroidSegments>(
"/Episode/{itemId}/IntroSkipperSegments",
pathParameters,
).content
val segmentConverted = mutableListOf(
val segmentConverted =
mutableListOf(
segmentToConvert.intro!!.let {
FindroidSegment(
type = "intro",
@ -413,7 +463,11 @@ class JellyfinRepositoryImpl(
}
}
override suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray? =
override suspend fun getTrickplayData(
itemId: UUID,
width: Int,
index: Int,
): ByteArray? =
withContext(Dispatchers.IO) {
try {
try {
@ -421,9 +475,13 @@ class JellyfinRepositoryImpl(
if (sources != null) {
return@withContext File(sources.first(), index.toString()).readBytes()
}
} catch (_: Exception) { }
} catch (_: Exception) {
}
return@withContext jellyfinApi.trickplayApi.getTrickplayTileImage(itemId, width, index).content.toByteArray()
return@withContext jellyfinApi.trickplayApi
.getTrickplayTileImage(itemId, width, index)
.content
.toByteArray()
} catch (e: Exception) {
return@withContext null
}
@ -434,7 +492,8 @@ class JellyfinRepositoryImpl(
withContext(Dispatchers.IO) {
jellyfinApi.sessionApi.postCapabilities(
playableMediaTypes = listOf(MediaType.VIDEO),
supportedCommands = listOf(
supportedCommands =
listOf(
GeneralCommandType.VOLUME_UP,
GeneralCommandType.VOLUME_DOWN,
GeneralCommandType.TOGGLE_MUTE,
@ -570,57 +629,62 @@ class JellyfinRepositoryImpl(
}
}
override suspend fun getUserConfiguration(): UserConfiguration = withContext(Dispatchers.IO) {
jellyfinApi.userApi.getCurrentUser().content.configuration!!
override suspend fun getUserConfiguration(): UserConfiguration =
withContext(Dispatchers.IO) {
jellyfinApi.userApi
.getCurrentUser()
.content.configuration!!
}
override suspend fun getDownloads(): List<FindroidItem> =
withContext(Dispatchers.IO) {
val items = mutableListOf<FindroidItem>()
items.addAll(
database.getMoviesByServerId(appPreferences.currentServer!!)
database
.getMoviesByServerId(appPreferences.currentServer!!)
.map { it.toFindroidMovie(database, jellyfinApi.userId!!) },
)
items.addAll(
database.getShowsByServerId(appPreferences.currentServer!!)
database
.getShowsByServerId(appPreferences.currentServer!!)
.map { it.toFindroidShow(database, jellyfinApi.userId!!) },
)
items
}
override fun getUserId(): UUID {
return jellyfinApi.userId!!
}
override fun getUserId(): UUID = jellyfinApi.userId!!
override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int, Int> {
return when (transcodeResolution) {
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 -> 12000000 to 384000
}
}
override suspend fun buildDeviceProfile(maxBitrate: Int, container: String, context: EncodingContext): DeviceProfile {
val deviceProfile = ClientCapabilitiesDto(
override suspend fun buildDeviceProfile(
maxBitrate: Int,
container: String,
context: EncodingContext,
): DeviceProfile {
val deviceProfile =
ClientCapabilitiesDto(
supportedCommands = emptyList(),
playableMediaTypes = emptyList(),
playableMediaTypes =
listOf(
MediaType.VIDEO,
MediaType.AUDIO,
MediaType.UNKNOWN,
),
supportsMediaControl = true,
supportsPersistentIdentifier = true,
deviceProfile = DeviceProfile(
deviceProfile =
DeviceProfile(
name = "AnanasUser",
id = getUserId().toString(),
maxStaticBitrate = maxBitrate,
maxStreamingBitrate = maxBitrate,
codecProfiles = emptyList(),
containerProfiles = listOf(),
directPlayProfiles = listOf(
directPlayProfiles =
listOf(
DirectPlayProfile(type = DlnaProfileType.VIDEO),
DirectPlayProfile(type = DlnaProfileType.AUDIO),
),
transcodingProfiles = listOf(
transcodingProfiles =
listOf(
TranscodingProfile(
container = container,
context = context,
@ -628,20 +692,22 @@ class JellyfinRepositoryImpl(
audioCodec = "aac,ac3,eac3",
videoCodec = "hevc,h264",
type = DlnaProfileType.VIDEO,
conditions = listOf(
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(
subtitleProfiles =
listOf(
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("sub", SubtitleDeliveryMethod.EXTERNAL),
@ -649,16 +715,21 @@ class JellyfinRepositoryImpl(
SubtitleProfile("ssa", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("pgs", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("dvb_teletext", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("dvd_subtitle", SubtitleDeliveryMethod.EXTERNAL)
SubtitleProfile("dvd_subtitle", SubtitleDeliveryMethod.EXTERNAL),
),
),
)
)
return deviceProfile.deviceProfile!!
}
override suspend fun getPostedPlaybackInfo(itemId: UUID ,enableDirectStream: Boolean ,deviceProfile: DeviceProfile ,maxBitrate: Int): Response<PlaybackInfoResponse> {
val playbackInfo = jellyfinApi.mediaInfoApi.getPostedPlaybackInfo(
override suspend fun getPostedPlaybackInfo(
itemId: UUID,
enableDirectStream: Boolean,
deviceProfile: DeviceProfile,
maxBitrate: Int,
): Response<PlaybackInfoResponse> {
val playbackInfo =
jellyfinApi.mediaInfoApi.getPostedPlaybackInfo(
itemId = itemId,
PlaybackInfoDto(
userId = jellyfinApi.userId!!,
@ -666,37 +737,56 @@ class JellyfinRepositoryImpl(
enableDirectPlay = false,
enableDirectStream = enableDirectStream,
autoOpenLiveStream = true,
deviceProfile = deviceProfile,
deviceProfile = buildDeviceProfile(maxBitrate, "ts", EncodingContext.STREAMING),
allowAudioStreamCopy = true,
allowVideoStreamCopy = true,
maxStreamingBitrate = maxBitrate,
)
),
)
return playbackInfo
}
override suspend fun getVideoStreambyContainerUrl(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int, container: String): String {
val url = jellyfinApi.videosApi.getVideoStreamByContainerUrl(
override suspend fun getVideoStreambyContainerUrl(
itemId: UUID,
deviceId: String,
mediaSourceId: String,
playSessionId: String,
videoBitrate: Int,
maxHeight: Int,
container: String,
): String {
val url =
jellyfinApi.videosApi.getVideoStreamByContainerUrl(
itemId,
static = false,
deviceId = deviceId,
mediaSourceId = mediaSourceId,
playSessionId = playSessionId,
videoBitRate = videoBitrate,
audioBitRate = 384000,
maxHeight = maxHeight,
audioBitRate = 128000,
videoCodec = "hevc",
audioCodec = "aac,ac3,eac3",
audioCodec = "aac",
container = container,
startTimeTicks = 0,
copyTimestamps = true,
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
)
return url
}
override suspend fun getTranscodedVideoStream(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int): String {
val isAuto = videoBitrate == 12000000
val url = if (!isAuto) {
override suspend fun getTranscodedVideoStream(
itemId: UUID,
deviceId: String,
mediaSourceId: String,
playSessionId: String,
videoBitrate: Int,
): String {
val isAuto = videoBitrate == VideoQuality.getBitrate(VideoQuality.PAuto)
val url: String
try {
url =
if (!isAuto) {
jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl(
itemId,
static = false,
@ -705,9 +795,9 @@ class JellyfinRepositoryImpl(
playSessionId = playSessionId,
videoBitRate = videoBitrate,
enableAdaptiveBitrateStreaming = false,
audioBitRate = 384000,
audioBitRate = 128000,
videoCodec = "hevc",
audioCodec = "aac,ac3,eac3",
audioCodec = "aac",
startTimeTicks = 0,
copyTimestamps = true,
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
@ -724,7 +814,7 @@ class JellyfinRepositoryImpl(
playSessionId = playSessionId,
enableAdaptiveBitrateStreaming = true,
videoCodec = "hevc",
audioCodec = "aac,ac3,eac3",
audioCodec = "aac",
startTimeTicks = 0,
copyTimestamps = true,
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
@ -733,23 +823,20 @@ class JellyfinRepositoryImpl(
transcodeReasons = "ContainerBitrateExceedsLimit",
)
}
} catch (e: Exception) {
Timber.e(e)
throw e
}
return url
}
override suspend fun getDeviceId(): String {
val devices = jellyfinApi.devicesApi.getDevices(getUserId())
return devices.content.items?.firstOrNull()?.id!!
}
override suspend fun getDeviceId(): String = jellyfinApi.api.deviceInfo.id
override suspend fun stopEncodingProcess(playSessionId: String) {
val deviceId = getDeviceId()
jellyfinApi.api.hlsSegmentApi.stopEncodingProcess(
deviceId = deviceId,
playSessionId = playSessionId
playSessionId = playSessionId,
)
}
}

View file

@ -1,7 +1,6 @@
package com.nomadics9.ananas.repository
import android.content.Context
import android.devicelock.DeviceId
import androidx.paging.PagingData
import com.nomadics9.ananas.AppPreferences
import com.nomadics9.ananas.api.JellyfinApi
@ -27,7 +26,6 @@ import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.Response
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.DeviceInfo
import org.jellyfin.sdk.model.api.DeviceProfile
import org.jellyfin.sdk.model.api.EncodingContext
import org.jellyfin.sdk.model.api.ItemFields
@ -44,14 +42,9 @@ class JellyfinRepositoryOfflineImpl(
private val database: ServerDatabaseDao,
private val appPreferences: AppPreferences,
) : JellyfinRepository {
override suspend fun getPublicSystemInfo(): PublicSystemInfo = throw Exception("System info not available in offline mode")
override suspend fun getPublicSystemInfo(): PublicSystemInfo {
throw Exception("System info not available in offline mode")
}
override suspend fun getUserViews(): List<BaseItemDto> {
return emptyList()
}
override suspend fun getUserViews(): List<BaseItemDto> = emptyList()
override suspend fun getItem(itemId: UUID): BaseItemDto {
TODO("Not yet implemented")
@ -115,36 +108,59 @@ class JellyfinRepositoryOfflineImpl(
TODO("Not yet implemented")
}
override suspend fun getSearchItems(searchQuery: String): List<FindroidItem> {
return withContext(Dispatchers.IO) {
val movies = database.searchMovies(appPreferences.currentServer!!, searchQuery).map { it.toFindroidMovie(database, jellyfinApi.userId!!) }
val shows = database.searchShows(appPreferences.currentServer!!, searchQuery).map { it.toFindroidShow(database, jellyfinApi.userId!!) }
val episodes = database.searchEpisodes(appPreferences.currentServer!!, searchQuery).map { it.toFindroidEpisode(database, jellyfinApi.userId!!) }
override suspend fun getSearchItems(searchQuery: String): List<FindroidItem> =
withContext(Dispatchers.IO) {
val movies =
database
.searchMovies(
appPreferences.currentServer!!,
searchQuery,
).map { it.toFindroidMovie(database, jellyfinApi.userId!!) }
val shows =
database
.searchShows(
appPreferences.currentServer!!,
searchQuery,
).map { it.toFindroidShow(database, jellyfinApi.userId!!) }
val episodes =
database.searchEpisodes(appPreferences.currentServer!!, searchQuery).map {
it.toFindroidEpisode(database, jellyfinApi.userId!!)
}
movies + shows + episodes
}
}
override suspend fun getResumeItems(): List<FindroidItem> {
return withContext(Dispatchers.IO) {
val movies = database.getMoviesByServerId(appPreferences.currentServer!!).map { it.toFindroidMovie(database, jellyfinApi.userId!!) }.filter { it.playbackPositionTicks > 0 }
val episodes = database.getEpisodesByServerId(appPreferences.currentServer!!).map { it.toFindroidEpisode(database, jellyfinApi.userId!!) }.filter { it.playbackPositionTicks > 0 }
override suspend fun getResumeItems(): List<FindroidItem> =
withContext(Dispatchers.IO) {
val movies =
database
.getMoviesByServerId(appPreferences.currentServer!!)
.map {
it.toFindroidMovie(database, jellyfinApi.userId!!)
}.filter { it.playbackPositionTicks > 0 }
val episodes =
database
.getEpisodesByServerId(appPreferences.currentServer!!)
.map {
it.toFindroidEpisode(database, jellyfinApi.userId!!)
}.filter { it.playbackPositionTicks > 0 }
movies + episodes
}
}
override suspend fun getLatestMedia(parentId: UUID): List<FindroidItem> {
return emptyList()
}
override suspend fun getLatestMedia(parentId: UUID): List<FindroidItem> = emptyList()
override suspend fun getSeasons(seriesId: UUID, offline: Boolean): List<FindroidSeason> =
override suspend fun getSeasons(
seriesId: UUID,
offline: Boolean,
): List<FindroidSeason> =
withContext(Dispatchers.IO) {
database.getSeasonsByShowId(seriesId).map { it.toFindroidSeason(database, jellyfinApi.userId!!) }
}
override suspend fun getNextUp(seriesId: UUID?): List<FindroidEpisode> {
return withContext(Dispatchers.IO) {
override suspend fun getNextUp(seriesId: UUID?): List<FindroidEpisode> =
withContext(Dispatchers.IO) {
val result = mutableListOf<FindroidEpisode>()
val shows = database.getShowsByServerId(appPreferences.currentServer!!).filter {
val shows =
database.getShowsByServerId(appPreferences.currentServer!!).filter {
if (seriesId != null) it.id == seriesId else true
}
for (show in shows) {
@ -158,7 +174,6 @@ class JellyfinRepositoryOfflineImpl(
}
result.filter { it.playbackPositionTicks == 0L }
}
}
override suspend fun getEpisodes(
seriesId: UUID,
@ -174,12 +189,19 @@ class JellyfinRepositoryOfflineImpl(
items
}
override suspend fun getMediaSources(itemId: UUID, includePath: Boolean): List<FindroidSource> =
override suspend fun getMediaSources(
itemId: UUID,
includePath: Boolean,
): List<FindroidSource> =
withContext(Dispatchers.IO) {
database.getSources(itemId).map { it.toFindroidSource(database) }
}
override suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String, playSessionId: String?): String {
override suspend fun getStreamUrl(
itemId: UUID,
mediaSourceId: String,
playSessionId: String?,
): String {
TODO("Not yet implemented")
}
@ -188,7 +210,11 @@ class JellyfinRepositoryOfflineImpl(
database.getSegments(itemId)?.toFindroidSegments()
}
override suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray? =
override suspend fun getTrickplayData(
itemId: UUID,
width: Int,
index: Int,
): ByteArray? =
withContext(Dispatchers.IO) {
try {
val sources = File(context.filesDir, "trickplay/$itemId").listFiles() ?: return@withContext null
@ -202,7 +228,11 @@ class JellyfinRepositoryOfflineImpl(
override suspend fun postPlaybackStart(itemId: UUID) {}
override suspend fun postPlaybackStop(itemId: UUID, positionTicks: Long, playedPercentage: Int) {
override suspend fun postPlaybackStop(
itemId: UUID,
positionTicks: Long,
playedPercentage: Int,
) {
withContext(Dispatchers.IO) {
when {
playedPercentage < 10 -> {
@ -262,35 +292,31 @@ class JellyfinRepositoryOfflineImpl(
}
}
override fun getBaseUrl(): String {
return ""
}
override fun getBaseUrl(): String = ""
override suspend fun updateDeviceName(name: String) {
TODO("Not yet implemented")
}
override suspend fun getUserConfiguration(): UserConfiguration? {
return null
}
override suspend fun getUserConfiguration(): UserConfiguration? = null
override suspend fun getDownloads(): List<FindroidItem> =
withContext(Dispatchers.IO) {
val items = mutableListOf<FindroidItem>()
items.addAll(
database.getMoviesByServerId(appPreferences.currentServer!!)
database
.getMoviesByServerId(appPreferences.currentServer!!)
.map { it.toFindroidMovie(database, jellyfinApi.userId!!) },
)
items.addAll(
database.getShowsByServerId(appPreferences.currentServer!!)
database
.getShowsByServerId(appPreferences.currentServer!!)
.map { it.toFindroidShow(database, jellyfinApi.userId!!) },
)
items
}
override fun getUserId(): UUID {
return jellyfinApi.userId!!
}
override fun getUserId(): UUID = jellyfinApi.userId!!
override suspend fun getDeviceId(): String {
TODO("Not yet implemented")
@ -299,7 +325,7 @@ class JellyfinRepositoryOfflineImpl(
override suspend fun buildDeviceProfile(
maxBitrate: Int,
container: String,
context: EncodingContext
context: EncodingContext,
): DeviceProfile {
TODO("Not yet implemented")
}
@ -310,7 +336,8 @@ class JellyfinRepositoryOfflineImpl(
mediaSourceId: String,
playSessionId: String,
videoBitrate: Int,
container: String
maxHeight: Int,
container: String,
): String {
TODO("Not yet implemented")
}
@ -320,7 +347,7 @@ class JellyfinRepositoryOfflineImpl(
deviceId: String,
mediaSourceId: String,
playSessionId: String,
videoBitrate: Int
videoBitrate: Int,
): String {
TODO("Not yet implemented")
}
@ -329,7 +356,7 @@ class JellyfinRepositoryOfflineImpl(
itemId: UUID,
enableDirectStream: Boolean,
deviceProfile: DeviceProfile,
maxBitrate: Int
maxBitrate: Int,
): Response<PlaybackInfoResponse> {
TODO("Not yet implemented")
}
@ -337,8 +364,4 @@ class JellyfinRepositoryOfflineImpl(
override suspend fun stopEncodingProcess(playSessionId: String) {
TODO("Not yet implemented")
}
override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int, Int> {
TODO("Not yet implemented")
}
}

View file

@ -27,6 +27,7 @@ import com.nomadics9.ananas.models.FindroidSegment
import com.nomadics9.ananas.models.PlayerChapter
import com.nomadics9.ananas.models.PlayerItem
import com.nomadics9.ananas.models.Trickplay
import com.nomadics9.ananas.models.VideoQuality
import com.nomadics9.ananas.mpv.MPVPlayer
import com.nomadics9.ananas.player.video.R
import com.nomadics9.ananas.repository.JellyfinRepository
@ -51,18 +52,20 @@ import kotlin.math.ceil
@HiltViewModel
class PlayerActivityViewModel
@Inject
constructor(
@Inject
constructor(
private val application: Application,
private val jellyfinRepository: JellyfinRepository,
private val appPreferences: AppPreferences,
private val jellyfinApi: JellyfinApi,
private val savedStateHandle: SavedStateHandle,
) : ViewModel(), Player.Listener {
) : ViewModel(),
Player.Listener {
val player: Player
private var originalHeight: Int = 0
private val _uiState = MutableStateFlow(
private val _uiState =
MutableStateFlow(
UiState(
currentItemTitle = "",
currentSegment = null,
@ -101,11 +104,14 @@ constructor(
init {
if (appPreferences.playerMpv) {
val trackSelectionParameters = TrackSelectionParameters.Builder(application)
val trackSelectionParameters =
TrackSelectionParameters
.Builder(application)
.setPreferredAudioLanguage(appPreferences.preferredAudioLanguage)
.setPreferredTextLanguage(appPreferences.preferredSubtitleLanguage)
.build()
player = MPVPlayer(
player =
MPVPlayer(
context = application,
requestAudioFocus = true,
trackSelectionParameters = trackSelectionParameters,
@ -121,30 +127,31 @@ constructor(
DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON,
)
trackSelector.setParameters(
trackSelector.buildUponParameters()
trackSelector
.buildUponParameters()
.setTunnelingEnabled(true)
.setPreferredAudioLanguage(appPreferences.preferredAudioLanguage)
.setPreferredTextLanguage(appPreferences.preferredSubtitleLanguage),
)
player = ExoPlayer.Builder(application, renderersFactory)
player =
ExoPlayer
.Builder(application, renderersFactory)
.setTrackSelector(trackSelector)
.setAudioAttributes(
AudioAttributes.Builder()
AudioAttributes
.Builder()
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
.setUsage(C.USAGE_MEDIA)
.build(),
/* handleAudioFocus = */
// handleAudioFocus =
true,
)
.setSeekBackIncrementMs(appPreferences.playerSeekBackIncrement)
).setSeekBackIncrementMs(appPreferences.playerSeekBackIncrement)
.setSeekForwardIncrementMs(appPreferences.playerSeekForwardIncrement)
.build()
}
}
fun initializePlayer(
items: Array<PlayerItem>,
) {
fun initializePlayer(items: Array<PlayerItem>) {
this.items = items
player.addListener(this)
@ -153,8 +160,10 @@ constructor(
try {
for (item in items) {
val streamUrl = item.mediaSourceUri
val mediaSubtitles = item.externalSubtitles.map { externalSubtitle ->
MediaItem.SubtitleConfiguration.Builder(externalSubtitle.uri)
val mediaSubtitles =
item.externalSubtitles.map { externalSubtitle ->
MediaItem.SubtitleConfiguration
.Builder(externalSubtitle.uri)
.setLabel(externalSubtitle.title.ifBlank { application.getString(R.string.external) })
.setMimeType(externalSubtitle.mimeType)
.setLanguage(externalSubtitle.language)
@ -170,24 +179,25 @@ constructor(
Timber.d("Stream url: $streamUrl")
val mediaItem =
MediaItem.Builder()
MediaItem
.Builder()
.setMediaId(item.itemId.toString())
.setUri(streamUrl)
.setMediaMetadata(
MediaMetadata.Builder()
MediaMetadata
.Builder()
.setTitle(item.name)
.build(),
)
.setSubtitleConfigurations(mediaSubtitles)
).setSubtitleConfigurations(mediaSubtitles)
.build()
mediaItems.add(mediaItem)
}
} catch (e: Exception) {
Timber.e(e)
}
val startPosition = if (playbackPosition == 0L) {
val startPosition =
if (playbackPosition == 0L) {
items.getOrNull(currentMediaItemIndex)?.playbackPosition ?: C.TIME_UNSET
} else {
playbackPosition
@ -231,7 +241,8 @@ constructor(
}
private fun pollPosition(player: Player) {
val playbackProgressRunnable = object : Runnable {
val playbackProgressRunnable =
object : Runnable {
override fun run() {
savedStateHandle["position"] = player.currentPosition
viewModelScope.launch {
@ -251,7 +262,8 @@ constructor(
handler.postDelayed(this, 5000L)
}
}
val segmentCheckRunnable = object : Runnable {
val segmentCheckRunnable =
object : Runnable {
override fun run() {
val currentMediaItem = player.currentMediaItem
if (currentMediaItem != null && currentMediaItem.mediaId.isNotEmpty()) {
@ -276,12 +288,16 @@ constructor(
if (segments.isNotEmpty()) handler.post(segmentCheckRunnable)
}
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
override fun onMediaItemTransition(
mediaItem: MediaItem?,
reason: Int,
) {
Timber.d("Playing MediaItem: ${mediaItem?.mediaId}")
savedStateHandle["mediaItemIndex"] = player.currentMediaItemIndex
viewModelScope.launch {
try {
items.first { it.itemId.toString() == player.currentMediaItem?.mediaId }
items
.first { it.itemId.toString() == player.currentMediaItem?.mediaId }
.let { item ->
val itemTitle =
if (item.parentIndexNumber != null && item.indexNumber != null) {
@ -345,24 +361,30 @@ constructor(
releasePlayer()
}
fun switchToTrack(trackType: @C.TrackType Int, index: Int) {
fun switchToTrack(
trackType: @C.TrackType Int,
index: Int,
) {
// Index -1 equals disable track
if (index == -1) {
player.trackSelectionParameters = player.trackSelectionParameters
player.trackSelectionParameters =
player.trackSelectionParameters
.buildUpon()
.clearOverridesOfType(trackType)
.setTrackTypeDisabled(trackType, true)
.build()
} else {
player.trackSelectionParameters = player.trackSelectionParameters
player.trackSelectionParameters =
player.trackSelectionParameters
.buildUpon()
.setOverrideForType(
TrackSelectionOverride(
player.currentTracks.groups.filter { it.type == trackType && it.isSupported }[index].mediaTrackGroup,
0
player.currentTracks.groups
.filter { it.type == trackType && it.isSupported }[index]
.mediaTrackGroup,
0,
),
)
.setTrackTypeDisabled(trackType, false)
).setTrackTypeDisabled(trackType, false)
.build()
}
}
@ -377,14 +399,17 @@ constructor(
Timber.d("Trickplay Resolution: ${trickplayInfo.width}")
withContext(Dispatchers.Default) {
val maxIndex = ceil(
trickplayInfo.thumbnailCount.toDouble()
.div(trickplayInfo.tileWidth * trickplayInfo.tileHeight)
val maxIndex =
ceil(
trickplayInfo.thumbnailCount
.toDouble()
.div(trickplayInfo.tileWidth * trickplayInfo.tileHeight),
).toInt()
val bitmaps = mutableListOf<Bitmap>()
for (i in 0..maxIndex) {
jellyfinRepository.getTrickplayData(
jellyfinRepository
.getTrickplayData(
item.itemId,
trickplayInfo.width,
i,
@ -392,12 +417,13 @@ 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(
val bitmap =
Bitmap.createBitmap(
fullBitmap,
offsetX,
offsetY,
trickplayInfo.width,
trickplayInfo.height
trickplayInfo.height,
)
bitmaps.add(bitmap)
}
@ -406,10 +432,11 @@ constructor(
}
_uiState.update {
it.copy(
currentTrickplay = Trickplay(
currentTrickplay =
Trickplay(
trickplayInfo.interval,
bitmaps
)
bitmaps,
),
)
}
}
@ -420,9 +447,7 @@ constructor(
*
* @return list of [PlayerChapter]
*/
private fun getChapters(): List<PlayerChapter>? {
return uiState.value.currentChapters
}
private fun getChapters(): List<PlayerChapter>? = uiState.value.currentChapters
/**
* Get the index of the current chapter
@ -473,8 +498,8 @@ 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
@ -482,20 +507,17 @@ constructor(
* @param chapterIndex the index of the chapter to seek to
* @return the [PlayerChapter] which has been sought to
*/
private fun seekToChapter(chapterIndex: Int): PlayerChapter? {
return getChapters()?.getOrNull(chapterIndex)?.also { chapter ->
private fun seekToChapter(chapterIndex: Int): PlayerChapter? =
getChapters()?.getOrNull(chapterIndex)?.also { chapter ->
player.seekTo(chapter.startPosition)
}
}
/**
* Seek to the next chapter
*
* @return the [PlayerChapter] which has been sought to
*/
fun seekToNextChapter(): PlayerChapter? {
return getNextChapterIndex()?.let { seekToChapter(it) }
}
fun seekToNextChapter(): PlayerChapter? = getNextChapterIndex()?.let { seekToChapter(it) }
/**
* Seek to the previous chapter Will seek to start of current chapter if
@ -503,55 +525,54 @@ constructor(
*
* @return the [PlayerChapter] which has been sought to
*/
fun seekToPreviousChapter(): PlayerChapter? {
return getPreviousChapterIndex()?.let { seekToChapter(it) }
}
fun seekToPreviousChapter(): PlayerChapter? = getPreviousChapterIndex()?.let { seekToChapter(it) }
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
eventsChannel.trySend(PlayerEvents.IsPlayingChanged(isPlaying))
}
private fun getTranscodeResolutions(preferredQuality: String): Int {
return when (preferredQuality) {
"1080p" -> 1080
"720p - 2Mbps" -> 720
"480p - 1Mbps" -> 480
"360p - 800kbps" -> 360
"Auto" -> 1
else -> 1080
}
}
fun changeVideoQuality(quality: String) {
val mediaId = player.currentMediaItem?.mediaId ?: return
val currentItem = items.firstOrNull { it.itemId.toString() == mediaId } ?: return
val currentPosition = player.currentPosition
viewModelScope.launch {
val videoQuality = VideoQuality.fromString(quality)!!
try {
val transcodingResolution = getTranscodeResolutions(quality)
val (videoBitRate, audioBitRate) = jellyfinRepository.getVideoTranscodeBitRate(
transcodingResolution
val deviceProfile =
jellyfinRepository.buildDeviceProfile(
VideoQuality.getBitrate(videoQuality),
"ts",
EncodingContext.STREAMING,
)
val playbackInfo =
jellyfinRepository.getPostedPlaybackInfo(
currentItem.itemId,
true,
deviceProfile,
VideoQuality.getBitrate(videoQuality),
)
val deviceProfile = jellyfinRepository.buildDeviceProfile(videoBitRate, "mkv", EncodingContext.STREAMING)
val playbackInfo = jellyfinRepository.getPostedPlaybackInfo(currentItem.itemId,true,deviceProfile,videoBitRate)
val playSessionId = playbackInfo.content.playSessionId
if (playSessionId != null) {
jellyfinRepository.stopEncodingProcess(playSessionId)
}
val mediaSources = jellyfinRepository.getMediaSources(currentItem.itemId, true)
val externalSubtitles = currentItem.externalSubtitles.map { externalSubtitle ->
MediaItem.SubtitleConfiguration.Builder(externalSubtitle.uri)
val externalSubtitles =
currentItem.externalSubtitles.map { externalSubtitle ->
MediaItem.SubtitleConfiguration
.Builder(externalSubtitle.uri)
.setLabel(externalSubtitle.title.ifBlank { application.getString(R.string.external) })
.setLanguage(externalSubtitle.language.ifBlank { "Unknown" })
.setMimeType(externalSubtitle.mimeType)
.build()
}
val embeddedSubtitles = mediaSources[currentMediaItemIndex].mediaStreams
val embeddedSubtitles =
mediaSources[currentMediaItemIndex]
.mediaStreams
.filter { it.type == MediaStreamType.SUBTITLE && !it.isExternal && it.path != null }
.map { mediaStream ->
val test = mediaStream.codec
@ -559,8 +580,10 @@ constructor(
var deliveryUrl = mediaStream.path
Timber.d("Deliverurl: %s", deliveryUrl)
if (mediaStream.codec == "webvtt") {
deliveryUrl = deliveryUrl?.replace("Stream.srt", "Stream.vtt")}
MediaItem.SubtitleConfiguration.Builder(Uri.parse(deliveryUrl))
deliveryUrl = deliveryUrl?.replace("Stream.srt", "Stream.vtt")
}
MediaItem.SubtitleConfiguration
.Builder(Uri.parse(deliveryUrl))
.setMimeType(
when (mediaStream.codec) {
"subrip" -> MimeTypes.APPLICATION_SUBRIP
@ -575,44 +598,50 @@ constructor(
"stl" -> MimeTypes.APPLICATION_TTML // EBU STL (Subtitling Data Exchange Format)
"sbv" -> MimeTypes.APPLICATION_SUBRIP // YouTube's SBV format is similar to SubRip
else -> MimeTypes.TEXT_UNKNOWN
}
)
.setLanguage(mediaStream.language.ifBlank { "Unknown" })
},
).setLanguage(mediaStream.language.ifBlank { "Unknown" })
.setLabel("Embedded")
.build()
}
.toMutableList()
}.toMutableList()
val allSubtitles = embeddedSubtitles.apply { addAll(externalSubtitles) }
val url = if (transcodingResolution == 1080){
val url =
if (VideoQuality.getQualityString(videoQuality) == "Original") {
jellyfinRepository.getStreamUrl(currentItem.itemId, currentItem.mediaSourceId, playSessionId)
} else {
val mediaSourceId = mediaSources[currentMediaItemIndex].id
val deviceId = jellyfinRepository.getDeviceId()
val url = jellyfinRepository.getTranscodedVideoStream(currentItem.itemId, deviceId ,mediaSourceId, playSessionId!!, videoBitRate)
val deviceId = jellyfinApi.api.deviceInfo.id
Timber.d("deviceid = %s", deviceId)
val url =
jellyfinRepository.getTranscodedVideoStream(
currentItem.itemId,
deviceId,
mediaSourceId,
playSessionId!!,
VideoQuality.getBitrate(videoQuality),
)
val uriBuilder = url.toUri().buildUpon()
val apiKey = jellyfinApi.api.accessToken
uriBuilder.appendQueryParameter("api_key",apiKey )
uriBuilder.appendQueryParameter("api_key", apiKey)
val newUri = uriBuilder.build()
newUri.toString()
}
Timber.e("URI IS %s", url)
val mediaItemBuilder = MediaItem.Builder()
val mediaItemBuilder =
MediaItem
.Builder()
.setMediaId(currentItem.itemId.toString())
.setUri(url)
.setSubtitleConfigurations(allSubtitles)
.setMediaMetadata(
MediaMetadata.Builder()
MediaMetadata
.Builder()
.setTitle(currentItem.name)
.build(),
)
player.pause()
player.setMediaItem(mediaItemBuilder.build())
player.prepare()
@ -620,28 +649,30 @@ constructor(
playWhenReady = true
player.play()
val originalHeight = mediaSources[currentMediaItemIndex].mediaStreams
val originalHeight =
mediaSources[currentMediaItemIndex]
.mediaStreams
.filter { it.type == MediaStreamType.VIDEO }
.map {mediaStream -> mediaStream.height}.first() ?: 1080
.map { mediaStream -> mediaStream.height }
.first() ?: 1080
// Store the original height
this@PlayerActivityViewModel.originalHeight = originalHeight
//isQualityChangeInProgress = true
// isQualityChangeInProgress = true
} catch (e: Exception) {
Timber.e(e)
}
}
}
fun getOriginalHeight(): Int {
return originalHeight
fun getOriginalHeight(): Int = originalHeight
}
}
sealed interface PlayerEvents {
data object NavigateBack : PlayerEvents
data class IsPlayingChanged(val isPlaying: Boolean) : PlayerEvents
data class IsPlayingChanged(
val isPlaying: Boolean,
) : PlayerEvents
}