[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