mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 22:33:12 -06:00
Play the first item of a playlist on a fresh panel, instead of skipping it
A newly paired panel always learns its playlist BEFORE the media arrives, so start() finds nothing playable and the 3-second content re-check is what really begins playback. updatePlaylist() has already seeded currentIndex = 0 for a playlist that has not started, but the re-check advanced PAST that index — so the first pass ran 1,2,3,0 and item 1 only appeared after the list wrapped. On the emulator, a fresh pair with a 4-item playlist reproduced it every time: Starting playback Playing: red.png (index 1) <- clip32.mp4 (index 0) never got its turn Playing: clip7.mp4 (index 2) Playing: blue.png (index 3) Playing: clip32.mp4 (index 0) <- 54s late, on the second pass On a two-item playlist that is indistinguishable from "only one of the two ever plays", which is how it was reported. The distinction the re-check was missing is hasContentOnScreen. With content up, currentIndex is a real position that has had its turn and the scan must move past it. With nothing up, currentIndex is only where playback INTENDED to start, so skipping it drops that item. PlaylistSelection.recheckIndex now makes that choice explicitly, and playableFromIndex treats a negative index as "no position yet" rather than wrapping onto the last item. Verified on the emulator against the same cold start: the first pass is now 0,1,2,3,4 in order. Playback resume (#234) is untouched — it never reaches the re-check when its target is cached, confirmed by an Activity relaunch resuming mid-playlist as before. Tests: 6 new cases in PlaylistSelectionTest covering both sides of the rule, the still-downloading item, the no-position-yet start, and the empty case. Android 151/151, server 1298/1298. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
This commit is contained in:
parent
2237edab12
commit
5d56e538af
|
|
@ -544,7 +544,10 @@ class PlaylistController(
|
|||
retryRunnable = Runnable {
|
||||
if (isRunning && items.isNotEmpty()) {
|
||||
if (firstActiveIndex() < 0) { showNothingScheduled(); return@Runnable }
|
||||
val idx = PlaylistSelection.nextPlayableIndex(items.size, currentIndex) { playableNow(it) }
|
||||
// Include currentIndex when nothing is on screen: there it is the intended START,
|
||||
// not a position that has had its turn. Skipping it dropped item 1 on every cold
|
||||
// start, because a fresh panel always gets the playlist before the media.
|
||||
val idx = PlaylistSelection.recheckIndex(items.size, currentIndex, hasContentOnScreen) { playableNow(it) }
|
||||
if (idx >= 0) { currentIndex = idx; playCurrentItem() } else onContentNotReady()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,39 @@ object PlaylistSelection {
|
|||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* First playable index AT OR AFTER [from] (wrapping), or -1 if none. A negative [from] means
|
||||
* "no position yet" and starts at 0 rather than wrapping onto the last item.
|
||||
*/
|
||||
fun playableFromIndex(size: Int, from: Int, isPlayable: (Int) -> Boolean): Int {
|
||||
if (size <= 0) return -1
|
||||
val start = if (from < 0) 0 else from % size
|
||||
for (i in 0 until size) {
|
||||
val idx = (start + i) % size
|
||||
if (isPlayable(idx)) return idx
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Which item a content re-check should play once something finally becomes ready.
|
||||
*
|
||||
* [hasContentOnScreen] is the entire distinction, and getting it wrong costs the operator the
|
||||
* first item of their playlist. When content IS up, currentIndex is a real position that has
|
||||
* already had its turn, so the scan must move PAST it. When nothing is up, currentIndex is only
|
||||
* where playback INTENDED to begin — updatePlaylist() seeds it to 0 for a playlist that has not
|
||||
* started yet — so it has never been shown, and advancing past it silently drops item 1 from the
|
||||
* first pass through the playlist.
|
||||
*
|
||||
* That is the cold-start case on every fresh panel: the playlist arrives before its media has
|
||||
* downloaded, start() finds nothing playable, and the re-check three seconds later is what
|
||||
* actually begins playback. On a two-item playlist it looks exactly like "only one of the two
|
||||
* ever plays" until the list wraps.
|
||||
*/
|
||||
fun recheckIndex(size: Int, from: Int, hasContentOnScreen: Boolean, isPlayable: (Int) -> Boolean): Int =
|
||||
if (hasContentOnScreen) nextPlayableIndex(size, from, isPlayable)
|
||||
else playableFromIndex(size, from, isPlayable)
|
||||
|
||||
enum class NonePlayable { KEEP_CURRENT, SHOW_WAITING }
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -57,4 +57,49 @@ class PlaylistSelectionTest {
|
|||
@Test fun `a single downloaded item loops instead of blanking`() {
|
||||
assertEquals(0, PlaylistSelection.nextPlayableIndex(1, 0, readyPredicate(0)))
|
||||
}
|
||||
|
||||
// ===== the cold-start re-check: item 1 of the playlist must not be skipped =====
|
||||
//
|
||||
// Observed on the emulator: a freshly paired panel gets its playlist BEFORE the media has
|
||||
// downloaded, so start() finds nothing playable and the 3-second content re-check is what
|
||||
// actually begins playback. updatePlaylist() has already seeded currentIndex = 0, and the
|
||||
// re-check used to advance PAST it — so a 4-item playlist played 1,2,3,0 on its first pass and
|
||||
// a 2-item playlist looked like "only one of the two ever plays".
|
||||
|
||||
@Test fun `a cold-start re-check begins at the seeded index instead of skipping past it`() {
|
||||
// currentIndex seeded to 0, nothing on screen yet, everything now downloaded.
|
||||
assertEquals("item 0 has never played — it must not be skipped", 0,
|
||||
PlaylistSelection.recheckIndex(4, from = 0, hasContentOnScreen = false, isPlayable = readyPredicate(0, 1, 2, 3)))
|
||||
}
|
||||
|
||||
@Test fun `a re-check with content already on screen still advances past the current item`() {
|
||||
// The other half of the rule: a real position has had its turn, so we must move on.
|
||||
assertEquals(1,
|
||||
PlaylistSelection.recheckIndex(4, from = 0, hasContentOnScreen = true, isPlayable = readyPredicate(0, 1, 2, 3)))
|
||||
}
|
||||
|
||||
@Test fun `a cold-start re-check still skips an item whose content is not downloaded`() {
|
||||
// Item 0 is still downloading; the panel starts on the first item it can actually show.
|
||||
assertEquals(2,
|
||||
PlaylistSelection.recheckIndex(4, from = 0, hasContentOnScreen = false, isPlayable = readyPredicate(2, 3)))
|
||||
}
|
||||
|
||||
@Test fun `a cold-start re-check with no position yet starts at the top, not the last item`() {
|
||||
// currentIndex is -1 before updatePlaylist seeds it; wrapping onto the last item here would
|
||||
// start a fresh panel at the END of its playlist.
|
||||
assertEquals(0,
|
||||
PlaylistSelection.recheckIndex(3, from = -1, hasContentOnScreen = false, isPlayable = readyPredicate(0, 1, 2)))
|
||||
}
|
||||
|
||||
@Test fun `a cold-start re-check returns -1 while nothing is downloaded`() {
|
||||
assertEquals(-1,
|
||||
PlaylistSelection.recheckIndex(3, from = 0, hasContentOnScreen = false, isPlayable = readyPredicate()))
|
||||
}
|
||||
|
||||
@Test fun `playableFromIndex is inclusive of its start and wraps`() {
|
||||
assertEquals(1, PlaylistSelection.playableFromIndex(4, 1, readyPredicate(1, 3)))
|
||||
assertEquals(3, PlaylistSelection.playableFromIndex(4, 2, readyPredicate(1, 3)))
|
||||
assertEquals(1, PlaylistSelection.playableFromIndex(4, 3, readyPredicate(1))) // wraps
|
||||
assertEquals(-1, PlaylistSelection.playableFromIndex(0, 0, readyPredicate(0)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue