From ccc6788a02c9151917d500a06d17f4df67f8669e Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Fri, 19 Jul 2024 02:20:55 +0300 Subject: [PATCH 01/13] feat: Transcoding stream in player selection /code: prep repo for next commit transcoding downloads --- .../dev/jdtech/jellyfin/PlayerActivity.kt | 22 +++ .../src/main/res/layout/exo_main_controls.xml | 18 ++ core/src/main/res/drawable/ic_quality.xml | 35 ++++ .../jellyfin/repository/JellyfinRepository.kt | 20 +- .../repository/JellyfinRepositoryImpl.kt | 176 +++++++++++++++++- .../JellyfinRepositoryOfflineImpl.kt | 56 +++++- .../viewmodels/PlayerActivityViewModel.kt | 141 ++++++++++++++ .../jellyfin/viewmodels/PlayerViewModel.kt | 25 ++- 8 files changed, 489 insertions(+), 4 deletions(-) create mode 100644 core/src/main/res/drawable/ic_quality.xml diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index e21c79b3d1..891170e8b1 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -33,6 +33,7 @@ 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 dev.jdtech.jellyfin.databinding.ActivityPlayerBinding import dev.jdtech.jellyfin.dialogs.SpeedSelectionDialogFragment @@ -82,6 +83,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 @@ -342,6 +347,23 @@ class PlayerActivity : BasePlayerActivity() { } catch (_: IllegalArgumentException) { } } + private fun showQualitySelectionDialog() { + val height = viewModel.getOriginalHeight() // TODO: rewrite getting height stuff I don't like that its only update after changing quality + 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() + } + override fun onPictureInPictureModeChanged( isInPictureInPictureMode: Boolean, newConfig: Configuration, 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 00431e7035..b136be357f 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,24 @@ android:layout_height="0dp" android:layout_weight="1" /> + + + + + + + + + + diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt index e2f117a392..2b4380c033 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt @@ -11,9 +11,13 @@ import dev.jdtech.jellyfin.models.FindroidSource import dev.jdtech.jellyfin.models.Intro import dev.jdtech.jellyfin.models.SortBy 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.DeviceProfile +import org.jellyfin.sdk.model.api.EncodingContext import org.jellyfin.sdk.model.api.ItemFields +import org.jellyfin.sdk.model.api.PlaybackInfoResponse import org.jellyfin.sdk.model.api.PublicSystemInfo import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.api.UserConfiguration @@ -81,7 +85,7 @@ interface JellyfinRepository { suspend fun getMediaSources(itemId: UUID, includePath: Boolean = false): List - suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String): String + suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String, playSessionId: String? = null): String suspend fun getIntroTimestamps(itemId: UUID): Intro? @@ -112,4 +116,18 @@ interface JellyfinRepository { suspend fun getDownloads(): List fun getUserId(): UUID + + suspend fun getDeviceId(): String + + suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair + + suspend fun buildDeviceProfile(maxBitrate: Int, container: String, context: EncodingContext): DeviceProfile + + 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 getPostedPlaybackInfo(itemId: UUID, enableDirectStream: Boolean, deviceProfile: DeviceProfile ,maxBitrate: Int): Response + + suspend fun stopEncodingProcess(playSessionId: String) } diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index 995619d2bf..ae178c7b55 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -28,23 +28,35 @@ import io.ktor.util.toByteArray import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext +import org.jellyfin.sdk.api.client.Response +import org.jellyfin.sdk.api.client.extensions.dynamicHlsApi import org.jellyfin.sdk.api.client.extensions.get +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.DeviceOptionsDto import org.jellyfin.sdk.model.api.DeviceProfile import org.jellyfin.sdk.model.api.DirectPlayProfile import org.jellyfin.sdk.model.api.DlnaProfileType +import org.jellyfin.sdk.model.api.EncodingContext import org.jellyfin.sdk.model.api.GeneralCommandType import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.ItemFilter import org.jellyfin.sdk.model.api.ItemSortBy +import org.jellyfin.sdk.model.api.MediaStreamProtocol import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.api.PlaybackInfoDto +import org.jellyfin.sdk.model.api.PlaybackInfoResponse +import org.jellyfin.sdk.model.api.ProfileCondition +import org.jellyfin.sdk.model.api.ProfileConditionType +import org.jellyfin.sdk.model.api.ProfileConditionValue 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.TranscodeSeekInfo +import org.jellyfin.sdk.model.api.TranscodingProfile import org.jellyfin.sdk.model.api.UserConfiguration import timber.log.Timber import java.io.File @@ -322,13 +334,14 @@ class JellyfinRepositoryImpl( sources } - override suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String): String = + override suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String, playSessionId: String?): String = withContext(Dispatchers.IO) { try { jellyfinApi.videosApi.getVideoStreamUrl( itemId, static = true, mediaSourceId = mediaSourceId, + playSessionId = playSessionId ) } catch (e: Exception) { Timber.e(e) @@ -536,4 +549,165 @@ class JellyfinRepositoryImpl( override fun getUserId(): UUID { return jellyfinApi.userId!! } + + + override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair { + return when (transcodeResolution) { + 1080 -> 8000000 to 384000 // Adjusted for personal can be other values + 720 -> 2000000 to 384000 // 720p + 480 -> 1000000 to 384000 // 480p + 360 -> 800000 to 128000 // 360p + else -> 12000000 to 384000 // its adaptive but setting max here + } + } + + 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, + ), + ), + 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 { + 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, + ) + ) + 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( + itemId, + static = false, + deviceId = deviceId, + mediaSourceId = mediaSourceId, + playSessionId = playSessionId, + videoBitRate = videoBitrate, + enableAdaptiveBitrateStreaming = false, + audioBitRate = 384000, //could also be passed with audioBitrate but i preferred not as its not much data anyways + videoCodec = "hevc,h264", + audioCodec = "aac,ac3,eac3", + 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 getDeviceId(): String { + val devices = jellyfinApi.devicesApi.getDevices(getUserId()) + return devices.content.items?.firstOrNull()?.id!! + } + + override suspend fun stopEncodingProcess(playSessionId: String) { + val deviceId = getDeviceId() + jellyfinApi.api.hlsSegmentApi.stopEncodingProcess( + deviceId = deviceId, + playSessionId = playSessionId + ) + } + } + + diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt index 0a78ec47d4..6901c09d11 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt @@ -23,9 +23,13 @@ import dev.jdtech.jellyfin.models.toIntro import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow 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.DeviceProfile +import org.jellyfin.sdk.model.api.EncodingContext import org.jellyfin.sdk.model.api.ItemFields +import org.jellyfin.sdk.model.api.PlaybackInfoResponse import org.jellyfin.sdk.model.api.PublicSystemInfo import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.api.UserConfiguration @@ -173,7 +177,7 @@ class JellyfinRepositoryOfflineImpl( database.getSources(itemId).map { it.toFindroidSource(database) } } - override suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String): String { + override suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String, playSessionId: String?): String { TODO("Not yet implemented") } @@ -285,4 +289,54 @@ class JellyfinRepositoryOfflineImpl( override fun getUserId(): UUID { return jellyfinApi.userId!! } + + override suspend fun getDeviceId(): String { + TODO("Not yet implemented") + } + + override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair { + TODO("Not yet implemented") + } + + override suspend fun buildDeviceProfile( + maxBitrate: Int, + container: String, + context: EncodingContext + ): DeviceProfile { + TODO("Not yet implemented") + } + + override suspend fun getVideoStreambyContainerUrl( + itemId: UUID, + deviceId: String, + mediaSourceId: String, + playSessionId: String, + videoBitrate: Int, + container: String + ): String { + TODO("Not yet implemented") + } + + override suspend fun getTranscodedVideoStream( + itemId: UUID, + deviceId: String, + mediaSourceId: String, + playSessionId: String, + videoBitrate: Int + ): String { + TODO("Not yet implemented") + } + + override suspend fun getPostedPlaybackInfo( + itemId: UUID, + enableDirectStream: Boolean, + deviceProfile: DeviceProfile, + maxBitrate: Int + ): Response { + TODO("Not yet implemented") + } + + override suspend fun stopEncodingProcess(playSessionId: String) { + TODO("Not yet implemented") + } } diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt index 37b1ed425c..e804df6eb9 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -3,8 +3,10 @@ package dev.jdtech.jellyfin.viewmodels import android.app.Application import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.net.Uri import android.os.Handler import android.os.Looper +import androidx.core.net.toUri import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -12,6 +14,7 @@ import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata +import androidx.media3.common.MimeTypes import androidx.media3.common.Player import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.TrackSelectionParameters @@ -20,6 +23,7 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import dagger.hilt.android.lifecycle.HiltViewModel import dev.jdtech.jellyfin.AppPreferences +import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.models.Intro import dev.jdtech.jellyfin.models.PlayerChapter import dev.jdtech.jellyfin.models.PlayerItem @@ -38,6 +42,8 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.jellyfin.sdk.model.api.EncodingContext +import org.jellyfin.sdk.model.api.MediaStreamType import timber.log.Timber import java.util.UUID import javax.inject.Inject @@ -49,10 +55,12 @@ class PlayerActivityViewModel constructor( private val application: Application, private val jellyfinRepository: JellyfinRepository, + private val jellyfinApi: JellyfinApi, private val appPreferences: AppPreferences, private val savedStateHandle: SavedStateHandle, ) : ViewModel(), Player.Listener { val player: Player + private var originalHeight: Int = 0 private val _uiState = MutableStateFlow( UiState( @@ -455,8 +463,141 @@ constructor( super.onIsPlayingChanged(isPlaying) eventsChannel.trySend(PlayerEvents.IsPlayingChanged(isPlaying)) } + + private fun getTranscodeResolutions(preferredQuality: String): Int { + return when (preferredQuality) { + "1080p" -> 1080 // TODO: 1080p this logic is based on 1080p being original + "720p - 2Mbps" -> 720 + "480p - 1Mbps" -> 480 + "360p - 800kbps" -> 360 + "Auto" -> 1 + else -> 1080 //default to Original + } + } + + 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 { + try { + val transcodingResolution = getTranscodeResolutions(quality) + val (videoBitRate, audioBitRate) = jellyfinRepository.getVideoTranscodeBitRate( + transcodingResolution + ) + val deviceProfile = jellyfinRepository.buildDeviceProfile(videoBitRate, "mkv", EncodingContext.STREAMING) + val playbackInfo = jellyfinRepository.getPostedPlaybackInfo(currentItem.itemId,true,deviceProfile,videoBitRate) + val playSessionId = playbackInfo.content.playSessionId + if (playSessionId != null) { + jellyfinRepository.stopEncodingProcess(playSessionId) + } + val mediaSources = jellyfinRepository.getMediaSources(currentItem.itemId, true) + + // 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 -> + 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 + "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 + } + ) + .setLanguage(mediaStream.language.ifBlank { "Unknown" }) + .setLabel("Embedded") + .build() + } + .toMutableList() + + + val allSubtitles = + if (transcodingResolution == 1080) { + externalSubtitles + }else { + embeddedSubtitles.apply { addAll(externalSubtitles) } + } + + val url = if (transcodingResolution == 1080){ + jellyfinRepository.getStreamUrl(currentItem.itemId, currentItem.mediaSourceId, playSessionId) + } else { + val mediaSourceId = mediaSources[currentMediaItemIndex].id + val deviceId = jellyfinRepository.getDeviceId() + val url = jellyfinRepository.getTranscodedVideoStream(currentItem.itemId, deviceId ,mediaSourceId, playSessionId!!, videoBitRate) + val uriBuilder = url.toUri().buildUpon() + val apiKey = jellyfinApi.api.accessToken // TODO: add in repo + 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) + } + } + } + + fun getOriginalHeight(): Int { + return originalHeight + } } + sealed interface PlayerEvents { data object NavigateBack : PlayerEvents data class IsPlayingChanged(val isPlaying: Boolean) : PlayerEvents diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt index 9b3f76ff79..50a0fc1c8f 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt @@ -136,7 +136,28 @@ class PlayerViewModel @Inject internal constructor( } else { mediaSources[mediaSourceIndex] } - val externalSubtitles = mediaSource.mediaStreams + // Embedded Sub externally for offline prep next commit + val externalSubtitles = if (mediaSource.type.toString() == "LOCAL" ) { + mediaSource.mediaStreams + .filter { mediaStream -> + mediaStream.type == MediaStreamType.SUBTITLE && !mediaStream.path.isNullOrBlank() + } + .map { mediaStream -> + ExternalSubtitle( + 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 + }, + ) + } + }else { + mediaSource.mediaStreams .filter { mediaStream -> mediaStream.isExternal && mediaStream.type == MediaStreamType.SUBTITLE && !mediaStream.path.isNullOrBlank() } @@ -148,11 +169,13 @@ class PlayerViewModel @Inject internal constructor( when (mediaStream.codec) { "subrip" -> MimeTypes.APPLICATION_SUBRIP "webvtt" -> MimeTypes.APPLICATION_SUBRIP + "pgs" -> MimeTypes.APPLICATION_PGS "ass" -> MimeTypes.TEXT_SSA else -> MimeTypes.TEXT_UNKNOWN }, ) } + } val trickplayInfo = when (this) { is FindroidSources -> { this.trickplayInfo?.get(mediaSource.id)?.let { From 062781a43dabc0eea9fc724dcaa384d3b37990df Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Fri, 19 Jul 2024 03:44:43 +0300 Subject: [PATCH 02/13] feat: Download transcoded media --- .../fragments/EpisodeBottomSheetFragment.kt | 153 +++++++++++------- .../jellyfin/fragments/MovieFragment.kt | 153 +++++++++++------- .../jdtech/jellyfin/utils/DownloaderImpl.kt | 148 ++++++++++++++--- core/src/main/res/values/string_arrays.xml | 12 ++ .../res/xml/fragment_settings_downloads.xml | 12 ++ .../dev/jdtech/jellyfin/AppPreferences.kt | 13 ++ .../java/dev/jdtech/jellyfin/Constants.kt | 2 + 7 files changed, 357 insertions(+), 136 deletions(-) diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt index 17c61caf71..925f70f3b8 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt @@ -157,70 +157,80 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { } binding.itemActions.downloadButton.setOnClickListener { - if (viewModel.item.isDownloaded()) { - viewModel.deleteEpisode() - binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) - } else if (viewModel.item.isDownloading()) { - createCancelDialog() - } else { - binding.itemActions.downloadButton.setIconResource(AndroidR.color.transparent) - binding.itemActions.progressDownload.isIndeterminate = true - binding.itemActions.progressDownload.isVisible = true - if (requireContext().getExternalFilesDirs(null).filterNotNull().size > 1) { - val storageDialog = getStorageSelectionDialog( - requireContext(), - onItemSelected = { storageIndex -> - if (viewModel.item.sources.size > 1) { - val dialog = getVideoVersionDialog( - requireContext(), - viewModel.item, - onItemSelected = { sourceIndex -> - createDownloadPreparingDialog() - viewModel.download(sourceIndex, storageIndex) - }, - onCancel = { - binding.itemActions.progressDownload.isVisible = false - binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) - }, - ) - dialog.show() - return@getStorageSelectionDialog - } - createDownloadPreparingDialog() - viewModel.download(storageIndex = storageIndex) - }, - onCancel = { - binding.itemActions.progressDownload.isVisible = false - binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) - }, - ) - storageDialog.show() - return@setOnClickListener - } - if (viewModel.item.sources.size > 1) { - val dialog = getVideoVersionDialog( - requireContext(), - viewModel.item, - onItemSelected = { sourceIndex -> - createDownloadPreparingDialog() - viewModel.download(sourceIndex) - }, - onCancel = { - binding.itemActions.progressDownload.isVisible = false - binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) - }, - ) - dialog.show() - return@setOnClickListener - } - createDownloadPreparingDialog() - viewModel.download() - } + handleDownload() } return binding.root } + private fun handleDownload() { + if (viewModel.item.isDownloaded()) { + viewModel.deleteEpisode() + binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) + } else if (viewModel.item.isDownloading()) { + createCancelDialog() + }else if (!appPreferences.downloadQualityDefault) { + createPickQualityDialog() + } else { + download() + } + } + + private fun download(){ + binding.itemActions.downloadButton.setIconResource(AndroidR.color.transparent) + binding.itemActions.progressDownload.isIndeterminate = true + binding.itemActions.progressDownload.isVisible = true + if (requireContext().getExternalFilesDirs(null).filterNotNull().size > 1) { + val storageDialog = getStorageSelectionDialog( + requireContext(), + onItemSelected = { storageIndex -> + if (viewModel.item.sources.size > 1) { + val dialog = getVideoVersionDialog( + requireContext(), + viewModel.item, + onItemSelected = { sourceIndex -> + createDownloadPreparingDialog() + viewModel.download(sourceIndex, storageIndex) + }, + onCancel = { + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) + }, + ) + dialog.show() + return@getStorageSelectionDialog + } + createDownloadPreparingDialog() + viewModel.download(storageIndex = storageIndex) + }, + onCancel = { + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) + }, + ) + storageDialog.show() + return + } + if (viewModel.item.sources.size > 1) { + val dialog = getVideoVersionDialog( + requireContext(), + viewModel.item, + onItemSelected = { sourceIndex -> + createDownloadPreparingDialog() + viewModel.download(sourceIndex) + }, + onCancel = { + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) + }, + ) + dialog.show() + return + } + createDownloadPreparingDialog() + viewModel.download() + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { dialog?.let { val sheet = it as BottomSheetDialog @@ -402,6 +412,31 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { dialog.show() } + private fun createPickQualityDialog() { + val qualityEntries = resources.getStringArray(CoreR.array.quality_entries) + val qualityValues = resources.getStringArray(CoreR.array.quality_values) + val quality = appPreferences.downloadQuality + val currentQualityIndex = qualityValues.indexOf(quality) + var selectedQuality = quality + + + val builder = MaterialAlertDialogBuilder(requireContext()) + builder.setTitle("Download Quality") + builder.setSingleChoiceItems(qualityEntries, currentQualityIndex) { _, which -> + selectedQuality = qualityValues[which] + } + builder.setPositiveButton("Download") { dialog, _ -> + appPreferences.downloadQuality = selectedQuality + dialog.dismiss() + download() + } + builder.setNegativeButton("Cancel") { dialog, _ -> + dialog.dismiss() + } + val dialog = builder.create() + dialog.show() + } + private fun navigateToPlayerActivity( playerItems: Array, ) { diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt index ed6b88944b..b5aded7c47 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt @@ -192,65 +192,7 @@ class MovieFragment : Fragment() { } binding.itemActions.downloadButton.setOnClickListener { - if (viewModel.item.isDownloaded()) { - viewModel.deleteItem() - binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) - } else if (viewModel.item.isDownloading()) { - createCancelDialog() - } else { - binding.itemActions.downloadButton.setIconResource(android.R.color.transparent) - binding.itemActions.progressDownload.isIndeterminate = true - binding.itemActions.progressDownload.isVisible = true - if (requireContext().getExternalFilesDirs(null).filterNotNull().size > 1) { - val storageDialog = getStorageSelectionDialog( - requireContext(), - onItemSelected = { storageIndex -> - if (viewModel.item.sources.size > 1) { - val dialog = getVideoVersionDialog( - requireContext(), - viewModel.item, - onItemSelected = { sourceIndex -> - createDownloadPreparingDialog() - viewModel.download(sourceIndex, storageIndex) - }, - onCancel = { - binding.itemActions.progressDownload.isVisible = false - binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) - }, - ) - dialog.show() - return@getStorageSelectionDialog - } - createDownloadPreparingDialog() - viewModel.download(storageIndex = storageIndex) - }, - onCancel = { - binding.itemActions.progressDownload.isVisible = false - binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) - }, - ) - storageDialog.show() - return@setOnClickListener - } - if (viewModel.item.sources.size > 1) { - val dialog = getVideoVersionDialog( - requireContext(), - viewModel.item, - onItemSelected = { sourceIndex -> - createDownloadPreparingDialog() - viewModel.download(sourceIndex) - }, - onCancel = { - binding.itemActions.progressDownload.isVisible = false - binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) - }, - ) - dialog.show() - return@setOnClickListener - } - createDownloadPreparingDialog() - viewModel.download() - } + handleDownload() } binding.peopleRecyclerView.adapter = PersonListAdapter { person -> @@ -258,6 +200,74 @@ class MovieFragment : Fragment() { } } + private fun handleDownload() { + if (viewModel.item.isDownloaded()) { + viewModel.deleteItem() + binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) + } else if (viewModel.item.isDownloading()) { + createCancelDialog() + } else if (!appPreferences.downloadQualityDefault) { + createPickQualityDialog() + } else { + download() + } + } + + private fun download() { + binding.itemActions.downloadButton.setIconResource(android.R.color.transparent) + binding.itemActions.progressDownload.isIndeterminate = true + binding.itemActions.progressDownload.isVisible = true + if (requireContext().getExternalFilesDirs(null).filterNotNull().size > 1) { + val storageDialog = getStorageSelectionDialog( + requireContext(), + onItemSelected = { storageIndex -> + if (viewModel.item.sources.size > 1) { + val dialog = getVideoVersionDialog( + requireContext(), + viewModel.item, + onItemSelected = { sourceIndex -> + createDownloadPreparingDialog() + viewModel.download(sourceIndex, storageIndex) + }, + onCancel = { + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) + }, + ) + dialog.show() + return@getStorageSelectionDialog + } + createDownloadPreparingDialog() + viewModel.download(storageIndex = storageIndex) + }, + onCancel = { + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) + }, + ) + storageDialog.show() + return + } + if (viewModel.item.sources.size > 1) { + val dialog = getVideoVersionDialog( + requireContext(), + viewModel.item, + onItemSelected = { sourceIndex -> + createDownloadPreparingDialog() + viewModel.download(sourceIndex) + }, + onCancel = { + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) + }, + ) + dialog.show() + return + } + createDownloadPreparingDialog() + viewModel.download() + } + override fun onResume() { super.onResume() @@ -495,6 +505,31 @@ class MovieFragment : Fragment() { dialog.show() } + private fun createPickQualityDialog() { + val qualityEntries = resources.getStringArray(CoreR.array.quality_entries) + val qualityValues = resources.getStringArray(CoreR.array.quality_values) + val quality = appPreferences.downloadQuality + val currentQualityIndex = qualityValues.indexOf(quality) + var selectedQuality = quality + + + val builder = MaterialAlertDialogBuilder(requireContext()) + builder.setTitle("Download Quality") + builder.setSingleChoiceItems(qualityEntries, currentQualityIndex) { _, which -> + selectedQuality = qualityValues[which] + } + builder.setPositiveButton("Download") { dialog, _ -> + appPreferences.downloadQuality = selectedQuality + download() + dialog.dismiss() + } + builder.setNegativeButton("Cancel") { dialog, _ -> + dialog.dismiss() + } + val dialog = builder.create() + dialog.show() + } + private fun navigateToPlayerActivity( playerItems: Array, ) { diff --git a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt index 9b0d2090d2..d04f911068 100644 --- a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt +++ b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt @@ -26,6 +26,8 @@ import dev.jdtech.jellyfin.models.toFindroidTrickplayInfoDto import dev.jdtech.jellyfin.models.toFindroidUserDataDto import dev.jdtech.jellyfin.models.toIntroDto import dev.jdtech.jellyfin.repository.JellyfinRepository +import org.jellyfin.sdk.model.api.EncodingContext +import org.jellyfin.sdk.model.api.MediaStreamType import java.io.File import java.util.UUID import kotlin.Exception @@ -82,15 +84,29 @@ class DownloaderImpl( if (intro != null) { database.insertIntro(intro.toIntroDto(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 != "Original") { + 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 -> { @@ -111,15 +127,29 @@ class DownloaderImpl( if (intro != null) { database.insertIntro(intro.toIntroDto(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 != "Original") { + 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) @@ -230,6 +260,45 @@ class DownloaderImpl( } } + private fun downloadEmbeddedMediaStreams( + item: FindroidItem, + source: FindroidSource, + storageIndex: Int = 0 + ) { + val storageLocation = context.getExternalFilesDirs(null)[storageIndex] + val subtitleStreams = source.mediaStreams.filter { !it.isExternal && it.type == MediaStreamType.SUBTITLE && it.path != null } + for (mediaStream in subtitleStreams) { + var deliveryUrl = mediaStream.path!! + if (mediaStream.codec == "webvtt") { + deliveryUrl = deliveryUrl.replace("Stream.srt", "Stream.vtt") + } + val id = UUID.randomUUID() + val streamPath = Uri.fromFile( + File( + storageLocation, + "downloads/${item.id}.${source.id}.$id.download" + ) + ) + database.insertMediaStream( + mediaStream.toFindroidMediaStreamDto( + id, + source.id, + 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 downloadId = downloadManager.enqueue(request) + database.setMediaStreamDownloadId(id, downloadId) + } + } + + private suspend fun downloadTrickplayData( itemId: UUID, sourceId: String, @@ -263,4 +332,47 @@ class DownloaderImpl( file.writeBytes(byteArray) } } + + 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 + } + + 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 playSessionId = playbackInfo.content.playSessionId!! + val deviceId = jellyfinRepository.getDeviceId() + val downloadUrl = jellyfinRepository.getVideoStreambyContainerUrl(itemId, deviceId, mediaSourceId, playSessionId, maxBitrate, "ts") + + val transcodeUri = buildTranscodeUri(downloadUrl, maxBitrate, quality) + transcodeUri + } catch (e: Exception) { + null + } + } + + // TODO: I believe building upon the uri is not necessary anymore all is handled in the sdk api + 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") + .build() + } } diff --git a/core/src/main/res/values/string_arrays.xml b/core/src/main/res/values/string_arrays.xml index 6e92f5c042..d198af5ace 100644 --- a/core/src/main/res/values/string_arrays.xml +++ b/core/src/main/res/values/string_arrays.xml @@ -25,4 +25,16 @@ audiotrack opensles + + Original + 720p - 2Mbps + 480p - 1Mbps + 360p - 800Kbps + + + Original + 720p + 480p + 360p + \ No newline at end of file diff --git a/core/src/main/res/xml/fragment_settings_downloads.xml b/core/src/main/res/xml/fragment_settings_downloads.xml index 358972ee24..9ebeb35624 100644 --- a/core/src/main/res/xml/fragment_settings_downloads.xml +++ b/core/src/main/res/xml/fragment_settings_downloads.xml @@ -9,4 +9,16 @@ android:defaultValue="false" app:key="pref_downloads_roaming" app:title="@string/download_roaming" /> + + + \ No newline at end of file diff --git a/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt b/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt index eb7e9dca71..c88d2d9da1 100644 --- a/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt +++ b/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt @@ -123,6 +123,19 @@ constructor( false, ) + var downloadQuality get() = sharedPreferences.getString( + Constants.PREF_DOWNLOADS_QUALITY, + "Original") + set(value) { + sharedPreferences.edit().putString(Constants.PREF_DOWNLOADS_QUALITY, value).apply() + } + + val downloadQualityDefault get() = sharedPreferences.getBoolean( + Constants.PREF_DOWNLOADS_QUALITY_DEFAULT, + false, + ) + + // Sorting var sortBy: String get() = sharedPreferences.getString( diff --git a/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt b/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt index cca99608a8..852dac19fb 100644 --- a/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt +++ b/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt @@ -42,6 +42,8 @@ object Constants { const val PREF_NETWORK_SOCKET_TIMEOUT = "pref_network_socket_timeout" 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" + const val PREF_DOWNLOADS_QUALITY_DEFAULT = "pref_downloads_quality_default" const val PREF_SORT_BY = "pref_sort_by" const val PREF_SORT_ORDER = "pref_sort_order" const val PREF_DISPLAY_EXTRA_INFO = "pref_display_extra_info" From 633ee6b8c42f731240285eff243aa3a9022cccee Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Fri, 19 Jul 2024 05:01:09 +0300 Subject: [PATCH 03/13] lint: klint standard --- .../jdtech/jellyfin/utils/DownloaderImpl.kt | 283 +++++--- .../repository/JellyfinRepositoryImpl.kt | 659 ++++++++++-------- .../JellyfinRepositoryOfflineImpl.kt | 130 ++-- 3 files changed, 635 insertions(+), 437 deletions(-) diff --git a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt index d04f911068..0a463d73d0 100644 --- a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt +++ b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt @@ -30,7 +30,6 @@ import org.jellyfin.sdk.model.api.EncodingContext import org.jellyfin.sdk.model.api.MediaStreamType import java.io.File import java.util.UUID -import kotlin.Exception import kotlin.math.ceil import dev.jdtech.jellyfin.core.R as CoreR @@ -48,13 +47,15 @@ class DownloaderImpl( storageIndex: Int, ): Pair { try { - val source = jellyfinRepository.getMediaSources(item.id, true).first { it.id == sourceId } + val source = + jellyfinRepository.getMediaSources(item.id, true).first { it.id == sourceId } val intro = jellyfinRepository.getIntroTimestamps(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)) @@ -85,24 +86,29 @@ class DownloaderImpl( database.insertIntro(intro.toIntroDto(item.id)) } if (appPreferences.downloadQuality != "Original") { - 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) + 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) + } 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) @@ -111,7 +117,8 @@ class DownloaderImpl( is FindroidEpisode -> { database.insertShow( - jellyfinRepository.getShow(item.seriesId) + jellyfinRepository + .getShow(item.seriesId) .toFindroidShowDto(appPreferences.currentServer!!), ) database.insertSeason( @@ -128,24 +135,29 @@ class DownloaderImpl( database.insertIntro(intro.toIntroDto(item.id)) } if (appPreferences.downloadQuality != "Original") { - 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) + 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) + } 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) @@ -157,24 +169,41 @@ class DownloaderImpl( try { val source = jellyfinRepository.getMediaSources(item.id).first { it.id == sourceId } deleteItem(item, source) - } catch (_: Exception) {} + } catch (_: Exception) { + } - return Pair(-1, if (e.message != null) UiText.DynamicString(e.message!!) else UiText.StringResource(CoreR.string.unknown_error)) + return Pair( + -1, + if (e.message != null) { + UiText.DynamicString(e.message!!) + } else { + UiText.StringResource( + CoreR.string.unknown_error, + ) + }, + ) } } - 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) } + is FindroidEpisode -> { database.deleteEpisode(item.id) val remainingEpisodes = database.getEpisodesBySeasonId(item.seasonId) @@ -212,23 +241,29 @@ 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 = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)) + val totalBytes = + cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)) if (totalBytes > 0) { - val downloadedBytes = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)) + val downloadedBytes = + cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)) progress = downloadedBytes.times(100).div(totalBytes).toInt() } } + DownloadManager.STATUS_SUCCESSFUL -> { progress = 100 } @@ -247,14 +282,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")) - database.insertMediaStream(mediaStream.toFindroidMediaStreamDto(id, source.id, 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 streamPath = + Uri.fromFile( + File( + storageLocation, + "downloads/${item.id}.${source.id}.$id.download", + ), + ) + database.insertMediaStream( + mediaStream.toFindroidMediaStreamDto( + id, + source.id, + 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 downloadId = downloadManager.enqueue(request) database.setMediaStreamDownloadId(id, downloadId) } @@ -263,57 +312,66 @@ 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 } + val subtitleStreams = + source.mediaStreams.filter { !it.isExternal && it.type == MediaStreamType.SUBTITLE && it.path != null } for (mediaStream in subtitleStreams) { var deliveryUrl = mediaStream.path!! if (mediaStream.codec == "webvtt") { 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() 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) } @@ -333,22 +391,38 @@ 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 - } + 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 + } 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(maxBitrate, "mkv", EncodingContext.STATIC) + val playbackInfo = + jellyfinRepository.getPostedPlaybackInfo(itemId, false, deviceProfile, maxBitrate) + 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, + maxBitrate, + "ts", + ) val transcodeUri = buildTranscodeUri(downloadUrl, maxBitrate, quality) transcodeUri @@ -361,15 +435,18 @@ class DownloaderImpl( private fun buildTranscodeUri( transcodingUrl: String, maxBitrate: Int, - quality: String + quality: String, ): Uri { - val resolution = when (quality) { - "720p" -> "720" - "480p" -> "480" - "360p" -> "360" - else -> "720" - } - return Uri.parse(transcodingUrl).buildUpon() + 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") diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index ae178c7b55..c8d6d66d45 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -68,55 +68,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 = withContext(Dispatchers.IO) { - jellyfinApi.viewsApi.getUserViews(jellyfinApi.userId!!).content.items.orEmpty() - } + override suspend fun getUserViews(): List = + 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 = withContext(Dispatchers.IO) { - jellyfinApi.itemsApi.getItems( - jellyfinApi.userId!!, - ).content.items + jellyfinApi.itemsApi + .getItems( + jellyfinApi.userId!!, + ).content.items .orEmpty() .mapNotNull { it.toFindroidCollection(this@JellyfinRepositoryImpl) } } @@ -131,16 +146,17 @@ class JellyfinRepositoryImpl( limit: Int?, ): List = 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) } } @@ -151,13 +167,14 @@ class JellyfinRepositoryImpl( recursive: Boolean, sortBy: SortBy, sortOrder: SortOrder, - ): Flow> { - return Pager( - config = PagingConfig( - pageSize = 10, - maxSize = 100, - enablePlaceholders = false, - ), + ): Flow> = + Pager( + config = + PagingConfig( + pageSize = 10, + maxSize = 100, + enablePlaceholders = false, + ), pagingSourceFactory = { ItemsPagingSource( this, @@ -169,87 +186,102 @@ class JellyfinRepositoryImpl( ) }, ).flow - } override suspend fun getPersonItems( personIds: List, includeTypes: List?, recursive: Boolean, - ): List = withContext(Dispatchers.IO) { - jellyfinApi.itemsApi.getItems( - jellyfinApi.userId!!, - personIds = personIds, - includeItemTypes = includeTypes, - recursive = recursive, - ).content.items - .orEmpty() - .mapNotNull { - it.toFindroidItem(this@JellyfinRepositoryImpl, database) - } - } + ): List = + 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 = 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 = 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 { - 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 { - 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 = + override suspend fun getSeasons( + seriesId: UUID, + offline: Boolean, + ): List = 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 { @@ -259,12 +291,13 @@ class JellyfinRepositoryImpl( override suspend fun getNextUp(seriesId: UUID?): List = 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) } } @@ -279,14 +312,15 @@ class JellyfinRepositoryImpl( ): List = 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 { @@ -294,39 +328,47 @@ class JellyfinRepositoryImpl( } } - override suspend fun getMediaSources(itemId: UUID, includePath: Boolean): List = + override suspend fun getMediaSources( + itemId: UUID, + includePath: Boolean, + ): List = withContext(Dispatchers.IO) { val sources = mutableListOf() sources.addAll( - jellyfinApi.mediaInfoApi.getPostedPlaybackInfo( - itemId, - PlaybackInfoDto( - userId = jellyfinApi.userId!!, - deviceProfile = DeviceProfile( - name = "Direct play all", - maxStaticBitrate = 1_000_000_000, + 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, - 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, - ) - }, + ).content.mediaSources + .map { + it.toFindroidSource( + this@JellyfinRepositoryImpl, + itemId, + includePath, + ) + }, ) sources.addAll( database.getSources(itemId).map { it.toFindroidSource(database) }, @@ -334,14 +376,18 @@ 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) { try { jellyfinApi.videosApi.getVideoStreamUrl( itemId, static = true, mediaSourceId = mediaSourceId, - playSessionId = playSessionId + playSessionId = playSessionId, ) } catch (e: Exception) { Timber.e(e) @@ -362,16 +408,21 @@ class JellyfinRepositoryImpl( pathParameters["itemId"] = itemId try { - return@withContext jellyfinApi.api.get( - "/Episode/{itemId}/IntroTimestamps/v1", - pathParameters, - ).content + return@withContext jellyfinApi.api + .get( + "/Episode/{itemId}/IntroTimestamps/v1", + pathParameters, + ).content } catch (e: Exception) { return@withContext null } } - 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 { @@ -379,9 +430,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 } @@ -392,21 +447,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, ) } @@ -528,186 +584,215 @@ 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 = withContext(Dispatchers.IO) { val items = mutableListOf() 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 { - return when (transcodeResolution) { + override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair = + when (transcodeResolution) { 1080 -> 8000000 to 384000 // Adjusted for personal can be other values 720 -> 2000000 to 384000 // 720p 480 -> 1000000 to 384000 // 480p - 360 -> 800000 to 128000 // 360p + 360 -> 800000 to 128000 // 360p else -> 12000000 to 384000 // its adaptive but setting max here } - } - 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 = 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, + ), + ), + 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 { - 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 { + 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, + ), ) - ) 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, + container: String, + ): String { + val url = + jellyfinApi.videosApi.getVideoStreamByContainerUrl( itemId, static = false, deviceId = deviceId, mediaSourceId = mediaSourceId, playSessionId = playSessionId, videoBitRate = videoBitrate, - enableAdaptiveBitrateStreaming = false, - audioBitRate = 384000, //could also be passed with audioBitrate but i preferred not as its not much data anyways - videoCodec = "hevc,h264", - audioCodec = "aac,ac3,eac3", - 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, + audioBitRate = 384000, videoCodec = "hevc", audioCodec = "aac,ac3,eac3", + container = container, 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 == 12000000 + val url = + if (!isAuto) { + jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl( + itemId, + static = false, + deviceId = deviceId, + mediaSourceId = mediaSourceId, + playSessionId = playSessionId, + videoBitRate = videoBitrate, + enableAdaptiveBitrateStreaming = false, + audioBitRate = 384000, // could also be passed with audioBitrate but i preferred not as its not much data anyways + videoCodec = "hevc,h264", + audioCodec = "aac,ac3,eac3", + 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 getDeviceId(): String { val devices = jellyfinApi.devicesApi.getDevices(getUserId()) - return devices.content.items?.firstOrNull()?.id!! + return devices.content.items + ?.firstOrNull() + ?.id!! } override suspend fun stopEncodingProcess(playSessionId: String) { val deviceId = getDeviceId() jellyfinApi.api.hlsSegmentApi.stopEncodingProcess( deviceId = deviceId, - playSessionId = playSessionId + playSessionId = playSessionId, ) } - } - - diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt index 6901c09d11..9658541feb 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt @@ -42,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 { - return emptyList() - } + override suspend fun getUserViews(): List = emptyList() override suspend fun getItem(itemId: UUID): BaseItemDto { TODO("Not yet implemented") @@ -113,38 +108,69 @@ class JellyfinRepositoryOfflineImpl( TODO("Not yet implemented") } - override suspend fun getSearchItems(searchQuery: String): List { - 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 = + withContext(Dispatchers.IO) { + val movies = + database.searchMovies(appPreferences.currentServer!!, searchQuery).map { + it.toFindroidMovie( + database, + @Suppress("ktlint:standard:max-line-length") + 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 { - 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 = + 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 { - return emptyList() - } + override suspend fun getLatestMedia(parentId: UUID): List = emptyList() - override suspend fun getSeasons(seriesId: UUID, offline: Boolean): List = + override suspend fun getSeasons( + seriesId: UUID, + offline: Boolean, + ): List = withContext(Dispatchers.IO) { database.getSeasonsByShowId(seriesId).map { it.toFindroidSeason(database, jellyfinApi.userId!!) } } - override suspend fun getNextUp(seriesId: UUID?): List { - return withContext(Dispatchers.IO) { + override suspend fun getNextUp(seriesId: UUID?): List = + withContext(Dispatchers.IO) { val result = mutableListOf() - 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 } @@ -156,7 +182,6 @@ class JellyfinRepositoryOfflineImpl( } result.filter { it.playbackPositionTicks == 0L } } - } override suspend fun getEpisodes( seriesId: UUID, @@ -172,12 +197,19 @@ class JellyfinRepositoryOfflineImpl( items } - override suspend fun getMediaSources(itemId: UUID, includePath: Boolean): List = + override suspend fun getMediaSources( + itemId: UUID, + includePath: Boolean, + ): List = 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") } @@ -186,7 +218,11 @@ class JellyfinRepositoryOfflineImpl( database.getIntro(itemId)?.toIntro() } - 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 @@ -200,7 +236,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 -> { @@ -260,35 +300,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 = withContext(Dispatchers.IO) { val items = mutableListOf() 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") @@ -301,7 +337,7 @@ class JellyfinRepositoryOfflineImpl( override suspend fun buildDeviceProfile( maxBitrate: Int, container: String, - context: EncodingContext + context: EncodingContext, ): DeviceProfile { TODO("Not yet implemented") } @@ -312,7 +348,7 @@ class JellyfinRepositoryOfflineImpl( mediaSourceId: String, playSessionId: String, videoBitrate: Int, - container: String + container: String, ): String { TODO("Not yet implemented") } @@ -322,7 +358,7 @@ class JellyfinRepositoryOfflineImpl( deviceId: String, mediaSourceId: String, playSessionId: String, - videoBitrate: Int + videoBitrate: Int, ): String { TODO("Not yet implemented") } @@ -331,7 +367,7 @@ class JellyfinRepositoryOfflineImpl( itemId: UUID, enableDirectStream: Boolean, deviceProfile: DeviceProfile, - maxBitrate: Int + maxBitrate: Int, ): Response { TODO("Not yet implemented") } From 4baa7bc0463e0f46e879ca33e2ef4a916ed52c9d Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Fri, 19 Jul 2024 05:10:32 +0300 Subject: [PATCH 04/13] lint: klint standard --- .../jellyfin/repository/JellyfinRepository.kt | 74 +++++++++++++++---- .../repository/JellyfinRepositoryImpl.kt | 1 + 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt index 2b4380c033..7c1d6725d8 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt @@ -29,7 +29,9 @@ interface JellyfinRepository { suspend fun getUserViews(): List suspend fun getItem(itemId: UUID): BaseItemDto + suspend fun getEpisode(itemId: UUID): FindroidEpisode + suspend fun getMovie(itemId: UUID): FindroidMovie suspend fun getShow(itemId: UUID): FindroidShow @@ -70,7 +72,10 @@ interface JellyfinRepository { suspend fun getLatestMedia(parentId: UUID): List - suspend fun getSeasons(seriesId: UUID, offline: Boolean = false): List + suspend fun getSeasons( + seriesId: UUID, + offline: Boolean = false, + ): List suspend fun getNextUp(seriesId: UUID? = null): List @@ -83,21 +88,40 @@ 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 getIntroTimestamps(itemId: UUID): Intro? - 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,13 +145,37 @@ interface JellyfinRepository { suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair - suspend fun buildDeviceProfile(maxBitrate: Int, container: String, context: EncodingContext): DeviceProfile - - 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 getPostedPlaybackInfo(itemId: UUID, enableDirectStream: Boolean, deviceProfile: DeviceProfile ,maxBitrate: Int): Response + suspend fun buildDeviceProfile( + maxBitrate: Int, + container: String, + context: EncodingContext, + ): DeviceProfile + + suspend fun getVideoStreambyContainerUrl( + itemId: UUID, + deviceId: String, + mediaSourceId: String, + playSessionId: String, + videoBitrate: + @Suppress("ktlint:standard:max-line-length") + Int, + container: String, + ): 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 suspend fun stopEncodingProcess(playSessionId: String) } diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index c8d6d66d45..5e4e1a6e75 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -710,6 +710,7 @@ class JellyfinRepositoryImpl( deviceId: String, mediaSourceId: String, playSessionId: String, + @Suppress("ktlint:standard:max-line-length") videoBitrate: Int, container: String, ): String { From ba580f8769e18e27ee89081312c844babc8a3367 Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Fri, 19 Jul 2024 05:27:17 +0300 Subject: [PATCH 05/13] lint: fix --- .../dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index 5e4e1a6e75..c8d6d66d45 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -710,7 +710,6 @@ class JellyfinRepositoryImpl( deviceId: String, mediaSourceId: String, playSessionId: String, - @Suppress("ktlint:standard:max-line-length") videoBitrate: Int, container: String, ): String { From 6dded2e72641868731d4716e6b35e906496b9799 Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Sat, 20 Jul 2024 08:36:23 +0300 Subject: [PATCH 06/13] bugfixes: deviceId / code: New Enum VideoQuality --- .../dev/jdtech/jellyfin/PlayerActivity.kt | 49 ++++++++++++++----- .../fragments/EpisodeBottomSheetFragment.kt | 4 +- .../jellyfin/fragments/MovieFragment.kt | 4 +- .../jdtech/jellyfin/utils/DownloaderImpl.kt | 42 +++------------- core/src/main/res/values/string_arrays.xml | 22 ++++++++- .../res/xml/fragment_settings_downloads.xml | 4 +- .../jdtech/jellyfin/models/VideoQuality.kt | 25 ++++++++++ .../jellyfin/repository/JellyfinRepository.kt | 7 +-- .../repository/JellyfinRepositoryImpl.kt | 41 ++++++---------- .../JellyfinRepositoryOfflineImpl.kt | 5 +- .../viewmodels/PlayerActivityViewModel.kt | 27 +++------- 11 files changed, 121 insertions(+), 109 deletions(-) create mode 100644 data/src/main/java/dev/jdtech/jellyfin/models/VideoQuality.kt diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index 891170e8b1..1b4eb03f86 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -45,6 +45,7 @@ import dev.jdtech.jellyfin.viewmodels.PlayerEvents import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject +import dev.jdtech.jellyfin.core.R as CoreR var isControlsLocked: Boolean = false @@ -348,20 +349,44 @@ class PlayerActivity : BasePlayerActivity() { } private fun showQualitySelectionDialog() { - val height = viewModel.getOriginalHeight() // TODO: rewrite getting height stuff I don't like that its only update after changing quality - 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") - } + 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 = + when (height) { + 0 -> qualityEntries + in 1001..1999 -> + listOf( + qualityEntries[0], + "${qualityEntries[1]} (1080p)", + qualityEntries[2], + qualityEntries[3], + qualityEntries[4], + qualityEntries[5], + ) + in 2000..3000 -> + listOf( + qualityEntries[0], + "${qualityEntries[1]} (4K)", + qualityEntries[2], + qualityEntries[3], + qualityEntries[4], + qualityEntries[5], + ) + else -> qualityEntries + } MaterialAlertDialogBuilder(this) .setTitle("Select Video Quality") - .setItems(qualities) { _, which -> - val selectedQuality = qualities[which] - viewModel.changeVideoQuality(selectedQuality) - } - .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( diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt index 925f70f3b8..83d134077b 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt @@ -413,8 +413,8 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { } 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 diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt index b5aded7c47..a70d445652 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt @@ -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 diff --git a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt index 0a463d73d0..34ae76f0f5 100644 --- a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt +++ b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt @@ -16,6 +16,7 @@ import dev.jdtech.jellyfin.models.FindroidSource import dev.jdtech.jellyfin.models.FindroidSources import dev.jdtech.jellyfin.models.FindroidTrickplayInfo import dev.jdtech.jellyfin.models.UiText +import dev.jdtech.jellyfin.models.VideoQuality import dev.jdtech.jellyfin.models.toFindroidEpisodeDto import dev.jdtech.jellyfin.models.toFindroidMediaStreamDto import dev.jdtech.jellyfin.models.toFindroidMovieDto @@ -395,19 +396,12 @@ class DownloaderImpl( itemId: UUID, quality: String, ): Uri? { - val maxBitrate = - when (quality) { - "720p" -> 2000000 // 2 Mbps - "480p" -> 1000000 // 1 Mbps - "360p" -> 800000 // 800Kbps - else -> 2000000 - } - + val videoQuality = VideoQuality.fromString(quality)!! return try { val deviceProfile = - jellyfinRepository.buildDeviceProfile(maxBitrate, "mkv", EncodingContext.STATIC) + jellyfinRepository.buildDeviceProfile(VideoQuality.getBitrate(videoQuality), "mkv", EncodingContext.STATIC) val playbackInfo = - jellyfinRepository.getPostedPlaybackInfo(itemId, false, deviceProfile, maxBitrate) + jellyfinRepository.getPostedPlaybackInfo(itemId, false, deviceProfile, VideoQuality.getBitrate(videoQuality)) val mediaSourceId = playbackInfo.content.mediaSources .firstOrNull() @@ -420,36 +414,14 @@ class DownloaderImpl( deviceId, mediaSourceId, playSessionId, - maxBitrate, + VideoQuality.getBitrate(videoQuality), "ts", + VideoQuality.getQualityInt(videoQuality) ) - val transcodeUri = buildTranscodeUri(downloadUrl, maxBitrate, quality) - transcodeUri + downloadUrl.toUri() } catch (e: Exception) { null } } - - // TODO: I believe building upon the uri is not necessary anymore all is handled in the sdk api - 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") - .build() - } } diff --git a/core/src/main/res/values/string_arrays.xml b/core/src/main/res/values/string_arrays.xml index d198af5ace..b5dd209509 100644 --- a/core/src/main/res/values/string_arrays.xml +++ b/core/src/main/res/values/string_arrays.xml @@ -26,13 +26,31 @@ opensles + Auto Original - 720p - 2Mbps - 480p - 1Mbps + 1080p - 8Mbps + 720p - 3Mbps + 480p - 1.5Mbps 360p - 800Kbps + Auto Original + 1080p + 720p + 480p + 360p + + + Original + 1080p - 8Mbps + 720p - 3Mbps + 480p - 1.5Mbps + 360p - 800Kbps + + + Original + 1080p 720p 480p 360p diff --git a/core/src/main/res/xml/fragment_settings_downloads.xml b/core/src/main/res/xml/fragment_settings_downloads.xml index 9ebeb35624..c88d3b8104 100644 --- a/core/src/main/res/xml/fragment_settings_downloads.xml +++ b/core/src/main/res/xml/fragment_settings_downloads.xml @@ -14,8 +14,8 @@ android:key="pref_downloads_quality" android:title="Download Quality" android:defaultValue="Original" - android:entries="@array/quality_entries" - android:entryValues="@array/quality_values" + android:entries="@array/download_quality_entries" + android:entryValues="@array/download_quality_values" android:summary="%s" /> - suspend fun buildDeviceProfile( maxBitrate: Int, container: String, @@ -156,10 +154,9 @@ interface JellyfinRepository { deviceId: String, mediaSourceId: String, playSessionId: String, - videoBitrate: - @Suppress("ktlint:standard:max-line-length") - Int, + videoBitrate: Int, container: String, + maxHeight: Int, ): String suspend fun getTranscodedVideoStream( diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index c8d6d66d45..40d86bc6f4 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -16,6 +16,7 @@ import dev.jdtech.jellyfin.models.FindroidShow import dev.jdtech.jellyfin.models.FindroidSource import dev.jdtech.jellyfin.models.Intro import dev.jdtech.jellyfin.models.SortBy +import dev.jdtech.jellyfin.models.VideoQuality import dev.jdtech.jellyfin.models.toFindroidCollection import dev.jdtech.jellyfin.models.toFindroidEpisode import dev.jdtech.jellyfin.models.toFindroidItem @@ -609,15 +610,6 @@ class JellyfinRepositoryImpl( override fun getUserId(): UUID = jellyfinApi.userId!! - override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair = - when (transcodeResolution) { - 1080 -> 8000000 to 384000 // Adjusted for personal can be other values - 720 -> 2000000 to 384000 // 720p - 480 -> 1000000 to 384000 // 480p - 360 -> 800000 to 128000 // 360p - else -> 12000000 to 384000 // its adaptive but setting max here - } - override suspend fun buildDeviceProfile( maxBitrate: Int, container: String, @@ -631,7 +623,7 @@ class JellyfinRepositoryImpl( supportsPersistentIdentifier = true, deviceProfile = DeviceProfile( - name = "AnanasUser", + name = "FindroidUser", id = getUserId().toString(), maxStaticBitrate = maxBitrate, maxStreamingBitrate = maxBitrate, @@ -648,8 +640,8 @@ class JellyfinRepositoryImpl( container = container, context = context, protocol = MediaStreamProtocol.HLS, - audioCodec = "aac,ac3,eac3", - videoCodec = "hevc,h264", + audioCodec = "aac", + videoCodec = "h264", type = DlnaProfileType.VIDEO, conditions = listOf( @@ -712,6 +704,7 @@ class JellyfinRepositoryImpl( playSessionId: String, videoBitrate: Int, container: String, + maxHeight: Int, ): String { val url = jellyfinApi.videosApi.getVideoStreamByContainerUrl( @@ -721,10 +714,11 @@ class JellyfinRepositoryImpl( mediaSourceId = mediaSourceId, playSessionId = playSessionId, videoBitRate = videoBitrate, - audioBitRate = 384000, - videoCodec = "hevc", - audioCodec = "aac,ac3,eac3", + audioBitRate = 128000, + videoCodec = "h264", + audioCodec = "aac", container = container, + maxHeight = maxHeight, startTimeTicks = 0, copyTimestamps = true, subtitleMethod = SubtitleDeliveryMethod.EXTERNAL, @@ -739,7 +733,7 @@ class JellyfinRepositoryImpl( playSessionId: String, videoBitrate: Int, ): String { - val isAuto = videoBitrate == 12000000 + val isAuto = videoBitrate == VideoQuality.getBitrate(VideoQuality.PAuto) val url = if (!isAuto) { jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl( @@ -750,9 +744,9 @@ class JellyfinRepositoryImpl( playSessionId = playSessionId, videoBitRate = videoBitrate, enableAdaptiveBitrateStreaming = false, - audioBitRate = 384000, // could also be passed with audioBitrate but i preferred not as its not much data anyways - videoCodec = "hevc,h264", - audioCodec = "aac,ac3,eac3", + audioBitRate = 128000, + videoCodec = "h264", + audioCodec = "aac", startTimeTicks = 0, copyTimestamps = true, subtitleMethod = SubtitleDeliveryMethod.EXTERNAL, @@ -768,8 +762,8 @@ class JellyfinRepositoryImpl( mediaSourceId = mediaSourceId, playSessionId = playSessionId, enableAdaptiveBitrateStreaming = true, - videoCodec = "hevc", - audioCodec = "aac,ac3,eac3", + videoCodec = "h264", + audioCodec = "aac", startTimeTicks = 0, copyTimestamps = true, subtitleMethod = SubtitleDeliveryMethod.EXTERNAL, @@ -782,10 +776,7 @@ class JellyfinRepositoryImpl( } override suspend fun getDeviceId(): String { - val devices = jellyfinApi.devicesApi.getDevices(getUserId()) - return devices.content.items - ?.firstOrNull() - ?.id!! + return jellyfinApi.api.deviceInfo.id } override suspend fun stopEncodingProcess(playSessionId: String) { diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt index 9658541feb..dcc4a39e1b 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt @@ -330,10 +330,6 @@ class JellyfinRepositoryOfflineImpl( TODO("Not yet implemented") } - override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair { - TODO("Not yet implemented") - } - override suspend fun buildDeviceProfile( maxBitrate: Int, container: String, @@ -349,6 +345,7 @@ class JellyfinRepositoryOfflineImpl( playSessionId: String, videoBitrate: Int, container: String, + maxHeight: Int, ): String { TODO("Not yet implemented") } diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt index e804df6eb9..3ca1771f2c 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -28,6 +28,7 @@ import dev.jdtech.jellyfin.models.Intro import dev.jdtech.jellyfin.models.PlayerChapter import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.Trickplay +import dev.jdtech.jellyfin.models.VideoQuality import dev.jdtech.jellyfin.mpv.MPVPlayer import dev.jdtech.jellyfin.player.video.R import dev.jdtech.jellyfin.repository.JellyfinRepository @@ -464,17 +465,6 @@ constructor( eventsChannel.trySend(PlayerEvents.IsPlayingChanged(isPlaying)) } - private fun getTranscodeResolutions(preferredQuality: String): Int { - return when (preferredQuality) { - "1080p" -> 1080 // TODO: 1080p this logic is based on 1080p being original - "720p - 2Mbps" -> 720 - "480p - 1Mbps" -> 480 - "360p - 800kbps" -> 360 - "Auto" -> 1 - else -> 1080 //default to Original - } - } - fun changeVideoQuality(quality: String) { val mediaId = player.currentMediaItem?.mediaId ?: return val currentItem = items.firstOrNull { it.itemId.toString() == mediaId } ?: return @@ -482,12 +472,9 @@ constructor( viewModelScope.launch { try { - val transcodingResolution = getTranscodeResolutions(quality) - val (videoBitRate, audioBitRate) = jellyfinRepository.getVideoTranscodeBitRate( - transcodingResolution - ) - val deviceProfile = jellyfinRepository.buildDeviceProfile(videoBitRate, "mkv", EncodingContext.STREAMING) - val playbackInfo = jellyfinRepository.getPostedPlaybackInfo(currentItem.itemId,true,deviceProfile,videoBitRate) + val videoQuality = VideoQuality.fromString(quality)!! + 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) @@ -537,18 +524,18 @@ constructor( val allSubtitles = - if (transcodingResolution == 1080) { + if (VideoQuality.getQualityString(videoQuality) == "Original") { externalSubtitles }else { embeddedSubtitles.apply { addAll(externalSubtitles) } } - val url = if (transcodingResolution == 1080){ + val url = if (VideoQuality.getQualityString(videoQuality) == "Original"){ jellyfinRepository.getStreamUrl(currentItem.itemId, currentItem.mediaSourceId, playSessionId) } else { val mediaSourceId = mediaSources[currentMediaItemIndex].id val deviceId = jellyfinRepository.getDeviceId() - val url = jellyfinRepository.getTranscodedVideoStream(currentItem.itemId, deviceId ,mediaSourceId, playSessionId!!, videoBitRate) + val url = jellyfinRepository.getTranscodedVideoStream(currentItem.itemId, deviceId ,mediaSourceId, playSessionId!!, VideoQuality.getBitrate(videoQuality)) val uriBuilder = url.toUri().buildUpon() val apiKey = jellyfinApi.api.accessToken // TODO: add in repo uriBuilder.appendQueryParameter("api_key",apiKey ) From 0ace01f5f820391aa61df56073662154f78c7177 Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Sat, 20 Jul 2024 08:40:23 +0300 Subject: [PATCH 07/13] klint --- core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt index 34ae76f0f5..5b608a9127 100644 --- a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt +++ b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt @@ -416,7 +416,7 @@ class DownloaderImpl( playSessionId, VideoQuality.getBitrate(videoQuality), "ts", - VideoQuality.getQualityInt(videoQuality) + VideoQuality.getQualityInt(videoQuality), ) downloadUrl.toUri() From 7adcc50d750d82e08104987e97665ab9c65c85f9 Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Sat, 20 Jul 2024 22:28:38 +0300 Subject: [PATCH 08/13] rework: Enum --- .../dev/jdtech/jellyfin/PlayerActivity.kt | 51 +++++++------------ .../jdtech/jellyfin/utils/DownloaderImpl.kt | 2 +- .../jdtech/jellyfin/models/VideoQuality.kt | 34 +++++++------ .../repository/JellyfinRepositoryImpl.kt | 2 +- .../viewmodels/PlayerActivityViewModel.kt | 4 +- 5 files changed, 42 insertions(+), 51 deletions(-) diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index 1b4eb03f86..122bd8bfda 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -1,5 +1,6 @@ package dev.jdtech.jellyfin +import android.annotation.SuppressLint import android.app.AppOpsManager import android.app.PictureInPictureParams import android.content.Context @@ -38,6 +39,7 @@ import dagger.hilt.android.AndroidEntryPoint import dev.jdtech.jellyfin.databinding.ActivityPlayerBinding import dev.jdtech.jellyfin.dialogs.SpeedSelectionDialogFragment import dev.jdtech.jellyfin.dialogs.TrackSelectionDialogFragment +import dev.jdtech.jellyfin.models.VideoQuality import dev.jdtech.jellyfin.utils.PlayerGestureHelper import dev.jdtech.jellyfin.utils.PreviewScrubListener import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel @@ -288,6 +290,7 @@ class PlayerActivity : BasePlayerActivity() { viewModel.initializePlayer(args.items) } + @SuppressLint("MissingSuperCall") override fun onUserLeaveHint() { super.onUserLeaveHint() if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && @@ -348,45 +351,29 @@ class PlayerActivity : BasePlayerActivity() { } catch (_: IllegalArgumentException) { } } + private var selectedIndex = 1 // Default to "Original" (index 1) private fun showQualitySelectionDialog() { - val height = viewModel.getOriginalHeight() + val originalHeight = viewModel.getOriginalHeight() // TODO: Rework getting originalHeight 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 - originalHeight) } + + 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 + .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/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt index 5b608a9127..934537966b 100644 --- a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt +++ b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt @@ -416,7 +416,7 @@ class DownloaderImpl( playSessionId, VideoQuality.getBitrate(videoQuality), "ts", - VideoQuality.getQualityInt(videoQuality), + VideoQuality.getHeight(videoQuality), ) downloadUrl.toUri() diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/VideoQuality.kt b/data/src/main/java/dev/jdtech/jellyfin/models/VideoQuality.kt index 46b0f07b90..5df83ececb 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/models/VideoQuality.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/models/VideoQuality.kt @@ -2,24 +2,28 @@ package dev.jdtech.jellyfin.models enum class VideoQuality( val bitrate: Int, - val qualityString: String, - val qualityInt: Int, + val height: Int, + val width: Int, + val original: Boolean, ) { - PAuto(10000000, "Auto", 1080), - POriginal(1000000000, "Original", 1080), - P1080(8000000, "1080p", 1080), - P720(3000000, "720p", 720), - P480(1500000, "480p", 480), - P360(800000, "360p", 360), - ; + Auto(10000000, 1080, 1920, false), + Original(1000000000, 1080, 1920, true), + P1080(8000000, 1080, 1920, false), + P720(3000000, 720, 1280, false), + P480(1500000, 480, 854, false), + P360(800000, 360, 640, false); - companion object { - fun fromString(quality: String): VideoQuality? = entries.find { it.qualityString == quality } + override fun toString(): String = when (this) { + Auto -> "Auto" + Original -> "Original" + else -> "${height}p" + } + companion object { + 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 getOriginal(quality: VideoQuality): Boolean = quality.original } } \ No newline at end of file diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index 40d86bc6f4..e16b9fc5ef 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -733,7 +733,7 @@ class JellyfinRepositoryImpl( playSessionId: String, videoBitrate: Int, ): String { - val isAuto = videoBitrate == VideoQuality.getBitrate(VideoQuality.PAuto) + val isAuto = videoBitrate == VideoQuality.getBitrate(VideoQuality.Auto) val url = if (!isAuto) { jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl( diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt index 3ca1771f2c..897e84c2fe 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -524,13 +524,13 @@ constructor( val allSubtitles = - if (VideoQuality.getQualityString(videoQuality) == "Original") { + if (VideoQuality.getOriginal(videoQuality)) { externalSubtitles }else { embeddedSubtitles.apply { addAll(externalSubtitles) } } - val url = if (VideoQuality.getQualityString(videoQuality) == "Original"){ + val url = if (VideoQuality.getOriginal(videoQuality)){ jellyfinRepository.getStreamUrl(currentItem.itemId, currentItem.mediaSourceId, playSessionId) } else { val mediaSourceId = mediaSources[currentMediaItemIndex].id From c79342523be289a34d0bfd1a143e3c3abf3ecd88 Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Sat, 20 Jul 2024 23:12:12 +0300 Subject: [PATCH 09/13] refactor: strings & naming standard for icon --- .../dev/jdtech/jellyfin/PlayerActivity.kt | 2 +- .../src/main/res/layout/exo_main_controls.xml | 2 +- .../jdtech/jellyfin/utils/DownloaderImpl.kt | 2 +- .../{ic_quality.xml => ic_monitor_play.xml} | 0 core/src/main/res/values/string_arrays.xml | 22 +++++++++---------- core/src/main/res/values/strings.xml | 9 ++++++++ .../res/xml/fragment_settings_downloads.xml | 6 ++--- 7 files changed, 26 insertions(+), 17 deletions(-) rename core/src/main/res/drawable/{ic_quality.xml => ic_monitor_play.xml} (100%) diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index 122bd8bfda..a7c51e0241 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -366,7 +366,7 @@ class PlayerActivity : BasePlayerActivity() { qualities[1] = "${qualities[1]} (${closestQuality})" } MaterialAlertDialogBuilder(this) - .setTitle("Select Video Quality") + .setTitle(CoreR.string.select_quality) .setSingleChoiceItems(qualities.toTypedArray(), selectedIndex) { dialog, which -> selectedIndex = which val selectedQualityValue = qualityValues[which] 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 b136be357f..940f0f972f 100644 --- a/app/phone/src/main/res/layout/exo_main_controls.xml +++ b/app/phone/src/main/res/layout/exo_main_controls.xml @@ -81,7 +81,7 @@ android:background="@drawable/transparent_circle_background" android:contentDescription="Quality" android:padding="16dp" - android:src="@drawable/ic_quality" + android:src="@drawable/ic_monitor_play" android:layout_gravity="end" app:tint="@android:color/white" /> diff --git a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt index 934537966b..1586f020a7 100644 --- a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt +++ b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt @@ -86,7 +86,7 @@ class DownloaderImpl( if (intro != null) { database.insertIntro(intro.toIntroDto(item.id)) } - if (appPreferences.downloadQuality != "Original") { + if (appPreferences.downloadQuality != VideoQuality.Original.toString()) { downloadEmbeddedMediaStreams(item, source, storageIndex) val transcodingUrl = getTranscodedUrl(item.id, appPreferences.downloadQuality!!) diff --git a/core/src/main/res/drawable/ic_quality.xml b/core/src/main/res/drawable/ic_monitor_play.xml similarity index 100% rename from core/src/main/res/drawable/ic_quality.xml rename to core/src/main/res/drawable/ic_monitor_play.xml diff --git a/core/src/main/res/values/string_arrays.xml b/core/src/main/res/values/string_arrays.xml index b5dd209509..472228cf52 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 - 3Mbps - 480p - 1.5Mbps - 360p - 800Kbps + @string/quality_auto + @string/quality_original + @string/quality_1080p + @string/quality_720p + @string/quality_480p + @string/quality_360p Auto @@ -42,11 +42,11 @@ 360p - Original - 1080p - 8Mbps - 720p - 3Mbps - 480p - 1.5Mbps - 360p - 800Kbps + @string/quality_original + @string/quality_1080p + @string/quality_720p + @string/quality_480p + @string/quality_360p Original diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index b00c4f8901..7716be386f 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -192,4 +192,13 @@ 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 diff --git a/core/src/main/res/xml/fragment_settings_downloads.xml b/core/src/main/res/xml/fragment_settings_downloads.xml index c88d3b8104..f0d0aa531d 100644 --- a/core/src/main/res/xml/fragment_settings_downloads.xml +++ b/core/src/main/res/xml/fragment_settings_downloads.xml @@ -12,13 +12,13 @@ + app:summary="@string/quality_default" /> \ No newline at end of file From 21ae815223643a8dae0fa28cdf2e4ca00486b07a Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Sat, 20 Jul 2024 23:55:34 +0300 Subject: [PATCH 10/13] rework: getting original resolution for quality selection dialog --- .../dev/jdtech/jellyfin/PlayerActivity.kt | 4 +-- .../jdtech/jellyfin/models/VideoQuality.kt | 2 ++ .../viewmodels/PlayerActivityViewModel.kt | 28 +++++++++++++------ 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index a7c51e0241..00f53e82d8 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -353,14 +353,14 @@ class PlayerActivity : BasePlayerActivity() { private var selectedIndex = 1 // Default to "Original" (index 1) private fun showQualitySelectionDialog() { - val originalHeight = viewModel.getOriginalHeight() // TODO: Rework getting originalHeight + val originalResolution = viewModel.getoriginalResolution() // TODO: Rework getting originalResolution val qualityEntries = resources.getStringArray(CoreR.array.quality_entries).toList() val qualityValues = resources.getStringArray(CoreR.array.quality_values).toList() val qualities = qualityEntries.toMutableList() val closestQuality = VideoQuality.entries .filter { it != VideoQuality.Auto && it != VideoQuality.Original } - .minByOrNull { kotlin.math.abs(it.height - originalHeight) } + .minByOrNull { kotlin.math.abs(it.height*it.width - originalResolution!!) } if (closestQuality != null) { qualities[1] = "${qualities[1]} (${closestQuality})" diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/VideoQuality.kt b/data/src/main/java/dev/jdtech/jellyfin/models/VideoQuality.kt index 5df83ececb..bcd8744880 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/models/VideoQuality.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/models/VideoQuality.kt @@ -8,6 +8,7 @@ enum class VideoQuality( ) { 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), @@ -16,6 +17,7 @@ enum class VideoQuality( override fun toString(): String = when (this) { Auto -> "Auto" Original -> "Original" + P3840 -> "4K" else -> "${height}p" } diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt index 897e84c2fe..ed808c498e 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -18,6 +18,7 @@ import androidx.media3.common.MimeTypes import androidx.media3.common.Player import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.TrackSelectionParameters +import androidx.media3.common.VideoSize import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.trackselection.DefaultTrackSelector @@ -61,7 +62,7 @@ constructor( private val savedStateHandle: SavedStateHandle, ) : ViewModel(), Player.Listener { val player: Player - private var originalHeight: Int = 0 + private var originalResolution: Int? = null private val _uiState = MutableStateFlow( UiState( @@ -179,6 +180,22 @@ constructor( .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) @@ -564,13 +581,8 @@ constructor( 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) { @@ -579,8 +591,8 @@ constructor( } } - fun getOriginalHeight(): Int { - return originalHeight + fun getoriginalResolution(): Int? { + return originalResolution } } From 8482df97334301057081a735491b00bd8d84e1c5 Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Sun, 21 Jul 2024 00:42:35 +0300 Subject: [PATCH 11/13] feat: choice of codec in network settings / bugfix: nullsafe fix --- .../src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt | 4 ++-- core/src/main/res/values/string_arrays.xml | 4 ++++ core/src/main/res/values/strings.xml | 1 + core/src/main/res/xml/fragment_settings_network.xml | 7 +++++++ .../jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt | 8 ++++---- .../src/main/java/dev/jdtech/jellyfin/AppPreferences.kt | 5 +++++ .../src/main/java/dev/jdtech/jellyfin/Constants.kt | 2 ++ 7 files changed, 25 insertions(+), 6 deletions(-) diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index 00f53e82d8..b7b4fb7c12 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -353,14 +353,14 @@ class PlayerActivity : BasePlayerActivity() { private var selectedIndex = 1 // Default to "Original" (index 1) private fun showQualitySelectionDialog() { - val originalResolution = viewModel.getoriginalResolution() // TODO: Rework getting originalResolution + val originalResolution = viewModel.getoriginalResolution() ?: 0// TODO: Rework getting originalResolution val qualityEntries = resources.getStringArray(CoreR.array.quality_entries).toList() val qualityValues = resources.getStringArray(CoreR.array.quality_values).toList() val qualities = qualityEntries.toMutableList() val closestQuality = VideoQuality.entries .filter { it != VideoQuality.Auto && it != VideoQuality.Original } - .minByOrNull { kotlin.math.abs(it.height*it.width - originalResolution!!) } + .minByOrNull { kotlin.math.abs(it.height*it.width - originalResolution) } if (closestQuality != null) { qualities[1] = "${qualities[1]} (${closestQuality})" diff --git a/core/src/main/res/values/string_arrays.xml b/core/src/main/res/values/string_arrays.xml index 472228cf52..36b6658f6f 100644 --- a/core/src/main/res/values/string_arrays.xml +++ b/core/src/main/res/values/string_arrays.xml @@ -55,4 +55,8 @@ 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 7716be386f..5bc5be72eb 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 diff --git a/core/src/main/res/xml/fragment_settings_network.xml b/core/src/main/res/xml/fragment_settings_network.xml index 5e5bd8a2f3..fd96636460 100644 --- a/core/src/main/res/xml/fragment_settings_network.xml +++ b/core/src/main/res/xml/fragment_settings_network.xml @@ -15,4 +15,11 @@ app:key="pref_network_socket_timeout" app:title="@string/settings_socket_timeout" app:useSimpleSummaryProvider="true" /> + \ No newline at end of file diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index e16b9fc5ef..a084734945 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -641,7 +641,7 @@ class JellyfinRepositoryImpl( context = context, protocol = MediaStreamProtocol.HLS, audioCodec = "aac", - videoCodec = "h264", + videoCodec = appPreferences.transcodeCodec!!, type = DlnaProfileType.VIDEO, conditions = listOf( @@ -715,7 +715,7 @@ class JellyfinRepositoryImpl( playSessionId = playSessionId, videoBitRate = videoBitrate, audioBitRate = 128000, - videoCodec = "h264", + videoCodec = appPreferences.transcodeCodec, audioCodec = "aac", container = container, maxHeight = maxHeight, @@ -745,7 +745,7 @@ class JellyfinRepositoryImpl( videoBitRate = videoBitrate, enableAdaptiveBitrateStreaming = false, audioBitRate = 128000, - videoCodec = "h264", + videoCodec = appPreferences.transcodeCodec, audioCodec = "aac", startTimeTicks = 0, copyTimestamps = true, @@ -762,7 +762,7 @@ class JellyfinRepositoryImpl( mediaSourceId = mediaSourceId, playSessionId = playSessionId, enableAdaptiveBitrateStreaming = true, - videoCodec = "h264", + videoCodec = appPreferences.transcodeCodec, audioCodec = "aac", startTimeTicks = 0, copyTimestamps = true, diff --git a/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt b/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt index c88d2d9da1..957f9febed 100644 --- a/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt +++ b/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt @@ -103,6 +103,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/dev/jdtech/jellyfin/Constants.kt b/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt index 852dac19fb..4f5554223b 100644 --- a/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt +++ b/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt @@ -40,6 +40,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" @@ -60,6 +61,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] From d70253140dee07b6c3e786f13953c0499b71c3f6 Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Sun, 21 Jul 2024 00:43:12 +0300 Subject: [PATCH 12/13] refactor: string --- core/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 5bc5be72eb..2f65247c13 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -139,7 +139,7 @@ Request timeout (ms) Connect timeout (ms) Socket timeout (ms) - Transcoding Codec + Transcoding codec Users Add user Hardware decoding From 5609f7368d34ed6a7067c29d35dfdcc3eca03f00 Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Sun, 21 Jul 2024 01:49:30 +0300 Subject: [PATCH 13/13] code: cleanup --- .../dev/jdtech/jellyfin/PlayerActivity.kt | 2 +- .../fragments/EpisodeBottomSheetFragment.kt | 6 ++-- .../jellyfin/fragments/MovieFragment.kt | 6 ++-- .../res/xml/fragment_settings_downloads.xml | 1 - .../jdtech/jellyfin/models/VideoQuality.kt | 4 +-- .../jellyfin/repository/JellyfinRepository.kt | 2 ++ .../repository/JellyfinRepositoryImpl.kt | 4 +++ .../JellyfinRepositoryOfflineImpl.kt | 4 +++ .../java/dev/jdtech/jellyfin/SubtitleUtils.kt | 20 +++++++++++ .../viewmodels/PlayerActivityViewModel.kt | 33 ++++--------------- .../jellyfin/viewmodels/PlayerViewModel.kt | 20 +++-------- 11 files changed, 51 insertions(+), 51 deletions(-) create mode 100644 player/video/src/main/java/dev/jdtech/jellyfin/SubtitleUtils.kt diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index b7b4fb7c12..d9f2077081 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -353,7 +353,7 @@ class PlayerActivity : BasePlayerActivity() { private var selectedIndex = 1 // Default to "Original" (index 1) private fun showQualitySelectionDialog() { - val originalResolution = viewModel.getoriginalResolution() ?: 0// TODO: Rework getting originalResolution + val originalResolution = viewModel.getOriginalResolution() ?: 0 val qualityEntries = resources.getStringArray(CoreR.array.quality_entries).toList() val qualityValues = resources.getStringArray(CoreR.array.quality_values).toList() diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt index 83d134077b..a2245399ab 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/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 @@ -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/dev/jdtech/jellyfin/fragments/MovieFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt index a70d445652..abfa1f94a4 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/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 @@ -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/core/src/main/res/xml/fragment_settings_downloads.xml b/core/src/main/res/xml/fragment_settings_downloads.xml index f0d0aa531d..295a08a99f 100644 --- a/core/src/main/res/xml/fragment_settings_downloads.xml +++ b/core/src/main/res/xml/fragment_settings_downloads.xml @@ -9,7 +9,6 @@ android:defaultValue="false" app:key="pref_downloads_roaming" app:title="@string/download_roaming" /> - suspend fun stopEncodingProcess(playSessionId: String) + + suspend fun getAccessToken(): String? } diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index a084734945..43c65951d9 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -786,4 +786,8 @@ class JellyfinRepositoryImpl( playSessionId = playSessionId, ) } + + override suspend fun getAccessToken(): String? { + return jellyfinApi.api.accessToken + } } diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt index dcc4a39e1b..2bc0a7dbd9 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt @@ -372,4 +372,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/dev/jdtech/jellyfin/SubtitleUtils.kt b/player/video/src/main/java/dev/jdtech/jellyfin/SubtitleUtils.kt new file mode 100644 index 0000000000..1403ac2c70 --- /dev/null +++ b/player/video/src/main/java/dev/jdtech/jellyfin/SubtitleUtils.kt @@ -0,0 +1,20 @@ +package dev.jdtech.jellyfin + +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/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt index ed808c498e..ec9ab0340c 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -14,22 +14,20 @@ import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata -import androidx.media3.common.MimeTypes import androidx.media3.common.Player import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.TrackSelectionParameters -import androidx.media3.common.VideoSize import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import dagger.hilt.android.lifecycle.HiltViewModel import dev.jdtech.jellyfin.AppPreferences -import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.models.Intro import dev.jdtech.jellyfin.models.PlayerChapter import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.Trickplay import dev.jdtech.jellyfin.models.VideoQuality +import dev.jdtech.jellyfin.setSubtitlesMimeTypes import dev.jdtech.jellyfin.mpv.MPVPlayer import dev.jdtech.jellyfin.player.video.R import dev.jdtech.jellyfin.repository.JellyfinRepository @@ -57,7 +55,6 @@ class PlayerActivityViewModel constructor( private val application: Application, private val jellyfinRepository: JellyfinRepository, - private val jellyfinApi: JellyfinApi, private val appPreferences: AppPreferences, private val savedStateHandle: SavedStateHandle, ) : ViewModel(), Player.Listener { @@ -510,29 +507,13 @@ constructor( 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) +// Not sure if still needed 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 - "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 - } - ) + .setMimeType(setSubtitlesMimeTypes(mediaStream.codec)) .setLanguage(mediaStream.language.ifBlank { "Unknown" }) .setLabel("Embedded") .build() @@ -541,20 +522,20 @@ constructor( val allSubtitles = - if (VideoQuality.getOriginal(videoQuality)) { + if (VideoQuality.getIsOriginalQuality(videoQuality)) { externalSubtitles }else { embeddedSubtitles.apply { addAll(externalSubtitles) } } - val url = if (VideoQuality.getOriginal(videoQuality)){ + 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 = jellyfinApi.api.accessToken // TODO: add in repo + val apiKey = jellyfinRepository.getAccessToken() uriBuilder.appendQueryParameter("api_key",apiKey ) val newUri = uriBuilder.build() newUri.toString() @@ -591,7 +572,7 @@ constructor( } } - fun getoriginalResolution(): Int? { + fun getOriginalResolution(): Int? { return originalResolution } } diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt index 50a0fc1c8f..317367d057 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt @@ -18,6 +18,7 @@ import dev.jdtech.jellyfin.models.PlayerChapter import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.TrickplayInfo import dev.jdtech.jellyfin.repository.JellyfinRepository +import dev.jdtech.jellyfin.setSubtitlesMimeTypes import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch @@ -126,6 +127,7 @@ class PlayerViewModel @Inject internal constructor( .map { episode -> episode.toPlayerItem(mediaSourceIndex, playbackPosition) } } + private suspend fun FindroidItem.toPlayerItem( mediaSourceIndex: Int?, playbackPosition: Long, @@ -136,7 +138,7 @@ class PlayerViewModel @Inject internal constructor( } else { mediaSources[mediaSourceIndex] } - // Embedded Sub externally for offline prep next commit + // Embedded Sub externally for offline playback val externalSubtitles = if (mediaSource.type.toString() == "LOCAL" ) { mediaSource.mediaStreams .filter { mediaStream -> @@ -147,13 +149,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 { @@ -166,13 +162,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) ) } }