mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
replace only the queue items a delete actually changed
Deleting any download re-set the whole queue window, which makes the player re-prepare the current item and audibly pauses. It now rebuilds only the items whose local file appeared or went away, comparing what the window reports with what the repository can resolve, and falls back to the full reload when the window no longer lines up.
This commit is contained in:
@@ -95,6 +95,20 @@ class Media3PlayerHandle(
|
||||
|
||||
override fun clear() = controller.clearMediaItems()
|
||||
|
||||
override fun itemUris(): List<String?> =
|
||||
(0 until controller.mediaItemCount).map {
|
||||
controller
|
||||
.getMediaItemAt(it)
|
||||
.localConfiguration
|
||||
?.uri
|
||||
?.toString()
|
||||
}
|
||||
|
||||
override fun replaceItem(
|
||||
index: Int,
|
||||
item: QueueItem,
|
||||
) = controller.replaceMediaItem(index, toMediaItem(item))
|
||||
|
||||
override fun seekToIndex(index: Int) {
|
||||
Log.d(TAG, "seekToIndex($index) playerDuration=${controller.duration} state=${controller.playbackState}")
|
||||
controller.seekTo(index, 0)
|
||||
|
||||
@@ -959,7 +959,30 @@ class PlaybackController(
|
||||
|
||||
fun refreshMediaItems() {
|
||||
scope.launch {
|
||||
startLock.withLock { reloadWindowLocked() }
|
||||
startLock.withLock {
|
||||
val player = player ?: return@withLock
|
||||
val snapshot = snapshot ?: return@withLock
|
||||
if (snapshot.size == 0L || player.isEnded) return@withLock
|
||||
downloads.awaitLoaded()
|
||||
val position = (windowStart + player.currentIndex).coerceIn(0, snapshot.size - 1)
|
||||
val window = queueRepository.window(snapshot, position, QUEUE_WINDOW_RADIUS)
|
||||
val uris = player.itemUris()
|
||||
if (window.size != player.itemCount || uris.size != window.size) {
|
||||
reloadWindowLocked()
|
||||
return@withLock
|
||||
}
|
||||
// Only an item whose local file appeared or went away needs rebuilding. Re-setting
|
||||
// the whole window would re-prepare the player and audibly pause playback.
|
||||
var replaced = false
|
||||
window.forEachIndexed { index, entry ->
|
||||
val local = downloads.localUri(entry.item.song.id) != null
|
||||
if (local != (uris[index]?.startsWith(FILE_SCHEME) == true)) {
|
||||
player.replaceItem(index, entry.item.toQueueItem())
|
||||
replaced = true
|
||||
}
|
||||
}
|
||||
if (replaced) refresh(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1249,5 +1272,6 @@ class PlaybackController(
|
||||
const val WINDOW_SHIFT_DELAY_MS = 400L
|
||||
const val RESTART_THRESHOLD_MS = 3_000L
|
||||
const val SEED_PIN_TRIES = 1_000_000
|
||||
const val FILE_SCHEME = "file:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,15 @@ interface PlayerHandle {
|
||||
|
||||
fun clear()
|
||||
|
||||
/** The resolved URI of each item in the current window, in window order. */
|
||||
fun itemUris(): List<String?> = emptyList()
|
||||
|
||||
/** Rebuilds one window item in place, without re-preparing the playing one. */
|
||||
fun replaceItem(
|
||||
index: Int,
|
||||
item: QueueItem,
|
||||
) = Unit
|
||||
|
||||
fun seekToIndex(index: Int)
|
||||
|
||||
fun seekTo(positionMs: Long)
|
||||
|
||||
@@ -61,6 +61,20 @@ class FakePlayerHandle : PlayerHandle {
|
||||
|
||||
override val currentItem: QueueItem? get() = items.getOrNull(index)
|
||||
|
||||
/** Overrides the URIs the window reports; defaults to a stream URI per item. */
|
||||
@Volatile
|
||||
var uris: List<String> = emptyList()
|
||||
|
||||
override fun itemUris(): List<String?> = uris.ifEmpty { items.map { "http://stream/${it.id}" } }
|
||||
|
||||
override fun replaceItem(
|
||||
index: Int,
|
||||
item: QueueItem,
|
||||
) {
|
||||
operations += "replaceItem($index, ${item.id})"
|
||||
if (index in items.indices) items[index] = item
|
||||
}
|
||||
|
||||
override fun setWindow(
|
||||
items: List<QueueItem>,
|
||||
startIndex: Int,
|
||||
|
||||
@@ -2300,7 +2300,7 @@ class PlaybackControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun refreshingMediaItemsRebuildsTheWindowAtTheSamePosition() {
|
||||
fun refreshingMediaItemsLeavesAnUnaffectedQueueAlone() {
|
||||
seedAlbum(3, sourceId = 1)
|
||||
controller.playAlbum(1, "al1", 0)
|
||||
await { handle.operations.contains("play") }
|
||||
@@ -2308,12 +2308,28 @@ class PlaybackControllerTest {
|
||||
val windows = handle.operations.count { it.startsWith("setWindow") }
|
||||
|
||||
controller.refreshMediaItems()
|
||||
Thread.sleep(200)
|
||||
|
||||
await { handle.operations.count { it.startsWith("setWindow") } > windows }
|
||||
assertEquals(windows, handle.operations.count { it.startsWith("setWindow") })
|
||||
assertEquals("s1", handle.currentItem?.id)
|
||||
assertEquals(42_000L, handle.positionMs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun refreshingMediaItemsReplacesOnlyAnItemWhoseLocalFileIsGone() {
|
||||
seedAlbum(3, sourceId = 1)
|
||||
controller.playAlbum(1, "al1", 0)
|
||||
await { handle.operations.contains("play") }
|
||||
val windows = handle.operations.count { it.startsWith("setWindow") }
|
||||
handle.uris = handle.items.mapIndexed { index, item -> if (index == 0) "file:///gone" else "http://stream/${item.id}" }
|
||||
|
||||
controller.refreshMediaItems()
|
||||
|
||||
await { handle.operations.any { it.startsWith("replaceItem(0,") } }
|
||||
assertEquals(windows, handle.operations.count { it.startsWith("setWindow") })
|
||||
assertEquals(0, handle.operations.count { it.startsWith("replaceItem(1,") })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun positionTickerAdvancesPositionWithoutEmittingPlaybackState() =
|
||||
runBlocking {
|
||||
|
||||
Reference in New Issue
Block a user