Rework how player items are created

Add support for intros and improve loading speed
This commit is contained in:
jarnedemeulemeester 2021-08-26 15:36:56 +02:00
parent fb1755e8b8
commit 25ac5524d7
No known key found for this signature in database
GPG key ID: 60884A0C1EBA43E5
11 changed files with 160 additions and 89 deletions

View file

@ -38,7 +38,7 @@ class PlayerActivity : AppCompatActivity() {
})
if (viewModel.player.value == null) {
viewModel.initializePlayer(args.items, args.playbackPosition)
viewModel.initializePlayer(args.items)
}
hideSystemUI()
}

View file

@ -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")

View file

@ -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<PlayerItem>,
playbackPosition: Long
) {
findNavController().navigate(
EpisodeBottomSheetFragmentDirections.actionEpisodeBottomSheetFragmentToPlayerActivity(
playerItems,
playbackPosition
)
)
}

View file

@ -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<PlayerItem>,
playbackPosition: Long,
) {
findNavController().navigate(
MediaInfoFragmentDirections.actionMediaInfoFragmentToPlayerActivity(
playerItems,
playbackPosition
)
)
}

View file

@ -7,5 +7,6 @@ import java.util.*
@Parcelize
data class PlayerItem(
val itemId: UUID,
val mediaSourceId: String
val mediaSourceId: String,
val playbackPosition: Long
) : Parcelable

View file

@ -54,4 +54,6 @@ interface JellyfinRepository {
suspend fun markAsPlayed(itemId: UUID)
suspend fun markAsUnplayed(itemId: UUID)
suspend fun getIntros(itemId: UUID): List<BaseItemDto>
}

View file

@ -266,4 +266,14 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
jellyfinApi.playStateApi.markUnplayedItem(jellyfinApi.userId!!, itemId)
}
}
override suspend fun getIntros(itemId: UUID): List<BaseItemDto> {
val intros: List<BaseItemDto>
withContext(Dispatchers.IO) {
intros =
jellyfinApi.userLibraryApi.getIntros(jellyfinApi.userId!!, itemId).content.items
?: listOf()
}
return intros
}
}

View file

@ -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) {

View file

@ -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<List<BaseItemDto>>()
val seasons: LiveData<List<BaseItemDto>> = _seasons
private val _mediaSources = MutableLiveData<List<MediaSourceInfo>>()
val mediaSources: LiveData<List<MediaSourceInfo>> = _mediaSources
private val _navigateToPlayer = MutableLiveData<Array<PlayerItem>>()
val navigateToPlayer: LiveData<Array<PlayerItem>> = _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() {

View file

@ -39,8 +39,7 @@ constructor(
private val sp = PreferenceManager.getDefaultSharedPreferences(application)
fun initializePlayer(
items: Array<PlayerItem>,
playbackPosition: Long
items: Array<PlayerItem>
) {
val renderersFactory =
@ -61,18 +60,22 @@ constructor(
viewModelScope.launch {
val mediaItems: MutableList<MediaItem> = 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)
}
}
}

View file

@ -160,9 +160,6 @@
<argument
android:name="items"
app:argType="dev.jdtech.jellyfin.models.PlayerItem[]" />
<argument
android:name="playbackPosition"
app:argType="long" />
</activity>
<fragment
android:id="@+id/favoriteFragment"