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
2021-06-12 20:57:12 +02:00
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
2022-07-10 13:50:53 +02:00
import kotlinx.coroutines.*
2020-12-27 20:11:01 +01:00
import org.mosad.teapod.R
2022-04-15 17:47:17 +02:00
import org.mosad.teapod.parser.crunchyroll.*
2020-12-15 23:15:14 +01:00
import org.mosad.teapod.preferences.Preferences
2022-03-20 14:29:32 +01:00
import org.mosad.teapod.util.metadb.EpisodeMeta
import org.mosad.teapod.util.metadb.Meta
import org.mosad.teapod.util.metadb.MetaDBController
import org.mosad.teapod.util.metadb.TVShowMeta
2020-12-26 14:39:35 +01:00
import java.util.*
2022-07-16 14:35:22 +02:00
import kotlin.concurrent.scheduleAtFixedRate
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 ) {
2022-03-20 14:29:32 +01:00
private val classTag = javaClass . name
2020-12-26 20:09:35 +01:00
2022-04-02 19:47:49 +02:00
val player = ExoPlayer . Builder ( application ) . build ( )
2021-06-12 20:57:12 +02:00
private val mediaSession = MediaSessionCompat ( application , " TEAPOD_PLAYER_SESSION " )
2022-07-16 14:35:22 +02:00
private val playheadAutoUpdate : TimerTask
2021-06-12 20:57:12 +02:00
2020-12-15 23:15:14 +01:00
val currentEpisodeChangedListener = ArrayList < ( ) -> Unit > ( )
2022-01-29 19:56:39 +01:00
private var currentPlayhead : Long = 0
2020-12-15 23:15:14 +01:00
2022-03-06 18:43:02 +01:00
// tmdb/meta data
2022-03-20 14:29:32 +01:00
var mediaMeta : Meta ? = null
2021-09-05 00:08:03 +02:00
internal set
2021-07-17 19:40:16 +02:00
var currentEpisodeMeta : EpisodeMeta ? = null
internal set
2022-04-15 17:47:17 +02:00
var currentPlayheads : PlayheadsMap = mutableMapOf ( )
internal set
2022-10-28 22:57:42 +02:00
var currentIntroMetadata : DatalabIntro = NoneDatalabIntro
internal set
2022-03-20 14:29:32 +01:00
// var tmdbTVSeason: TMDBTVSeason? =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
2023-02-19 14:21:46 +01:00
var currentVersion = NoneVersion
internal set
2023-01-25 19:51:38 +01:00
var currentStreams = NoneStreams
2023-02-19 14:21:46 +01:00
internal set
2021-12-27 21:14:35 +01:00
// current playback settings
2023-02-19 14:21:46 +01:00
var currentAudioLocale : Locale = Preferences . preferredAudioLocale
internal set
var currentSubtitleLocale : Locale = Preferences . preferredSubtitleLocale
2021-12-20 22:14:58 +01:00
internal set
2021-06-12 20:57:12 +02:00
init {
2022-12-02 23:59:39 +01:00
// disable platform diagnostics since they might be shared with google
ExoPlayer . Builder ( application ) . setUsePlatformDiagnostics ( false )
2021-06-12 20:57:12 +02:00
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 ( )
}
} )
2022-07-16 14:35:22 +02:00
playheadAutoUpdate = Timer ( ) . scheduleAtFixedRate ( 0 , 30000 ) {
viewModelScope . launch {
if ( player . isPlaying ) {
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 ( )
2022-03-20 14:29:32 +01:00
Log . d ( classTag , " Released player " )
2020-12-27 20:31:18 +01:00
}
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
}
2022-01-29 19:56:39 +01:00
fun loadMediaAsync ( seasonId : String , episodeId : String ) = viewModelScope . launch {
episodes = Crunchyroll . episodes ( seasonId )
2022-04-15 17:47:17 +02:00
listOf (
2023-01-25 19:51:38 +01:00
viewModelScope . launch { mediaMeta = loadMediaMeta ( episodes . data . first ( ) . seriesId ) } ,
2022-04-15 17:47:17 +02:00
viewModelScope . launch {
2023-01-25 19:51:38 +01:00
val episodeIDs = episodes . data . map { it . id }
2022-04-15 17:47:17 +02:00
currentPlayheads = Crunchyroll . playheads ( episodeIDs )
}
) . joinAll ( )
2022-03-20 14:29:32 +01:00
Log . d ( classTag , " meta: $mediaMeta " )
2021-12-20 22:14:58 +01:00
2022-01-29 19:56:39 +01:00
setCurrentEpisode ( episodeId )
2022-02-04 23:07:48 +01:00
playCurrentMedia ( currentPlayhead )
2020-12-15 23:15:14 +01:00
}
2023-02-19 14:21:46 +01:00
fun setLanguage ( newAudioLocale : Locale , newSubtitleLocale : Locale ) {
// TODO if the audio locale has changes update the streams, if only the subtitle locale has changed load the new stream
if ( newAudioLocale != currentAudioLocale ) {
currentAudioLocale = newAudioLocale
currentVersion = currentEpisode . versions . firstOrNull {
it . audioLocale == currentAudioLocale . toLanguageTag ( )
} ?: currentEpisode . versions . first ( )
viewModelScope . launch {
currentStreams = Crunchyroll . streamsFromMediaGUID ( currentVersion . mediaGUID )
Log . d ( classTag , currentVersion . toString ( ) )
playCurrentMedia ( player . currentPosition )
}
} else if ( newSubtitleLocale != currentSubtitleLocale ) {
currentSubtitleLocale = newSubtitleLocale
playCurrentMedia ( player . currentPosition )
}
// else nothing has changed so no need do do anything
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
2022-10-28 22:57:42 +02:00
/ * *
* Seeks to a offset position specified in milliseconds in the current MediaItem .
* @param offset The offset position in the current MediaItem .
* /
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 ->
2022-02-04 23:07:48 +01:00
updatePlayhead ( ) // update playhead before switching to new episode
2022-11-26 18:34:32 +01:00
viewModelScope . launch { setCurrentEpisode ( nextEpisodeId , startPlayback = true ) }
2021-12-26 20:22:00 +01:00
}
/ * *
* Set currentEpisodeCr to the episode of the given ID
* @param episodeId The ID of the episode you want to set currentEpisodeCr to
* /
2022-11-26 18:34:32 +01:00
suspend fun setCurrentEpisode ( episodeId : String , startPlayback : Boolean = false ) {
2023-01-25 19:51:38 +01:00
currentEpisode = episodes . data . find { episode ->
2021-12-26 20:22:00 +01:00
episode . id == episodeId
} ?: NoneEpisode
2022-04-22 23:51:51 +02:00
// TODO improve handling of none present seasons/episodes
2022-03-20 14:29:32 +01:00
// update current episode meta
currentEpisodeMeta = if ( mediaMeta is TVShowMeta && currentEpisode . episodeNumber != null ) {
( mediaMeta as TVShowMeta )
2022-04-22 23:51:51 +02:00
. seasons . getOrNull ( currentEpisode . seasonNumber - 1 )
?. episodes ?. getOrNull ( currentEpisode . episodeNumber !! - 1 )
2022-03-20 14:29:32 +01:00
} else {
null
}
2022-01-29 19:56:39 +01:00
// update player gui (title, next ep button) after currentEpisode has changed
currentEpisodeChangedListener . forEach { it ( ) }
// needs to be blocking, currentPlayback must be present when calling playCurrentMedia()
2022-11-26 18:34:32 +01:00
joinAll (
viewModelScope . launch ( Dispatchers . IO ) {
2023-02-19 14:21:46 +01:00
currentVersion = if ( Preferences . preferSubbed ) {
currentEpisode . versions . first { it . original }
} else {
currentEpisode . versions
. firstOrNull { it . audioLocale == currentAudioLocale . toLanguageTag ( ) }
?: currentEpisode . versions . first ( )
}
currentStreams = Crunchyroll . streamsFromMediaGUID ( currentVersion . mediaGUID )
Log . d ( classTag , currentVersion . toString ( ) )
2022-11-26 18:34:32 +01:00
} ,
viewModelScope . launch ( Dispatchers . IO ) {
Crunchyroll . playheads ( listOf ( currentEpisode . id ) ) [ currentEpisode . id ] ?. let {
// if the episode was fully watched, start at the beginning
currentPlayhead = if ( it . fullyWatched ) {
0
} else {
( it . playhead . times ( 1000 ) ) . toLong ( )
2022-01-29 19:56:39 +01:00
}
}
2022-11-26 18:34:32 +01:00
} ,
viewModelScope . launch ( Dispatchers . IO ) {
2023-01-25 19:51:38 +01:00
currentIntroMetadata = NoneDatalabIntro //Crunchyroll.datalabIntro(currentEpisode.id)
2022-11-26 18:34:32 +01:00
}
)
2023-02-19 14:21:46 +01:00
Log . d ( classTag , " streams: ${currentEpisode.streamsLink} " )
2021-12-26 20:22:00 +01:00
if ( startPlayback ) {
playCurrentMedia ( )
}
2020-12-27 20:11:01 +01:00
}
/ * *
2023-02-19 14:21:46 +01:00
* Play the current media from currentStreams .
2020-12-27 20:11:01 +01:00
*
2023-02-19 14:21:46 +01:00
* @param seekPosition The seek position for the media ( default = 0 ) .
2020-12-27 20:11:01 +01:00
* /
2021-12-26 20:22:00 +01:00
fun playCurrentMedia ( seekPosition : Long = 0 ) {
2021-12-29 20:51:53 +01:00
// get preferred stream url, set current language if it differs from the preferred one
2023-02-19 14:21:46 +01:00
val preferredLocale = currentSubtitleLocale
2021-12-29 20:51:53 +01:00
val fallbackLocal = Locale . US
val url = when {
2023-01-25 19:51:38 +01:00
currentStreams . data [ 0 ] . adaptive _hls . containsKey ( preferredLocale . toLanguageTag ( ) ) -> {
currentStreams . data [ 0 ] . adaptive _hls [ preferredLocale . toLanguageTag ( ) ] ?. url
2021-12-29 20:51:53 +01:00
}
2023-01-25 19:51:38 +01:00
currentStreams . data [ 0 ] . adaptive _hls . containsKey ( fallbackLocal . toLanguageTag ( ) ) -> {
2023-02-19 14:21:46 +01:00
currentSubtitleLocale = fallbackLocal
2023-01-25 19:51:38 +01:00
currentStreams . data [ 0 ] . adaptive _hls [ fallbackLocal . toLanguageTag ( ) ] ?. url
2021-12-29 20:51:53 +01:00
}
else -> {
2022-03-06 18:43:02 +01:00
// if no language tag is present use the first entry
2023-02-19 14:21:46 +01:00
currentSubtitleLocale = Locale . ROOT
2023-01-25 19:51:38 +01:00
currentStreams . data [ 0 ] . adaptive _hls . entries . first ( ) . value . url
2021-12-29 20:51:53 +01:00
}
}
2022-04-02 19:47:49 +02:00
Log . i ( classTag , " stream url: $url " )
2021-12-26 20:22:00 +01:00
2022-04-02 19:47:49 +02:00
// create the media item
val mediaItem = MediaItem . fromUri ( Uri . parse ( url ) )
player . setMediaItem ( mediaItem )
2021-12-26 20:22:00 +01:00
player . prepare ( )
2022-04-02 19:47:49 +02:00
2021-12-26 20:22:00 +01:00
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
}
}
2022-01-18 18:03:56 +01:00
/ * *
* Check if the current episode is the last in the episodes list .
*
* @return Boolean : true if it is the last , else false .
* /
fun currentEpisodeIsLastEpisode ( ) : Boolean {
2023-01-25 19:51:38 +01:00
return episodes . data . lastOrNull ( ) ?. id == currentEpisode . id
2022-01-18 18:03:56 +01:00
}
2022-03-20 14:29:32 +01:00
private suspend fun loadMediaMeta ( crSeriesId : String ) : Meta ? {
return MetaDBController . getTVShowMetadata ( crSeriesId )
}
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 )
2022-03-20 12:38:49 +01:00
if ( playhead > 0 && Preferences . updatePlayhead ) {
2022-07-10 13:50:53 +02:00
// don't use viewModelScope here. This task may needs to finish, when ViewModel will be cleared
CoroutineScope ( Dispatchers . IO ) . launch { Crunchyroll . postPlayheads ( currentEpisode . id , playhead . toInt ( ) ) }
2022-01-09 19:23:33 +01:00
Log . i ( javaClass . name , " Set playhead for episode ${currentEpisode.id} to $playhead sec. " )
}
2022-04-15 17:47:17 +02:00
viewModelScope . launch {
2023-01-25 19:51:38 +01:00
val episodeIDs = episodes . data . map { it . id }
2022-04-15 17:47:17 +02:00
currentPlayheads = Crunchyroll . playheads ( episodeIDs )
}
2022-01-09 19:23:33 +01:00
}
}