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,12 +112,13 @@ class PlayerActivity : BasePlayerActivity() {
configureInsets(lockedControls)
if (appPreferences.playerGestures) {
playerGestureHelper = PlayerGestureHelper(
appPreferences,
this,
binding.playerView,
getSystemService(AUDIO_SERVICE) as AudioManager,
)
playerGestureHelper =
PlayerGestureHelper(
appPreferences,
this,
binding.playerView,
getSystemService(AUDIO_SERVICE) as AudioManager,
)
}
binding.playerView.findViewById<View>(R.id.back_button).setOnClickListener {
@ -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,11 +356,12 @@ class PlayerActivity : BasePlayerActivity() {
if (appPreferences.playerTrickplay) {
val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview)
previewScrubListener = PreviewScrubListener(
imagePreview,
timeBar,
viewModel.player,
)
previewScrubListener =
PreviewScrubListener(
imagePreview,
timeBar,
viewModel.player,
)
timeBar.addListener(previewScrubListener!!)
}
@ -381,34 +392,38 @@ 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 {
Rational(
it.width.coerceAtMost((it.height * 2.39f).toInt()),
it.height.coerceAtMost((it.width * 2.39f).toInt()),
)
}
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 space = ((binding.playerView.height - (binding.playerView.width.toFloat() / aspectRatio.toFloat())) / 2).toInt()
Rect(
0,
space,
binding.playerView.width,
(binding.playerView.width.toFloat() / aspectRatio.toFloat()).toInt() + space,
)
} else {
val space = ((binding.playerView.width - (binding.playerView.height.toFloat() * aspectRatio.toFloat())) / 2).toInt()
Rect(
space,
0,
(binding.playerView.height.toFloat() * aspectRatio.toFloat()).toInt() + space,
binding.playerView.height,
)
}
val sourceRectHint =
if (displayAspectRatio < aspectRatio!!) {
val space = ((binding.playerView.height - (binding.playerView.width.toFloat() / aspectRatio.toFloat())) / 2).toInt()
Rect(
0,
space,
binding.playerView.width,
(binding.playerView.width.toFloat() / aspectRatio.toFloat()).toInt() + space,
)
} else {
val space = ((binding.playerView.width - (binding.playerView.height.toFloat() * aspectRatio.toFloat())) / 2).toInt()
Rect(
space,
0,
(binding.playerView.height.toFloat() * aspectRatio.toFloat()).toInt() + space,
binding.playerView.height,
)
}
val builder = PictureInPictureParams.Builder()
.setAspectRatio(aspectRatio)
.setSourceRectHint(sourceRectHint)
val builder =
PictureInPictureParams
.Builder()
.setAspectRatio(aspectRatio)
.setSourceRectHint(sourceRectHint)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
builder.setAutoEnterEnabled(enableAutoEnter)
@ -424,31 +439,52 @@ class PlayerActivity : BasePlayerActivity() {
try {
enterPictureInPictureMode(pipParams())
} catch (_: IllegalArgumentException) { }
} catch (_: IllegalArgumentException) {
}
}
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()
// 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
}
val qualities = when (height) {
0 -> arrayOf("Auto", "Original - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps")
in 1001..1999 -> arrayOf("Auto", "Original (1080p) - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps")
in 2000..3000 -> arrayOf("Auto", "Original (4K) - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps")
else -> arrayOf("Auto", "Original - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps")
}
MaterialAlertDialogBuilder(this)
.setTitle("Select Video Quality")
.setItems(qualities) { _, which ->
val selectedQuality = qualities[which]
viewModel.changeVideoQuality(selectedQuality)
}
.show()
.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()
}
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration,
@ -463,25 +499,29 @@ class PlayerActivity : BasePlayerActivity() {
playerGestureHelper?.updateZoomMode(false)
// Brightness mode Auto
window.attributes = window.attributes.apply {
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
}
window.attributes =
window.attributes.apply {
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
}
}
false -> {
binding.playerView.useController = true
playerGestureHelper?.updateZoomMode(wasZoom)
// Override auto brightness
window.attributes = window.attributes.apply {
screenBrightness = if (appPreferences.playerBrightnessRemember) {
appPreferences.playerBrightness
} else {
Settings.System.getInt(
contentResolver,
Settings.System.SCREEN_BRIGHTNESS,
).toFloat() / 255
window.attributes =
window.attributes.apply {
screenBrightness =
if (appPreferences.playerBrightnessRemember) {
appPreferences.playerBrightness
} else {
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,17 +51,17 @@ 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) {
item.trickplayInfo?.get(sourceId)
} else {
null
}
val trickplayInfo =
if (item is FindroidSources) {
item.trickplayInfo?.get(sourceId)
} else {
null
}
val storageLocation = context.getExternalFilesDirs(null)[storageIndex]
if (storageLocation == null || Environment.getExternalStorageState(storageLocation) != Environment.MEDIA_MOUNTED) {
return Pair(-1, UiText.StringResource(CoreR.string.storage_unavailable))
@ -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,12 +131,14 @@ class DownloaderImpl(
if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
}
val request = DownloadManager.Request(transcodingUrl)
.setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationUri(path)
val request =
DownloadManager
.Request(transcodingUrl)
.setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationUri(path)
val downloadId = downloadManager.enqueue(request)
database.setSourceDownloadId(source.id, downloadId)
return Pair(downloadId, null)
@ -139,7 +146,8 @@ class DownloaderImpl(
is FindroidEpisode -> {
database.insertShow(
jellyfinRepository.getShow(item.seriesId)
jellyfinRepository
.getShow(item.seriesId)
.toFindroidShowDto(appPreferences.currentServer!!),
)
database.insertSeason(
@ -156,12 +164,14 @@ class DownloaderImpl(
if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
}
val request = DownloadManager.Request(transcodingUrl)
.setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationUri(path)
val request =
DownloadManager
.Request(transcodingUrl)
.setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationUri(path)
val downloadId = downloadManager.enqueue(request)
database.setSourceDownloadId(source.id, downloadId)
return Pair(downloadId, null)
@ -190,12 +200,14 @@ class DownloaderImpl(
if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
}
val request = DownloadManager.Request(source.path.toUri())
.setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationUri(path)
val request =
DownloadManager
.Request(source.path.toUri())
.setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationUri(path)
val downloadId = downloadManager.enqueue(request)
database.setSourceDownloadId(source.id, downloadId)
return Pair(downloadId, null)
@ -203,7 +215,8 @@ class DownloaderImpl(
is FindroidEpisode -> {
database.insertShow(
jellyfinRepository.getShow(item.seriesId)
jellyfinRepository
.getShow(item.seriesId)
.toFindroidShowDto(appPreferences.currentServer!!),
)
database.insertSeason(
@ -219,12 +232,14 @@ class DownloaderImpl(
if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
}
val request = DownloadManager.Request(source.path.toUri())
.setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationUri(path)
val request =
DownloadManager
.Request(source.path.toUri())
.setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationUri(path)
val downloadId = downloadManager.enqueue(request)
database.setSourceDownloadId(source.id, downloadId)
return Pair(downloadId, null)
@ -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,15 +303,18 @@ class DownloaderImpl(
if (downloadId == null) {
return Pair(downloadStatus, progress)
}
val query = DownloadManager.Query()
.setFilterById(downloadId)
val query =
DownloadManager
.Query()
.setFilterById(downloadId)
val cursor = downloadManager.query(query)
if (cursor.moveToFirst()) {
downloadStatus = cursor.getInt(
cursor.getColumnIndexOrThrow(
DownloadManager.COLUMN_STATUS,
),
)
downloadStatus =
cursor.getInt(
cursor.getColumnIndexOrThrow(
DownloadManager.COLUMN_STATUS,
),
)
when (downloadStatus) {
DownloadManager.STATUS_RUNNING -> {
val totalBytes =
@ -320,25 +343,28 @@ class DownloaderImpl(
val storageLocation = context.getExternalFilesDirs(null)[storageIndex]
for (mediaStream in source.mediaStreams.filter { it.isExternal }) {
val id = UUID.randomUUID()
val streamPath = Uri.fromFile(
File(
storageLocation,
"downloads/${item.id}.${source.id}.$id.download"
val streamPath =
Uri.fromFile(
File(
storageLocation,
"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))
.setTitle(mediaStream.title)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
.setDestinationUri(streamPath)
val request =
DownloadManager
.Request(Uri.parse(mediaStream.path))
.setTitle(mediaStream.title)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
.setDestinationUri(streamPath)
val downloadId = downloadManager.enqueue(request)
database.setMediaStreamDownloadId(id, downloadId)
}
@ -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,50 +383,55 @@ class DownloaderImpl(
deliveryUrl = deliveryUrl.replace("Stream.srt", "Stream.vtt")
}
val id = UUID.randomUUID()
val streamPath = Uri.fromFile(
File(
storageLocation,
"downloads/${item.id}.${source.id}.$id.download"
val streamPath =
Uri.fromFile(
File(
storageLocation,
"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))
.setTitle(mediaStream.title)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
.setDestinationUri(streamPath)
val request =
DownloadManager
.Request(Uri.parse(deliveryUrl))
.setTitle(mediaStream.title)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
.setDestinationUri(streamPath)
val downloadId = downloadManager.enqueue(request)
database.setMediaStreamDownloadId(id, downloadId)
}
}
private suspend fun downloadTrickplayData(
itemId: UUID,
sourceId: String,
trickplayInfo: FindroidTrickplayInfo,
) {
val maxIndex = ceil(
trickplayInfo.thumbnailCount.toDouble()
.div(trickplayInfo.tileWidth * trickplayInfo.tileHeight)
).toInt()
val maxIndex =
ceil(
trickplayInfo.thumbnailCount
.toDouble()
.div(trickplayInfo.tileWidth * trickplayInfo.tileHeight),
).toInt()
val byteArrays = mutableListOf<ByteArray>()
for (i in 0..maxIndex) {
jellyfinRepository.getTrickplayData(
itemId,
trickplayInfo.width,
i,
)?.let { byteArray ->
byteArrays.add(byteArray)
}
jellyfinRepository
.getTrickplayData(
itemId,
trickplayInfo.width,
i,
)?.let { byteArray ->
byteArrays.add(byteArray)
}
}
saveTrickplayData(itemId, sourceId, trickplayInfo, byteArrays)
}
@ -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,55 +70,70 @@ class JellyfinRepositoryImpl(
private val database: ServerDatabaseDao,
private val appPreferences: AppPreferences,
) : JellyfinRepository {
override suspend fun getPublicSystemInfo(): PublicSystemInfo = withContext(Dispatchers.IO) {
jellyfinApi.systemApi.getPublicSystemInfo().content
}
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) {
jellyfinApi.userLibraryApi.getItem(itemId, jellyfinApi.userId!!).content
}
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(
itemId,
jellyfinApi.userId!!,
).content.toFindroidEpisode(this@JellyfinRepositoryImpl, database)!!
jellyfinApi.userLibraryApi
.getItem(
itemId,
jellyfinApi.userId!!,
).content
.toFindroidEpisode(this@JellyfinRepositoryImpl, database)!!
}
override suspend fun getMovie(itemId: UUID): FindroidMovie =
withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getItem(
itemId,
jellyfinApi.userId!!,
).content.toFindroidMovie(this@JellyfinRepositoryImpl, database)
jellyfinApi.userLibraryApi
.getItem(
itemId,
jellyfinApi.userId!!,
).content
.toFindroidMovie(this@JellyfinRepositoryImpl, database)
}
override suspend fun getShow(itemId: UUID): FindroidShow =
withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getItem(
itemId,
jellyfinApi.userId!!,
).content.toFindroidShow(this@JellyfinRepositoryImpl)
jellyfinApi.userLibraryApi
.getItem(
itemId,
jellyfinApi.userId!!,
).content
.toFindroidShow(this@JellyfinRepositoryImpl)
}
override suspend fun getSeason(itemId: UUID): FindroidSeason =
withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getItem(
itemId,
jellyfinApi.userId!!,
).content.toFindroidSeason(this@JellyfinRepositoryImpl)
jellyfinApi.userLibraryApi
.getItem(
itemId,
jellyfinApi.userId!!,
).content
.toFindroidSeason(this@JellyfinRepositoryImpl)
}
override suspend fun getLibraries(): List<FindroidCollection> =
withContext(Dispatchers.IO) {
jellyfinApi.itemsApi.getItems(
jellyfinApi.userId!!,
).content.items
jellyfinApi.itemsApi
.getItems(
jellyfinApi.userId!!,
).content.items
.orEmpty()
.mapNotNull { it.toFindroidCollection(this@JellyfinRepositoryImpl) }
}
@ -134,16 +148,17 @@ class JellyfinRepositoryImpl(
limit: Int?,
): List<FindroidItem> =
withContext(Dispatchers.IO) {
jellyfinApi.itemsApi.getItems(
jellyfinApi.userId!!,
parentId = parentId,
includeItemTypes = includeTypes,
recursive = recursive,
sortBy = listOf(ItemSortBy.fromName(sortBy.sortString)),
sortOrder = listOf(sortOrder),
startIndex = startIndex,
limit = limit,
).content.items
jellyfinApi.itemsApi
.getItems(
jellyfinApi.userId!!,
parentId = parentId,
includeItemTypes = includeTypes,
recursive = recursive,
sortBy = listOf(ItemSortBy.fromName(sortBy.sortString)),
sortOrder = listOf(sortOrder),
startIndex = startIndex,
limit = limit,
).content.items
.orEmpty()
.mapNotNull { it.toFindroidItem(this@JellyfinRepositoryImpl, database) }
}
@ -154,13 +169,14 @@ class JellyfinRepositoryImpl(
recursive: Boolean,
sortBy: SortBy,
sortOrder: SortOrder,
): Flow<PagingData<FindroidItem>> {
return Pager(
config = PagingConfig(
pageSize = 10,
maxSize = 100,
enablePlaceholders = false,
),
): Flow<PagingData<FindroidItem>> =
Pager(
config =
PagingConfig(
pageSize = 10,
maxSize = 100,
enablePlaceholders = false,
),
pagingSourceFactory = {
ItemsPagingSource(
this,
@ -172,87 +188,102 @@ class JellyfinRepositoryImpl(
)
},
).flow
}
override suspend fun getPersonItems(
personIds: List<UUID>,
includeTypes: List<BaseItemKind>?,
recursive: Boolean,
): List<FindroidItem> = withContext(Dispatchers.IO) {
jellyfinApi.itemsApi.getItems(
jellyfinApi.userId!!,
personIds = personIds,
includeItemTypes = includeTypes,
recursive = recursive,
).content.items
.orEmpty()
.mapNotNull {
it.toFindroidItem(this@JellyfinRepositoryImpl, database)
}
}
): List<FindroidItem> =
withContext(Dispatchers.IO) {
jellyfinApi.itemsApi
.getItems(
jellyfinApi.userId!!,
personIds = personIds,
includeItemTypes = includeTypes,
recursive = recursive,
).content.items
.orEmpty()
.mapNotNull {
it.toFindroidItem(this@JellyfinRepositoryImpl, database)
}
}
override suspend fun getFavoriteItems(): List<FindroidItem> =
withContext(Dispatchers.IO) {
jellyfinApi.itemsApi.getItems(
jellyfinApi.userId!!,
filters = listOf(ItemFilter.IS_FAVORITE),
includeItemTypes = listOf(
BaseItemKind.MOVIE,
BaseItemKind.SERIES,
BaseItemKind.EPISODE,
),
recursive = true,
).content.items
jellyfinApi.itemsApi
.getItems(
jellyfinApi.userId!!,
filters = listOf(ItemFilter.IS_FAVORITE),
includeItemTypes =
listOf(
BaseItemKind.MOVIE,
BaseItemKind.SERIES,
BaseItemKind.EPISODE,
),
recursive = true,
).content.items
.orEmpty()
.mapNotNull { it.toFindroidItem(this@JellyfinRepositoryImpl, database) }
}
override suspend fun getSearchItems(searchQuery: String): List<FindroidItem> =
withContext(Dispatchers.IO) {
jellyfinApi.itemsApi.getItems(
jellyfinApi.userId!!,
searchTerm = searchQuery,
includeItemTypes = listOf(
BaseItemKind.MOVIE,
BaseItemKind.SERIES,
BaseItemKind.EPISODE,
),
recursive = true,
).content.items
jellyfinApi.itemsApi
.getItems(
jellyfinApi.userId!!,
searchTerm = searchQuery,
includeItemTypes =
listOf(
BaseItemKind.MOVIE,
BaseItemKind.SERIES,
BaseItemKind.EPISODE,
),
recursive = true,
).content.items
.orEmpty()
.mapNotNull { it.toFindroidItem(this@JellyfinRepositoryImpl, database) }
}
override suspend fun getResumeItems(): List<FindroidItem> {
val items = withContext(Dispatchers.IO) {
jellyfinApi.itemsApi.getResumeItems(
jellyfinApi.userId!!,
limit = 12,
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE),
).content.items.orEmpty()
}
val items =
withContext(Dispatchers.IO) {
jellyfinApi.itemsApi
.getResumeItems(
jellyfinApi.userId!!,
limit = 12,
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE),
).content.items
.orEmpty()
}
return items.mapNotNull {
it.toFindroidItem(this, database)
}
}
override suspend fun getLatestMedia(parentId: UUID): List<FindroidItem> {
val items = withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getLatestMedia(
jellyfinApi.userId!!,
parentId = parentId,
limit = 16,
).content
}
val items =
withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi
.getLatestMedia(
jellyfinApi.userId!!,
parentId = parentId,
limit = 16,
).content
}
return items.mapNotNull {
it.toFindroidItem(this, database)
}
}
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,12 +293,13 @@ class JellyfinRepositoryImpl(
override suspend fun getNextUp(seriesId: UUID?): List<FindroidEpisode> =
withContext(Dispatchers.IO) {
jellyfinApi.showsApi.getNextUp(
jellyfinApi.userId!!,
limit = 24,
seriesId = seriesId,
enableResumable = false,
).content.items
jellyfinApi.showsApi
.getNextUp(
jellyfinApi.userId!!,
limit = 24,
seriesId = seriesId,
enableResumable = false,
).content.items
.orEmpty()
.mapNotNull { it.toFindroidEpisode(this@JellyfinRepositoryImpl) }
}
@ -282,14 +314,15 @@ class JellyfinRepositoryImpl(
): List<FindroidEpisode> =
withContext(Dispatchers.IO) {
if (!offline) {
jellyfinApi.showsApi.getEpisodes(
seriesId,
jellyfinApi.userId!!,
seasonId = seasonId,
fields = fields,
startItemId = startItemId,
limit = limit,
).content.items
jellyfinApi.showsApi
.getEpisodes(
seriesId,
jellyfinApi.userId!!,
seasonId = seasonId,
fields = fields,
startItemId = startItemId,
limit = limit,
).content.items
.orEmpty()
.mapNotNull { it.toFindroidEpisode(this@JellyfinRepositoryImpl, database) }
} else {
@ -297,39 +330,47 @@ 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(
itemId,
PlaybackInfoDto(
userId = jellyfinApi.userId!!,
deviceProfile = DeviceProfile(
name = "Direct play all",
maxStaticBitrate = 1_000_000_000,
maxStreamingBitrate = 1_000_000_000,
codecProfiles = emptyList(),
containerProfiles = emptyList(),
directPlayProfiles = listOf(
DirectPlayProfile(type = DlnaProfileType.VIDEO),
DirectPlayProfile(type = DlnaProfileType.AUDIO),
),
transcodingProfiles = emptyList(),
subtitleProfiles = listOf(
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
),
),
maxStreamingBitrate = 1_000_000_000,
),
).content.mediaSources.map {
it.toFindroidSource(
this@JellyfinRepositoryImpl,
jellyfinApi.mediaInfoApi
.getPostedPlaybackInfo(
itemId,
includePath,
)
},
PlaybackInfoDto(
userId = jellyfinApi.userId!!,
deviceProfile =
DeviceProfile(
name = "Direct play all",
maxStaticBitrate = 1_000_000_000,
maxStreamingBitrate = 1_000_000_000,
codecProfiles = emptyList(),
containerProfiles = emptyList(),
directPlayProfiles =
listOf(
DirectPlayProfile(type = DlnaProfileType.VIDEO),
DirectPlayProfile(type = DlnaProfileType.AUDIO),
),
transcodingProfiles = emptyList(),
subtitleProfiles =
listOf(
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
),
),
maxStreamingBitrate = 1_000_000_000,
),
).content.mediaSources
.map {
it.toFindroidSource(
this@JellyfinRepositoryImpl,
itemId,
includePath,
)
},
)
sources.addAll(
database.getSources(itemId).map { it.toFindroidSource(database) },
@ -337,26 +378,32 @@ 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) {
jellyfinApi.videosApi.getVideoStreamUrl(
itemId,
static = true,
mediaSourceId = mediaSourceId,
playSessionId = playSessionId,
deviceId = getDeviceId(),
context = EncodingContext.STATIC
)
} else {
jellyfinApi.videosApi.getVideoStreamUrl(
itemId,
static = true,
mediaSourceId = mediaSourceId,
deviceId = getDeviceId(),
)
}
val url =
if (playSessionId != null) {
jellyfinApi.videosApi.getVideoStreamUrl(
itemId,
static = true,
mediaSourceId = mediaSourceId,
playSessionId = playSessionId,
// deviceId = deviceId,
context = EncodingContext.STREAMING,
)
} else {
jellyfinApi.videosApi.getVideoStreamUrl(
itemId,
static = true,
mediaSourceId = mediaSourceId,
// deviceId = deviceId,
)
}
url
} catch (e: Exception) {
Timber.e(e)
@ -377,33 +424,36 @@ class JellyfinRepositoryImpl(
pathParameters["itemId"] = itemId
try {
val segmentToConvert = jellyfinApi.api.get<FindroidSegments>(
"/Episode/{itemId}/IntroSkipperSegments",
pathParameters,
).content
val segmentToConvert =
jellyfinApi.api
.get<FindroidSegments>(
"/Episode/{itemId}/IntroSkipperSegments",
pathParameters,
).content
val segmentConverted = mutableListOf(
segmentToConvert.intro!!.let {
FindroidSegment(
type = "intro",
skip = true,
startTime = it.startTime,
endTime = it.endTime,
showAt = it.showAt,
hideAt = it.hideAt,
)
},
segmentToConvert.credit!!.let {
FindroidSegment(
type = "credit",
skip = true,
startTime = it.startTime,
endTime = it.endTime,
showAt = it.showAt,
hideAt = it.hideAt,
)
},
)
val segmentConverted =
mutableListOf(
segmentToConvert.intro!!.let {
FindroidSegment(
type = "intro",
skip = true,
startTime = it.startTime,
endTime = it.endTime,
showAt = it.showAt,
hideAt = it.hideAt,
)
},
segmentToConvert.credit!!.let {
FindroidSegment(
type = "credit",
skip = true,
startTime = it.startTime,
endTime = it.endTime,
showAt = it.showAt,
hideAt = it.hideAt,
)
},
)
Timber.tag("SegmentInfo").d("segmentToConvert: %s", segmentToConvert)
Timber.tag("SegmentInfo").d("segmentConverted: %s", segmentConverted)
@ -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,21 +492,22 @@ class JellyfinRepositoryImpl(
withContext(Dispatchers.IO) {
jellyfinApi.sessionApi.postCapabilities(
playableMediaTypes = listOf(MediaType.VIDEO),
supportedCommands = listOf(
GeneralCommandType.VOLUME_UP,
GeneralCommandType.VOLUME_DOWN,
GeneralCommandType.TOGGLE_MUTE,
GeneralCommandType.SET_AUDIO_STREAM_INDEX,
GeneralCommandType.SET_SUBTITLE_STREAM_INDEX,
GeneralCommandType.MUTE,
GeneralCommandType.UNMUTE,
GeneralCommandType.SET_VOLUME,
GeneralCommandType.DISPLAY_MESSAGE,
GeneralCommandType.PLAY,
GeneralCommandType.PLAY_STATE,
GeneralCommandType.PLAY_NEXT,
GeneralCommandType.PLAY_MEDIA_SOURCE,
),
supportedCommands =
listOf(
GeneralCommandType.VOLUME_UP,
GeneralCommandType.VOLUME_DOWN,
GeneralCommandType.TOGGLE_MUTE,
GeneralCommandType.SET_AUDIO_STREAM_INDEX,
GeneralCommandType.SET_SUBTITLE_STREAM_INDEX,
GeneralCommandType.MUTE,
GeneralCommandType.UNMUTE,
GeneralCommandType.SET_VOLUME,
GeneralCommandType.DISPLAY_MESSAGE,
GeneralCommandType.PLAY,
GeneralCommandType.PLAY_STATE,
GeneralCommandType.PLAY_NEXT,
GeneralCommandType.PLAY_MEDIA_SOURCE,
),
supportsMediaControl = true,
)
}
@ -570,186 +629,214 @@ 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(
supportedCommands = emptyList(),
playableMediaTypes = emptyList(),
supportsMediaControl = true,
supportsPersistentIdentifier = true,
deviceProfile = DeviceProfile(
name = "AnanasUser",
id = getUserId().toString(),
maxStaticBitrate = maxBitrate,
maxStreamingBitrate = maxBitrate,
codecProfiles = emptyList(),
containerProfiles = listOf(),
directPlayProfiles = listOf(
DirectPlayProfile(type = DlnaProfileType.VIDEO),
DirectPlayProfile(type = DlnaProfileType.AUDIO),
),
transcodingProfiles = listOf(
TranscodingProfile(
container = container,
context = context,
protocol = MediaStreamProtocol.HLS,
audioCodec = "aac,ac3,eac3",
videoCodec = "hevc,h264",
type = DlnaProfileType.VIDEO,
conditions = listOf(
ProfileCondition(
condition = ProfileConditionType.LESS_THAN_EQUAL,
property = ProfileConditionValue.VIDEO_BITRATE,
value = "8000000",
isRequired = true,
)
),
copyTimestamps = true,
enableSubtitlesInManifest = true,
transcodeSeekInfo = TranscodeSeekInfo.AUTO,
override suspend fun buildDeviceProfile(
maxBitrate: Int,
container: String,
context: EncodingContext,
): DeviceProfile {
val deviceProfile =
ClientCapabilitiesDto(
supportedCommands = emptyList(),
playableMediaTypes =
listOf(
MediaType.VIDEO,
MediaType.AUDIO,
MediaType.UNKNOWN,
),
supportsMediaControl = true,
supportsPersistentIdentifier = true,
deviceProfile =
DeviceProfile(
name = "AnanasUser",
id = getUserId().toString(),
maxStaticBitrate = maxBitrate,
maxStreamingBitrate = maxBitrate,
codecProfiles = emptyList(),
containerProfiles = listOf(),
directPlayProfiles =
listOf(
DirectPlayProfile(type = DlnaProfileType.VIDEO),
DirectPlayProfile(type = DlnaProfileType.AUDIO),
),
transcodingProfiles =
listOf(
TranscodingProfile(
container = container,
context = context,
protocol = MediaStreamProtocol.HLS,
audioCodec = "aac,ac3,eac3",
videoCodec = "hevc,h264",
type = DlnaProfileType.VIDEO,
conditions =
listOf(
ProfileCondition(
condition = ProfileConditionType.LESS_THAN_EQUAL,
property = ProfileConditionValue.VIDEO_BITRATE,
value = "8000000",
isRequired = true,
),
),
copyTimestamps = true,
enableSubtitlesInManifest = true,
transcodeSeekInfo = TranscodeSeekInfo.AUTO,
),
),
subtitleProfiles =
listOf(
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("sub", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("vtt", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("ssa", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("pgs", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("dvb_teletext", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("dvd_subtitle", SubtitleDeliveryMethod.EXTERNAL),
),
),
),
subtitleProfiles = listOf(
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("sub", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("vtt", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("ssa", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("pgs", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("dvb_teletext", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("dvd_subtitle", SubtitleDeliveryMethod.EXTERNAL)
),
)
)
return deviceProfile.deviceProfile!!
}
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!!,
enableTranscoding = true,
enableDirectPlay = false,
enableDirectStream = enableDirectStream,
autoOpenLiveStream = true,
deviceProfile = deviceProfile,
allowAudioStreamCopy = true,
allowVideoStreamCopy = true,
maxStreamingBitrate = maxBitrate,
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!!,
enableTranscoding = true,
enableDirectPlay = false,
enableDirectStream = enableDirectStream,
autoOpenLiveStream = true,
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(
itemId,
static = false,
deviceId = deviceId,
mediaSourceId = mediaSourceId,
playSessionId = playSessionId,
videoBitRate = videoBitrate,
audioBitRate = 384000,
videoCodec = "hevc",
audioCodec = "aac,ac3,eac3",
container = container,
startTimeTicks = 0,
copyTimestamps = true,
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) {
jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl(
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,
enableAdaptiveBitrateStreaming = false,
audioBitRate = 384000,
maxHeight = maxHeight,
audioBitRate = 128000,
videoCodec = "hevc",
audioCodec = "aac,ac3,eac3",
audioCodec = "aac",
container = container,
startTimeTicks = 0,
copyTimestamps = true,
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
context = EncodingContext.STREAMING,
segmentContainer = "ts",
transcodeReasons = "ContainerBitrateExceedsLimit",
)
} else {
jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl(
itemId,
static = false,
deviceId = deviceId,
mediaSourceId = mediaSourceId,
playSessionId = playSessionId,
enableAdaptiveBitrateStreaming = true,
videoCodec = "hevc",
audioCodec = "aac,ac3,eac3",
startTimeTicks = 0,
copyTimestamps = true,
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
context = EncodingContext.STREAMING,
segmentContainer = "ts",
transcodeReasons = "ContainerBitrateExceedsLimit",
)
return url
}
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,
deviceId = deviceId,
mediaSourceId = mediaSourceId,
playSessionId = playSessionId,
videoBitRate = videoBitrate,
enableAdaptiveBitrateStreaming = false,
audioBitRate = 128000,
videoCodec = "hevc",
audioCodec = "aac",
startTimeTicks = 0,
copyTimestamps = true,
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
context = EncodingContext.STREAMING,
segmentContainer = "ts",
transcodeReasons = "ContainerBitrateExceedsLimit",
)
} else {
jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl(
itemId,
static = false,
deviceId = deviceId,
mediaSourceId = mediaSourceId,
playSessionId = playSessionId,
enableAdaptiveBitrateStreaming = true,
videoCodec = "hevc",
audioCodec = "aac",
startTimeTicks = 0,
copyTimestamps = true,
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
context = EncodingContext.STREAMING,
segmentContainer = "ts",
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,38 +108,61 @@ 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 {
if (seriesId != null) it.id == seriesId else true
}
val shows =
database.getShowsByServerId(appPreferences.currentServer!!).filter {
if (seriesId != null) it.id == seriesId else true
}
for (show in shows) {
val episodes = database.getEpisodesByShowId(show.id).map { it.toFindroidEpisode(database, jellyfinApi.userId!!) }
val indexOfLastPlayed = episodes.indexOfLast { it.played }
@ -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")
}
}