player language settings [Part 2]

* move player object to PlayerViewModel
* minor code clean up
This commit is contained in:
Jannik 2020-12-26 20:09:35 +01:00
parent 8a43567737
commit 94da8c6cee
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
7 changed files with 151 additions and 90 deletions

View File

@ -315,7 +315,6 @@ object AoDParser {
description = ep.description, description = ep.description,
number = getNumberFromTitle(ep.title, media.type) number = getNumberFromTitle(ep.title, media.type)
)) ))
println(getNumberFromTitle(ep.title, media.type))
} }
} catch (ex: Exception) { } catch (ex: Exception) {
Log.w(javaClass.name, "Could not parse episode information.", ex) Log.w(javaClass.name, "Could not parse episode information.", ex)

View File

@ -3,7 +3,6 @@ package org.mosad.teapod.player
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
@ -13,13 +12,8 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GestureDetectorCompat import androidx.core.view.GestureDetectorCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player 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.ui.StyledPlayerControlView
import com.google.android.exoplayer2.upstream.DataSource
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.util.Util import com.google.android.exoplayer2.util.Util
import kotlinx.android.synthetic.main.activity_player.* import kotlinx.android.synthetic.main.activity_player.*
import kotlinx.android.synthetic.main.player_controls.* import kotlinx.android.synthetic.main.player_controls.*
@ -40,8 +34,6 @@ class PlayerActivity : AppCompatActivity() {
private val model: PlayerViewModel by viewModels() 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 controller: StyledPlayerControlView
private lateinit var gestureDetector: GestureDetectorCompat private lateinit var gestureDetector: GestureDetectorCompat
private lateinit var timerUpdates: TimerTask private lateinit var timerUpdates: TimerTask
@ -52,8 +44,8 @@ class PlayerActivity : AppCompatActivity() {
private var playbackPosition: Long = 0 private var playbackPosition: Long = 0
private var remainingTime: Long = 0 private var remainingTime: Long = 0
private val rwdTime = 10000 private val rwdTime: Long = 10000.unaryMinus()
private val fwdTime = 10000 private val fwdTime: Long = 10000
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -117,8 +109,8 @@ class PlayerActivity : AppCompatActivity() {
} }
private fun initPlayer() { private fun initPlayer() {
if (model.mediaId <= 0) { if (model.media.id < 0) {
Log.e(javaClass.name, "No media id was set.") Log.e(javaClass.name, "No media was set.")
this.finish() this.finish()
} }
@ -134,14 +126,12 @@ class PlayerActivity : AppCompatActivity() {
} }
private fun initExoPlayer() { private fun initExoPlayer() {
player = SimpleExoPlayer.Builder(this).build()
dataSourceFactory = DefaultDataSourceFactory(this, Util.getUserAgent(this, "Teapod"))
controller = video_view.findViewById(R.id.exo_controller) controller = video_view.findViewById(R.id.exo_controller)
controller.isAnimationEnabled = false // disable controls (time-bar) animation controller.isAnimationEnabled = false // disable controls (time-bar) animation
player.playWhenReady = playWhenReady model.player.playWhenReady = playWhenReady
player.addListener(object : Player.EventListener { model.player.addListener(object : Player.EventListener {
override fun onPlaybackStateChanged(state: Int) { override fun onPlaybackStateChanged(state: Int) {
super.onPlaybackStateChanged(state) super.onPlaybackStateChanged(state)
@ -172,7 +162,7 @@ class PlayerActivity : AppCompatActivity() {
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
private fun initVideoView() { private fun initVideoView() {
video_view.player = player video_view.player = model.player
// when the player controls get hidden, hide the bars too // when the player controls get hidden, hide the bars too
video_view.setControllerVisibilityListener { video_view.setControllerVisibilityListener {
@ -208,10 +198,10 @@ class PlayerActivity : AppCompatActivity() {
var btnNextEpIsVisible: Boolean var btnNextEpIsVisible: Boolean
var controlsVisible: Boolean var controlsVisible: Boolean
withContext(Dispatchers.Main) { remainingTime = model.player.duration - model.player.currentPosition
remainingTime = player.duration - player.currentPosition remainingTime = if (remainingTime < 0) 0 else remainingTime
remainingTime = if (remainingTime < 0) 0 else remainingTime
withContext(Dispatchers.Main) {
btnNextEpIsVisible = button_next_ep.isVisible btnNextEpIsVisible = button_next_ep.isVisible
controlsVisible = controller.isVisible controlsVisible = controller.isVisible
} }
@ -234,10 +224,10 @@ class PlayerActivity : AppCompatActivity() {
} }
private fun releasePlayer(){ private fun releasePlayer(){
playbackPosition = player.currentPosition playbackPosition = model.player.currentPosition
currentWindow = player.currentWindowIndex currentWindow = model.player.currentWindowIndex
playWhenReady = player.playWhenReady playWhenReady = model.player.playWhenReady
player.release() model.player.release()
timerUpdates.cancel() timerUpdates.cancel()
Log.d(javaClass.name, "Released player") Log.d(javaClass.name, "Released player")
@ -265,7 +255,7 @@ class PlayerActivity : AppCompatActivity() {
*/ */
private fun rewind() { private fun rewind() {
player.seekTo(player.currentPosition - rwdTime) model.seekToOffset(rwdTime)
// hide/show needed components // hide/show needed components
exo_double_tap_indicator.visibility = View.VISIBLE exo_double_tap_indicator.visibility = View.VISIBLE
@ -283,7 +273,7 @@ class PlayerActivity : AppCompatActivity() {
} }
private fun fastForward() { private fun fastForward() {
player.seekTo(player.currentPosition + fwdTime) model.seekToOffset(fwdTime)
// hide/show needed components // hide/show needed components
exo_double_tap_indicator.visibility = View.VISIBLE exo_double_tap_indicator.visibility = View.VISIBLE
@ -300,10 +290,6 @@ class PlayerActivity : AppCompatActivity() {
ffwd_10_indicator.runOnClickAnimation() ffwd_10_indicator.runOnClickAnimation()
} }
private fun togglePausePlay() {
if (player.isPlaying) player.pause() else player.play()
}
private fun playNextEpisode() = model.nextEpisode?.let { private fun playNextEpisode() = model.nextEpisode?.let {
model.nextEpisode() // current = next, next = new or null model.nextEpisode() // current = next, next = new or null
hideButtonNextEp() hideButtonNextEp()
@ -330,15 +316,8 @@ class PlayerActivity : AppCompatActivity() {
} }
// update player/media item // update player/media item
player.playWhenReady = true val seekPosition = if (seekToPosition) playbackPosition else 0
player.clearMediaItems() //remove previous item model.playMedia(model.currentEpisode, true, seekPosition)
val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(
MediaItem.fromUri(Uri.parse(model.autoSelectStream(model.currentEpisode)))
)
if (seekToPosition) player.seekTo(playbackPosition)
player.setMediaSource(mediaSource)
player.prepare()
} }
/** /**
@ -393,23 +372,23 @@ class PlayerActivity : AppCompatActivity() {
private fun showEpisodesList() { private fun showEpisodesList() {
val episodesList = EpisodesListPlayer(this, model = model).apply { val episodesList = EpisodesListPlayer(this, model = model).apply {
onViewRemovedAction = { player.play() } onViewRemovedAction = { model.player.play() }
} }
player_layout.addView(episodesList) player_layout.addView(episodesList)
// hide player controls and pause playback // hide player controls and pause playback
player.pause() model.player.pause()
controller.hide() controller.hide()
} }
private fun showLanguageSettings() { private fun showLanguageSettings() {
val languageSettings = LanguageSettingsPlayer(this, model = model).apply { val languageSettings = LanguageSettingsPlayer(this, model = model).apply {
onViewRemovedAction = { player.play() } onViewRemovedAction = { model.player.play() }
} }
player_layout.addView(languageSettings) player_layout.addView(languageSettings)
// hide player controls and pause playback // hide player controls and pause playback
player.pause() model.player.pause()
controller.hideImmediately() controller.hideImmediately()
} }
@ -447,7 +426,7 @@ class PlayerActivity : AppCompatActivity() {
* on long press toggle pause/play * on long press toggle pause/play
*/ */
override fun onLongPress(e: MotionEvent?) { override fun onLongPress(e: MotionEvent?) {
togglePausePlay() model.togglePausePlay()
} }
} }

View File

@ -1,7 +1,15 @@
package org.mosad.teapod.player package org.mosad.teapod.player
import android.util.Log import android.app.Application
import androidx.lifecycle.ViewModel 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 kotlinx.coroutines.runBlocking
import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.preferences.Preferences 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), * When currentEpisode is changed the player will start playing it (not initial media),
* the next episode will be update and the callback is handled. * 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>() val currentEpisodeChangedListener = ArrayList<() -> Unit>()
var mediaId = 0 var media: Media = Media(-1, "", DataTypes.MediaType.OTHER)
internal set
var episodeId = 0
internal set internal set
var media: Media = Media(0, "", DataTypes.MediaType.OTHER) // TODO rework
internal set
var currentEpisode: Episode by Delegates.observable(Episode()) { _, _, _ -> var currentEpisode: Episode by Delegates.observable(Episode()) { _, _, _ ->
currentEpisodeChangedListener.forEach { it() } currentEpisodeChangedListener.forEach { it() }
MediaFragment.instance.updateWatchedState(currentEpisode) // watchedCallback for the new episode MediaFragment.instance.updateWatchedState(currentEpisode) // watchedCallback for the new episode
currentStreamUrl = autoSelectStream(currentEpisode)
nextEpisode = selectNextEpisode() // update next ep 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 var nextEpisode: Episode? = null
internal set internal set
fun loadMedia(iMediaId: Int, iEpisodeId: Int) { fun loadMedia(mediaId: Int, episodeId: Int) {
mediaId = iMediaId
episodeId = iEpisodeId
runBlocking { runBlocking {
media = AoDParser.getMediaById(mediaId) media = AoDParser.getMediaById(mediaId)
} }
currentEpisode = media.episodes.first { it.id == episodeId } currentEpisode = media.getEpisodeById(episodeId)
currentStreamUrl = autoSelectStream(currentEpisode)
nextEpisode = selectNextEpisode() 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), * update currentEpisode
* 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
* updateWatchedState for the next (now current) episode * updateWatchedState for the next (now current) episode
*/ */
fun nextEpisode() = nextEpisode?.let { nextEp -> fun nextEpisode() = nextEpisode?.let { nextEp ->
currentEpisode = nextEp // set current ep to next ep 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
}
}
} }

View File

@ -28,15 +28,15 @@ class EpisodesListPlayer @JvmOverloads constructor(
} }
model?.let { model?.let {
adapterRecEpisodes = PlayerEpisodeItemAdapter(it.media.episodes) adapterRecEpisodes = PlayerEpisodeItemAdapter(model.media.episodes)
adapterRecEpisodes.onImageClick = { _, position -> adapterRecEpisodes.onImageClick = { _, position ->
(this.parent as ViewGroup).removeView(this) (this.parent as ViewGroup).removeView(this)
it.currentEpisode = it.media.episodes[position] model.currentEpisode = model.media.episodes[position]
} }
binding.recyclerEpisodesPlayer.adapter = adapterRecEpisodes binding.recyclerEpisodesPlayer.adapter = adapterRecEpisodes
binding.recyclerEpisodesPlayer.scrollToPosition(it.currentEpisode.number - 1) // number != index binding.recyclerEpisodesPlayer.scrollToPosition(model.currentEpisode.number - 1) // number != index
} }
} }

View File

@ -10,6 +10,7 @@ import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.view.children
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.databinding.PlayerLanguageSettingsBinding import org.mosad.teapod.databinding.PlayerLanguageSettingsBinding
import org.mosad.teapod.player.PlayerViewModel import org.mosad.teapod.player.PlayerViewModel
@ -24,12 +25,24 @@ class LanguageSettingsPlayer @JvmOverloads constructor(
private val binding = PlayerLanguageSettingsBinding.inflate(LayoutInflater.from(context), this, true) private val binding = PlayerLanguageSettingsBinding.inflate(LayoutInflater.from(context), this, true)
var onViewRemovedAction: (() -> Unit)? = null // TODO find a better solution for this var onViewRemovedAction: (() -> Unit)? = null // TODO find a better solution for this
private var currentStreamUrl = model?.currentStreamUrl ?: ""
init { init {
addLanguage("primary", true) { model?.changeLanguage(0) } model?.let {
addLanguage("secondary", false ) { model?.changeLanguage(1) } model.currentEpisode.streams.forEach { stream ->
addLanguage(stream.language.displayName, stream.url == currentStreamUrl) {
currentStreamUrl = stream.url
updateSelectedLanguage(it as TextView)
}
}
}
binding.buttonCloseLanguageSettings.setOnClickListener { close() } binding.buttonCloseLanguageSettings.setOnClickListener { close() }
binding.buttonCancel.setOnClickListener { close() } binding.buttonCancel.setOnClickListener { close() }
binding.buttonSelect.setOnClickListener {
model?.changeLanguage(currentStreamUrl)
close()
}
} }
private fun addLanguage(str: String, isSelected: Boolean, onClick: OnClickListener) { private fun addLanguage(str: String, isSelected: Boolean, onClick: OnClickListener) {
@ -56,6 +69,31 @@ class LanguageSettingsPlayer @JvmOverloads constructor(
binding.linearLanguages.addView(text) 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() { private fun close() {
(this.parent as ViewGroup).removeView(this) (this.parent as ViewGroup).removeView(this)
onViewRemovedAction?.invoke() onViewRemovedAction?.invoke()

View File

@ -171,8 +171,12 @@ class MediaFragment(private val mediaId: Int) : Fragment() {
fun updateWatchedState(ep: Episode) { fun updateWatchedState(ep: Episode) {
AoDParser.sendCallback(ep.watchedCallback) 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()
}
} }
} }

View File

@ -49,7 +49,7 @@ data class Media(
val link: String, val link: String,
val type: DataTypes.MediaType, val type: DataTypes.MediaType,
val info: Info = Info(), val info: Info = Info(),
var episodes: ArrayList<Episode> = arrayListOf() val episodes: ArrayList<Episode> = arrayListOf()
) { ) {
fun hasEpisode(id: Int) = episodes.any { it.id == id } fun hasEpisode(id: Int) = episodes.any { it.id == id }
fun getEpisodeById(id: Int) = episodes.first { it.id == id } fun getEpisodeById(id: Int) = episodes.first { it.id == id }
@ -71,11 +71,11 @@ data class Info(
data class Episode( data class Episode(
val id: Int = 0, val id: Int = 0,
val streams: MutableList<Stream> = mutableListOf(), val streams: MutableList<Stream> = mutableListOf(),
var title: String = "", val title: String = "",
var posterUrl: String = "", val posterUrl: String = "",
var description: String = "", val description: String = "",
var shortDesc: String = "", var shortDesc: String = "",
var number: Int = 0, val number: Int = 0,
var watched: Boolean = false, var watched: Boolean = false,
var watchedCallback: String = "" var watchedCallback: String = ""
) { ) {