Rework how player items are created
Add support for intros and improve loading speed
This commit is contained in:
parent
fb1755e8b8
commit
25ac5524d7
11 changed files with 160 additions and 89 deletions
|
@ -38,7 +38,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||
})
|
||||
|
||||
if (viewModel.player.value == null) {
|
||||
viewModel.initializePlayer(args.items, args.playbackPosition)
|
||||
viewModel.initializePlayer(args.items)
|
||||
}
|
||||
hideSystemUI()
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -7,5 +7,6 @@ import java.util.*
|
|||
@Parcelize
|
||||
data class PlayerItem(
|
||||
val itemId: UUID,
|
||||
val mediaSourceId: String
|
||||
val mediaSourceId: String,
|
||||
val playbackPosition: Long
|
||||
) : Parcelable
|
|
@ -54,4 +54,6 @@ interface JellyfinRepository {
|
|||
suspend fun markAsPlayed(itemId: UUID)
|
||||
|
||||
suspend fun markAsUnplayed(itemId: UUID)
|
||||
|
||||
suspend fun getIntros(itemId: UUID): List<BaseItemDto>
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue