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.Space
import android.widget.TextView import android.widget.TextView
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.Player
import androidx.media3.ui.DefaultTimeBar import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.PlayerControlView import androidx.media3.ui.PlayerControlView
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import androidx.navigation.navArgs import androidx.navigation.navArgs
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import com.nomadics9.ananas.databinding.ActivityPlayerBinding import com.nomadics9.ananas.databinding.ActivityPlayerBinding
import com.nomadics9.ananas.dialogs.SpeedSelectionDialogFragment import com.nomadics9.ananas.dialogs.SpeedSelectionDialogFragment
import com.nomadics9.ananas.dialogs.TrackSelectionDialogFragment 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.utils.PreviewScrubListener
import com.nomadics9.ananas.viewmodels.PlayerActivityViewModel import com.nomadics9.ananas.viewmodels.PlayerActivityViewModel
import com.nomadics9.ananas.viewmodels.PlayerEvents import com.nomadics9.ananas.viewmodels.PlayerEvents
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -54,7 +52,6 @@ var isControlsLocked: Boolean = false
@AndroidEntryPoint @AndroidEntryPoint
class PlayerActivity : BasePlayerActivity() { class PlayerActivity : BasePlayerActivity() {
@Inject @Inject
lateinit var appPreferences: AppPreferences lateinit var appPreferences: AppPreferences
@ -115,7 +112,8 @@ class PlayerActivity : BasePlayerActivity() {
configureInsets(lockedControls) configureInsets(lockedControls)
if (appPreferences.playerGestures) { if (appPreferences.playerGestures) {
playerGestureHelper = PlayerGestureHelper( playerGestureHelper =
PlayerGestureHelper(
appPreferences, appPreferences,
this, this,
binding.playerView, binding.playerView,
@ -155,7 +153,12 @@ class PlayerActivity : BasePlayerActivity() {
skipButton.text = skipButton.text =
getString(CoreR.string.skip_intro_button) getString(CoreR.string.skip_intro_button)
skipButton.isVisible = 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 watchCreditsButton.isVisible = false
} }
@ -167,7 +170,10 @@ class PlayerActivity : BasePlayerActivity() {
getString(CoreR.string.skip_credit_button_last) getString(CoreR.string.skip_credit_button_last)
} }
skipButton.isVisible = skipButton.isVisible =
!isInPictureInPictureMode && !buttonPressed && currentSegment?.skip == true && !binding.playerView.isControllerFullyVisible !isInPictureInPictureMode &&
!buttonPressed &&
currentSegment?.skip == true &&
!binding.playerView.isControllerFullyVisible
watchCreditsButton.isVisible = skipButton.isVisible watchCreditsButton.isVisible = skipButton.isVisible
} }
@ -181,12 +187,15 @@ class PlayerActivity : BasePlayerActivity() {
when (currentSegment?.type) { when (currentSegment?.type) {
"intro" -> { "intro" -> {
skipButton.isVisible = skipButton.isVisible =
!buttonPressed && (showSkip == true || (visibility == View.VISIBLE && currentSegment?.skip == true)) !buttonPressed &&
(showSkip == true || (visibility == View.VISIBLE && currentSegment?.skip == true))
} }
"credit" -> { "credit" -> {
skipButton.isVisible = skipButton.isVisible =
!buttonPressed && currentSegment?.skip == true && visibility == View.GONE !buttonPressed &&
currentSegment?.skip == true &&
visibility == View.GONE
watchCreditsButton.isVisible = skipButton.isVisible watchCreditsButton.isVisible = skipButton.isVisible
} }
} }
@ -268,7 +277,8 @@ class PlayerActivity : BasePlayerActivity() {
if (appPreferences.playerPipGesture) { if (appPreferences.playerPipGesture) {
try { try {
setPictureInPictureParams(pipParams(event.isPlaying)) setPictureInPictureParams(pipParams(event.isPlaying))
} catch (_: IllegalArgumentException) { } } catch (_: IllegalArgumentException) {
}
} }
} }
} }
@ -346,7 +356,8 @@ class PlayerActivity : BasePlayerActivity() {
if (appPreferences.playerTrickplay) { if (appPreferences.playerTrickplay) {
val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview) val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview)
previewScrubListener = PreviewScrubListener( previewScrubListener =
PreviewScrubListener(
imagePreview, imagePreview,
timeBar, timeBar,
viewModel.player, viewModel.player,
@ -381,14 +392,16 @@ class PlayerActivity : BasePlayerActivity() {
private fun pipParams(enableAutoEnter: Boolean = viewModel.player.isPlaying): PictureInPictureParams { private fun pipParams(enableAutoEnter: Boolean = viewModel.player.isPlaying): PictureInPictureParams {
val displayAspectRatio = Rational(binding.playerView.width, binding.playerView.height) val displayAspectRatio = Rational(binding.playerView.width, binding.playerView.height)
val aspectRatio = binding.playerView.player?.videoSize?.let { val aspectRatio =
binding.playerView.player?.videoSize?.let {
Rational( Rational(
it.width.coerceAtMost((it.height * 2.39f).toInt()), it.width.coerceAtMost((it.height * 2.39f).toInt()),
it.height.coerceAtMost((it.width * 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() val space = ((binding.playerView.height - (binding.playerView.width.toFloat() / aspectRatio.toFloat())) / 2).toInt()
Rect( Rect(
0, 0,
@ -406,7 +419,9 @@ class PlayerActivity : BasePlayerActivity() {
) )
} }
val builder = PictureInPictureParams.Builder() val builder =
PictureInPictureParams
.Builder()
.setAspectRatio(aspectRatio) .setAspectRatio(aspectRatio)
.setSourceRectHint(sourceRectHint) .setSourceRectHint(sourceRectHint)
@ -424,30 +439,51 @@ class PlayerActivity : BasePlayerActivity() {
try { try {
enterPictureInPictureMode(pipParams()) enterPictureInPictureMode(pipParams())
} catch (_: IllegalArgumentException) { } } catch (_: IllegalArgumentException) {
}
} }
private fun showQualitySelectionDialog() { private fun showQualitySelectionDialog() {
val height = viewModel.getOriginalHeight() 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) { // Map entries to values
0 -> arrayOf("Auto", "Original - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps") val qualityMap = qualityEntries.zip(qualityValues).toMap()
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") val qualities: List<String> =
else -> arrayOf("Auto", "Original - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps") 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) MaterialAlertDialogBuilder(this)
.setTitle("Select Video Quality") .setTitle("Select Video Quality")
.setItems(qualities) { _, which -> .setItems(qualities.toTypedArray()) { _, which ->
val selectedQuality = qualities[which] val selectedQualityEntry = qualities[which]
viewModel.changeVideoQuality(selectedQuality) val selectedQualityValue =
qualityMap.entries.find { it.key.contains(selectedQualityEntry.split(" ")[0]) }?.value ?: selectedQualityEntry
viewModel.changeVideoQuality(selectedQualityValue)
}.show()
} }
.show()
}
override fun onPictureInPictureModeChanged( override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean, isInPictureInPictureMode: Boolean,
@ -463,7 +499,8 @@ class PlayerActivity : BasePlayerActivity() {
playerGestureHelper?.updateZoomMode(false) playerGestureHelper?.updateZoomMode(false)
// Brightness mode Auto // Brightness mode Auto
window.attributes = window.attributes.apply { window.attributes =
window.attributes.apply {
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
} }
} }
@ -472,11 +509,14 @@ class PlayerActivity : BasePlayerActivity() {
playerGestureHelper?.updateZoomMode(wasZoom) playerGestureHelper?.updateZoomMode(wasZoom)
// Override auto brightness // Override auto brightness
window.attributes = window.attributes.apply { window.attributes =
screenBrightness = if (appPreferences.playerBrightnessRemember) { window.attributes.apply {
screenBrightness =
if (appPreferences.playerBrightnessRemember) {
appPreferences.playerBrightness appPreferences.playerBrightness
} else { } else {
Settings.System.getInt( Settings.System
.getInt(
contentResolver, contentResolver,
Settings.System.SCREEN_BRIGHTNESS, Settings.System.SCREEN_BRIGHTNESS,
).toFloat() / 255 ).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.FindroidSources
import com.nomadics9.ananas.models.FindroidTrickplayInfo import com.nomadics9.ananas.models.FindroidTrickplayInfo
import com.nomadics9.ananas.models.UiText import com.nomadics9.ananas.models.UiText
import com.nomadics9.ananas.models.VideoQuality
import com.nomadics9.ananas.models.toFindroidEpisodeDto import com.nomadics9.ananas.models.toFindroidEpisodeDto
import com.nomadics9.ananas.models.toFindroidMediaStreamDto import com.nomadics9.ananas.models.toFindroidMediaStreamDto
import com.nomadics9.ananas.models.toFindroidMovieDto import com.nomadics9.ananas.models.toFindroidMovieDto
@ -50,13 +51,13 @@ class DownloaderImpl(
storageIndex: Int, storageIndex: Int,
): Pair<Long, UiText?> { ): Pair<Long, UiText?> {
try { try {
Timber.d("Downloading item: ${item.id} with sourceId: $sourceId") Timber.d("Downloading item: ${item.id} with sourceId: $sourceId")
val source = val source =
jellyfinRepository.getMediaSources(item.id, true).first { it.id == sourceId } jellyfinRepository.getMediaSources(item.id, true).first { it.id == sourceId }
val segments = jellyfinRepository.getSegmentsTimestamps(item.id) val segments = jellyfinRepository.getSegmentsTimestamps(item.id)
val trickplayInfo = if (item is FindroidSources) { val trickplayInfo =
if (item is FindroidSources) {
item.trickplayInfo?.get(sourceId) item.trickplayInfo?.get(sourceId)
} else { } else {
null null
@ -96,9 +97,13 @@ class DownloaderImpl(
return Pair( return Pair(
-1, -1,
if (e.message != null) UiText.DynamicString(e.message!!) else UiText.StringResource( if (e.message != null) {
CoreR.string.unknown_error UiText.DynamicString(e.message!!)
} else {
UiText.StringResource(
CoreR.string.unknown_error,
) )
},
) )
} }
} }
@ -110,7 +115,7 @@ class DownloaderImpl(
trickplayInfo: FindroidTrickplayInfo?, trickplayInfo: FindroidTrickplayInfo?,
segments: List<FindroidSegment>?, segments: List<FindroidSegment>?,
path: Uri, path: Uri,
quality: String quality: String,
): Pair<Long, UiText?> { ): Pair<Long, UiText?> {
val transcodingUrl = getTranscodedUrl(item.id, quality) val transcodingUrl = getTranscodedUrl(item.id, quality)
when (item) { when (item) {
@ -126,7 +131,9 @@ class DownloaderImpl(
if (segments != null) { if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id)) database.insertSegments(segments.toFindroidSegmentsDto(item.id))
} }
val request = DownloadManager.Request(transcodingUrl) val request =
DownloadManager
.Request(transcodingUrl)
.setTitle(item.name) .setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData) .setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming) .setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
@ -139,7 +146,8 @@ class DownloaderImpl(
is FindroidEpisode -> { is FindroidEpisode -> {
database.insertShow( database.insertShow(
jellyfinRepository.getShow(item.seriesId) jellyfinRepository
.getShow(item.seriesId)
.toFindroidShowDto(appPreferences.currentServer!!), .toFindroidShowDto(appPreferences.currentServer!!),
) )
database.insertSeason( database.insertSeason(
@ -156,7 +164,9 @@ class DownloaderImpl(
if (segments != null) { if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id)) database.insertSegments(segments.toFindroidSegmentsDto(item.id))
} }
val request = DownloadManager.Request(transcodingUrl) val request =
DownloadManager
.Request(transcodingUrl)
.setTitle(item.name) .setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData) .setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming) .setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
@ -190,7 +200,9 @@ class DownloaderImpl(
if (segments != null) { if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id)) database.insertSegments(segments.toFindroidSegmentsDto(item.id))
} }
val request = DownloadManager.Request(source.path.toUri()) val request =
DownloadManager
.Request(source.path.toUri())
.setTitle(item.name) .setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData) .setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming) .setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
@ -203,7 +215,8 @@ class DownloaderImpl(
is FindroidEpisode -> { is FindroidEpisode -> {
database.insertShow( database.insertShow(
jellyfinRepository.getShow(item.seriesId) jellyfinRepository
.getShow(item.seriesId)
.toFindroidShowDto(appPreferences.currentServer!!), .toFindroidShowDto(appPreferences.currentServer!!),
) )
database.insertSeason( database.insertSeason(
@ -219,7 +232,9 @@ class DownloaderImpl(
if (segments != null) { if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id)) database.insertSegments(segments.toFindroidSegmentsDto(item.id))
} }
val request = DownloadManager.Request(source.path.toUri()) val request =
DownloadManager
.Request(source.path.toUri())
.setTitle(item.name) .setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData) .setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming) .setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
@ -233,15 +248,20 @@ class DownloaderImpl(
return Pair(-1, null) return Pair(-1, null)
} }
override suspend fun cancelDownload(
override suspend fun cancelDownload(item: FindroidItem, source: FindroidSource) { item: FindroidItem,
source: FindroidSource,
) {
if (source.downloadId != null) { if (source.downloadId != null) {
downloadManager.remove(source.downloadId!!) downloadManager.remove(source.downloadId!!)
} }
deleteItem(item, source) deleteItem(item, source)
} }
override suspend fun deleteItem(item: FindroidItem, source: FindroidSource) { override suspend fun deleteItem(
item: FindroidItem,
source: FindroidSource,
) {
when (item) { when (item) {
is FindroidMovie -> { is FindroidMovie -> {
database.deleteMovie(item.id) database.deleteMovie(item.id)
@ -283,11 +303,14 @@ class DownloaderImpl(
if (downloadId == null) { if (downloadId == null) {
return Pair(downloadStatus, progress) return Pair(downloadStatus, progress)
} }
val query = DownloadManager.Query() val query =
DownloadManager
.Query()
.setFilterById(downloadId) .setFilterById(downloadId)
val cursor = downloadManager.query(query) val cursor = downloadManager.query(query)
if (cursor.moveToFirst()) { if (cursor.moveToFirst()) {
downloadStatus = cursor.getInt( downloadStatus =
cursor.getInt(
cursor.getColumnIndexOrThrow( cursor.getColumnIndexOrThrow(
DownloadManager.COLUMN_STATUS, DownloadManager.COLUMN_STATUS,
), ),
@ -320,20 +343,23 @@ class DownloaderImpl(
val storageLocation = context.getExternalFilesDirs(null)[storageIndex] val storageLocation = context.getExternalFilesDirs(null)[storageIndex]
for (mediaStream in source.mediaStreams.filter { it.isExternal }) { for (mediaStream in source.mediaStreams.filter { it.isExternal }) {
val id = UUID.randomUUID() val id = UUID.randomUUID()
val streamPath = Uri.fromFile( val streamPath =
Uri.fromFile(
File( File(
storageLocation, storageLocation,
"downloads/${item.id}.${source.id}.$id.download" "downloads/${item.id}.${source.id}.$id.download",
) ),
) )
database.insertMediaStream( database.insertMediaStream(
mediaStream.toFindroidMediaStreamDto( mediaStream.toFindroidMediaStreamDto(
id, id,
source.id, source.id,
streamPath.path.orEmpty() streamPath.path.orEmpty(),
),
) )
) val request =
val request = DownloadManager.Request(Uri.parse(mediaStream.path)) DownloadManager
.Request(Uri.parse(mediaStream.path))
.setTitle(mediaStream.title) .setTitle(mediaStream.title)
.setAllowedOverMetered(appPreferences.downloadOverMobileData) .setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming) .setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
@ -347,7 +373,7 @@ class DownloaderImpl(
private fun downloadEmbeddedMediaStreams( private fun downloadEmbeddedMediaStreams(
item: FindroidItem, item: FindroidItem,
source: FindroidSource, source: FindroidSource,
storageIndex: Int = 0 storageIndex: Int = 0,
) { ) {
val storageLocation = context.getExternalFilesDirs(null)[storageIndex] val storageLocation = context.getExternalFilesDirs(null)[storageIndex]
val subtitleStreams = source.mediaStreams.filter { !it.isExternal && it.type == MediaStreamType.SUBTITLE && it.path != null } 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") deliveryUrl = deliveryUrl.replace("Stream.srt", "Stream.vtt")
} }
val id = UUID.randomUUID() val id = UUID.randomUUID()
val streamPath = Uri.fromFile( val streamPath =
Uri.fromFile(
File( File(
storageLocation, storageLocation,
"downloads/${item.id}.${source.id}.$id.download" "downloads/${item.id}.${source.id}.$id.download",
) ),
) )
database.insertMediaStream( database.insertMediaStream(
mediaStream.toFindroidMediaStreamDto( mediaStream.toFindroidMediaStreamDto(
id, id,
source.id, source.id,
streamPath.path.orEmpty() streamPath.path.orEmpty(),
),
) )
) val request =
val request = DownloadManager.Request(Uri.parse(deliveryUrl)) DownloadManager
.Request(Uri.parse(deliveryUrl))
.setTitle(mediaStream.title) .setTitle(mediaStream.title)
.setAllowedOverMetered(appPreferences.downloadOverMobileData) .setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming) .setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
@ -382,19 +411,21 @@ class DownloaderImpl(
} }
} }
private suspend fun downloadTrickplayData( private suspend fun downloadTrickplayData(
itemId: UUID, itemId: UUID,
sourceId: String, sourceId: String,
trickplayInfo: FindroidTrickplayInfo, trickplayInfo: FindroidTrickplayInfo,
) { ) {
val maxIndex = ceil( val maxIndex =
trickplayInfo.thumbnailCount.toDouble() ceil(
.div(trickplayInfo.tileWidth * trickplayInfo.tileHeight) trickplayInfo.thumbnailCount
.toDouble()
.div(trickplayInfo.tileWidth * trickplayInfo.tileHeight),
).toInt() ).toInt()
val byteArrays = mutableListOf<ByteArray>() val byteArrays = mutableListOf<ByteArray>()
for (i in 0..maxIndex) { for (i in 0..maxIndex) {
jellyfinRepository.getTrickplayData( jellyfinRepository
.getTrickplayData(
itemId, itemId,
trickplayInfo.width, trickplayInfo.width,
i, i,
@ -420,52 +451,46 @@ class DownloaderImpl(
} }
} }
private suspend fun getTranscodedUrl(itemId: UUID, quality: String): Uri? { private suspend fun getTranscodedUrl(
val maxBitrate = when (quality) { itemId: UUID,
"720p" -> 2000000 // 2 Mbps quality: String,
"480p" -> 1000000 // 1 Mbps ): Uri? {
"360p" -> 800000 // 800Kbps val videoQuality = VideoQuality.fromString(quality)!!
else -> 2000000 // Default to 2 Mbps if not specified
}
return try { return try {
val deviceProfile =
val deviceProfile = jellyfinRepository.buildDeviceProfile(maxBitrate,"mkv", EncodingContext.STATIC) jellyfinRepository.buildDeviceProfile(
val playbackInfo = jellyfinRepository.getPostedPlaybackInfo(itemId,false,deviceProfile,maxBitrate) VideoQuality.getBitrate(videoQuality),
val mediaSourceId = playbackInfo.content.mediaSources.firstOrNull()?.id!! "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 playSessionId = playbackInfo.content.playSessionId!!
val deviceId = jellyfinRepository.getDeviceId() 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) return downloadUrl.toUri()
Timber.d("Constructed Transcode URL: $transcodeUri")
transcodeUri
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
null 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> <item>opensles</item>
</string-array> </string-array>
<string-array name="quality_entries"> <string-array name="quality_entries">
<item>Auto</item>
<item>Original</item> <item>Original</item>
<item>1080p - 8Mbps</item>
<item>720p - 2Mbps</item> <item>720p - 2Mbps</item>
<item>480p - 1Mbps</item> <item>480p - 1Mbps</item>
<item>360p - 800Kbps</item> <item>360p - 800Kbps</item>
</string-array> </string-array>
<string-array name="quality_values"> <string-array name="quality_values">
<item>Auto</item>
<item>Original</item> <item>Original</item>
<item>1080p</item>
<item>720p</item> <item>720p</item>
<item>480p</item> <item>480p</item>
<item>360p</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.api.client.Response
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind 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.DeviceProfile
import org.jellyfin.sdk.model.api.EncodingContext import org.jellyfin.sdk.model.api.EncodingContext
import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.ItemFields
@ -31,7 +29,9 @@ interface JellyfinRepository {
suspend fun getUserViews(): List<BaseItemDto> suspend fun getUserViews(): List<BaseItemDto>
suspend fun getItem(itemId: UUID): BaseItemDto suspend fun getItem(itemId: UUID): BaseItemDto
suspend fun getEpisode(itemId: UUID): FindroidEpisode suspend fun getEpisode(itemId: UUID): FindroidEpisode
suspend fun getMovie(itemId: UUID): FindroidMovie suspend fun getMovie(itemId: UUID): FindroidMovie
suspend fun getShow(itemId: UUID): FindroidShow suspend fun getShow(itemId: UUID): FindroidShow
@ -72,7 +72,10 @@ interface JellyfinRepository {
suspend fun getLatestMedia(parentId: UUID): List<FindroidItem> 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> suspend fun getNextUp(seriesId: UUID? = null): List<FindroidEpisode>
@ -85,21 +88,40 @@ interface JellyfinRepository {
offline: Boolean = false, offline: Boolean = false,
): List<FindroidEpisode> ): 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 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 postCapabilities()
suspend fun postPlaybackStart(itemId: UUID) 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) suspend fun markAsFavorite(itemId: UUID)
@ -121,15 +143,36 @@ interface JellyfinRepository {
suspend fun getDeviceId(): String 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,
suspend fun getPostedPlaybackInfo(itemId: UUID, enableDirectStream: Boolean, deviceProfile: DeviceProfile ,maxBitrate: Int): Response<PlaybackInfoResponse> enableDirectStream: Boolean,
deviceProfile: DeviceProfile,
maxBitrate: Int,
): Response<PlaybackInfoResponse>
suspend fun stopEncodingProcess(playSessionId: String) 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.FindroidShow
import com.nomadics9.ananas.models.FindroidSource import com.nomadics9.ananas.models.FindroidSource
import com.nomadics9.ananas.models.SortBy import com.nomadics9.ananas.models.SortBy
import com.nomadics9.ananas.models.VideoQuality
import com.nomadics9.ananas.models.toFindroidCollection import com.nomadics9.ananas.models.toFindroidCollection
import com.nomadics9.ananas.models.toFindroidEpisode import com.nomadics9.ananas.models.toFindroidEpisode
import com.nomadics9.ananas.models.toFindroidItem 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.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ClientCapabilitiesDto 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.DeviceOptionsDto
import org.jellyfin.sdk.model.api.DeviceProfile import org.jellyfin.sdk.model.api.DeviceProfile
import org.jellyfin.sdk.model.api.DirectPlayProfile import org.jellyfin.sdk.model.api.DirectPlayProfile
@ -57,7 +57,6 @@ import org.jellyfin.sdk.model.api.PublicSystemInfo
import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.api.SortOrder
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
import org.jellyfin.sdk.model.api.SubtitleProfile import org.jellyfin.sdk.model.api.SubtitleProfile
import org.jellyfin.sdk.model.api.TranscodeReason
import org.jellyfin.sdk.model.api.TranscodeSeekInfo import org.jellyfin.sdk.model.api.TranscodeSeekInfo
import org.jellyfin.sdk.model.api.TranscodingProfile import org.jellyfin.sdk.model.api.TranscodingProfile
import org.jellyfin.sdk.model.api.UserConfiguration import org.jellyfin.sdk.model.api.UserConfiguration
@ -71,53 +70,68 @@ class JellyfinRepositoryImpl(
private val database: ServerDatabaseDao, private val database: ServerDatabaseDao,
private val appPreferences: AppPreferences, private val appPreferences: AppPreferences,
) : JellyfinRepository { ) : JellyfinRepository {
override suspend fun getPublicSystemInfo(): PublicSystemInfo = withContext(Dispatchers.IO) { override suspend fun getPublicSystemInfo(): PublicSystemInfo =
withContext(Dispatchers.IO) {
jellyfinApi.systemApi.getPublicSystemInfo().content jellyfinApi.systemApi.getPublicSystemInfo().content
} }
override suspend fun getUserViews(): List<BaseItemDto> = withContext(Dispatchers.IO) { override suspend fun getUserViews(): List<BaseItemDto> =
jellyfinApi.viewsApi.getUserViews(jellyfinApi.userId!!).content.items.orEmpty() 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 jellyfinApi.userLibraryApi.getItem(itemId, jellyfinApi.userId!!).content
} }
override suspend fun getEpisode(itemId: UUID): FindroidEpisode = override suspend fun getEpisode(itemId: UUID): FindroidEpisode =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getItem( jellyfinApi.userLibraryApi
.getItem(
itemId, itemId,
jellyfinApi.userId!!, jellyfinApi.userId!!,
).content.toFindroidEpisode(this@JellyfinRepositoryImpl, database)!! ).content
.toFindroidEpisode(this@JellyfinRepositoryImpl, database)!!
} }
override suspend fun getMovie(itemId: UUID): FindroidMovie = override suspend fun getMovie(itemId: UUID): FindroidMovie =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getItem( jellyfinApi.userLibraryApi
.getItem(
itemId, itemId,
jellyfinApi.userId!!, jellyfinApi.userId!!,
).content.toFindroidMovie(this@JellyfinRepositoryImpl, database) ).content
.toFindroidMovie(this@JellyfinRepositoryImpl, database)
} }
override suspend fun getShow(itemId: UUID): FindroidShow = override suspend fun getShow(itemId: UUID): FindroidShow =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getItem( jellyfinApi.userLibraryApi
.getItem(
itemId, itemId,
jellyfinApi.userId!!, jellyfinApi.userId!!,
).content.toFindroidShow(this@JellyfinRepositoryImpl) ).content
.toFindroidShow(this@JellyfinRepositoryImpl)
} }
override suspend fun getSeason(itemId: UUID): FindroidSeason = override suspend fun getSeason(itemId: UUID): FindroidSeason =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getItem( jellyfinApi.userLibraryApi
.getItem(
itemId, itemId,
jellyfinApi.userId!!, jellyfinApi.userId!!,
).content.toFindroidSeason(this@JellyfinRepositoryImpl) ).content
.toFindroidSeason(this@JellyfinRepositoryImpl)
} }
override suspend fun getLibraries(): List<FindroidCollection> = override suspend fun getLibraries(): List<FindroidCollection> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.itemsApi.getItems( jellyfinApi.itemsApi
.getItems(
jellyfinApi.userId!!, jellyfinApi.userId!!,
).content.items ).content.items
.orEmpty() .orEmpty()
@ -134,7 +148,8 @@ class JellyfinRepositoryImpl(
limit: Int?, limit: Int?,
): List<FindroidItem> = ): List<FindroidItem> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.itemsApi.getItems( jellyfinApi.itemsApi
.getItems(
jellyfinApi.userId!!, jellyfinApi.userId!!,
parentId = parentId, parentId = parentId,
includeItemTypes = includeTypes, includeItemTypes = includeTypes,
@ -154,9 +169,10 @@ class JellyfinRepositoryImpl(
recursive: Boolean, recursive: Boolean,
sortBy: SortBy, sortBy: SortBy,
sortOrder: SortOrder, sortOrder: SortOrder,
): Flow<PagingData<FindroidItem>> { ): Flow<PagingData<FindroidItem>> =
return Pager( Pager(
config = PagingConfig( config =
PagingConfig(
pageSize = 10, pageSize = 10,
maxSize = 100, maxSize = 100,
enablePlaceholders = false, enablePlaceholders = false,
@ -172,14 +188,15 @@ class JellyfinRepositoryImpl(
) )
}, },
).flow ).flow
}
override suspend fun getPersonItems( override suspend fun getPersonItems(
personIds: List<UUID>, personIds: List<UUID>,
includeTypes: List<BaseItemKind>?, includeTypes: List<BaseItemKind>?,
recursive: Boolean, recursive: Boolean,
): List<FindroidItem> = withContext(Dispatchers.IO) { ): List<FindroidItem> =
jellyfinApi.itemsApi.getItems( withContext(Dispatchers.IO) {
jellyfinApi.itemsApi
.getItems(
jellyfinApi.userId!!, jellyfinApi.userId!!,
personIds = personIds, personIds = personIds,
includeItemTypes = includeTypes, includeItemTypes = includeTypes,
@ -193,10 +210,12 @@ class JellyfinRepositoryImpl(
override suspend fun getFavoriteItems(): List<FindroidItem> = override suspend fun getFavoriteItems(): List<FindroidItem> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.itemsApi.getItems( jellyfinApi.itemsApi
.getItems(
jellyfinApi.userId!!, jellyfinApi.userId!!,
filters = listOf(ItemFilter.IS_FAVORITE), filters = listOf(ItemFilter.IS_FAVORITE),
includeItemTypes = listOf( includeItemTypes =
listOf(
BaseItemKind.MOVIE, BaseItemKind.MOVIE,
BaseItemKind.SERIES, BaseItemKind.SERIES,
BaseItemKind.EPISODE, BaseItemKind.EPISODE,
@ -209,10 +228,12 @@ class JellyfinRepositoryImpl(
override suspend fun getSearchItems(searchQuery: String): List<FindroidItem> = override suspend fun getSearchItems(searchQuery: String): List<FindroidItem> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.itemsApi.getItems( jellyfinApi.itemsApi
.getItems(
jellyfinApi.userId!!, jellyfinApi.userId!!,
searchTerm = searchQuery, searchTerm = searchQuery,
includeItemTypes = listOf( includeItemTypes =
listOf(
BaseItemKind.MOVIE, BaseItemKind.MOVIE,
BaseItemKind.SERIES, BaseItemKind.SERIES,
BaseItemKind.EPISODE, BaseItemKind.EPISODE,
@ -224,12 +245,15 @@ class JellyfinRepositoryImpl(
} }
override suspend fun getResumeItems(): List<FindroidItem> { override suspend fun getResumeItems(): List<FindroidItem> {
val items = withContext(Dispatchers.IO) { val items =
jellyfinApi.itemsApi.getResumeItems( withContext(Dispatchers.IO) {
jellyfinApi.itemsApi
.getResumeItems(
jellyfinApi.userId!!, jellyfinApi.userId!!,
limit = 12, limit = 12,
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE), includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE),
).content.items.orEmpty() ).content.items
.orEmpty()
} }
return items.mapNotNull { return items.mapNotNull {
it.toFindroidItem(this, database) it.toFindroidItem(this, database)
@ -237,8 +261,10 @@ class JellyfinRepositoryImpl(
} }
override suspend fun getLatestMedia(parentId: UUID): List<FindroidItem> { override suspend fun getLatestMedia(parentId: UUID): List<FindroidItem> {
val items = withContext(Dispatchers.IO) { val items =
jellyfinApi.userLibraryApi.getLatestMedia( withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi
.getLatestMedia(
jellyfinApi.userId!!, jellyfinApi.userId!!,
parentId = parentId, parentId = parentId,
limit = 16, 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) { withContext(Dispatchers.IO) {
if (!offline) { if (!offline) {
jellyfinApi.showsApi.getSeasons(seriesId, jellyfinApi.userId!!).content.items jellyfinApi.showsApi
.getSeasons(seriesId, jellyfinApi.userId!!)
.content.items
.orEmpty() .orEmpty()
.map { it.toFindroidSeason(this@JellyfinRepositoryImpl) } .map { it.toFindroidSeason(this@JellyfinRepositoryImpl) }
} else { } else {
@ -262,7 +293,8 @@ class JellyfinRepositoryImpl(
override suspend fun getNextUp(seriesId: UUID?): List<FindroidEpisode> = override suspend fun getNextUp(seriesId: UUID?): List<FindroidEpisode> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.showsApi.getNextUp( jellyfinApi.showsApi
.getNextUp(
jellyfinApi.userId!!, jellyfinApi.userId!!,
limit = 24, limit = 24,
seriesId = seriesId, seriesId = seriesId,
@ -282,7 +314,8 @@ class JellyfinRepositoryImpl(
): List<FindroidEpisode> = ): List<FindroidEpisode> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (!offline) { if (!offline) {
jellyfinApi.showsApi.getEpisodes( jellyfinApi.showsApi
.getEpisodes(
seriesId, seriesId,
jellyfinApi.userId!!, jellyfinApi.userId!!,
seasonId = seasonId, 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) { withContext(Dispatchers.IO) {
val sources = mutableListOf<FindroidSource>() val sources = mutableListOf<FindroidSource>()
sources.addAll( sources.addAll(
jellyfinApi.mediaInfoApi.getPostedPlaybackInfo( jellyfinApi.mediaInfoApi
.getPostedPlaybackInfo(
itemId, itemId,
PlaybackInfoDto( PlaybackInfoDto(
userId = jellyfinApi.userId!!, userId = jellyfinApi.userId!!,
deviceProfile = DeviceProfile( deviceProfile =
DeviceProfile(
name = "Direct play all", name = "Direct play all",
maxStaticBitrate = 1_000_000_000, maxStaticBitrate = 1_000_000_000,
maxStreamingBitrate = 1_000_000_000, maxStreamingBitrate = 1_000_000_000,
codecProfiles = emptyList(), codecProfiles = emptyList(),
containerProfiles = emptyList(), containerProfiles = emptyList(),
directPlayProfiles = listOf( directPlayProfiles =
listOf(
DirectPlayProfile(type = DlnaProfileType.VIDEO), DirectPlayProfile(type = DlnaProfileType.VIDEO),
DirectPlayProfile(type = DlnaProfileType.AUDIO), DirectPlayProfile(type = DlnaProfileType.AUDIO),
), ),
transcodingProfiles = emptyList(), transcodingProfiles = emptyList(),
subtitleProfiles = listOf( subtitleProfiles =
listOf(
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL), SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL), SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
), ),
), ),
maxStreamingBitrate = 1_000_000_000, maxStreamingBitrate = 1_000_000_000,
), ),
).content.mediaSources.map { ).content.mediaSources
.map {
it.toFindroidSource( it.toFindroidSource(
this@JellyfinRepositoryImpl, this@JellyfinRepositoryImpl,
itemId, itemId,
@ -337,24 +378,30 @@ class JellyfinRepositoryImpl(
sources 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) { withContext(Dispatchers.IO) {
// val deviceId = getDeviceId()
try { try {
val url = if (playSessionId != null) { val url =
if (playSessionId != null) {
jellyfinApi.videosApi.getVideoStreamUrl( jellyfinApi.videosApi.getVideoStreamUrl(
itemId, itemId,
static = true, static = true,
mediaSourceId = mediaSourceId, mediaSourceId = mediaSourceId,
playSessionId = playSessionId, playSessionId = playSessionId,
deviceId = getDeviceId(), // deviceId = deviceId,
context = EncodingContext.STATIC context = EncodingContext.STREAMING,
) )
} else { } else {
jellyfinApi.videosApi.getVideoStreamUrl( jellyfinApi.videosApi.getVideoStreamUrl(
itemId, itemId,
static = true, static = true,
mediaSourceId = mediaSourceId, mediaSourceId = mediaSourceId,
deviceId = getDeviceId(), // deviceId = deviceId,
) )
} }
url url
@ -377,12 +424,15 @@ class JellyfinRepositoryImpl(
pathParameters["itemId"] = itemId pathParameters["itemId"] = itemId
try { try {
val segmentToConvert = jellyfinApi.api.get<FindroidSegments>( val segmentToConvert =
jellyfinApi.api
.get<FindroidSegments>(
"/Episode/{itemId}/IntroSkipperSegments", "/Episode/{itemId}/IntroSkipperSegments",
pathParameters, pathParameters,
).content ).content
val segmentConverted = mutableListOf( val segmentConverted =
mutableListOf(
segmentToConvert.intro!!.let { segmentToConvert.intro!!.let {
FindroidSegment( FindroidSegment(
type = "intro", 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) { withContext(Dispatchers.IO) {
try { try {
try { try {
@ -421,9 +475,13 @@ class JellyfinRepositoryImpl(
if (sources != null) { if (sources != null) {
return@withContext File(sources.first(), index.toString()).readBytes() 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) { } catch (e: Exception) {
return@withContext null return@withContext null
} }
@ -434,7 +492,8 @@ class JellyfinRepositoryImpl(
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.sessionApi.postCapabilities( jellyfinApi.sessionApi.postCapabilities(
playableMediaTypes = listOf(MediaType.VIDEO), playableMediaTypes = listOf(MediaType.VIDEO),
supportedCommands = listOf( supportedCommands =
listOf(
GeneralCommandType.VOLUME_UP, GeneralCommandType.VOLUME_UP,
GeneralCommandType.VOLUME_DOWN, GeneralCommandType.VOLUME_DOWN,
GeneralCommandType.TOGGLE_MUTE, GeneralCommandType.TOGGLE_MUTE,
@ -570,57 +629,62 @@ class JellyfinRepositoryImpl(
} }
} }
override suspend fun getUserConfiguration(): UserConfiguration = withContext(Dispatchers.IO) { override suspend fun getUserConfiguration(): UserConfiguration =
jellyfinApi.userApi.getCurrentUser().content.configuration!! withContext(Dispatchers.IO) {
jellyfinApi.userApi
.getCurrentUser()
.content.configuration!!
} }
override suspend fun getDownloads(): List<FindroidItem> = override suspend fun getDownloads(): List<FindroidItem> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val items = mutableListOf<FindroidItem>() val items = mutableListOf<FindroidItem>()
items.addAll( items.addAll(
database.getMoviesByServerId(appPreferences.currentServer!!) database
.getMoviesByServerId(appPreferences.currentServer!!)
.map { it.toFindroidMovie(database, jellyfinApi.userId!!) }, .map { it.toFindroidMovie(database, jellyfinApi.userId!!) },
) )
items.addAll( items.addAll(
database.getShowsByServerId(appPreferences.currentServer!!) database
.getShowsByServerId(appPreferences.currentServer!!)
.map { it.toFindroidShow(database, jellyfinApi.userId!!) }, .map { it.toFindroidShow(database, jellyfinApi.userId!!) },
) )
items items
} }
override fun getUserId(): UUID { override fun getUserId(): UUID = jellyfinApi.userId!!
return jellyfinApi.userId!!
}
override suspend fun buildDeviceProfile(
override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int, Int> { maxBitrate: Int,
return when (transcodeResolution) { container: String,
1080 -> 8000000 to 384000 // Adjusted for 1080p context: EncodingContext,
720 -> 2000000 to 384000 // Adjusted for 720p ): DeviceProfile {
480 -> 1000000 to 384000 // Adjusted for 480p val deviceProfile =
360 -> 800000 to 128000 // Adjusted for 360p ClientCapabilitiesDto(
else -> 12000000 to 384000
}
}
override suspend fun buildDeviceProfile(maxBitrate: Int, container: String, context: EncodingContext): DeviceProfile {
val deviceProfile = ClientCapabilitiesDto(
supportedCommands = emptyList(), supportedCommands = emptyList(),
playableMediaTypes = emptyList(), playableMediaTypes =
listOf(
MediaType.VIDEO,
MediaType.AUDIO,
MediaType.UNKNOWN,
),
supportsMediaControl = true, supportsMediaControl = true,
supportsPersistentIdentifier = true, supportsPersistentIdentifier = true,
deviceProfile = DeviceProfile( deviceProfile =
DeviceProfile(
name = "AnanasUser", name = "AnanasUser",
id = getUserId().toString(), id = getUserId().toString(),
maxStaticBitrate = maxBitrate, maxStaticBitrate = maxBitrate,
maxStreamingBitrate = maxBitrate, maxStreamingBitrate = maxBitrate,
codecProfiles = emptyList(), codecProfiles = emptyList(),
containerProfiles = listOf(), containerProfiles = listOf(),
directPlayProfiles = listOf( directPlayProfiles =
listOf(
DirectPlayProfile(type = DlnaProfileType.VIDEO), DirectPlayProfile(type = DlnaProfileType.VIDEO),
DirectPlayProfile(type = DlnaProfileType.AUDIO), DirectPlayProfile(type = DlnaProfileType.AUDIO),
), ),
transcodingProfiles = listOf( transcodingProfiles =
listOf(
TranscodingProfile( TranscodingProfile(
container = container, container = container,
context = context, context = context,
@ -628,20 +692,22 @@ class JellyfinRepositoryImpl(
audioCodec = "aac,ac3,eac3", audioCodec = "aac,ac3,eac3",
videoCodec = "hevc,h264", videoCodec = "hevc,h264",
type = DlnaProfileType.VIDEO, type = DlnaProfileType.VIDEO,
conditions = listOf( conditions =
listOf(
ProfileCondition( ProfileCondition(
condition = ProfileConditionType.LESS_THAN_EQUAL, condition = ProfileConditionType.LESS_THAN_EQUAL,
property = ProfileConditionValue.VIDEO_BITRATE, property = ProfileConditionValue.VIDEO_BITRATE,
value = "8000000", value = "8000000",
isRequired = true, isRequired = true,
) ),
), ),
copyTimestamps = true, copyTimestamps = true,
enableSubtitlesInManifest = true, enableSubtitlesInManifest = true,
transcodeSeekInfo = TranscodeSeekInfo.AUTO, transcodeSeekInfo = TranscodeSeekInfo.AUTO,
), ),
), ),
subtitleProfiles = listOf( subtitleProfiles =
listOf(
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL), SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL), SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("sub", SubtitleDeliveryMethod.EXTERNAL), SubtitleProfile("sub", SubtitleDeliveryMethod.EXTERNAL),
@ -649,16 +715,21 @@ class JellyfinRepositoryImpl(
SubtitleProfile("ssa", SubtitleDeliveryMethod.EXTERNAL), SubtitleProfile("ssa", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("pgs", SubtitleDeliveryMethod.EXTERNAL), SubtitleProfile("pgs", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("dvb_teletext", SubtitleDeliveryMethod.EXTERNAL), SubtitleProfile("dvb_teletext", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("dvd_subtitle", SubtitleDeliveryMethod.EXTERNAL) SubtitleProfile("dvd_subtitle", SubtitleDeliveryMethod.EXTERNAL),
),
), ),
)
) )
return deviceProfile.deviceProfile!! return deviceProfile.deviceProfile!!
} }
override suspend fun getPostedPlaybackInfo(
override suspend fun getPostedPlaybackInfo(itemId: UUID ,enableDirectStream: Boolean ,deviceProfile: DeviceProfile ,maxBitrate: Int): Response<PlaybackInfoResponse> { itemId: UUID,
val playbackInfo = jellyfinApi.mediaInfoApi.getPostedPlaybackInfo( enableDirectStream: Boolean,
deviceProfile: DeviceProfile,
maxBitrate: Int,
): Response<PlaybackInfoResponse> {
val playbackInfo =
jellyfinApi.mediaInfoApi.getPostedPlaybackInfo(
itemId = itemId, itemId = itemId,
PlaybackInfoDto( PlaybackInfoDto(
userId = jellyfinApi.userId!!, userId = jellyfinApi.userId!!,
@ -666,37 +737,56 @@ class JellyfinRepositoryImpl(
enableDirectPlay = false, enableDirectPlay = false,
enableDirectStream = enableDirectStream, enableDirectStream = enableDirectStream,
autoOpenLiveStream = true, autoOpenLiveStream = true,
deviceProfile = deviceProfile, deviceProfile = buildDeviceProfile(maxBitrate, "ts", EncodingContext.STREAMING),
allowAudioStreamCopy = true, allowAudioStreamCopy = true,
allowVideoStreamCopy = true, allowVideoStreamCopy = true,
maxStreamingBitrate = maxBitrate, maxStreamingBitrate = maxBitrate,
) ),
) )
return playbackInfo return playbackInfo
} }
override suspend fun getVideoStreambyContainerUrl(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int, container: String): String { override suspend fun getVideoStreambyContainerUrl(
val url = jellyfinApi.videosApi.getVideoStreamByContainerUrl( itemId: UUID,
deviceId: String,
mediaSourceId: String,
playSessionId: String,
videoBitrate: Int,
maxHeight: Int,
container: String,
): String {
val url =
jellyfinApi.videosApi.getVideoStreamByContainerUrl(
itemId, itemId,
static = false, static = false,
deviceId = deviceId, deviceId = deviceId,
mediaSourceId = mediaSourceId, mediaSourceId = mediaSourceId,
playSessionId = playSessionId, playSessionId = playSessionId,
videoBitRate = videoBitrate, videoBitRate = videoBitrate,
audioBitRate = 384000, maxHeight = maxHeight,
audioBitRate = 128000,
videoCodec = "hevc", videoCodec = "hevc",
audioCodec = "aac,ac3,eac3", audioCodec = "aac",
container = container, container = container,
startTimeTicks = 0, startTimeTicks = 0,
copyTimestamps = true, copyTimestamps = true,
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
) )
return url return url
} }
override suspend fun getTranscodedVideoStream(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int): String { override suspend fun getTranscodedVideoStream(
val isAuto = videoBitrate == 12000000 itemId: UUID,
val url = if (!isAuto) { 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( jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl(
itemId, itemId,
static = false, static = false,
@ -705,9 +795,9 @@ class JellyfinRepositoryImpl(
playSessionId = playSessionId, playSessionId = playSessionId,
videoBitRate = videoBitrate, videoBitRate = videoBitrate,
enableAdaptiveBitrateStreaming = false, enableAdaptiveBitrateStreaming = false,
audioBitRate = 384000, audioBitRate = 128000,
videoCodec = "hevc", videoCodec = "hevc",
audioCodec = "aac,ac3,eac3", audioCodec = "aac",
startTimeTicks = 0, startTimeTicks = 0,
copyTimestamps = true, copyTimestamps = true,
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL, subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
@ -724,7 +814,7 @@ class JellyfinRepositoryImpl(
playSessionId = playSessionId, playSessionId = playSessionId,
enableAdaptiveBitrateStreaming = true, enableAdaptiveBitrateStreaming = true,
videoCodec = "hevc", videoCodec = "hevc",
audioCodec = "aac,ac3,eac3", audioCodec = "aac",
startTimeTicks = 0, startTimeTicks = 0,
copyTimestamps = true, copyTimestamps = true,
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL, subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
@ -733,23 +823,20 @@ class JellyfinRepositoryImpl(
transcodeReasons = "ContainerBitrateExceedsLimit", transcodeReasons = "ContainerBitrateExceedsLimit",
) )
} }
} catch (e: Exception) {
Timber.e(e)
throw e
}
return url return url
} }
override suspend fun getDeviceId(): String = jellyfinApi.api.deviceInfo.id
override suspend fun getDeviceId(): String {
val devices = jellyfinApi.devicesApi.getDevices(getUserId())
return devices.content.items?.firstOrNull()?.id!!
}
override suspend fun stopEncodingProcess(playSessionId: String) { override suspend fun stopEncodingProcess(playSessionId: String) {
val deviceId = getDeviceId() val deviceId = getDeviceId()
jellyfinApi.api.hlsSegmentApi.stopEncodingProcess( jellyfinApi.api.hlsSegmentApi.stopEncodingProcess(
deviceId = deviceId, deviceId = deviceId,
playSessionId = playSessionId playSessionId = playSessionId,
) )
} }
} }

View file

@ -1,7 +1,6 @@
package com.nomadics9.ananas.repository package com.nomadics9.ananas.repository
import android.content.Context import android.content.Context
import android.devicelock.DeviceId
import androidx.paging.PagingData import androidx.paging.PagingData
import com.nomadics9.ananas.AppPreferences import com.nomadics9.ananas.AppPreferences
import com.nomadics9.ananas.api.JellyfinApi 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.api.client.Response
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind 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.DeviceProfile
import org.jellyfin.sdk.model.api.EncodingContext import org.jellyfin.sdk.model.api.EncodingContext
import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.ItemFields
@ -44,14 +42,9 @@ class JellyfinRepositoryOfflineImpl(
private val database: ServerDatabaseDao, private val database: ServerDatabaseDao,
private val appPreferences: AppPreferences, private val appPreferences: AppPreferences,
) : JellyfinRepository { ) : JellyfinRepository {
override suspend fun getPublicSystemInfo(): PublicSystemInfo = throw Exception("System info not available in offline mode")
override suspend fun getPublicSystemInfo(): PublicSystemInfo { override suspend fun getUserViews(): List<BaseItemDto> = emptyList()
throw Exception("System info not available in offline mode")
}
override suspend fun getUserViews(): List<BaseItemDto> {
return emptyList()
}
override suspend fun getItem(itemId: UUID): BaseItemDto { override suspend fun getItem(itemId: UUID): BaseItemDto {
TODO("Not yet implemented") TODO("Not yet implemented")
@ -115,36 +108,59 @@ class JellyfinRepositoryOfflineImpl(
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun getSearchItems(searchQuery: String): List<FindroidItem> { override suspend fun getSearchItems(searchQuery: String): List<FindroidItem> =
return withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val movies = database.searchMovies(appPreferences.currentServer!!, searchQuery).map { it.toFindroidMovie(database, jellyfinApi.userId!!) } val movies =
val shows = database.searchShows(appPreferences.currentServer!!, searchQuery).map { it.toFindroidShow(database, jellyfinApi.userId!!) } database
val episodes = database.searchEpisodes(appPreferences.currentServer!!, searchQuery).map { it.toFindroidEpisode(database, jellyfinApi.userId!!) } .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 movies + shows + episodes
} }
}
override suspend fun getResumeItems(): List<FindroidItem> { override suspend fun getResumeItems(): List<FindroidItem> =
return withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val movies = database.getMoviesByServerId(appPreferences.currentServer!!).map { it.toFindroidMovie(database, jellyfinApi.userId!!) }.filter { it.playbackPositionTicks > 0 } val movies =
val episodes = database.getEpisodesByServerId(appPreferences.currentServer!!).map { it.toFindroidEpisode(database, jellyfinApi.userId!!) }.filter { it.playbackPositionTicks > 0 } 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 movies + episodes
} }
}
override suspend fun getLatestMedia(parentId: UUID): List<FindroidItem> { override suspend fun getLatestMedia(parentId: UUID): List<FindroidItem> = emptyList()
return emptyList()
}
override suspend fun getSeasons(seriesId: UUID, offline: Boolean): List<FindroidSeason> = override suspend fun getSeasons(
seriesId: UUID,
offline: Boolean,
): List<FindroidSeason> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
database.getSeasonsByShowId(seriesId).map { it.toFindroidSeason(database, jellyfinApi.userId!!) } database.getSeasonsByShowId(seriesId).map { it.toFindroidSeason(database, jellyfinApi.userId!!) }
} }
override suspend fun getNextUp(seriesId: UUID?): List<FindroidEpisode> { override suspend fun getNextUp(seriesId: UUID?): List<FindroidEpisode> =
return withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val result = mutableListOf<FindroidEpisode>() 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 if (seriesId != null) it.id == seriesId else true
} }
for (show in shows) { for (show in shows) {
@ -158,7 +174,6 @@ class JellyfinRepositoryOfflineImpl(
} }
result.filter { it.playbackPositionTicks == 0L } result.filter { it.playbackPositionTicks == 0L }
} }
}
override suspend fun getEpisodes( override suspend fun getEpisodes(
seriesId: UUID, seriesId: UUID,
@ -174,12 +189,19 @@ class JellyfinRepositoryOfflineImpl(
items items
} }
override suspend fun getMediaSources(itemId: UUID, includePath: Boolean): List<FindroidSource> = override suspend fun getMediaSources(
itemId: UUID,
includePath: Boolean,
): List<FindroidSource> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
database.getSources(itemId).map { it.toFindroidSource(database) } 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") TODO("Not yet implemented")
} }
@ -188,7 +210,11 @@ class JellyfinRepositoryOfflineImpl(
database.getSegments(itemId)?.toFindroidSegments() 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) { withContext(Dispatchers.IO) {
try { try {
val sources = File(context.filesDir, "trickplay/$itemId").listFiles() ?: return@withContext null 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 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) { withContext(Dispatchers.IO) {
when { when {
playedPercentage < 10 -> { playedPercentage < 10 -> {
@ -262,35 +292,31 @@ class JellyfinRepositoryOfflineImpl(
} }
} }
override fun getBaseUrl(): String { override fun getBaseUrl(): String = ""
return ""
}
override suspend fun updateDeviceName(name: String) { override suspend fun updateDeviceName(name: String) {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun getUserConfiguration(): UserConfiguration? { override suspend fun getUserConfiguration(): UserConfiguration? = null
return null
}
override suspend fun getDownloads(): List<FindroidItem> = override suspend fun getDownloads(): List<FindroidItem> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val items = mutableListOf<FindroidItem>() val items = mutableListOf<FindroidItem>()
items.addAll( items.addAll(
database.getMoviesByServerId(appPreferences.currentServer!!) database
.getMoviesByServerId(appPreferences.currentServer!!)
.map { it.toFindroidMovie(database, jellyfinApi.userId!!) }, .map { it.toFindroidMovie(database, jellyfinApi.userId!!) },
) )
items.addAll( items.addAll(
database.getShowsByServerId(appPreferences.currentServer!!) database
.getShowsByServerId(appPreferences.currentServer!!)
.map { it.toFindroidShow(database, jellyfinApi.userId!!) }, .map { it.toFindroidShow(database, jellyfinApi.userId!!) },
) )
items items
} }
override fun getUserId(): UUID { override fun getUserId(): UUID = jellyfinApi.userId!!
return jellyfinApi.userId!!
}
override suspend fun getDeviceId(): String { override suspend fun getDeviceId(): String {
TODO("Not yet implemented") TODO("Not yet implemented")
@ -299,7 +325,7 @@ class JellyfinRepositoryOfflineImpl(
override suspend fun buildDeviceProfile( override suspend fun buildDeviceProfile(
maxBitrate: Int, maxBitrate: Int,
container: String, container: String,
context: EncodingContext context: EncodingContext,
): DeviceProfile { ): DeviceProfile {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
@ -310,7 +336,8 @@ class JellyfinRepositoryOfflineImpl(
mediaSourceId: String, mediaSourceId: String,
playSessionId: String, playSessionId: String,
videoBitrate: Int, videoBitrate: Int,
container: String maxHeight: Int,
container: String,
): String { ): String {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
@ -320,7 +347,7 @@ class JellyfinRepositoryOfflineImpl(
deviceId: String, deviceId: String,
mediaSourceId: String, mediaSourceId: String,
playSessionId: String, playSessionId: String,
videoBitrate: Int videoBitrate: Int,
): String { ): String {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
@ -329,7 +356,7 @@ class JellyfinRepositoryOfflineImpl(
itemId: UUID, itemId: UUID,
enableDirectStream: Boolean, enableDirectStream: Boolean,
deviceProfile: DeviceProfile, deviceProfile: DeviceProfile,
maxBitrate: Int maxBitrate: Int,
): Response<PlaybackInfoResponse> { ): Response<PlaybackInfoResponse> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
@ -337,8 +364,4 @@ class JellyfinRepositoryOfflineImpl(
override suspend fun stopEncodingProcess(playSessionId: String) { override suspend fun stopEncodingProcess(playSessionId: String) {
TODO("Not yet implemented") 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.PlayerChapter
import com.nomadics9.ananas.models.PlayerItem import com.nomadics9.ananas.models.PlayerItem
import com.nomadics9.ananas.models.Trickplay import com.nomadics9.ananas.models.Trickplay
import com.nomadics9.ananas.models.VideoQuality
import com.nomadics9.ananas.mpv.MPVPlayer import com.nomadics9.ananas.mpv.MPVPlayer
import com.nomadics9.ananas.player.video.R import com.nomadics9.ananas.player.video.R
import com.nomadics9.ananas.repository.JellyfinRepository import com.nomadics9.ananas.repository.JellyfinRepository
@ -58,11 +59,13 @@ constructor(
private val appPreferences: AppPreferences, private val appPreferences: AppPreferences,
private val jellyfinApi: JellyfinApi, private val jellyfinApi: JellyfinApi,
private val savedStateHandle: SavedStateHandle, private val savedStateHandle: SavedStateHandle,
) : ViewModel(), Player.Listener { ) : ViewModel(),
Player.Listener {
val player: Player val player: Player
private var originalHeight: Int = 0 private var originalHeight: Int = 0
private val _uiState = MutableStateFlow( private val _uiState =
MutableStateFlow(
UiState( UiState(
currentItemTitle = "", currentItemTitle = "",
currentSegment = null, currentSegment = null,
@ -101,11 +104,14 @@ constructor(
init { init {
if (appPreferences.playerMpv) { if (appPreferences.playerMpv) {
val trackSelectionParameters = TrackSelectionParameters.Builder(application) val trackSelectionParameters =
TrackSelectionParameters
.Builder(application)
.setPreferredAudioLanguage(appPreferences.preferredAudioLanguage) .setPreferredAudioLanguage(appPreferences.preferredAudioLanguage)
.setPreferredTextLanguage(appPreferences.preferredSubtitleLanguage) .setPreferredTextLanguage(appPreferences.preferredSubtitleLanguage)
.build() .build()
player = MPVPlayer( player =
MPVPlayer(
context = application, context = application,
requestAudioFocus = true, requestAudioFocus = true,
trackSelectionParameters = trackSelectionParameters, trackSelectionParameters = trackSelectionParameters,
@ -121,30 +127,31 @@ constructor(
DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON,
) )
trackSelector.setParameters( trackSelector.setParameters(
trackSelector.buildUponParameters() trackSelector
.buildUponParameters()
.setTunnelingEnabled(true) .setTunnelingEnabled(true)
.setPreferredAudioLanguage(appPreferences.preferredAudioLanguage) .setPreferredAudioLanguage(appPreferences.preferredAudioLanguage)
.setPreferredTextLanguage(appPreferences.preferredSubtitleLanguage), .setPreferredTextLanguage(appPreferences.preferredSubtitleLanguage),
) )
player = ExoPlayer.Builder(application, renderersFactory) player =
ExoPlayer
.Builder(application, renderersFactory)
.setTrackSelector(trackSelector) .setTrackSelector(trackSelector)
.setAudioAttributes( .setAudioAttributes(
AudioAttributes.Builder() AudioAttributes
.Builder()
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE) .setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
.setUsage(C.USAGE_MEDIA) .setUsage(C.USAGE_MEDIA)
.build(), .build(),
/* handleAudioFocus = */ // handleAudioFocus =
true, true,
) ).setSeekBackIncrementMs(appPreferences.playerSeekBackIncrement)
.setSeekBackIncrementMs(appPreferences.playerSeekBackIncrement)
.setSeekForwardIncrementMs(appPreferences.playerSeekForwardIncrement) .setSeekForwardIncrementMs(appPreferences.playerSeekForwardIncrement)
.build() .build()
} }
} }
fun initializePlayer( fun initializePlayer(items: Array<PlayerItem>) {
items: Array<PlayerItem>,
) {
this.items = items this.items = items
player.addListener(this) player.addListener(this)
@ -153,8 +160,10 @@ constructor(
try { try {
for (item in items) { for (item in items) {
val streamUrl = item.mediaSourceUri val streamUrl = item.mediaSourceUri
val mediaSubtitles = item.externalSubtitles.map { externalSubtitle -> val mediaSubtitles =
MediaItem.SubtitleConfiguration.Builder(externalSubtitle.uri) item.externalSubtitles.map { externalSubtitle ->
MediaItem.SubtitleConfiguration
.Builder(externalSubtitle.uri)
.setLabel(externalSubtitle.title.ifBlank { application.getString(R.string.external) }) .setLabel(externalSubtitle.title.ifBlank { application.getString(R.string.external) })
.setMimeType(externalSubtitle.mimeType) .setMimeType(externalSubtitle.mimeType)
.setLanguage(externalSubtitle.language) .setLanguage(externalSubtitle.language)
@ -170,24 +179,25 @@ constructor(
Timber.d("Stream url: $streamUrl") Timber.d("Stream url: $streamUrl")
val mediaItem = val mediaItem =
MediaItem.Builder() MediaItem
.Builder()
.setMediaId(item.itemId.toString()) .setMediaId(item.itemId.toString())
.setUri(streamUrl) .setUri(streamUrl)
.setMediaMetadata( .setMediaMetadata(
MediaMetadata.Builder() MediaMetadata
.Builder()
.setTitle(item.name) .setTitle(item.name)
.build(), .build(),
) ).setSubtitleConfigurations(mediaSubtitles)
.setSubtitleConfigurations(mediaSubtitles)
.build() .build()
mediaItems.add(mediaItem) mediaItems.add(mediaItem)
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
} }
val startPosition = if (playbackPosition == 0L) { val startPosition =
if (playbackPosition == 0L) {
items.getOrNull(currentMediaItemIndex)?.playbackPosition ?: C.TIME_UNSET items.getOrNull(currentMediaItemIndex)?.playbackPosition ?: C.TIME_UNSET
} else { } else {
playbackPosition playbackPosition
@ -231,7 +241,8 @@ constructor(
} }
private fun pollPosition(player: Player) { private fun pollPosition(player: Player) {
val playbackProgressRunnable = object : Runnable { val playbackProgressRunnable =
object : Runnable {
override fun run() { override fun run() {
savedStateHandle["position"] = player.currentPosition savedStateHandle["position"] = player.currentPosition
viewModelScope.launch { viewModelScope.launch {
@ -251,7 +262,8 @@ constructor(
handler.postDelayed(this, 5000L) handler.postDelayed(this, 5000L)
} }
} }
val segmentCheckRunnable = object : Runnable { val segmentCheckRunnable =
object : Runnable {
override fun run() { override fun run() {
val currentMediaItem = player.currentMediaItem val currentMediaItem = player.currentMediaItem
if (currentMediaItem != null && currentMediaItem.mediaId.isNotEmpty()) { if (currentMediaItem != null && currentMediaItem.mediaId.isNotEmpty()) {
@ -276,12 +288,16 @@ constructor(
if (segments.isNotEmpty()) handler.post(segmentCheckRunnable) 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}") Timber.d("Playing MediaItem: ${mediaItem?.mediaId}")
savedStateHandle["mediaItemIndex"] = player.currentMediaItemIndex savedStateHandle["mediaItemIndex"] = player.currentMediaItemIndex
viewModelScope.launch { viewModelScope.launch {
try { try {
items.first { it.itemId.toString() == player.currentMediaItem?.mediaId } items
.first { it.itemId.toString() == player.currentMediaItem?.mediaId }
.let { item -> .let { item ->
val itemTitle = val itemTitle =
if (item.parentIndexNumber != null && item.indexNumber != null) { if (item.parentIndexNumber != null && item.indexNumber != null) {
@ -345,24 +361,30 @@ constructor(
releasePlayer() releasePlayer()
} }
fun switchToTrack(trackType: @C.TrackType Int, index: Int) { fun switchToTrack(
trackType: @C.TrackType Int,
index: Int,
) {
// Index -1 equals disable track // Index -1 equals disable track
if (index == -1) { if (index == -1) {
player.trackSelectionParameters = player.trackSelectionParameters player.trackSelectionParameters =
player.trackSelectionParameters
.buildUpon() .buildUpon()
.clearOverridesOfType(trackType) .clearOverridesOfType(trackType)
.setTrackTypeDisabled(trackType, true) .setTrackTypeDisabled(trackType, true)
.build() .build()
} else { } else {
player.trackSelectionParameters = player.trackSelectionParameters player.trackSelectionParameters =
player.trackSelectionParameters
.buildUpon() .buildUpon()
.setOverrideForType( .setOverrideForType(
TrackSelectionOverride( TrackSelectionOverride(
player.currentTracks.groups.filter { it.type == trackType && it.isSupported }[index].mediaTrackGroup, player.currentTracks.groups
0 .filter { it.type == trackType && it.isSupported }[index]
.mediaTrackGroup,
0,
), ),
) ).setTrackTypeDisabled(trackType, false)
.setTrackTypeDisabled(trackType, false)
.build() .build()
} }
} }
@ -377,14 +399,17 @@ constructor(
Timber.d("Trickplay Resolution: ${trickplayInfo.width}") Timber.d("Trickplay Resolution: ${trickplayInfo.width}")
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
val maxIndex = ceil( val maxIndex =
trickplayInfo.thumbnailCount.toDouble() ceil(
.div(trickplayInfo.tileWidth * trickplayInfo.tileHeight) trickplayInfo.thumbnailCount
.toDouble()
.div(trickplayInfo.tileWidth * trickplayInfo.tileHeight),
).toInt() ).toInt()
val bitmaps = mutableListOf<Bitmap>() val bitmaps = mutableListOf<Bitmap>()
for (i in 0..maxIndex) { for (i in 0..maxIndex) {
jellyfinRepository.getTrickplayData( jellyfinRepository
.getTrickplayData(
item.itemId, item.itemId,
trickplayInfo.width, trickplayInfo.width,
i, i,
@ -392,12 +417,13 @@ constructor(
val fullBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) val fullBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
for (offsetY in 0..<trickplayInfo.height * trickplayInfo.tileHeight step trickplayInfo.height) { for (offsetY in 0..<trickplayInfo.height * trickplayInfo.tileHeight step trickplayInfo.height) {
for (offsetX in 0..<trickplayInfo.width * trickplayInfo.tileWidth step trickplayInfo.width) { for (offsetX in 0..<trickplayInfo.width * trickplayInfo.tileWidth step trickplayInfo.width) {
val bitmap = Bitmap.createBitmap( val bitmap =
Bitmap.createBitmap(
fullBitmap, fullBitmap,
offsetX, offsetX,
offsetY, offsetY,
trickplayInfo.width, trickplayInfo.width,
trickplayInfo.height trickplayInfo.height,
) )
bitmaps.add(bitmap) bitmaps.add(bitmap)
} }
@ -406,10 +432,11 @@ constructor(
} }
_uiState.update { _uiState.update {
it.copy( it.copy(
currentTrickplay = Trickplay( currentTrickplay =
Trickplay(
trickplayInfo.interval, trickplayInfo.interval,
bitmaps bitmaps,
) ),
) )
} }
} }
@ -420,9 +447,7 @@ constructor(
* *
* @return list of [PlayerChapter] * @return list of [PlayerChapter]
*/ */
private fun getChapters(): List<PlayerChapter>? { private fun getChapters(): List<PlayerChapter>? = uiState.value.currentChapters
return uiState.value.currentChapters
}
/** /**
* Get the index of the current chapter * Get the index of the current chapter
@ -473,8 +498,8 @@ constructor(
} }
fun isFirstChapter(): Boolean? = getChapters()?.let { getCurrentChapterIndex() == 0 } fun isFirstChapter(): Boolean? = getChapters()?.let { getCurrentChapterIndex() == 0 }
fun isLastChapter(): Boolean? =
getChapters()?.let { chapters -> getCurrentChapterIndex() == chapters.size - 1 } fun isLastChapter(): Boolean? = getChapters()?.let { chapters -> getCurrentChapterIndex() == chapters.size - 1 }
/** /**
* Seek to chapter * Seek to chapter
@ -482,20 +507,17 @@ constructor(
* @param chapterIndex the index of the chapter to seek to * @param chapterIndex the index of the chapter to seek to
* @return the [PlayerChapter] which has been sought to * @return the [PlayerChapter] which has been sought to
*/ */
private fun seekToChapter(chapterIndex: Int): PlayerChapter? { private fun seekToChapter(chapterIndex: Int): PlayerChapter? =
return getChapters()?.getOrNull(chapterIndex)?.also { chapter -> getChapters()?.getOrNull(chapterIndex)?.also { chapter ->
player.seekTo(chapter.startPosition) player.seekTo(chapter.startPosition)
} }
}
/** /**
* Seek to the next chapter * Seek to the next chapter
* *
* @return the [PlayerChapter] which has been sought to * @return the [PlayerChapter] which has been sought to
*/ */
fun seekToNextChapter(): PlayerChapter? { fun seekToNextChapter(): PlayerChapter? = getNextChapterIndex()?.let { seekToChapter(it) }
return getNextChapterIndex()?.let { seekToChapter(it) }
}
/** /**
* Seek to the previous chapter Will seek to start of current chapter if * 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 * @return the [PlayerChapter] which has been sought to
*/ */
fun seekToPreviousChapter(): PlayerChapter? { fun seekToPreviousChapter(): PlayerChapter? = getPreviousChapterIndex()?.let { seekToChapter(it) }
return getPreviousChapterIndex()?.let { seekToChapter(it) }
}
override fun onIsPlayingChanged(isPlaying: Boolean) { override fun onIsPlayingChanged(isPlaying: Boolean) {
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 - 2Mbps" -> 720
"480p - 1Mbps" -> 480
"360p - 800kbps" -> 360
"Auto" -> 1
else -> 1080
}
}
fun changeVideoQuality(quality: String) { fun changeVideoQuality(quality: String) {
val mediaId = player.currentMediaItem?.mediaId ?: return val mediaId = player.currentMediaItem?.mediaId ?: return
val currentItem = items.firstOrNull { it.itemId.toString() == mediaId } ?: return val currentItem = items.firstOrNull { it.itemId.toString() == mediaId } ?: return
val currentPosition = player.currentPosition val currentPosition = player.currentPosition
viewModelScope.launch { viewModelScope.launch {
val videoQuality = VideoQuality.fromString(quality)!!
try { try {
val transcodingResolution = getTranscodeResolutions(quality) val deviceProfile =
val (videoBitRate, audioBitRate) = jellyfinRepository.getVideoTranscodeBitRate( jellyfinRepository.buildDeviceProfile(
transcodingResolution 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 val playSessionId = playbackInfo.content.playSessionId
if (playSessionId != null) { if (playSessionId != null) {
jellyfinRepository.stopEncodingProcess(playSessionId) jellyfinRepository.stopEncodingProcess(playSessionId)
} }
val mediaSources = jellyfinRepository.getMediaSources(currentItem.itemId, true) val mediaSources = jellyfinRepository.getMediaSources(currentItem.itemId, true)
val externalSubtitles =
val externalSubtitles = currentItem.externalSubtitles.map { externalSubtitle -> currentItem.externalSubtitles.map { externalSubtitle ->
MediaItem.SubtitleConfiguration.Builder(externalSubtitle.uri) MediaItem.SubtitleConfiguration
.Builder(externalSubtitle.uri)
.setLabel(externalSubtitle.title.ifBlank { application.getString(R.string.external) }) .setLabel(externalSubtitle.title.ifBlank { application.getString(R.string.external) })
.setLanguage(externalSubtitle.language.ifBlank { "Unknown" }) .setLanguage(externalSubtitle.language.ifBlank { "Unknown" })
.setMimeType(externalSubtitle.mimeType) .setMimeType(externalSubtitle.mimeType)
.build() .build()
} }
val embeddedSubtitles = mediaSources[currentMediaItemIndex].mediaStreams val embeddedSubtitles =
mediaSources[currentMediaItemIndex]
.mediaStreams
.filter { it.type == MediaStreamType.SUBTITLE && !it.isExternal && it.path != null } .filter { it.type == MediaStreamType.SUBTITLE && !it.isExternal && it.path != null }
.map { mediaStream -> .map { mediaStream ->
val test = mediaStream.codec val test = mediaStream.codec
@ -559,8 +580,10 @@ constructor(
var deliveryUrl = mediaStream.path var deliveryUrl = mediaStream.path
Timber.d("Deliverurl: %s", deliveryUrl) Timber.d("Deliverurl: %s", deliveryUrl)
if (mediaStream.codec == "webvtt") { if (mediaStream.codec == "webvtt") {
deliveryUrl = deliveryUrl?.replace("Stream.srt", "Stream.vtt")} deliveryUrl = deliveryUrl?.replace("Stream.srt", "Stream.vtt")
MediaItem.SubtitleConfiguration.Builder(Uri.parse(deliveryUrl)) }
MediaItem.SubtitleConfiguration
.Builder(Uri.parse(deliveryUrl))
.setMimeType( .setMimeType(
when (mediaStream.codec) { when (mediaStream.codec) {
"subrip" -> MimeTypes.APPLICATION_SUBRIP "subrip" -> MimeTypes.APPLICATION_SUBRIP
@ -575,23 +598,29 @@ constructor(
"stl" -> MimeTypes.APPLICATION_TTML // EBU STL (Subtitling Data Exchange Format) "stl" -> MimeTypes.APPLICATION_TTML // EBU STL (Subtitling Data Exchange Format)
"sbv" -> MimeTypes.APPLICATION_SUBRIP // YouTube's SBV format is similar to SubRip "sbv" -> MimeTypes.APPLICATION_SUBRIP // YouTube's SBV format is similar to SubRip
else -> MimeTypes.TEXT_UNKNOWN else -> MimeTypes.TEXT_UNKNOWN
} },
) ).setLanguage(mediaStream.language.ifBlank { "Unknown" })
.setLanguage(mediaStream.language.ifBlank { "Unknown" })
.setLabel("Embedded") .setLabel("Embedded")
.build() .build()
} }.toMutableList()
.toMutableList()
val allSubtitles = embeddedSubtitles.apply { addAll(externalSubtitles) } 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) jellyfinRepository.getStreamUrl(currentItem.itemId, currentItem.mediaSourceId, playSessionId)
} else { } else {
val mediaSourceId = mediaSources[currentMediaItemIndex].id val mediaSourceId = mediaSources[currentMediaItemIndex].id
val deviceId = jellyfinRepository.getDeviceId() val deviceId = jellyfinApi.api.deviceInfo.id
val url = jellyfinRepository.getTranscodedVideoStream(currentItem.itemId, deviceId ,mediaSourceId, playSessionId!!, videoBitRate) Timber.d("deviceid = %s", deviceId)
val url =
jellyfinRepository.getTranscodedVideoStream(
currentItem.itemId,
deviceId,
mediaSourceId,
playSessionId!!,
VideoQuality.getBitrate(videoQuality),
)
val uriBuilder = url.toUri().buildUpon() val uriBuilder = url.toUri().buildUpon()
val apiKey = jellyfinApi.api.accessToken val apiKey = jellyfinApi.api.accessToken
uriBuilder.appendQueryParameter("api_key", apiKey) uriBuilder.appendQueryParameter("api_key", apiKey)
@ -599,20 +628,20 @@ constructor(
newUri.toString() newUri.toString()
} }
Timber.e("URI IS %s", url) Timber.e("URI IS %s", url)
val mediaItemBuilder = MediaItem.Builder() val mediaItemBuilder =
MediaItem
.Builder()
.setMediaId(currentItem.itemId.toString()) .setMediaId(currentItem.itemId.toString())
.setUri(url) .setUri(url)
.setSubtitleConfigurations(allSubtitles) .setSubtitleConfigurations(allSubtitles)
.setMediaMetadata( .setMediaMetadata(
MediaMetadata.Builder() MediaMetadata
.Builder()
.setTitle(currentItem.name) .setTitle(currentItem.name)
.build(), .build(),
) )
player.pause() player.pause()
player.setMediaItem(mediaItemBuilder.build()) player.setMediaItem(mediaItemBuilder.build())
player.prepare() player.prepare()
@ -620,10 +649,12 @@ constructor(
playWhenReady = true playWhenReady = true
player.play() player.play()
val originalHeight = mediaSources[currentMediaItemIndex].mediaStreams val originalHeight =
mediaSources[currentMediaItemIndex]
.mediaStreams
.filter { it.type == MediaStreamType.VIDEO } .filter { it.type == MediaStreamType.VIDEO }
.map {mediaStream -> mediaStream.height}.first() ?: 1080 .map { mediaStream -> mediaStream.height }
.first() ?: 1080
// Store the original height // Store the original height
this@PlayerActivityViewModel.originalHeight = originalHeight this@PlayerActivityViewModel.originalHeight = originalHeight
@ -635,13 +666,13 @@ constructor(
} }
} }
fun getOriginalHeight(): Int { fun getOriginalHeight(): Int = originalHeight
return originalHeight
} }
}
sealed interface PlayerEvents { sealed interface PlayerEvents {
data object NavigateBack : PlayerEvents data object NavigateBack : PlayerEvents
data class IsPlayingChanged(val isPlaying: Boolean) : PlayerEvents
data class IsPlayingChanged(
val isPlaying: Boolean,
) : PlayerEvents
} }