Merge pull request #1241 from Bnyro/master

Seperate Player logic
This commit is contained in:
Bnyro 2022-09-09 11:09:58 +02:00 committed by GitHub
commit 020fd50c9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 343 additions and 298 deletions

View File

@ -46,8 +46,6 @@ import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.SubscriptionHelper import com.github.libretube.api.SubscriptionHelper
import com.github.libretube.constants.IntentData import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PreferenceKeys import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.constants.PreferenceRanges
import com.github.libretube.databinding.DialogSliderBinding
import com.github.libretube.databinding.DoubleTapOverlayBinding import com.github.libretube.databinding.DoubleTapOverlayBinding
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
import com.github.libretube.databinding.FragmentPlayerBinding import com.github.libretube.databinding.FragmentPlayerBinding
@ -61,10 +59,8 @@ import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.await import com.github.libretube.extensions.await
import com.github.libretube.extensions.formatShort import com.github.libretube.extensions.formatShort
import com.github.libretube.extensions.hideKeyboard import com.github.libretube.extensions.hideKeyboard
import com.github.libretube.extensions.setSliderRangeAndValue
import com.github.libretube.extensions.toID import com.github.libretube.extensions.toID
import com.github.libretube.interfaces.DoubleTapInterface import com.github.libretube.interfaces.OnlinePlayerOptionsInterface
import com.github.libretube.interfaces.PlayerOptionsInterface
import com.github.libretube.models.PlayerViewModel import com.github.libretube.models.PlayerViewModel
import com.github.libretube.obj.ChapterSegment import com.github.libretube.obj.ChapterSegment
import com.github.libretube.obj.Segment import com.github.libretube.obj.Segment
@ -77,7 +73,6 @@ import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NowPlayingNotification import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.PlayerHelper import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.PreferenceHelper import com.github.libretube.util.PreferenceHelper
import com.github.libretube.views.PlayerOptionsBottomSheet
import com.google.android.exoplayer2.C import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.DefaultLoadControl import com.google.android.exoplayer2.DefaultLoadControl
import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.ExoPlayer
@ -92,13 +87,11 @@ import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.MergingMediaSource import com.google.android.exoplayer2.source.MergingMediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.CaptionStyleCompat import com.google.android.exoplayer2.ui.CaptionStyleCompat
import com.google.android.exoplayer2.ui.StyledPlayerView import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.upstream.DataSource import com.google.android.exoplayer2.upstream.DataSource
import com.google.android.exoplayer2.upstream.DefaultDataSource import com.google.android.exoplayer2.upstream.DefaultDataSource
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.google.android.exoplayer2.util.RepeatModeUtil
import com.google.android.exoplayer2.video.VideoSize import com.google.android.exoplayer2.video.VideoSize
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -153,7 +146,6 @@ class PlayerFragment : BaseFragment() {
* for the player view * for the player view
*/ */
private lateinit var exoPlayerView: StyledPlayerView private lateinit var exoPlayerView: StyledPlayerView
private var isPlayerLocked: Boolean = false
private var subtitle = mutableListOf<SubtitleConfiguration>() private var subtitle = mutableListOf<SubtitleConfiguration>()
/** /**
@ -161,7 +153,6 @@ class PlayerFragment : BaseFragment() {
*/ */
private var token = "" private var token = ""
private var relatedStreamsEnabled = true private var relatedStreamsEnabled = true
private var autoplayEnabled = false
private var autoRotationEnabled = true private var autoRotationEnabled = true
private var playbackSpeed = "1F" private var playbackSpeed = "1F"
private var pausePlayerOnScreenOffEnabled = false private var pausePlayerOnScreenOffEnabled = false
@ -169,7 +160,6 @@ class PlayerFragment : BaseFragment() {
private var watchHistoryEnabled = true private var watchHistoryEnabled = true
private var watchPositionsEnabled = true private var watchPositionsEnabled = true
private var useSystemCaptionStyle = true private var useSystemCaptionStyle = true
private var seekIncrement = 5L
private var videoFormatPreference = "webm" private var videoFormatPreference = "webm"
private var defRes = "" private var defRes = ""
private var bufferingGoal = 50000 private var bufferingGoal = 50000
@ -178,7 +168,6 @@ class PlayerFragment : BaseFragment() {
private var sponsorBlockNotifications = true private var sponsorBlockNotifications = true
private var skipButtonsEnabled = false private var skipButtonsEnabled = false
private var pipEnabled = true private var pipEnabled = true
private var resizeModePref = "fit"
/** /**
* for autoplay * for autoplay
@ -245,7 +234,7 @@ class PlayerFragment : BaseFragment() {
* somehow the bottom bar is invisible on low screen resolutions, this fixes it * somehow the bottom bar is invisible on low screen resolutions, this fixes it
*/ */
private fun showBottomBar() { private fun showBottomBar() {
if (this::playerBinding.isInitialized && !isPlayerLocked) { if (this::playerBinding.isInitialized && !binding.player.isPlayerLocked) {
playerBinding.exoBottomBar.visibility = View.VISIBLE playerBinding.exoBottomBar.visibility = View.VISIBLE
} }
Handler(Looper.getMainLooper()).postDelayed(this::showBottomBar, 100) Handler(Looper.getMainLooper()).postDelayed(this::showBottomBar, 100)
@ -260,11 +249,7 @@ class PlayerFragment : BaseFragment() {
false false
) )
// save whether related streams and autoplay are enabled // save whether related streams are enabled
autoplayEnabled = PreferenceHelper.getBoolean(
PreferenceKeys.AUTO_PLAY,
false
)
relatedStreamsEnabled = PreferenceHelper.getBoolean( relatedStreamsEnabled = PreferenceHelper.getBoolean(
PreferenceKeys.RELATED_STREAMS, PreferenceKeys.RELATED_STREAMS,
true true
@ -300,11 +285,6 @@ class PlayerFragment : BaseFragment() {
true true
) )
seekIncrement = PreferenceHelper.getString(
PreferenceKeys.SEEK_INCREMENT,
"5"
).toLong() * 1000
videoFormatPreference = PreferenceHelper.getString( videoFormatPreference = PreferenceHelper.getString(
PreferenceKeys.PLAYER_VIDEO_FORMAT, PreferenceKeys.PLAYER_VIDEO_FORMAT,
"webm" "webm"
@ -348,11 +328,6 @@ class PlayerFragment : BaseFragment() {
PreferenceKeys.PICTURE_IN_PICTURE, PreferenceKeys.PICTURE_IN_PICTURE,
true true
) )
resizeModePref = PreferenceHelper.getString(
PreferenceKeys.PLAYER_RESIZE_MODE,
"fit"
)
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
@ -424,25 +399,7 @@ class PlayerFragment : BaseFragment() {
} }
} }
private val playerOptionsInterface = object : PlayerOptionsInterface { private val onlinePlayerOptionsInterface = object : OnlinePlayerOptionsInterface {
override fun onAutoplayClicked() {
// autoplay options dialog
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.player_autoplay)
.setItems(
arrayOf(
context?.getString(R.string.enabled),
context?.getString(R.string.disabled)
)
) { _, index ->
when (index) {
0 -> autoplayEnabled = true
1 -> autoplayEnabled = false
}
}
.show()
}
override fun onCaptionClicked() { override fun onCaptionClicked() {
if (!this@PlayerFragment::streams.isInitialized || if (!this@PlayerFragment::streams.isInitialized ||
streams.subtitles == null || streams.subtitles == null ||
@ -515,62 +472,6 @@ class PlayerFragment : BaseFragment() {
} }
.show() .show()
} }
override fun onPlaybackSpeedClicked() {
val playbackSpeedBinding = DialogSliderBinding.inflate(layoutInflater)
playbackSpeedBinding.slider.setSliderRangeAndValue(
PreferenceRanges.playbackSpeed
)
playbackSpeedBinding.slider.value = exoPlayer.playbackParameters.speed
// change playback speed dialog
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.change_playback_speed)
.setView(playbackSpeedBinding.root)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.okay) { _, _ ->
exoPlayer.setPlaybackSpeed(
playbackSpeedBinding.slider.value
)
}
.show()
}
override fun onResizeModeClicked() {
// switching between original aspect ratio (black bars) and zoomed to fill device screen
val aspectRatioModeNames = context?.resources?.getStringArray(R.array.resizeMode)
val aspectRatioModes = arrayOf(
AspectRatioFrameLayout.RESIZE_MODE_FIT,
AspectRatioFrameLayout.RESIZE_MODE_ZOOM,
AspectRatioFrameLayout.RESIZE_MODE_FILL
)
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.aspect_ratio)
.setItems(aspectRatioModeNames) { _, index ->
exoPlayerView.resizeMode = aspectRatioModes[index]
}
.show()
}
override fun onRepeatModeClicked() {
val repeatModeNames = arrayOf(
context?.getString(R.string.repeat_mode_none),
context?.getString(R.string.repeat_mode_current)
)
val repeatModes = arrayOf(
RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL,
RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE
)
// repeat mode options dialog
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.repeat_mode)
.setItems(repeatModeNames) { _, index ->
exoPlayer.repeatMode = repeatModes[index]
}
.show()
}
} }
// actions that don't depend on video information // actions that don't depend on video information
@ -591,55 +492,6 @@ class PlayerFragment : BaseFragment() {
.remove(this) .remove(this)
.commit() .commit()
} }
// show the advanced player options
playerBinding.toggleOptions.setOnClickListener {
val bottomSheetFragment = PlayerOptionsBottomSheet().apply {
setOnClickListeners(playerOptionsInterface)
// set the auto play mode
currentAutoplayMode = if (autoplayEnabled) {
context.getString(R.string.enabled)
} else {
context.getString(R.string.disabled)
}
// set the current caption language
currentCaptions =
if (trackSelector.parameters.preferredTextLanguages.isNotEmpty()) {
trackSelector.parameters.preferredTextLanguages[0]
} else {
context.getString(R.string.none)
}
// set the playback speed
currentPlaybackSpeed = "${
exoPlayer.playbackParameters.speed.toString()
.replace(".0", "")
}x"
// set the quality text
val isAdaptive = exoPlayer.videoFormat?.codecs != null
val quality = exoPlayer.videoSize.height
if (quality != 0) {
currentQuality =
if (isAdaptive) {
"${context.getString(R.string.hls)}${quality}p"
} else {
"${quality}p"
}
}
// set the repeat mode
currentRepeatMode =
if (exoPlayer.repeatMode == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) {
context.getString(R.string.repeat_mode_none)
} else {
context.getString(R.string.repeat_mode_current)
}
// set the aspect ratio mode
currentResizeMode = when (exoPlayerView.resizeMode) {
AspectRatioFrameLayout.RESIZE_MODE_FIT -> context.getString(R.string.resize_mode_fit)
AspectRatioFrameLayout.RESIZE_MODE_FILL -> context.getString(R.string.resize_mode_fill)
else -> context.getString(R.string.resize_mode_zoom)
}
}
bottomSheetFragment.show(childFragmentManager, null)
}
binding.playImageView.setOnClickListener { binding.playImageView.setOnClickListener {
if (!exoPlayer.isPlaying) { if (!exoPlayer.isPlaying) {
@ -677,22 +529,6 @@ class PlayerFragment : BaseFragment() {
} }
} }
// lock and unlock the player
playerBinding.lockPlayer.setOnClickListener {
// change the locked/unlocked icon
if (!isPlayerLocked) {
playerBinding.lockPlayer.setImageResource(R.drawable.ic_locked)
} else {
playerBinding.lockPlayer.setImageResource(R.drawable.ic_unlocked)
}
// show/hide all the controls
lockPlayer(isPlayerLocked)
// change locked status
isPlayerLocked = !isPlayerLocked
}
// set default playback speed // set default playback speed
exoPlayer.setPlaybackSpeed(playbackSpeed.toFloat()) exoPlayer.setPlaybackSpeed(playbackSpeed.toFloat())
@ -919,7 +755,7 @@ class PlayerFragment : BaseFragment() {
// show comments if related streams disabled // show comments if related streams disabled
if (!relatedStreamsEnabled) toggleComments() if (!relatedStreamsEnabled) toggleComments()
// prepare for autoplay // prepare for autoplay
if (autoplayEnabled) setNextStream() if (binding.player.autoplayEnabled) setNextStream()
// add the video to the watch history // add the video to the watch history
if (watchHistoryEnabled) DatabaseHelper.addToWatchHistory(videoId!!, streams) if (watchHistoryEnabled) DatabaseHelper.addToWatchHistory(videoId!!, streams)
@ -1023,11 +859,6 @@ class PlayerFragment : BaseFragment() {
controllerHideOnTouch = true controllerHideOnTouch = true
useController = false useController = false
player = exoPlayer player = exoPlayer
resizeMode = when (resizeModePref) {
"fill" -> AspectRatioFrameLayout.RESIZE_MODE_FILL
"zoom" -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
}
} }
if (useSystemCaptionStyle) { if (useSystemCaptionStyle) {
@ -1049,6 +880,14 @@ class PlayerFragment : BaseFragment() {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
private fun initializePlayerView(response: Streams) { private fun initializePlayerView(response: Streams) {
// initialize the player view actions
binding.player.initialize(
childFragmentManager,
onlinePlayerOptionsInterface,
doubleTapOverlayBinding,
trackSelector
)
binding.apply { binding.apply {
playerViewsInfo.text = playerViewsInfo.text =
context?.getString(R.string.views, response.views.formatShort()) + context?.getString(R.string.views, response.views.formatShort()) +
@ -1073,8 +912,6 @@ class PlayerFragment : BaseFragment() {
playerBinding.exoTitle.text = response.title playerBinding.exoTitle.text = response.title
enableDoubleTapToSeek()
// init the chapters recyclerview // init the chapters recyclerview
if (response.chapters != null) { if (response.chapters != null) {
chapters = response.chapters chapters = response.chapters
@ -1120,11 +957,11 @@ class PlayerFragment : BaseFragment() {
playbackState == Player.STATE_ENDED && playbackState == Player.STATE_ENDED &&
nextStreamId != null && nextStreamId != null &&
!transitioning && !transitioning &&
autoplayEnabled binding.player.autoplayEnabled
) { ) {
transitioning = true transitioning = true
// check whether autoplay is enabled // check whether autoplay is enabled
if (autoplayEnabled) playNextVideo() if (binding.player.autoplayEnabled) playNextVideo()
} }
if (playbackState == Player.STATE_READY) { if (playbackState == Player.STATE_READY) {
@ -1250,83 +1087,6 @@ class PlayerFragment : BaseFragment() {
} }
} }
private fun enableDoubleTapToSeek() {
// set seek increment text
val seekIncrementText = (seekIncrement / 1000).toString()
doubleTapOverlayBinding.rewindTV.text = seekIncrementText
doubleTapOverlayBinding.forwardTV.text = seekIncrementText
binding.player.setOnDoubleTapListener(
object : DoubleTapInterface {
override fun onEvent(x: Float) {
val width = exoPlayerView.width
when {
width * 0.5 > x -> rewind()
width * 0.5 < x -> forward()
}
}
}
)
}
private fun rewind() {
exoPlayer.seekTo(exoPlayer.currentPosition - seekIncrement)
// show the rewind button
doubleTapOverlayBinding.rewindBTN.apply {
visibility = View.VISIBLE
// clear previous animation
animate().rotation(0F).setDuration(0).start()
// start new animation
animate()
.rotation(-30F)
.setDuration(100)
.withEndAction {
// reset the animation when finished
animate().rotation(0F).setDuration(100).start()
}
.start()
removeCallbacks(hideRewindButtonRunnable)
// start callback to hide the button
postDelayed(hideRewindButtonRunnable, 700)
}
}
private fun forward() {
exoPlayer.seekTo(exoPlayer.currentPosition + seekIncrement)
// show the forward button
doubleTapOverlayBinding.forwardBTN.apply {
visibility = View.VISIBLE
// clear previous animation
animate().rotation(0F).setDuration(0).start()
// start new animation
animate()
.rotation(30F)
.setDuration(100)
.withEndAction {
// reset the animation when finished
animate().rotation(0F).setDuration(100).start()
}
.start()
// start callback to hide the button
removeCallbacks(hideForwardButtonRunnable)
postDelayed(hideForwardButtonRunnable, 700)
}
}
private val hideForwardButtonRunnable = Runnable {
doubleTapOverlayBinding.forwardBTN.apply {
visibility = View.GONE
}
}
private val hideRewindButtonRunnable = Runnable {
doubleTapOverlayBinding.rewindBTN.apply {
visibility = View.GONE
}
}
private fun initializeChapters() { private fun initializeChapters() {
if (chapters.isEmpty()) { if (chapters.isEmpty()) {
binding.chaptersRecView.visibility = View.GONE binding.chaptersRecView.visibility = View.GONE
@ -1533,7 +1293,7 @@ class PlayerFragment : BaseFragment() {
// handles the audio focus // handles the audio focus
val audioAttributes = AudioAttributes.Builder() val audioAttributes = AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA) .setUsage(C.USAGE_MEDIA)
.setContentType(C.CONTENT_TYPE_MOVIE) .setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
.build() .build()
// handles the duration of media to retain in the buffer prior to the current playback position (for fast backward seeking) // handles the duration of media to retain in the buffer prior to the current playback position (for fast backward seeking)
@ -1570,34 +1330,6 @@ class PlayerFragment : BaseFragment() {
nowPlayingNotification.updatePlayerNotification(streams) nowPlayingNotification.updatePlayerNotification(streams)
} }
// lock the player
private fun lockPlayer(isLocked: Boolean) {
// isLocked is the current (old) state of the player lock
val visibility = if (isLocked) View.VISIBLE else View.GONE
playerBinding.exoTopBarRight.visibility = visibility
playerBinding.exoCenterControls.visibility = visibility
playerBinding.exoBottomBar.visibility = visibility
playerBinding.closeImageButton.visibility = visibility
playerBinding.exoTitle.visibility =
if (isLocked &&
viewModel.isFullscreen.value == true
) {
View.VISIBLE
} else {
View.INVISIBLE
}
// disable double tap to seek when the player is locked
if (isLocked) {
// enable fast forward and rewind by double tapping
enableDoubleTapToSeek()
} else {
// disable fast forward and rewind by double tapping
binding.player.setOnDoubleTapListener(null)
}
}
private fun isSubscribed() { private fun isSubscribed() {
fun run() { fun run() {
val channelId = streams.uploaderUrl!!.toID() val channelId = streams.uploaderUrl!!.toID()

View File

@ -0,0 +1,7 @@
package com.github.libretube.interfaces
interface OnlinePlayerOptionsInterface {
fun onCaptionClicked()
fun onQualityClicked()
}

View File

@ -1,13 +1,8 @@
package com.github.libretube.interfaces package com.github.libretube.interfaces
interface PlayerOptionsInterface { interface PlayerOptionsInterface {
fun onAutoplayClicked() fun onAutoplayClicked()
fun onCaptionClicked()
fun onQualityClicked()
fun onPlaybackSpeedClicked() fun onPlaybackSpeedClicked()
fun onResizeModeClicked() fun onResizeModeClicked()

View File

@ -2,13 +2,31 @@ package com.github.libretube.views
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View
import androidx.fragment.app.FragmentManager
import com.github.libretube.R
import com.github.libretube.activities.MainActivity import com.github.libretube.activities.MainActivity
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.constants.PreferenceRanges
import com.github.libretube.databinding.DialogSliderBinding
import com.github.libretube.databinding.DoubleTapOverlayBinding
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
import com.github.libretube.extensions.setSliderRangeAndValue
import com.github.libretube.interfaces.DoubleTapInterface import com.github.libretube.interfaces.DoubleTapInterface
import com.github.libretube.interfaces.OnlinePlayerOptionsInterface
import com.github.libretube.interfaces.PlayerOptionsInterface
import com.github.libretube.util.DoubleTapListener import com.github.libretube.util.DoubleTapListener
import com.github.libretube.util.PreferenceHelper
import com.google.android.exoplayer2.trackselection.TrackSelector
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.StyledPlayerView import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.util.RepeatModeUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
internal class CustomExoPlayerView( internal class CustomExoPlayerView(
@ -16,17 +34,40 @@ internal class CustomExoPlayerView(
attributeSet: AttributeSet? = null attributeSet: AttributeSet? = null
) : StyledPlayerView(context, attributeSet) { ) : StyledPlayerView(context, attributeSet) {
val binding: ExoStyledPlayerControlViewBinding = ExoStyledPlayerControlViewBinding.bind(this) val binding: ExoStyledPlayerControlViewBinding = ExoStyledPlayerControlViewBinding.bind(this)
private var doubleTapOverlayBinding: DoubleTapOverlayBinding? = null
/**
* Objects from the parent fragment
*/
private var doubleTapListener: DoubleTapInterface? = null private var doubleTapListener: DoubleTapInterface? = null
private var onlinePlayerOptionsInterface: OnlinePlayerOptionsInterface? = null
private lateinit var childFragmentManager: FragmentManager
private var trackSelector: TrackSelector? = null
private val runnableHandler = Handler(Looper.getMainLooper())
// the x-position of where the user clicked // the x-position of where the user clicked
private var xPos = 0F private var xPos = 0F
fun setOnDoubleTapListener( var isPlayerLocked: Boolean = false
eventListener: DoubleTapInterface?
) { /**
doubleTapListener = eventListener * Preferences
} */
var autoplayEnabled = PreferenceHelper.getBoolean(
PreferenceKeys.AUTO_PLAY,
false
)
private val seekIncrement = PreferenceHelper.getString(
PreferenceKeys.SEEK_INCREMENT,
"5"
).toLong() * 1000
private var resizeModePref = PreferenceHelper.getString(
PreferenceKeys.PLAYER_RESIZE_MODE,
"fit"
)
private fun toggleController() { private fun toggleController() {
if (isControllerFullyVisible) hideController() else showController() if (isControllerFullyVisible) hideController() else showController()
@ -42,9 +83,47 @@ internal class CustomExoPlayerView(
} }
} }
init { fun initialize(
childFragmentManager: FragmentManager,
playerViewInterface: OnlinePlayerOptionsInterface,
doubleTapOverlayBinding: DoubleTapOverlayBinding,
trackSelector: TrackSelector
) {
this.childFragmentManager = childFragmentManager
this.onlinePlayerOptionsInterface = playerViewInterface
this.doubleTapOverlayBinding = doubleTapOverlayBinding
this.trackSelector = trackSelector
// set the double click listener for rewind/forward // set the double click listener for rewind/forward
setOnClickListener(doubleTouchListener) setOnClickListener(doubleTouchListener)
enableDoubleTapToSeek()
initializeAdvancedOptions()
// locking the player
binding.lockPlayer.setOnClickListener {
// change the locked/unlocked icon
binding.lockPlayer.setImageResource(
if (!isPlayerLocked) {
R.drawable.ic_locked
} else {
R.drawable.ic_unlocked
}
)
// show/hide all the controls
lockPlayer(isPlayerLocked)
// change locked status
isPlayerLocked = !isPlayerLocked
}
resizeMode = when (resizeModePref) {
"fill" -> AspectRatioFrameLayout.RESIZE_MODE_FILL
"zoom" -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
}
} }
override fun hideController() { override fun hideController() {
@ -59,4 +138,225 @@ internal class CustomExoPlayerView(
doubleTouchListener.onClick(this) doubleTouchListener.onClick(this)
return false return false
} }
private fun initializeAdvancedOptions() {
binding.toggleOptions.setOnClickListener {
val bottomSheetFragment = PlayerOptionsBottomSheet().apply {
setOnClickListeners(
playerOptionsInterface,
onlinePlayerOptionsInterface
)
// set the auto play mode
currentAutoplayMode = if (autoplayEnabled) {
context?.getString(R.string.enabled)
} else {
context?.getString(R.string.disabled)
}
// set the current caption language
currentCaptions =
if (trackSelector != null && trackSelector!!.parameters.preferredTextLanguages.isNotEmpty()) {
trackSelector!!.parameters.preferredTextLanguages[0]
} else {
context?.getString(R.string.none)
}
// set the playback speed
currentPlaybackSpeed = "${
player?.playbackParameters?.speed.toString()
.replace(".0", "")
}x"
// set the quality text
val quality = player?.videoSize?.height
if (quality != 0) {
currentQuality = "${quality}p"
}
// set the repeat mode
currentRepeatMode =
if (player?.repeatMode == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) {
context?.getString(R.string.repeat_mode_none)
} else {
context?.getString(R.string.repeat_mode_current)
}
// set the aspect ratio mode
currentResizeMode = when (resizeMode) {
AspectRatioFrameLayout.RESIZE_MODE_FIT -> context?.getString(R.string.resize_mode_fit)
AspectRatioFrameLayout.RESIZE_MODE_FILL -> context?.getString(R.string.resize_mode_fill)
else -> context?.getString(R.string.resize_mode_zoom)
}
}
bottomSheetFragment.show(childFragmentManager, null)
}
}
// lock the player
private fun lockPlayer(isLocked: Boolean) {
// isLocked is the current (old) state of the player lock
val visibility = if (isLocked) View.VISIBLE else View.GONE
binding.exoTopBarRight.visibility = visibility
binding.exoCenterControls.visibility = visibility
binding.exoBottomBar.visibility = visibility
binding.closeImageButton.visibility = visibility
// disable double tap to seek when the player is locked
if (isLocked) {
// enable fast forward and rewind by double tapping
enableDoubleTapToSeek()
} else {
// disable fast forward and rewind by double tapping
doubleTapListener = null
}
}
private fun enableDoubleTapToSeek() {
// set seek increment text
val seekIncrementText = (seekIncrement / 1000).toString()
doubleTapOverlayBinding?.rewindTV?.text = seekIncrementText
doubleTapOverlayBinding?.forwardTV?.text = seekIncrementText
doubleTapListener =
object : DoubleTapInterface {
override fun onEvent(x: Float) {
when {
width * 0.5 > x -> rewind()
width * 0.5 < x -> forward()
}
}
}
}
private fun rewind() {
player?.seekTo((player?.currentPosition ?: 0L) - seekIncrement)
// show the rewind button
doubleTapOverlayBinding?.rewindBTN.run {
this!!.visibility = View.VISIBLE
// clear previous animation
this.animate().rotation(0F).setDuration(0).start()
// start new animation
this.animate()
.rotation(-30F)
.setDuration(100)
.withEndAction {
// reset the animation when finished
animate().rotation(0F).setDuration(100).start()
}
.start()
runnableHandler.removeCallbacks(hideRewindButtonRunnable)
// start callback to hide the button
runnableHandler.postDelayed(hideRewindButtonRunnable, 700)
}
}
private fun forward() {
player?.seekTo(player!!.currentPosition + seekIncrement)
// show the forward button
doubleTapOverlayBinding?.forwardBTN.apply {
visibility = View.VISIBLE
// clear previous animation
this!!.animate().rotation(0F).setDuration(0).start()
// start new animation
this.animate()
.rotation(30F)
.setDuration(100)
.withEndAction {
// reset the animation when finished
animate().rotation(0F).setDuration(100).start()
}
.start()
// start callback to hide the button
runnableHandler.removeCallbacks(hideForwardButtonRunnable)
runnableHandler.postDelayed(hideForwardButtonRunnable, 700)
}
}
private val hideForwardButtonRunnable = Runnable {
doubleTapOverlayBinding?.forwardBTN.apply {
this!!.visibility = View.GONE
}
}
private val hideRewindButtonRunnable = Runnable {
doubleTapOverlayBinding?.rewindBTN.apply {
this!!.visibility = View.GONE
}
}
private val playerOptionsInterface = object : PlayerOptionsInterface {
override fun onAutoplayClicked() {
// autoplay options dialog
MaterialAlertDialogBuilder(context)
.setTitle(R.string.player_autoplay)
.setItems(
arrayOf(
context.getString(R.string.enabled),
context.getString(R.string.disabled)
)
) { _, index ->
when (index) {
0 -> autoplayEnabled = true
1 -> autoplayEnabled = false
}
}
.show()
}
override fun onPlaybackSpeedClicked() {
val playbackSpeedBinding = DialogSliderBinding.inflate(
LayoutInflater.from(context)
)
playbackSpeedBinding.slider.setSliderRangeAndValue(
PreferenceRanges.playbackSpeed
)
playbackSpeedBinding.slider.value = player?.playbackParameters?.speed ?: 1f
// change playback speed dialog
MaterialAlertDialogBuilder(context)
.setTitle(R.string.change_playback_speed)
.setView(playbackSpeedBinding.root)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.okay) { _, _ ->
player?.setPlaybackSpeed(
playbackSpeedBinding.slider.value
)
}
.show()
}
override fun onResizeModeClicked() {
// switching between original aspect ratio (black bars) and zoomed to fill device screen
val aspectRatioModeNames = context.resources?.getStringArray(R.array.resizeMode)
val aspectRatioModes = arrayOf(
AspectRatioFrameLayout.RESIZE_MODE_FIT,
AspectRatioFrameLayout.RESIZE_MODE_ZOOM,
AspectRatioFrameLayout.RESIZE_MODE_FILL
)
MaterialAlertDialogBuilder(context)
.setTitle(R.string.aspect_ratio)
.setItems(aspectRatioModeNames) { _, index ->
resizeMode = aspectRatioModes[index]
}
.show()
}
override fun onRepeatModeClicked() {
val repeatModeNames = arrayOf(
context.getString(R.string.repeat_mode_none),
context.getString(R.string.repeat_mode_current)
)
val repeatModes = arrayOf(
RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL,
RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE
)
// repeat mode options dialog
MaterialAlertDialogBuilder(context)
.setTitle(R.string.repeat_mode)
.setItems(repeatModeNames) { _, index ->
player?.repeatMode = repeatModes[index]
}
.show()
}
}
} }

View File

@ -6,6 +6,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import com.github.libretube.databinding.BottomSheetBinding import com.github.libretube.databinding.BottomSheetBinding
import com.github.libretube.interfaces.OnlinePlayerOptionsInterface
import com.github.libretube.interfaces.PlayerOptionsInterface import com.github.libretube.interfaces.PlayerOptionsInterface
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
@ -17,6 +18,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
class PlayerOptionsBottomSheet : BottomSheetDialogFragment() { class PlayerOptionsBottomSheet : BottomSheetDialogFragment() {
lateinit var binding: BottomSheetBinding lateinit var binding: BottomSheetBinding
private lateinit var playerOptionsInterface: PlayerOptionsInterface private lateinit var playerOptionsInterface: PlayerOptionsInterface
private var onlinePlayerOptionsInterface: OnlinePlayerOptionsInterface? = null
/** /**
* current values * current values
@ -46,13 +48,22 @@ class PlayerOptionsBottomSheet : BottomSheetDialogFragment() {
return binding.root return binding.root
} }
fun setOnClickListeners(playerOptionsInterface: PlayerOptionsInterface) { fun setOnClickListeners(
playerOptionsInterface: PlayerOptionsInterface,
onlinePlayerOptionsInterface: OnlinePlayerOptionsInterface?
) {
this.playerOptionsInterface = playerOptionsInterface this.playerOptionsInterface = playerOptionsInterface
this.onlinePlayerOptionsInterface = onlinePlayerOptionsInterface
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
if (onlinePlayerOptionsInterface == null) {
binding.captions.visibility = View.GONE
binding.quality.visibility = View.GONE
}
/** /**
* update the text if a value is selected * update the text if a value is selected
*/ */
@ -75,7 +86,7 @@ class PlayerOptionsBottomSheet : BottomSheetDialogFragment() {
} }
binding.quality.setOnClickListener { binding.quality.setOnClickListener {
playerOptionsInterface.onQualityClicked() onlinePlayerOptionsInterface?.onQualityClicked()
this.dismiss() this.dismiss()
} }
@ -85,7 +96,7 @@ class PlayerOptionsBottomSheet : BottomSheetDialogFragment() {
} }
binding.captions.setOnClickListener { binding.captions.setOnClickListener {
playerOptionsInterface.onCaptionClicked() onlinePlayerOptionsInterface?.onCaptionClicked()
this.dismiss() this.dismiss()
} }