From ccc6788a02c9151917d500a06d17f4df67f8669e Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Fri, 19 Jul 2024 02:20:55 +0300 Subject: [PATCH] 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 e21c79b3..891170e8 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 00431e70..b136be35 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 e2f117a3..2b4380c0 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 995619d2..ae178c7b 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 0a78ec47..6901c09d 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 37b1ed42..e804df6e 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 9b3f76ff..50a0fc1c 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 {