From bfc007f24fa21bc16a7b6a4ebde120c0dae33a39 Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Fri, 1 May 2026 20:06:21 +0200 Subject: [PATCH] feat(player): batch relative seek requests --- .../player/viewmodel/PlayerViewModel.kt | 11 ++++- .../player/viewmodel/SeekByCollector.kt | 48 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/hu/bbara/purefin/player/viewmodel/SeekByCollector.kt diff --git a/core/src/main/java/hu/bbara/purefin/player/viewmodel/PlayerViewModel.kt b/core/src/main/java/hu/bbara/purefin/player/viewmodel/PlayerViewModel.kt index 4edfff77..c3fbc9a8 100644 --- a/core/src/main/java/hu/bbara/purefin/player/viewmodel/PlayerViewModel.kt +++ b/core/src/main/java/hu/bbara/purefin/player/viewmodel/PlayerViewModel.kt @@ -49,6 +49,9 @@ class PlayerViewModel @Inject constructor( val controlsVisible: StateFlow = _controlsVisible.asStateFlow() private val controlsAutoHidePolicy = ControlsAutoHidePolicy(DEFAULT_CONTROLS_AUTO_HIDE_MS) + private val seekByCollector = SeekByCollector(viewModelScope) { deltaMs -> + playerManager.seekBy(deltaMs) + } private var autoHideJob: Job? = null private var dataErrorMessage: String? = null @@ -191,18 +194,21 @@ class PlayerViewModel @Inject constructor( } fun seekTo(positionMs: Long) { + seekByCollector.clear() playerManager.seekTo(positionMs) } fun seekBy(deltaMs: Long) { - playerManager.seekBy(deltaMs) + seekByCollector.seekBySoon(deltaMs) } fun seekToLiveEdge() { + seekByCollector.clear() playerManager.seekToLiveEdge() } fun skipActiveSegment() { + seekByCollector.clear() playerManager.skipActiveSegment() } @@ -240,11 +246,13 @@ class PlayerViewModel @Inject constructor( } fun next(autoHideDelayMs: Long = DEFAULT_CONTROLS_AUTO_HIDE_MS) { + seekByCollector.clear() playerManager.next() showControls(autoHideDelayMs) } fun previous(autoHideDelayMs: Long = DEFAULT_CONTROLS_AUTO_HIDE_MS) { + seekByCollector.clear() playerManager.previous() showControls(autoHideDelayMs) } @@ -258,6 +266,7 @@ class PlayerViewModel @Inject constructor( } fun playQueueItem(id: String) { + seekByCollector.clear() playerManager.play(id.toUuidOrNull() ?: return) showControls() } diff --git a/core/src/main/java/hu/bbara/purefin/player/viewmodel/SeekByCollector.kt b/core/src/main/java/hu/bbara/purefin/player/viewmodel/SeekByCollector.kt new file mode 100644 index 00000000..2f6a5aa1 --- /dev/null +++ b/core/src/main/java/hu/bbara/purefin/player/viewmodel/SeekByCollector.kt @@ -0,0 +1,48 @@ +package hu.bbara.purefin.player.viewmodel + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class SeekByCollector( + private val scope: CoroutineScope, + private val intervalMs: Long = 120, + private val seekBy: (Long) -> Unit, +) { + private var pendingSeekMs = 0L + private var flushJob: Job? = null + + fun seekBySoon(deltaMs: Long) { + pendingSeekMs += deltaMs + if (flushJob?.isActive != true) { + scheduleFlush() + } + } + + fun clear() { + pendingSeekMs = 0 + flushJob?.cancel() + flushJob = null + } + + private fun scheduleFlush() { + flushJob = scope.launch { + delay(intervalMs) + flush() + } + } + + private fun flush() { + val deltaMs = pendingSeekMs + pendingSeekMs = 0 + if (deltaMs != 0L) { + seekBy(deltaMs) + } + if (pendingSeekMs != 0L) { + scheduleFlush() + } else { + flushJob = null + } + } +}