2020-11-25 22:35:55 +01:00
package org.mosad.teapod.player
2020-10-11 13:18:20 +02:00
2020-11-15 13:39:33 +01:00
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
2020-11-08 18:05:46 +01:00
import android.annotation.SuppressLint
2020-10-11 13:18:20 +02:00
import android.net.Uri
2020-10-11 14:14:38 +02:00
import android.os.Build
2020-10-11 13:18:20 +02:00
import android.os.Bundle
2020-10-11 14:14:38 +02:00
import android.util.Log
2020-11-08 18:05:46 +01:00
import android.view.*
2020-11-25 16:04:04 +01:00
import androidx.activity.viewModels
2020-10-11 13:18:20 +02:00
import androidx.appcompat.app.AppCompatActivity
2020-11-08 18:05:46 +01:00
import androidx.core.view.GestureDetectorCompat
2020-11-13 15:36:12 +01:00
import androidx.core.view.isVisible
2020-11-21 19:40:55 +01:00
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
2020-10-11 13:18:20 +02:00
import com.google.android.exoplayer2.source.hls.HlsMediaSource
2020-11-06 10:21:57 +01:00
import com.google.android.exoplayer2.ui.StyledPlayerControlView
2020-10-11 13:18:20 +02:00
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.*
2020-11-05 20:07:41 +01:00
import kotlinx.android.synthetic.main.player_controls.*
2020-11-20 11:20:11 +01:00
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
2020-11-25 22:35:55 +01:00
import org.mosad.teapod.R
2020-11-13 11:23:09 +01:00
import org.mosad.teapod.preferences.Preferences
2020-12-15 23:15:14 +01:00
import org.mosad.teapod.ui.components.EpisodesListPlayer
2020-12-02 11:14:09 +01:00
import org.mosad.teapod.util.DataTypes
2020-11-15 17:17:56 +01:00
import java.util.*
2020-11-07 13:33:59 +01:00
import java.util.concurrent.TimeUnit
2020-11-15 17:17:56 +01:00
import kotlin.concurrent.scheduleAtFixedRate
2020-10-11 13:18:20 +02:00
class PlayerActivity : AppCompatActivity ( ) {
2020-11-25 22:35:55 +01:00
private val model : PlayerViewModel by viewModels ( )
2020-10-11 13:18:20 +02:00
private lateinit var player : SimpleExoPlayer
private lateinit var dataSourceFactory : DataSource . Factory
2020-11-06 10:21:57 +01:00
private lateinit var controller : StyledPlayerControlView
2020-11-08 18:05:46 +01:00
private lateinit var gestureDetector : GestureDetectorCompat
2020-11-15 17:17:56 +01:00
private lateinit var timerUpdates : TimerTask
2020-10-11 13:18:20 +02:00
2020-12-02 11:14:09 +01:00
private var nextEpManually = false
2020-10-11 13:18:20 +02:00
private var playWhenReady = true
private var currentWindow = 0
private var playbackPosition : Long = 0
2020-11-07 13:33:59 +01:00
private var remainingTime : Long = 0
2020-10-11 13:18:20 +02:00
2020-11-05 20:07:41 +01:00
private val rwdTime = 10000
private val fwdTime = 10000
2020-10-11 14:14:38 +02:00
2020-10-11 13:18:20 +02:00
override fun onCreate ( savedInstanceState : Bundle ? ) {
super . onCreate ( savedInstanceState )
setContentView ( R . layout . activity _player )
2020-10-11 14:14:38 +02:00
hideBars ( ) // Initial hide the bars
2020-10-11 13:18:20 +02:00
2020-10-11 15:19:42 +02:00
savedInstanceState ?. let {
currentWindow = it . getInt ( getString ( R . string . state _resume _window ) )
playbackPosition = it . getLong ( getString ( R . string . state _resume _position ) )
playWhenReady = it . getBoolean ( getString ( R . string . state _is _playing ) )
}
2020-11-13 11:23:09 +01:00
2020-11-25 16:04:04 +01:00
model . loadMedia (
intent . getIntExtra ( getString ( R . string . intent _media _id ) , 0 ) ,
intent . getIntExtra ( getString ( R . string . intent _episode _id ) , 0 )
)
2020-11-05 20:07:41 +01:00
2020-11-08 18:05:46 +01:00
gestureDetector = GestureDetectorCompat ( this , PlayerGestureListener ( ) )
2020-11-05 20:07:41 +01:00
initActions ( )
2020-10-11 13:18:20 +02:00
}
override fun onStart ( ) {
super . onStart ( )
if ( Util . SDK _INT > 23 ) {
initPlayer ( )
2020-11-25 22:35:55 +01:00
video _view ?. onResume ( )
2020-10-11 13:18:20 +02:00
}
}
override fun onResume ( ) {
super . onResume ( )
if ( Util . SDK _INT <= 23 ) {
initPlayer ( )
2020-11-25 22:35:55 +01:00
video _view ?. onResume ( )
2020-10-11 13:18:20 +02:00
}
}
override fun onPause ( ) {
super . onPause ( )
if ( Util . SDK _INT <= 23 ) {
2020-11-25 22:35:55 +01:00
video _view ?. onPause ( )
2020-10-11 13:18:20 +02:00
releasePlayer ( )
}
}
override fun onStop ( ) {
super . onStop ( )
if ( Util . SDK _INT > 23 ) {
2020-11-08 18:05:46 +01:00
video _view ?. onPause ( )
2020-10-11 13:18:20 +02:00
releasePlayer ( )
}
}
2020-10-11 15:19:42 +02:00
override fun onSaveInstanceState ( outState : Bundle ) {
outState . putInt ( getString ( R . string . state _resume _window ) , currentWindow )
outState . putLong ( getString ( R . string . state _resume _position ) , playbackPosition )
outState . putBoolean ( getString ( R . string . state _is _playing ) , playWhenReady )
super . onSaveInstanceState ( outState )
}
2020-10-11 13:18:20 +02:00
private fun initPlayer ( ) {
2020-11-25 16:04:04 +01:00
if ( model . mediaId <= 0 ) {
2020-11-13 11:23:09 +01:00
Log . e ( javaClass . name , " No media id was set. " )
this . finish ( )
2020-10-11 13:18:20 +02:00
}
2020-11-08 18:05:46 +01:00
initExoPlayer ( )
initVideoView ( )
2020-11-13 15:36:12 +01:00
initTimeUpdates ( )
2020-12-15 23:15:14 +01:00
// add listener after initial media is started
model . currentEpisodeChangedListener . add {
nextEpManually = true // make sure on STATE_ENDED doesn't skip another episode
playCurrentMedia ( false )
}
2020-11-08 18:05:46 +01:00
}
private fun initExoPlayer ( ) {
2020-10-11 13:18:20 +02:00
player = SimpleExoPlayer . Builder ( this ) . build ( )
dataSourceFactory = DefaultDataSourceFactory ( this , Util . getUserAgent ( this , " Teapod " ) )
2020-11-07 13:33:59 +01:00
controller = video _view . findViewById ( R . id . exo _controller )
2020-10-11 13:18:20 +02:00
2020-12-02 11:14:09 +01:00
controller . isAnimationEnabled = false // disable controls (time-bar) animation
2020-10-11 13:18:20 +02:00
2020-10-11 15:19:42 +02:00
player . playWhenReady = playWhenReady
2020-10-11 13:18:20 +02:00
player . addListener ( object : Player . EventListener {
override fun onPlaybackStateChanged ( state : Int ) {
super . onPlaybackStateChanged ( state )
loading . visibility = when ( state ) {
2020-10-11 14:14:38 +02:00
ExoPlayer . STATE _READY -> View . GONE
2020-10-11 13:18:20 +02:00
ExoPlayer . STATE _BUFFERING -> View . VISIBLE
else -> View . GONE
}
2020-11-05 20:07:41 +01:00
exo _play _pause . visibility = when ( loading . visibility ) {
View . GONE -> View . VISIBLE
View . VISIBLE -> View . INVISIBLE
else -> View . VISIBLE
}
2020-11-13 15:36:12 +01:00
2020-11-25 16:04:04 +01:00
if ( state == ExoPlayer . STATE _ENDED && model . nextEpisode != null && Preferences . autoplay ) {
2020-12-14 23:46:55 +01:00
// if next episode btn was clicked, skipp playNextEpisode() on STATE_ENDED
2020-12-15 23:15:14 +01:00
if ( ! nextEpManually ) {
2020-12-02 11:14:09 +01:00
playNextEpisode ( )
}
2020-12-15 23:15:14 +01:00
nextEpManually = false
2020-11-13 15:36:12 +01:00
}
2020-10-11 13:18:20 +02:00
}
} )
2020-11-21 18:05:34 +01:00
2020-12-15 23:15:14 +01:00
playCurrentMedia ( true ) // start initial media
2020-11-08 18:05:46 +01:00
}
@SuppressLint ( " ClickableViewAccessibility " )
private fun initVideoView ( ) {
2020-11-07 18:23:09 +01:00
video _view . player = player
2020-10-11 13:18:20 +02:00
2020-10-11 14:14:38 +02:00
// when the player controls get hidden, hide the bars too
video _view . setControllerVisibilityListener {
2020-11-21 18:05:34 +01:00
when ( it ) {
View . GONE -> hideBars ( )
View . VISIBLE -> updateControls ( )
2020-11-07 13:33:59 +01:00
}
}
2020-11-08 18:05:46 +01:00
video _view . setOnTouchListener { _ , event ->
gestureDetector . onTouchEvent ( event )
true
}
}
2020-11-05 20:07:41 +01:00
private fun initActions ( ) {
2020-11-08 18:05:46 +01:00
exo _close _player . setOnClickListener { this . finish ( ) }
2020-11-16 19:23:06 +01:00
rwd _10 . setOnButtonClickListener { rewind ( ) }
2020-11-25 23:33:06 +01:00
ffwd _10 . setOnButtonClickListener { fastForward ( ) }
2020-11-13 15:36:12 +01:00
button _next _ep . setOnClickListener { playNextEpisode ( ) }
2020-12-02 11:14:09 +01:00
button _next _ep _c . setOnClickListener { playNextEpisode ( ) }
2020-12-14 23:46:55 +01:00
button _episodes . setOnClickListener { showEpisodesList ( ) }
2020-11-13 15:36:12 +01:00
}
2020-11-15 17:17:56 +01:00
private fun initTimeUpdates ( ) {
if ( this :: timerUpdates . isInitialized ) {
timerUpdates . cancel ( )
}
2020-11-21 18:05:34 +01:00
timerUpdates = Timer ( ) . scheduleAtFixedRate ( 0 , 500 ) {
2020-11-15 17:17:56 +01:00
GlobalScope . launch {
var btnNextEpIsVisible : Boolean
2020-11-21 18:05:34 +01:00
var controlsVisible : Boolean
2020-11-13 15:36:12 +01:00
withContext ( Dispatchers . Main ) {
2020-11-15 17:17:56 +01:00
remainingTime = player . duration - player . currentPosition
2020-11-21 18:05:34 +01:00
remainingTime = if ( remainingTime < 0 ) 0 else remainingTime
btnNextEpIsVisible = button _next _ep . isVisible
controlsVisible = controller . isVisible
}
2020-11-22 14:20:17 +01:00
if ( remainingTime in 1. . 20000 ) {
2020-11-21 18:05:34 +01:00
// if the next ep button is not visible, make it visible
2020-11-25 16:04:04 +01:00
if ( ! btnNextEpIsVisible && model . nextEpisode != null && Preferences . autoplay ) {
2020-11-22 14:20:17 +01:00
withContext ( Dispatchers . Main ) { showButtonNextEp ( ) }
}
2020-11-21 18:05:34 +01:00
} else if ( btnNextEpIsVisible ) {
withContext ( Dispatchers . Main ) { hideButtonNextEp ( ) }
2020-11-15 13:39:33 +01:00
}
2020-11-15 17:17:56 +01:00
2020-11-21 18:05:34 +01:00
// if controls are visible, update them
if ( controlsVisible ) {
withContext ( Dispatchers . Main ) { updateControls ( ) }
2020-11-13 15:36:12 +01:00
}
}
}
2020-11-05 20:07:41 +01:00
}
2020-10-11 13:18:20 +02:00
private fun releasePlayer ( ) {
playbackPosition = player . currentPosition
currentWindow = player . currentWindowIndex
playWhenReady = player . playWhenReady
player . release ( )
2020-11-15 17:17:56 +01:00
timerUpdates . cancel ( )
2020-11-07 18:23:09 +01:00
Log . d ( javaClass . name , " Released player " )
2020-10-11 13:18:20 +02:00
}
2020-11-21 18:05:34 +01:00
/ * *
* update the custom controls
* /
private fun updateControls ( ) {
// update remaining time label
val hours = TimeUnit . MILLISECONDS . toHours ( remainingTime ) % 24
val minutes = TimeUnit . MILLISECONDS . toMinutes ( remainingTime ) % 60
val seconds = TimeUnit . MILLISECONDS . toSeconds ( remainingTime ) % 60
// if remaining time is below 60 minutes, don't show hours
exo _remaining . text = if ( TimeUnit . MILLISECONDS . toMinutes ( remainingTime ) < 60 ) {
getString ( R . string . time _min _sec , minutes , seconds )
} else {
getString ( R . string . time _hour _min _sec , hours , minutes , seconds )
}
}
2020-11-20 11:20:11 +01:00
/ * *
* TODO set position of rewind / fast forward indicators programmatically
* /
2020-11-08 18:05:46 +01:00
private fun rewind ( ) {
player . seekTo ( player . currentPosition - rwdTime )
2020-11-20 11:20:11 +01:00
// hide/show needed components
exo _double _tap _indicator . visibility = View . VISIBLE
ffwd _10 _indicator . visibility = View . INVISIBLE
2020-11-21 18:05:34 +01:00
rwd _10 . visibility = View . INVISIBLE
2020-11-20 11:20:11 +01:00
rwd _10 _indicator . onAnimationEndCallback = {
exo _double _tap _indicator . visibility = View . GONE
ffwd _10 _indicator . visibility = View . VISIBLE
2020-11-21 18:05:34 +01:00
rwd _10 . visibility = View . VISIBLE
2020-11-20 11:20:11 +01:00
}
// run animation
rwd _10 _indicator . runOnClickAnimation ( )
2020-11-08 18:05:46 +01:00
}
2020-11-25 23:33:06 +01:00
private fun fastForward ( ) {
2020-11-08 18:05:46 +01:00
player . seekTo ( player . currentPosition + fwdTime )
2020-11-20 11:20:11 +01:00
// hide/show needed components
exo _double _tap _indicator . visibility = View . VISIBLE
rwd _10 _indicator . visibility = View . INVISIBLE
ffwd _10 . visibility = View . INVISIBLE
ffwd _10 _indicator . onAnimationEndCallback = {
exo _double _tap _indicator . visibility = View . GONE
rwd _10 _indicator . visibility = View . VISIBLE
ffwd _10 . visibility = View . VISIBLE
}
// run animation
ffwd _10 _indicator . runOnClickAnimation ( )
}
private fun togglePausePlay ( ) {
2020-12-15 23:15:14 +01:00
if ( player . isPlaying ) player . pause ( ) else player . play ( )
2020-11-08 18:05:46 +01:00
}
2020-12-02 11:14:09 +01:00
private fun playNextEpisode ( ) = model . nextEpisode ?. let {
model . nextEpisode ( ) // current = next, next = new or null
2020-11-21 18:05:34 +01:00
hideButtonNextEp ( )
2020-12-02 11:14:09 +01:00
}
/ * *
* start playing a episode
* Note : movies are episodes too !
* /
private fun playCurrentMedia ( seekToPosition : Boolean ) {
// update the gui
exo _text _title . text = if ( model . media . type == DataTypes . MediaType . TVSHOW ) {
2020-12-14 23:46:55 +01:00
getString (
R . string . component _episode _title ,
model . currentEpisode . number ,
model . currentEpisode . description
)
2020-12-02 11:14:09 +01:00
} else {
model . currentEpisode . title
}
if ( model . nextEpisode == null ) {
button _next _ep _c . visibility = View . GONE
}
2020-12-15 23:15:14 +01:00
// update player/media item
player . playWhenReady = true
2020-11-21 18:05:34 +01:00
player . clearMediaItems ( ) //remove previous item
2020-12-02 11:14:09 +01:00
val mediaSource = HlsMediaSource . Factory ( dataSourceFactory ) . createMediaSource (
2020-12-15 23:15:14 +01:00
MediaItem . fromUri ( Uri . parse ( model . autoSelectStream ( model . currentEpisode ) ) )
2020-12-02 11:14:09 +01:00
)
if ( seekToPosition ) player . seekTo ( playbackPosition )
2020-11-21 18:05:34 +01:00
player . setMediaSource ( mediaSource )
player . prepare ( )
2020-11-13 11:23:09 +01:00
}
2020-10-11 14:14:38 +02:00
/ * *
* hide the status and navigation bar
* /
private fun hideBars ( ) {
if ( Build . VERSION . SDK _INT >= Build . VERSION_CODES . R ) {
window . setDecorFitsSystemWindows ( false )
window . insetsController ?. apply {
hide ( WindowInsets . Type . statusBars ( ) or WindowInsets . Type . navigationBars ( ) )
systemBarsBehavior = WindowInsetsController . BEHAVIOR _SHOW _BARS _BY _SWIPE
}
} else {
@Suppress ( " deprecation " )
2020-10-11 15:19:42 +02:00
window . decorView . systemUiVisibility = ( View . SYSTEM _UI _FLAG _IMMERSIVE
or View . SYSTEM _UI _FLAG _LAYOUT _STABLE
or View . SYSTEM _UI _FLAG _LAYOUT _HIDE _NAVIGATION
or View . SYSTEM _UI _FLAG _LAYOUT _FULLSCREEN
or View . SYSTEM _UI _FLAG _HIDE _NAVIGATION
or View . SYSTEM _UI _FLAG _FULLSCREEN )
2020-10-11 14:14:38 +02:00
}
}
2020-11-08 18:05:46 +01:00
2020-11-15 13:39:33 +01:00
/ * *
* show the next episode button
* TODO improve the show animation
* /
private fun showButtonNextEp ( ) {
button _next _ep . visibility = View . VISIBLE
button _next _ep . alpha = 0.0f
button _next _ep . animate ( )
. alpha ( 1.0f )
. setListener ( null )
}
/ * *
* hide the next episode button
* TODO improve the hide animation
* /
private fun hideButtonNextEp ( ) {
button _next _ep . animate ( )
. alpha ( 0.0f )
. setListener ( object : AnimatorListenerAdapter ( ) {
2020-11-20 11:20:11 +01:00
override fun onAnimationEnd ( animation : Animator ? ) {
super . onAnimationEnd ( animation )
button _next _ep . visibility = View . GONE
}
} )
2020-11-15 13:39:33 +01:00
}
2020-12-14 23:46:55 +01:00
private fun showEpisodesList ( ) {
2020-12-15 23:15:14 +01:00
val episodesList = EpisodesListPlayer ( this , model = model ) . apply {
onViewRemovedAction = { player . play ( ) }
}
player _layout . addView ( episodesList )
// hide player controls and pause playback
player . pause ( )
controller . hideImmediately ( )
2020-12-14 23:46:55 +01:00
}
2020-11-08 18:05:46 +01:00
inner class PlayerGestureListener : GestureDetector . SimpleOnGestureListener ( ) {
/ * *
* on single tap hide or show the controls
* /
override fun onSingleTapConfirmed ( e : MotionEvent ? ) : Boolean {
if ( controller . isVisible ) controller . hide ( ) else controller . show ( )
return true
}
/ * *
* on double tap rewind or forward
* /
override fun onDoubleTap ( e : MotionEvent ? ) : Boolean {
2020-11-15 17:17:56 +01:00
val eventPosX = e ?. x ?. toInt ( ) ?: 0
val viewCenterX = video _view . measuredWidth / 2
2020-11-08 18:05:46 +01:00
// if the event position is on the left side rewind, if it's on the right forward
2020-12-15 23:15:14 +01:00
if ( eventPosX < viewCenterX ) rewind ( ) else fastForward ( )
2020-11-08 18:05:46 +01:00
return true
}
/ * *
* not used
* /
override fun onDoubleTapEvent ( e : MotionEvent ? ) : Boolean {
return true
}
2020-11-21 18:05:34 +01:00
/ * *
* on long press toggle pause / play
* /
2020-11-20 11:20:11 +01:00
override fun onLongPress ( e : MotionEvent ? ) {
togglePausePlay ( )
}
2020-11-08 18:05:46 +01:00
}
2020-10-11 13:18:20 +02:00
}