Dantotsu/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt
TwistedUmbrellaX c054e2f2ac
feat: progress for starting manga (#245)
The caveat is that the user must have disabled updating each title individually, along with the other standard checks. This will only apply when a chapter has not been completed.
2024-03-16 23:00:58 -05:00

2124 lines
83 KiB
Kotlin

package ani.dantotsu.media.anime
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.Dialog
import android.app.PictureInPictureParams
import android.app.PictureInPictureUiState
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.drawable.Animatable
import android.hardware.SensorManager
import android.media.AudioManager
import android.media.AudioManager.*
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.CountDownTimer
import android.os.Handler
import android.os.Looper
import android.provider.Settings.System
import android.util.AttributeSet
import android.util.Rational
import android.util.TypedValue
import android.view.*
import android.view.KeyEvent.*
import android.view.animation.AnimationUtils
import android.widget.AdapterView
import android.widget.ImageButton
import android.widget.Spinner
import android.widget.TextView
import androidx.activity.addCallback
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat
import androidx.core.math.MathUtils.clamp
import androidx.core.view.isVisible
import androidx.core.view.WindowCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.media3.cast.CastPlayer
import androidx.media3.cast.SessionAvailabilityListener
import androidx.media3.common.*
import androidx.media3.common.C.AUDIO_CONTENT_TYPE_MOVIE
import androidx.media3.common.C.TRACK_TYPE_VIDEO
import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultDataSourceFactory
import androidx.media3.datasource.HttpDataSource
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.exoplayer.util.EventLogger
import androidx.media3.session.MediaSession
import androidx.media3.ui.*
import androidx.media3.ui.CaptionStyleCompat.*
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.mediarouter.app.MediaRouteButton
import ani.dantotsu.*
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.connections.discord.Discord
import ani.dantotsu.connections.discord.DiscordService
import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton
import ani.dantotsu.connections.discord.RPC
import ani.dantotsu.connections.updateProgress
import ani.dantotsu.databinding.ActivityExoplayerBinding
import ani.dantotsu.download.video.Helper
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.SubtitleDownloader
import ani.dantotsu.others.AniSkip
import ani.dantotsu.others.AniSkip.getType
import ani.dantotsu.others.ResettableTimer
import ani.dantotsu.others.getSerialized
import ani.dantotsu.parsers.*
import ani.dantotsu.settings.PlayerSettingsActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.Logger
import com.bumptech.glide.Glide
import com.google.android.gms.cast.framework.CastButtonFactory
import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.material.slider.Slider
import com.lagradost.nicehttp.ignoreAllSSLErrors
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.*
import java.util.concurrent.*
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
@UnstableApi
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityListener {
private val resumeWindow = "resumeWindow"
private val resumePosition = "resumePosition"
private val playerFullscreen = "playerFullscreen"
private val playerOnPlay = "playerOnPlay"
private var disappeared: Boolean = false
private var functionstarted: Boolean = false
private lateinit var exoPlayer: ExoPlayer
private var castPlayer: CastPlayer? = null
private var castContext: CastContext? = null
private var isCastApiAvailable = false
private lateinit var trackSelector: DefaultTrackSelector
private lateinit var cacheFactory: CacheDataSource.Factory
private lateinit var playbackParameters: PlaybackParameters
private lateinit var mediaItem: MediaItem
private var mediaSession: MediaSession? = null
private lateinit var binding: ActivityExoplayerBinding
private lateinit var playerView: PlayerView
private lateinit var exoPlay: ImageButton
private lateinit var exoSource: ImageButton
private lateinit var exoSettings: ImageButton
private lateinit var exoSubtitle: ImageButton
private lateinit var exoSubtitleView: SubtitleView
private lateinit var exoRotate: ImageButton
private lateinit var exoSpeed: ImageButton
private lateinit var exoScreen: ImageButton
private lateinit var exoNext: ImageButton
private lateinit var exoPrev: ImageButton
private lateinit var exoSkipOpEd: ImageButton
private lateinit var exoPip: ImageButton
private lateinit var exoBrightness: Slider
private lateinit var exoVolume: Slider
private lateinit var exoBrightnessCont: View
private lateinit var exoVolumeCont: View
private lateinit var exoSkip: View
private lateinit var skipTimeButton: View
private lateinit var skipTimeText: TextView
private lateinit var timeStampText: TextView
private lateinit var animeTitle: TextView
private lateinit var videoInfo: TextView
private lateinit var episodeTitle: Spinner
private var orientationListener: OrientationEventListener? = null
private var downloadId: String? = null
companion object {
var initialized = false
lateinit var media: Media
}
private lateinit var episode: Episode
private lateinit var episodes: MutableMap<String, Episode>
private lateinit var episodeArr: List<String>
private lateinit var episodeTitleArr: ArrayList<String>
private var currentEpisodeIndex = 0
private var epChanging = false
private var extractor: VideoExtractor? = null
private var video: Video? = null
private var subtitle: Subtitle? = null
private val player = "player_settings"
private var notchHeight: Int = 0
private var currentWindow = 0
private var playbackPosition: Long = 0
private var episodeLength: Float = 0f
private var isFullscreen: Int = 0
private var isInitialized = false
private var isPlayerPlaying = true
private var changingServer = false
private var interacted = false
private var pipEnabled = false
private var aspectRatio = Rational(16, 9)
private val handler = Handler(Looper.getMainLooper())
val model: MediaDetailsViewModel by viewModels()
private var isTimeStampsLoaded = false
private var isSeeking = false
private var isFastForwarding = false
var rotation = 0
override fun onAttachedToWindow() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val displayCutout = window.decorView.rootWindowInsets.displayCutout
if (displayCutout != null) {
if (displayCutout.boundingRects.size > 0) {
notchHeight = min(
displayCutout.boundingRects[0].width(),
displayCutout.boundingRects[0].height()
)
checkNotch()
}
}
}
super.onAttachedToWindow()
}
private fun checkNotch() {
if (notchHeight != 0) {
val orientation = resources.configuration.orientation
playerView.findViewById<View>(R.id.exo_controller_margin)
.updateLayoutParams<ViewGroup.MarginLayoutParams> {
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
marginStart = notchHeight
marginEnd = notchHeight
topMargin = 0
} else {
topMargin = notchHeight
marginStart = 0
marginEnd = 0
}
}
playerView.findViewById<View>(androidx.media3.ui.R.id.exo_buffering).translationY =
(if (orientation == Configuration.ORIENTATION_LANDSCAPE) 0 else (notchHeight + 8f.px)).dp
exoBrightnessCont.updateLayoutParams<ViewGroup.MarginLayoutParams> {
marginEnd =
if (orientation == Configuration.ORIENTATION_LANDSCAPE) notchHeight else 0
}
exoVolumeCont.updateLayoutParams<ViewGroup.MarginLayoutParams> {
marginStart =
if (orientation == Configuration.ORIENTATION_LANDSCAPE) notchHeight else 0
}
}
}
private fun setupSubFormatting(playerView: PlayerView) {
val primaryColor = when (PrefManager.getVal<Int>(PrefName.PrimaryColor)) {
0 -> Color.BLACK
1 -> Color.DKGRAY
2 -> Color.GRAY
3 -> Color.LTGRAY
4 -> Color.WHITE
5 -> Color.RED
6 -> Color.YELLOW
7 -> Color.GREEN
8 -> Color.CYAN
9 -> Color.BLUE
10 -> Color.MAGENTA
11 -> Color.TRANSPARENT
else -> Color.WHITE
}
val secondaryColor = when (PrefManager.getVal<Int>(PrefName.SecondaryColor)) {
0 -> Color.BLACK
1 -> Color.DKGRAY
2 -> Color.GRAY
3 -> Color.LTGRAY
4 -> Color.WHITE
5 -> Color.RED
6 -> Color.YELLOW
7 -> Color.GREEN
8 -> Color.CYAN
9 -> Color.BLUE
10 -> Color.MAGENTA
11 -> Color.TRANSPARENT
else -> Color.BLACK
}
val outline = when (PrefManager.getVal<Int>(PrefName.Outline)) {
0 -> EDGE_TYPE_OUTLINE // Normal
1 -> EDGE_TYPE_DEPRESSED // Shine
2 -> EDGE_TYPE_DROP_SHADOW // Drop shadow
3 -> EDGE_TYPE_NONE // No outline
else -> EDGE_TYPE_OUTLINE // Normal
}
val subBackground = when (PrefManager.getVal<Int>(PrefName.SubBackground)) {
0 -> Color.TRANSPARENT
1 -> Color.BLACK
2 -> Color.DKGRAY
3 -> Color.GRAY
4 -> Color.LTGRAY
5 -> Color.WHITE
6 -> Color.RED
7 -> Color.YELLOW
8 -> Color.GREEN
9 -> Color.CYAN
10 -> Color.BLUE
11 -> Color.MAGENTA
else -> Color.TRANSPARENT
}
val subWindow = when (PrefManager.getVal<Int>(PrefName.SubWindow)) {
0 -> Color.TRANSPARENT
1 -> Color.BLACK
2 -> Color.DKGRAY
3 -> Color.GRAY
4 -> Color.LTGRAY
5 -> Color.WHITE
6 -> Color.RED
7 -> Color.YELLOW
8 -> Color.GREEN
9 -> Color.CYAN
10 -> Color.BLUE
11 -> Color.MAGENTA
else -> Color.TRANSPARENT
}
val font = when (PrefManager.getVal<Int>(PrefName.Font)) {
0 -> ResourcesCompat.getFont(this, R.font.poppins_semi_bold)
1 -> ResourcesCompat.getFont(this, R.font.poppins_bold)
2 -> ResourcesCompat.getFont(this, R.font.poppins)
3 -> ResourcesCompat.getFont(this, R.font.poppins_thin)
4 -> ResourcesCompat.getFont(this, R.font.century_gothic_regular)
5 -> ResourcesCompat.getFont(this, R.font.levenim_mt_bold)
6 -> ResourcesCompat.getFont(this, R.font.blocky)
else -> ResourcesCompat.getFont(this, R.font.poppins_semi_bold)
}
playerView.subtitleView?.setStyle(
CaptionStyleCompat(
primaryColor,
subBackground,
subWindow,
outline,
secondaryColor,
font
)
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
binding = ActivityExoplayerBinding.inflate(layoutInflater)
setContentView(binding.root)
//Initialize
isCastApiAvailable = GoogleApiAvailability.getInstance()
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
try {
castContext = CastContext.getSharedInstance(this)
castPlayer = CastPlayer(castContext!!)
castPlayer!!.setSessionAvailabilityListener(this)
} catch (e: Exception) {
isCastApiAvailable = false
}
WindowCompat.setDecorFitsSystemWindows(window, false)
hideSystemBars()
onBackPressedDispatcher.addCallback(this) {
finishAndRemoveTask()
}
playerView = findViewById(R.id.player_view)
exoPlay = playerView.findViewById(androidx.media3.ui.R.id.exo_play)
exoSource = playerView.findViewById(R.id.exo_source)
exoSettings = playerView.findViewById(R.id.exo_settings)
exoSubtitle = playerView.findViewById(R.id.exo_sub)
exoSubtitleView = playerView.findViewById(androidx.media3.ui.R.id.exo_subtitles)
exoRotate = playerView.findViewById(R.id.exo_rotate)
exoSpeed = playerView.findViewById(androidx.media3.ui.R.id.exo_playback_speed)
exoScreen = playerView.findViewById(R.id.exo_screen)
exoBrightness = playerView.findViewById(R.id.exo_brightness)
exoVolume = playerView.findViewById(R.id.exo_volume)
exoBrightnessCont = playerView.findViewById(R.id.exo_brightness_cont)
exoVolumeCont = playerView.findViewById(R.id.exo_volume_cont)
exoPip = playerView.findViewById(R.id.exo_pip)
exoSkipOpEd = playerView.findViewById(R.id.exo_skip_op_ed)
exoSkip = playerView.findViewById(R.id.exo_skip)
skipTimeButton = playerView.findViewById(R.id.exo_skip_timestamp)
skipTimeText = skipTimeButton.findViewById(R.id.exo_skip_timestamp_text)
timeStampText = playerView.findViewById(R.id.exo_time_stamp_text)
animeTitle = playerView.findViewById(R.id.exo_anime_title)
episodeTitle = playerView.findViewById(R.id.exo_ep_sel)
playerView.controllerShowTimeoutMs = 5000
val audioManager = applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager
@Suppress("DEPRECATION")
audioManager.requestAudioFocus({ focus ->
when (focus) {
AUDIOFOCUS_LOSS_TRANSIENT, AUDIOFOCUS_LOSS -> if (isInitialized) exoPlayer.pause()
}
}, AUDIO_CONTENT_TYPE_MOVIE, AUDIOFOCUS_GAIN)
if (System.getInt(contentResolver, System.ACCELEROMETER_ROTATION, 0) != 1) {
if (PrefManager.getVal(PrefName.RotationPlayer)) {
orientationListener =
object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
override fun onOrientationChanged(orientation: Int) {
if (orientation in 45..135) {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
exoRotate.visibility = View.VISIBLE
}
rotation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
} else if (orientation in 225..315) {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
exoRotate.visibility = View.VISIBLE
}
rotation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
} else if (orientation in 315..360 || orientation in 0..45) {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
exoRotate.visibility = View.VISIBLE
}
rotation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
}
orientationListener?.enable()
}
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
exoRotate.setOnClickListener {
requestedOrientation = rotation
it.visibility = View.GONE
}
}
setupSubFormatting(playerView)
playerView.subtitleView?.alpha = when (PrefManager.getVal<Boolean>(PrefName.Subtitles)) {
true -> 1f
false -> 0f
}
val fontSize = PrefManager.getVal<Int>(PrefName.FontSize).toFloat()
playerView.subtitleView?.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, fontSize)
if (savedInstanceState != null) {
currentWindow = savedInstanceState.getInt(resumeWindow)
playbackPosition = savedInstanceState.getLong(resumePosition)
isFullscreen = savedInstanceState.getInt(playerFullscreen)
isPlayerPlaying = savedInstanceState.getBoolean(playerOnPlay)
}
//BackButton
playerView.findViewById<ImageButton>(R.id.exo_back).setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
//TimeStamps
model.timeStamps.observe(this) { it ->
isTimeStampsLoaded = true
exoSkipOpEd.visibility = if (it != null) {
val adGroups = it.flatMap {
listOf(
it.interval.startTime.toLong() * 1000,
it.interval.endTime.toLong() * 1000
)
}.toLongArray()
val playedAdGroups = it.flatMap {
listOf(false, false)
}.toBooleanArray()
playerView.setExtraAdGroupMarkers(adGroups, playedAdGroups)
View.VISIBLE
} else View.GONE
}
exoSkipOpEd.alpha = if (PrefManager.getVal(PrefName.AutoSkipOPED)) 1f else 0.3f
exoSkipOpEd.setOnClickListener {
if (PrefManager.getVal(PrefName.AutoSkipOPED)) {
snackString(getString(R.string.disabled_auto_skip))
PrefManager.setVal(PrefName.AutoSkipOPED, false)
} else {
snackString(getString(R.string.auto_skip))
PrefManager.setVal(PrefName.AutoSkipOPED, true)
}
exoSkipOpEd.alpha = if (PrefManager.getVal(PrefName.AutoSkipOPED)) 1f else 0.3f
}
//Play Pause
exoPlay.setOnClickListener {
if (isInitialized) {
isPlayerPlaying = exoPlayer.isPlaying
(exoPlay.drawable as Animatable?)?.start()
if (isPlayerPlaying || castPlayer?.isPlaying == true) {
Glide.with(this).load(R.drawable.anim_play_to_pause).into(exoPlay)
exoPlayer.pause()
castPlayer?.pause()
} else {
if (castPlayer?.isPlaying == false && castPlayer?.currentMediaItem != null) {
Glide.with(this).load(R.drawable.anim_pause_to_play).into(exoPlay)
castPlayer?.play()
} else if (!isPlayerPlaying) {
Glide.with(this).load(R.drawable.anim_pause_to_play).into(exoPlay)
exoPlayer.play()
}
}
}
}
// Picture-in-picture
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
pipEnabled =
packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && PrefManager.getVal(
PrefName.Pip
)
if (pipEnabled) {
exoPip.visibility = View.VISIBLE
exoPip.setOnClickListener {
enterPipMode()
}
} else exoPip.visibility = View.GONE
}
//Lock Button
var locked = false
val container = playerView.findViewById<View>(R.id.exo_controller_cont)
val screen = playerView.findViewById<View>(R.id.exo_black_screen)
val lockButton = playerView.findViewById<ImageButton>(R.id.exo_unlock)
val timeline =
playerView.findViewById<ExtendedTimeBar>(androidx.media3.ui.R.id.exo_progress)
playerView.findViewById<ImageButton>(R.id.exo_lock).setOnClickListener {
locked = true
screen.visibility = View.GONE
container.visibility = View.GONE
lockButton.visibility = View.VISIBLE
timeline.setForceDisabled(true)
}
lockButton.setOnClickListener {
locked = false
screen.visibility = View.VISIBLE
container.visibility = View.VISIBLE
it.visibility = View.GONE
timeline.setForceDisabled(false)
}
//Skip Time Button
var skipTime = PrefManager.getVal<Int>(PrefName.SkipTime)
if (skipTime > 0) {
exoSkip.findViewById<TextView>(R.id.exo_skip_time).text = skipTime.toString()
exoSkip.setOnClickListener {
if (isInitialized)
exoPlayer.seekTo(exoPlayer.currentPosition + skipTime * 1000)
}
exoSkip.setOnLongClickListener {
val dialog = Dialog(this, R.style.MyPopup)
dialog.setContentView(R.layout.item_seekbar_dialog)
dialog.setCancelable(true)
dialog.setCanceledOnTouchOutside(true)
dialog.window?.setLayout(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
if (skipTime <= 120) {
dialog.findViewById<Slider>(R.id.seekbar).value = skipTime.toFloat()
} else {
dialog.findViewById<Slider>(R.id.seekbar).value = 120f
}
dialog.findViewById<Slider>(R.id.seekbar).addOnChangeListener { _, value, _ ->
skipTime = value.toInt()
//saveData(player, settings)
PrefManager.setVal(PrefName.SkipTime, skipTime)
playerView.findViewById<TextView>(R.id.exo_skip_time).text =
skipTime.toString()
dialog.findViewById<TextView>(R.id.seekbar_value).text =
skipTime.toString()
}
dialog.findViewById<Slider>(R.id.seekbar)
.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
override fun onStartTrackingTouch(slider: Slider) {}
override fun onStopTrackingTouch(slider: Slider) {
dialog.dismiss()
}
})
dialog.findViewById<TextView>(R.id.seekbar_title).text =
getString(R.string.skip_time)
dialog.findViewById<TextView>(R.id.seekbar_value).text =
skipTime.toString()
@Suppress("DEPRECATION")
dialog.window?.decorView?.systemUiVisibility = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
dialog.show()
true
}
} else {
exoSkip.visibility = View.GONE
}
val gestureSpeed = (300 * PrefManager.getVal<Float>(PrefName.AnimationSpeed)).toLong()
//Player UI Visibility Handler
val brightnessRunnable = Runnable {
if (exoBrightnessCont.alpha == 1f)
lifecycleScope.launch {
ObjectAnimator.ofFloat(exoBrightnessCont, "alpha", 1f, 0f)
.setDuration(gestureSpeed).start()
delay(gestureSpeed)
exoBrightnessCont.visibility = View.GONE
checkNotch()
}
}
val volumeRunnable = Runnable {
if (exoVolumeCont.alpha == 1f)
lifecycleScope.launch {
ObjectAnimator.ofFloat(exoVolumeCont, "alpha", 1f, 0f).setDuration(gestureSpeed)
.start()
delay(gestureSpeed)
exoVolumeCont.visibility = View.GONE
checkNotch()
}
}
playerView.setControllerVisibilityListener(PlayerView.ControllerVisibilityListener { visibility ->
if (visibility == View.GONE) {
hideSystemBars()
brightnessRunnable.run()
volumeRunnable.run()
}
})
val overshoot = AnimationUtils.loadInterpolator(this, R.anim.over_shoot)
val controllerDuration = (300 * PrefManager.getVal<Float>(PrefName.AnimationSpeed)).toLong()
fun handleController() {
if (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) !isInPictureInPictureMode else true) {
if (playerView.isControllerFullyVisible) {
ObjectAnimator.ofFloat(
playerView.findViewById(R.id.exo_controller),
"alpha",
1f,
0f
)
.setDuration(controllerDuration).start()
ObjectAnimator.ofFloat(
playerView.findViewById(R.id.exo_bottom_cont),
"translationY",
0f,
128f
)
.apply { interpolator = overshoot;duration = controllerDuration;start() }
ObjectAnimator.ofFloat(
playerView.findViewById(R.id.exo_timeline_cont),
"translationY",
0f,
128f
)
.apply { interpolator = overshoot;duration = controllerDuration;start() }
ObjectAnimator.ofFloat(
playerView.findViewById(R.id.exo_top_cont),
"translationY",
0f,
-128f
)
.apply { interpolator = overshoot;duration = controllerDuration;start() }
playerView.postDelayed({ playerView.hideController() }, controllerDuration)
} else {
checkNotch()
playerView.showController()
ObjectAnimator.ofFloat(
playerView.findViewById(R.id.exo_controller),
"alpha",
0f,
1f
)
.setDuration(controllerDuration).start()
ObjectAnimator.ofFloat(
playerView.findViewById(R.id.exo_bottom_cont),
"translationY",
128f,
0f
)
.apply { interpolator = overshoot;duration = controllerDuration;start() }
ObjectAnimator.ofFloat(
playerView.findViewById(R.id.exo_timeline_cont),
"translationY",
128f,
0f
)
.apply { interpolator = overshoot;duration = controllerDuration;start() }
ObjectAnimator.ofFloat(
playerView.findViewById(R.id.exo_top_cont),
"translationY",
-128f,
0f
)
.apply { interpolator = overshoot;duration = controllerDuration;start() }
}
}
}
playerView.findViewById<View>(R.id.exo_full_area).setOnClickListener {
handleController()
}
val rewindText = playerView.findViewById<TextView>(R.id.exo_fast_rewind_anim)
val forwardText = playerView.findViewById<TextView>(R.id.exo_fast_forward_anim)
val fastForwardCard = playerView.findViewById<View>(R.id.exo_fast_forward)
val fastRewindCard = playerView.findViewById<View>(R.id.exo_fast_rewind)
//Seeking
val seekTimerF = ResettableTimer()
val seekTimerR = ResettableTimer()
var seekTimesF = 0
var seekTimesR = 0
fun seek(forward: Boolean, event: MotionEvent? = null) {
val seekTime = PrefManager.getVal<Int>(PrefName.SeekTime)
val (card, text) = if (forward) {
forwardText.text = "+${seekTime * ++seekTimesF}"
handler.post { exoPlayer.seekTo(exoPlayer.currentPosition + seekTime * 1000) }
fastForwardCard to forwardText
} else {
rewindText.text = "-${seekTime * ++seekTimesR}"
handler.post { exoPlayer.seekTo(exoPlayer.currentPosition - seekTime * 1000) }
fastRewindCard to rewindText
}
//region Double Tap Animation
val showCardAnim = ObjectAnimator.ofFloat(card, "alpha", 0f, 1f).setDuration(300)
val showTextAnim = ObjectAnimator.ofFloat(text, "alpha", 0f, 1f).setDuration(150)
fun startAnim() {
showTextAnim.start()
(text.compoundDrawables[1] as Animatable).apply {
if (!isRunning) start()
}
if (!isSeeking && event != null) {
playerView.hideController()
card.circularReveal(event.x.toInt(), event.y.toInt(), !forward, 800)
showCardAnim.start()
}
}
fun stopAnim() {
handler.post {
showCardAnim.cancel()
showTextAnim.cancel()
ObjectAnimator.ofFloat(card, "alpha", card.alpha, 0f).setDuration(150).start()
ObjectAnimator.ofFloat(text, "alpha", 1f, 0f).setDuration(150).start()
}
}
//endregion
startAnim()
isSeeking = true
if (forward) {
seekTimerR.reset(object : TimerTask() {
override fun run() {
isSeeking = false
stopAnim()
seekTimesF = 0
}
}, 850)
} else {
seekTimerF.reset(object : TimerTask() {
override fun run() {
isSeeking = false
stopAnim()
seekTimesR = 0
}
}, 850)
}
}
if (!PrefManager.getVal<Boolean>(PrefName.DoubleTap)) {
playerView.findViewById<View>(R.id.exo_fast_forward_button_cont).visibility =
View.VISIBLE
playerView.findViewById<View>(R.id.exo_fast_rewind_button_cont).visibility =
View.VISIBLE
playerView.findViewById<ImageButton>(R.id.exo_fast_forward_button).setOnClickListener {
if (isInitialized) {
seek(true)
}
}
playerView.findViewById<ImageButton>(R.id.exo_fast_rewind_button).setOnClickListener {
if (isInitialized) {
seek(false)
}
}
}
keyMap[KEYCODE_DPAD_RIGHT] = { seek(true) }
keyMap[KEYCODE_DPAD_LEFT] = { seek(false) }
//Screen Gestures
if (PrefManager.getVal<Boolean>(PrefName.Gestures) || PrefManager.getVal<Boolean>(PrefName.DoubleTap)) {
fun doubleTap(forward: Boolean, event: MotionEvent) {
if (!locked && isInitialized && PrefManager.getVal<Boolean>(PrefName.DoubleTap)) {
seek(forward, event)
}
}
//Brightness
var brightnessTimer = Timer()
exoBrightnessCont.visibility = View.GONE
fun brightnessHide() {
brightnessTimer.cancel()
brightnessTimer.purge()
val timerTask: TimerTask = object : TimerTask() {
override fun run() {
handler.post(brightnessRunnable)
}
}
brightnessTimer = Timer()
brightnessTimer.schedule(timerTask, 3000)
}
exoBrightness.value = (getCurrentBrightnessValue(this) * 10f)
exoBrightness.addOnChangeListener { _, value, _ ->
val lp = window.attributes
lp.screenBrightness =
brightnessConverter((value.takeIf { !it.isNaN() } ?: 0f) / 10, false)
window.attributes = lp
brightnessHide()
}
//Volume
var volumeTimer = Timer()
exoVolumeCont.visibility = View.GONE
val volumeMax = audioManager.getStreamMaxVolume(STREAM_MUSIC)
exoVolume.value = audioManager.getStreamVolume(STREAM_MUSIC).toFloat() / volumeMax * 10
fun volumeHide() {
volumeTimer.cancel()
volumeTimer.purge()
val timerTask: TimerTask = object : TimerTask() {
override fun run() {
handler.post(volumeRunnable)
}
}
volumeTimer = Timer()
volumeTimer.schedule(timerTask, 3000)
}
exoVolume.addOnChangeListener { _, value, _ ->
val volume = ((value.takeIf { !it.isNaN() } ?: 0f) / 10 * volumeMax).roundToInt()
audioManager.setStreamVolume(STREAM_MUSIC, volume, 0)
volumeHide()
}
val fastForward = playerView.findViewById<TextView>(R.id.exo_fast_forward_text)
fun fastForward() {
isFastForwarding = true
exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed * 2)
fastForward.visibility = View.VISIBLE
fastForward.text = "${exoPlayer.playbackParameters.speed}x"
}
fun stopFastForward() {
if (isFastForwarding) {
isFastForwarding = false
exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed / 2)
fastForward.visibility = View.GONE
}
}
//FastRewind (Left Panel)
val fastRewindDetector = GestureDetector(this, object : GesturesListener() {
override fun onLongClick(event: MotionEvent) {
if (PrefManager.getVal(PrefName.FastForward)) fastForward()
}
override fun onDoubleClick(event: MotionEvent) {
doubleTap(false, event)
}
override fun onScrollYClick(y: Float) {
if (!locked && PrefManager.getVal(PrefName.Gestures)) {
exoBrightness.value = clamp(exoBrightness.value + y / 100, 0f, 10f)
if (exoBrightnessCont.visibility != View.VISIBLE) {
exoBrightnessCont.visibility = View.VISIBLE
}
exoBrightnessCont.alpha = 1f
}
}
override fun onSingleClick(event: MotionEvent) =
if (isSeeking) doubleTap(false, event) else handleController()
})
val rewindArea = playerView.findViewById<View>(R.id.exo_rewind_area)
rewindArea.isClickable = true
rewindArea.setOnTouchListener { v, event ->
fastRewindDetector.onTouchEvent(event)
if (event.action == MotionEvent.ACTION_UP) stopFastForward()
v.performClick()
true
}
//FastForward (Right Panel)
val fastForwardDetector = GestureDetector(this, object : GesturesListener() {
override fun onLongClick(event: MotionEvent) {
if (PrefManager.getVal(PrefName.FastForward)) fastForward()
}
override fun onDoubleClick(event: MotionEvent) {
doubleTap(true, event)
}
override fun onScrollYClick(y: Float) {
if (!locked && PrefManager.getVal(PrefName.Gestures)) {
exoVolume.value = clamp(exoVolume.value + y / 100, 0f, 10f)
if (exoVolumeCont.visibility != View.VISIBLE) {
exoVolumeCont.visibility = View.VISIBLE
}
exoVolumeCont.alpha = 1f
}
}
override fun onSingleClick(event: MotionEvent) =
if (isSeeking) doubleTap(true, event) else handleController()
})
val forwardArea = playerView.findViewById<View>(R.id.exo_forward_area)
forwardArea.isClickable = true
forwardArea.setOnTouchListener { v, event ->
fastForwardDetector.onTouchEvent(event)
if (event.action == MotionEvent.ACTION_UP) stopFastForward()
v.performClick()
true
}
}
//Handle Media
if (!initialized) return startMainActivity(this)
model.setMedia(media)
title = media.userPreferredName
episodes = media.anime?.episodes ?: return startMainActivity(this)
videoInfo = playerView.findViewById(R.id.exo_video_info)
model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources
model.epChanged.observe(this) {
epChanging = !it
}
//Anime Title
animeTitle.text = media.userPreferredName
episodeArr = episodes.keys.toList()
currentEpisodeIndex = episodeArr.indexOf(media.anime!!.selectedEpisode!!)
episodeTitleArr = arrayListOf<String>()
episodes.forEach {
val episode = it.value
val cleanedTitle = AnimeNameAdapter.removeEpisodeNumberCompletely(episode.title ?: "")
episodeTitleArr.add("Episode ${episode.number}${if (episode.filler) " [Filler]" else ""}${if (cleanedTitle.isNotBlank() && cleanedTitle != "null") ": $cleanedTitle" else ""}")
}
//Episode Change
fun change(index: Int) {
if (isInitialized) {
changingServer = false
PrefManager.setCustomVal(
"${media.id}_${episodeArr[currentEpisodeIndex]}",
exoPlayer.currentPosition
)
exoPlayer.seekTo(0)
val prev = episodeArr[currentEpisodeIndex]
isTimeStampsLoaded = false
episodeLength = 0f
media.anime!!.selectedEpisode = episodeArr[index]
model.setMedia(media)
model.epChanged.postValue(false)
model.setEpisode(episodes[media.anime!!.selectedEpisode!!]!!, "change")
model.onEpisodeClick(
media, media.anime!!.selectedEpisode!!, this.supportFragmentManager,
false,
prev
)
}
}
//EpisodeSelector
episodeTitle.adapter = NoPaddingArrayAdapter(this, R.layout.item_dropdown, episodeTitleArr)
episodeTitle.setSelection(currentEpisodeIndex)
episodeTitle.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) {
if (position != currentEpisodeIndex) {
disappeared = false
functionstarted = false
change(position)
}
}
override fun onNothingSelected(parent: AdapterView<*>) {}
}
//Next Episode
exoNext = playerView.findViewById(R.id.exo_next_ep)
exoNext.setOnClickListener {
if (isInitialized) {
nextEpisode { i ->
updateAniProgress()
disappeared = false
functionstarted = false
change(currentEpisodeIndex + i)
}
}
}
//Prev Episode
exoPrev = playerView.findViewById(R.id.exo_prev_ep)
exoPrev.setOnClickListener {
if (currentEpisodeIndex > 0) {
disappeared = false
change(currentEpisodeIndex - 1)
} else
snackString(getString(R.string.first_episode))
}
model.getEpisode().observe(this) { ep ->
hideSystemBars()
if (ep != null && !epChanging) {
episode = ep
media.selected = model.loadSelected(media)
model.setMedia(media)
currentEpisodeIndex = episodeArr.indexOf(ep.number)
episodeTitle.setSelection(currentEpisodeIndex)
if (isInitialized) releasePlayer()
playbackPosition = PrefManager.getCustomVal(
"${media.id}_${ep.number}",
0
)
initPlayer()
preloading = false
val context = this
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode)
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
if ((isOnline(context) && !offline) && Discord.token != null && !incognito) {
lifecycleScope.launch {
val presence = RPC.createPresence(RPC.Companion.RPCData(
applicationId = Discord.application_Id,
type = RPC.Type.WATCHING,
activityName = media.userPreferredName,
details = ep.title?.takeIf { it.isNotEmpty() } ?: getString(
R.string.episode_num,
ep.number
),
state = "Episode : ${ep.number}/${media.anime?.totalEpisodes ?: "??"}",
largeImage = media.cover?.let {
RPC.Link(
media.userPreferredName,
it
)
},
smallImage = RPC.Link(
"Dantotsu",
Discord.small_Image
),
buttons = mutableListOf(
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
RPC.Link(
"Stream on Dantotsu",
"https://github.com/rebelonion/Dantotsu/"
)
)
)
)
val intent = Intent(context, DiscordService::class.java).apply {
putExtra("presence", presence)
}
DiscordServiceRunningSingleton.running = true
startService(intent)
}
}
updateProgress()
}
}
//FullScreen
isFullscreen = PrefManager.getCustomVal("${media.id}_fullscreenInt", isFullscreen)
playerView.resizeMode = when (isFullscreen) {
0 -> AspectRatioFrameLayout.RESIZE_MODE_FIT
1 -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
2 -> AspectRatioFrameLayout.RESIZE_MODE_FILL
else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
}
exoScreen.setOnClickListener {
if (isFullscreen < 2) isFullscreen += 1 else isFullscreen = 0
playerView.resizeMode = when (isFullscreen) {
0 -> AspectRatioFrameLayout.RESIZE_MODE_FIT
1 -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
2 -> AspectRatioFrameLayout.RESIZE_MODE_FILL
else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
}
snackString(
when (isFullscreen) {
0 -> "Original"
1 -> "Zoom"
2 -> "Stretch"
else -> "Original"
}
)
PrefManager.setCustomVal("${media.id}_fullscreenInt", isFullscreen)
}
//Cast
if (PrefManager.getVal(PrefName.Cast)) {
playerView.findViewById<CustomCastButton>(R.id.exo_cast).apply {
visibility = View.VISIBLE
if(PrefManager.getVal(PrefName.UseInternalCast)) {
try {
CastButtonFactory.setUpMediaRouteButton(context, this)
dialogFactory = CustomCastThemeFactory()
} catch (e: Exception) {
isCastApiAvailable = false
}
} else {
setCastCallback { cast() }
}
}
}
//Settings
exoSettings.setOnClickListener {
PrefManager.setCustomVal(
"${media.id}_${media.anime!!.selectedEpisode}",
exoPlayer.currentPosition
)
val intent = Intent(this, PlayerSettingsActivity::class.java).apply {
putExtra("subtitle", subtitle)
}
exoPlayer.pause()
startActivity(intent)
}
//Speed
val speeds =
if (PrefManager.getVal(PrefName.CursedSpeeds))
arrayOf(1f, 1.25f, 1.5f, 1.75f, 2f, 2.5f, 3f, 4f, 5f, 10f, 25f, 50f)
else
arrayOf(
0.25f,
0.33f,
0.5f,
0.66f,
0.75f,
1f,
1.15f,
1.25f,
1.33f,
1.5f,
1.66f,
1.75f,
2f
)
val speedsName = speeds.map { "${it}x" }.toTypedArray()
//var curSpeed = loadData("${media.id}_speed", this) ?: settings.defaultSpeed
var curSpeed = PrefManager.getCustomVal(
"${media.id}_speed",
PrefManager.getVal<Int>(PrefName.DefaultSpeed)
)
playbackParameters = PlaybackParameters(speeds[curSpeed])
var speed: Float
val speedDialog =
AlertDialog.Builder(this, R.style.MyPopup).setTitle(getString(R.string.speed))
exoSpeed.setOnClickListener {
val dialog = speedDialog.setSingleChoiceItems(speedsName, curSpeed) { dialog, i ->
if (isInitialized) {
PrefManager.setCustomVal("${media.id}_speed", i)
speed = speeds[i]
curSpeed = i
playbackParameters = PlaybackParameters(speed)
exoPlayer.playbackParameters = playbackParameters
dialog.dismiss()
hideSystemBars()
}
}.show()
dialog.window?.setDimAmount(0.8f)
}
speedDialog.setOnCancelListener { hideSystemBars() }
if (PrefManager.getVal(PrefName.AutoPlay)) {
var touchTimer = Timer()
fun touched() {
interacted = true
touchTimer.apply {
cancel()
purge()
}
touchTimer = Timer()
touchTimer.schedule(object : TimerTask() {
override fun run() {
interacted = false
}
}, 1000 * 60 * 60)
}
playerView.findViewById<View>(R.id.exo_touch_view).setOnTouchListener { _, _ ->
touched()
false
}
}
isFullscreen = PrefManager.getVal(PrefName.Resize)
playerView.resizeMode = when (isFullscreen) {
0 -> AspectRatioFrameLayout.RESIZE_MODE_FIT
1 -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
2 -> AspectRatioFrameLayout.RESIZE_MODE_FILL
else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
}
preloading = false
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
val showProgressDialog =
if (PrefManager.getVal(PrefName.AskIndividualPlayer)) PrefManager.getCustomVal(
"${media.id}_ProgressDialog",
true
) else false
if (!incognito && showProgressDialog && Anilist.userid != null && if (media.isAdult) PrefManager.getVal(
PrefName.UpdateForHPlayer
) else true
) {
AlertDialog.Builder(this, R.style.MyPopup)
.setTitle(getString(R.string.auto_update, media.userPreferredName))
.apply {
setOnCancelListener { hideSystemBars() }
setCancelable(false)
setPositiveButton(getString(R.string.yes)) { dialog, _ ->
PrefManager.setCustomVal(
"${media.id}_ProgressDialog",
false
)
PrefManager.setCustomVal(
"${media.id}_save_progress",
true
)
dialog.dismiss()
model.setEpisode(episodes[media.anime!!.selectedEpisode!!]!!, "invoke")
}
setNegativeButton(getString(R.string.no)) { dialog, _ ->
PrefManager.setCustomVal(
"${media.id}_ProgressDialog",
false
)
PrefManager.setCustomVal(
"${media.id}_save_progress",
false
)
toast(getString(R.string.reset_auto_update))
dialog.dismiss()
model.setEpisode(episodes[media.anime!!.selectedEpisode!!]!!, "invoke")
}
show()
}
} else model.setEpisode(episodes[media.anime!!.selectedEpisode!!]!!, "invoke")
//Start the recursive Fun
if (PrefManager.getVal(PrefName.TimeStampsEnabled))
updateTimeStamp()
}
private fun initPlayer() {
checkNotch()
PrefManager.setCustomVal(
"${media.id}_current_ep",
media.anime!!.selectedEpisode!!
)
val list = (PrefManager.getNullableCustomVal("continueAnimeList", listOf<Int>(), List::class.java) as List<Int>).toMutableList()
if (list.contains(media.id)) list.remove(media.id)
list.add(media.id)
PrefManager.setCustomVal("continueAnimeList", list)
lifecycleScope.launch(Dispatchers.IO) {
extractor?.onVideoStopped(video)
}
val ext = episode.extractors?.find { it.server.name == episode.selectedExtractor } ?: return
extractor = ext
video = ext.videos.getOrNull(episode.selectedVideo) ?: return
subtitle = intent.getSerialized("subtitle")
?: when (val subLang: String? =
PrefManager.getNullableCustomVal("subLang_${media.id}", null, String::class.java)) {
null -> {
when (episode.selectedSubtitle) {
null -> null
-1 -> ext.subtitles.find { it.language.trim() == "English" || it.language == "en-US" }
else -> ext.subtitles.getOrNull(episode.selectedSubtitle!!)
}
}
"None" -> ext.subtitles.let { null }
else -> ext.subtitles.find { it.language == subLang }
}
//Subtitles
exoSubtitle.visibility = if (ext.subtitles.isNotEmpty()) View.VISIBLE else View.GONE
exoSubtitle.setOnClickListener {
subClick()
}
var sub: MediaItem.SubtitleConfiguration? = null
if (subtitle != null) {
//var localFile: String? = null
if (subtitle?.type == SubtitleType.UNKNOWN) {
val context = this
runBlocking {
val type = SubtitleDownloader.loadSubtitleType(context, subtitle!!.file.url)
val fileUri = Uri.parse(subtitle!!.file.url)
sub = MediaItem.SubtitleConfiguration
.Builder(fileUri)
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
.setMimeType(
when (type) {
SubtitleType.VTT -> MimeTypes.TEXT_SSA
SubtitleType.ASS -> MimeTypes.TEXT_SSA
SubtitleType.SRT -> MimeTypes.TEXT_SSA
else -> MimeTypes.TEXT_SSA
}
)
.setId("69")
.build()
}
println("sub: $sub")
} else {
val subUri = Uri.parse(subtitle!!.file.url)
sub = MediaItem.SubtitleConfiguration
.Builder(subUri)
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
.setMimeType(
when (subtitle?.type) {
SubtitleType.VTT -> MimeTypes.TEXT_VTT
SubtitleType.ASS -> MimeTypes.TEXT_SSA
SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP
else -> MimeTypes.TEXT_UNKNOWN
}
)
.setId("69")
.build()
}
}
lifecycleScope.launch(Dispatchers.IO) {
ext.onVideoPlayed(video)
}
val simpleCache = VideoCache.getInstance(this)
val httpClient = okHttpClient.newBuilder().apply {
ignoreAllSSLErrors()
followRedirects(true)
followSslRedirects(true)
}.build()
val dataSourceFactory = DataSource.Factory {
val dataSource: HttpDataSource = OkHttpDataSource.Factory(httpClient).createDataSource()
defaultHeaders.forEach {
dataSource.setRequestProperty(it.key, it.value)
}
video?.file?.headers?.forEach {
dataSource.setRequestProperty(it.key, it.value)
}
dataSource
}
val dafuckDataSourceFactory =
DefaultDataSourceFactory(this, Util.getUserAgent(this, R.string.app_name.toString()))
cacheFactory = CacheDataSource.Factory().apply {
setCache(Helper.getSimpleCache(this@ExoplayerView))
if (ext.server.offline) {
setUpstreamDataSourceFactory(dafuckDataSourceFactory)
} else {
setUpstreamDataSourceFactory(dataSourceFactory)
}
setCacheWriteDataSinkFactory(null)
}
val mimeType = when (video?.format) {
VideoType.M3U8 -> MimeTypes.APPLICATION_M3U8
VideoType.DASH -> MimeTypes.APPLICATION_MPD
else -> MimeTypes.APPLICATION_MP4
}
val downloadedMediaItem = if (ext.server.offline) {
val key = ext.server.name
downloadId = PrefManager.getAnimeDownloadPreferences()
.getString(key, null)
if (downloadId != null) {
Helper.downloadManager(this)
.downloadIndex.getDownload(downloadId!!)?.request?.toMediaItem()
} else {
snackString("Download not found")
null
}
} else null
mediaItem = if (downloadedMediaItem == null) {
val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType)
Logger.log("url: ${video!!.file.url}")
Logger.log("mimeType: $mimeType")
if (sub != null) {
val listofnotnullsubs = listOfNotNull(sub)
builder.setSubtitleConfigurations(listofnotnullsubs)
}
builder.build()
} else {
val addedSubsDownloadedMediaItem = downloadedMediaItem.buildUpon()
if (sub != null) {
val listofnotnullsubs = listOfNotNull(sub)
val addLanguage = listofnotnullsubs[0].buildUpon().setLanguage("en").build()
addedSubsDownloadedMediaItem.setSubtitleConfigurations(listOf(addLanguage))
episode.selectedSubtitle = 0
}
addedSubsDownloadedMediaItem.build()
}
//Source
exoSource.setOnClickListener {
sourceClick()
}
//Quality Track
trackSelector = DefaultTrackSelector(this)
trackSelector.setParameters(
trackSelector.buildUponParameters()
.setAllowVideoMixedMimeTypeAdaptiveness(true)
.setAllowVideoNonSeamlessAdaptiveness(true)
.setSelectUndeterminedTextLanguage(true)
.setAllowAudioMixedMimeTypeAdaptiveness(true)
.setAllowMultipleAdaptiveSelections(true)
.setPreferredTextLanguage(subtitle?.language ?: "en")
.setPreferredTextRoleFlags(C.ROLE_FLAG_SUBTITLE)
.setRendererDisabled(TRACK_TYPE_VIDEO, false)
.setRendererDisabled(C.TRACK_TYPE_AUDIO, false)
.setRendererDisabled(C.TRACK_TYPE_TEXT, false)
.setMaxVideoSize(1, 1)
//.setOverrideForType(
// TrackSelectionOverride(trackSelector, 2))
)
if (playbackPosition != 0L && !changingServer && !PrefManager.getVal<Boolean>(PrefName.AlwaysContinue)) {
val time = String.format(
"%02d:%02d:%02d", TimeUnit.MILLISECONDS.toHours(playbackPosition),
TimeUnit.MILLISECONDS.toMinutes(playbackPosition) - TimeUnit.HOURS.toMinutes(
TimeUnit.MILLISECONDS.toHours(
playbackPosition
)
),
TimeUnit.MILLISECONDS.toSeconds(playbackPosition) - TimeUnit.MINUTES.toSeconds(
TimeUnit.MILLISECONDS.toMinutes(
playbackPosition
)
)
)
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
.setTitle(getString(R.string.continue_from, time)).apply {
setCancelable(false)
setPositiveButton(getString(R.string.yes)) { d, _ ->
buildExoplayer()
d.dismiss()
}
setNegativeButton(getString(R.string.no)) { d, _ ->
playbackPosition = 0L
buildExoplayer()
d.dismiss()
}
}.show()
dialog.window?.setDimAmount(0.8f)
} else buildExoplayer()
}
private fun buildExoplayer() {
//Player
val DEFAULT_MIN_BUFFER_MS = 600000
val DEFAULT_MAX_BUFFER_MS = 600000
val BUFFER_FOR_PLAYBACK_MS = 2500
val BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000
val loadControl = DefaultLoadControl.Builder()
.setBackBuffer(1000 * 60 * 2, true)
.setBufferDurationsMs(
DEFAULT_MIN_BUFFER_MS,
DEFAULT_MAX_BUFFER_MS,
BUFFER_FOR_PLAYBACK_MS,
BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
)
.build()
hideSystemBars()
exoPlayer = ExoPlayer.Builder(this)
.setMediaSourceFactory(DefaultMediaSourceFactory(cacheFactory))
.setTrackSelector(trackSelector)
.setLoadControl(loadControl)
.build().apply {
playWhenReady = true
this.playbackParameters = this@ExoplayerView.playbackParameters
setMediaItem(mediaItem)
prepare()
PrefManager.getCustomVal(
"${media.id}_${media.anime!!.selectedEpisode}_max",
Long.MAX_VALUE
)
.takeIf { it != Long.MAX_VALUE }
?.let { if (it <= playbackPosition) playbackPosition = max(0, it - 5) }
seekTo(playbackPosition)
}
playerView.player = exoPlayer
try {
val rightNow = Calendar.getInstance()
mediaSession = MediaSession.Builder(this, exoPlayer)
.setId(rightNow.timeInMillis.toString())
.build()
} catch (e: Exception) {
toast(e.toString())
}
exoPlayer.addListener(this)
exoPlayer.addAnalyticsListener(EventLogger())
isInitialized = true
}
/*private fun selectSubtitleTrack() { saving this for later
// Get the current track groups
val trackGroups = exoPlayer.currentTrackGroups
// Prepare a track selector parameters builder
val parametersBuilder = DefaultTrackSelector.ParametersBuilder(this)
// Iterate through the track groups to find the subtitle tracks
for (i in 0 until trackGroups.length) {
val trackGroup = trackGroups[i]
for (j in 0 until trackGroup.length) {
val trackMetadata = trackGroup.getFormat(j)
// Check if the track is a subtitle track
if (MimeTypes.isText(trackMetadata.sampleMimeType)) {
parametersBuilder.setRendererDisabled(i, false) // Enable the renderer for this track group
parametersBuilder.setSelectionOverride(i, trackGroups, DefaultTrackSelector.SelectionOverride(j, 0)) // Override to select this track
break
}
}
}
// Apply the track selector parameters to select the subtitle
trackSelector.setParameters(parametersBuilder)
}*/
private fun releasePlayer() {
isPlayerPlaying = exoPlayer.playWhenReady
playbackPosition = exoPlayer.currentPosition
disappeared = false
functionstarted = false
exoPlayer.release()
VideoCache.release()
mediaSession?.release()
if (DiscordServiceRunningSingleton.running) {
val stopIntent = Intent(this, DiscordService::class.java)
DiscordServiceRunningSingleton.running = false
stopService(stopIntent)
}
}
override fun onSaveInstanceState(outState: Bundle) {
if (isInitialized) {
outState.putInt(resumeWindow, exoPlayer.currentMediaItemIndex)
outState.putLong(resumePosition, exoPlayer.currentPosition)
}
outState.putInt(playerFullscreen, isFullscreen)
outState.putBoolean(playerOnPlay, isPlayerPlaying)
super.onSaveInstanceState(outState)
}
private fun sourceClick() {
changingServer = true
media.selected!!.server = null
PrefManager.setCustomVal(
"${media.id}_${media.anime!!.selectedEpisode}", exoPlayer.currentPosition
)
model.saveSelected(media.id, media.selected!!)
model.onEpisodeClick(
media, episode.number, this.supportFragmentManager,
launch = false
)
}
private fun subClick() {
PrefManager.setCustomVal(
"${media.id}_${media.anime!!.selectedEpisode}", exoPlayer.currentPosition
)
model.saveSelected(media.id, media.selected!!)
SubtitleDialogFragment().show(supportFragmentManager, "dialog")
}
override fun onPause() {
super.onPause()
orientationListener?.disable()
if (isInitialized) {
if (castPlayer?.isPlaying == false) {
playerView.player?.pause()
}
if (exoPlayer.currentPosition > 5000) {
PrefManager.setCustomVal(
"${media.id}_${media.anime!!.selectedEpisode}",
exoPlayer.currentPosition
)
}
}
}
override fun onResume() {
super.onResume()
orientationListener?.enable()
hideSystemBars()
if (isInitialized) {
playerView.onResume()
playerView.useController = true
}
}
override fun onStop() {
if (castPlayer?.isPlaying == false) {
playerView.player?.pause()
}
super.onStop()
}
private var wasPlaying = false
override fun onWindowFocusChanged(hasFocus: Boolean) {
if (PrefManager.getVal(PrefName.FocusPause) && !epChanging) {
if (isInitialized && !hasFocus) wasPlaying = exoPlayer.isPlaying
if (hasFocus) {
if (isInitialized && wasPlaying) exoPlayer.play()
} else {
if (isInitialized) exoPlayer.pause()
}
}
super.onWindowFocusChanged(hasFocus)
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
if (!isBuffering) {
isPlayerPlaying = isPlaying
playerView.keepScreenOn = isPlaying
(exoPlay.drawable as Animatable?)?.start()
if (!this.isDestroyed) Glide.with(this)
.load(if (isPlaying) R.drawable.anim_play_to_pause else R.drawable.anim_pause_to_play)
.into(exoPlay)
}
}
override fun onRenderedFirstFrame() {
super.onRenderedFirstFrame()
PrefManager.setCustomVal(
"${media.id}_${media.anime!!.selectedEpisode}_max",
exoPlayer.duration
)
val height = (exoPlayer.videoFormat ?: return).height
val width = (exoPlayer.videoFormat ?: return).width
aspectRatio = Rational(width, height)
videoInfo.text = "Quality: ${height}p"
if (exoPlayer.duration < playbackPosition)
exoPlayer.seekTo(0)
//if playbackPosition is within 92% of the episode length, reset it to 0
if (playbackPosition > exoPlayer.duration.toFloat() * 0.92) {
playbackPosition = 0
exoPlayer.seekTo(0)
}
if (!isTimeStampsLoaded && PrefManager.getVal(PrefName.TimeStampsEnabled)) {
val dur = exoPlayer.duration
lifecycleScope.launch(Dispatchers.IO) {
model.loadTimeStamps(
media.idMAL,
media.anime?.selectedEpisode?.trim()?.toIntOrNull(),
dur / 1000,
PrefManager.getVal(PrefName.UseProxyForTimeStamps)
)
}
}
}
//Link Preloading
private var preloading = false
private fun updateProgress() {
if (isInitialized) {
if (exoPlayer.currentPosition.toFloat() / exoPlayer.duration > PrefManager.getVal<Float>(
PrefName.WatchPercentage
)
) {
preloading = true
nextEpisode(false) { i ->
val ep = episodes[episodeArr[currentEpisodeIndex + i]] ?: return@nextEpisode
val selected = media.selected ?: return@nextEpisode
lifecycleScope.launch(Dispatchers.IO) {
if (media.selected!!.server != null)
model.loadEpisodeSingleVideo(ep, selected, false)
else
model.loadEpisodeVideos(ep, selected.sourceIndex, false)
}
}
}
}
if (!preloading) handler.postDelayed({
updateProgress()
}, 2500)
}
//TimeStamp Updating
private var currentTimeStamp: AniSkip.Stamp? = null
private var skippedTimeStamps: MutableList<AniSkip.Stamp> = mutableListOf()
private fun updateTimeStamp() {
if (isInitialized) {
val playerCurrentTime = exoPlayer.currentPosition / 1000
currentTimeStamp = model.timeStamps.value?.find { timestamp ->
timestamp.interval.startTime < playerCurrentTime && playerCurrentTime < (timestamp.interval.endTime - 1)
}
val new = currentTimeStamp
timeStampText.text = if (new != null) {
fun disappearSkip() {
functionstarted = true
skipTimeButton.visibility = View.VISIBLE
exoSkip.visibility = View.GONE
skipTimeText.text = new.skipType.getType()
skipTimeButton.setOnClickListener {
exoPlayer.seekTo((new.interval.endTime * 1000).toLong())
}
var timer: CountDownTimer? = null
fun cancelTimer() {
timer?.cancel()
timer = null
return
}
if (timer == null) {
timer = object : CountDownTimer(5000, 1000) {
override fun onTick(millisUntilFinished: Long) {
if (new == null){
skipTimeButton.visibility = View.GONE
exoSkip.visibility = View.VISIBLE
disappeared = false
functionstarted = false
cancelTimer()
}
}
override fun onFinish() {
skipTimeButton.visibility = View.GONE
exoSkip.visibility = View.VISIBLE
disappeared = true
functionstarted = false
cancelTimer()
}
}
timer?.start()
}
}
if (PrefManager.getVal(PrefName.ShowTimeStampButton)) {
if (!functionstarted && !disappeared && PrefManager.getVal<Boolean>(PrefName.AutoHideTimeStamps)) {
disappearSkip()
} else if (!PrefManager.getVal<Boolean>(PrefName.AutoHideTimeStamps)){
skipTimeButton.visibility = View.VISIBLE
exoSkip.visibility = View.GONE
skipTimeText.text = new.skipType.getType()
skipTimeButton.setOnClickListener {
exoPlayer.seekTo((new.interval.endTime * 1000).toLong())
}
}
}
if (PrefManager.getVal(PrefName.AutoSkipOPED) && (new.skipType == "op" || new.skipType == "ed")
&& !skippedTimeStamps.contains(new)
) {
exoPlayer.seekTo((new.interval.endTime * 1000).toLong())
skippedTimeStamps.add(new)
}
new.skipType.getType()
} else {
disappeared = false
functionstarted = false
skipTimeButton.visibility = View.GONE
exoSkip.isVisible = PrefManager.getVal<Int>(PrefName.SkipTime) > 0
""
}
}
handler.postDelayed({
updateTimeStamp()
}, 500)
}
override fun onTracksChanged(tracks: Tracks) {
tracks.groups.forEach {
println("Track__: $it")
println("Track__: ${it.length}")
println("Track__: ${it.isSelected}")
println("Track__: ${it.type}")
println("Track__: ${it.mediaTrackGroup.id}")
if (it.type == 3 && it.mediaTrackGroup.id == "1:") {
playerView.player?.trackSelectionParameters =
playerView.player?.trackSelectionParameters?.buildUpon()
?.setOverrideForType(
TrackSelectionOverride(it.mediaTrackGroup, it.length - 1)
)
?.build()!!
} else if (it.type == 3) {
playerView.player?.trackSelectionParameters =
playerView.player?.trackSelectionParameters?.buildUpon()
?.addOverride(
TrackSelectionOverride(it.mediaTrackGroup, listOf())
)
?.build()!!
}
}
println("Track: ${tracks.groups.size}")
}
override fun onPlayerError(error: PlaybackException) {
when (error.errorCode) {
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED
-> {
toast("Source Exception : ${error.message}")
isPlayerPlaying = true
sourceClick()
}
else
-> {
toast("Player Error ${error.errorCode} (${error.errorCodeName}) : ${error.message}")
Injekt.get<CrashlyticsInterface>().logException(error)
}
}
}
private var isBuffering = true
override fun onPlaybackStateChanged(playbackState: Int) {
if (playbackState == ExoPlayer.STATE_READY) {
exoPlayer.play()
if (episodeLength == 0f) {
episodeLength = exoPlayer.duration.toFloat()
}
}
isBuffering = playbackState == Player.STATE_BUFFERING
if (playbackState == Player.STATE_ENDED && PrefManager.getVal(PrefName.AutoPlay)) {
if (interacted) exoNext.performClick()
else toast(getString(R.string.autoplay_cancelled))
}
super.onPlaybackStateChanged(playbackState)
}
private fun updateAniProgress() {
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
val episodeEnd = exoPlayer.currentPosition / episodeLength > PrefManager.getVal<Float>(
PrefName.WatchPercentage
)
val episode0 = currentEpisodeIndex == 0 && PrefManager.getVal(PrefName.ChapterZeroPlayer)
if (!incognito && (episodeEnd || episode0) && Anilist.userid != null
)
if (PrefManager.getCustomVal(
"${media.id}_save_progress",
true
) && (if (media.isAdult) PrefManager.getVal(PrefName.UpdateForHPlayer) else true)
) {
if (episode0) {
updateProgress(media, "0")
} else {
media.anime!!.selectedEpisode?.apply {
updateProgress(media, this)
}
}
}
}
private fun nextEpisode(toast: Boolean = true, runnable: ((Int) -> Unit)) {
var isFiller = true
var i = 1
while (isFiller) {
if (episodeArr.size > currentEpisodeIndex + i) {
isFiller =
if (PrefManager.getVal(PrefName.AutoSkipFiller)) episodes[episodeArr[currentEpisodeIndex + i]]?.filler
?: false else false
if (!isFiller) runnable.invoke(i)
i++
} else {
if (toast)
toast(getString(R.string.no_next_episode))
isFiller = false
}
}
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
finishAndRemoveTask()
startActivity(intent)
}
override fun onDestroy() {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
CoroutineScope(Dispatchers.IO).launch {
tryWithSuspend(true) {
extractor?.onVideoStopped(video)
}
}
if (isInitialized) {
updateAniProgress()
disappeared = false
functionstarted = false
releasePlayer()
}
super.onDestroy()
finishAndRemoveTask()
}
// Cast
private fun cast() {
val videoURL = video?.file?.url ?: return
val shareVideo = Intent(Intent.ACTION_VIEW)
shareVideo.setDataAndType(Uri.parse(videoURL), "video/*")
shareVideo.setPackage("com.instantbits.cast.webvideo")
if (subtitle != null) shareVideo.putExtra("subtitle", subtitle!!.file.url)
shareVideo.putExtra(
"title",
media.userPreferredName + " : Ep " + episodeTitleArr[currentEpisodeIndex]
)
shareVideo.putExtra("poster", episode.thumb?.url ?: media.cover)
val headers = Bundle()
defaultHeaders.forEach {
headers.putString(it.key, it.value)
}
video?.file?.headers?.forEach {
headers.putString(it.key, it.value)
}
shareVideo.putExtra("android.media.intent.extra.HTTP_HEADERS", headers)
shareVideo.putExtra("secure_uri", true)
try {
startActivity(shareVideo)
} catch (ex: ActivityNotFoundException) {
val intent = Intent(Intent.ACTION_VIEW)
val uriString = "market://details?id=com.instantbits.cast.webvideo"
intent.data = Uri.parse(uriString)
startActivity(intent)
}
}
// Enter PiP Mode
@Suppress("DEPRECATION")
private fun enterPipMode() {
wasPlaying = isPlayerPlaying
if (!pipEnabled) return
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
enterPictureInPictureMode(
PictureInPictureParams
.Builder()
.setAspectRatio(aspectRatio)
.build()
)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
enterPictureInPictureMode()
}
} catch (e: Exception) {
logError(e)
}
}
private fun onPiPChanged(isInPictureInPictureMode: Boolean) {
playerView.useController = !isInPictureInPictureMode
if (isInPictureInPictureMode) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
orientationListener?.disable()
} else {
orientationListener?.enable()
}
if (isInitialized) {
PrefManager.setCustomVal(
"${media.id}_${episode.number}",
exoPlayer.currentPosition
)
if (wasPlaying) exoPlayer.play()
}
}
@Deprecated("Deprecated in Java")
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
onPiPChanged(isInPictureInPictureMode)
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
}
@RequiresApi(Build.VERSION_CODES.N)
override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {
onPiPChanged(isInPictureInPictureMode)
super.onPictureInPictureUiStateChanged(pipState)
}
@RequiresApi(Build.VERSION_CODES.O)
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration
) {
onPiPChanged(isInPictureInPictureMode)
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
}
private val keyMap: MutableMap<Int, (() -> Unit)?> = mutableMapOf(
KEYCODE_DPAD_RIGHT to null,
KEYCODE_DPAD_LEFT to null,
KEYCODE_SPACE to { exoPlay.performClick() },
KEYCODE_N to { exoNext.performClick() },
KEYCODE_B to { exoPrev.performClick() }
)
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
return if (keyMap.containsKey(event.keyCode)) {
(event.action == ACTION_UP).also {
if (isInitialized && it) keyMap[event.keyCode]?.invoke()
}
} else {
super.dispatchKeyEvent(event)
}
}
private fun startCastPlayer() {
if (!isCastApiAvailable) {
snackString("Cast API not available")
return
}
//make sure mediaItem is initialized and castPlayer is not null
if (!this::mediaItem.isInitialized || castPlayer == null) return
castPlayer?.setMediaItem(mediaItem)
castPlayer?.prepare()
playerView.player = castPlayer
exoPlayer.stop()
castPlayer?.addListener(object : Player.Listener {
//if the player is paused changed, we want to update the UI
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason)
if (playWhenReady) {
(exoPlay.drawable as Animatable?)?.start()
Glide.with(this@ExoplayerView)
.load(R.drawable.anim_play_to_pause)
.into(exoPlay)
} else {
(exoPlay.drawable as Animatable?)?.start()
Glide.with(this@ExoplayerView)
.load(R.drawable.anim_pause_to_play)
.into(exoPlay)
}
}
})
}
private fun startExoPlayer() {
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
playerView.player = exoPlayer
castPlayer?.stop()
}
override fun onCastSessionAvailable() {
if (isCastApiAvailable && !this.isDestroyed) {
startCastPlayer()
}
}
override fun onCastSessionUnavailable() {
startExoPlayer()
}
@SuppressLint("ViewConstructor")
class ExtendedTimeBar(
context: Context,
attrs: AttributeSet?
) : DefaultTimeBar(context, attrs) {
private var enabled = false
private var forceDisabled = false
override fun setEnabled(enabled: Boolean) {
this.enabled = enabled
super.setEnabled(!forceDisabled && this.enabled)
}
fun setForceDisabled(forceDisabled: Boolean) {
this.forceDisabled = forceDisabled
isEnabled = enabled
}
}
}
class CustomCastButton : MediaRouteButton {
private var castCallback: (() -> Unit)? = null
fun setCastCallback(castCallback: () -> Unit) {
this.castCallback = castCallback
}
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context, attrs: AttributeSet, castCallback: () -> Unit) : super(context, attrs) {
this.castCallback = castCallback
}
override fun performClick(): Boolean {
return if (PrefManager.getVal(PrefName.UseInternalCast)) {
super.performClick()
} else {
castCallback?.let { it() }
true
}
}
}