mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
feat: add PendingSeekTracker to manage seek requests and improve playback accuracy
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package hu.bbara.purefin.core.player.manager
|
||||
|
||||
import kotlin.math.abs
|
||||
|
||||
internal class PendingSeekTracker(
|
||||
private val settleToleranceMs: Long = 500L
|
||||
) {
|
||||
private var pendingSeek: PendingSeek? = null
|
||||
|
||||
fun currentPosition(playerPositionMs: Long): Long {
|
||||
val pending = pendingSeek ?: return playerPositionMs
|
||||
if (pending.isSatisfiedBy(playerPositionMs, settleToleranceMs)) {
|
||||
pendingSeek = null
|
||||
return playerPositionMs
|
||||
}
|
||||
return pending.targetPositionMs
|
||||
}
|
||||
|
||||
fun recordSeek(basePositionMs: Long, targetPositionMs: Long): Long {
|
||||
val target = targetPositionMs.coerceAtLeast(0L)
|
||||
pendingSeek = PendingSeek(
|
||||
targetPositionMs = target,
|
||||
direction = target.compareTo(basePositionMs)
|
||||
)
|
||||
return target
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
pendingSeek = null
|
||||
}
|
||||
|
||||
private data class PendingSeek(
|
||||
val targetPositionMs: Long,
|
||||
val direction: Int
|
||||
) {
|
||||
fun isSatisfiedBy(playerPositionMs: Long, settleToleranceMs: Long): Boolean =
|
||||
when {
|
||||
direction > 0 -> playerPositionMs >= targetPositionMs - settleToleranceMs
|
||||
direction < 0 -> playerPositionMs <= targetPositionMs + settleToleranceMs
|
||||
else -> abs(playerPositionMs - targetPositionMs) <= settleToleranceMs
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ class PlayerManager @Inject constructor(
|
||||
) {
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
private val pendingSeekTracker = PendingSeekTracker()
|
||||
|
||||
private var currentMediaContext: MediaContext? = null
|
||||
|
||||
@@ -97,6 +98,7 @@ class PlayerManager @Inject constructor(
|
||||
}
|
||||
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
pendingSeekTracker.clear()
|
||||
refreshMetadata(mediaItem)
|
||||
refreshQueue()
|
||||
}
|
||||
@@ -124,6 +126,7 @@ class PlayerManager @Inject constructor(
|
||||
|
||||
fun play(mediaItem: MediaItem, mediaContext: MediaContext? = null, startPositionMs: Long? = null) {
|
||||
currentMediaContext = mediaContext
|
||||
pendingSeekTracker.clear()
|
||||
if (startPositionMs != null) {
|
||||
player.setMediaItem(mediaItem, startPositionMs)
|
||||
} else {
|
||||
@@ -139,6 +142,7 @@ class PlayerManager @Inject constructor(
|
||||
|
||||
fun replaceCurrentMediaItem(mediaItem: MediaItem, mediaContext: MediaContext? = null, startPositionMs: Long? = null) {
|
||||
currentMediaContext = mediaContext
|
||||
pendingSeekTracker.clear()
|
||||
val currentIndex = player.currentMediaItemIndex.takeIf { it != C.INDEX_UNSET } ?: run {
|
||||
play(mediaItem, mediaContext, startPositionMs)
|
||||
return
|
||||
@@ -167,16 +171,17 @@ class PlayerManager @Inject constructor(
|
||||
}
|
||||
|
||||
fun seekTo(positionMs: Long) {
|
||||
player.seekTo(positionMs)
|
||||
requestSeek(positionMs)
|
||||
}
|
||||
|
||||
fun seekBy(deltaMs: Long) {
|
||||
val target = (player.currentPosition + deltaMs).coerceAtLeast(0L)
|
||||
seekTo(target)
|
||||
val basePositionMs = pendingSeekTracker.currentPosition(player.currentPosition)
|
||||
requestSeek(basePositionMs + deltaMs, basePositionMs)
|
||||
}
|
||||
|
||||
fun seekToLiveEdge() {
|
||||
if (player.isCurrentMediaItemLive) {
|
||||
pendingSeekTracker.clear()
|
||||
player.seekToDefaultPosition()
|
||||
player.play()
|
||||
}
|
||||
@@ -184,12 +189,14 @@ class PlayerManager @Inject constructor(
|
||||
|
||||
fun next() {
|
||||
if (player.hasNextMediaItem()) {
|
||||
pendingSeekTracker.clear()
|
||||
player.seekToNextMediaItem()
|
||||
}
|
||||
}
|
||||
|
||||
fun previous() {
|
||||
if (player.hasPreviousMediaItem()) {
|
||||
pendingSeekTracker.clear()
|
||||
player.seekToPreviousMediaItem()
|
||||
}
|
||||
}
|
||||
@@ -251,6 +258,7 @@ class PlayerManager @Inject constructor(
|
||||
val items = _queue.value
|
||||
val targetIndex = items.indexOfFirst { it.id == id }
|
||||
if (targetIndex >= 0) {
|
||||
pendingSeekTracker.clear()
|
||||
player.seekToDefaultPosition(targetIndex)
|
||||
player.playWhenReady = true
|
||||
refreshQueue()
|
||||
@@ -265,12 +273,31 @@ class PlayerManager @Inject constructor(
|
||||
val duration = player.duration.takeIf { it > 0 } ?: _progress.value.durationMs
|
||||
return PlaybackProgressSnapshot(
|
||||
durationMs = duration,
|
||||
positionMs = player.currentPosition,
|
||||
positionMs = pendingSeekTracker.currentPosition(player.currentPosition),
|
||||
bufferedMs = player.bufferedPosition,
|
||||
isLive = player.isCurrentMediaItemLive
|
||||
)
|
||||
}
|
||||
|
||||
private fun requestSeek(positionMs: Long, basePositionMs: Long = pendingSeekTracker.currentPosition(player.currentPosition)) {
|
||||
val targetPositionMs = clampSeekPosition(positionMs)
|
||||
pendingSeekTracker.recordSeek(basePositionMs = basePositionMs, targetPositionMs = targetPositionMs)
|
||||
_progress.update {
|
||||
it.copy(
|
||||
durationMs = player.duration.takeIf { value -> value > 0L } ?: it.durationMs,
|
||||
positionMs = targetPositionMs,
|
||||
bufferedMs = player.bufferedPosition,
|
||||
isLive = player.isCurrentMediaItemLive
|
||||
)
|
||||
}
|
||||
player.seekTo(targetPositionMs)
|
||||
}
|
||||
|
||||
private fun clampSeekPosition(positionMs: Long): Long {
|
||||
val durationMs = player.duration.takeIf { it > 0L } ?: _progress.value.durationMs.takeIf { it > 0L }
|
||||
return durationMs?.let { positionMs.coerceIn(0L, it) } ?: positionMs.coerceAtLeast(0L)
|
||||
}
|
||||
|
||||
private suspend fun applyTrackPreferences() {
|
||||
val context = currentMediaContext ?: return
|
||||
val preferences = trackPreferencesRepository.getMediaPreferences(context.preferenceKey).firstOrNull() ?: return
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package hu.bbara.purefin.core.player.manager
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class PendingSeekTrackerTest {
|
||||
|
||||
@Test
|
||||
fun repeatedForwardSeeksAccumulateWhilePlayerPositionIsStale() {
|
||||
val tracker = PendingSeekTracker()
|
||||
|
||||
val firstBase = tracker.currentPosition(playerPositionMs = 30_000L)
|
||||
tracker.recordSeek(basePositionMs = firstBase, targetPositionMs = 40_000L)
|
||||
|
||||
val secondBase = tracker.currentPosition(playerPositionMs = 30_000L)
|
||||
tracker.recordSeek(basePositionMs = secondBase, targetPositionMs = secondBase + 10_000L)
|
||||
|
||||
assertEquals(50_000L, tracker.currentPosition(playerPositionMs = 30_000L))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun repeatedBackwardSeeksAccumulateWhilePlayerPositionIsStale() {
|
||||
val tracker = PendingSeekTracker()
|
||||
|
||||
val firstBase = tracker.currentPosition(playerPositionMs = 30_000L)
|
||||
tracker.recordSeek(basePositionMs = firstBase, targetPositionMs = 20_000L)
|
||||
|
||||
val secondBase = tracker.currentPosition(playerPositionMs = 30_000L)
|
||||
tracker.recordSeek(basePositionMs = secondBase, targetPositionMs = secondBase - 10_000L)
|
||||
|
||||
assertEquals(10_000L, tracker.currentPosition(playerPositionMs = 30_000L))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pendingForwardSeekClearsOncePlayerCatchesUp() {
|
||||
val tracker = PendingSeekTracker()
|
||||
|
||||
tracker.recordSeek(basePositionMs = 30_000L, targetPositionMs = 40_000L)
|
||||
|
||||
assertEquals(40_000L, tracker.currentPosition(playerPositionMs = 30_000L))
|
||||
assertEquals(39_700L, tracker.currentPosition(playerPositionMs = 39_700L))
|
||||
assertEquals(41_000L, tracker.currentPosition(playerPositionMs = 41_000L))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pendingBackwardSeekClearsOncePlayerCatchesUp() {
|
||||
val tracker = PendingSeekTracker()
|
||||
|
||||
tracker.recordSeek(basePositionMs = 30_000L, targetPositionMs = 20_000L)
|
||||
|
||||
assertEquals(20_000L, tracker.currentPosition(playerPositionMs = 30_000L))
|
||||
assertEquals(20_300L, tracker.currentPosition(playerPositionMs = 20_300L))
|
||||
assertEquals(19_000L, tracker.currentPosition(playerPositionMs = 19_000L))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user