diff --git a/application/vlc-android/src/org/videolan/vlc/MediaSessionCallback.kt b/application/vlc-android/src/org/videolan/vlc/MediaSessionCallback.kt index 2e82ef82b..56eb4910d 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@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@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 { + 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 { + 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 +}