[Android] Add support for jumping to chapters and bookmarks in podcast mode
Robert Stone
git at videolan.org
Wed Nov 20 06:47:54 UTC 2024
vlc-android | branch: master | Robert Stone <rhstone at gmail.com> | Wed Aug 7 21:56:07 2024 -0700| [ef65d49c7e088f9a6b9adf19a9d07c8fa4acbeb9] | committer: Duncan McNamara
Add support for jumping to chapters and bookmarks in podcast mode
> https://code.videolan.org/videolan/vlc-android/commit/ef65d49c7e088f9a6b9adf19a9d07c8fa4acbeb9
---
.../src/org/videolan/vlc/MediaSessionCallback.kt | 67 ++++++++++++++++++++--
1 file changed, 62 insertions(+), 5 deletions(-)
diff --git a/application/vlc-android/src/org/videolan/vlc/MediaSessionCallback.kt b/application/vlc-android/src/org/videolan/vlc/MediaSessionCallback.kt
index 2e82ef82be..56eb4910db 100644
--- a/application/vlc-android/src/org/videolan/vlc/MediaSessionCallback.kt
+++ b/application/vlc-android/src/org/videolan/vlc/MediaSessionCallback.kt
@@ -50,10 +50,13 @@ import org.videolan.tools.retrieveParent
import org.videolan.vlc.gui.helpers.MediaComparators
import org.videolan.vlc.media.MediaSessionBrowser
import org.videolan.vlc.util.PlaybackAction
+import org.videolan.vlc.util.TextUtils
import org.videolan.vlc.util.VoiceSearchParams
import org.videolan.vlc.util.awaitMedialibraryStarted
+import org.videolan.vlc.util.mergeSorted
import java.security.SecureRandom
import kotlin.math.abs
+import kotlin.math.absoluteValue
import kotlin.math.min
private const val TAG = "VLC/MediaSessionCallback"
@@ -116,8 +119,8 @@ internal class MediaSessionCallback(private val playbackService: PlaybackService
if (!prevActionSeek) {
val enabledActions = playbackService.enabledActions
when (keyEvent.keyCode) {
- KeyEvent.KEYCODE_MEDIA_NEXT -> if (enabledActions.contains(PlaybackAction.ACTION_SKIP_TO_NEXT)) onSkipToNext()
- KeyEvent.KEYCODE_MEDIA_PREVIOUS -> if (enabledActions.contains(PlaybackAction.ACTION_SKIP_TO_PREVIOUS)) onSkipToPrevious()
+ KeyEvent.KEYCODE_MEDIA_NEXT -> onSkipToNext()
+ KeyEvent.KEYCODE_MEDIA_PREVIOUS -> onSkipToPrevious()
}
}
prevActionSeek = false
@@ -128,6 +131,52 @@ internal class MediaSessionCallback(private val playbackService: PlaybackService
return super.onMediaButtonEvent(mediaButtonEvent)
}
+ private fun jumpToTimelineEntry(previous: Boolean) {
+ val ctx = playbackService.applicationContext
+ playbackService.lifecycleScope.launch {
+ val currentTime = playbackService.getTime()
+ val entryList = getChapterList().toMutableList().apply {
+ mergeSorted(getBookmarkList()) { it.time }
+ }
+ if (entryList.isEmpty()) {
+ playbackService.displaySubtitleMessage(ctx.getString(R.string.no_bookmark))
+ return at launch
+ }
+ val index = entryList.binarySearchBy(currentTime) { it.time }
+ var eIndex = when {
+ index >= 0 -> index + if (previous) -1 else 1
+ else -> -index - if (previous) 2 else 1
+ }.coerceIn(entryList.indices)
+ // Point to the previous element if the time difference is less than 5 seconds
+ if (previous && (currentTime - entryList[eIndex].time) < 5_000L) {
+ eIndex = (eIndex - 1).coerceIn(entryList.indices)
+ }
+ // Ignore button press if within 2 seconds of the first or last entry
+ if ((eIndex == 0 || eIndex == entryList.size - 1) && (currentTime - entryList[eIndex].time).absoluteValue <= 2_000L)
+ return at launch
+ // Seek to the correct time
+ if ((!previous && entryList[eIndex].time >= currentTime) || (previous && entryList[eIndex].time <= currentTime)) {
+ seek(entryList[eIndex].time)
+ playbackService.displaySubtitleMessage("${ctx.getString(R.string.jump_to)} ${entryList[eIndex].name}")
+ }
+ }
+ }
+
+ private fun getChapterList(): List<TimelineEntry> {
+ val ctx = playbackService.applicationContext
+ return playbackService.getChapters(-1)?.mapIndexed { index, item ->
+ TimelineEntry(0, item.timeOffset, TextUtils.formatChapterTitle(ctx, index + 1, item.name))
+ } ?: emptyList()
+ }
+
+ private fun getBookmarkList(): List<TimelineEntry> {
+ return playbackService.currentMediaWrapper?.bookmarks?.map { bookmark ->
+ TimelineEntry(1, bookmark.time, bookmark.title)
+ } ?: emptyList()
+ }
+
+ data class TimelineEntry(val type: Int, val time: Long, val name: String)
+
/**
* The following two functions are based on the following KeyEvent captures. They may need to be updated if the behavior changes in the future.
*
@@ -378,9 +427,17 @@ internal class MediaSessionCallback(private val playbackService: PlaybackService
override fun onStop() = playbackService.stop()
- override fun onSkipToNext() = playbackService.next()
+ override fun onSkipToNext() = when {
+ playbackService.isPodcastMode -> jumpToTimelineEntry(false)
+ playbackService.hasNext() -> playbackService.next()
+ else -> {}
+ }
- override fun onSkipToPrevious() = playbackService.previous(false)
+ override fun onSkipToPrevious() = when {
+ playbackService.isPodcastMode -> jumpToTimelineEntry(true)
+ playbackService.hasPrevious() -> playbackService.previous(false)
+ else -> {}
+ }
override fun onSeekTo(pos: Long) = seek(if (pos < 0) playbackService.getTime() + pos else pos)
@@ -398,4 +455,4 @@ internal class MediaSessionCallback(private val playbackService: PlaybackService
override fun onSetPlaybackSpeed(speed: Float) = playbackService.setRate(speed.coerceIn(0.5f, 2.0f), false)
-}
\ No newline at end of file
+}
More information about the Android
mailing list