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 {