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,
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)

View File

@ -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()
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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()

View File

@ -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()
}
}
}

View File

@ -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 = ""
) {