2021-02-06 19:02:12 +01:00
package org.mosad.teapod.ui.activity.player
2020-11-25 16:04:04 +01:00
2020-12-26 20:09:35 +01:00
import android.app.Application
import android.net.Uri
2021-06-12 20:57:12 +02:00
import android.support.v4.media.session.MediaSessionCompat
2020-12-27 20:31:18 +01:00
import android.util.Log
2020-12-26 20:09:35 +01:00
import androidx.lifecycle.AndroidViewModel
2021-06-06 17:54:19 +02:00
import androidx.lifecycle.viewModelScope
2020-12-26 20:09:35 +01:00
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.SimpleExoPlayer
2021-06-12 20:57:12 +02:00
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
2020-12-26 20:09:35 +01:00
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.util.Util
2021-06-06 17:54:19 +02:00
import kotlinx.coroutines.launch
2020-11-27 11:06:16 +01:00
import kotlinx.coroutines.runBlocking
2020-12-27 20:11:01 +01:00
import org.mosad.teapod.R
2020-11-25 16:04:04 +01:00
import org.mosad.teapod.parser.AoDParser
2020-12-15 23:15:14 +01:00
import org.mosad.teapod.preferences.Preferences
2021-07-11 12:56:21 +02:00
import org.mosad.teapod.util.*
2021-09-05 00:08:03 +02:00
import org.mosad.teapod.util.tmdb.TMDBApiController
import org.mosad.teapod.util.tmdb.TMDBTVSeason
2020-12-26 14:39:35 +01:00
import java.util.*
import kotlin.collections.ArrayList
2020-11-25 16:04:04 +01:00
2020-12-15 23:15:14 +01:00
/ * *
* PlayerViewModel handles all stuff related to media / episodes .
* When currentEpisode is changed the player will start playing it ( not initial media ) ,
* the next episode will be update and the callback is handled .
* /
2020-12-26 20:09:35 +01:00
class PlayerViewModel ( application : Application ) : AndroidViewModel ( application ) {
val player = SimpleExoPlayer . Builder ( application ) . build ( )
2021-06-12 20:57:12 +02:00
private val dataSourceFactory = DefaultDataSourceFactory ( application , Util . getUserAgent ( application , " Teapod " ) )
private val mediaSession = MediaSessionCompat ( application , " TEAPOD_PLAYER_SESSION " )
2020-12-15 23:15:14 +01:00
val currentEpisodeChangedListener = ArrayList < ( ) -> Unit > ( )
2021-06-12 20:57:12 +02:00
private val preferredLanguage = if ( Preferences . preferSecondary ) Locale . JAPANESE else Locale . GERMAN
2020-12-15 23:15:14 +01:00
2021-09-04 13:33:46 +02:00
var media : AoDMedia = AoDMediaNone
2020-11-25 16:04:04 +01:00
internal set
2021-09-05 11:54:55 +02:00
var mediaMeta : Meta ? = null
2020-11-25 16:04:04 +01:00
internal set
2021-09-05 00:08:03 +02:00
var tmdbTVSeason : TMDBTVSeason ? = null
internal set
2021-09-05 11:54:55 +02:00
var currentEpisode = AoDEpisodeNone
2021-07-11 12:56:21 +02:00
internal set
2021-07-17 19:40:16 +02:00
var currentEpisodeMeta : EpisodeMeta ? = null
internal set
2021-09-05 11:54:55 +02:00
var nextEpisodeId : Int ? = null
internal set
2020-12-27 20:11:01 +01:00
var currentLanguage : Locale = Locale . ROOT
internal set
2020-11-25 16:04:04 +01:00
2021-06-12 20:57:12 +02:00
init {
initMediaSession ( )
}
2020-12-27 20:31:18 +01:00
override fun onCleared ( ) {
super . onCleared ( )
2021-06-12 20:57:12 +02:00
mediaSession . release ( )
2020-12-27 20:31:18 +01:00
player . release ( )
Log . d ( javaClass . name , " Released player " )
}
2021-06-12 20:57:12 +02:00
/ * *
* set the media session to active
* create a media session connector to set title and description
* /
private fun initMediaSession ( ) {
val mediaSessionConnector = MediaSessionConnector ( mediaSession )
mediaSessionConnector . setPlayer ( player )
mediaSession . isActive = true
}
2020-12-26 20:09:35 +01:00
fun loadMedia ( mediaId : Int , episodeId : Int ) {
2020-11-27 11:06:16 +01:00
runBlocking {
media = AoDParser . getMediaById ( mediaId )
2021-09-04 13:33:46 +02:00
mediaMeta = loadMediaMeta ( media . aodId ) // can be done blocking, since it should be cached
2020-11-27 11:06:16 +01:00
}
2021-09-05 00:08:03 +02:00
// run async as it should be loaded by the time the episodes a
viewModelScope . launch {
// get season info, if metaDB knows the tv show
if ( media . type == DataTypes . MediaType . TVSHOW && mediaMeta != null ) {
val tvShowMeta = mediaMeta as TVShowMeta
2021-09-05 00:08:53 +02:00
tmdbTVSeason = TMDBApiController ( ) . getTVSeasonDetails ( tvShowMeta . tmdbId , tvShowMeta . tmdbSeasonNumber )
2021-09-05 00:08:03 +02:00
}
}
2020-12-26 20:09:35 +01:00
currentEpisode = media . getEpisodeById ( episodeId )
2021-09-05 11:54:55 +02:00
nextEpisodeId = selectNextEpisode ( )
2021-09-04 13:33:46 +02:00
currentEpisodeMeta = getEpisodeMetaByAoDMediaId ( currentEpisode . mediaId )
2020-12-31 13:12:37 +01:00
currentLanguage = currentEpisode . getPreferredStream ( preferredLanguage ) . language
2020-12-15 23:15:14 +01:00
}
2020-12-27 20:11:01 +01:00
fun setLanguage ( language : Locale ) {
currentLanguage = language
2020-11-25 16:04:04 +01:00
2020-12-26 20:09:35 +01:00
val seekTime = player . currentPosition
val mediaSource = HlsMediaSource . Factory ( dataSourceFactory ) . createMediaSource (
2020-12-27 20:11:01 +01:00
MediaItem . fromUri ( Uri . parse ( currentEpisode . getPreferredStream ( language ) . url ) )
2020-12-26 20:09:35 +01:00
)
playMedia ( mediaSource , true , seekTime )
2020-12-20 20:21:27 +01:00
}
2020-12-26 20:09:35 +01:00
// player actions
2020-12-27 20:11:01 +01:00
2020-12-26 20:09:35 +01:00
fun seekToOffset ( offset : Long ) {
player . seekTo ( player . currentPosition + offset )
}
fun togglePausePlay ( ) {
if ( player . isPlaying ) player . pause ( ) else player . play ( )
}
2020-12-27 20:11:01 +01:00
/ * *
* play the next episode , if nextEpisode is not null
* /
2021-09-05 11:54:55 +02:00
fun playNextEpisode ( ) = nextEpisodeId ?. let { it ->
2020-12-27 20:11:01 +01:00
playEpisode ( it , replace = true )
}
/ * *
2021-09-05 11:54:55 +02:00
* Set currentEpisode and start playing it .
* Update nextEpisode to reflect the change and update
* the watched state for the now playing episode .
2020-12-27 20:11:01 +01:00
*
2021-09-05 11:54:55 +02:00
* @param episodeId The aod media id of the episode to play .
* @param replace ( default = false )
* @param seekPosition The seek position for the episode ( default = 0 ) .
2020-12-27 20:11:01 +01:00
* /
2021-09-05 11:54:55 +02:00
fun playEpisode ( episodeId : Int , replace : Boolean = false , seekPosition : Long = 0 ) {
currentEpisode = media . getEpisodeById ( episodeId )
currentLanguage = currentEpisode . getPreferredStream ( currentLanguage ) . language
currentEpisodeMeta = getEpisodeMetaByAoDMediaId ( currentEpisode . mediaId )
nextEpisodeId = selectNextEpisode ( )
// update player gui (title, next ep button) after nextEpisodeId has been set
currentEpisodeChangedListener . forEach { it ( ) }
2020-12-27 20:11:01 +01:00
2020-12-26 20:09:35 +01:00
val mediaSource = HlsMediaSource . Factory ( dataSourceFactory ) . createMediaSource (
2021-09-05 11:54:55 +02:00
MediaItem . fromUri ( Uri . parse ( currentEpisode . getPreferredStream ( currentLanguage ) . url ) )
2020-12-26 20:09:35 +01:00
)
playMedia ( mediaSource , replace , seekPosition )
2020-12-27 20:11:01 +01:00
2021-01-01 13:08:25 +01:00
// if episodes has not been watched, mark as watched
2021-09-05 11:54:55 +02:00
if ( ! currentEpisode . watched ) {
2021-06-06 17:54:19 +02:00
viewModelScope . launch {
2021-09-05 11:54:55 +02:00
AoDParser . markAsWatched ( media . aodId , currentEpisode . mediaId )
2021-06-06 17:54:19 +02:00
}
2021-01-01 13:08:25 +01:00
}
2020-12-26 20:09:35 +01:00
}
2021-06-12 20:57:12 +02:00
/ * *
* change the players media source and start playback
* /
2020-12-26 20:09:35 +01:00
fun playMedia ( source : MediaSource , replace : Boolean = false , seekPosition : Long = 0 ) {
if ( replace || player . contentDuration == C . TIME _UNSET ) {
player . setMediaSource ( source )
player . prepare ( )
if ( seekPosition > 0 ) player . seekTo ( seekPosition )
player . playWhenReady = true
}
2020-11-25 16:04:04 +01:00
}
2020-12-27 20:11:01 +01:00
fun getMediaTitle ( ) : String {
return if ( media . type == DataTypes . MediaType . TVSHOW ) {
getApplication < Application > ( ) . getString (
R . string . component _episode _title ,
2021-09-05 00:08:03 +02:00
currentEpisode . numberStr ,
2020-12-27 20:11:01 +01:00
currentEpisode . description
)
} else {
currentEpisode . title
}
}
2021-07-17 19:40:16 +02:00
fun getEpisodeMetaByAoDMediaId ( aodMediaId : Int ) : EpisodeMeta ? {
val meta = mediaMeta
return if ( meta is TVShowMeta ) {
meta . episodes . firstOrNull { it . aodMediaId == aodMediaId }
} else {
null
}
}
2021-07-11 12:56:21 +02:00
private suspend fun loadMediaMeta ( aodId : Int ) : Meta ? {
return if ( media . type == DataTypes . MediaType . TVSHOW ) {
MetaDBController ( ) . getTVShowMetadata ( aodId )
} else {
null
}
}
2020-11-25 16:04:04 +01:00
/ * *
2021-09-05 11:54:55 +02:00
* Based on the current episodes index , get the next episode .
* @return The next episode or null if there is none .
2020-11-25 16:04:04 +01:00
* /
2021-09-05 11:54:55 +02:00
private fun selectNextEpisode ( ) : Int ? {
return media . playlist . firstOrNull {
it . index > media . getEpisodeById ( currentEpisode . mediaId ) . index
} ?. mediaId
2020-11-25 16:04:04 +01:00
}
}