2022-01-02 17:59:23 +01:00
/ * *
* Teapod
*
* Copyright 2020 - 2022 < seil0 @mosad . xyz >
*
* This program is free software ; you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation ; either version 3 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with this program ; if not , write to the Free Software
* Foundation , Inc . , 51 Franklin Street , Fifth Floor , Boston ,
* MA 02110 - 1301 , USA .
*
* /
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
2022-01-09 19:23:33 +01:00
import androidx.lifecycle.viewModelScope
import com.google.android.exoplayer2.ExoPlayer
2020-12-26 20:09:35 +01:00
import com.google.android.exoplayer2.MediaItem
2022-01-09 19:23:33 +01:00
import com.google.android.exoplayer2.Player
2020-12-26 20:09:35 +01:00
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.hls.HlsMediaSource
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.util.Util
2022-01-09 19:23:33 +01: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
2021-12-20 22:14:58 +01:00
import org.mosad.teapod.parser.crunchyroll.Crunchyroll
import org.mosad.teapod.parser.crunchyroll.NoneEpisode
import org.mosad.teapod.parser.crunchyroll.NoneEpisodes
import org.mosad.teapod.parser.crunchyroll.NonePlayback
2020-12-15 23:15:14 +01:00
import org.mosad.teapod.preferences.Preferences
2021-12-26 20:22:00 +01:00
import org.mosad.teapod.util.EpisodeMeta
import org.mosad.teapod.util.Meta
import org.mosad.teapod.util.TVShowMeta
2021-09-05 00:08:03 +02:00
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-12-27 21:14:35 +01:00
// tmdb/meta data TODO currently not implemented for cr
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-07-17 19:40:16 +02:00
var currentEpisodeMeta : EpisodeMeta ? = null
internal set
2020-11-25 16:04:04 +01:00
2021-12-27 21:14:35 +01:00
// crunchyroll episodes/playback
var episodes = NoneEpisodes
2021-12-20 22:14:58 +01:00
internal set
2021-12-27 21:14:35 +01:00
var currentEpisode = NoneEpisode
internal set
2021-12-29 20:51:53 +01:00
var currentPlayback = NonePlayback
2021-12-27 21:14:35 +01:00
// current playback settings
2021-12-29 20:51:53 +01:00
var currentLanguage : Locale = Preferences . preferredLocal
2021-12-20 22:14:58 +01:00
internal set
2021-06-12 20:57:12 +02:00
init {
initMediaSession ( )
2022-01-09 19:23:33 +01:00
player . addListener ( object : Player . Listener {
override fun onPlaybackStateChanged ( state : Int ) {
super . onPlaybackStateChanged ( state )
if ( state == ExoPlayer . STATE _ENDED ) updatePlayhead ( )
}
override fun onIsPlayingChanged ( isPlaying : Boolean ) {
super . onIsPlayingChanged ( isPlaying )
if ( !is Playing ) updatePlayhead ( )
}
} )
2021-06-12 20:57:12 +02:00
}
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
}
2021-12-20 22:14:58 +01:00
fun loadMedia ( seasonId : String , episodeId : String ) {
2020-11-27 11:06:16 +01:00
runBlocking {
2021-12-27 21:14:35 +01:00
episodes = Crunchyroll . episodes ( seasonId )
2021-12-20 22:14:58 +01:00
//mediaMeta = loadMediaMeta(media.aodId) // can be done blocking, since it should be cached
2021-12-26 20:22:00 +01:00
// TODO replace this with setCurrentEpisode
2021-12-27 21:14:35 +01:00
currentEpisode = episodes . items . find { episode ->
2021-12-20 22:14:58 +01:00
episode . id == episodeId
} ?: NoneEpisode
2021-12-27 21:14:35 +01:00
println ( " loading playback ${currentEpisode.playback} " )
2021-12-20 22:14:58 +01:00
2021-12-27 21:14:35 +01:00
currentPlayback = Crunchyroll . playback ( currentEpisode . playback )
2020-11-27 11:06:16 +01:00
}
2021-12-26 20:22:00 +01:00
// TODO reimplement for cr
2021-09-05 00:08:03 +02:00
// run async as it should be loaded by the time the episodes a
2021-12-26 20:22:00 +01:00
// viewModelScope.launch {
// // get tmdb season info, if metaDB knows the tv show
// if (media.type == DataTypes.MediaType.TVSHOW && mediaMeta != null) {
// val tvShowMeta = mediaMeta as TVShowMeta
// tmdbTVSeason = TMDBApiController().getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber)
// }
// }
2021-12-27 21:14:35 +01:00
//
// currentEpisodeMeta = getEpisodeMetaByAoDMediaId(currentEpisodeAoD.mediaId)
// currentLanguage = currentEpisodeAoD.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
2021-12-26 20:22:00 +01:00
playCurrentMedia ( player . currentPosition )
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
/ * *
2021-12-27 21:14:35 +01:00
* play the next episode , if nextEpisodeId is not null
2020-12-27 20:11:01 +01:00
* /
2021-12-27 21:14:35 +01:00
fun playNextEpisode ( ) = currentEpisode . nextEpisodeId ?. let { nextEpisodeId ->
2021-12-26 20:22:00 +01:00
setCurrentEpisode ( nextEpisodeId , startPlayback = true )
}
/ * *
* Set currentEpisodeCr to the episode of the given ID
* @param episodeId The ID of the episode you want to set currentEpisodeCr to
* /
fun setCurrentEpisode ( episodeId : String , startPlayback : Boolean = false ) {
2021-12-27 21:14:35 +01:00
currentEpisode = episodes . items . find { episode ->
2021-12-26 20:22:00 +01:00
episode . id == episodeId
} ?: NoneEpisode
// TODO don't run blocking
runBlocking {
2021-12-27 21:14:35 +01:00
currentPlayback = Crunchyroll . playback ( currentEpisode . playback )
2021-12-26 20:22:00 +01:00
}
// TODO update metadata and language (it should not be needed to update the language here!)
if ( startPlayback ) {
playCurrentMedia ( )
}
2020-12-27 20:11:01 +01:00
}
/ * *
2021-12-26 20:22:00 +01:00
* Play the current media from currentPlaybackCr .
2020-12-27 20:11:01 +01:00
*
2021-09-05 11:54:55 +02:00
* @param seekPosition The seek position for the episode ( default = 0 ) .
2020-12-27 20:11:01 +01:00
* /
2021-12-26 20:22:00 +01:00
fun playCurrentMedia ( seekPosition : Long = 0 ) {
2021-09-05 11:54:55 +02:00
// update player gui (title, next ep button) after nextEpisodeId has been set
currentEpisodeChangedListener . forEach { it ( ) }
2020-12-27 20:11:01 +01:00
2021-12-29 20:51:53 +01:00
// get preferred stream url, set current language if it differs from the preferred one
val preferredLocale = currentLanguage
val fallbackLocal = Locale . US
val url = when {
currentPlayback . streams . adaptive _hls . containsKey ( preferredLocale . toLanguageTag ( ) ) -> {
currentPlayback . streams . adaptive _hls [ preferredLocale . toLanguageTag ( ) ] ?. url
}
currentPlayback . streams . adaptive _hls . containsKey ( fallbackLocal . toLanguageTag ( ) ) -> {
currentLanguage = fallbackLocal
currentPlayback . streams . adaptive _hls [ fallbackLocal . toLanguageTag ( ) ] ?. url
}
else -> {
currentLanguage = Locale . ROOT
currentPlayback . streams . adaptive _hls [ Locale . ROOT . toLanguageTag ( ) ] ?. url ?: " "
}
}
2021-12-26 20:22:00 +01:00
println ( " stream url: $url " )
// create the media source object
2020-12-26 20:09:35 +01:00
val mediaSource = HlsMediaSource . Factory ( dataSourceFactory ) . createMediaSource (
2021-12-26 20:22:00 +01:00
MediaItem . fromUri ( Uri . parse ( url ) )
2020-12-26 20:09:35 +01:00
)
2020-12-27 20:11:01 +01:00
2021-12-26 20:22:00 +01:00
// the actual player playback code
player . setMediaSource ( mediaSource )
player . prepare ( )
if ( seekPosition > 0 ) player . seekTo ( seekPosition )
player . playWhenReady = true
2020-11-25 16:04:04 +01:00
}
2021-12-29 20:51:53 +01:00
/ * *
* Returns the current episode title ( with episode number , if it ' s a tv show )
* /
2020-12-27 20:11:01 +01:00
fun getMediaTitle ( ) : String {
2021-12-29 20:51:53 +01:00
// currentEpisode.episodeNumber defines the media type (tv show = none null, movie = null)
return if ( currentEpisode . episodeNumber != null ) {
2020-12-27 20:11:01 +01:00
getApplication < Application > ( ) . getString (
R . string . component _episode _title ,
2021-12-27 21:14:35 +01:00
currentEpisode . episode ,
currentEpisode . title
2020-12-27 20:11:01 +01:00
)
} else {
2021-12-27 21:14:35 +01:00
currentEpisode . title
2020-12-27 20:11:01 +01:00
}
}
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-12-26 20:22:00 +01:00
// TODO reimplement for cr
2021-07-11 12:56:21 +02:00
private suspend fun loadMediaMeta ( aodId : Int ) : Meta ? {
2021-12-26 20:22:00 +01:00
// return if (media.type == DataTypes.MediaType.TVSHOW) {
// MetaDBController().getTVShowMetadata(aodId)
// } else {
// null
// }
return null
2021-07-11 12:56:21 +02:00
}
2022-01-09 19:23:33 +01:00
/ * *
* Update the playhead of the current episode , if currentPosition > 1000 ms .
* /
private fun updatePlayhead ( ) {
val playhead = ( player . currentPosition / 1000 )
if ( playhead > 0 ) {
viewModelScope . launch { Crunchyroll . postPlayheads ( currentEpisode . id , playhead . toInt ( ) ) }
Log . i ( javaClass . name , " Set playhead for episode ${currentEpisode.id} to $playhead sec. " )
}
}
}