feat: Embedded subtitle in transcoding stream / bugfixes: Download quality dialog loop / code: clean up

This commit is contained in:
nomadics9 2024-07-18 03:59:38 +03:00
parent 5a8403f6a9
commit c84ec082be
9 changed files with 278 additions and 270 deletions

View file

@ -172,59 +172,63 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
}else if (!appPreferences.downloadQualityDefault) {
createPickQualityDialog()
} 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
}
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()
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?) {
@ -424,7 +428,7 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
builder.setPositiveButton("Download") { dialog, _ ->
appPreferences.downloadQuality = selectedQuality
dialog.dismiss()
handleDownload()
download()
}
builder.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss()

View file

@ -209,61 +209,65 @@ class MovieFragment : Fragment() {
} else if (!appPreferences.downloadQualityDefault) {
createPickQualityDialog()
} 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
}
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()
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()
@ -502,8 +506,8 @@ class MovieFragment : Fragment() {
}
private fun createPickQualityDialog() {
val qualityEntries = resources.getStringArray(com.nomadics9.ananas.core.R.array.quality_entries)
val qualityValues = resources.getStringArray(com.nomadics9.ananas.core.R.array.quality_values)
val qualityEntries = resources.getStringArray(CoreR.array.quality_entries)
val qualityValues = resources.getStringArray(CoreR.array.quality_values)
val quality = appPreferences.downloadQuality
val currentQualityIndex = qualityValues.indexOf(quality)
var selectedQuality = quality
@ -516,8 +520,8 @@ class MovieFragment : Fragment() {
}
builder.setPositiveButton("Download") { dialog, _ ->
appPreferences.downloadQuality = selectedQuality
download()
dialog.dismiss()
handleDownload()
}
builder.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss()

View file

@ -240,8 +240,8 @@ class SeasonFragment : Fragment() {
}
builder.setPositiveButton("Download") { dialog, _ ->
appPreferences.downloadQuality = selectedQuality
dialog.dismiss()
onQualitySelected()
dialog.dismiss()
}
builder.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss()

View file

@ -6,10 +6,8 @@ import android.net.Uri
import android.os.Environment
import android.os.StatFs
import android.text.format.Formatter
import androidx.core.net.toFile
import androidx.core.net.toUri
import com.nomadics9.ananas.AppPreferences
import com.nomadics9.ananas.api.JellyfinApi
import com.nomadics9.ananas.database.ServerDatabaseDao
import com.nomadics9.ananas.models.FindroidEpisode
import com.nomadics9.ananas.models.FindroidItem
@ -29,34 +27,9 @@ import com.nomadics9.ananas.models.toFindroidSourceDto
import com.nomadics9.ananas.models.toFindroidTrickplayInfoDto
import com.nomadics9.ananas.models.toFindroidUserDataDto
import com.nomadics9.ananas.repository.JellyfinRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.extensions.dynamicHlsApi
import org.jellyfin.sdk.api.client.extensions.videosApi
import org.jellyfin.sdk.model.api.ClientCapabilitiesDto
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.MediaStreamProtocol
import org.jellyfin.sdk.model.api.PlaybackInfoDto
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.SubtitleDeliveryMethod
import org.jellyfin.sdk.model.api.SubtitleProfile
import org.jellyfin.sdk.model.api.TranscodeSeekInfo
import org.jellyfin.sdk.model.api.TranscodingProfile
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.net.URL
import java.util.UUID
import kotlin.Exception
import kotlin.math.ceil
@ -419,7 +392,8 @@ class DownloaderImpl(
val playbackInfo = jellyfinRepository.getPostedPlaybackInfo(itemId,false,deviceProfile,maxBitrate)
val mediaSourceId = playbackInfo.content.mediaSources.firstOrNull()?.id!!
val playSessionId = playbackInfo.content.playSessionId!!
val downloadUrl = jellyfinRepository.getVideoStreambyContainerUrl(itemId, mediaSourceId, playSessionId, maxBitrate, "ts")
val deviceId = jellyfinRepository.getDeviceId()
val downloadUrl = jellyfinRepository.getVideoStreambyContainerUrl(itemId, deviceId, mediaSourceId, playSessionId, maxBitrate, "ts")
val transcodeUri = buildTranscodeUri(downloadUrl, maxBitrate, quality)
Timber.d("Constructed Transcode URL: $transcodeUri")

View file

@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.Flow
import org.jellyfin.sdk.api.client.Response
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.DeviceInfo
import org.jellyfin.sdk.model.api.DeviceInfoQueryResult
import org.jellyfin.sdk.model.api.DeviceProfile
import org.jellyfin.sdk.model.api.EncodingContext
@ -86,7 +87,7 @@ interface JellyfinRepository {
suspend fun getMediaSources(itemId: UUID, includePath: Boolean = false): List<FindroidSource>
suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String): String
suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String, playSessionId: String? = null): String
suspend fun getSegmentsTimestamps(itemId: UUID): List<FindroidSegment>?
@ -124,7 +125,9 @@ interface JellyfinRepository {
suspend fun buildDeviceProfile(maxBitrate: Int, container: String, context: EncodingContext): DeviceProfile
suspend fun getVideoStreambyContainerUrl(itemId: UUID, mediaSourceId: String, playSessionId: String, videoBitrate: Int, container: String): String
suspend fun getVideoStreambyContainerUrl(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int, container: String): String
suspend fun getTranscodedVideoStream(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int): String
suspend fun getPostedPlaybackInfo(itemId: UUID, enableDirectStream: Boolean, deviceProfile: DeviceProfile ,maxBitrate: Int): Response<PlaybackInfoResponse>

View file

@ -30,6 +30,7 @@ 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
@ -56,6 +57,7 @@ import org.jellyfin.sdk.model.api.PublicSystemInfo
import org.jellyfin.sdk.model.api.SortOrder
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
import org.jellyfin.sdk.model.api.SubtitleProfile
import org.jellyfin.sdk.model.api.TranscodeReason
import org.jellyfin.sdk.model.api.TranscodeSeekInfo
import org.jellyfin.sdk.model.api.TranscodingProfile
import org.jellyfin.sdk.model.api.UserConfiguration
@ -335,14 +337,27 @@ 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,
)
val url = if (playSessionId != null) {
jellyfinApi.videosApi.getVideoStreamUrl(
itemId,
static = true,
mediaSourceId = mediaSourceId,
playSessionId = playSessionId,
deviceId = getDeviceId(),
context = EncodingContext.STATIC
)
} else {
jellyfinApi.videosApi.getVideoStreamUrl(
itemId,
static = true,
mediaSourceId = mediaSourceId,
deviceId = getDeviceId(),
)
}
url
} catch (e: Exception) {
Timber.e(e)
""
@ -584,7 +599,7 @@ class JellyfinRepositoryImpl(
720 -> 2000000 to 384000 // Adjusted for 720p
480 -> 1000000 to 384000 // Adjusted for 480p
360 -> 800000 to 128000 // Adjusted for 360p
else -> 8000000 to 384000
else -> 12000000 to 384000
}
}
@ -660,10 +675,11 @@ class JellyfinRepositoryImpl(
return playbackInfo
}
override suspend fun getVideoStreambyContainerUrl(itemId: UUID, mediaSourceId: String, playSessionId: String, videoBitrate: Int, container: String): String {
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,
@ -673,18 +689,63 @@ class JellyfinRepositoryImpl(
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,
videoCodec = "hevc",
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 deviceId = jellyfinApi.devicesApi.getDevices(getUserId())
return deviceId.toString()
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 = getDeviceId(),
deviceId = deviceId,
playSessionId = playSessionId
)
}

