forked from recloudstream/cloudstream
videoplayer stuff
This commit is contained in:
parent
c328a315b9
commit
35040e793a
11 changed files with 613 additions and 188 deletions
|
@ -151,6 +151,10 @@ interface LoadResponse {
|
||||||
val plot: String?
|
val plot: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun LoadResponse.isEpisodeBased(): Boolean {
|
||||||
|
return this is AnimeLoadResponse || this is TvSeriesLoadResponse
|
||||||
|
}
|
||||||
|
|
||||||
data class AnimeLoadResponse(
|
data class AnimeLoadResponse(
|
||||||
val engName: String?,
|
val engName: String?,
|
||||||
val japName: String?,
|
val japName: String?,
|
||||||
|
|
|
@ -2,14 +2,20 @@ package com.lagradost.cloudstream3
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
|
import android.media.AudioAttributes
|
||||||
|
import android.media.AudioFocusRequest
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.os.Build
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||||
|
import com.lagradost.cloudstream3.utils.Event
|
||||||
|
|
||||||
object UIHelper {
|
object UIHelper {
|
||||||
val Int.toPx: Int get() = (this * Resources.getSystem().displayMetrics.density).toInt()
|
val Int.toPx: Int get() = (this * Resources.getSystem().displayMetrics.density).toInt()
|
||||||
|
@ -70,4 +76,58 @@ object UIHelper {
|
||||||
fun Activity.getGridIsCompact(): Boolean {
|
fun Activity.getGridIsCompact(): Boolean {
|
||||||
return getGridFormat() != "grid"
|
return getGridFormat() != "grid"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Activity.requestLocalAudioFocus(focusRequest: AudioFocusRequest?) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && focusRequest != null) {
|
||||||
|
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
audioManager.requestAudioFocus(focusRequest)
|
||||||
|
} else {
|
||||||
|
val audioManager: AudioManager =
|
||||||
|
getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
audioManager.requestAudioFocus(
|
||||||
|
null,
|
||||||
|
AudioManager.STREAM_MUSIC,
|
||||||
|
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _AudioFocusRequest: AudioFocusRequest? = null
|
||||||
|
private var _OnAudioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null
|
||||||
|
var onAudioFocusEvent = Event<Boolean>()
|
||||||
|
|
||||||
|
fun getAudioListener(): AudioManager.OnAudioFocusChangeListener? {
|
||||||
|
if(_OnAudioFocusChangeListener != null) return _OnAudioFocusChangeListener
|
||||||
|
_OnAudioFocusChangeListener = AudioManager.OnAudioFocusChangeListener {
|
||||||
|
onAudioFocusEvent.invoke(
|
||||||
|
when (it) {
|
||||||
|
AudioManager.AUDIOFOCUS_GAIN -> true
|
||||||
|
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE -> true
|
||||||
|
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return _OnAudioFocusChangeListener
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFocusRequest(): AudioFocusRequest? {
|
||||||
|
if (_AudioFocusRequest != null) return _AudioFocusRequest
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
|
||||||
|
setAudioAttributes(AudioAttributes.Builder().run {
|
||||||
|
setUsage(AudioAttributes.USAGE_MEDIA)
|
||||||
|
setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
|
||||||
|
build()
|
||||||
|
})
|
||||||
|
setAcceptsDelayedFocusGain(true)
|
||||||
|
getAudioListener()?.let {
|
||||||
|
setOnAudioFocusChangeListener(it)
|
||||||
|
}
|
||||||
|
build()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,5 @@
|
||||||
package com.lagradost.cloudstream3.ui.player
|
package com.lagradost.cloudstream3.ui.player
|
||||||
|
|
||||||
import android.animation.Animator
|
|
||||||
import android.animation.AnimatorListenerAdapter
|
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
@ -13,10 +11,7 @@ import android.database.ContentObserver
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.*
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.os.SystemClock
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -26,6 +21,7 @@ import android.view.animation.*
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import android.widget.Toast.LENGTH_LONG
|
import android.widget.Toast.LENGTH_LONG
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
@ -44,20 +40,24 @@ import com.google.android.exoplayer2.upstream.DataSource
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
|
||||||
import com.google.android.exoplayer2.util.MimeTypes
|
import com.google.android.exoplayer2.util.MimeTypes
|
||||||
import com.lagradost.cloudstream3.LoadResponse
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
|
import com.lagradost.cloudstream3.UIHelper.getFocusRequest
|
||||||
|
import com.lagradost.cloudstream3.UIHelper.requestLocalAudioFocus
|
||||||
import com.lagradost.cloudstream3.UIHelper.toPx
|
import com.lagradost.cloudstream3.UIHelper.toPx
|
||||||
import com.lagradost.cloudstream3.USER_AGENT
|
|
||||||
import com.lagradost.cloudstream3.mvvm.Resource
|
import com.lagradost.cloudstream3.mvvm.Resource
|
||||||
import com.lagradost.cloudstream3.mvvm.observe
|
import com.lagradost.cloudstream3.mvvm.observeDirectly
|
||||||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||||
import com.lagradost.cloudstream3.ui.result.ResultViewModel
|
import com.lagradost.cloudstream3.ui.result.ResultViewModel
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.getId
|
||||||
import kotlinx.android.synthetic.main.fragment_player.*
|
import kotlinx.android.synthetic.main.fragment_player.*
|
||||||
import kotlinx.android.synthetic.main.player_custom_layout.*
|
import kotlinx.android.synthetic.main.player_custom_layout.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.*
|
||||||
import javax.net.ssl.HttpsURLConnection
|
import javax.net.ssl.HttpsURLConnection
|
||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
import javax.net.ssl.SSLSession
|
import javax.net.ssl.SSLSession
|
||||||
|
@ -77,6 +77,9 @@ const val PLAYBACK_SPEED = "playback_speed"
|
||||||
const val RESIZE_MODE_KEY = "resize_mode" // Last used resize mode
|
const val RESIZE_MODE_KEY = "resize_mode" // Last used resize mode
|
||||||
const val PLAYBACK_SPEED_KEY = "playback_speed" // Last used playback speed
|
const val PLAYBACK_SPEED_KEY = "playback_speed" // Last used playback speed
|
||||||
|
|
||||||
|
const val OPENING_PROCENTAGE = 50
|
||||||
|
const val AUTOLOAD_NEXT_EPISODE_PROCENTAGE = 80
|
||||||
|
|
||||||
enum class PlayerEventType(val value: Int) {
|
enum class PlayerEventType(val value: Int) {
|
||||||
Stop(-1),
|
Stop(-1),
|
||||||
Pause(0),
|
Pause(0),
|
||||||
|
@ -100,6 +103,8 @@ data class PlayerData(
|
||||||
)*/
|
)*/
|
||||||
data class PlayerData(
|
data class PlayerData(
|
||||||
val episodeIndex: Int,
|
val episodeIndex: Int,
|
||||||
|
val seasonIndex: Int?,
|
||||||
|
val mirrorId: Int,
|
||||||
)
|
)
|
||||||
|
|
||||||
class PlayerFragment : Fragment() {
|
class PlayerFragment : Fragment() {
|
||||||
|
@ -115,6 +120,12 @@ class PlayerFragment : Fragment() {
|
||||||
private var isShowing = true
|
private var isShowing = true
|
||||||
private lateinit var exoPlayer: SimpleExoPlayer
|
private lateinit var exoPlayer: SimpleExoPlayer
|
||||||
|
|
||||||
|
//private var currentPercentage = 0
|
||||||
|
// private var hasNextEpisode = true
|
||||||
|
|
||||||
|
val formatBuilder = StringBuilder()
|
||||||
|
val formatter = Formatter(formatBuilder, Locale.getDefault())
|
||||||
|
|
||||||
private var width = Resources.getSystem().displayMetrics.heightPixels
|
private var width = Resources.getSystem().displayMetrics.heightPixels
|
||||||
private var height = Resources.getSystem().displayMetrics.widthPixels
|
private var height = Resources.getSystem().displayMetrics.widthPixels
|
||||||
|
|
||||||
|
@ -200,13 +211,18 @@ class PlayerFragment : Fragment() {
|
||||||
start()
|
start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ObjectAnimator.ofFloat(video_title_rez, "translationY", titleMove).apply {
|
||||||
|
duration = 200
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
val playerBarMove = if (isShowing) 0f else 50.toPx.toFloat()
|
val playerBarMove = if (isShowing) 0f else 50.toPx.toFloat()
|
||||||
ObjectAnimator.ofFloat(bottom_player_bar, "translationY", playerBarMove).apply {
|
ObjectAnimator.ofFloat(bottom_player_bar, "translationY", playerBarMove).apply {
|
||||||
duration = 200
|
duration = 200
|
||||||
start()
|
start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changeSkip()
|
||||||
val fadeTo = if (isShowing) 1f else 0f
|
val fadeTo = if (isShowing) 1f else 0f
|
||||||
val fadeAnimation = AlphaAnimation(1f - fadeTo, fadeTo)
|
val fadeAnimation = AlphaAnimation(1f - fadeTo, fadeTo)
|
||||||
|
|
||||||
|
@ -217,6 +233,7 @@ class PlayerFragment : Fragment() {
|
||||||
shadow_overlay?.startAnimation(fadeAnimation)
|
shadow_overlay?.startAnimation(fadeAnimation)
|
||||||
}
|
}
|
||||||
video_holder?.startAnimation(fadeAnimation)
|
video_holder?.startAnimation(fadeAnimation)
|
||||||
|
|
||||||
//video_lock_holder?.startAnimation(fadeAnimation)
|
//video_lock_holder?.startAnimation(fadeAnimation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,6 +269,9 @@ class PlayerFragment : Fragment() {
|
||||||
private var hasPassedSkipLimit = false
|
private var hasPassedSkipLimit = false
|
||||||
private val swipeEnabled = true //<settingsManager!!.getBoolean("swipe_enabled", true)
|
private val swipeEnabled = true //<settingsManager!!.getBoolean("swipe_enabled", true)
|
||||||
private val swipeVerticalEnabled = true//settingsManager.getBoolean("swipe_vertical_enabled", true)
|
private val swipeVerticalEnabled = true//settingsManager.getBoolean("swipe_vertical_enabled", true)
|
||||||
|
private val playBackSpeedEnabled = true//settingsManager!!.getBoolean("playback_speed_enabled", false)
|
||||||
|
private val playerResizeEnabled = true//settingsManager!!.getBoolean("player_resize_enabled", false)
|
||||||
|
|
||||||
private var isMovingStartTime = 0L
|
private var isMovingStartTime = 0L
|
||||||
private var currentX = 0F
|
private var currentX = 0F
|
||||||
private var currentY = 0F
|
private var currentY = 0F
|
||||||
|
@ -290,7 +310,7 @@ class PlayerFragment : Fragment() {
|
||||||
val currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
val currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||||
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
|
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
|
||||||
|
|
||||||
if (progressBarLeftHolder.alpha <= 0f) {
|
if (progressBarLeftHolder?.alpha ?: 0f <= 0f) {
|
||||||
cachedVolume = currentVolume.toFloat() / maxVolume.toFloat()
|
cachedVolume = currentVolume.toFloat() / maxVolume.toFloat()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -324,18 +344,9 @@ class PlayerFragment : Fragment() {
|
||||||
val alpha = minOf(0.95f,
|
val alpha = minOf(0.95f,
|
||||||
brightness_overlay.alpha + diffY.toFloat() * 0.5f) // 0.05f *if (diffY > 0) 1 else -1
|
brightness_overlay.alpha + diffY.toFloat() * 0.5f) // 0.05f *if (diffY > 0) 1 else -1
|
||||||
brightness_overlay?.alpha = alpha
|
brightness_overlay?.alpha = alpha
|
||||||
//progressBarRight?.progress = ((1f - alpha) * 100).toInt()
|
|
||||||
|
|
||||||
progressBarRight?.max = 100 * 100
|
progressBarRight?.max = 100 * 100
|
||||||
progressBarRight?.progress = ((1f - alpha) * 100 * 100).toInt()
|
progressBarRight?.progress = ((1f - alpha) * 100 * 100).toInt()
|
||||||
/* val animation: ObjectAnimator = ObjectAnimator.ofInt(progressBarRight,
|
|
||||||
"progress",
|
|
||||||
progressBarRight.progress,
|
|
||||||
.toInt())
|
|
||||||
animation.duration = 100
|
|
||||||
animation.setAutoCancel(true)
|
|
||||||
animation.interpolator = DecelerateInterpolator()
|
|
||||||
animation.start()*/
|
|
||||||
|
|
||||||
currentY = motionEvent.rawY
|
currentY = motionEvent.rawY
|
||||||
}
|
}
|
||||||
|
@ -377,8 +388,11 @@ class PlayerFragment : Fragment() {
|
||||||
TransitionManager.beginDelayedTransition(player_holder, transition)
|
TransitionManager.beginDelayedTransition(player_holder, transition)
|
||||||
|
|
||||||
if (abs(skipTime) > 7000 && !preventHorizontalSwipe && swipeEnabled) {
|
if (abs(skipTime) > 7000 && !preventHorizontalSwipe && swipeEnabled) {
|
||||||
exoPlayer.seekTo(maxOf(minOf(skipTime + isMovingStartTime, exoPlayer.duration), 0))
|
seekTo(skipTime + isMovingStartTime)
|
||||||
|
//exoPlayer.seekTo(maxOf(minOf(skipTime + isMovingStartTime, exoPlayer.duration), 0))
|
||||||
}
|
}
|
||||||
|
changeSkip()
|
||||||
|
|
||||||
hasPassedSkipLimit = false
|
hasPassedSkipLimit = false
|
||||||
hasPassedVerticalSwipeThreshold = false
|
hasPassedVerticalSwipeThreshold = false
|
||||||
preventHorizontalSwipe = false
|
preventHorizontalSwipe = false
|
||||||
|
@ -402,10 +416,44 @@ class PlayerFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun seekTime(time: Long) {
|
fun changeSkip(position: Long? = null) {
|
||||||
exoPlayer.seekTo(maxOf(minOf(exoPlayer.currentPosition + time, exoPlayer.duration), 0))
|
if (exoPlayer.currentPosition >= 0) {
|
||||||
|
val percentage = ((position ?: exoPlayer.currentPosition) * 100 / exoPlayer.contentDuration).toInt()
|
||||||
|
val hasNext = hasNextEpisode()
|
||||||
|
|
||||||
|
if (percentage >= AUTOLOAD_NEXT_EPISODE_PROCENTAGE && hasNext) {
|
||||||
|
val ep =
|
||||||
|
episodes[playerData.episodeIndex + 1]
|
||||||
|
|
||||||
|
if ((allEpisodes[ep.id]?.size ?: 0) <= 0) {
|
||||||
|
viewModel.loadEpisode(ep, false) {
|
||||||
|
//NOTHING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val nextEp = percentage >= OPENING_PROCENTAGE
|
||||||
|
|
||||||
|
skip_op_text.text = if (nextEp) "Next Episode" else "Skip OP"
|
||||||
|
val isVis =
|
||||||
|
if (nextEp) hasNext //&& !isCurrentlySkippingEp
|
||||||
|
else (localData is AnimeLoadResponse)
|
||||||
|
skip_op.visibility = if (isVis) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun seekTime(time: Long) {
|
||||||
|
changeSkip()
|
||||||
|
seekTo(exoPlayer.currentPosition + time)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun seekTo(time: Long) {
|
||||||
|
val correctTime = maxOf(minOf(time, exoPlayer.duration), 0)
|
||||||
|
exoPlayer.seekTo(correctTime)
|
||||||
|
changeSkip(correctTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var hasUsedFirstRender = false
|
||||||
|
|
||||||
private fun releasePlayer() {
|
private fun releasePlayer() {
|
||||||
val alphaAnimation = AlphaAnimation(0f, 1f)
|
val alphaAnimation = AlphaAnimation(0f, 1f)
|
||||||
alphaAnimation.duration = 100
|
alphaAnimation.duration = 100
|
||||||
|
@ -460,6 +508,8 @@ class PlayerFragment : Fragment() {
|
||||||
AspectRatioFrameLayout.RESIZE_MODE_ZOOM,
|
AspectRatioFrameLayout.RESIZE_MODE_ZOOM,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private var localData: LoadResponse? = null
|
||||||
|
|
||||||
private fun updateLock() {
|
private fun updateLock() {
|
||||||
video_locked_img.setImageResource(if (isLocked) R.drawable.video_locked else R.drawable.video_unlocked)
|
video_locked_img.setImageResource(if (isLocked) R.drawable.video_locked else R.drawable.video_unlocked)
|
||||||
val color = if (isLocked) ContextCompat.getColor(requireContext(), R.color.videoColorPrimary)
|
val color = if (isLocked) ContextCompat.getColor(requireContext(), R.color.videoColorPrimary)
|
||||||
|
@ -533,7 +583,7 @@ class PlayerFragment : Fragment() {
|
||||||
playerData = mapper.readValue(it, PlayerData::class.java)
|
playerData = mapper.readValue(it, PlayerData::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
observe(viewModel.episodes) { _episodes ->
|
observeDirectly(viewModel.episodes) { _episodes ->
|
||||||
episodes = _episodes
|
episodes = _episodes
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
if (playerData.episodeIndex > 0 && playerData.episodeIndex < episodes.size) {
|
if (playerData.episodeIndex > 0 && playerData.episodeIndex < episodes.size) {
|
||||||
|
@ -544,16 +594,16 @@ class PlayerFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
observe(viewModel.allEpisodes) { _allEpisodes ->
|
observeDirectly(viewModel.allEpisodes) { _allEpisodes ->
|
||||||
allEpisodes = _allEpisodes
|
allEpisodes = _allEpisodes
|
||||||
}
|
}
|
||||||
|
|
||||||
observe(viewModel.resultResponse) { data ->
|
observeDirectly(viewModel.resultResponse) { data ->
|
||||||
when (data) {
|
when (data) {
|
||||||
is Resource.Success -> {
|
is Resource.Success -> {
|
||||||
val d = data.value
|
val d = data.value
|
||||||
if (d is LoadResponse) {
|
if (d is LoadResponse) {
|
||||||
|
localData = d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Resource.Failure -> {
|
is Resource.Failure -> {
|
||||||
|
@ -574,12 +624,9 @@ class PlayerFragment : Fragment() {
|
||||||
|
|
||||||
val goLeft = AnimationUtils.loadAnimation(context, R.anim.go_left)
|
val goLeft = AnimationUtils.loadAnimation(context, R.anim.go_left)
|
||||||
goLeft.setAnimationListener(object : Animation.AnimationListener {
|
goLeft.setAnimationListener(object : Animation.AnimationListener {
|
||||||
override fun onAnimationStart(animation: Animation?) {
|
override fun onAnimationStart(animation: Animation?) {}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationRepeat(animation: Animation?) {
|
override fun onAnimationRepeat(animation: Animation?) {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationEnd(animation: Animation?) {
|
override fun onAnimationEnd(animation: Animation?) {
|
||||||
exo_rew_text.post { exo_rew_text.text = "$fastForwardTime" }
|
exo_rew_text.post { exo_rew_text.text = "$fastForwardTime" }
|
||||||
|
@ -600,12 +647,9 @@ class PlayerFragment : Fragment() {
|
||||||
|
|
||||||
val goRight = AnimationUtils.loadAnimation(context, R.anim.go_right)
|
val goRight = AnimationUtils.loadAnimation(context, R.anim.go_right)
|
||||||
goRight.setAnimationListener(object : Animation.AnimationListener {
|
goRight.setAnimationListener(object : Animation.AnimationListener {
|
||||||
override fun onAnimationStart(animation: Animation?) {
|
override fun onAnimationStart(animation: Animation?) {}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationRepeat(animation: Animation?) {
|
override fun onAnimationRepeat(animation: Animation?) {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationEnd(animation: Animation?) {
|
override fun onAnimationEnd(animation: Animation?) {
|
||||||
exo_ffwd_text.post { exo_ffwd_text.text = "$fastForwardTime" }
|
exo_ffwd_text.post { exo_ffwd_text.text = "$fastForwardTime" }
|
||||||
|
@ -634,6 +678,10 @@ class PlayerFragment : Fragment() {
|
||||||
centerMenu.startAnimation(fadeAnimation)
|
centerMenu.startAnimation(fadeAnimation)
|
||||||
//video_bar.startAnimation(fadeAnimation)
|
//video_bar.startAnimation(fadeAnimation)
|
||||||
|
|
||||||
|
//TITLE
|
||||||
|
video_title_rez.startAnimation(fadeAnimation)
|
||||||
|
video_title.startAnimation(fadeAnimation)
|
||||||
|
|
||||||
// BOTTOM
|
// BOTTOM
|
||||||
resize_player.startAnimation(fadeAnimation)
|
resize_player.startAnimation(fadeAnimation)
|
||||||
playback_speed_btt.startAnimation(fadeAnimation)
|
playback_speed_btt.startAnimation(fadeAnimation)
|
||||||
|
@ -676,14 +724,136 @@ class PlayerFragment : Fragment() {
|
||||||
click_overlay?.setOnTouchListener(
|
click_overlay?.setOnTouchListener(
|
||||||
Listener()
|
Listener()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
playback_speed_btt.visibility = if (playBackSpeedEnabled) VISIBLE else GONE
|
||||||
|
playback_speed_btt.setOnClickListener {
|
||||||
|
lateinit var dialog: AlertDialog
|
||||||
|
// Lmao kind bad
|
||||||
|
val speedsText = arrayOf("0.5x", "0.75x", "1x", "1.25x", "1.5x", "1.75x", "2x")
|
||||||
|
val speedsNumbers = arrayOf(0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f)
|
||||||
|
val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom)
|
||||||
|
builder.setTitle("Pick playback speed")
|
||||||
|
|
||||||
|
builder.setSingleChoiceItems(speedsText, speedsNumbers.indexOf(playbackSpeed)) { _, which ->
|
||||||
|
|
||||||
|
//val speed = speedsText[which]
|
||||||
|
//Toast.makeText(requireContext(), "$speed selected.", Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
|
playbackSpeed = speedsNumbers[which]
|
||||||
|
requireContext().setKey(PLAYBACK_SPEED_KEY, playbackSpeed)
|
||||||
|
val param = PlaybackParameters(playbackSpeed)
|
||||||
|
exoPlayer.playbackParameters = param
|
||||||
|
player_speed_text.text = "Speed (${playbackSpeed}x)".replace(".0x", "x")
|
||||||
|
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
dialog = builder.create()
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
sources_btt.setOnClickListener {
|
||||||
|
lateinit var dialog: AlertDialog
|
||||||
|
getUrls()?.let { it1 ->
|
||||||
|
sortUrls(it1).let { sources ->
|
||||||
|
val sourcesText = sources.map { it.name }
|
||||||
|
val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom)
|
||||||
|
builder.setTitle("Pick source")
|
||||||
|
builder.setSingleChoiceItems(sourcesText.toTypedArray(),
|
||||||
|
sources.indexOf(getCurrentUrl())) { _, which ->
|
||||||
|
//val speed = speedsText[which]
|
||||||
|
//Toast.makeText(requireContext(), "$speed selected.", Toast.LENGTH_SHORT).show()
|
||||||
|
setMirrorId(sources[which].getId())
|
||||||
|
initPlayer(getCurrentUrl())
|
||||||
|
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
dialog = builder.create()
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
player_view.resizeMode = resizeModes[resizeMode]
|
||||||
|
if (playerResizeEnabled) {
|
||||||
|
resize_player.visibility = VISIBLE
|
||||||
|
resize_player.setOnClickListener {
|
||||||
|
resizeMode = (resizeMode + 1) % resizeModes.size
|
||||||
|
|
||||||
|
requireContext().setKey(RESIZE_MODE_KEY, resizeMode)
|
||||||
|
player_view.resizeMode = resizeModes[resizeMode]
|
||||||
|
//exoPlayer.videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resize_player.visibility = GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
skip_op.setOnClickListener {
|
||||||
|
if (exoPlayer.currentPosition * 100 / exoPlayer.duration >= OPENING_PROCENTAGE) {
|
||||||
|
if (hasNextEpisode()) {
|
||||||
|
// skip_op.visibility = View.GONE
|
||||||
|
skipToNextEpisode()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
skipOP()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrentUrl(): ExtractorLink {
|
private fun getCurrentUrl(): ExtractorLink? {
|
||||||
return ExtractorLink("",
|
val urls = getUrls() ?: return null
|
||||||
"TEST",
|
for (i in urls) {
|
||||||
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
|
if (i.getId() == playerData.mirrorId) {
|
||||||
"",
|
return i
|
||||||
0)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
/*ExtractorLink("",
|
||||||
|
"TEST",
|
||||||
|
"https://v6.4animu.me/Overlord/Overlord-Episode-01-1080p.mp4",
|
||||||
|
//"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
|
||||||
|
"",
|
||||||
|
0)*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUrls(): List<ExtractorLink>? {
|
||||||
|
return try {
|
||||||
|
allEpisodes[getEpisode()?.id]
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sortUrls(urls: List<ExtractorLink>): List<ExtractorLink> {
|
||||||
|
return urls.sortedBy { t -> -t.quality }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getEpisode(): ResultEpisode? {
|
||||||
|
return try {
|
||||||
|
episodes[playerData.episodeIndex]
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasNextEpisode(): Boolean {
|
||||||
|
return episodes.size > playerData.episodeIndex + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isCurrentlySkippingEp = false
|
||||||
|
|
||||||
|
private fun skipToNextEpisode() {
|
||||||
|
if (isCurrentlySkippingEp) return
|
||||||
|
isCurrentlySkippingEp = true
|
||||||
|
val copy = playerData.copy(episodeIndex = playerData.episodeIndex + 1)
|
||||||
|
playerData = copy
|
||||||
|
initPlayer()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setMirrorId(id: Int) {
|
||||||
|
val copy = playerData.copy(mirrorId = id)
|
||||||
|
playerData = copy
|
||||||
|
initPlayer()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
@ -730,153 +900,284 @@ class PlayerFragment : Fragment() {
|
||||||
outState.putBoolean(STATE_PLAYER_PLAYING, isPlayerPlaying)
|
outState.putBoolean(STATE_PLAYER_PLAYING, isPlayerPlaying)
|
||||||
outState.putInt(RESIZE_MODE_KEY, resizeMode)
|
outState.putInt(RESIZE_MODE_KEY, resizeMode)
|
||||||
outState.putFloat(PLAYBACK_SPEED, playbackSpeed)
|
outState.putFloat(PLAYBACK_SPEED, playbackSpeed)
|
||||||
|
outState.putString("data", mapper.writeValueAsString(playerData))
|
||||||
savePos()
|
savePos()
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var currentWindow = 0
|
private var currentWindow = 0
|
||||||
private var playbackPosition: Long = 0
|
private var playbackPosition: Long = 0
|
||||||
|
/*
|
||||||
|
private fun updateProgressBar() {
|
||||||
|
val duration: Long =exoPlayer.getDuration()
|
||||||
|
val position: Long =exoPlayer.getCurrentPosition()
|
||||||
|
|
||||||
|
handler.removeCallbacks(updateProgressAction)
|
||||||
|
val playbackState = exoPlayer.getPlaybackState()
|
||||||
|
if (playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED) {
|
||||||
|
var delayMs: Long
|
||||||
|
if (player.getPlayWhenReady() && playbackState == Player.STATE_READY) {
|
||||||
|
delayMs = 1000 - position % 1000
|
||||||
|
if (delayMs < 200) {
|
||||||
|
delayMs += 1000
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delayMs = 1000
|
||||||
|
}
|
||||||
|
handler.postDelayed(updateProgressAction, delayMs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val updateProgressAction = Runnable { updateProgressBar() }*/
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
fun initPlayer(currentUrl: ExtractorLink?) {
|
||||||
|
if (currentUrl == null) return
|
||||||
|
hasUsedFirstRender = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this::exoPlayer.isInitialized) {
|
||||||
|
savePos()
|
||||||
|
exoPlayer.release()
|
||||||
|
}
|
||||||
|
val isOnline =
|
||||||
|
currentUrl.url.startsWith("https://") || currentUrl.url.startsWith("http://")
|
||||||
|
|
||||||
|
if (settingsManager.getBoolean("ignore_ssl", true)) {
|
||||||
|
// Disables ssl check
|
||||||
|
val sslContext: SSLContext = SSLContext.getInstance("TLS")
|
||||||
|
sslContext.init(null, arrayOf(SSLTrustManager()), java.security.SecureRandom())
|
||||||
|
sslContext.createSSLEngine()
|
||||||
|
HttpsURLConnection.setDefaultHostnameVerifier { _: String, _: SSLSession ->
|
||||||
|
true
|
||||||
|
}
|
||||||
|
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomFactory : DataSource.Factory {
|
||||||
|
override fun createDataSource(): DataSource {
|
||||||
|
return if (isOnline) {
|
||||||
|
val dataSource = DefaultHttpDataSourceFactory(USER_AGENT).createDataSource()
|
||||||
|
/*FastAniApi.currentHeaders?.forEach {
|
||||||
|
dataSource.setRequestProperty(it.key, it.value)
|
||||||
|
}*/
|
||||||
|
dataSource.setRequestProperty("Referer", currentUrl.referer)
|
||||||
|
dataSource
|
||||||
|
} else {
|
||||||
|
DefaultDataSourceFactory(requireContext(), USER_AGENT).createDataSource()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val mimeType = if (currentUrl.isM3u8) MimeTypes.APPLICATION_M3U8 else MimeTypes.APPLICATION_MP4
|
||||||
|
val _mediaItem = MediaItem.Builder()
|
||||||
|
//Replace needed for android 6.0.0 https://github.com/google/ExoPlayer/issues/5983
|
||||||
|
.setMimeType(mimeType)
|
||||||
|
|
||||||
|
if (isOnline) {
|
||||||
|
_mediaItem.setUri(currentUrl.url)
|
||||||
|
} else {
|
||||||
|
_mediaItem.setUri(Uri.fromFile(File(currentUrl.url)))
|
||||||
|
}
|
||||||
|
|
||||||
|
val mediaItem = _mediaItem.build()
|
||||||
|
val trackSelector = DefaultTrackSelector(requireContext())
|
||||||
|
// Disable subtitles
|
||||||
|
trackSelector.parameters = DefaultTrackSelector.ParametersBuilder(requireContext())
|
||||||
|
.setRendererDisabled(C.TRACK_TYPE_VIDEO, true)
|
||||||
|
.setRendererDisabled(C.TRACK_TYPE_TEXT, true)
|
||||||
|
.setDisabledTextTrackSelectionFlags(C.TRACK_TYPE_TEXT)
|
||||||
|
.clearSelectionOverrides()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val _exoPlayer =
|
||||||
|
SimpleExoPlayer.Builder(this.requireContext())
|
||||||
|
.setTrackSelector(trackSelector)
|
||||||
|
|
||||||
|
_exoPlayer.setMediaSourceFactory(DefaultMediaSourceFactory(CustomFactory()))
|
||||||
|
exoPlayer = _exoPlayer.build().apply {
|
||||||
|
playWhenReady = isPlayerPlaying
|
||||||
|
seekTo(currentWindow, playbackPosition)
|
||||||
|
setMediaItem(mediaItem, false)
|
||||||
|
prepare()
|
||||||
|
}
|
||||||
|
|
||||||
|
val alphaAnimation = AlphaAnimation(1f, 0f)
|
||||||
|
alphaAnimation.duration = 300
|
||||||
|
alphaAnimation.fillAfter = true
|
||||||
|
loading_overlay.startAnimation(alphaAnimation)
|
||||||
|
video_go_back_holder.visibility = GONE
|
||||||
|
|
||||||
|
exoPlayer.setHandleAudioBecomingNoisy(true) // WHEN HEADPHONES ARE PLUGGED OUT https://github.com/google/ExoPlayer/issues/7288
|
||||||
|
player_view.player = exoPlayer
|
||||||
|
// Sets the speed
|
||||||
|
exoPlayer.playbackParameters = PlaybackParameters(playbackSpeed)
|
||||||
|
player_speed_text?.text = "Speed (${playbackSpeed}x)".replace(".0x", "x")
|
||||||
|
|
||||||
|
if (localData != null) {
|
||||||
|
val data = localData!!
|
||||||
|
val localEpisode = getEpisode()
|
||||||
|
if (localEpisode != null) {
|
||||||
|
val episode = localEpisode.episode
|
||||||
|
val season: Int? = localEpisode.season
|
||||||
|
val isEpisodeBased = data.isEpisodeBased()
|
||||||
|
video_title?.text = data.name +
|
||||||
|
if (isEpisodeBased)
|
||||||
|
if (season == null)
|
||||||
|
" - Episode $episode"
|
||||||
|
else
|
||||||
|
" \"S${season}:E${episode}\""
|
||||||
|
else ""
|
||||||
|
video_title_rez?.text = currentUrl.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
exo_remaining.text = Util.getStringForTime(formatBuilder,
|
||||||
|
formatter,
|
||||||
|
exoPlayer.contentDuration - exoPlayer.currentPosition)
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
//https://stackoverflow.com/questions/47731779/detect-pause-resume-in-exoplayer
|
||||||
|
exoPlayer.addListener(object : Player.Listener {
|
||||||
|
override fun onRenderedFirstFrame() {
|
||||||
|
super.onRenderedFirstFrame()
|
||||||
|
isCurrentlySkippingEp = false
|
||||||
|
|
||||||
|
val height = exoPlayer.videoFormat?.height
|
||||||
|
val width = exoPlayer.videoFormat?.width
|
||||||
|
video_title_rez?.text =
|
||||||
|
if (height == null || width == null) currentUrl.name else "${currentUrl.name} - ${width}x${height}"
|
||||||
|
|
||||||
|
if(!hasUsedFirstRender) { // DON'T WANT TO SET MULTIPLE MESSAGES
|
||||||
|
println("FIRST RENDER")
|
||||||
|
changeSkip()
|
||||||
|
exoPlayer
|
||||||
|
.createMessage { messageType, payload ->
|
||||||
|
changeSkip()
|
||||||
|
}
|
||||||
|
.setLooper(Looper.getMainLooper())
|
||||||
|
.setPosition( /* positionMs= */exoPlayer.contentDuration * OPENING_PROCENTAGE / 100)
|
||||||
|
// .setPayload(customPayloadData)
|
||||||
|
.setDeleteAfterDelivery(false)
|
||||||
|
.send()
|
||||||
|
exoPlayer
|
||||||
|
.createMessage { messageType, payload ->
|
||||||
|
changeSkip()
|
||||||
|
}
|
||||||
|
.setLooper(Looper.getMainLooper())
|
||||||
|
.setPosition( /* positionMs= */exoPlayer.contentDuration * AUTOLOAD_NEXT_EPISODE_PROCENTAGE / 100)
|
||||||
|
// .setPayload(customPayloadData)
|
||||||
|
.setDeleteAfterDelivery(false)
|
||||||
|
|
||||||
|
.send()
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
changeSkip()
|
||||||
|
}
|
||||||
|
hasUsedFirstRender = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
|
||||||
|
// updatePIPModeActions()
|
||||||
|
if (playWhenReady) {
|
||||||
|
when (playbackState) {
|
||||||
|
Player.STATE_READY -> {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
requireActivity().requestLocalAudioFocus(getFocusRequest())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Player.STATE_ENDED -> {
|
||||||
|
if (hasNextEpisode()) {
|
||||||
|
skipToNextEpisode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Player.STATE_BUFFERING -> {
|
||||||
|
changeSkip()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlayerError(error: ExoPlaybackException) {
|
||||||
|
// Lets pray this doesn't spam Toasts :)
|
||||||
|
when (error.type) {
|
||||||
|
ExoPlaybackException.TYPE_SOURCE -> {
|
||||||
|
if (currentUrl.url != "") {
|
||||||
|
Toast.makeText(
|
||||||
|
activity,
|
||||||
|
"Source error\n" + error.sourceException.message,
|
||||||
|
LENGTH_LONG
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ExoPlaybackException.TYPE_REMOTE -> {
|
||||||
|
Toast.makeText(activity, "Remote error", LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
ExoPlaybackException.TYPE_RENDERER -> {
|
||||||
|
Toast.makeText(
|
||||||
|
activity,
|
||||||
|
"Renderer error\n" + error.rendererException.message,
|
||||||
|
LENGTH_LONG
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
ExoPlaybackException.TYPE_UNEXPECTED -> {
|
||||||
|
Toast.makeText(
|
||||||
|
activity,
|
||||||
|
"Unexpected player error\n" + error.unexpectedException.message,
|
||||||
|
LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e: java.lang.IllegalStateException) {
|
||||||
|
println("Warning: Illegal state exception in PlayerFragment")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
|
//http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
private fun initPlayer() {
|
private fun initPlayer() {
|
||||||
println("INIT PLAYER")
|
println("INIT PLAYER")
|
||||||
view?.setOnTouchListener { _, _ -> return@setOnTouchListener true } // VERY IMPORTANT https://stackoverflow.com/questions/28818926/prevent-clicking-on-a-button-in-an-activity-while-showing-a-fragment
|
view?.setOnTouchListener { _, _ -> return@setOnTouchListener true } // VERY IMPORTANT https://stackoverflow.com/questions/28818926/prevent-clicking-on-a-button-in-an-activity-while-showing-a-fragment
|
||||||
thread {
|
val tempUrl = getCurrentUrl()
|
||||||
val currentUrl = getCurrentUrl()
|
println("TEMP:" + tempUrl?.name)
|
||||||
if (currentUrl == null) {
|
if (tempUrl == null) {
|
||||||
activity?.runOnUiThread {
|
val localEpisode = getEpisode()
|
||||||
Toast.makeText(activity, "Error getting link", LENGTH_LONG).show()
|
if (localEpisode != null) {
|
||||||
//MainActivity.popCurrentPage()
|
viewModel.loadEpisode(localEpisode, false) {
|
||||||
}
|
//if(it is Resource.Success && it.value == true)
|
||||||
} else {
|
val currentUrls = getUrls()
|
||||||
|
if (currentUrls != null && currentUrls.isNotEmpty()) {
|
||||||
try {
|
setMirrorId(sortUrls(currentUrls)[0].getId()) // BECAUSE URLS CANT BE REORDERED
|
||||||
activity?.runOnUiThread {
|
|
||||||
val isOnline =
|
|
||||||
currentUrl.url.startsWith("https://") || currentUrl.url.startsWith("http://")
|
|
||||||
|
|
||||||
if (settingsManager?.getBoolean("ignore_ssl", true) == true) {
|
|
||||||
// Disables ssl check
|
|
||||||
val sslContext: SSLContext = SSLContext.getInstance("TLS")
|
|
||||||
sslContext.init(null, arrayOf(SSLTrustManager()), java.security.SecureRandom())
|
|
||||||
sslContext.createSSLEngine()
|
|
||||||
HttpsURLConnection.setDefaultHostnameVerifier { _: String, _: SSLSession ->
|
|
||||||
true
|
|
||||||
}
|
|
||||||
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
|
|
||||||
}
|
|
||||||
|
|
||||||
class CustomFactory : DataSource.Factory {
|
|
||||||
override fun createDataSource(): DataSource {
|
|
||||||
return if (isOnline) {
|
|
||||||
val dataSource = DefaultHttpDataSourceFactory(USER_AGENT).createDataSource()
|
|
||||||
/*FastAniApi.currentHeaders?.forEach {
|
|
||||||
dataSource.setRequestProperty(it.key, it.value)
|
|
||||||
}*/
|
|
||||||
dataSource.setRequestProperty("Referer", currentUrl.referer)
|
|
||||||
dataSource
|
|
||||||
} else {
|
|
||||||
DefaultDataSourceFactory(requireContext(), USER_AGENT).createDataSource()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val mimeType = if (currentUrl.isM3u8) MimeTypes.APPLICATION_M3U8 else MimeTypes.APPLICATION_MP4
|
|
||||||
val _mediaItem = MediaItem.Builder()
|
|
||||||
//Replace needed for android 6.0.0 https://github.com/google/ExoPlayer/issues/5983
|
|
||||||
.setMimeType(mimeType)
|
|
||||||
|
|
||||||
if (isOnline) {
|
|
||||||
_mediaItem.setUri(currentUrl.url)
|
|
||||||
} else {
|
|
||||||
_mediaItem.setUri(Uri.fromFile(File(currentUrl.url)))
|
|
||||||
}
|
|
||||||
|
|
||||||
val mediaItem = _mediaItem.build()
|
|
||||||
val trackSelector = DefaultTrackSelector(requireContext())
|
|
||||||
// Disable subtitles
|
|
||||||
trackSelector.parameters = DefaultTrackSelector.ParametersBuilder(requireContext())
|
|
||||||
.setRendererDisabled(C.TRACK_TYPE_VIDEO, true)
|
|
||||||
.setRendererDisabled(C.TRACK_TYPE_TEXT, true)
|
|
||||||
.setDisabledTextTrackSelectionFlags(C.TRACK_TYPE_TEXT)
|
|
||||||
.clearSelectionOverrides()
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val _exoPlayer =
|
|
||||||
SimpleExoPlayer.Builder(this.requireContext())
|
|
||||||
.setTrackSelector(trackSelector)
|
|
||||||
|
|
||||||
_exoPlayer.setMediaSourceFactory(DefaultMediaSourceFactory(CustomFactory()))
|
|
||||||
exoPlayer = _exoPlayer.build().apply {
|
|
||||||
playWhenReady = isPlayerPlaying
|
|
||||||
seekTo(currentWindow, playbackPosition)
|
|
||||||
setMediaItem(mediaItem, false)
|
|
||||||
prepare()
|
|
||||||
}
|
|
||||||
|
|
||||||
val alphaAnimation = AlphaAnimation(1f, 0f)
|
|
||||||
alphaAnimation.duration = 300
|
|
||||||
alphaAnimation.fillAfter = true
|
|
||||||
loading_overlay.startAnimation(alphaAnimation)
|
|
||||||
video_go_back_holder.visibility = GONE
|
|
||||||
|
|
||||||
exoPlayer.setHandleAudioBecomingNoisy(true) // WHEN HEADPHONES ARE PLUGGED OUT https://github.com/google/ExoPlayer/issues/7288
|
|
||||||
player_view.player = exoPlayer
|
|
||||||
// Sets the speed
|
|
||||||
exoPlayer.setPlaybackParameters(PlaybackParameters(playbackSpeed!!))
|
|
||||||
player_speed_text?.text = "Speed (${playbackSpeed}x)".replace(".0x", "x")
|
|
||||||
|
|
||||||
//https://stackoverflow.com/questions/47731779/detect-pause-resume-in-exoplayer
|
|
||||||
exoPlayer.addListener(object : Player.Listener {
|
|
||||||
// @SuppressLint("NewApi")
|
|
||||||
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
|
|
||||||
// updatePIPModeActions()
|
|
||||||
if (playWhenReady && playbackState == Player.STATE_READY) {
|
|
||||||
// focusRequest?.let { activity?.requestAudioFocus(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPlayerError(error: ExoPlaybackException) {
|
|
||||||
// Lets pray this doesn't spam Toasts :)
|
|
||||||
when (error.type) {
|
|
||||||
ExoPlaybackException.TYPE_SOURCE -> {
|
|
||||||
if (currentUrl.url != "") {
|
|
||||||
Toast.makeText(
|
|
||||||
activity,
|
|
||||||
"Source error\n" + error.sourceException.message,
|
|
||||||
LENGTH_LONG
|
|
||||||
)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ExoPlaybackException.TYPE_REMOTE -> {
|
|
||||||
Toast.makeText(activity, "Remote error", LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
ExoPlaybackException.TYPE_RENDERER -> {
|
|
||||||
Toast.makeText(
|
|
||||||
activity,
|
|
||||||
"Renderer error\n" + error.rendererException.message,
|
|
||||||
LENGTH_LONG
|
|
||||||
)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
ExoPlaybackException.TYPE_UNEXPECTED -> {
|
|
||||||
Toast.makeText(
|
|
||||||
activity,
|
|
||||||
"Unexpected player error\n" + error.unexpectedException.message,
|
|
||||||
LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
} catch (e: java.lang.IllegalStateException) {
|
initPlayer(getCurrentUrl())
|
||||||
println("Warning: Illegal state exception in PlayerFragment")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
initPlayer(tempUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
val currentUrl = tempUrl
|
||||||
|
if (currentUrl == null) {
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
Toast.makeText(activity, "Error getting link", LENGTH_LONG).show()
|
||||||
|
//MainActivity.popCurrentPage()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
}*/
|
||||||
|
|
||||||
//isLoadingNextEpisode = false
|
//isLoadingNextEpisode = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@ const val MAX_SYNO_LENGH = 600
|
||||||
data class ResultEpisode(
|
data class ResultEpisode(
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val episode: Int,
|
val episode: Int,
|
||||||
|
val season: Int?,
|
||||||
val data: Any,
|
val data: Any,
|
||||||
val apiName: String,
|
val apiName: String,
|
||||||
val id: Int,
|
val id: Int,
|
||||||
|
@ -122,7 +123,7 @@ class ResultFragment : Fragment() {
|
||||||
R.anim.exit_anim,
|
R.anim.exit_anim,
|
||||||
R.anim.pop_enter,
|
R.anim.pop_enter,
|
||||||
R.anim.pop_exit)
|
R.anim.pop_exit)
|
||||||
.add(R.id.homeRoot, PlayerFragment.newInstance(PlayerData(index)))
|
.add(R.id.homeRoot, PlayerFragment.newInstance(PlayerData(index, null, 0)))
|
||||||
.commit()
|
.commit()
|
||||||
} else {
|
} else {
|
||||||
when (episodeClick.action) {
|
when (episodeClick.action) {
|
||||||
|
@ -157,7 +158,7 @@ class ResultFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
observe(viewModel.episodes) { episodes ->
|
observe(viewModel.episodes) { episodes ->
|
||||||
if(result_episodes == null || result_episodes.adapter == null) return@observe
|
if (result_episodes == null || result_episodes.adapter == null) return@observe
|
||||||
(result_episodes.adapter as EpisodeAdapter).cardList = episodes
|
(result_episodes.adapter as EpisodeAdapter).cardList = episodes
|
||||||
(result_episodes.adapter as EpisodeAdapter).notifyDataSetChanged()
|
(result_episodes.adapter as EpisodeAdapter).notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@ class ResultViewModel : ViewModel() {
|
||||||
episodes.add(ResultEpisode(
|
episodes.add(ResultEpisode(
|
||||||
null, // TODO ADD NAMES
|
null, // TODO ADD NAMES
|
||||||
index + 1, //TODO MAKE ABLE TO NOT HAVE SOME EPISODE
|
index + 1, //TODO MAKE ABLE TO NOT HAVE SOME EPISODE
|
||||||
|
null, // TODO FIX SEASON
|
||||||
i,
|
i,
|
||||||
apiName,
|
apiName,
|
||||||
(d.url + index).hashCode(),
|
(d.url + index).hashCode(),
|
||||||
|
@ -59,6 +60,7 @@ class ResultViewModel : ViewModel() {
|
||||||
episodes.add(ResultEpisode(
|
episodes.add(ResultEpisode(
|
||||||
null, // TODO ADD NAMES
|
null, // TODO ADD NAMES
|
||||||
index + 1, //TODO MAKE ABLE TO NOT HAVE SOME EPISODE
|
index + 1, //TODO MAKE ABLE TO NOT HAVE SOME EPISODE
|
||||||
|
null, // TODO FIX SEASON
|
||||||
i,
|
i,
|
||||||
apiName,
|
apiName,
|
||||||
(d.url + index).hashCode(),
|
(d.url + index).hashCode(),
|
||||||
|
@ -70,7 +72,7 @@ class ResultViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
is MovieLoadResponse -> {
|
is MovieLoadResponse -> {
|
||||||
_episodes.postValue(arrayListOf(ResultEpisode(null,
|
_episodes.postValue(arrayListOf(ResultEpisode(null,
|
||||||
0,
|
0, null,
|
||||||
d.movieUrl,
|
d.movieUrl,
|
||||||
d.apiName,
|
d.apiName,
|
||||||
(d.url).hashCode(),
|
(d.url).hashCode(),
|
||||||
|
@ -78,9 +80,7 @@ class ResultViewModel : ViewModel() {
|
||||||
0f)))
|
0f)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
|
||||||
|
@ -95,20 +95,25 @@ class ResultViewModel : ViewModel() {
|
||||||
|
|
||||||
private var _apiName: MutableLiveData<String> = MutableLiveData()
|
private var _apiName: MutableLiveData<String> = MutableLiveData()
|
||||||
|
|
||||||
fun loadEpisode(episode: ResultEpisode, callback: (Resource<Boolean>) -> Unit) {
|
fun loadEpisode(episode: ResultEpisode, isCasting : Boolean, callback: (Resource<Boolean>) -> Unit) {
|
||||||
loadEpisode(episode.id, episode.data, callback)
|
loadEpisode(episode.id, episode.data, isCasting, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadEpisode(id: Int, data: Any, callback: (Resource<Boolean>) -> Unit) =
|
fun loadEpisode(id: Int, data: Any, isCasting : Boolean, callback: (Resource<Boolean>) -> Unit) =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (_allEpisodes.value?.contains(id) == true) {
|
if (_allEpisodes.value?.contains(id) == true) {
|
||||||
_allEpisodes.value?.remove(id)
|
_allEpisodes.value?.remove(id)
|
||||||
}
|
}
|
||||||
val links = ArrayList<ExtractorLink>()
|
val links = ArrayList<ExtractorLink>()
|
||||||
val data = safeApiCall {
|
val data = safeApiCall {
|
||||||
getApiFromName(_apiName.value).loadLinks(data, true) { //TODO IMPLEMENT CASTING
|
getApiFromName(_apiName.value).loadLinks(data, isCasting) { //TODO IMPLEMENT CASTING
|
||||||
|
for (i in links) {
|
||||||
|
if (i.url == it.url) return@loadLinks
|
||||||
|
}
|
||||||
|
|
||||||
links.add(it)
|
links.add(it)
|
||||||
_allEpisodes.value?.set(id, links)
|
_allEpisodes.value?.set(id, links)
|
||||||
|
|
||||||
// _allEpisodes.value?.get(episode.id)?.add(it)
|
// _allEpisodes.value?.get(episode.id)?.add(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,13 +130,13 @@ class SearchFragment : Fragment() {
|
||||||
search_loading_bar.alpha = 0f
|
search_loading_bar.alpha = 0f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
(requireActivity() as AppCompatActivity).supportFragmentManager.beginTransaction()
|
(requireActivity() as AppCompatActivity).supportFragmentManager.beginTransaction()
|
||||||
.setCustomAnimations(R.anim.enter_anim,
|
.setCustomAnimations(R.anim.enter_anim,
|
||||||
R.anim.exit_anim,
|
R.anim.exit_anim,
|
||||||
R.anim.pop_enter,
|
R.anim.pop_enter,
|
||||||
R.anim.pop_exit)
|
R.anim.pop_exit)
|
||||||
.add(R.id.homeRoot, PlayerFragment.newInstance(PlayerData(0)))
|
.add(R.id.homeRoot, PlayerFragment.newInstance(PlayerData(0, null,0)))
|
||||||
.commit()
|
.commit()*/
|
||||||
}
|
}
|
||||||
}
|
}
|
18
app/src/main/java/com/lagradost/cloudstream3/utils/Event.kt
Normal file
18
app/src/main/java/com/lagradost/cloudstream3/utils/Event.kt
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package com.lagradost.cloudstream3.utils
|
||||||
|
|
||||||
|
class Event<T> {
|
||||||
|
private val observers = mutableSetOf<(T) -> Unit>()
|
||||||
|
|
||||||
|
operator fun plusAssign(observer: (T) -> Unit) {
|
||||||
|
observers.add(observer)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun minusAssign(observer: (T) -> Unit) {
|
||||||
|
observers.remove(observer)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun invoke(value: T) {
|
||||||
|
for (observer in observers)
|
||||||
|
observer(value)
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,10 @@ data class ExtractorLink(
|
||||||
val isM3u8: Boolean = false,
|
val isM3u8: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun ExtractorLink.getId() : Int {
|
||||||
|
return url.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
enum class Qualities(var value: Int) {
|
enum class Qualities(var value: Int) {
|
||||||
Unknown(0),
|
Unknown(0),
|
||||||
SD(-1), // 360p - 480p
|
SD(-1), // 360p - 480p
|
||||||
|
|
|
@ -109,13 +109,27 @@
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:textFontWeight="900"
|
|
||||||
android:layout_marginTop="15dp"
|
android:layout_marginTop="15dp"
|
||||||
|
android:textStyle="bold"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:id="@+id/video_title"
|
android:id="@+id/video_title"
|
||||||
tools:text="Hello world"
|
tools:text="Hello world"
|
||||||
>
|
>
|
||||||
</TextView>
|
</TextView>
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="250dp"
|
||||||
|
android:layout_marginStart="250dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginTop="35dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:id="@+id/video_title_rez"
|
||||||
|
tools:text="1920x1080"
|
||||||
|
>
|
||||||
|
</TextView>
|
||||||
<!--
|
<!--
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
@ -374,6 +388,7 @@
|
||||||
app:played_color="@color/videoColorPrimary"
|
app:played_color="@color/videoColorPrimary"
|
||||||
app:unplayed_color="@color/videoProgress"/>
|
app:unplayed_color="@color/videoProgress"/>
|
||||||
|
|
||||||
|
<!-- exo_duration-->
|
||||||
<TextView
|
<TextView
|
||||||
tools:text="23:20"
|
tools:text="23:20"
|
||||||
android:id="@id/exo_duration"
|
android:id="@id/exo_duration"
|
||||||
|
@ -625,6 +640,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:text="Skip OP"
|
android:text="Skip OP"
|
||||||
|
android:id="@+id/skip_op_text"
|
||||||
android:gravity="start|center"
|
android:gravity="start|center"
|
||||||
android:paddingStart="10dp"
|
android:paddingStart="10dp"
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="colorPrimary">#3d50fa</color>
|
<color name="colorPrimary">#3d50fa</color>
|
||||||
|
<color name="colorPrimarySecond">@color/colorPrimary</color>
|
||||||
<color name="colorRipple">#1A3D50FA</color>
|
<color name="colorRipple">#1A3D50FA</color>
|
||||||
<color name="colorSearch">#303135</color> <!--#3444D1 @color/itemBackground-->
|
<color name="colorSearch">#303135</color> <!--#3444D1 @color/itemBackground-->
|
||||||
<color name="colorItemSeen">#1E1E32</color>
|
<color name="colorItemSeen">#1E1E32</color>
|
||||||
|
|
|
@ -82,4 +82,19 @@
|
||||||
<item name="tabMinWidth">75dp</item>
|
<item name="tabMinWidth">75dp</item>
|
||||||
<item name="tabMode">scrollable</item>
|
<item name="tabMode">scrollable</item>
|
||||||
</style>
|
</style>
|
||||||
|
<style name="AlertDialogCustom" parent="Theme.AppCompat.Dialog.Alert">
|
||||||
|
<item name="android:textColor">@color/textColor</item>
|
||||||
|
<item name="android:textColorPrimary">@color/textColor</item>
|
||||||
|
<!--<item name="android:background">@color/darkBackground</item>-->
|
||||||
|
<item name="android:textAllCaps">false</item>
|
||||||
|
<!--<item name="android:colorBackground">@color/darkBackground</item>-->
|
||||||
|
<item name="textColorAlertDialogListItem">@color/textColor</item>
|
||||||
|
<item name="colorControlNormal">@color/textColor</item>
|
||||||
|
<!-- colorPrimarySecond used because colorPrimary fails for no reason -->
|
||||||
|
<item name="colorControlActivated">@color/colorPrimarySecond</item>
|
||||||
|
|
||||||
|
<item name="android:windowMinWidthMajor">@android:dimen/dialog_min_width_major</item>
|
||||||
|
<item name="android:windowMinWidthMinor">@android:dimen/dialog_min_width_minor</item>
|
||||||
|
<item name="android:windowBackground">@drawable/dialog__window_background</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in a new issue