Refactor playback code (#55)

* Refactor playback code

* Fix back state when playing media and rotating device

Problem was playerItems were re-emitted on fragment creation after config change. LiveData by design emit on every subscribe (observe) so to avoid that there are several possibilities.

1) easiest, observe playerItems not in onCreate but in playButton.clickListener. Stupid, since then we need to remember to only observe in this special place.

2) SingleLiveData - kind of hacky since LiveData were designed to behave this way so we don't want to go against their design.

3) Use Kotlin flow instead.

I chose the flow approach since it's Kotlin native and modern way to do things and behaves much more Rx-like. Since now we need to call collect instead of observe and launch in coroutine, I added utility method to make this easier. Also, in the future we might want to improve this further, either by coming up with new way entirely or by at least moving this to parent fragment from which all fragments that want to play media will inherit and thus making it easy to use and maintain.

Co-authored-by: Jarne Demeulemeester <32322857+jarnedemeulemeester@users.noreply.github.com>
This commit is contained in:
lsrom 2021-10-26 18:11:22 +02:00 committed by GitHub
parent 28014eaadf
commit 308d97068f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 258 additions and 276 deletions

View file

@ -5,21 +5,24 @@ import android.os.Bundle
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
import org.jellyfin.sdk.model.api.BaseItemDto
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
import java.lang.IllegalStateException
class VideoVersionDialogFragment(
private val viewModel: MediaInfoViewModel
private val item: BaseItemDto,
private val viewModel: PlayerViewModel
) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val items = viewModel.item.value?.mediaSources?.map { it.name }
return activity?.let {
val builder = MaterialAlertDialogBuilder(it)
builder.setTitle(getString(R.string.select_a_version))
.setItems(items?.toTypedArray()) { _, which ->
viewModel.preparePlayerItems(which)
}
builder.create()
val items = item.mediaSources?.map { it.name }?.toTypedArray()
return activity?.let { activity ->
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.select_a_version)
.setItems(items) { _, which ->
viewModel.loadPlayerItems(item, which)
}.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
}

View file

