diff --git a/app/src/main/java/dev/jdtech/jellyfin/models/ExternalSubtitle.kt b/app/src/main/java/dev/jdtech/jellyfin/models/ExternalSubtitle.kt new file mode 100644 index 00000000..99d35c60 --- /dev/null +++ b/app/src/main/java/dev/jdtech/jellyfin/models/ExternalSubtitle.kt @@ -0,0 +1,13 @@ +package dev.jdtech.jellyfin.models + +import android.net.Uri +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +class ExternalSubtitle( + val title: String, + val language: String, + val uri: Uri, + val mimeType: String, +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt b/app/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt index 52e250b4..dfa08e35 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt @@ -13,5 +13,6 @@ data class PlayerItem( val mediaSourceUri: String = "", val parentIndexNumber: Int? = null, val indexNumber: Int? = null, - val item: DownloadItem? = null + val item: DownloadItem? = null, + val externalSubtitles: List = emptyList() ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/mpv/MPVPlayer.kt b/app/src/main/java/dev/jdtech/jellyfin/mpv/MPVPlayer.kt index d8a6c763..163a8671 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/mpv/MPVPlayer.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/mpv/MPVPlayer.kt @@ -154,7 +154,6 @@ class MPVPlayer( // Internal state. private var internalMediaItems: List? = null - private var internalMediaItem: MediaItem? = null @Player.State private var playbackState: Int = Player.STATE_IDLE @@ -300,7 +299,6 @@ class MPVPlayer( if (!isPlayerReady) { isPlayerReady = true listeners.sendEvent(Player.EVENT_TRACKS_CHANGED) { listener -> - //listener.onTracksChanged(currentTrackGroups, currentTrackSelections) listener.onTracksInfoChanged(currentTracksInfo) } seekTo(C.TIME_UNSET) @@ -840,7 +838,6 @@ class MPVPlayer( private fun prepareMediaItem(index: Int) { internalMediaItems?.get(index)?.let { mediaItem -> - internalMediaItem = mediaItem resetInternalState() mediaItem.localConfiguration?.subtitleConfigurations?.forEach { subtitle -> initialCommands.add( diff --git a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt index f7375c60..e84dd855 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt @@ -74,4 +74,6 @@ interface JellyfinRepository { suspend fun markAsUnplayed(itemId: UUID) suspend fun getIntros(itemId: UUID): List + + fun getBaseUrl(): String } \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index c98eb613..c702ff8e 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -159,13 +159,16 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep codecProfiles = emptyList(), containerProfiles = emptyList(), directPlayProfiles = listOf( - DirectPlayProfile( - type = DlnaProfileType.VIDEO - ), DirectPlayProfile(type = DlnaProfileType.AUDIO) + DirectPlayProfile(type = DlnaProfileType.VIDEO), + DirectPlayProfile(type = DlnaProfileType.AUDIO) ), transcodingProfiles = emptyList(), responseProfiles = emptyList(), - subtitleProfiles = emptyList(), + subtitleProfiles = listOf( + SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL), + SubtitleProfile("vtt", SubtitleDeliveryMethod.EXTERNAL), + SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL), + ), xmlRootAttributes = emptyList(), supportedMediaTypes = "", enableAlbumArtInDidl = false, @@ -179,9 +182,6 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep requiresPlainVideoItems = false, timelineOffsetSeconds = 0 ), - startTimeTicks = null, - audioStreamIndex = null, - subtitleStreamIndex = null, maxStreamingBitrate = 1_000_000_000, ) ).content.mediaSources @@ -286,4 +286,6 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep override suspend fun getIntros(itemId: UUID): List = withContext(Dispatchers.IO) { jellyfinApi.userLibraryApi.getIntros(jellyfinApi.userId!!, itemId).content.items.orEmpty() } -} \ No newline at end of file + + override fun getBaseUrl() = jellyfinApi.api.baseUrl.orEmpty() +} diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt index fdf2d005..5eedb5b8 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -108,6 +108,13 @@ constructor( item.mediaSourceUri.isNotEmpty() -> item.mediaSourceUri else -> jellyfinRepository.getStreamUrl(item.itemId, item.mediaSourceId) } + val mediaSubtitles = item.externalSubtitles.map { externalSubtitle -> + MediaItem.SubtitleConfiguration.Builder(externalSubtitle.uri) + .setLabel(externalSubtitle.title) + .setMimeType(externalSubtitle.mimeType) + .setLanguage(externalSubtitle.language) + .build() + } playFromDownloads = item.mediaSourceUri.isNotEmpty() Timber.d("Stream url: $streamUrl") @@ -115,6 +122,7 @@ constructor( MediaItem.Builder() .setMediaId(item.itemId.toString()) .setUri(streamUrl) + .setSubtitleConfigurations(mediaSubtitles) .build() mediaItems.add(mediaItem) } diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt index c0b401ef..415eebc4 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt @@ -1,10 +1,13 @@ package dev.jdtech.jellyfin.viewmodels +import android.net.Uri import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.google.android.exoplayer2.util.MimeTypes import dagger.hilt.android.lifecycle.HiltViewModel import dev.jdtech.jellyfin.database.DownloadDatabaseDao +import dev.jdtech.jellyfin.models.ExternalSubtitle import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.utils.getDownloadPlayerItem @@ -17,6 +20,7 @@ import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.LocationType.VIRTUAL import org.jellyfin.sdk.model.api.MediaProtocol +import org.jellyfin.sdk.model.api.MediaStreamType import timber.log.Timber import javax.inject.Inject @@ -105,7 +109,7 @@ class PlayerViewModel @Inject internal constructor( else -> emptyList() } - private fun itemToMoviePlayerItems( + private suspend fun itemToMoviePlayerItems( item: BaseItemDto, playbackPosition: Long, mediaSourceIndex: Int @@ -160,11 +164,37 @@ class PlayerViewModel @Inject internal constructor( .map { episode -> episode.toPlayerItem(mediaSourceIndex, playbackPosition) } } - private fun BaseItemDto.toPlayerItem( + private suspend fun BaseItemDto.toPlayerItem( mediaSourceIndex: Int, playbackPosition: Long ): PlayerItem { - val mediaSource = mediaSources!![mediaSourceIndex] + val mediaSource = repository.getMediaSources(id)[mediaSourceIndex] + val externalSubtitles = mutableListOf() + for (mediaStream in mediaSource.mediaStreams!!) { + if (mediaStream.isExternal && mediaStream.type == MediaStreamType.SUBTITLE && !mediaStream.deliveryUrl.isNullOrBlank()) { + + // Temp fix for vtt + // Jellyfin returns a srt stream when it should return vtt stream. + var deliveryUrl = mediaStream.deliveryUrl!! + if (mediaStream.codec == "webvtt") { + deliveryUrl = deliveryUrl.replace("Stream.srt", "Stream.vtt") + } + + externalSubtitles.add( + ExternalSubtitle( + mediaStream.title.orEmpty(), + mediaStream.language.orEmpty(), + Uri.parse(repository.getBaseUrl() + deliveryUrl), + when (mediaStream.codec) { + "subrip" -> MimeTypes.APPLICATION_SUBRIP + "webvtt" -> MimeTypes.TEXT_VTT + "ass" -> MimeTypes.TEXT_SSA + else -> MimeTypes.TEXT_UNKNOWN + } + ) + ) + } + } return when (mediaSource.protocol) { MediaProtocol.FILE -> PlayerItem( name = name, @@ -172,7 +202,8 @@ class PlayerViewModel @Inject internal constructor( mediaSourceId = mediaSource.id!!, playbackPosition = playbackPosition, parentIndexNumber = parentIndexNumber, - indexNumber = indexNumber + indexNumber = indexNumber, + externalSubtitles = externalSubtitles ) MediaProtocol.HTTP -> PlayerItem( name = name, @@ -181,7 +212,8 @@ class PlayerViewModel @Inject internal constructor( mediaSourceUri = mediaSource.path!!, playbackPosition = playbackPosition, parentIndexNumber = parentIndexNumber, - indexNumber = indexNumber + indexNumber = indexNumber, + externalSubtitles = externalSubtitles ) else -> PlayerItem( name = name, @@ -189,7 +221,8 @@ class PlayerViewModel @Inject internal constructor( mediaSourceId = mediaSource.id!!, playbackPosition = playbackPosition, parentIndexNumber = parentIndexNumber, - indexNumber = indexNumber + indexNumber = indexNumber, + externalSubtitles = externalSubtitles ) } }