Rework player to allow for playing multiple episodes in a row

This commit is contained in:
jarnedemeulemeester 2021-08-05 16:09:08 +02:00
parent 23a3937e86
commit d67f195789
No known key found for this signature in database
GPG key ID: 60884A0C1EBA43E5
10 changed files with 122 additions and 103 deletions

View file

@ -33,14 +33,14 @@ class PlayerActivity : AppCompatActivity() {
playerView.player = it
})
viewModel.playbackStateListener.navigateBack.observe(this, {
viewModel.navigateBack.observe(this, {
if (it) {
onBackPressed()
}
})
if (viewModel.player.value == null) {
viewModel.initializePlayer(args.itemId, args.mediaSourceId, args.playbackPosition)
viewModel.initializePlayer(args.items, args.playbackPosition)
}
hideSystemUI()
}

View file

@ -12,6 +12,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.databinding.EpisodeBottomSheetBinding
import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.viewmodels.EpisodeBottomSheetViewModel
import java.util.*
@ -33,13 +34,10 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
binding.viewModel = viewModel
binding.playButton.setOnClickListener {
viewModel.mediaSources.value?.get(0)?.id?.let { mediaSourceId ->
navigateToPlayerActivity(
args.episodeId,
mediaSourceId,
viewModel.item.value!!.userData!!.playbackPositionTicks.div(10000)
)
}
navigateToPlayerActivity(
viewModel.playerItems.toTypedArray(),
viewModel.item.value!!.userData!!.playbackPositionTicks.div(10000)
)
}
binding.checkButton.setOnClickListener {
@ -95,14 +93,12 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
}
private fun navigateToPlayerActivity(
itemId: UUID,
mediaSourceId: String,
playerItems: Array<PlayerItem>,
playbackPosition: Long
) {
findNavController().navigate(
EpisodeBottomSheetFragmentDirections.actionEpisodeBottomSheetFragmentToPlayerActivity(
itemId,
mediaSourceId,
playerItems,
playbackPosition
)
)

View file

@ -17,9 +17,9 @@ import dev.jdtech.jellyfin.adapters.PersonListAdapter
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
import dev.jdtech.jellyfin.databinding.FragmentMediaInfoBinding
import dev.jdtech.jellyfin.dialogs.VideoVersionDialogFragment
import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
import org.jellyfin.sdk.model.api.BaseItemDto
import java.util.*
@AndroidEntryPoint
class MediaInfoFragment : Fragment() {
@ -84,13 +84,10 @@ class MediaInfoFragment : Fragment() {
})
viewModel.navigateToPlayer.observe(viewLifecycleOwner, { mediaSource ->
mediaSource.id?.let {
navigateToPlayerActivity(
args.itemId,
it,
viewModel.item.value!!.userData!!.playbackPositionTicks.div(10000)
)
}
navigateToPlayerActivity(
arrayOf(PlayerItem(args.itemId, mediaSource.id!!)),
viewModel.item.value!!.userData!!.playbackPositionTicks.div(10000)
)
})
viewModel.played.observe(viewLifecycleOwner, {
@ -139,9 +136,8 @@ class MediaInfoFragment : Fragment() {
)
} else {
navigateToPlayerActivity(
args.itemId,
viewModel.mediaSources.value!![0].id!!,
viewModel.item.value!!.userData!!.playbackPositionTicks.div(10000)
arrayOf(PlayerItem(args.itemId, viewModel.mediaSources.value!![0].id!!)),
viewModel.item.value!!.userData!!.playbackPositionTicks.div(10000),
)
}
}
@ -185,14 +181,12 @@ class MediaInfoFragment : Fragment() {
}
private fun navigateToPlayerActivity(
itemId: UUID,
mediaSourceId: String,
playbackPosition: Long
playerItems: Array<PlayerItem>,
playbackPosition: Long,
) {
findNavController().navigate(
MediaInfoFragmentDirections.actionMediaInfoFragmentToPlayerActivity(
itemId,
mediaSourceId,
playerItems,
playbackPosition
)
)

View file

@ -0,0 +1,11 @@
package dev.jdtech.jellyfin.models
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import java.util.*
@Parcelize
data class PlayerItem(
val itemId: UUID,
val mediaSourceId: String
) : Parcelable

View file

@ -24,7 +24,12 @@ interface JellyfinRepository {
suspend fun getNextUp(seriesId: UUID? = null): List<BaseItemDto>
suspend fun getEpisodes(seriesId: UUID, seasonId: UUID, fields: List<ItemFields>? = null): List<BaseItemDto>
suspend fun getEpisodes(
seriesId: UUID,
seasonId: UUID,
fields: List<ItemFields>? = null,
startIndex: Int? = null
): List<BaseItemDto>
suspend fun getMediaSources(itemId: UUID): List<MediaSourceInfo>

View file

@ -105,12 +105,17 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
override suspend fun getEpisodes(
seriesId: UUID,
seasonId: UUID,
fields: List<ItemFields>?
fields: List<ItemFields>?,
startIndex: Int?
): List<BaseItemDto> {
val episodes: List<BaseItemDto>
withContext(Dispatchers.IO) {
episodes = jellyfinApi.showsApi.getEpisodes(
seriesId, jellyfinApi.userId!!, seasonId = seasonId, fields = fields
seriesId,
jellyfinApi.userId!!,
seasonId = seasonId,
fields = fields,
startIndex = startIndex
).content.items ?: listOf()
}
return episodes

View file

@ -6,10 +6,10 @@ 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.MediaSourceInfo
import timber.log.Timber
import java.text.DateFormat
import java.time.ZoneOffset
@ -32,15 +32,14 @@ constructor(
private val _dateString = MutableLiveData<String>()
val dateString: LiveData<String> = _dateString
private val _mediaSources = MutableLiveData<List<MediaSourceInfo>>()
val mediaSources: LiveData<List<MediaSourceInfo>> = _mediaSources
private val _played = MutableLiveData<Boolean>()
val played: LiveData<Boolean> = _played
private val _favorite = MutableLiveData<Boolean>()
val favorite: LiveData<Boolean> = _favorite
var playerItems: MutableList<PlayerItem> = mutableListOf()
fun loadEpisode(episodeId: UUID) {
viewModelScope.launch {
try {
@ -48,7 +47,7 @@ constructor(
_item.value = item
_runTime.value = "${item.runTimeTicks?.div(600000000)} min"
_dateString.value = getDateString(item)
_mediaSources.value = jellyfinRepository.getMediaSources(episodeId)
createPlayerItems(item)
_played.value = item.userData?.played
_favorite.value = item.userData?.isFavorite
} catch (e: Exception) {
@ -57,6 +56,14 @@ constructor(
}
}
private suspend fun createPlayerItems(startEpisode: BaseItemDto) {
val episodes = jellyfinRepository.getEpisodes(startEpisode.seriesId!!, startEpisode.seasonId!!, startIndex = startEpisode.indexNumber?.minus(1))
for (episode in episodes) {
val mediaSources = jellyfinRepository.getMediaSources(episode.id)
playerItems.add(PlayerItem(episode.id, mediaSources[0].id!!))
}
}
fun markAsPlayed(itemId: UUID) {
viewModelScope.launch {
jellyfinRepository.markAsPlayed(itemId)

View file

@ -11,6 +11,7 @@ import androidx.preference.PreferenceManager
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.repository.JellyfinRepository
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@ -24,29 +25,23 @@ class PlayerActivityViewModel
constructor(
private val application: Application,
private val jellyfinRepository: JellyfinRepository
) : ViewModel() {
) : ViewModel(), Player.Listener {
private var _player = MutableLiveData<SimpleExoPlayer>()
var player: LiveData<SimpleExoPlayer> = _player
private val _navigateBack = MutableLiveData<Boolean>()
val navigateBack: LiveData<Boolean> = _navigateBack
private var playWhenReady = true
private var currentWindow = 0
private var playbackPosition: Long = 0
private var _playbackStateListener: PlaybackStateListener
private var itemId: UUID? = null
val playbackStateListener: PlaybackStateListener
get() = _playbackStateListener
private val sp = PreferenceManager.getDefaultSharedPreferences(application)
init {
_playbackStateListener = PlaybackStateListener()
}
fun initializePlayer(itemId: UUID, mediaSourceId: String, playbackPosition: Long) {
this.itemId = itemId
fun initializePlayer(
items: Array<PlayerItem>,
playbackPosition: Long
) {
val renderersFactory =
DefaultRenderersFactory(application).setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
@ -61,33 +56,38 @@ constructor(
.setTrackSelector(trackSelector)
.build()
player.addListener(_playbackStateListener)
player.addListener(this)
viewModelScope.launch {
val streamUrl = jellyfinRepository.getStreamUrl(itemId, mediaSourceId)
Timber.d("Stream url: $streamUrl")
val mediaItem =
MediaItem.Builder()
.setMediaId(itemId.toString())
.setUri(streamUrl)
.build()
player.setMediaItem(mediaItem, playbackPosition)
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)
}
player.setMediaItems(mediaItems, currentWindow, playbackPosition)
player.playWhenReady = playWhenReady
player.prepare()
_player.value = player
jellyfinRepository.postPlaybackStart(itemId)
}
pollPosition(player, itemId)
pollPosition(player)
}
private fun releasePlayer() {
itemId?.let { itemId ->
_player.value?.let { player ->
runBlocking {
jellyfinRepository.postPlaybackStop(itemId, player.currentPosition.times(10000))
}
_player.value?.let { player ->
runBlocking {
jellyfinRepository.postPlaybackStop(
UUID.fromString(player.currentMediaItem?.mediaId),
player.currentPosition.times(10000)
)
}
}
@ -95,22 +95,24 @@ constructor(
playWhenReady = player.value!!.playWhenReady
playbackPosition = player.value!!.currentPosition
currentWindow = player.value!!.currentWindowIndex
player.value!!.removeListener(_playbackStateListener)
player.value!!.removeListener(this)
player.value!!.release()
_player.value = null
}
}
private fun pollPosition(player: SimpleExoPlayer, itemId: UUID) {
private fun pollPosition(player: SimpleExoPlayer) {
val handler = Handler(Looper.getMainLooper())
val runnable: Runnable = object : Runnable {
val runnable = object : Runnable {
override fun run() {
viewModelScope.launch {
jellyfinRepository.postPlaybackProgress(
itemId,
player.currentPosition.times(10000),
!player.isPlaying
)
if (player.currentMediaItem != null) {
jellyfinRepository.postPlaybackProgress(
UUID.fromString(player.currentMediaItem!!.mediaId),
player.currentPosition.times(10000),
!player.isPlaying
)
}
}
handler.postDelayed(this, 2000)
}
@ -118,31 +120,33 @@ constructor(
handler.post(runnable)
}
class PlaybackStateListener : Player.Listener {
private val _navigateBack = MutableLiveData<Boolean>()
val navigateBack: LiveData<Boolean> = _navigateBack
override fun onPlaybackStateChanged(state: Int) {
var stateString = "UNKNOWN_STATE -"
when (state) {
ExoPlayer.STATE_IDLE -> {
stateString = "ExoPlayer.STATE_IDLE -"
}
ExoPlayer.STATE_BUFFERING -> {
stateString = "ExoPlayer.STATE_BUFFERING -"
}
ExoPlayer.STATE_READY -> {
stateString = "ExoPlayer.STATE_READY -"
}
ExoPlayer.STATE_ENDED -> {
stateString = "ExoPlayer.STATE_ENDED -"
_navigateBack.value = true
}
}
Timber.d("Changed player state to $stateString")
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
Timber.d("Playing MediaItem: ${mediaItem?.mediaId}")
viewModelScope.launch {
jellyfinRepository.postPlaybackStart(UUID.fromString(mediaItem?.mediaId))
}
}
override fun onPlaybackStateChanged(state: Int) {
var stateString = "UNKNOWN_STATE -"
when (state) {
ExoPlayer.STATE_IDLE -> {
stateString = "ExoPlayer.STATE_IDLE -"
}
ExoPlayer.STATE_BUFFERING -> {
stateString = "ExoPlayer.STATE_BUFFERING -"
}
ExoPlayer.STATE_READY -> {
stateString = "ExoPlayer.STATE_READY -"
}
ExoPlayer.STATE_ENDED -> {
stateString = "ExoPlayer.STATE_ENDED -"
_navigateBack.value = true
}
}
Timber.d("Changed player state to $stateString")
}
override fun onCleared() {
super.onCleared()
Timber.d("Clearing Player ViewModel")

View file

@ -11,6 +11,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
app:show_subtitle_button="true"/>
app:show_subtitle_button="true" />
</FrameLayout>

View file

@ -151,11 +151,8 @@
android:label="activity_player"
tools:layout="@layout/activity_player">
<argument
android:name="itemId"
app:argType="java.util.UUID" />
<argument
android:name="mediaSourceId"
app:argType="string" />
android:name="items"
app:argType="dev.jdtech.jellyfin.models.PlayerItem[]" />
<argument
android:name="playbackPosition"
app:argType="long" />