From 25ac5524d733e31a37b2fecdb48c331329898395 Mon Sep 17 00:00:00 2001 From: jarnedemeulemeester Date: Thu, 26 Aug 2021 15:36:56 +0200 Subject: [PATCH] Rework how player items are created Add support for intros and improve loading speed --- .../dev/jdtech/jellyfin/PlayerActivity.kt | 2 +- .../dialogs/VideoVersionDialogFragment.kt | 6 +- .../fragments/EpisodeBottomSheetFragment.kt | 21 ++-- .../jellyfin/fragments/MediaInfoFragment.kt | 35 ++---- .../dev/jdtech/jellyfin/models/PlayerItem.kt | 3 +- .../jellyfin/repository/JellyfinRepository.kt | 2 + .../repository/JellyfinRepositoryImpl.kt | 10 ++ .../viewmodels/EpisodeBottomSheetViewModel.kt | 33 +++++- .../jellyfin/viewmodels/MediaInfoViewModel.kt | 101 ++++++++++++------ .../viewmodels/PlayerActivityViewModel.kt | 33 +++--- .../main/res/navigation/main_navigation.xml | 3 - 11 files changed, 160 insertions(+), 89 deletions(-) diff --git a/app/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index f4ad3546..ea3ad3ad 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -38,7 +38,7 @@ class PlayerActivity : AppCompatActivity() { }) if (viewModel.player.value == null) { - viewModel.initializePlayer(args.items, args.playbackPosition) + viewModel.initializePlayer(args.items) } hideSystemUI() } diff --git a/app/src/main/java/dev/jdtech/jellyfin/dialogs/VideoVersionDialogFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/dialogs/VideoVersionDialogFragment.kt index 912d4074..84caf7bf 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/dialogs/VideoVersionDialogFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/dialogs/VideoVersionDialogFragment.kt @@ -11,12 +11,12 @@ class VideoVersionDialogFragment( private val viewModel: MediaInfoViewModel ) : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val items = viewModel.mediaSources.value!!.map { it.name } + val items = viewModel.item.value?.mediaSources?.map { it.name } return activity?.let { val builder = AlertDialog.Builder(it) builder.setTitle("Select a version") - .setItems(items.toTypedArray()) { _, which -> - viewModel.navigateToPlayer(viewModel.mediaSources.value!![which]) + .setItems(items?.toTypedArray()) { _, which -> + viewModel.preparePlayerItems(which) } builder.create() } ?: throw IllegalStateException("Activity cannot be null") diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt index a6535000..e4fadaf3 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt @@ -91,10 +91,14 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { if (it) { navigateToPlayerActivity( viewModel.playerItems.toTypedArray(), - viewModel.item.value!!.userData!!.playbackPositionTicks.div(10000) ) viewModel.doneNavigateToPlayer() - binding.playButton.setImageDrawable(ContextCompat.getDrawable(requireActivity(), R.drawable.ic_play)) + binding.playButton.setImageDrawable( + ContextCompat.getDrawable( + requireActivity(), + R.drawable.ic_play + ) + ) binding.progressCircular.visibility = View.INVISIBLE } }) @@ -102,7 +106,12 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { viewModel.playerItemsError.observe(viewLifecycleOwner, { errorMessage -> if (errorMessage != null) { binding.playerItemsError.visibility = View.VISIBLE - binding.playButton.setImageDrawable(ContextCompat.getDrawable(requireActivity(), R.drawable.ic_play)) + binding.playButton.setImageDrawable( + ContextCompat.getDrawable( + requireActivity(), + R.drawable.ic_play + ) + ) binding.progressCircular.visibility = View.INVISIBLE } else { binding.playerItemsError.visibility = View.GONE @@ -110,7 +119,9 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { }) binding.playerItemsErrorDetails.setOnClickListener { - ErrorDialogFragment(viewModel.playerItemsError.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog") + ErrorDialogFragment( + viewModel.playerItemsError.value ?: getString(R.string.unknown_error) + ).show(parentFragmentManager, "errordialog") } viewModel.loadEpisode(args.episodeId) @@ -120,12 +131,10 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { private fun navigateToPlayerActivity( playerItems: Array, - playbackPosition: Long ) { findNavController().navigate( EpisodeBottomSheetFragmentDirections.actionEpisodeBottomSheetFragmentToPlayerActivity( playerItems, - playbackPosition ) ) } diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt index b02f3991..4bb4ffd2 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt @@ -63,7 +63,10 @@ class MediaInfoFragment : Fragment() { } binding.errorLayout.errorDetailsButton.setOnClickListener { - ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog") + ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show( + parentFragmentManager, + "errordialog" + ) } viewModel.item.observe(viewLifecycleOwner, { item -> @@ -91,8 +94,7 @@ class MediaInfoFragment : Fragment() { viewModel.navigateToPlayer.observe(viewLifecycleOwner, { playerItems -> if (playerItems != null) { navigateToPlayerActivity( - playerItems, - viewModel.item.value!!.userData!!.playbackPositionTicks.div(10000) + playerItems ) viewModel.doneNavigatingToPlayer() binding.playButton.setImageDrawable( @@ -139,7 +141,9 @@ class MediaInfoFragment : Fragment() { }) binding.playerItemsErrorDetails.setOnClickListener { - ErrorDialogFragment(viewModel.playerItemsError.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog") + ErrorDialogFragment( + viewModel.playerItemsError.value ?: getString(R.string.unknown_error) + ).show(parentFragmentManager, "errordialog") } binding.trailerButton.setOnClickListener { @@ -164,29 +168,14 @@ class MediaInfoFragment : Fragment() { binding.playButton.setImageResource(android.R.color.transparent) binding.progressCircular.visibility = View.VISIBLE if (args.itemType == "Movie") { - if (!viewModel.mediaSources.value.isNullOrEmpty()) { - if (viewModel.mediaSources.value!!.size > 1) { + if (viewModel.item.value?.mediaSources != null) { + if (viewModel.item.value?.mediaSources?.size!! > 1) { VideoVersionDialogFragment(viewModel).show( parentFragmentManager, "videoversiondialog" ) } else { - navigateToPlayerActivity( - arrayOf( - PlayerItem( - args.itemId, - viewModel.mediaSources.value!![0].id!! - ) - ), - viewModel.item.value!!.userData!!.playbackPositionTicks.div(10000), - ) - binding.playButton.setImageDrawable( - ContextCompat.getDrawable( - requireActivity(), - R.drawable.ic_play - ) - ) - binding.progressCircular.visibility = View.INVISIBLE + viewModel.preparePlayerItems() } } } else if (args.itemType == "Series") { @@ -232,12 +221,10 @@ class MediaInfoFragment : Fragment() { private fun navigateToPlayerActivity( playerItems: Array, - playbackPosition: Long, ) { findNavController().navigate( MediaInfoFragmentDirections.actionMediaInfoFragmentToPlayerActivity( playerItems, - playbackPosition ) ) } 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 0f6852d4..726d62a3 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt @@ -7,5 +7,6 @@ import java.util.* @Parcelize data class PlayerItem( val itemId: UUID, - val mediaSourceId: String + val mediaSourceId: String, + val playbackPosition: Long ) : Parcelable \ No newline at end of file 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 526d0745..d99de89b 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt @@ -54,4 +54,6 @@ interface JellyfinRepository { suspend fun markAsPlayed(itemId: UUID) suspend fun markAsUnplayed(itemId: UUID) + + suspend fun getIntros(itemId: UUID): List } \ 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 4dc70436..c206fb53 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -266,4 +266,14 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep jellyfinApi.playStateApi.markUnplayedItem(jellyfinApi.userId!!, itemId) } } + + override suspend fun getIntros(itemId: UUID): List { + val intros: List + withContext(Dispatchers.IO) { + intros = + jellyfinApi.userLibraryApi.getIntros(jellyfinApi.userId!!, itemId).content.items + ?: listOf() + } + return intros + } } \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt index 383deb1d..1ee2c478 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt @@ -10,6 +10,7 @@ import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.repository.JellyfinRepository import kotlinx.coroutines.launch import org.jellyfin.sdk.model.api.BaseItemDto +import org.jellyfin.sdk.model.api.ItemFields import timber.log.Timber import java.text.DateFormat import java.time.ZoneOffset @@ -74,17 +75,39 @@ constructor( } private suspend fun createPlayerItems(startEpisode: BaseItemDto) { + playerItems.clear() + + val playbackPosition = startEpisode.userData?.playbackPositionTicks?.div(10000) ?: 0 + // Intros + var introsCount = 0 + + if (playbackPosition <= 0) { + val intros = jellyfinRepository.getIntros(startEpisode.id) + for (intro in intros) { + if (intro.mediaSources.isNullOrEmpty()) continue + playerItems.add(PlayerItem(intro.id, intro.mediaSources?.get(0)?.id!!, 0)) + introsCount += 1 + } + } + val episodes = jellyfinRepository.getEpisodes( startEpisode.seriesId!!, startEpisode.seasonId!!, - startItemId = startEpisode.id + startItemId = startEpisode.id, + fields = listOf(ItemFields.MEDIA_SOURCES) ) for (episode in episodes) { - val mediaSources = jellyfinRepository.getMediaSources(episode.id) - if (mediaSources.isEmpty()) continue - playerItems.add(PlayerItem(episode.id, mediaSources[0].id!!)) + if (episode.mediaSources.isNullOrEmpty()) continue + playerItems.add( + PlayerItem( + episode.id, + episode.mediaSources?.get(0)?.id!!, + playbackPosition + ) + ) } - if (playerItems.isEmpty()) throw Exception("No playable items found") + + if (playerItems.isEmpty() || playerItems.count() == introsCount) throw Exception("No playable items found") } fun markAsPlayed(itemId: UUID) { diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt index ac3a068d..975672f2 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemPerson -import org.jellyfin.sdk.model.api.MediaSourceInfo +import org.jellyfin.sdk.model.api.ItemFields import timber.log.Timber import java.util.* import javax.inject.Inject @@ -52,9 +52,6 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { private val _seasons = MutableLiveData>() val seasons: LiveData> = _seasons - private val _mediaSources = MutableLiveData>() - val mediaSources: LiveData> = _mediaSources - private val _navigateToPlayer = MutableLiveData>() val navigateToPlayer: LiveData> = _navigateToPlayer @@ -91,9 +88,6 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { _nextUp.value = getNextUp(itemId) _seasons.value = jellyfinRepository.getSeasons(itemId) } - if (itemType == "Movie") { - _mediaSources.value = jellyfinRepository.getMediaSources(itemId) - } } catch (e: Exception) { Timber.e(e) _error.value = e.toString() @@ -183,11 +177,11 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { } } - fun preparePlayerItems() { + fun preparePlayerItems(mediaSourceIndex: Int? = null) { _playerItemsError.value = null viewModelScope.launch { try { - createPlayerItems(_item.value!!) + createPlayerItems(_item.value!!, mediaSourceIndex) _navigateToPlayer.value = playerItems.toTypedArray() } catch (e: Exception) { _playerItemsError.value = e.message @@ -195,35 +189,76 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { } } - private suspend fun createPlayerItems(series: BaseItemDto) { - if (nextUp.value != null) { - val startEpisode = nextUp.value!! - val episodes = jellyfinRepository.getEpisodes( - startEpisode.seriesId!!, - startEpisode.seasonId!!, - startItemId = startEpisode.id - ) - for (episode in episodes) { - val mediaSources = jellyfinRepository.getMediaSources(episode.id) - if (mediaSources.isEmpty()) continue - playerItems.add(PlayerItem(episode.id, mediaSources[0].id!!)) + private suspend fun createPlayerItems(series: BaseItemDto, mediaSourceIndex: Int? = null) { + playerItems.clear() + + val playbackPosition = item.value?.userData?.playbackPositionTicks?.div(10000) ?: 0 + + // Intros + var introsCount = 0 + + if (playbackPosition <= 0) { + val intros = jellyfinRepository.getIntros(series.id) + for (intro in intros) { + if (intro.mediaSources.isNullOrEmpty()) continue + playerItems.add(PlayerItem(intro.id, intro.mediaSources?.get(0)?.id!!, 0)) + introsCount += 1 } - } else { - for (season in seasons.value!!) { - if (season.indexNumber == 0) continue - val episodes = jellyfinRepository.getEpisodes(series.id, season.id) - for (episode in episodes) { - val mediaSources = jellyfinRepository.getMediaSources(episode.id) - if (mediaSources.isEmpty()) continue - playerItems.add(PlayerItem(episode.id, mediaSources[0].id!!)) + } + + when (series.type) { + "Movie" -> { + playerItems.add( + PlayerItem( + series.id, + series.mediaSources?.get(mediaSourceIndex ?: 0)?.id!!, + playbackPosition + ) + ) + } + "Series" -> { + if (nextUp.value != null) { + val startEpisode = nextUp.value!! + val episodes = jellyfinRepository.getEpisodes( + startEpisode.seriesId!!, + startEpisode.seasonId!!, + startItemId = startEpisode.id, + fields = listOf(ItemFields.MEDIA_SOURCES) + ) + for (episode in episodes) { + if (episode.mediaSources.isNullOrEmpty()) continue + playerItems.add( + PlayerItem( + episode.id, + episode.mediaSources?.get(0)?.id!!, + 0 + ) + ) + } + } else { + for (season in seasons.value!!) { + if (season.indexNumber == 0) continue + val episodes = jellyfinRepository.getEpisodes( + series.id, + season.id, + fields = listOf(ItemFields.MEDIA_SOURCES) + ) + for (episode in episodes) { + if (episode.mediaSources.isNullOrEmpty()) continue + playerItems.add( + PlayerItem( + episode.id, + episode.mediaSources?.get(0)?.id!!, + 0 + ) + ) + } + } } } } - if (playerItems.isEmpty()) throw Exception("No playable items found") - } - fun navigateToPlayer(mediaSource: MediaSourceInfo) { - _navigateToPlayer.value = arrayOf(PlayerItem(item.value!!.id, mediaSource.id!!)) + if (playerItems.isEmpty() || playerItems.count() == introsCount) throw Exception("No playable items found") } fun doneNavigatingToPlayer() { 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 2b40efe4..53d9cfd7 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -39,8 +39,7 @@ constructor( private val sp = PreferenceManager.getDefaultSharedPreferences(application) fun initializePlayer( - items: Array, - playbackPosition: Long + items: Array ) { val renderersFactory = @@ -61,18 +60,22 @@ constructor( viewModelScope.launch { val mediaItems: MutableList = mutableListOf() - for (item in items) { - val streamUrl = jellyfinRepository.getStreamUrl(item.itemId, item.mediaSourceId) - Timber.d("Stream url: $streamUrl") - val mediaItem = - MediaItem.Builder() - .setMediaId(item.itemId.toString()) - .setUri(streamUrl) - .build() - mediaItems.add(mediaItem) + try { + for (item in items) { + val streamUrl = jellyfinRepository.getStreamUrl(item.itemId, item.mediaSourceId) + Timber.d("Stream url: $streamUrl") + val mediaItem = + MediaItem.Builder() + .setMediaId(item.itemId.toString()) + .setUri(streamUrl) + .build() + mediaItems.add(mediaItem) + } + } catch (e: Exception) { + Timber.e(e) } - player.setMediaItems(mediaItems, currentWindow, playbackPosition) + player.setMediaItems(mediaItems, currentWindow, items[0].playbackPosition) player.playWhenReady = playWhenReady player.prepare() _player.value = player @@ -131,7 +134,11 @@ constructor( override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { Timber.d("Playing MediaItem: ${mediaItem?.mediaId}") viewModelScope.launch { - jellyfinRepository.postPlaybackStart(UUID.fromString(mediaItem?.mediaId)) + try { + jellyfinRepository.postPlaybackStart(UUID.fromString(mediaItem?.mediaId)) + } catch (e: Exception) { + Timber.e(e) + } } } diff --git a/app/src/main/res/navigation/main_navigation.xml b/app/src/main/res/navigation/main_navigation.xml index 559933c0..aa8393ad 100644 --- a/app/src/main/res/navigation/main_navigation.xml +++ b/app/src/main/res/navigation/main_navigation.xml @@ -160,9 +160,6 @@ -