2020-10-11 13:18:20 +02:00
package org.mosad.teapod
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-08 18:05:46 +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-13 15:36:12 +01:00
import kotlinx.coroutines.*
2020-11-13 11:23:09 +01:00
import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.util.DataTypes.MediaType
import org.mosad.teapod.util.Episode
import org.mosad.teapod.util.Media
2020-11-07 13:33:59 +01:00
import java.util.concurrent.TimeUnit
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-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 ( )
initController ( )
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-13 15:36:12 +01:00
. createMediaSource ( MediaItem . fromUri ( Uri . parse ( selectStream ( 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
if ( state == ExoPlayer . STATE _ENDED && nextEpisode != null ) {
playNextEpisode ( )
}
2020-10-11 13:18:20 +02:00
}
} )
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-07 13:33:59 +01:00
if ( it == View . GONE ) {
hideBars ( )
}
}
2020-11-08 18:05:46 +01:00
video _view . setOnTouchListener { _ , event ->
gestureDetector . onTouchEvent ( event )
true
}
}
private fun initController ( ) {
2020-11-07 13:33:59 +01:00
controller . isAnimationEnabled = false // disable controls (time-bar) animation
controller . setProgressUpdateListener { _ , _ ->
remainingTime = player . duration - player . currentPosition
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 ) {
2020-11-08 18:05:46 +01:00
getString ( R . string . time _min _sec , minutes , seconds )
2020-11-07 13:33:59 +01:00
} else {
2020-11-08 18:05:46 +01:00
getString ( R . string . time _hour _min _sec , hours , minutes , seconds )
2020-11-07 13:33:59 +01:00
}
2020-11-13 15:36:12 +01:00
2020-10-11 14:14:38 +02:00
}
2020-11-13 11:23:09 +01:00
exo _text _title . text = currentEpisode . title // set media title
2020-10-11 13:18:20 +02:00
}
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 ( ) }
exo _rew _10 . setOnClickListener { rewind ( ) }
exo _ffwd _10 . setOnClickListener { forward ( ) }
2020-11-13 15:36:12 +01:00
button _next _ep . setOnClickListener { playNextEpisode ( ) }
}
private fun initTimeUpdates ( ) = GlobalScope . launch {
while ( true ) {
val remainingTime = withContext ( Dispatchers . Main ) {
player . duration - player . currentPosition
}
if ( remainingTime in 0. . 20000 ) {
withContext ( Dispatchers . Main ) {
// if the next ep button is not visible, make it visible
2020-11-13 15:45:52 +01:00
if ( ! button _next _ep . isVisible && nextEpisode != null ) {
2020-11-13 15:36:12 +01:00
button _next _ep . visibility = View . VISIBLE // TODO animation
}
}
}
delay ( 1000 )
}
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-07 18:23:09 +01:00
Log . d ( javaClass . name , " Released player " )
2020-10-11 13:18:20 +02:00
}
2020-11-08 18:05:46 +01:00
private fun rewind ( ) {
player . seekTo ( player . currentPosition - rwdTime )
}
private fun forward ( ) {
player . seekTo ( player . currentPosition + fwdTime )
}
2020-11-13 11:23:09 +01:00
private fun playNextEpisode ( ) {
2020-11-13 15:36:12 +01:00
nextEpisode ?. let { nextEp ->
currentEpisode = nextEp // set current ep to next ep
episodeId = nextEp . id
// update the gui
exo _text _title . text = nextEp . title
button _next _ep . visibility = View . GONE // TODO animation
player . clearMediaItems ( ) //remove previous item
val mediaSource = HlsMediaSource . Factory ( dataSourceFactory )
. createMediaSource ( MediaItem . fromUri ( Uri . parse ( selectStream ( nextEp ) ) ) )
player . setMediaSource ( mediaSource )
player . prepare ( )
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-13 15:36:12 +01:00
private fun selectStream ( episode : Episode ) : String {
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
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 {
val eventPos = e ?. x ?. toInt ( ) ?: 0
val viewCenter = video _view . measuredWidth / 2
// TODO show indicator for tap action
// if the event position is on the left side rewind, if it's on the right forward
if ( eventPos < viewCenter ) {
rewind ( )
} else {
forward ( )
}
return true
}
/ * *
* not used
* /
override fun onDoubleTapEvent ( e : MotionEvent ? ) : Boolean {
return true
}
}
2020-10-11 13:18:20 +02:00
}