mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 22:03:13 -06:00
Merge branch 'fix/resume-self-advance-on-follower-exit'
This commit is contained in:
commit
0b6907704e
|
|
@ -84,8 +84,36 @@ class PlaylistController(
|
|||
|
||||
val isPlaying: Boolean get() = isRunning && currentIndex >= 0
|
||||
|
||||
/** Video wall: true on followers (suppress auto-advance; leader drives the index). */
|
||||
fun setWallFollower(b: Boolean) { wallFollower = b }
|
||||
/**
|
||||
* Enter/leave follower mode (video wall follower, or group sync). While it is ON no advance
|
||||
* timer is ever armed — playCurrentItem() skips scheduleAdvance() and the wall/group tick owns
|
||||
* the index instead.
|
||||
*
|
||||
* That makes BOTH edges stateful, and both were being ignored:
|
||||
* - leaving, the flag was cleared but nothing re-armed the timer for the item already on
|
||||
* screen, so the playlist sat on one frame forever (until the app restarted). Unchecking
|
||||
* "sync" on a group froze every member showing an image.
|
||||
* - entering, a timer armed by the previous playCurrentItem() stayed live and would fire a
|
||||
* next() that fights the tick for the index.
|
||||
*
|
||||
* Video is exempt on the way out: it advances from its completion callback, which onVideoComplete
|
||||
* already gates on this same flag.
|
||||
*/
|
||||
fun setWallFollower(b: Boolean) {
|
||||
val was = wallFollower
|
||||
wallFollower = b
|
||||
if (was == b) return
|
||||
if (b) { cancelAdvance(); return }
|
||||
val item = currentItem ?: return
|
||||
val delay = FollowerExit.resumeDelayMs(
|
||||
isRunning = isRunning,
|
||||
isImageOrWidget = item.mimeType.startsWith("image/") || item.isWidget,
|
||||
slotMs = slotMs(item),
|
||||
elapsedMs = System.currentTimeMillis() - itemStartedAt
|
||||
) ?: return
|
||||
Log.i("PlaylistController", "follower mode off — resuming self-advance in ${delay}ms")
|
||||
scheduleAdvance(delay)
|
||||
}
|
||||
|
||||
/** Current playlist index (-1 if nothing is playing). */
|
||||
fun getIndex(): Int = currentIndex
|
||||
|
|
@ -496,3 +524,26 @@ class PlaylistController(
|
|||
return out
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure decision for re-arming self-advance when follower mode (video wall / group sync) is turned
|
||||
* off, extracted so it's unit-testable without a Handler or Android runtime — same seam pattern as
|
||||
* TierLogic / ManagedLogic.
|
||||
*
|
||||
* Returns the delay to schedule, or null when nothing should be armed. The remaining slot is
|
||||
* measured from when the item actually started, so leaving sync 8s into a 10s image advances in
|
||||
* ~2s rather than restarting the full duration. A slot that already elapsed yields 0 and the
|
||||
* caller's MIN_ADVANCE_MS backstop keeps it off a busy loop.
|
||||
*/
|
||||
object FollowerExit {
|
||||
fun resumeDelayMs(
|
||||
isRunning: Boolean,
|
||||
isImageOrWidget: Boolean,
|
||||
slotMs: Long,
|
||||
elapsedMs: Long
|
||||
): Long? {
|
||||
if (!isRunning) return null // nothing is playing; playCurrentItem() will arm it
|
||||
if (!isImageOrWidget) return null // video advances from its completion callback
|
||||
return (slotMs - elapsedMs).coerceAtLeast(0L)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
package com.remotedisplay.player.player
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Leaving follower mode (video wall follower, or group sync) has to re-arm self-advance.
|
||||
*
|
||||
* While follower mode is on, playCurrentItem() never calls scheduleAdvance() — the wall/group tick
|
||||
* owns the index. So clearing the flag is not enough: the item on screen has NOTHING scheduled to
|
||||
* move it. Unchecking "sync" on a group left every member showing an image frozen on one frame
|
||||
* until the app was restarted, which is exactly what a 30-frame sample of a real panel showed —
|
||||
* one unique frame, indefinitely.
|
||||
*/
|
||||
class FollowerExitTest {
|
||||
|
||||
private val TEN_SEC = 10_000L
|
||||
|
||||
@Test fun THE_BUG_an_image_mid_slot_gets_the_remaining_time() {
|
||||
// 8s into a 10s image: advance in ~2s, don't restart the whole slot.
|
||||
assertEquals(2_000L, FollowerExit.resumeDelayMs(
|
||||
isRunning = true, isImageOrWidget = true, slotMs = TEN_SEC, elapsedMs = 8_000L))
|
||||
}
|
||||
|
||||
@Test fun THE_BUG_an_image_whose_slot_already_elapsed_advances_immediately() {
|
||||
// The freeze case: sync held this item well past its duration. 0 is honest; the caller's
|
||||
// MIN_ADVANCE_MS backstop keeps it off a busy loop.
|
||||
assertEquals(0L, FollowerExit.resumeDelayMs(
|
||||
isRunning = true, isImageOrWidget = true, slotMs = TEN_SEC, elapsedMs = 45_000L))
|
||||
}
|
||||
|
||||
@Test fun a_freshly_started_image_gets_its_full_slot() {
|
||||
assertEquals(TEN_SEC, FollowerExit.resumeDelayMs(
|
||||
isRunning = true, isImageOrWidget = true, slotMs = TEN_SEC, elapsedMs = 0L))
|
||||
}
|
||||
|
||||
@Test fun a_widget_is_time_driven_like_an_image() {
|
||||
assertEquals(3_000L, FollowerExit.resumeDelayMs(
|
||||
isRunning = true, isImageOrWidget = true, slotMs = TEN_SEC, elapsedMs = 7_000L))
|
||||
}
|
||||
|
||||
@Test fun video_is_left_alone_because_it_advances_on_completion() {
|
||||
// onVideoComplete() -> next() already handles this, and arming a timer would cut the clip.
|
||||
assertNull(FollowerExit.resumeDelayMs(
|
||||
isRunning = true, isImageOrWidget = false, slotMs = TEN_SEC, elapsedMs = 8_000L))
|
||||
}
|
||||
|
||||
@Test fun nothing_is_armed_when_the_playlist_is_not_running() {
|
||||
// playCurrentItem() will arm it when playback actually starts.
|
||||
assertNull(FollowerExit.resumeDelayMs(
|
||||
isRunning = false, isImageOrWidget = true, slotMs = TEN_SEC, elapsedMs = 1_000L))
|
||||
}
|
||||
|
||||
@Test fun the_delay_is_never_negative() {
|
||||
for (elapsed in listOf(10_001L, 60_000L, Long.MAX_VALUE / 2)) {
|
||||
val d = FollowerExit.resumeDelayMs(true, true, TEN_SEC, elapsed)!!
|
||||
assert(d >= 0L) { "delay must never be negative, got $d for elapsed=$elapsed" }
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue