2020-10-11 13:18:20 +02:00
package org.mosad.teapod
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-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-13 11:23:09 +01:00
import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.preferences.Preferences
2020-11-18 18:58:39 +01:00
import org.mosad.teapod.ui.fragments.MediaFragment
2020-11-13 11:23:09 +01:00
import org.mosad.teapod.util.DataTypes.MediaType
import org.mosad.teapod.util.Episode
import org.mosad.teapod.util.Media
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 ( ) {
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-11-13 11:23:09 +01:00
private var mediaId = 0
private var episodeId = 0
private var media : Media = Media ( 0 , " " , MediaType . OTHER )
private var currentEpisode = Episode ( )
private var nextEpisode : Episode ? = null
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
mediaId = intent . getIntExtra ( getString ( R . string . intent _media _id ) , 0 )
episodeId = 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 ( )
if ( video _view != null ) video _view . onResume ( )
}
}
override fun onResume ( ) {
super . onResume ( )
if ( Util . SDK _INT <= 23 ) {
initPlayer ( )
if ( video _view != null ) video _view . onResume ( )
}
}
override fun onPause ( ) {
super . onPause ( )
if ( Util . SDK _INT <= 23 ) {
if ( video _view != null ) video _view . onPause ( )
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-13 11:23:09 +01:00
if ( mediaId <= 0 ) {
Log . e ( javaClass . name , " No media id was set. " )
this . finish ( )
2020-10-11 13:18:20 +02:00
}
2020-11-13 11:23:09 +01:00
initMedia ( )
2020-11-08 18:05:46 +01:00
initExoPlayer ( )
initVideoView ( )
2020-11-13 15:36:12 +01:00
initTimeUpdates ( )
2020-11-08 18:05:46 +01:00
}
2020-11-13 11:23:09 +01:00
private fun initMedia ( ) {
media = AoDParser . getMediaById ( mediaId )
currentEpisode = media . episodes . first { it . id == episodeId }
2020-11-13 15:36:12 +01:00
nextEpisode = selectNextEpisode ( )
2020-11-13 11:23:09 +01:00
}
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
val mediaSource = HlsMediaSource . Factory ( dataSourceFactory )
2020-11-21 18:05:34 +01:00
. createMediaSource ( MediaItem . fromUri ( Uri . parse ( autoSelectStream ( currentEpisode ) ) ) )
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 . setMediaSource ( mediaSource )
2020-10-11 15:19:42 +02:00
player . seekTo ( playbackPosition )
2020-10-11 13:18:20 +02:00
player . prepare ( )
2020-10-11 15:19:42 +02:00
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-15 13:39:33 +01:00
if ( state == ExoPlayer . STATE _ENDED && nextEpisode != null && Preferences . autoplay ) {
2020-11-13 15:36:12 +01:00
playNextEpisode ( )
}
2020-10-11 13:18:20 +02:00
}
} )
2020-11-21 18:05:34 +01:00
controller . isAnimationEnabled = false // disable controls (time-bar) animation
exo _text _title . text = currentEpisode . title // set media title
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 ( ) }
ffwd _10 . setOnButtonClickListener { forward ( ) }
2020-11-13 15:36:12 +01:00
button _next _ep . setOnClickListener { playNextEpisode ( ) }
}
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-22 14:20:17 +01:00
if ( ! btnNextEpIsVisible && nextEpisode != null && Preferences . autoplay ) {
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
}
private fun forward ( ) {
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 ( ) {
if ( player . isPlaying ) {
player . pause ( )
} else {
player . play ( )
}
2020-11-08 18:05:46 +01:00
}
2020-11-21 18:05:34 +01:00
private fun playNextEpisode ( ) = nextEpisode ?. let { nextEp ->
// update the gui
exo _text _title . text = nextEp . title
hideButtonNextEp ( )
2020-11-18 18:58:39 +01:00
2020-11-21 18:05:34 +01:00
player . clearMediaItems ( ) //remove previous item
val mediaSource = HlsMediaSource . Factory ( dataSourceFactory )
. createMediaSource ( MediaItem . fromUri ( Uri . parse ( autoSelectStream ( nextEp ) ) ) )
player . setMediaSource ( mediaSource )
player . prepare ( )
2020-11-13 15:36:12 +01:00
2020-11-21 18:05:34 +01:00
// watchedCallback for next ep
currentEpisode = nextEp // set current ep to next ep
episodeId = nextEp . id
MediaFragment . instance . updateWatchedState ( nextEp )
2020-11-18 18:58:39 +01:00
2020-11-21 18:05:34 +01:00
nextEpisode = selectNextEpisode ( )
2020-11-13 11:23:09 +01:00
}
/ * *
* 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 , close the activity .
* /
2020-11-21 18:05:34 +01:00
private fun autoSelectStream ( episode : Episode ) : String {
2020-11-13 15:36:12 +01:00
return if ( ( Preferences . preferSecondary || episode . priStreamUrl . isEmpty ( ) ) && episode . secStreamOmU ) {
2020-11-13 11:23:09 +01:00
episode . secStreamUrl
} else if ( episode . priStreamUrl . isNotEmpty ( ) ) {
episode . priStreamUrl
} else {
Log . e ( javaClass . name , " No stream url set. " )
this . finish ( )
2020-11-13 15:36:12 +01:00
" "
2020-11-13 11:23:09 +01:00
}
}
2020-11-13 15:36:12 +01:00
/ * *
* Based on the current episodeId , get the next episode . If there is no next
* episode , return null
* /
private fun selectNextEpisode ( ) : Episode ? {
val nextEpIndex = media . episodes . indexOfFirst { it . id == currentEpisode . id } + 1
return if ( nextEpIndex < ( media . episodes . size ) ) {
media . episodes [ nextEpIndex ]
} else {
null
}
}
2020-11-08 18:05:46 +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 )
}
2020-11-15 17:17:56 +01:00
2020-11-15 13:39:33 +01:00
/ * *
* 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-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-11-15 17:17:56 +01:00
if ( eventPosX < viewCenterX ) {
2020-11-08 18:05:46 +01:00
rewind ( )
} else {
forward ( )
}
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
}