From af286826dc10c4fe1130dc8e6146f87c5290c47d Mon Sep 17 00:00:00 2001 From: screentinker Date: Fri, 17 Jul 2026 00:01:19 -0500 Subject: [PATCH] fix(android): stop zero-duration widget self-loop pegging the main thread (#198) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A solo fullscreen widget (or image) with duration_sec=0 hit an unclamped scheduleAdvance(item.durationSec * 1000L) in PlaylistController.playCurrentItem, scheduling a 0ms auto-advance. For a single-item playlist next() re-selects the same item, so it re-played every looper tick (~20x/sec) — black-screening the TV and locking the UI (couldn't even reach home). Triggered when a schedule collapses the playlist to a single always-on duration-0 widget. Use slotMs() (the max(1, duration||10) contract shared with the web/Tizen players) so a zero/negative duration floors to 10s. Also floor scheduleAdvance() itself to MIN_ADVANCE_MS (500ms) as a backstop so no future path can busy-loop the main thread. Co-authored-by: Claude Opus 4.8 (1M context) --- .../player/player/PlaylistController.kt | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/java/com/remotedisplay/player/player/PlaylistController.kt b/android/app/src/main/java/com/remotedisplay/player/player/PlaylistController.kt index 3bc0b2d..c4483ed 100644 --- a/android/app/src/main/java/com/remotedisplay/player/player/PlaylistController.kt +++ b/android/app/src/main/java/com/remotedisplay/player/player/PlaylistController.kt @@ -36,7 +36,13 @@ class PlaylistController( // nothing has ever played (fresh device). Never used while content is on screen. private val onWaitingForContent: (() -> Unit)? = null ) { - private companion object { const val CONTENT_RECHECK_MS = 3000L } + private companion object { + const val CONTENT_RECHECK_MS = 3000L + // Backstop against a busy-loop: an auto-advance is never scheduled faster than + // this, so a bad/zero duration on any path can't peg the main thread (see #widget + // zero-duration self-loop — a solo fullscreen widget with duration_sec=0). + const val MIN_ADVANCE_MS = 500L + } private val items = mutableListOf() private var currentIndex = -1 @@ -325,14 +331,19 @@ class PlaylistController( // for the completion callback. Wall followers never auto-advance — the // leader's wall:sync index drives every switch. if (!wallFollower && (item.mimeType.startsWith("image/") || item.isWidget)) { - scheduleAdvance(item.durationSec * 1000L) + // slotMs() floors a zero/negative duration to 10s (the max(1, duration||10) + // contract shared with the web/Tizen players). A raw durationSec*1000 here let a + // solo fullscreen widget with duration_sec=0 schedule a 0ms advance -> self-loop. + scheduleAdvance(slotMs(item)) } } private fun scheduleAdvance(delayMs: Long) { cancelAdvance() + // Backstop: never busy-loop, even if a future caller passes a tiny/zero delay. + val safeDelayMs = maxOf(delayMs, MIN_ADVANCE_MS) advanceRunnable = Runnable { next() } - handler.postDelayed(advanceRunnable!!, delayMs) + handler.postDelayed(advanceRunnable!!, safeDelayMs) } private fun cancelAdvance() {