From 94da8c6ceeb0a6f8d45a953d6b488e494f398e84 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 26 Dec 2020 20:09:35 +0100 Subject: [PATCH] player language settings [Part 2] * move player object to PlayerViewModel * minor code clean up --- .../java/org/mosad/teapod/parser/AoDParser.kt | 1 - .../org/mosad/teapod/player/PlayerActivity.kt | 67 ++++------- .../mosad/teapod/player/PlayerViewModel.kt | 107 ++++++++++++------ .../ui/components/EpisodesListPlayer.kt | 6 +- .../ui/components/LanguageSettingsPlayer.kt | 42 ++++++- .../teapod/ui/fragments/MediaFragment.kt | 8 +- .../java/org/mosad/teapod/util/DataTypes.kt | 10 +- 7 files changed, 151 insertions(+), 90 deletions(-) diff --git a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt index 9e38568..0943256 100644 --- a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt +++ b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt @@ -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) diff --git a/app/src/main/java/org/mosad/teapod/player/PlayerActivity.kt b/app/src/main/java/org/mosad/teapod/player/PlayerActivity.kt index 72fbf5f..9c13c45 100644 --- a/app/src/main/java/org/mosad/teapod/player/PlayerActivity.kt +++ b/app/src/main/java/org/mosad/teapod/player/PlayerActivity.kt @@ -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() } } diff --git a/app/src/main/java/org/mosad/teapod/player/PlayerViewModel.kt b/app/src/main/java/org/mosad/teapod/player/PlayerViewModel.kt index ea7a9a8..e87b378 100644 --- a/app/src/main/java/org/mosad/teapod/player/PlayerViewModel.kt +++ b/app/src/main/java/org/mosad/teapod/player/PlayerViewModel.kt @@ -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 + } + } + + } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt b/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt index ef03512..5799a53 100644 --- a/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt +++ b/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt @@ -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 } } diff --git a/app/src/main/java/org/mosad/teapod/ui/components/LanguageSettingsPlayer.kt b/app/src/main/java/org/mosad/teapod/ui/components/LanguageSettingsPlayer.kt index 5eb2ace..9329cc6 100644 --- a/app/src/main/java/org/mosad/teapod/ui/components/LanguageSettingsPlayer.kt +++ b/app/src/main/java/org/mosad/teapod/ui/components/LanguageSettingsPlayer.kt @@ -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() diff --git a/app/src/main/java/org/mosad/teapod/ui/fragments/MediaFragment.kt b/app/src/main/java/org/mosad/teapod/ui/fragments/MediaFragment.kt index 8b7197a..5dc630a 100644 --- a/app/src/main/java/org/mosad/teapod/ui/fragments/MediaFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/fragments/MediaFragment.kt @@ -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() + } } } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt index 0aa9094..1ec2faa 100644 --- a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt +++ b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt @@ -49,7 +49,7 @@ data class Media( val link: String, val type: DataTypes.MediaType, val info: Info = Info(), - var episodes: ArrayList = arrayListOf() + val episodes: ArrayList = 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 = 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 = "" ) {