diff --git a/app/phone/src/main/java/com/nomadics9/ananas/PlayerActivity.kt b/app/phone/src/main/java/com/nomadics9/ananas/PlayerActivity.kt index f2cead62..3f39b6ec 100644 --- a/app/phone/src/main/java/com/nomadics9/ananas/PlayerActivity.kt +++ b/app/phone/src/main/java/com/nomadics9/ananas/PlayerActivity.kt @@ -47,6 +47,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject import com.nomadics9.ananas.core.R as CoreR +import com.nomadics9.ananas.models.VideoQuality var isControlsLocked: Boolean = false @@ -86,12 +87,10 @@ class PlayerActivity : BasePlayerActivity() { binding = ActivityPlayerBinding.inflate(layoutInflater) setContentView(binding.root) - val changeQualityButton: ImageButton = findViewById(R.id.btnChangeQuality) changeQualityButton.setOnClickListener { showQualitySelectionDialog() } - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) binding.playerView.player = viewModel.player @@ -356,12 +355,11 @@ class PlayerActivity : BasePlayerActivity() { if (appPreferences.playerTrickplay) { val imagePreview = binding.playerView.findViewById(R.id.image_preview) - previewScrubListener = - PreviewScrubListener( - imagePreview, - timeBar, - viewModel.player, - ) + previewScrubListener = PreviewScrubListener( + imagePreview, + timeBar, + viewModel.player, + ) timeBar.addListener(previewScrubListener!!) } @@ -439,50 +437,32 @@ class PlayerActivity : BasePlayerActivity() { try { enterPictureInPictureMode(pipParams()) - } catch (_: IllegalArgumentException) { - } + } catch (_: IllegalArgumentException) { } } + private var selectedIndex = 1 // Default to "Original" (index 1) private fun showQualitySelectionDialog() { - val height = viewModel.getOriginalHeight() + val originalResolution = viewModel.getOriginalResolution() ?: 0 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 = - 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 = qualityEntries.toMutableList() + val closestQuality = VideoQuality.entries + .filter { it != VideoQuality.Auto && it != VideoQuality.Original } + .minByOrNull { kotlin.math.abs(it.height*it.width - originalResolution) } + if (closestQuality != null) { + qualities[1] = "${qualities[1]} (${closestQuality})" + } MaterialAlertDialogBuilder(this) - .setTitle("Select Video Quality") - .setItems(qualities.toTypedArray()) { _, which -> - val selectedQualityEntry = qualities[which] - val selectedQualityValue = - qualityMap.entries.find { it.key.contains(selectedQualityEntry.split(" ")[0]) }?.value ?: selectedQualityEntry + .setTitle(CoreR.string.select_quality) + .setSingleChoiceItems(qualities.toTypedArray(), selectedIndex) { dialog, which -> + selectedIndex = which + val selectedQualityValue = qualityValues[which] viewModel.changeVideoQuality(selectedQualityValue) - }.show() + dialog.dismiss() + } + .show() } override fun onPictureInPictureModeChanged( diff --git a/app/phone/src/main/java/com/nomadics9/ananas/fragments/EpisodeBottomSheetFragment.kt b/app/phone/src/main/java/com/nomadics9/ananas/fragments/EpisodeBottomSheetFragment.kt index da55f8d8..9b5144d9 100644 --- a/app/phone/src/main/java/com/nomadics9/ananas/fragments/EpisodeBottomSheetFragment.kt +++ b/app/phone/src/main/java/com/nomadics9/ananas/fragments/EpisodeBottomSheetFragment.kt @@ -172,11 +172,11 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { }else if (!appPreferences.downloadQualityDefault) { createPickQualityDialog() } else { - download() + startDownload() } } - private fun download(){ + private fun startDownload(){ binding.itemActions.downloadButton.setIconResource(AndroidR.color.transparent) binding.itemActions.progressDownload.isIndeterminate = true binding.itemActions.progressDownload.isVisible = true @@ -413,8 +413,8 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { } private fun createPickQualityDialog() { - val qualityEntries = resources.getStringArray(com.nomadics9.ananas.core.R.array.quality_entries) - val qualityValues = resources.getStringArray(com.nomadics9.ananas.core.R.array.quality_values) + val qualityEntries = resources.getStringArray(CoreR.array.download_quality_entries) + val qualityValues = resources.getStringArray(CoreR.array.download_quality_values) val quality = appPreferences.downloadQuality val currentQualityIndex = qualityValues.indexOf(quality) var selectedQuality = quality @@ -428,7 +428,7 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { builder.setPositiveButton("Download") { dialog, _ -> appPreferences.downloadQuality = selectedQuality dialog.dismiss() - download() + startDownload() } builder.setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() diff --git a/app/phone/src/main/java/com/nomadics9/ananas/fragments/MovieFragment.kt b/app/phone/src/main/java/com/nomadics9/ananas/fragments/MovieFragment.kt index 2420c8e2..865b3140 100644 --- a/app/phone/src/main/java/com/nomadics9/ananas/fragments/MovieFragment.kt +++ b/app/phone/src/main/java/com/nomadics9/ananas/fragments/MovieFragment.kt @@ -209,11 +209,11 @@ class MovieFragment : Fragment() { } else if (!appPreferences.downloadQualityDefault) { createPickQualityDialog() } else { - download() + startDownload() } } - private fun download() { + private fun startDownload() { binding.itemActions.downloadButton.setIconResource(android.R.color.transparent) binding.itemActions.progressDownload.isIndeterminate = true binding.itemActions.progressDownload.isVisible = true @@ -506,8 +506,8 @@ class MovieFragment : Fragment() { } private fun createPickQualityDialog() { - val qualityEntries = resources.getStringArray(CoreR.array.quality_entries) - val qualityValues = resources.getStringArray(CoreR.array.quality_values) + val qualityEntries = resources.getStringArray(CoreR.array.download_quality_entries) + val qualityValues = resources.getStringArray(CoreR.array.download_quality_values) val quality = appPreferences.downloadQuality val currentQualityIndex = qualityValues.indexOf(quality) var selectedQuality = quality @@ -520,7 +520,7 @@ class MovieFragment : Fragment() { } builder.setPositiveButton("Download") { dialog, _ -> appPreferences.downloadQuality = selectedQuality - download() + startDownload() dialog.dismiss() } builder.setNegativeButton("Cancel") { dialog, _ -> diff --git a/app/phone/src/main/res/layout/exo_main_controls.xml b/app/phone/src/main/res/layout/exo_main_controls.xml index 55d86feb..940f0f97 100644 --- a/app/phone/src/main/res/layout/exo_main_controls.xml +++ b/app/phone/src/main/res/layout/exo_main_controls.xml @@ -73,6 +73,7 @@ android:layout_height="0dp" android:layout_weight="1" /> + diff --git a/core/src/main/java/com/nomadics9/ananas/utils/DownloaderImpl.kt b/core/src/main/java/com/nomadics9/ananas/utils/DownloaderImpl.kt index c1f90fd1..5e2846db 100644 --- a/core/src/main/java/com/nomadics9/ananas/utils/DownloaderImpl.kt +++ b/core/src/main/java/com/nomadics9/ananas/utils/DownloaderImpl.kt @@ -79,15 +79,8 @@ class DownloaderImpl( ), ) } - val qualityPreference = appPreferences.downloadQuality!! - Timber.d("Quality preference: $qualityPreference") - return if (qualityPreference != "Original") { - Timber.d("Handling Transcoding download for item: ${item.id}") - handleTranscodeDownload(item, source, storageIndex, trickplayInfo, segments, path, qualityPreference) - } else { - Timber.d("Handling original download for item: ${item.id}") - downloadOriginalItem(item, source, storageIndex, trickplayInfo, segments, path) - } + handleDownload(item, source, storageIndex, trickplayInfo, segments, path) + return Pair(-1, null) } catch (e: Exception) { try { val source = jellyfinRepository.getMediaSources(item.id).first { it.id == sourceId } @@ -108,79 +101,7 @@ class DownloaderImpl( } } - private suspend fun handleTranscodeDownload( - item: FindroidItem, - source: FindroidSource, - storageIndex: Int, - trickplayInfo: FindroidTrickplayInfo?, - segments: List?, - path: Uri, - quality: String, - ): Pair { - val transcodingUrl = getTranscodedUrl(item.id, quality) - when (item) { - is FindroidMovie -> { - database.insertMovie(item.toFindroidMovieDto(appPreferences.currentServer!!)) - database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty())) - database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId())) - downloadExternalMediaStreams(item, source, storageIndex) - downloadEmbeddedMediaStreams(item, source, storageIndex) - if (trickplayInfo != null) { - downloadTrickplayData(item.id, source.id, trickplayInfo) - } - 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 downloadId = downloadManager.enqueue(request) - database.setSourceDownloadId(source.id, downloadId) - return Pair(downloadId, null) - } - - is FindroidEpisode -> { - database.insertShow( - jellyfinRepository - .getShow(item.seriesId) - .toFindroidShowDto(appPreferences.currentServer!!), - ) - database.insertSeason( - jellyfinRepository.getSeason(item.seasonId).toFindroidSeasonDto(), - ) - database.insertEpisode(item.toFindroidEpisodeDto(appPreferences.currentServer!!)) - database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty())) - database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId())) - downloadExternalMediaStreams(item, source, storageIndex) - downloadEmbeddedMediaStreams(item, source, storageIndex) - if (trickplayInfo != null) { - downloadTrickplayData(item.id, source.id, trickplayInfo) - } - 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 downloadId = downloadManager.enqueue(request) - database.setSourceDownloadId(source.id, downloadId) - return Pair(downloadId, null) - } - } - return Pair(-1, null) - } - - private suspend fun downloadOriginalItem( + private suspend fun handleDownload( item: FindroidItem, source: FindroidSource, storageIndex: Int, @@ -200,17 +121,34 @@ 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 downloadId = downloadManager.enqueue(request) - database.setSourceDownloadId(source.id, downloadId) - return Pair(downloadId, null) + if (appPreferences.downloadQuality != VideoQuality.Original.toString()) { + downloadEmbeddedMediaStreams(item, source, storageIndex) + val transcodingUrl = + getTranscodedUrl(item.id, appPreferences.downloadQuality!!) + 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) + } else { + 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) + } } is FindroidEpisode -> { @@ -232,17 +170,34 @@ 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 downloadId = downloadManager.enqueue(request) - database.setSourceDownloadId(source.id, downloadId) - return Pair(downloadId, null) + if (appPreferences.downloadQuality != VideoQuality.Original.toString()) { + downloadEmbeddedMediaStreams(item, source, storageIndex) + val transcodingUrl = + getTranscodedUrl(item.id, appPreferences.downloadQuality!!) + 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) + } else { + 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) + } } } return Pair(-1, null) @@ -483,8 +438,8 @@ class DownloaderImpl( mediaSourceId, playSessionId, VideoQuality.getBitrate(videoQuality), - VideoQuality.getQualityInt(videoQuality), "mkv", + VideoQuality.getHeight(videoQuality), ) return downloadUrl.toUri() diff --git a/core/src/main/res/drawable/ic_monitor_play.xml b/core/src/main/res/drawable/ic_monitor_play.xml new file mode 100644 index 00000000..52913d81 --- /dev/null +++ b/core/src/main/res/drawable/ic_monitor_play.xml @@ -0,0 +1,35 @@ + + + + + + diff --git a/core/src/main/res/values-ar/strings.xml b/core/src/main/res/values-ar/strings.xml index a6b3daec..87a35e43 100644 --- a/core/src/main/res/values-ar/strings.xml +++ b/core/src/main/res/values-ar/strings.xml @@ -1,2 +1,20 @@ - \ No newline at end of file + + اصدار الخادم قديم: %1$s. الرجاء تحديث الخادم + ليس خادم جيلي فن:%1$s + اصدار الخادم غير مدعوم: %1$s. الرجاء تحديث الخادم + رد الخادم بطيء: %1$s + تسجيل دخول + اسم المستخدم او الكلمه السريه غير صحيحه + اختر الخادم + عنوان الخادم + اسم المستخدم + الكلمه السريه + اتصل + تسجيل دخول + حذف الخادم + اضافه خادم + الخادم غير موجود + عنوان الخادم فاضي + الخادم غير معرف بالid , يبدو انه هناك خلل في الخادم + \ No newline at end of file diff --git a/core/src/main/res/values/string_arrays.xml b/core/src/main/res/values/string_arrays.xml index 33721829..36b6658f 100644 --- a/core/src/main/res/values/string_arrays.xml +++ b/core/src/main/res/values/string_arrays.xml @@ -26,12 +26,12 @@ opensles - Auto - Original - 1080p - 8Mbps - 720p - 2Mbps - 480p - 1Mbps - 360p - 800Kbps + @string/quality_auto + @string/quality_original + @string/quality_1080p + @string/quality_720p + @string/quality_480p + @string/quality_360p Auto @@ -41,4 +41,22 @@ 480p 360p + + @string/quality_original + @string/quality_1080p + @string/quality_720p + @string/quality_480p + @string/quality_360p + + + Original + 1080p + 720p + 480p + 360p + + + h264 + hevc + \ No newline at end of file diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index aea6c11d..ea6d7575 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -139,6 +139,7 @@ Request timeout (ms) Connect timeout (ms) Socket timeout (ms) + Transcoding codec Users Add user Hardware decoding @@ -193,6 +194,15 @@ Unmark as played Add to favorites Remove from favorites + Default to selected download quality + Download Quality + Select Video Quality + Auto + Original + 1080p - 8Mbps + 720p - 3Mbps + 480p - 1.5Mbps + 360p - 0.8Mbps AlaskarTV Requests Trick Play in seek gesture Requires \'Seek gesture\' and \'Trick Play\' diff --git a/core/src/main/res/xml/fragment_settings_downloads.xml b/core/src/main/res/xml/fragment_settings_downloads.xml index 30c12114..295a08a9 100644 --- a/core/src/main/res/xml/fragment_settings_downloads.xml +++ b/core/src/main/res/xml/fragment_settings_downloads.xml @@ -11,15 +11,13 @@ app:title="@string/download_roaming" /> - - + app:summary="@string/quality_default" /> \ No newline at end of file diff --git a/core/src/main/res/xml/fragment_settings_network.xml b/core/src/main/res/xml/fragment_settings_network.xml index 7322a966..5e2f671c 100644 --- a/core/src/main/res/xml/fragment_settings_network.xml +++ b/core/src/main/res/xml/fragment_settings_network.xml @@ -20,4 +20,11 @@ android:defaultValue="true" app:key="pref_auto_offline" app:title="@string/turn_on_offline_mode_automatically" /> + \ No newline at end of file diff --git a/data/src/main/java/com/nomadics9/ananas/models/VideoQuality.kt b/data/src/main/java/com/nomadics9/ananas/models/VideoQuality.kt index 823335cf..7908e6f6 100644 --- a/data/src/main/java/com/nomadics9/ananas/models/VideoQuality.kt +++ b/data/src/main/java/com/nomadics9/ananas/models/VideoQuality.kt @@ -2,24 +2,30 @@ package com.nomadics9.ananas.models enum class VideoQuality( val bitrate: Int, - val qualityString: String, - val qualityInt: Int, + val height: Int, + val width: Int, + val isOriginalQuality: Boolean, ) { - PAuto(1, "Auto", 1080), - POriginal(1000000000, "Original", 1080), - P1080(8000000, "1080p", 1080), - P720(2000000, "720p", 720), - P480(1000000, "480p", 480), - P360(700000, "360p", 360), - ; + Auto(10000000, 1080, 1920, false), + Original(1000000000, 1080, 1920, true), + P3840(12000000,3840, 2160, false), // Here for future proofing and to calculate original resolution only + P1080(8000000, 1080, 1920, false), + P720(3000000, 720, 1280, false), + P480(1500000, 480, 854, false), + P360(800000, 360, 640, false); + + override fun toString(): String = when (this) { + Auto -> "Auto" + Original -> "Original" + P3840 -> "4K" + else -> "${height}p" + } companion object { - fun fromString(quality: String): VideoQuality? = entries.find { it.qualityString == quality } - + fun fromString(quality: String): VideoQuality? = entries.find { it.toString() == quality } fun getBitrate(quality: VideoQuality): Int = quality.bitrate - - fun getQualityString(quality: VideoQuality): String = quality.qualityString - - fun getQualityInt(quality: VideoQuality): Int = quality.qualityInt + fun getHeight(quality: VideoQuality): Int = quality.height + fun getWidth(quality: VideoQuality): Int = quality.width + fun getIsOriginalQuality(quality: VideoQuality): Boolean = quality.isOriginalQuality } -} +} \ No newline at end of file diff --git a/data/src/main/java/com/nomadics9/ananas/repository/JellyfinRepository.kt b/data/src/main/java/com/nomadics9/ananas/repository/JellyfinRepository.kt index 23adc8c8..911f0dc3 100644 --- a/data/src/main/java/com/nomadics9/ananas/repository/JellyfinRepository.kt +++ b/data/src/main/java/com/nomadics9/ananas/repository/JellyfinRepository.kt @@ -88,40 +88,21 @@ interface JellyfinRepository { offline: Boolean = false, ): List - suspend fun getMediaSources( - itemId: UUID, - includePath: Boolean = false, - ): List + suspend fun getMediaSources(itemId: UUID, includePath: Boolean = false): List - 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? - 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) @@ -155,8 +136,8 @@ interface JellyfinRepository { mediaSourceId: String, playSessionId: String, videoBitrate: Int, - maxHeight: Int, container: String, + maxHeight: Int, ): String suspend fun getTranscodedVideoStream( @@ -175,4 +156,6 @@ interface JellyfinRepository { ): Response suspend fun stopEncodingProcess(playSessionId: String) + + suspend fun getAccessToken(): String? } diff --git a/data/src/main/java/com/nomadics9/ananas/repository/JellyfinRepositoryImpl.kt b/data/src/main/java/com/nomadics9/ananas/repository/JellyfinRepositoryImpl.kt index 52e1343d..0baa6a34 100644 --- a/data/src/main/java/com/nomadics9/ananas/repository/JellyfinRepositoryImpl.kt +++ b/data/src/main/java/com/nomadics9/ananas/repository/JellyfinRepositoryImpl.kt @@ -384,7 +384,7 @@ class JellyfinRepositoryImpl( playSessionId: String?, ): String = withContext(Dispatchers.IO) { - // val deviceId = getDeviceId() + val deviceId = getDeviceId() try { val url = if (playSessionId != null) { @@ -393,7 +393,7 @@ class JellyfinRepositoryImpl( static = true, mediaSourceId = mediaSourceId, playSessionId = playSessionId, - // deviceId = deviceId, + deviceId = deviceId, context = EncodingContext.STREAMING, ) } else { @@ -401,7 +401,7 @@ class JellyfinRepositoryImpl( itemId, static = true, mediaSourceId = mediaSourceId, - // deviceId = deviceId, + deviceId = deviceId, ) } url @@ -752,8 +752,8 @@ class JellyfinRepositoryImpl( mediaSourceId: String, playSessionId: String, videoBitrate: Int, - maxHeight: Int, container: String, + maxHeight: Int, ): String { val url = jellyfinApi.videosApi.getVideoStreamByContainerUrl( @@ -764,9 +764,9 @@ class JellyfinRepositoryImpl( playSessionId = playSessionId, videoBitRate = videoBitrate, maxHeight = maxHeight, - audioBitRate = 128000, - videoCodec = "hevc", - audioCodec = "aac", + audioBitRate = 328000, + videoCodec = appPreferences.transcodeCodec, + audioCodec = "aac,ac3,eac3", container = container, startTimeTicks = 0, copyTimestamps = true, @@ -782,7 +782,7 @@ class JellyfinRepositoryImpl( playSessionId: String, videoBitrate: Int, ): String { - val isAuto = videoBitrate == VideoQuality.getBitrate(VideoQuality.PAuto) + val isAuto = videoBitrate == VideoQuality.getBitrate(VideoQuality.Auto) val url: String try { url = @@ -795,9 +795,9 @@ class JellyfinRepositoryImpl( playSessionId = playSessionId, videoBitRate = videoBitrate, enableAdaptiveBitrateStreaming = false, - audioBitRate = 128000, - videoCodec = "hevc", - audioCodec = "aac", + audioBitRate = 328000, + videoCodec = appPreferences.transcodeCodec, + audioCodec = "aac,ac3,eac3", startTimeTicks = 0, copyTimestamps = true, subtitleMethod = SubtitleDeliveryMethod.EXTERNAL, @@ -813,8 +813,8 @@ class JellyfinRepositoryImpl( mediaSourceId = mediaSourceId, playSessionId = playSessionId, enableAdaptiveBitrateStreaming = true, - videoCodec = "hevc", - audioCodec = "aac", + videoCodec = appPreferences.transcodeCodec, + audioCodec = "aac,ac3,eac3", startTimeTicks = 0, copyTimestamps = true, subtitleMethod = SubtitleDeliveryMethod.EXTERNAL, @@ -839,4 +839,8 @@ class JellyfinRepositoryImpl( playSessionId = playSessionId, ) } + + override suspend fun getAccessToken(): String? { + return jellyfinApi.api.accessToken + } } diff --git a/data/src/main/java/com/nomadics9/ananas/repository/JellyfinRepositoryOfflineImpl.kt b/data/src/main/java/com/nomadics9/ananas/repository/JellyfinRepositoryOfflineImpl.kt index a44f96ec..bb46f2fd 100644 --- a/data/src/main/java/com/nomadics9/ananas/repository/JellyfinRepositoryOfflineImpl.kt +++ b/data/src/main/java/com/nomadics9/ananas/repository/JellyfinRepositoryOfflineImpl.kt @@ -336,8 +336,8 @@ class JellyfinRepositoryOfflineImpl( mediaSourceId: String, playSessionId: String, videoBitrate: Int, - maxHeight: Int, container: String, + maxHeight: Int, ): String { TODO("Not yet implemented") } @@ -364,4 +364,8 @@ class JellyfinRepositoryOfflineImpl( override suspend fun stopEncodingProcess(playSessionId: String) { TODO("Not yet implemented") } + + override suspend fun getAccessToken(): String? { + TODO("Not yet implemented") + } } diff --git a/player/video/src/main/java/com/nomadics9/ananas/SubtitleUtils.kt b/player/video/src/main/java/com/nomadics9/ananas/SubtitleUtils.kt new file mode 100644 index 00000000..7e293060 --- /dev/null +++ b/player/video/src/main/java/com/nomadics9/ananas/SubtitleUtils.kt @@ -0,0 +1,20 @@ +package com.nomadics9.ananas + +import androidx.media3.common.MimeTypes + +public fun setSubtitlesMimeTypes(codec: String): String { + return when (codec) { + "subrip" -> MimeTypes.APPLICATION_SUBRIP + "webvtt" -> MimeTypes.TEXT_VTT + "ssa" -> MimeTypes.TEXT_SSA + "pgs" -> MimeTypes.APPLICATION_PGS + "ass" -> MimeTypes.TEXT_SSA + "srt" -> MimeTypes.APPLICATION_SUBRIP + "vtt" -> MimeTypes.TEXT_VTT + "ttml" -> MimeTypes.APPLICATION_TTML + "dfxp" -> MimeTypes.APPLICATION_TTML + "stl" -> MimeTypes.APPLICATION_TTML + "sbv" -> MimeTypes.APPLICATION_SUBRIP + else -> MimeTypes.TEXT_UNKNOWN + } +} diff --git a/player/video/src/main/java/com/nomadics9/ananas/viewmodels/PlayerActivityViewModel.kt b/player/video/src/main/java/com/nomadics9/ananas/viewmodels/PlayerActivityViewModel.kt index 966b378f..daad6e51 100644 --- a/player/video/src/main/java/com/nomadics9/ananas/viewmodels/PlayerActivityViewModel.kt +++ b/player/video/src/main/java/com/nomadics9/ananas/viewmodels/PlayerActivityViewModel.kt @@ -31,6 +31,7 @@ import com.nomadics9.ananas.models.VideoQuality import com.nomadics9.ananas.mpv.MPVPlayer import com.nomadics9.ananas.player.video.R import com.nomadics9.ananas.repository.JellyfinRepository +import com.nomadics9.ananas.setSubtitlesMimeTypes import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers @@ -57,12 +58,11 @@ class PlayerActivityViewModel private val application: Application, private val jellyfinRepository: JellyfinRepository, private val appPreferences: AppPreferences, - private val jellyfinApi: JellyfinApi, private val savedStateHandle: SavedStateHandle, ) : ViewModel(), Player.Listener { val player: Player - private var originalHeight: Int = 0 + private var originalResolution: Int? = null private val _uiState = MutableStateFlow( @@ -191,6 +191,21 @@ class PlayerActivityViewModel ).setSubtitleConfigurations(mediaSubtitles) .build() mediaItems.add(mediaItem) + + player.addListener(object : Player.Listener { + override fun onPlaybackStateChanged(state: Int) { + if (state == Player.STATE_READY) { + val videoSize = player.videoSize + val initialHeight = videoSize.height + val initialWidth = videoSize.width + + originalResolution = initialHeight * initialWidth + Timber.d("Initial video size: $initialWidth x $initialHeight") + + player.removeListener(this) + } + } + }) } } catch (e: Exception) { Timber.e(e) @@ -532,143 +547,105 @@ class PlayerActivityViewModel eventsChannel.trySend(PlayerEvents.IsPlayingChanged(isPlaying)) } - fun changeVideoQuality(quality: String) { - val mediaId = player.currentMediaItem?.mediaId ?: return - val currentItem = items.firstOrNull { it.itemId.toString() == mediaId } ?: return - val currentPosition = player.currentPosition + fun changeVideoQuality(quality: String) { + val mediaId = player.currentMediaItem?.mediaId ?: return + val currentItem = items.firstOrNull { it.itemId.toString() == mediaId } ?: return + val currentPosition = player.currentPosition - viewModelScope.launch { + viewModelScope.launch { + try { val videoQuality = VideoQuality.fromString(quality)!! - try { - val deviceProfile = - jellyfinRepository.buildDeviceProfile( - VideoQuality.getBitrate(videoQuality), - "ts", - EncodingContext.STREAMING, - ) - - val playbackInfo = - jellyfinRepository.getPostedPlaybackInfo( - currentItem.itemId, - true, - deviceProfile, - VideoQuality.getBitrate(videoQuality), - ) - val playSessionId = playbackInfo.content.playSessionId - if (playSessionId != null) { - jellyfinRepository.stopEncodingProcess(playSessionId) - } - val mediaSources = jellyfinRepository.getMediaSources(currentItem.itemId, true) - - val externalSubtitles = - currentItem.externalSubtitles.map { externalSubtitle -> - MediaItem.SubtitleConfiguration - .Builder(externalSubtitle.uri) - .setLabel(externalSubtitle.title.ifBlank { application.getString(R.string.external) }) - .setLanguage(externalSubtitle.language.ifBlank { "Unknown" }) - .setMimeType(externalSubtitle.mimeType) - .build() - } - - val embeddedSubtitles = - mediaSources[currentMediaItemIndex] - .mediaStreams - .filter { it.type == MediaStreamType.SUBTITLE && !it.isExternal && it.path != null } - .map { mediaStream -> - val test = mediaStream.codec - Timber.d("Deliver: %s", test) - var deliveryUrl = mediaStream.path - Timber.d("Deliverurl: %s", deliveryUrl) - if (mediaStream.codec == "webvtt") { - deliveryUrl = deliveryUrl?.replace("Stream.srt", "Stream.vtt") - } - MediaItem.SubtitleConfiguration - .Builder(Uri.parse(deliveryUrl)) - .setMimeType( - when (mediaStream.codec) { - "subrip" -> MimeTypes.APPLICATION_SUBRIP - "webvtt" -> MimeTypes.TEXT_VTT - "ssa" -> MimeTypes.TEXT_SSA - "pgs" -> MimeTypes.APPLICATION_PGS - "ass" -> MimeTypes.TEXT_SSA // ASS is a subtitle format that is essentially an extension of SSA - "srt" -> MimeTypes.APPLICATION_SUBRIP // SRT is another common name for SubRip - "vtt" -> MimeTypes.TEXT_VTT // VTT is a common extension for WebVTT - "ttml" -> MimeTypes.APPLICATION_TTML // TTML (Timed Text Markup Language) - "dfxp" -> MimeTypes.APPLICATION_TTML // DFXP is a profile of TTML - "stl" -> MimeTypes.APPLICATION_TTML // EBU STL (Subtitling Data Exchange Format) - "sbv" -> MimeTypes.APPLICATION_SUBRIP // YouTube's SBV format is similar to SubRip - else -> MimeTypes.TEXT_UNKNOWN - }, - ).setLanguage(mediaStream.language.ifBlank { "Unknown" }) - .setLabel("Embedded") - .build() - }.toMutableList() - - val allSubtitles = embeddedSubtitles.apply { addAll(externalSubtitles) } - - val url = - if (VideoQuality.getQualityString(videoQuality) == "Original") { - jellyfinRepository.getStreamUrl(currentItem.itemId, currentItem.mediaSourceId, playSessionId) - } else { - val mediaSourceId = mediaSources[currentMediaItemIndex].id - val deviceId = jellyfinApi.api.deviceInfo.id - Timber.d("deviceid = %s", deviceId) - val url = - jellyfinRepository.getTranscodedVideoStream( - currentItem.itemId, - deviceId, - mediaSourceId, - playSessionId!!, - VideoQuality.getBitrate(videoQuality), - ) - val uriBuilder = url.toUri().buildUpon() - val apiKey = jellyfinApi.api.accessToken - uriBuilder.appendQueryParameter("api_key", apiKey) - val newUri = uriBuilder.build() - newUri.toString() - } - - Timber.e("URI IS %s", url) - val mediaItemBuilder = - MediaItem - .Builder() - .setMediaId(currentItem.itemId.toString()) - .setUri(url) - .setSubtitleConfigurations(allSubtitles) - .setMediaMetadata( - MediaMetadata - .Builder() - .setTitle(currentItem.name) - .build(), - ) - - player.pause() - player.setMediaItem(mediaItemBuilder.build()) - player.prepare() - player.seekTo(currentPosition) - playWhenReady = true - player.play() - - val originalHeight = - mediaSources[currentMediaItemIndex] - .mediaStreams - .filter { it.type == MediaStreamType.VIDEO } - .map { mediaStream -> mediaStream.height } - .first() ?: 1080 - - // Store the original height - this@PlayerActivityViewModel.originalHeight = originalHeight - - // isQualityChangeInProgress = true - } catch (e: Exception) { - Timber.e(e) + val deviceProfile = jellyfinRepository.buildDeviceProfile(VideoQuality.getBitrate(videoQuality), "mkv", EncodingContext.STREAMING) + val playbackInfo = jellyfinRepository.getPostedPlaybackInfo(currentItem.itemId,true,deviceProfile,VideoQuality.getBitrate(videoQuality)) + val playSessionId = playbackInfo.content.playSessionId + if (playSessionId != null) { + jellyfinRepository.stopEncodingProcess(playSessionId) } + val mediaSources = jellyfinRepository.getMediaSources(currentItem.itemId, true) + + // TODO: can maybe tidy the sub stuff up + val externalSubtitles = currentItem.externalSubtitles.map { externalSubtitle -> + MediaItem.SubtitleConfiguration.Builder(externalSubtitle.uri) + .setLabel(externalSubtitle.title.ifBlank { application.getString(R.string.external) }) + .setLanguage(externalSubtitle.language.ifBlank { "Unknown" }) + .setMimeType(externalSubtitle.mimeType) + .build() + } + + val embeddedSubtitles = mediaSources[currentMediaItemIndex].mediaStreams + .filter { it.type == MediaStreamType.SUBTITLE && !it.isExternal && it.path != null } + .map { mediaStream -> + var deliveryUrl = mediaStream.path + Timber.d("Deliverurl: %s", deliveryUrl) +// Not sure if still needed + if (mediaStream.codec == "webvtt") { + deliveryUrl = deliveryUrl?.replace("Stream.srt", "Stream.vtt")} + MediaItem.SubtitleConfiguration.Builder(Uri.parse(deliveryUrl)) + .setMimeType(setSubtitlesMimeTypes(mediaStream.codec)) + .setLanguage(mediaStream.language.ifBlank { "Unknown" }) + .setLabel("Embedded") + .build() + } + .toMutableList() + + + val allSubtitles = + if (VideoQuality.getIsOriginalQuality(videoQuality)) { + externalSubtitles + }else { + embeddedSubtitles.apply { addAll(externalSubtitles) } + } + + val url = if (VideoQuality.getIsOriginalQuality(videoQuality)){ + jellyfinRepository.getStreamUrl(currentItem.itemId, currentItem.mediaSourceId, playSessionId) + } else { + val mediaSourceId = mediaSources[currentMediaItemIndex].id + val deviceId = jellyfinRepository.getDeviceId() + val url = jellyfinRepository.getTranscodedVideoStream(currentItem.itemId, deviceId ,mediaSourceId, playSessionId!!, VideoQuality.getBitrate(videoQuality)) + val uriBuilder = url.toUri().buildUpon() + val apiKey = jellyfinRepository.getAccessToken() + uriBuilder.appendQueryParameter("api_key",apiKey ) + val newUri = uriBuilder.build() + newUri.toString() + } + + + + Timber.e("URI IS %s", url) + val mediaItemBuilder = MediaItem.Builder() + .setMediaId(currentItem.itemId.toString()) + .setUri(url) + .setSubtitleConfigurations(allSubtitles) + .setMediaMetadata( + MediaMetadata.Builder() + .setTitle(currentItem.name) + .build(), + ) + + + player.pause() + player.setMediaItem(mediaItemBuilder.build()) + player.prepare() + player.seekTo(currentPosition) + playWhenReady = true + player.play() + + + + + //isQualityChangeInProgress = true + } catch (e: Exception) { + Timber.e(e) } } - - fun getOriginalHeight(): Int = originalHeight } + fun getOriginalResolution(): Int? { + return originalResolution + } +} + + sealed interface PlayerEvents { data object NavigateBack : PlayerEvents diff --git a/player/video/src/main/java/com/nomadics9/ananas/viewmodels/PlayerViewModel.kt b/player/video/src/main/java/com/nomadics9/ananas/viewmodels/PlayerViewModel.kt index 6772c421..2dc78eeb 100644 --- a/player/video/src/main/java/com/nomadics9/ananas/viewmodels/PlayerViewModel.kt +++ b/player/video/src/main/java/com/nomadics9/ananas/viewmodels/PlayerViewModel.kt @@ -18,6 +18,7 @@ import com.nomadics9.ananas.models.PlayerChapter import com.nomadics9.ananas.models.PlayerItem import com.nomadics9.ananas.models.TrickplayInfo import com.nomadics9.ananas.repository.JellyfinRepository +import com.nomadics9.ananas.setSubtitlesMimeTypes import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch @@ -136,6 +137,7 @@ class PlayerViewModel @Inject internal constructor( } else { mediaSources[mediaSourceIndex] } + // Embedded Sub externally for offline playback val externalSubtitles = if (mediaSource.type.toString() == "LOCAL" ) { mediaSource.mediaStreams .filter { mediaStream -> @@ -146,13 +148,7 @@ class PlayerViewModel @Inject internal constructor( mediaStream.title, mediaStream.language, Uri.parse(mediaStream.path!!), - when (mediaStream.codec) { - "subrip" -> MimeTypes.APPLICATION_SUBRIP - "webvtt" -> MimeTypes.APPLICATION_SUBRIP - "pgs" -> MimeTypes.APPLICATION_PGS - "ass" -> MimeTypes.TEXT_SSA - else -> MimeTypes.TEXT_UNKNOWN - }, + setSubtitlesMimeTypes(mediaStream.codec), ) } }else { @@ -165,13 +161,7 @@ class PlayerViewModel @Inject internal constructor( mediaStream.title, mediaStream.language, Uri.parse(mediaStream.path!!), - when (mediaStream.codec) { - "subrip" -> MimeTypes.APPLICATION_SUBRIP - "webvtt" -> MimeTypes.APPLICATION_SUBRIP - "pgs" -> MimeTypes.APPLICATION_PGS - "ass" -> MimeTypes.TEXT_SSA - else -> MimeTypes.TEXT_UNKNOWN - }, + setSubtitlesMimeTypes(mediaStream.codec) ) } } diff --git a/preferences/src/main/java/com/nomadics9/ananas/AppPreferences.kt b/preferences/src/main/java/com/nomadics9/ananas/AppPreferences.kt index 7fabbfa9..a118b5ed 100644 --- a/preferences/src/main/java/com/nomadics9/ananas/AppPreferences.kt +++ b/preferences/src/main/java/com/nomadics9/ananas/AppPreferences.kt @@ -121,6 +121,11 @@ constructor( Constants.NETWORK_DEFAULT_SOCKET_TIMEOUT.toString(), )!!.toLongOrNull() ?: Constants.NETWORK_DEFAULT_SOCKET_TIMEOUT + val transcodeCodec get() = sharedPreferences.getString( + Constants.PREF_NETWORK_CODEC, + Constants.NETWORK_DEFAULT_CODEC, + ) + // Cache val imageCache get() = sharedPreferences.getBoolean( Constants.PREF_IMAGE_CACHE, diff --git a/preferences/src/main/java/com/nomadics9/ananas/Constants.kt b/preferences/src/main/java/com/nomadics9/ananas/Constants.kt index 24b93d2c..b93553e8 100644 --- a/preferences/src/main/java/com/nomadics9/ananas/Constants.kt +++ b/preferences/src/main/java/com/nomadics9/ananas/Constants.kt @@ -43,6 +43,7 @@ object Constants { const val PREF_NETWORK_REQUEST_TIMEOUT = "pref_network_request_timeout" const val PREF_NETWORK_CONNECT_TIMEOUT = "pref_network_connect_timeout" const val PREF_NETWORK_SOCKET_TIMEOUT = "pref_network_socket_timeout" + const val PREF_NETWORK_CODEC = "pref_network_codec" const val PREF_DOWNLOADS_MOBILE_DATA = "pref_downloads_mobile_data" const val PREF_DOWNLOADS_ROAMING = "pref_downloads_roaming" const val PREF_DOWNLOADS_QUALITY = "pref_downloads_quality" @@ -63,6 +64,7 @@ object Constants { const val NETWORK_DEFAULT_REQUEST_TIMEOUT = 30_000L const val NETWORK_DEFAULT_CONNECT_TIMEOUT = 6_000L const val NETWORK_DEFAULT_SOCKET_TIMEOUT = 10_000L + const val NETWORK_DEFAULT_CODEC = "h264" // sorting // This values must correspond to a SortString from [SortBy]