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.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.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.datasource.DataSource 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.mediarouter.app.MediaRouteButton import ani.dantotsu.* import ani.dantotsu.R import ani.dantotsu.connections.anilist.Anilist 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.Download.download import ani.dantotsu.others.LangSet import ani.dantotsu.others.ResettableTimer import ani.dantotsu.others.getSerialized import ani.dantotsu.parsers.* import ani.dantotsu.settings.PlayerSettings import ani.dantotsu.settings.PlayerSettingsActivity import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.themes.ThemeManager 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.google.firebase.crashlytics.FirebaseCrashlytics 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 okhttp3.internal.immutableListOf 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 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 exoQuality: 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 videoName: TextView private lateinit var videoInfo: TextView private lateinit var serverInfo: 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 private lateinit var episodeArr: List private lateinit var episodeTitleArr: ArrayList 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) var settings = PlayerSettings() private var uiSettings = UserInterfaceSettings() 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(R.id.exo_controller_margin) .updateLayoutParams { if (orientation == Configuration.ORIENTATION_LANDSCAPE) { marginStart = notchHeight marginEnd = notchHeight topMargin = 0 } else { topMargin = notchHeight marginStart = 0 marginEnd = 0 } } playerView.findViewById(androidx.media3.ui.R.id.exo_buffering).translationY = (if (orientation == Configuration.ORIENTATION_LANDSCAPE) 0 else (notchHeight + 8f.px)).dp exoBrightnessCont.updateLayoutParams { marginEnd = if (orientation == Configuration.ORIENTATION_LANDSCAPE) notchHeight else 0 } exoVolumeCont.updateLayoutParams { marginStart = if (orientation == Configuration.ORIENTATION_LANDSCAPE) notchHeight else 0 } } } private fun setupSubFormatting(playerView: PlayerView, settings: PlayerSettings) { val primaryColor = when (settings.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 (settings.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 (settings.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 (settings.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 (settings.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 (settings.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.century_gothic_bold) 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) LangSet.setLocale(this) 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() } settings = loadData("player_settings") ?: PlayerSettings().apply { saveData( "player_settings", this ) } uiSettings = loadData("ui_settings") ?: UserInterfaceSettings().apply { saveData( "ui_settings", this ) } playerView = findViewById(R.id.player_view) exoQuality = playerView.findViewById(R.id.exo_quality) 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) { 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 } } } orientationListener?.enable() requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE exoRotate.setOnClickListener { requestedOrientation = rotation it.visibility = View.GONE } } setupSubFormatting(playerView, settings) playerView.subtitleView?.alpha = when (settings.subtitles) { true -> 1f false -> 0f } val fontSize = settings.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(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 (settings.autoSkipOPED) 1f else 0.3f exoSkipOpEd.setOnClickListener { settings.autoSkipOPED = if (settings.autoSkipOPED) { snackString(getString(R.string.disabled_auto_skip)) false } else { snackString(getString(R.string.auto_skip)) true } saveData("player_settings", settings) exoSkipOpEd.alpha = if (settings.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) && settings.pip if (pipEnabled) { exoPip.visibility = View.VISIBLE exoPip.setOnClickListener { enterPipMode() } } else exoPip.visibility = View.GONE } //Lock Button var locked = false val container = playerView.findViewById(R.id.exo_controller_cont) val screen = playerView.findViewById(R.id.exo_black_screen) val lockButton = playerView.findViewById(R.id.exo_unlock) val timeline = playerView.findViewById(androidx.media3.ui.R.id.exo_progress) playerView.findViewById(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 if (settings.skipTime > 0) { exoSkip.findViewById(R.id.exo_skip_time).text = settings.skipTime.toString() exoSkip.setOnClickListener { if (isInitialized) exoPlayer.seekTo(exoPlayer.currentPosition + settings.skipTime * 1000) } exoSkip.setOnLongClickListener { val dialog = Dialog(this, R.style.DialogTheme) 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 (settings.skipTime <= 120) { dialog.findViewById(R.id.seekbar).value = settings.skipTime.toFloat() } else { dialog.findViewById(R.id.seekbar).value = 120f } dialog.findViewById(R.id.seekbar).addOnChangeListener { _, value, _ -> settings.skipTime = value.toInt() saveData(player, settings) playerView.findViewById(R.id.exo_skip_time).text = settings.skipTime.toString() dialog.findViewById(R.id.seekbar_value).text = settings.skipTime.toString() } dialog.findViewById(R.id.seekbar) .addOnSliderTouchListener(object : Slider.OnSliderTouchListener { override fun onStartTrackingTouch(slider: Slider) {} override fun onStopTrackingTouch(slider: Slider) { dialog.dismiss() } }) dialog.findViewById(R.id.seekbar_title).text = getString(R.string.skip_time) dialog.findViewById(R.id.seekbar_value).text = settings.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 * uiSettings.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 = (uiSettings.animationSpeed * 200).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(R.id.exo_full_area).setOnClickListener { handleController() } val rewindText = playerView.findViewById(R.id.exo_fast_rewind_anim) val forwardText = playerView.findViewById(R.id.exo_fast_forward_anim) val fastForwardCard = playerView.findViewById(R.id.exo_fast_forward) val fastRewindCard = playerView.findViewById(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 (card, text) = if (forward) { forwardText.text = "+${settings.seekTime * ++seekTimesF}" handler.post { exoPlayer.seekTo(exoPlayer.currentPosition + settings.seekTime * 1000) } fastForwardCard to forwardText } else { rewindText.text = "-${settings.seekTime * ++seekTimesR}" handler.post { exoPlayer.seekTo(exoPlayer.currentPosition - settings.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 (!settings.doubleTap) { playerView.findViewById(R.id.exo_fast_forward_button_cont).visibility = View.VISIBLE playerView.findViewById(R.id.exo_fast_rewind_button_cont).visibility = View.VISIBLE playerView.findViewById(R.id.exo_fast_forward_button).setOnClickListener { if (isInitialized) { seek(true) } } playerView.findViewById(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 (settings.gestures || settings.doubleTap) { fun doubleTap(forward: Boolean, event: MotionEvent) { if (!locked && isInitialized && settings.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(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 (settings.fastforward) fastForward() } override fun onDoubleClick(event: MotionEvent) { doubleTap(false, event) } override fun onScrollYClick(y: Float) { if (!locked && settings.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(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 (settings.fastforward) fastForward() } override fun onDoubleClick(event: MotionEvent) { doubleTap(true, event) } override fun onScrollYClick(y: Float) { if (!locked && settings.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(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) videoName = playerView.findViewById(R.id.exo_video_name) videoInfo = playerView.findViewById(R.id.exo_video_info) serverInfo = playerView.findViewById(R.id.exo_server_info) if (!settings.videoInfo) { videoName.visibility = View.GONE videoInfo.visibility = View.GONE serverInfo.visibility = View.GONE } else { videoName.isSelected = true } model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources serverInfo.text = model.watchSources!!.names.getOrNull(media.selected!!.sourceIndex) ?: model.watchSources!!.names[0] model.epChanged.observe(this) { epChanging = !it } //Anime Title animeTitle.text = media.userPreferredName episodeArr = episodes.keys.toList() currentEpisodeIndex = episodeArr.indexOf(media.anime!!.selectedEpisode!!) val episodeTitleArr = arrayListOf() episodes.forEach { val episode = it.value val cleanedTitle = AnimeNameAdapter.removeEpisodeNumberCompletely(episode.title ?: "") episodeTitleArr.add("Episode ${episode.number}${if (episode.filler) " [Filler]" else ""}${if (cleanedTitle.isNotEmpty() && cleanedTitle != "null") ": $cleanedTitle" else ""}") } for (i in episodeTitleArr.indices) { episodeTitleArr[i] = AnimeNameAdapter.removeEpisodeNumber(episodeTitleArr[i]) } //Episode Change fun change(index: Int) { if (isInitialized) { changingServer = false saveData( "${media.id}_${episodeArr[currentEpisodeIndex]}", exoPlayer.currentPosition, this ) 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) change(position) } override fun onNothingSelected(parent: AdapterView<*>) {} } //Next Episode exoNext = playerView.findViewById(R.id.exo_next_ep) exoNext.setOnClickListener { if (isInitialized) { nextEpisode { i -> updateAniProgress() change(currentEpisodeIndex + i) } } } //Prev Episode exoPrev = playerView.findViewById(R.id.exo_prev_ep) exoPrev.setOnClickListener { if (currentEpisodeIndex > 0) { 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 = loadData("${media.id}_${ep.number}", this) ?: 0 initPlayer() preloading = false val context = this val incognito = baseContext.getSharedPreferences("Dantotsu", MODE_PRIVATE) .getBoolean("incognito", false) if (isOnline(context) && 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 = loadData("${media.id}_fullscreenInt", this) ?: 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" } ) saveData("${media.id}_fullscreenInt", isFullscreen, this) } //Cast if (settings.cast) { playerView.findViewById(R.id.exo_cast).apply { visibility = View.VISIBLE try { CastButtonFactory.setUpMediaRouteButton(context, this) dialogFactory = CustomCastThemeFactory() } catch (e: Exception) { isCastApiAvailable = false } setOnLongClickListener { cast() true } } } //Settings exoSettings.setOnClickListener { saveData( "${media.id}_${media.anime!!.selectedEpisode}", exoPlayer.currentPosition, this ) val intent = Intent(this, PlayerSettingsActivity::class.java).apply { putExtra("subtitle", subtitle) } exoPlayer.pause() startActivity(intent) } //Speed val speeds = if (settings.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 playbackParameters = PlaybackParameters(speeds[curSpeed]) var speed: Float val speedDialog = AlertDialog.Builder(this, R.style.DialogTheme).setTitle(getString(R.string.speed)) exoSpeed.setOnClickListener { val dialog = speedDialog.setSingleChoiceItems(speedsName, curSpeed) { dialog, i -> if (isInitialized) { saveData("${media.id}_speed", i, this) speed = speeds[i] curSpeed = i playbackParameters = PlaybackParameters(speed) exoPlayer.playbackParameters = playbackParameters dialog.dismiss() hideSystemBars() } }.show() dialog.window?.setDimAmount(0.8f) } speedDialog.setOnCancelListener { hideSystemBars() } if (settings.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(R.id.exo_touch_view).setOnTouchListener { _, _ -> touched() false } } isFullscreen = settings.resize playerView.resizeMode = when (settings.resize) { 0 -> AspectRatioFrameLayout.RESIZE_MODE_FIT 1 -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM 2 -> AspectRatioFrameLayout.RESIZE_MODE_FILL else -> AspectRatioFrameLayout.RESIZE_MODE_FIT } preloading = false val incognito = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) ?.getBoolean("incognito", false) ?: false val showProgressDialog = if (settings.askIndividual) loadData("${media.id}_progressDialog") ?: true else false if (showProgressDialog && Anilist.userid != null && if (media.isAdult) settings.updateForH else true) AlertDialog.Builder(this, R.style.MyPopup) .setTitle(getString(R.string.auto_update, media.userPreferredName)) .apply { if (incognito) { setMessage(getString(R.string.incognito_will_not_update)) } setOnCancelListener { hideSystemBars() } setCancelable(false) setPositiveButton(getString(R.string.yes)) { dialog, _ -> saveData("${media.id}_progressDialog", false) saveData("${media.id}_save_progress", true) dialog.dismiss() model.setEpisode(episodes[media.anime!!.selectedEpisode!!]!!, "invoke") } setNegativeButton(getString(R.string.no)) { dialog, _ -> saveData("${media.id}_progressDialog", false) saveData("${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 (settings.timeStampsEnabled) updateTimeStamp() } private fun initPlayer() { checkNotch() saveData("${media.id}_current_ep", media.anime!!.selectedEpisode!!, this) val set = loadData>("continue_ANIME", this) ?: mutableSetOf() if (set.contains(media.id)) set.remove(media.id) set.add(media.id) saveData("continue_ANIME", set, this) 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? = loadData("subLang_${media.id}", this)) { 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.downloadSubtitles(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 { sub = MediaItem.SubtitleConfiguration .Builder(Uri.parse(subtitle!!.file.url)) .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 but = playerView.findViewById(R.id.exo_download) if (video?.format == VideoType.CONTAINER || (loadData("settings_download_manager") ?: 0) != 0 ) { //but.visibility = View.VISIBLE TODO: not sure if this is needed but.setOnClickListener { download(this, episode, animeTitle.text.toString()) } } else { but.visibility = View.GONE } 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 } cacheFactory = CacheDataSource.Factory().apply { setCache(Helper.getSimpleCache(this@ExoplayerView)) 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 = getSharedPreferences(getString(R.string.anime_downloads), MODE_PRIVATE) .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("url: ${video!!.file.url}") logger("mimeType: $mimeType") if (sub != null) { val listofnotnullsubs = immutableListOf(sub).filterNotNull() builder.setSubtitleConfigurations(listofnotnullsubs) } builder.build() } else { downloadedMediaItem } //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) .setMinVideoSize( loadData("maxWidth", this) ?: 720, loadData("maxHeight", this) ?: 480 ) .setMaxVideoSize(1, 1) //.setOverrideForType( // TrackSelectionOverride(trackSelector, 2)) ) if (playbackPosition != 0L && !changingServer && !settings.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.DialogTheme) .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 hideSystemBars() exoPlayer = ExoPlayer.Builder(this) .setMediaSourceFactory(DefaultMediaSourceFactory(cacheFactory)) .setTrackSelector(trackSelector) .build().apply { playWhenReady = true this.playbackParameters = this@ExoplayerView.playbackParameters setMediaItem(mediaItem) prepare() loadData("${media.id}_${media.anime!!.selectedEpisode}_max")?.apply { if (this <= playbackPosition) playbackPosition = max(0, this - 5) } seekTo(playbackPosition) } playerView.player = exoPlayer try { mediaSession = MediaSession.Builder(this, exoPlayer).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 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 saveData("${media.id}_${media.anime!!.selectedEpisode}", exoPlayer.currentPosition, this) model.saveSelected(media.id, media.selected!!, this) model.onEpisodeClick( media, episode.number, this.supportFragmentManager, launch = false ) } private fun subClick() { saveData("${media.id}_${media.anime!!.selectedEpisode}", exoPlayer.currentPosition, this) model.saveSelected(media.id, media.selected!!, this) SubtitleDialogFragment().show(supportFragmentManager, "dialog") } override fun onPause() { super.onPause() orientationListener?.disable() if (isInitialized) { if (castPlayer?.isPlaying == false) { playerView.player?.pause() } saveData( "${media.id}_${media.anime!!.selectedEpisode}", exoPlayer.currentPosition, this ) } } override fun onResume() { super.onResume() LangSet.setLocale(this) 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 (settings.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() saveData("${media.id}_${media.anime!!.selectedEpisode}_max", exoPlayer.duration, this) val height = (exoPlayer.videoFormat ?: return).height val width = (exoPlayer.videoFormat ?: return).width if (video?.format != VideoType.CONTAINER) { saveData("maxHeight", height) saveData("maxWidth", width) } aspectRatio = Rational(width, height) videoName.text = episode.selectedExtractor videoInfo.text = "$width x $height" 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 && settings.timeStampsEnabled) { val dur = exoPlayer.duration lifecycleScope.launch(Dispatchers.IO) { model.loadTimeStamps( media.idMAL, media.anime?.selectedEpisode?.trim()?.toIntOrNull(), dur / 1000, settings.useProxyForTimeStamps ) } } } //Link Preloading private var preloading = false private fun updateProgress() { if (isInitialized) { if (exoPlayer.currentPosition.toFloat() / exoPlayer.duration > settings.watchPercentage) { preloading = true nextEpisode(false) { i -> //TODO: make sure this works for offline episodes 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 = 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) { if (settings.showTimeStampButton) { skipTimeButton.visibility = View.VISIBLE exoSkip.visibility = View.GONE skipTimeText.text = new.skipType.getType() skipTimeButton.setOnClickListener { exoPlayer.seekTo((new.interval.endTime * 1000).toLong()) } } if (settings.autoSkipOPED && (new.skipType == "op" || new.skipType == "ed") && !skippedTimeStamps.contains( new ) ) { exoPlayer.seekTo((new.interval.endTime * 1000).toLong()) skippedTimeStamps.add(new) } new.skipType.getType() } else { skipTimeButton.visibility = View.GONE if (settings.skipTime > 0) exoSkip.visibility = View.VISIBLE "" } } 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}") if (tracks.groups.size <= 2) exoQuality.visibility = View.GONE else { exoQuality.visibility = View.VISIBLE exoQuality.setOnClickListener { initPopupQuality().show() } } } 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}") FirebaseCrashlytics.getInstance().recordException(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 && settings.autoPlay) { if (interacted) exoNext.performClick() else toast(getString(R.string.autoplay_cancelled)) } super.onPlaybackStateChanged(playbackState) } private fun updateAniProgress() { if (exoPlayer.currentPosition / episodeLength > settings.watchPercentage && Anilist.userid != null) if (loadData("${media.id}_save_progress") != false && if (media.isAdult) settings.updateForH else true) { 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 (settings.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() releasePlayer() } super.onDestroy() finishAndRemoveTask() } // QUALITY SELECTOR private fun initPopupQuality(): Dialog { val trackSelectionDialogBuilder = TrackSelectionDialogBuilder(this, "Available Qualities", exoPlayer, TRACK_TYPE_VIDEO) trackSelectionDialogBuilder.setTheme(R.style.DialogTheme) trackSelectionDialogBuilder.setTrackNameProvider { if (it.frameRate > 0f) it.height.toString() + "p" else it.height.toString() + "p (fps : N/A)" } val trackDialog = trackSelectionDialogBuilder.build() trackDialog.setOnDismissListener { hideSystemBars() } return trackDialog } // 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) { saveData("${media.id}_${episode.number}", exoPlayer.currentPosition, this) 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 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 } 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) { 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 } } }