player language settings [Part 2]
* move player object to PlayerViewModel * minor code clean up
This commit is contained in:
		| @ -315,7 +315,6 @@ object AoDParser { | ||||
|                             description = ep.description, | ||||
|                             number = getNumberFromTitle(ep.title, media.type) | ||||
|                         )) | ||||
|                         println(getNumberFromTitle(ep.title, media.type)) | ||||
|                     } | ||||
|                 } catch (ex: Exception) { | ||||
|                     Log.w(javaClass.name, "Could not parse episode information.", ex) | ||||
|  | ||||
| @ -3,7 +3,6 @@ package org.mosad.teapod.player | ||||
| import android.animation.Animator | ||||
| import android.animation.AnimatorListenerAdapter | ||||
| import android.annotation.SuppressLint | ||||
| import android.net.Uri | ||||
| import android.os.Build | ||||
| import android.os.Bundle | ||||
| import android.util.Log | ||||
| @ -13,13 +12,8 @@ import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.core.view.GestureDetectorCompat | ||||
| import androidx.core.view.isVisible | ||||
| import com.google.android.exoplayer2.ExoPlayer | ||||
| import com.google.android.exoplayer2.MediaItem | ||||
| import com.google.android.exoplayer2.Player | ||||
| import com.google.android.exoplayer2.SimpleExoPlayer | ||||
| import com.google.android.exoplayer2.source.hls.HlsMediaSource | ||||
| import com.google.android.exoplayer2.ui.StyledPlayerControlView | ||||
| import com.google.android.exoplayer2.upstream.DataSource | ||||
| import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory | ||||
| import com.google.android.exoplayer2.util.Util | ||||
| import kotlinx.android.synthetic.main.activity_player.* | ||||
| import kotlinx.android.synthetic.main.player_controls.* | ||||
| @ -40,8 +34,6 @@ class PlayerActivity : AppCompatActivity() { | ||||
|  | ||||
|     private val model: PlayerViewModel by viewModels() | ||||
|  | ||||
|     private lateinit var player: SimpleExoPlayer | ||||
|     private lateinit var dataSourceFactory: DataSource.Factory | ||||
|     private lateinit var controller: StyledPlayerControlView | ||||
|     private lateinit var gestureDetector: GestureDetectorCompat | ||||
|     private lateinit var timerUpdates: TimerTask | ||||
| @ -52,8 +44,8 @@ class PlayerActivity : AppCompatActivity() { | ||||
|     private var playbackPosition: Long = 0 | ||||
|     private var remainingTime: Long = 0 | ||||
|  | ||||
|     private val rwdTime = 10000 | ||||
|     private val fwdTime = 10000 | ||||
|     private val rwdTime: Long = 10000.unaryMinus() | ||||
|     private val fwdTime: Long = 10000 | ||||
|  | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
| @ -117,8 +109,8 @@ class PlayerActivity : AppCompatActivity() { | ||||
|     } | ||||
|  | ||||
|     private fun initPlayer() { | ||||
|         if (model.mediaId <= 0) { | ||||
|             Log.e(javaClass.name, "No media id was set.") | ||||
|         if (model.media.id < 0) { | ||||
|             Log.e(javaClass.name, "No media was set.") | ||||
|             this.finish() | ||||
|         } | ||||
|  | ||||
| @ -134,14 +126,12 @@ class PlayerActivity : AppCompatActivity() { | ||||
|     } | ||||
|  | ||||
|     private fun initExoPlayer() { | ||||
|         player = SimpleExoPlayer.Builder(this).build() | ||||
|         dataSourceFactory = DefaultDataSourceFactory(this, Util.getUserAgent(this, "Teapod")) | ||||
|         controller = video_view.findViewById(R.id.exo_controller) | ||||
|  | ||||
|         controller.isAnimationEnabled = false // disable controls (time-bar) animation | ||||
|  | ||||
|         player.playWhenReady = playWhenReady | ||||
|         player.addListener(object : Player.EventListener { | ||||
|         model.player.playWhenReady = playWhenReady | ||||
|         model.player.addListener(object : Player.EventListener { | ||||
|             override fun onPlaybackStateChanged(state: Int) { | ||||
|                 super.onPlaybackStateChanged(state) | ||||
|  | ||||
| @ -172,7 +162,7 @@ class PlayerActivity : AppCompatActivity() { | ||||
|  | ||||
|     @SuppressLint("ClickableViewAccessibility") | ||||
|     private fun initVideoView() { | ||||
|         video_view.player = player | ||||
|         video_view.player = model.player | ||||
|  | ||||
|         // when the player controls get hidden, hide the bars too | ||||
|         video_view.setControllerVisibilityListener { | ||||
| @ -208,10 +198,10 @@ class PlayerActivity : AppCompatActivity() { | ||||
|                 var btnNextEpIsVisible: Boolean | ||||
|                 var controlsVisible: Boolean | ||||
|  | ||||
|                 withContext(Dispatchers.Main) { | ||||
|                     remainingTime = player.duration - player.currentPosition | ||||
|                     remainingTime = if (remainingTime < 0) 0 else remainingTime | ||||
|                 remainingTime = model.player.duration - model.player.currentPosition | ||||
|                 remainingTime = if (remainingTime < 0) 0 else remainingTime | ||||
|  | ||||
|                 withContext(Dispatchers.Main) { | ||||
|                     btnNextEpIsVisible = button_next_ep.isVisible | ||||
|                     controlsVisible = controller.isVisible | ||||
|                 } | ||||
| @ -234,10 +224,10 @@ class PlayerActivity : AppCompatActivity() { | ||||
|     } | ||||
|  | ||||
|     private fun releasePlayer(){ | ||||
|         playbackPosition = player.currentPosition | ||||
|         currentWindow = player.currentWindowIndex | ||||
|         playWhenReady = player.playWhenReady | ||||
|         player.release() | ||||
|         playbackPosition = model.player.currentPosition | ||||
|         currentWindow = model.player.currentWindowIndex | ||||
|         playWhenReady = model.player.playWhenReady | ||||
|         model.player.release() | ||||
|         timerUpdates.cancel() | ||||
|  | ||||
|         Log.d(javaClass.name, "Released player") | ||||
| @ -265,7 +255,7 @@ class PlayerActivity : AppCompatActivity() { | ||||
|      */ | ||||
|  | ||||
|     private fun rewind() { | ||||
|         player.seekTo(player.currentPosition - rwdTime) | ||||
|         model.seekToOffset(rwdTime) | ||||
|  | ||||
|         // hide/show needed components | ||||
|         exo_double_tap_indicator.visibility = View.VISIBLE | ||||
| @ -283,7 +273,7 @@ class PlayerActivity : AppCompatActivity() { | ||||
|     } | ||||
|  | ||||
|     private fun fastForward() { | ||||
|         player.seekTo(player.currentPosition + fwdTime) | ||||
|         model.seekToOffset(fwdTime) | ||||
|  | ||||
|         // hide/show needed components | ||||
|         exo_double_tap_indicator.visibility = View.VISIBLE | ||||
| @ -300,10 +290,6 @@ class PlayerActivity : AppCompatActivity() { | ||||
|         ffwd_10_indicator.runOnClickAnimation() | ||||
|     } | ||||
|  | ||||
|     private fun togglePausePlay() { | ||||
|         if (player.isPlaying) player.pause() else player.play() | ||||
|     } | ||||
|  | ||||
|     private fun playNextEpisode() = model.nextEpisode?.let { | ||||
|         model.nextEpisode() // current = next, next = new or null | ||||
|         hideButtonNextEp() | ||||
| @ -330,15 +316,8 @@ class PlayerActivity : AppCompatActivity() { | ||||
|         } | ||||
|  | ||||
|         // update player/media item | ||||
|         player.playWhenReady = true | ||||
|         player.clearMediaItems() //remove previous item | ||||
|         val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource( | ||||
|             MediaItem.fromUri(Uri.parse(model.autoSelectStream(model.currentEpisode))) | ||||
|         ) | ||||
|         if (seekToPosition) player.seekTo(playbackPosition) | ||||
|         player.setMediaSource(mediaSource) | ||||
|         player.prepare() | ||||
|  | ||||
|         val seekPosition =  if (seekToPosition) playbackPosition else 0 | ||||
|         model.playMedia(model.currentEpisode, true, seekPosition) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @ -393,23 +372,23 @@ class PlayerActivity : AppCompatActivity() { | ||||
|  | ||||
|     private fun showEpisodesList() { | ||||
|         val episodesList = EpisodesListPlayer(this, model = model).apply { | ||||
|             onViewRemovedAction = { player.play() } | ||||
|             onViewRemovedAction = { model.player.play() } | ||||
|         } | ||||
|         player_layout.addView(episodesList) | ||||
|  | ||||
|         // hide player controls and pause playback | ||||
|         player.pause() | ||||
|         model.player.pause() | ||||
|         controller.hide() | ||||
|     } | ||||
|  | ||||
|     private fun showLanguageSettings() { | ||||
|         val languageSettings = LanguageSettingsPlayer(this, model = model).apply { | ||||
|             onViewRemovedAction = { player.play() } | ||||
|             onViewRemovedAction = { model.player.play() } | ||||
|         } | ||||
|         player_layout.addView(languageSettings) | ||||
|  | ||||
|         // hide player controls and pause playback | ||||
|         player.pause() | ||||
|         model.player.pause() | ||||
|         controller.hideImmediately() | ||||
|     } | ||||
|  | ||||
| @ -447,7 +426,7 @@ class PlayerActivity : AppCompatActivity() { | ||||
|          * on long press toggle pause/play | ||||
|          */ | ||||
|         override fun onLongPress(e: MotionEvent?) { | ||||
|             togglePausePlay() | ||||
|             model.togglePausePlay() | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
| @ -1,7 +1,15 @@ | ||||
| package org.mosad.teapod.player | ||||
|  | ||||
| import android.util.Log | ||||
| import androidx.lifecycle.ViewModel | ||||
| import android.app.Application | ||||
| import android.net.Uri | ||||
| import androidx.lifecycle.AndroidViewModel | ||||
| import com.google.android.exoplayer2.C | ||||
| import com.google.android.exoplayer2.MediaItem | ||||
| import com.google.android.exoplayer2.SimpleExoPlayer | ||||
| 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 | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import org.mosad.teapod.parser.AoDParser | ||||
| import org.mosad.teapod.preferences.Preferences | ||||
| @ -18,63 +26,82 @@ import kotlin.properties.Delegates | ||||
|  * When currentEpisode is changed the player will start playing it (not initial media), | ||||
|  * the next episode will be update and the callback is handled. | ||||
|  */ | ||||
| class PlayerViewModel : ViewModel() { | ||||
| class PlayerViewModel(application: Application) : AndroidViewModel(application) { | ||||
|  | ||||
|     val player = SimpleExoPlayer.Builder(application).build() | ||||
|     val dataSourceFactory = DefaultDataSourceFactory(application, Util.getUserAgent(application, "Teapod")) | ||||
|  | ||||
|     val currentEpisodeChangedListener = ArrayList<() -> Unit>() | ||||
|  | ||||
|     var mediaId = 0 | ||||
|         internal set | ||||
|     var episodeId = 0 | ||||
|     var media: Media = Media(-1, "", DataTypes.MediaType.OTHER) | ||||
|         internal set | ||||
|  | ||||
|     var media: Media = Media(0, "", DataTypes.MediaType.OTHER) | ||||
|         internal set | ||||
|     // TODO rework | ||||
|     var currentEpisode: Episode by Delegates.observable(Episode()) { _, _, _ -> | ||||
|         currentEpisodeChangedListener.forEach { it() } | ||||
|         MediaFragment.instance.updateWatchedState(currentEpisode) // watchedCallback for the new episode | ||||
|         currentStreamUrl = autoSelectStream(currentEpisode) | ||||
|         nextEpisode = selectNextEpisode() // update next ep | ||||
|     } | ||||
|     var currentStreamUrl = "" // TODO don't save selected stream for language, instead save selected language | ||||
|         internal set | ||||
|     var nextEpisode: Episode? = null | ||||
|         internal set | ||||
|  | ||||
|     fun loadMedia(iMediaId: Int, iEpisodeId: Int) { | ||||
|         mediaId = iMediaId | ||||
|         episodeId = iEpisodeId | ||||
|  | ||||
|     fun loadMedia(mediaId: Int, episodeId: Int) { | ||||
|         runBlocking { | ||||
|             media = AoDParser.getMediaById(mediaId) | ||||
|         } | ||||
|  | ||||
|         currentEpisode = media.episodes.first { it.id == episodeId } | ||||
|         currentEpisode = media.getEpisodeById(episodeId) | ||||
|         currentStreamUrl = autoSelectStream(currentEpisode) | ||||
|         nextEpisode = selectNextEpisode() | ||||
|     } | ||||
|  | ||||
|         currentEpisode | ||||
|     fun changeLanguage(url: String) { | ||||
|         println("new stream is: $url") | ||||
|  | ||||
|         val seekTime = player.currentPosition | ||||
|         val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource( | ||||
|             MediaItem.fromUri(Uri.parse(url)) | ||||
|         ) | ||||
|         currentStreamUrl = url | ||||
|  | ||||
|         playMedia(mediaSource, true, seekTime) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * If preferSecondary or priStreamUrl is empty and secondary is present (secStreamOmU), | ||||
|      * use the secondary stream. Else, if the primary stream is set use the primary stream. | ||||
|      * If no stream is present, return empty string. | ||||
|      */ | ||||
|     fun autoSelectStream(episode: Episode): String { | ||||
|         return if (Preferences.preferSecondary) { | ||||
|             episode.getPreferredStream(Locale.JAPANESE).url | ||||
|         } else { | ||||
|             episode.getPreferredStream(Locale.GERMAN).url | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun changeLanguage(id: Int) { | ||||
|         println("new Language is ABC with id $id") | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * update currentEpisode, episodeId, nextEpisode to new episode | ||||
|      * update currentEpisode | ||||
|      * updateWatchedState for the next (now current) episode | ||||
|      */ | ||||
|     fun nextEpisode() = nextEpisode?.let { nextEp -> | ||||
|         currentEpisode = nextEp // set current ep to next ep | ||||
|         episodeId = nextEp.id | ||||
|     } | ||||
|  | ||||
|     // player actions | ||||
|     fun seekToOffset(offset: Long) { | ||||
|         player.seekTo(player.currentPosition + offset) | ||||
|     } | ||||
|  | ||||
|     fun togglePausePlay() { | ||||
|         if (player.isPlaying) player.pause() else player.play() | ||||
|     } | ||||
|  | ||||
|     fun playMedia(episode: Episode, replace: Boolean = false, seekPosition: Long = 0) { | ||||
|         val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource( | ||||
|             MediaItem.fromUri(Uri.parse(autoSelectStream(episode))) | ||||
|         ) | ||||
|  | ||||
|         playMedia(mediaSource, replace, seekPosition) | ||||
|     } | ||||
|  | ||||
|     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 | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @ -90,4 +117,18 @@ class PlayerViewModel : ViewModel() { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * If preferSecondary use the japanese stream, if present. | ||||
|      * If the preferred stream is not present the default (first) | ||||
|      * stream will be used | ||||
|      */ | ||||
|     private fun autoSelectStream(episode: Episode): String { | ||||
|         return if (Preferences.preferSecondary) { | ||||
|             episode.getPreferredStream(Locale.JAPANESE).url | ||||
|         } else { | ||||
|             episode.getPreferredStream(Locale.GERMAN).url | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
| } | ||||
| @ -28,15 +28,15 @@ class EpisodesListPlayer @JvmOverloads constructor( | ||||
|         } | ||||
|  | ||||
|         model?.let { | ||||
|             adapterRecEpisodes = PlayerEpisodeItemAdapter(it.media.episodes) | ||||
|             adapterRecEpisodes = PlayerEpisodeItemAdapter(model.media.episodes) | ||||
|  | ||||
|             adapterRecEpisodes.onImageClick = { _, position -> | ||||
|                 (this.parent as ViewGroup).removeView(this) | ||||
|                 it.currentEpisode = it.media.episodes[position] | ||||
|                 model.currentEpisode = model.media.episodes[position] | ||||
|             } | ||||
|  | ||||
|             binding.recyclerEpisodesPlayer.adapter = adapterRecEpisodes | ||||
|             binding.recyclerEpisodesPlayer.scrollToPosition(it.currentEpisode.number - 1) // number != index | ||||
|             binding.recyclerEpisodesPlayer.scrollToPosition(model.currentEpisode.number - 1) // number != index | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -10,6 +10,7 @@ import android.view.LayoutInflater | ||||
| import android.view.ViewGroup | ||||
| import android.widget.LinearLayout | ||||
| import android.widget.TextView | ||||
| import androidx.core.view.children | ||||
| import org.mosad.teapod.R | ||||
| import org.mosad.teapod.databinding.PlayerLanguageSettingsBinding | ||||
| import org.mosad.teapod.player.PlayerViewModel | ||||
| @ -24,12 +25,24 @@ class LanguageSettingsPlayer @JvmOverloads constructor( | ||||
|     private val binding = PlayerLanguageSettingsBinding.inflate(LayoutInflater.from(context), this, true) | ||||
|     var onViewRemovedAction: (() -> Unit)? = null // TODO find a better solution for this | ||||
|  | ||||
|     private var currentStreamUrl = model?.currentStreamUrl ?: "" | ||||
|  | ||||
|     init { | ||||
|         addLanguage("primary", true) { model?.changeLanguage(0) } | ||||
|         addLanguage("secondary", false ) { model?.changeLanguage(1) } | ||||
|         model?.let { | ||||
|             model.currentEpisode.streams.forEach { stream -> | ||||
|                 addLanguage(stream.language.displayName, stream.url == currentStreamUrl) { | ||||
|                     currentStreamUrl = stream.url | ||||
|                     updateSelectedLanguage(it as TextView) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         binding.buttonCloseLanguageSettings.setOnClickListener { close() } | ||||
|         binding.buttonCancel.setOnClickListener { close() } | ||||
|         binding.buttonSelect.setOnClickListener { | ||||
|             model?.changeLanguage(currentStreamUrl) | ||||
|             close() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun addLanguage(str: String, isSelected: Boolean, onClick: OnClickListener) { | ||||
| @ -56,6 +69,31 @@ class LanguageSettingsPlayer @JvmOverloads constructor( | ||||
|         binding.linearLanguages.addView(text) | ||||
|     } | ||||
|  | ||||
|     private fun updateSelectedLanguage(selected: TextView) { | ||||
|         // rest all tf to not selected style | ||||
|         binding.linearLanguages.children.forEach { child -> | ||||
|             if (child is TextView) { | ||||
|                 child.apply { | ||||
|                     setTextColor(context.resources.getColor(R.color.textPrimaryDark, context.theme)) | ||||
|                     setTypeface(null, Typeface.NORMAL) | ||||
|                     setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0) | ||||
|                     setPadding(75, 0, 0, 0) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
|         // set selected to selected style | ||||
|         selected.apply { | ||||
|             setTextColor(context.resources.getColor(R.color.exo_white, context.theme)) | ||||
|             setTypeface(null, Typeface.BOLD) | ||||
|             setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_baseline_check_24, 0, 0, 0) | ||||
|             setPadding(0, 0, 0, 0) | ||||
|             compoundDrawablesRelative.getOrNull(0)?.setTint(Color.WHITE) | ||||
|             compoundDrawablePadding = 12 | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun close() { | ||||
|         (this.parent as ViewGroup).removeView(this) | ||||
|         onViewRemovedAction?.invoke() | ||||
|  | ||||
| @ -171,8 +171,12 @@ class MediaFragment(private val mediaId: Int) : Fragment() { | ||||
|  | ||||
|     fun updateWatchedState(ep: Episode) { | ||||
|         AoDParser.sendCallback(ep.watchedCallback) | ||||
|         adapterRecEpisodes.updateWatchedState(true, media.episodes.indexOf(ep)) | ||||
|         adapterRecEpisodes.notifyDataSetChanged() | ||||
|  | ||||
|         // only notify adapter, if initialized | ||||
|         if (this::adapterRecEpisodes.isInitialized) { | ||||
|             adapterRecEpisodes.updateWatchedState(true, media.episodes.indexOf(ep)) | ||||
|             adapterRecEpisodes.notifyDataSetChanged() | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @ -49,7 +49,7 @@ data class Media( | ||||
|     val link: String, | ||||
|     val type: DataTypes.MediaType, | ||||
|     val info: Info = Info(), | ||||
|     var episodes: ArrayList<Episode> = arrayListOf() | ||||
|     val episodes: ArrayList<Episode> = arrayListOf() | ||||
| ) { | ||||
|     fun hasEpisode(id: Int) = episodes.any { it.id == id } | ||||
|     fun getEpisodeById(id: Int) = episodes.first { it.id == id } | ||||
| @ -71,11 +71,11 @@ data class Info( | ||||
| data class Episode( | ||||
|     val id: Int = 0, | ||||
|     val streams: MutableList<Stream> = mutableListOf(), | ||||
|     var title: String = "", | ||||
|     var posterUrl: String = "", | ||||
|     var description: String = "", | ||||
|     val title: String = "", | ||||
|     val posterUrl: String = "", | ||||
|     val description: String = "", | ||||
|     var shortDesc: String = "", | ||||
|     var number: Int = 0, | ||||
|     val number: Int = 0, | ||||
|     var watched: Boolean = false, | ||||
|     var watchedCallback: String = "" | ||||
| ) { | ||||
|  | ||||
		Reference in New Issue
	
	Block a user