Rework player to allow for playing multiple episodes in a row
This commit is contained in:
parent
23a3937e86
commit
d67f195789
10 changed files with 122 additions and 103 deletions
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
|
|
11
app/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt
Normal file
11
app/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt
Normal 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
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
Loading…
Reference in a new issue