player language settings [Part 2]
* move player object to PlayerViewModel * minor code clean up
This commit is contained in:
parent
8a43567737
commit
94da8c6cee
|
@ -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 = ""
|
||||
) {
|
||||
|
|
Loading…
Reference in New Issue