Code refactor: Separate online and offline player

This commit is contained in:
Bnyro 2023-06-11 14:18:52 +02:00
parent 2a4aad88ee
commit 44d54d37c1
7 changed files with 229 additions and 194 deletions

View File

@ -5,13 +5,9 @@ import android.media.session.PlaybackState
import android.net.Uri
import android.os.Bundle
import android.text.format.DateUtils
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import androidx.activity.viewModels
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.view.marginTop
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.C
import androidx.media3.common.MediaItem
@ -30,9 +26,7 @@ import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.ActivityOfflinePlayerBinding
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
import com.github.libretube.db.DatabaseHolder.Database
import com.github.libretube.db.obj.DownloadItem
import com.github.libretube.enums.FileType
import com.github.libretube.extensions.dpToPx
import com.github.libretube.extensions.toAndroidUri
import com.github.libretube.extensions.updateParameters
import com.github.libretube.helpers.PlayerHelper
@ -105,24 +99,14 @@ class OfflinePlayerActivity : BaseActivity() {
playerView.player = player
playerBinding = binding.player.binding
// increase the margin to the status bar
playerBinding.topBar.setPadding(
playerBinding.topBar.paddingLeft,
playerBinding.topBar.paddingTop * 2,
playerBinding.topBar.paddingRight,
playerBinding.topBar.paddingBottom
)
playerBinding.fullscreen.isInvisible = true
playerBinding.closeImageButton.setOnClickListener {
finish()
}
binding.player.initialize(
null,
binding.doubleTapOverlay.binding,
binding.playerGestureControlsView.binding,
trackSelector,
)
}
@ -181,6 +165,7 @@ class OfflinePlayerActivity : BaseActivity() {
player.setMediaSource(mediaSource)
}
videoUri != null -> player.setMediaItem(
MediaItem.Builder()
.setUri(videoUri)
@ -189,6 +174,7 @@ class OfflinePlayerActivity : BaseActivity() {
}
.build(),
)
audioUri != null -> player.setMediaItem(
MediaItem.Builder()
.setUri(audioUri)

View File

@ -16,7 +16,6 @@ import android.os.PowerManager
import android.text.format.DateUtils
import android.text.method.LinkMovementMethod
import android.text.util.Linkify
import android.util.Base64
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -76,8 +75,6 @@ import com.github.libretube.extensions.toID
import com.github.libretube.extensions.toastFromMainDispatcher
import com.github.libretube.extensions.updateParameters
import com.github.libretube.helpers.BackgroundHelper
import com.github.libretube.helpers.DashHelper
import com.github.libretube.helpers.DisplayHelper
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.LocaleHelper
import com.github.libretube.helpers.NavigationHelper
@ -892,14 +889,8 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
@SuppressLint("SetTextI18n")
private fun initializePlayerView() {
// initialize the player view actions
binding.player.initialize(
this,
doubleTapOverlayBinding,
playerGestureControlsViewBinding,
trackSelector,
viewModel,
viewLifecycleOwner,
)
binding.player.initialize(doubleTapOverlayBinding, playerGestureControlsViewBinding)
binding.player.initPlayerOptions(viewModel, viewLifecycleOwner, trackSelector, this)
binding.apply {
val views = streams.views.formatShort()

View File

@ -17,16 +17,13 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.os.postDelayed
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.marginStart
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.LifecycleOwner
import androidx.media3.common.Player
import androidx.media3.common.text.Cue
import androidx.media3.common.util.RepeatModeUtil
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.trackselection.TrackSelector
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.CaptionStyleCompat
import androidx.media3.ui.PlayerView
@ -42,26 +39,23 @@ import com.github.libretube.extensions.round
import com.github.libretube.helpers.AudioHelper
import com.github.libretube.helpers.BrightnessHelper
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.helpers.WindowHelper
import com.github.libretube.obj.BottomSheetItem
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.extensions.toggleSystemBars
import com.github.libretube.ui.interfaces.OnlinePlayerOptions
import com.github.libretube.ui.interfaces.PlayerGestureOptions
import com.github.libretube.ui.interfaces.PlayerOptions
import com.github.libretube.ui.listeners.PlayerGestureController
import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.ui.sheets.BaseBottomSheet
import com.github.libretube.ui.sheets.PlaybackOptionsSheet
import com.github.libretube.util.PlayingQueue
@SuppressLint("ClickableViewAccessibility")
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
internal class CustomExoPlayerView(
open class CustomExoPlayerView(
context: Context,
attributeSet: AttributeSet? = null,
) : PlayerView(context, attributeSet), PlayerOptions, PlayerGestureOptions {
val binding: ExoStyledPlayerControlViewBinding = ExoStyledPlayerControlViewBinding.bind(this)
@Suppress("LeakingThis")
val binding = ExoStyledPlayerControlViewBinding.bind(this)
/**
* Objects for player tap and swipe gesture
@ -71,27 +65,20 @@ internal class CustomExoPlayerView(
private lateinit var brightnessHelper: BrightnessHelper
private lateinit var audioHelper: AudioHelper
private var doubleTapOverlayBinding: DoubleTapOverlayBinding? = null
private var playerViewModel: PlayerViewModel? = null
/**
* Objects from the parent fragment
*/
private var playerOptionsInterface: OnlinePlayerOptions? = null
private var trackSelector: TrackSelector? = null
private val runnableHandler = Handler(Looper.getMainLooper())
var isPlayerLocked: Boolean = false
/**
* Preferences
*/
var autoplayEnabled = PlayerHelper.autoPlayEnabled
private var resizeModePref = PlayerHelper.resizeModePref
private val activity
get() = context as BaseActivity
val activity get() = context as BaseActivity
private val supportFragmentManager
get() = activity.supportFragmentManager
@ -101,18 +88,11 @@ internal class CustomExoPlayerView(
}
fun initialize(
playerViewInterface: OnlinePlayerOptions?,
doubleTapOverlayBinding: DoubleTapOverlayBinding,
playerGestureControlsViewBinding: PlayerGestureControlsViewBinding,
trackSelector: TrackSelector?,
playerViewModel: PlayerViewModel? = null,
viewLifecycleOwner: LifecycleOwner? = null,
) {
this.playerOptionsInterface = playerViewInterface
this.doubleTapOverlayBinding = doubleTapOverlayBinding
this.trackSelector = trackSelector
this.gestureViewBinding = playerGestureControlsViewBinding
this.playerViewModel = playerViewModel
this.playerGestureController = PlayerGestureController(context as BaseActivity, this)
this.brightnessHelper = BrightnessHelper(context as Activity)
this.audioHelper = AudioHelper(context)
@ -123,7 +103,7 @@ internal class CustomExoPlayerView(
initRewindAndForward()
applyCaptionsStyle()
initializeAdvancedOptions(context)
initializeAdvancedOptions()
// don't let the player view hide its controls automatically
controllerShowTimeoutMs = -1
@ -148,12 +128,6 @@ internal class CustomExoPlayerView(
isPlayerLocked = !isPlayerLocked
}
binding.autoPlay.isChecked = autoplayEnabled
binding.autoPlay.setOnCheckedChangeListener { _, isChecked ->
autoplayEnabled = isChecked
}
resizeMode = when (resizeModePref) {
"fill" -> AspectRatioFrameLayout.RESIZE_MODE_FILL
"zoom" -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
@ -183,11 +157,7 @@ internal class CustomExoPlayerView(
// keep screen on if the video is playing
keepScreenOn = player.isPlaying == true
if (player.playbackState == Player.STATE_ENDED && !autoplayEnabled) {
showController()
cancelHideControllerTask()
}
onPlayerEvent(player, events)
}
}
})
@ -206,26 +176,10 @@ internal class CustomExoPlayerView(
enqueueHideControllerTask()
}
})
setControllerVisibilityListener(
ControllerVisibilityListener { visibility ->
playerViewModel?.isFullscreen?.value?.let { isFullscreen ->
if (!isFullscreen) return@let
// Show status bar only not navigation bar if the player controls are visible and hide it otherwise
activity.toggleSystemBars(
types = WindowInsetsCompat.Type.statusBars(),
showBars = visibility == View.VISIBLE,
)
}
},
)
playerViewModel?.isFullscreen?.observe(viewLifecycleOwner!!) { isFullscreen ->
WindowHelper.toggleFullscreen(activity, isFullscreen)
updateTopBarMargin()
}
}
open fun onPlayerEvent(player: Player, playerEvents: Player.Events) = Unit
private fun updatePlayPauseButton() {
binding.playPauseBTN.setImageResource(
when {
@ -250,16 +204,6 @@ internal class CustomExoPlayerView(
// remove the callback to hide the controller
cancelHideControllerTask()
super.hideController()
// hide system bars if in fullscreen or offline player
if (playerViewModel != null) {
if (playerViewModel!!.isFullscreen.value == true) {
WindowHelper.toggleFullscreen(activity, true)
}
updateTopBarMargin()
} else {
activity.toggleSystemBars(WindowInsetsCompat.Type.systemBars(), false)
}
}
override fun showController() {
@ -268,11 +212,6 @@ internal class CustomExoPlayerView(
// automatically hide the controller after 2 seconds
enqueueHideControllerTask()
super.showController()
// show the system bars when in offline player
if (playerViewModel == null) {
activity.toggleSystemBars(WindowInsetsCompat.Type.statusBars(), true)
}
}
override fun onTouchEvent(event: MotionEvent) = false
@ -300,97 +239,56 @@ internal class CustomExoPlayerView(
}
}
private fun initializeAdvancedOptions(context: Context) {
private fun initializeAdvancedOptions() {
binding.toggleOptions.setOnClickListener {
val items = mutableListOf(
BottomSheetItem(
context.getString(R.string.repeat_mode),
R.drawable.ic_repeat,
{
if (player?.repeatMode == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) {
context.getString(R.string.repeat_mode_none)
} else {
context.getString(R.string.repeat_mode_current)
}
},
) {
onRepeatModeClicked()
},
BottomSheetItem(
context.getString(R.string.player_resize_mode),
R.drawable.ic_aspect_ratio,
{
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)
}
},
) {
onResizeModeClicked()
},
BottomSheetItem(
context.getString(R.string.playback_speed),
R.drawable.ic_speed,
{
"${player?.playbackParameters?.speed?.round(2)}x"
},
) {
onPlaybackSpeedClicked()
},
)
playerOptionsInterface?.let {
items.addAll(
listOf(
BottomSheetItem(
context.getString(R.string.quality),
R.drawable.ic_hd,
{ "${player?.videoSize?.height}p" },
) {
it.onQualityClicked()
},
BottomSheetItem(
context.getString(R.string.audio_track),
R.drawable.ic_audio,
{
trackSelector?.parameters?.preferredAudioLanguages?.firstOrNull()
},
) {
it.onAudioStreamClicked()
},
BottomSheetItem(
context.getString(R.string.captions),
R.drawable.ic_caption,
{
if (trackSelector != null && trackSelector!!.parameters.preferredTextLanguages.isNotEmpty()) {
trackSelector!!.parameters.preferredTextLanguages[0]
} else {
context.getString(R.string.none)
}
},
) {
it.onCaptionsClicked()
},
BottomSheetItem(
context.getString(R.string.stats_for_nerds),
R.drawable.ic_info,
) {
it.onStatsClicked()
},
),
)
}
val items = getOptionsMenuItems()
val bottomSheetFragment = BaseBottomSheet().setItems(items, null)
bottomSheetFragment.show(supportFragmentManager, null)
}
}
open fun getOptionsMenuItems(): List<BottomSheetItem> = listOf(
BottomSheetItem(
context.getString(R.string.repeat_mode),
R.drawable.ic_repeat,
{
if (player?.repeatMode == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) {
context.getString(R.string.repeat_mode_none)
} else {
context.getString(R.string.repeat_mode_current)
}
},
) {
onRepeatModeClicked()
},
BottomSheetItem(
context.getString(R.string.player_resize_mode),
R.drawable.ic_aspect_ratio,
{
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)
}
},
) {
onResizeModeClicked()
},
BottomSheetItem(
context.getString(R.string.playback_speed),
R.drawable.ic_speed,
{
"${player?.playbackParameters?.speed?.round(2)}x"
},
) {
onPlaybackSpeedClicked()
},
)
// lock the player
private fun lockPlayer(isLocked: Boolean) {
// isLocked is the current (old) state of the player lock
@ -606,12 +504,14 @@ internal class CustomExoPlayerView(
.show(supportFragmentManager)
}
open fun isFullscreen() = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// add a larger bottom margin to the time bar in landscape mode
val offset = when {
playerViewModel?.isFullscreen?.value ?: (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) -> 20.dpToPx()
isFullscreen() -> 20.dpToPx()
else -> 10.dpToPx()
}
@ -656,17 +556,16 @@ internal class CustomExoPlayerView(
/**
* Add extra margin to the top bar to not overlap the status bar
*/
private fun updateTopBarMargin() {
val margin = when {
resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE -> 10
playerViewModel?.isFullscreen?.value == true -> 20
else -> 0
}
fun updateTopBarMargin() {
binding.topBar.updateLayoutParams<MarginLayoutParams> {
topMargin = margin.dpToPx().toInt()
topMargin = getTopBarMarginDp().dpToPx().toInt()
}
}
open fun getTopBarMarginDp(): Int {
return if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) 10 else 0
}
override fun onSingleTap() {
toggleController()
}
@ -719,7 +618,6 @@ internal class CustomExoPlayerView(
playerGestureController.isMoving = false
(context as? AppCompatActivity)?.onBackPressedDispatcher?.onBackPressed()
playerViewModel?.isFullscreen?.value = false
}
override fun onSwipeEnd() {

View File

@ -0,0 +1,28 @@
package com.github.libretube.ui.views
import android.content.Context
import android.util.AttributeSet
import androidx.core.view.WindowInsetsCompat
import com.github.libretube.ui.extensions.toggleSystemBars
class OfflinePlayerView(
context: Context,
attributeSet: AttributeSet? = null,
): CustomExoPlayerView(context, attributeSet) {
override fun hideController() {
super.hideController()
// hide the status bars when continuing to watch video
activity.toggleSystemBars(WindowInsetsCompat.Type.systemBars(), false)
}
override fun showController() {
super.showController()
// show status bar when showing player options
activity.toggleSystemBars(WindowInsetsCompat.Type.statusBars(), true)
}
override fun getTopBarMarginDp(): Int {
// the offline player requires a bigger top bar margin
return if (isFullscreen()) 18 else super.getTopBarMarginDp()
}
}

View File

@ -0,0 +1,132 @@
package com.github.libretube.ui.views
import android.content.Context
import android.content.res.Configuration
import android.util.AttributeSet
import android.view.View
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.LifecycleOwner
import androidx.media3.exoplayer.trackselection.TrackSelector
import com.github.libretube.R
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.helpers.WindowHelper
import com.github.libretube.obj.BottomSheetItem
import com.github.libretube.ui.extensions.toggleSystemBars
import com.github.libretube.ui.interfaces.OnlinePlayerOptions
import com.github.libretube.ui.models.PlayerViewModel
class OnlinePlayerView(
context: Context,
attributeSet: AttributeSet? = null,
) : CustomExoPlayerView(context, attributeSet) {
private var playerOptions: OnlinePlayerOptions? = null
private var playerViewModel: PlayerViewModel? = null
private var trackSelector: TrackSelector? = null
private var viewLifecycleOwner: LifecycleOwner? = null
var autoplayEnabled = PlayerHelper.autoPlayEnabled
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
override fun getOptionsMenuItems(): List<BottomSheetItem> {
return super.getOptionsMenuItems() +
listOf(
BottomSheetItem(
context.getString(R.string.quality),
R.drawable.ic_hd,
{ "${player?.videoSize?.height}p" },
) {
playerOptions?.onQualityClicked()
},
BottomSheetItem(
context.getString(R.string.audio_track),
R.drawable.ic_audio,
{
trackSelector?.parameters?.preferredAudioLanguages?.firstOrNull()
},
) {
playerOptions?.onAudioStreamClicked()
},
BottomSheetItem(
context.getString(R.string.captions),
R.drawable.ic_caption,
{
if (trackSelector != null && trackSelector!!.parameters.preferredTextLanguages.isNotEmpty()) {
trackSelector!!.parameters.preferredTextLanguages[0]
} else {
context.getString(R.string.none)
}
},
) {
playerOptions?.onCaptionsClicked()
},
BottomSheetItem(
context.getString(R.string.stats_for_nerds),
R.drawable.ic_info,
) {
playerOptions?.onStatsClicked()
},
)
}
fun initPlayerOptions(
playerViewModel: PlayerViewModel,
viewLifecycleOwner: LifecycleOwner,
trackSelector: TrackSelector,
playerOptions: OnlinePlayerOptions
) {
this.playerViewModel = playerViewModel
this.viewLifecycleOwner = viewLifecycleOwner
this.trackSelector = trackSelector
this.playerOptions = playerOptions
playerViewModel.isFullscreen.observe(viewLifecycleOwner) { isFullscreen ->
WindowHelper.toggleFullscreen(activity, isFullscreen)
updateTopBarMargin()
}
setControllerVisibilityListener(
ControllerVisibilityListener { visibility ->
playerViewModel.isFullscreen.value?.let { isFullscreen ->
if (!isFullscreen) return@let
// Show status bar only not navigation bar if the player controls are visible and hide it otherwise
activity.toggleSystemBars(
types = WindowInsetsCompat.Type.statusBars(),
showBars = visibility == View.VISIBLE,
)
}
},
)
binding.autoPlay.isChecked = autoplayEnabled
binding.autoPlay.setOnCheckedChangeListener { _, isChecked ->
autoplayEnabled = isChecked
}
}
override fun hideController() {
super.hideController()
if (playerViewModel?.isFullscreen?.value == true) {
WindowHelper.toggleFullscreen(activity, true)
}
updateTopBarMargin()
}
override fun onSwipeCenterScreen(distanceY: Float) {
super.onSwipeCenterScreen(distanceY)
playerViewModel?.isFullscreen?.value = false
}
override fun getTopBarMarginDp(): Int {
return when {
resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE -> 15
playerViewModel?.isFullscreen?.value == true -> 20
else -> super.getTopBarMarginDp()
}
}
override fun isFullscreen(): Boolean {
return playerViewModel?.isFullscreen?.value ?: super.isFullscreen()
}
}

View File

@ -5,7 +5,7 @@
android:layout_height="match_parent"
android:orientation="vertical">
<com.github.libretube.ui.views.CustomExoPlayerView
<com.github.libretube.ui.views.OfflinePlayerView
android:id="@+id/player"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -27,6 +27,6 @@
android:layout_gravity="center"
android:gravity="center" />
</com.github.libretube.ui.views.CustomExoPlayerView>
</com.github.libretube.ui.views.OfflinePlayerView>
</LinearLayout>

View File

@ -299,7 +299,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.github.libretube.ui.views.CustomExoPlayerView
<com.github.libretube.ui.views.OnlinePlayerView
android:id="@+id/player"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -357,7 +357,7 @@
android:layout_height="match_parent"
android:visibility="gone" />
</com.github.libretube.ui.views.CustomExoPlayerView>
</com.github.libretube.ui.views.OnlinePlayerView>
<ImageView
android:id="@+id/close_imageView"