@ -6,7 +6,9 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
@ -16,6 +18,8 @@ import dev.jdtech.jellyfin.databinding.EpisodeBottomSheetBinding
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.viewmodels.EpisodeBottomSheetViewModel
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
import timber.log.Timber
@AndroidEntryPoint
class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
@ -23,6 +27,7 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
private lateinit var binding: EpisodeBottomSheetBinding
private val viewModel: EpisodeBottomSheetViewModel by viewModels()
private val playerViewModel: PlayerViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
@ -37,7 +42,16 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
binding.playButton.setOnClickListener {
binding.playButton.setImageResource(android.R.color.transparent)
binding.progressCircular.visibility = View.VISIBLE
viewModel.preparePlayerItems()
viewModel.item.value?.let {
playerViewModel.loadPlayerItems(it)
}
}
playerViewModel.onPlaybackRequested(lifecycleScope) { playerItems ->
when (playerItems) {
is PlayerViewModel.PlayerItemError -> bindPlayerItemsError(playerItems)
is PlayerViewModel.PlayerItems -> bindPlayerItems(playerItems)
}
}
binding.checkButton.setOnClickListener {
@ -87,48 +101,38 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
binding.favoriteButton.setImageResource(drawable)
})
viewModel.navigateToPlayer.observe(viewLifecycleOwner, {
if (it) {
navigateToPlayerActivity(
viewModel.playerItems.toTypedArray(),
)
viewModel.doneNavigateToPlayer()
binding.playButton.setImageDrawable(
ContextCompat.getDrawable(
requireActivity(),
R.drawable.ic_play
)
)
binding.progressCircular.visibility = View.INVISIBLE
}
})
viewModel.playerItemsError.observe(viewLifecycleOwner, { errorMessage ->
if (errorMessage != null) {
binding.playerItemsError.visibility = View.VISIBLE
binding.playButton.setImageDrawable(
ContextCompat.getDrawable(
requireActivity(),
R.drawable.ic_play
)
)
binding.progressCircular.visibility = View.INVISIBLE
} else {
binding.playerItemsError.visibility = View.GONE
}
})
binding.playerItemsErrorDetails.setOnClickListener {
ErrorDialogFragment(
viewModel.playerItemsError.value ?: getString(R.string.unknown_error)
).show(parentFragmentManager, "errordialog")
}
viewModel.loadEpisode(args.episodeId)
return binding.root
}
private fun bindPlayerItems(items: PlayerViewModel.PlayerItems) {
navigateToPlayerActivity(items.items.toTypedArray())
binding.playButton.setImageDrawable(
ContextCompat.getDrawable(
requireActivity(),
R.drawable.ic_play
)
)
binding.progressCircular.visibility = View.INVISIBLE
}
private fun bindPlayerItemsError(error: PlayerViewModel.PlayerItemError) {
Timber.e(error.message)
binding.playerItemsError.isVisible = true
binding.playButton.setImageDrawable(
ContextCompat.getDrawable(
requireActivity(),
R.drawable.ic_play
)
)
binding.progressCircular.visibility = View.INVISIBLE
binding.playerItemsErrorDetails.setOnClickListener {
ErrorDialogFragment(error.message).show(parentFragmentManager, "errordialog")
}
}
private fun navigateToPlayerActivity(
playerItems: Array<PlayerItem>,
) {

View file

@ -10,6 +10,7 @@ import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import dagger.hilt.android.AndroidEntryPoint
@ -22,7 +23,9 @@ import dev.jdtech.jellyfin.dialogs.VideoVersionDialogFragment
import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
import org.jellyfin.sdk.model.api.BaseItemDto
import timber.log.Timber
import org.jellyfin.sdk.model.serializer.toUUID
import java.util.UUID
@ -31,6 +34,7 @@ class MediaInfoFragment : Fragment() {
private lateinit var binding: FragmentMediaInfoBinding
private val viewModel: MediaInfoViewModel by viewModels()
private val playerViewModel: PlayerViewModel by viewModels()
private val args: MediaInfoFragmentArgs by navArgs()
@ -65,13 +69,6 @@ class MediaInfoFragment : Fragment() {
viewModel.loadData(args.itemId, args.itemType)
}
binding.errorLayout.errorDetailsButton.setOnClickListener {
ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(
parentFragmentManager,
"errordialog"
)
}
viewModel.item.observe(viewLifecycleOwner, { item ->
if (item.originalTitle != item.name) {
binding.originalTitle.visibility = View.VISIBLE
@ -94,21 +91,12 @@ class MediaInfoFragment : Fragment() {
}
})
viewModel.navigateToPlayer.observe(viewLifecycleOwner, { playerItems ->
if (playerItems != null) {
navigateToPlayerActivity(
playerItems
)
viewModel.doneNavigatingToPlayer()
binding.playButton.setImageDrawable(
ContextCompat.getDrawable(
requireActivity(),
R.drawable.ic_play
)
)
binding.progressCircular.visibility = View.INVISIBLE
playerViewModel.onPlaybackRequested(lifecycleScope) { playerItems ->
when (playerItems) {
is PlayerViewModel.PlayerItemError -> bindPlayerItemsError(playerItems)
is PlayerViewModel.PlayerItems -> bindPlayerItems(playerItems)
}
})
}
viewModel.played.observe(viewLifecycleOwner, {
val drawable = when (it) {
@ -128,27 +116,6 @@ class MediaInfoFragment : Fragment() {
binding.favoriteButton.setImageResource(drawable)
})
viewModel.playerItemsError.observe(viewLifecycleOwner, { errorMessage ->
if (errorMessage != null) {
binding.playerItemsError.visibility = View.VISIBLE
binding.playButton.setImageDrawable(
ContextCompat.getDrawable(
requireActivity(),
R.drawable.ic_play
)
)
binding.progressCircular.visibility = View.INVISIBLE
} else {
binding.playerItemsError.visibility = View.GONE
}
})
binding.playerItemsErrorDetails.setOnClickListener {
ErrorDialogFragment(
viewModel.playerItemsError.value ?: getString(R.string.unknown_error)
).show(parentFragmentManager, "errordialog")
}
binding.trailerButton.setOnClickListener {
if (viewModel.item.value?.remoteTrailers.isNullOrEmpty()) return@setOnClickListener
val intent = Intent(
@ -178,19 +145,14 @@ class MediaInfoFragment : Fragment() {
binding.playButton.setOnClickListener {
binding.playButton.setImageResource(android.R.color.transparent)
binding.progressCircular.visibility = View.VISIBLE
if (args.itemType == "Movie") {
if (viewModel.item.value?.mediaSources != null) {
if (viewModel.item.value?.mediaSources?.size!! > 1) {
VideoVersionDialogFragment(viewModel).show(
parentFragmentManager,
"videoversiondialog"
)
} else {
viewModel.preparePlayerItems()
}
viewModel.item.value?.let { item ->
playerViewModel.loadPlayerItems(item) {
VideoVersionDialogFragment(item, playerViewModel).show(
parentFragmentManager,
"videoversiondialog"
)
}
} else if (args.itemType == "Series") {
viewModel.preparePlayerItems()
}
}
@ -211,6 +173,33 @@ class MediaInfoFragment : Fragment() {
viewModel.loadData(args.itemId, args.itemType)
}
private fun bindPlayerItems(items: PlayerViewModel.PlayerItems) {
navigateToPlayerActivity(items.items.toTypedArray())
binding.playButton.setImageDrawable(
ContextCompat.getDrawable(
requireActivity(),
R.drawable.ic_play
)
)
binding.progressCircular.visibility = View.INVISIBLE
}
private fun bindPlayerItemsError(error: PlayerViewModel.PlayerItemError) {
Timber.e(error.message)
binding.playerItemsError.visibility = View.VISIBLE
binding.playButton.setImageDrawable(
ContextCompat.getDrawable(
requireActivity(),
R.drawable.ic_play
)
)
binding.progressCircular.visibility = View.INVISIBLE
binding.errorLayout.errorDetailsButton.setOnClickListener {
ErrorDialogFragment(error.message).show(parentFragmentManager, "errordialog")
}
}
private fun navigateToEpisodeBottomSheetFragment(episode: BaseItemDto) {
findNavController().navigate(
MediaInfoFragmentDirections.actionMediaInfoFragmentToEpisodeBottomSheetFragment(

View file

@ -6,16 +6,14 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
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 org.jellyfin.sdk.model.api.LocationType
import timber.log.Timber
import java.text.DateFormat
import java.time.ZoneOffset
import java.util.*
import java.util.Date
import java.util.UUID
import javax.inject.Inject
@HiltViewModel
@ -40,14 +38,6 @@ constructor(
private val _favorite = MutableLiveData<Boolean>()
val favorite: LiveData<Boolean> = _favorite
private val _navigateToPlayer = MutableLiveData<Boolean>()
val navigateToPlayer: LiveData<Boolean> = _navigateToPlayer
var playerItems: MutableList<PlayerItem> = mutableListOf()
private val _playerItemsError = MutableLiveData<String>()
val playerItemsError: LiveData<String> = _playerItemsError
fun loadEpisode(episodeId: UUID) {
viewModelScope.launch {
try {
@ -63,56 +53,6 @@ constructor(
}
}
fun preparePlayerItems() {
_playerItemsError.value = null
viewModelScope.launch {
try {
createPlayerItems(_item.value!!)
_navigateToPlayer.value = true
} catch (e: Exception) {
_playerItemsError.value = e.toString()
}
}
}
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.name, intro.id, intro.mediaSources?.get(0)?.id!!, 0))
introsCount += 1
}
}
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
if (episode.locationType == LocationType.VIRTUAL) continue
playerItems.add(
PlayerItem(
episode.name,
episode.id,
episode.mediaSources?.get(0)?.id!!,
playbackPosition
)
)
}
if (playerItems.isEmpty() || playerItems.count() == introsCount) throw Exception("No playable items found")
}
fun markAsPlayed(itemId: UUID) {
viewModelScope.launch {
jellyfinRepository.markAsPlayed(itemId)
@ -151,8 +91,4 @@ constructor(
item.premiereDate.toString()
}
}
fun doneNavigateToPlayer() {
_navigateToPlayer.value = false
}
}

View file

@ -6,17 +6,14 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.repository.JellyfinRepository
import kotlinx.coroutines.Dispatchers
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.ItemFields
import org.jellyfin.sdk.model.api.LocationType
import timber.log.Timber
import java.util.*
import java.util.UUID
import javax.inject.Inject
@HiltViewModel
@ -53,9 +50,6 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
private val _seasons = MutableLiveData<List<BaseItemDto>>()
val seasons: LiveData<List<BaseItemDto>> = _seasons
private val _navigateToPlayer = MutableLiveData<Array<PlayerItem>>()
val navigateToPlayer: LiveData<Array<PlayerItem>> = _navigateToPlayer
private val _played = MutableLiveData<Boolean>()
val played: LiveData<Boolean> = _played
@ -65,11 +59,6 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
private val _error = MutableLiveData<String>()
val error: LiveData<String> = _error
var playerItems: MutableList<PlayerItem> = mutableListOf()
private val _playerItemsError = MutableLiveData<String>()
val playerItemsError: LiveData<String> = _playerItemsError
fun loadData(itemId: UUID, itemType: String) {
_error.value = null
viewModelScope.launch {
@ -177,97 +166,4 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
else -> dateString
}
}
fun preparePlayerItems(mediaSourceIndex: Int? = null) {
_playerItemsError.value = null
viewModelScope.launch {
try {
createPlayerItems(_item.value!!, mediaSourceIndex)
_navigateToPlayer.value = playerItems.toTypedArray()
} catch (e: Exception) {
_playerItemsError.value = e.message
}
}
}
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.name, intro.id, intro.mediaSources?.get(0)?.id!!, 0))
introsCount += 1
}
}
when (series.type) {
"Movie" -> {
playerItems.add(
PlayerItem(
series.name,
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
if (episode.locationType == LocationType.VIRTUAL) continue
playerItems.add(
PlayerItem(
episode.name,
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
if (episode.locationType == LocationType.VIRTUAL) continue
playerItems.add(
PlayerItem(
episode.name,
episode.id,
episode.mediaSources?.get(0)?.id!!,
0
)
)
}
}
}
}
}
if (playerItems.isEmpty() || playerItems.count() == introsCount) throw Exception("No playable items found")
}
fun doneNavigatingToPlayer() {
_navigateToPlayer.value = null
}
}

View file

@ -8,7 +8,12 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.preference.PreferenceManager
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.BasePlayer
import com.google.android.exoplayer2.DefaultRenderersFactory
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.models.PlayerItem
@ -18,7 +23,7 @@ import dev.jdtech.jellyfin.repository.JellyfinRepository
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import timber.log.Timber
import java.util.*
import java.util.UUID
import javax.inject.Inject
@HiltViewModel
@ -114,9 +119,8 @@ constructor(
player.setMediaItems(mediaItems, currentWindow, items[0].playbackPosition)
player.prepare()
player.play()
pollPosition(player)
}
pollPosition(player)
}
private fun releasePlayer() {

View file

@ -0,0 +1,150 @@
package dev.jdtech.jellyfin.viewmodels
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.repository.JellyfinRepository
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.ItemFields
import org.jellyfin.sdk.model.api.LocationType.VIRTUAL
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class PlayerViewModel @Inject internal constructor(
private val repository: JellyfinRepository
) : ViewModel() {
private val playerItems = MutableSharedFlow<PlayerItemState>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
fun onPlaybackRequested(scope: LifecycleCoroutineScope, collector: (PlayerItemState) -> Unit) {
scope.launch { playerItems.collect { collector(it) } }
}
fun loadPlayerItems(
item: BaseItemDto,
mediaSourceIndex: Int = 0,
onVersionSelectRequired: () -> Unit = { Unit }
) {
Timber.d("Loading player items for item ${item.id}")
if (item.mediaSources.orEmpty().size > 1) {
onVersionSelectRequired()
}
viewModelScope.launch {
val playbackPosition = item.userData?.playbackPositionTicks?.div(10000) ?: 0
val items = try {
createItems(item, playbackPosition, mediaSourceIndex).let(::PlayerItems)
} catch (e: Exception) {
PlayerItemError(e.message.orEmpty())
}
playerItems.tryEmit(items)
}
}
private suspend fun createItems(
item: BaseItemDto,
playbackPosition: Long,
mediaSourceIndex: Int
) = if (playbackPosition <= 0) {
prepareIntros(item) + prepareMediaPlayerItems(
item,
playbackPosition,
mediaSourceIndex
)
} else {
prepareMediaPlayerItems(item, playbackPosition, mediaSourceIndex)
}
private suspend fun prepareIntros(item: BaseItemDto): List<PlayerItem> {
return repository
.getIntros(item.id)
.filter { it.mediaSources != null && it.mediaSources?.isNotEmpty() == true }
.map { intro ->
PlayerItem(
intro.name,
intro.id,
intro.mediaSources?.get(0)?.id!!,
0
)
}
}
private suspend fun prepareMediaPlayerItems(
item: BaseItemDto,
playbackPosition: Long,
mediaSourceIndex: Int
): List<PlayerItem> = when (item.type) {
"Movie" -> itemToMoviePlayerItems(item, playbackPosition, mediaSourceIndex)
"Series" -> itemToPlayerItems(item, playbackPosition, mediaSourceIndex)
"Episode" -> itemToPlayerItems(item, playbackPosition, mediaSourceIndex)
else -> emptyList()
}
private fun itemToMoviePlayerItems(
item: BaseItemDto,
playbackPosition: Long,
mediaSourceIndex: Int
) = listOf(
PlayerItem(
item.name,
item.id,
item.mediaSources?.get(mediaSourceIndex)?.id!!,
playbackPosition
)
)
private suspend fun itemToPlayerItems(
item: BaseItemDto,
playbackPosition: Long,
mediaSourceIndex: Int
): List<PlayerItem> {
val nextUp = repository.getNextUp(item.seriesId)
return if (nextUp.isEmpty()) {
repository
.getSeasons(item.seriesId!!)
.flatMap { episodesToPlayerItems(item, playbackPosition, mediaSourceIndex) }
} else {
episodesToPlayerItems(item, playbackPosition, mediaSourceIndex)
}
}
private suspend fun episodesToPlayerItems(
item: BaseItemDto,
playbackPosition: Long,
mediaSourceIndex: Int
): List<PlayerItem> {
val episodes = repository.getEpisodes(
seriesId = item.seriesId!!,
seasonId = item.seasonId!!,
fields = listOf(ItemFields.MEDIA_SOURCES),
startItemId = item.id
)
return episodes
.filter { it.mediaSources != null && it.mediaSources?.isNotEmpty() == true }
.filter { it.locationType != VIRTUAL }
.map { episode ->
PlayerItem(
episode.name,
episode.id,
episode.mediaSources?.get(mediaSourceIndex)?.id!!,
playbackPosition
)
}
}
sealed class PlayerItemState
data class PlayerItemError(val message: String): PlayerItemState()
data class PlayerItems(val items: List<PlayerItem>): PlayerItemState()
}