View file

@ -1,6 +1,7 @@
package com.nomadics9.ananas.repository
import android.content.Context
import android.devicelock.DeviceId
import androidx.paging.PagingData
import com.nomadics9.ananas.AppPreferences
import com.nomadics9.ananas.api.JellyfinApi
@ -26,6 +27,7 @@ import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.Response
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.DeviceInfo
import org.jellyfin.sdk.model.api.DeviceProfile
import org.jellyfin.sdk.model.api.EncodingContext
import org.jellyfin.sdk.model.api.ItemFields
@ -177,7 +179,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")
}
@ -304,6 +306,7 @@ class JellyfinRepositoryOfflineImpl(
override suspend fun getVideoStreambyContainerUrl(
itemId: UUID,
deviceId: String,
mediaSourceId: String,
playSessionId: String,
videoBitrate: Int,
@ -312,6 +315,16 @@ class JellyfinRepositoryOfflineImpl(
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,

View file

@ -6,6 +6,7 @@ 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
@ -13,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
@ -40,22 +42,8 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.extensions.hlsSegmentApi
import org.jellyfin.sdk.model.api.ClientCapabilitiesDto
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.MediaStreamProtocol
import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.api.PlaybackInfoDto
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.SubtitleDeliveryMethod
import org.jellyfin.sdk.model.api.SubtitleProfile
import org.jellyfin.sdk.model.api.TranscodeSeekInfo
import org.jellyfin.sdk.model.api.TranscodingProfile
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
@ -68,6 +56,7 @@ constructor(
private val application: Application,
private val jellyfinRepository: JellyfinRepository,
private val appPreferences: AppPreferences,
private val jellyfinApi: JellyfinApi,
private val savedStateHandle: SavedStateHandle,
) : ViewModel(), Player.Listener {
val player: Player
@ -530,13 +519,12 @@ constructor(
"480p - 1Mbps" -> 480
"360p - 800kbps" -> 360
"Auto" -> 1
else -> 1
else -> 1080
}
}
fun changeVideoQuality(quality: String) {
val mediaId = player.currentMediaItem?.mediaId ?: return
val itemId = UUID.fromString(mediaId)
val currentItem = items.firstOrNull { it.itemId.toString() == mediaId } ?: return
val currentPosition = player.currentPosition
@ -546,137 +534,97 @@ constructor(
val (videoBitRate, audioBitRate) = jellyfinRepository.getVideoTranscodeBitRate(
transcodingResolution
)
val deviceProfile = jellyfinRepository.buildDeviceProfile(videoBitRate, "ts", EncodingContext.STREAMING)
val playbackInfo = jellyfinRepository.getPostedPlaybackInfo(itemId,true,deviceProfile,videoBitRate)
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 mediaSource = playbackInfo.content.mediaSources.firstOrNull()
if (mediaSource == null) {
Timber.e("Media source is null")
} else {
Timber.d("Media source found: $mediaSource")
}
val transcodingUrl = mediaSource!!.transcodingUrl
val mediaSubtitles = currentItem.externalSubtitles.map { externalSubtitle ->
val mediaSources = jellyfinRepository.getMediaSources(currentItem.itemId, true)
val externalSubtitles = currentItem.externalSubtitles.map { externalSubtitle ->
MediaItem.SubtitleConfiguration.Builder(externalSubtitle.uri)
.setLabel(externalSubtitle.title.ifBlank { application.getString(R.string.external) })
.setLanguage(externalSubtitle.language.ifBlank { "Unknown" })
.setMimeType(externalSubtitle.mimeType)
.build()
}
// TODO: Embedded sub support
// val embeddedSubtitles = mediaSource?.mediaStreams
// ?.filter { it.type == MediaStreamType.SUBTITLE && !it.isExternal }
// ?.map { mediaStream ->
// MediaItem.SubtitleConfiguration.Builder(Uri.parse(mediaStream.deliveryUrl!!))
// .setMimeType(
// when (mediaStream.codec) {
// "subrip" -> MimeTypes.APPLICATION_SUBRIP
// "webvtt" -> MimeTypes.APPLICATION_SUBRIP
// "ass" -> MimeTypes.TEXT_SSA
// else -> MimeTypes.TEXT_UNKNOWN
// }
// )
// .setLanguage(mediaStream.language ?: "und")
// .setLabel(mediaStream.title ?: "Embedded Subtitle")
// .build()
// }
// ?.toMutableList() ?: mutableListOf()
// val allSubtitles = embeddedSubtitles.apply { addAll(mediaSubtitles) }
val baseUrl = jellyfinRepository.getBaseUrl()
val cleanBaseUrl = baseUrl.removePrefix("http://").removePrefix("https://")
val staticUrl = jellyfinRepository.getStreamUrl(itemId, currentItem.mediaSourceId)
val uri =
Uri.parse(transcodingUrl).buildUpon()
.scheme("https")
.authority(cleanBaseUrl)
.build()
fun Uri.Builder.setOrReplaceQueryParameter(
name: String,
value: String
): Uri.Builder {
val currentQueryParams = this.build().queryParameterNames
// Create a new builder for the URI
val newBuilder = Uri.parse(this.build().toString()).buildUpon()
// Track if the parameter was replaced
var parameterReplaced = false
// Re-add all parameters
currentQueryParams.forEach { param ->
val paramValue = this.build().getQueryParameter(param)
if (param == name) {
// Replace the parameter value
parameterReplaced = true
newBuilder.appendQueryParameter(name, value)
} else {
// Append the existing parameter
newBuilder.appendQueryParameter(param, paramValue)
}
val embeddedSubtitles = mediaSources[currentMediaItemIndex].mediaStreams
.filter { it.type == MediaStreamType.SUBTITLE && !it.isExternal && it.path != null }
.map { mediaStream ->
val test = mediaStream.codec
Timber.d("Deliver: %s", test)
var deliveryUrl = mediaStream.path
Timber.d("Deliverurl: %s", deliveryUrl)
if (mediaStream.codec == "webvtt") {
deliveryUrl = deliveryUrl?.replace("Stream.srt", "Stream.vtt")}
MediaItem.SubtitleConfiguration.Builder(Uri.parse(deliveryUrl))
.setMimeType(
when (mediaStream.codec) {
"subrip" -> MimeTypes.APPLICATION_SUBRIP
"webvtt" -> MimeTypes.TEXT_VTT
"ssa" -> MimeTypes.TEXT_SSA
"pgs" -> MimeTypes.APPLICATION_PGS
"ass" -> MimeTypes.TEXT_SSA // ASS is a subtitle format that is essentially an extension of SSA
"srt" -> MimeTypes.APPLICATION_SUBRIP // SRT is another common name for SubRip
"vtt" -> MimeTypes.TEXT_VTT // VTT is a common extension for WebVTT
"ttml" -> MimeTypes.APPLICATION_TTML // TTML (Timed Text Markup Language)
"dfxp" -> MimeTypes.APPLICATION_TTML // DFXP is a profile of TTML
"stl" -> MimeTypes.APPLICATION_TTML // EBU STL (Subtitling Data Exchange Format)
"sbv" -> MimeTypes.APPLICATION_SUBRIP // YouTube's SBV format is similar to SubRip
else -> MimeTypes.TEXT_UNKNOWN
}
)
.setLanguage(mediaStream.language?.ifBlank { "Unknown" })
.setLabel("Embedded")
.build()
}
.toMutableList()
// Append the new parameter only if it wasn't replaced
if (!parameterReplaced) {
newBuilder.appendQueryParameter(name, value)
}
return newBuilder
}
val allSubtitles = embeddedSubtitles.apply { addAll(externalSubtitles) }
val uriBuilder = uri.buildUpon()
//.setOrReplaceQueryParameter("PlaySessionId", playSessionId!!)
if (transcodingResolution == 1) {
uriBuilder.setOrReplaceQueryParameter("EnableAdaptiveBitrateStreaming", "true")
uriBuilder.setOrReplaceQueryParameter("Static", "false")
uriBuilder.appendQueryParameter("MaxVideoHeight","1080" )
} else if (transcodingResolution == 720 || transcodingResolution == 480 || transcodingResolution == 360) {
uriBuilder.setOrReplaceQueryParameter(
"MaxVideoBitRate",
videoBitRate.toString()
)
uriBuilder.setOrReplaceQueryParameter("VideoBitrate", videoBitRate.toString())
uriBuilder.setOrReplaceQueryParameter("AudioBitrate", audioBitRate.toString())
uriBuilder.setOrReplaceQueryParameter("Static", "false")
uriBuilder.appendQueryParameter("PlaySessionId", playSessionId)
uriBuilder.appendQueryParameter(
"MaxVideoHeight",
transcodingResolution.toString()
)
uriBuilder.appendQueryParameter("subtitleMethod", "External")
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
uriBuilder.appendQueryParameter("api_key",apiKey )
val newUri = uriBuilder.build()
newUri.toString()
}
val newUri = uriBuilder.build()
Timber.e("URI IS %s", newUri)
Timber.e("URI IS %s", url)
val mediaItemBuilder = MediaItem.Builder()
.setMediaId(currentItem.itemId.toString())
if (transcodingResolution == 1080) {
mediaItemBuilder.setUri(staticUrl)
} else {
mediaItemBuilder.setUri(newUri)
}
.setUri(url)
.setSubtitleConfigurations(allSubtitles)
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle(currentItem.name)
.build(),
)
.setSubtitleConfigurations(mediaSubtitles)
player.pause()
player.setMediaItem(mediaItemBuilder.build())
player.prepare()
player.seekTo(currentPosition)
playWhenReady = true
player.play()
val originalHeight = mediaSource.mediaStreams
?.firstOrNull { it.type == MediaStreamType.VIDEO }?.height ?: -1
val originalHeight = mediaSources[currentMediaItemIndex].mediaStreams
.filter { it.type == MediaStreamType.VIDEO }
.map {mediaStream -> mediaStream.height}.first() ?: 1080
// Store the original height
this@PlayerActivityViewModel.originalHeight = originalHeight

View file

@ -150,8 +150,9 @@ constructor(
val downloadQualityDefault get() = sharedPreferences.getBoolean(
Constants.PREF_DOWNLOADS_QUALITY_DEFAULT,
false
)
false,
)
// Sorting
var sortBy: String