fix: chapters dont work in audio mode

This commit is contained in:
Bnyro 2024-05-18 21:37:09 +02:00
parent f3049b5118
commit 596230aa61
10 changed files with 129 additions and 77 deletions

View File

@ -45,6 +45,7 @@ import com.github.libretube.obj.PlayerNotificationData
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.interfaces.TimeFrameReceiver
import com.github.libretube.ui.listeners.SeekbarPreviewListener
import com.github.libretube.ui.models.ChaptersViewModel
import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.OfflineTimeFrameReceiver
@ -66,6 +67,7 @@ class OfflinePlayerActivity : BaseActivity() {
private lateinit var playerBinding: ExoStyledPlayerControlViewBinding
private val playerViewModel: PlayerViewModel by viewModels()
private val chaptersViewModel: ChaptersViewModel by viewModels()
private val watchPositionTimer = PauseableTimer(
onTick = this::saveWatchPosition,
@ -177,7 +179,8 @@ class OfflinePlayerActivity : BaseActivity() {
binding.player.initialize(
binding.doubleTapOverlay.binding,
binding.playerGestureControlsView.binding
binding.playerGestureControlsView.binding,
chaptersViewModel
)
nowPlayingNotification = NowPlayingNotification(this, player, NowPlayingNotification.Companion.NowPlayingNotificationType.VIDEO_OFFLINE)
@ -189,7 +192,7 @@ class OfflinePlayerActivity : BaseActivity() {
Database.downloadDao().findById(videoId)
}
val chapters = downloadChapters.map(DownloadChapter::toChapterSegment)
playerViewModel.chaptersLiveData.value = chapters
chaptersViewModel.chaptersLiveData.value = chapters
binding.player.setChapters(chapters)
val downloadFiles = downloadItems.filter { it.path.exists() }

View File

@ -14,7 +14,7 @@ import com.github.libretube.ui.viewholders.ChaptersViewHolder
class ChaptersAdapter(
var chapters: List<ChapterSegment>,
private val videoDuration: Long,
private val videoDurationSeconds: Long,
private val seekTo: (Long) -> Unit
) : RecyclerView.Adapter<ChaptersViewHolder>() {
private var selectedPosition = 0
@ -36,12 +36,11 @@ class ChaptersAdapter(
chapterTitle.text = chapter.title
timeStamp.text = DateUtils.formatElapsedTime(chapter.start)
val playerDurationSeconds = videoDuration / 1000
val chapterEnd = if (chapter.highlightDrawable == null) {
chapters.getOrNull(position + 1)?.start ?: playerDurationSeconds
chapters.getOrNull(position + 1)?.start ?: videoDurationSeconds
} else {
// the duration for chapters is hardcoded, since it's not provided by the SB API
minOf(chapter.start + ChapterSegment.HIGHLIGHT_LENGTH, playerDurationSeconds)
minOf(chapter.start + ChapterSegment.HIGHLIGHT_LENGTH, videoDurationSeconds)
}
val durationSpan = chapterEnd - chapter.start
duration.text = root.context.getString(

View File

@ -31,6 +31,7 @@ import com.github.libretube.extensions.normalize
import com.github.libretube.extensions.seekBy
import com.github.libretube.extensions.toID
import com.github.libretube.extensions.togglePlayPauseState
import com.github.libretube.extensions.updateIfChanged
import com.github.libretube.helpers.AudioHelper
import com.github.libretube.helpers.BackgroundHelper
import com.github.libretube.helpers.ImageHelper
@ -42,6 +43,7 @@ import com.github.libretube.services.OnlinePlayerService
import com.github.libretube.ui.activities.MainActivity
import com.github.libretube.ui.interfaces.AudioPlayerOptions
import com.github.libretube.ui.listeners.AudioPlayerThumbnailListener
import com.github.libretube.ui.models.ChaptersViewModel
import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.ui.sheets.ChaptersBottomSheet
import com.github.libretube.ui.sheets.PlaybackOptionsSheet
@ -61,6 +63,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
private lateinit var audioHelper: AudioHelper
private val mainActivity get() = context as MainActivity
private val viewModel: PlayerViewModel by activityViewModels()
private val chaptersModel: ChaptersViewModel by activityViewModels()
// for the transition
private var transitionStartId = 0
@ -167,12 +170,24 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
)
}
requireActivity().supportFragmentManager.setFragmentResultListener(
ChaptersBottomSheet.SEEK_TO_POSITION_REQUEST_KEY,
viewLifecycleOwner
) { _, bundle ->
playerService?.player?.seekTo(bundle.getLong(IntentData.currentPosition))
}
binding.openChapters.setOnClickListener {
val playerService = playerService ?: return@setOnClickListener
viewModel.chaptersLiveData.value = playerService.streams?.chapters.orEmpty()
chaptersModel.chaptersLiveData.value = playerService.streams?.chapters.orEmpty()
ChaptersBottomSheet()
.show(childFragmentManager)
.apply {
arguments = bundleOf(
IntentData.duration to playerService.player?.duration?.div(1000)
)
}
.show(requireActivity().supportFragmentManager)
}
binding.miniPlayerClose.setOnClickListener {
@ -205,6 +220,8 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
}
if (!PlayerHelper.playAutomatically) updatePlayPauseButton()
updateChapterIndex()
}
private fun killFragment() {
@ -435,4 +452,14 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
binding.volumeTextView.text = "${bar.progress.normalize(0, bar.max, 0, 100)}"
}
private fun updateChapterIndex() {
if (_binding == null) return
handler.postDelayed(this::updateChapterIndex, 100)
val player = playerService?.player ?: return
val currentIndex = PlayerHelper.getCurrentChapterIndex(player.currentPosition, chaptersModel.chapters)
chaptersModel.currentChapterIndex.updateIfChanged(currentIndex ?: return)
}
}

View File

@ -97,6 +97,7 @@ import com.github.libretube.ui.extensions.animateDown
import com.github.libretube.ui.extensions.setupSubscriptionButton
import com.github.libretube.ui.interfaces.OnlinePlayerOptions
import com.github.libretube.ui.listeners.SeekbarPreviewListener
import com.github.libretube.ui.models.ChaptersViewModel
import com.github.libretube.ui.models.CommentsViewModel
import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.ui.sheets.BaseBottomSheet
@ -127,6 +128,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
private val viewModel: PlayerViewModel by activityViewModels()
private val commentsViewModel: CommentsViewModel by activityViewModels()
private val chaptersViewModel: ChaptersViewModel by activityViewModels()
// Video information passed by the intent
private lateinit var videoId: String
@ -467,6 +469,8 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
}
(activity as MainActivity).requestOrientationChange()
}
updateMaxSheetHeight()
}
})
@ -634,7 +638,9 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
}
private fun updateMaxSheetHeight() {
viewModel.maxSheetHeightPx = binding.root.height - binding.player.height
val maxHeight = binding.root.height - binding.player.height
viewModel.maxSheetHeightPx = viewModel.maxSheetHeightPx
chaptersViewModel.maxSheetHeightPx = maxHeight
}
private fun playOnBackground() {
@ -1022,7 +1028,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
@SuppressLint("SetTextI18n")
private fun initializePlayerView() {
// initialize the player view actions
binding.player.initialize(doubleTapOverlayBinding, playerGestureControlsViewBinding)
binding.player.initialize(doubleTapOverlayBinding, playerGestureControlsViewBinding, chaptersViewModel)
binding.player.initPlayerOptions(viewModel, viewLifecycleOwner, trackSelector, this)
binding.descriptionLayout.setStreams(streams)
@ -1040,7 +1046,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
playerBinding.exoTitle.text = streams.title
// init the chapters recyclerview
viewModel.chaptersLiveData.value = streams.chapters
chaptersViewModel.chaptersLiveData.value = streams.chapters
if (PlayerHelper.relatedStreamsEnabled) {
val relatedLayoutManager = binding.relatedRecView.layoutManager as LinearLayoutManager
@ -1132,8 +1138,8 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
start = highlightStart,
highlightDrawable = frame?.toDrawable(requireContext().resources)
)
viewModel.chaptersLiveData.postValue(
viewModel.chapters.plus(highlightChapter).sortedBy { it.start }
chaptersViewModel.chaptersLiveData.postValue(
chaptersViewModel.chapters.plus(highlightChapter).sortedBy { it.start }
)
withContext(Dispatchers.Main) {

View File

@ -0,0 +1,12 @@
package com.github.libretube.ui.models
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.github.libretube.api.obj.ChapterSegment
class ChaptersViewModel: ViewModel() {
val chaptersLiveData = MutableLiveData<List<ChapterSegment>>()
val chapters get() = chaptersLiveData.value.orEmpty()
val currentChapterIndex = MutableLiveData<Int>()
var maxSheetHeightPx = 0
}

View File

@ -10,7 +10,6 @@ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import com.github.libretube.R
import com.github.libretube.api.JsonHelper
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.ChapterSegment
import com.github.libretube.api.obj.Message
import com.github.libretube.api.obj.Segment
import com.github.libretube.api.obj.Streams
@ -48,9 +47,6 @@ class PlayerViewModel : ViewModel() {
var maxSheetHeightPx = 0
val chaptersLiveData = MutableLiveData<List<ChapterSegment>>()
val chapters get() = chaptersLiveData.value.orEmpty()
var sponsorBlockEnabled = PlayerHelper.sponsorBlockEnabled
/**

View File

@ -2,29 +2,32 @@ package com.github.libretube.ui.sheets
import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import androidx.media3.common.util.UnstableApi
import androidx.fragment.app.setFragmentResult
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.BottomSheetBinding
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.ui.adapters.ChaptersAdapter
import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.ui.models.ChaptersViewModel
@UnstableApi
class ChaptersBottomSheet : UndimmedBottomSheet() {
private var _binding: BottomSheetBinding? = null
private val binding get() = _binding!!
private val handler = Handler(Looper.getMainLooper())
private val playerViewModel: PlayerViewModel by activityViewModels()
private val chaptersViewModel: ChaptersViewModel by activityViewModels()
private var duration = 0L
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
duration = requireArguments().getLong(IntentData.duration, 0L)
}
override fun onCreateView(
inflater: LayoutInflater,
@ -35,61 +38,45 @@ class ChaptersBottomSheet : UndimmedBottomSheet() {
return binding.root
}
private val updatePosition = object : Runnable {
override fun run() {
val binding = _binding ?: return
handler.postDelayed(this, 200)
val currentIndex = getCurrentIndex() ?: return
val adapter = binding.optionsRecycler.adapter as ChaptersAdapter
adapter.updateSelectedPosition(currentIndex)
}
}
@SuppressLint("NotifyDataSetChanged")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.optionsRecycler.layoutManager = LinearLayoutManager(context)
val adapter =
ChaptersAdapter(playerViewModel.chapters, playerViewModel.player?.duration ?: 0) {
playerViewModel.player?.seekTo(it)
ChaptersAdapter(chaptersViewModel.chapters, duration) {
setFragmentResult(SEEK_TO_POSITION_REQUEST_KEY, bundleOf(IntentData.currentPosition to it))
}
binding.optionsRecycler.adapter = adapter
binding.optionsRecycler.viewTreeObserver.addOnGlobalLayoutListener(
object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
val currentIndex = getCurrentIndex() ?: return
binding.optionsRecycler.scrollToPosition(currentIndex)
chaptersViewModel.currentChapterIndex.value?.let {
binding.optionsRecycler.scrollToPosition(it)
}
binding.optionsRecycler.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
}
)
chaptersViewModel.currentChapterIndex.observe(viewLifecycleOwner) { currentIndex ->
if (_binding == null) return@observe
adapter.updateSelectedPosition(currentIndex)
}
binding.bottomSheetTitle.text = context?.getString(R.string.chapters)
binding.bottomSheetTitleLayout.isVisible = true
playerViewModel.chaptersLiveData.observe(viewLifecycleOwner) {
chaptersViewModel.chaptersLiveData.observe(viewLifecycleOwner) {
adapter.chapters = it
adapter.notifyDataSetChanged()
}
updatePosition.run()
}
private fun getCurrentIndex(): Int? {
val player = playerViewModel.player ?: return null
return PlayerHelper.getCurrentChapterIndex(
player.currentPosition,
playerViewModel.chapters
)
}
override fun getSheetMaxHeightPx() = playerViewModel.maxSheetHeightPx
override fun getSheetMaxHeightPx() = chaptersViewModel.maxSheetHeightPx
override fun getDragHandle() = binding.dragHandle
@ -99,4 +86,8 @@ class ChaptersBottomSheet : UndimmedBottomSheet() {
super.onDestroyView()
_binding = null
}
companion object {
const val SEEK_TO_POSITION_REQUEST_KEY = "seek_to_position_request_key"
}
}

View File

@ -19,6 +19,7 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.widget.TooltipCompat
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.os.postDelayed
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isGone
@ -26,6 +27,7 @@ import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.view.marginStart
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.media3.common.C
import androidx.media3.common.Player
import androidx.media3.common.text.Cue
@ -36,7 +38,7 @@ import androidx.media3.ui.PlayerView
import androidx.media3.ui.SubtitleView
import androidx.media3.ui.TimeBar
import com.github.libretube.R
import com.github.libretube.api.obj.ChapterSegment
import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.DoubleTapOverlayBinding
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
@ -46,6 +48,7 @@ import com.github.libretube.extensions.normalize
import com.github.libretube.extensions.round
import com.github.libretube.extensions.seekBy
import com.github.libretube.extensions.togglePlayPauseState
import com.github.libretube.extensions.updateIfChanged
import com.github.libretube.helpers.AudioHelper
import com.github.libretube.helpers.BrightnessHelper
import com.github.libretube.helpers.ContextHelper
@ -59,6 +62,7 @@ import com.github.libretube.ui.fragments.PlayerFragment
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.ChaptersViewModel
import com.github.libretube.ui.sheets.BaseBottomSheet
import com.github.libretube.ui.sheets.ChaptersBottomSheet
import com.github.libretube.ui.sheets.PlaybackOptionsSheet
@ -81,6 +85,7 @@ abstract class CustomExoPlayerView(
private lateinit var playerGestureController: PlayerGestureController
private lateinit var brightnessHelper: BrightnessHelper
private lateinit var audioHelper: AudioHelper
private lateinit var chaptersViewModel: ChaptersViewModel
private var doubleTapOverlayBinding: DoubleTapOverlayBinding? = null
private var chaptersBottomSheet: ChaptersBottomSheet? = null
private var scrubbingTimeBar = false
@ -114,10 +119,12 @@ abstract class CustomExoPlayerView(
fun initialize(
doubleTapOverlayBinding: DoubleTapOverlayBinding,
playerGestureControlsViewBinding: PlayerGestureControlsViewBinding
playerGestureControlsViewBinding: PlayerGestureControlsViewBinding,
chaptersViewModel: ChaptersViewModel
) {
this.doubleTapOverlayBinding = doubleTapOverlayBinding
this.gestureViewBinding = playerGestureControlsViewBinding
this.chaptersViewModel = chaptersViewModel
this.playerGestureController = PlayerGestureController(context as BaseActivity, this)
this.brightnessHelper = BrightnessHelper(context as Activity)
this.audioHelper = AudioHelper(context)
@ -216,11 +223,24 @@ abstract class CustomExoPlayerView(
updateCurrentPosition()
activity.supportFragmentManager.setFragmentResultListener(
ChaptersBottomSheet.SEEK_TO_POSITION_REQUEST_KEY,
findViewTreeLifecycleOwner() ?: activity
) { _, bundle ->
player?.seekTo(bundle.getLong(IntentData.currentPosition))
}
// enable the chapters dialog in the player
binding.chapterName.setOnClickListener {
val sheet = chaptersBottomSheet ?: ChaptersBottomSheet().also {
chaptersBottomSheet = it
}
val sheet = chaptersBottomSheet ?: ChaptersBottomSheet()
.apply {
arguments = bundleOf(
IntentData.duration to player?.duration?.div(1000)
)
}
.also {
chaptersBottomSheet = it
}
if (sheet.isVisible) {
sheet.dismiss()
@ -231,9 +251,9 @@ abstract class CustomExoPlayerView(
}
// set the name of the video chapter in the exoPlayerView
fun setCurrentChapterName(forceUpdate: Boolean = false, enqueueNew: Boolean = true) {
val player = player ?: return
val chapters = getChapters()
fun setCurrentChapterName(forceUpdate: Boolean = false, enqueueNew: Boolean = true) {
val player = player ?: return
val chapters = chaptersViewModel.chapters
binding.chapterName.isInvisible = chapters.isEmpty()
@ -246,10 +266,9 @@ abstract class CustomExoPlayerView(
// if the user is scrubbing the time bar, don't update
if (scrubbingTimeBar && !forceUpdate) return
val newChapterName =
PlayerHelper.getCurrentChapterIndex(player.currentPosition, chapters)
?.let { chapters[it].title.trim() }
?: context.getString(R.string.no_chapter)
val currentIndex = PlayerHelper.getCurrentChapterIndex(player.currentPosition, chapters)
val newChapterName = currentIndex?.let { chapters[it].title.trim() } ?: context.getString(R.string.no_chapter)
chaptersViewModel.currentChapterIndex.updateIfChanged(currentIndex ?: return)
// change the chapter name textView text to the chapterName
if (newChapterName != binding.chapterName.text) {
@ -627,8 +646,10 @@ abstract class CustomExoPlayerView(
if (!activity.hasCutout && binding.topBar.marginStart == LANDSCAPE_MARGIN_HORIZONTAL_NONE) return
// add a margin to the top and the bottom bar in landscape mode for notches
val isForcedPortrait = activity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
val horizontalMargin = if (isFullscreen() && !isForcedPortrait) LANDSCAPE_MARGIN_HORIZONTAL else LANDSCAPE_MARGIN_HORIZONTAL_NONE
val isForcedPortrait =
activity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
val horizontalMargin =
if (isFullscreen() && !isForcedPortrait) LANDSCAPE_MARGIN_HORIZONTAL else LANDSCAPE_MARGIN_HORIZONTAL_NONE
listOf(binding.topBar, binding.bottomBar).forEach {
it.updateLayoutParams<MarginLayoutParams> {
@ -784,23 +805,29 @@ abstract class CustomExoPlayerView(
KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
player?.togglePlayPauseState()
}
KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
forward()
}
KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_MEDIA_REWIND -> {
rewind()
}
KeyEvent.KEYCODE_N, KeyEvent.KEYCODE_NAVIGATE_NEXT -> {
PlayingQueue.navigateNext()
}
KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_NAVIGATE_PREVIOUS -> {
PlayingQueue.navigatePrev()
}
KeyEvent.KEYCODE_F -> {
val fragmentManager = ContextHelper.unwrapActivity(context).supportFragmentManager
fragmentManager.fragments.filterIsInstance<PlayerFragment>().firstOrNull()
?.toggleFullscreen()
}
else -> return false
}
@ -826,8 +853,6 @@ abstract class CustomExoPlayerView(
open fun minimizeOrExitPlayer() = Unit
abstract fun getChapters(): List<ChapterSegment>
open fun getWindow(): Window = activity.window
companion object {

View File

@ -31,6 +31,4 @@ class OfflinePlayerView(
this.chapters = chapters
setCurrentChapterName()
}
override fun getChapters(): List<ChapterSegment> = chapters
}

View File

@ -16,7 +16,6 @@ import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.trackselection.TrackSelector
import com.github.libretube.R
import com.github.libretube.api.obj.ChapterSegment
import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.extensions.toID
@ -281,10 +280,6 @@ class OnlinePlayerView(
playerOptions?.exitFullscreen()
}
override fun getChapters(): List<ChapterSegment> {
return playerViewModel?.chapters.orEmpty()
}
override fun onPlaybackEvents(player: Player, events: Player.Events) {
super.onPlaybackEvents(player, events)
updateDisplayedDuration()