From 28cc54e8d1f930582d41fd97e0fc4dc8ab9316bf Mon Sep 17 00:00:00 2001 From: Pauljd Date: Fri, 6 Nov 2026 17:10:55 +0000 Subject: [PATCH] Picture hands over to full screen; shorter squeeze-back for shows Port of release 0.108.64. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/main/java/tv/orbit/classic/GuidePicture.kt | 10 ++++++++-- .../java/tv/orbit/classic/SampleChannelScreens.kt | 4 +++- .../src/main/java/tv/orbit/classic/SqueezeBack.kt | 13 ++++++++++--- .../test/java/tv/orbit/classic/SqueezeBackTest.kt | 8 ++++++++ 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/android/app/src/main/java/tv/orbit/classic/GuidePicture.kt b/android/app/src/main/java/tv/orbit/classic/GuidePicture.kt index e95e1c6..724f1a8 100644 --- a/android/app/src/main/java/tv/orbit/classic/GuidePicture.kt +++ b/android/app/src/main/java/tv/orbit/classic/GuidePicture.kt @@ -29,6 +29,12 @@ import kotlinx.coroutines.withContext internal class LiveStream(val channelNumber: Int, val startMs: Long, val plan: EmbyPlaybackPlan, private val player: ExoPlayer) { private var claimed = false val available get() = !claimed + /** + * Handed to full screen. The picture then leaves it running even though full screen, which opens in its own window, + * only claims it a moment later (Paul, 6 November 2026: choosing full screen still re-tuned, as the picture let go of + * the stream before full screen had taken it). + */ + var handedOff = false fun claim(): ExoPlayer? = if (claimed) null else player.also { claimed = true } fun discard(session: EmbySampleSession) { claim()?.let { closeLiveStream(session, plan, it) } } } @@ -39,7 +45,7 @@ internal class LiveHandoff { /** Call before going full screen. The picture then leaves its stream running for the player to take. */ fun handOver(channelNumber: Int) { android.util.Log.i("OrbitSchedule", "picture handover channel=$channelNumber pictureOpen=${open != null} sameChannel=${open?.channelNumber == channelNumber}") - handed = open?.takeIf { it.channelNumber == channelNumber }; open = null + handed = open?.takeIf { it.channelNumber == channelNumber }?.also { it.handedOff = true }; open = null } fun take(): LiveStream? = handed.also { handed = null } /** Left by the full-screen player on the way out, for the picture to carry on with. */ @@ -164,7 +170,7 @@ private fun GuideLivePlayer(session: EmbySampleSession, plan: EmbyPlaybackPlan, player.removeListener(listener) if (handoff.open === stream) handoff.open = null // Handed to the full-screen player, which owns it from here. - if (handoff.handed !== stream && stream.available) stream.discard(session) + if (!stream.handedOff && stream.available) stream.discard(session) } } LaunchedEffect(player, ready) { diff --git a/android/app/src/main/java/tv/orbit/classic/SampleChannelScreens.kt b/android/app/src/main/java/tv/orbit/classic/SampleChannelScreens.kt index 0004bec..99fee13 100644 --- a/android/app/src/main/java/tv/orbit/classic/SampleChannelScreens.kt +++ b/android/app/src/main/java/tv/orbit/classic/SampleChannelScreens.kt @@ -56,6 +56,8 @@ internal fun ScheduledChannelPlayer(session: EmbySampleSession, channel: SampleC if (stream != null && stream.channelNumber == channel.number && stream.startMs == current.startMs && clockOffsetMs == 0L) stream else { stream?.discard(session); null } } + // A handed stream full screen didn't end up using is closed with it, so it never plays on unseen. + DisposableEffect(Unit) { onDispose { handed?.discard(session) } } val next = shows[1] val identsOn = remember { OrbitSettings(settingsContext).idents } val specialOn = remember { OrbitSettings(settingsContext).specialIdents } @@ -66,7 +68,7 @@ internal fun ScheduledChannelPlayer(session: EmbySampleSession, channel: SampleC // The squeeze-back (Paul, 6 November 2026): over the last minute, with the next programme loading from its start. val squeezeOn = remember { OrbitSettings(settingsContext).squeezeBack } val calmMotion = remember { OrbitSettings(settingsContext).reduceMotion } - val squeezing = squeezeOn && channel is ScheduledChannel && squeezeBackDue(channel.number, current.startMs, current.endMs, next.startMs, now, if (identsHere) identMs else 0L, clockOffsetMs) + val squeezing = squeezeOn && channel is ScheduledChannel && squeezeBackDue(channel.number, current.startMs, current.endMs, next.startMs, now, if (identsHere) identMs else 0L, clockOffsetMs, squeezeLength(current.media.item.type)) val squeeze by androidx.compose.animation.core.animateFloatAsState(if (squeezing) 1f else 0f, if (calmMotion) androidx.compose.animation.core.snap() else androidx.compose.animation.core.tween(1_000), label = "squeeze") val phase = if (identsHere) identPhase(current.startMs, current.endMs, now, identMs, if (squeezeOn) SQUEEZE_MS + IDENT_LOAD_AHEAD_MS else IDENT_LOAD_AHEAD_MS) else IdentPhase.None diff --git a/android/app/src/main/java/tv/orbit/classic/SqueezeBack.kt b/android/app/src/main/java/tv/orbit/classic/SqueezeBack.kt index c7329a9..0c5c056 100644 --- a/android/app/src/main/java/tv/orbit/classic/SqueezeBack.kt +++ b/android/app/src/main/java/tv/orbit/classic/SqueezeBack.kt @@ -31,12 +31,19 @@ internal const val SQUEEZE_MS = 60_000L internal const val SQUEEZE_SHRINK = .46f private const val SQUEEZE_SHORTEST_MS = 10 * 60_000L -/** Whether the squeeze-back is showing now: the minute before the ident (or before the end, with no ident). */ -internal fun squeezeBackDue(number: Int, startMs: Long, endMs: Long, nextStartMs: Long, nowMs: Long, identMs: Long, offsetMs: Long): Boolean { +/** + * How long the squeeze-back lasts (Paul, 6 November 2026): a TV show's end goes small for its last 22 seconds, a film's for + * its last minute, as films have longer credits. + */ +internal const val SQUEEZE_SHOW_MS = 22_000L +internal fun squeezeLength(type: String) = if (type == "Movie") SQUEEZE_MS else SQUEEZE_SHOW_MS + +/** Whether the squeeze-back is showing now: the [lengthMs] before the ident (or before the end, with no ident). */ +internal fun squeezeBackDue(number: Int, startMs: Long, endMs: Long, nextStartMs: Long, nowMs: Long, identMs: Long, offsetMs: Long, lengthMs: Long = SQUEEZE_MS): Boolean { if (offsetMs != 0L || number == DRIVE_IN_NUMBER || endMs - startMs < SQUEEZE_SHORTEST_MS || nextStartMs != endMs) return false if (number == TOTS_NUMBER && totsMood(nowMs).evening) return false val until = endMs - identMs - return nowMs >= until - SQUEEZE_MS && nowMs < until + return nowMs >= until - lengthMs && nowMs < until } /** Each channel's cast member in the squeeze-back: Ollie on Tots, the others shared round the rest. */ diff --git a/android/app/src/test/java/tv/orbit/classic/SqueezeBackTest.kt b/android/app/src/test/java/tv/orbit/classic/SqueezeBackTest.kt index bc5c923..5c83c73 100644 --- a/android/app/src/test/java/tv/orbit/classic/SqueezeBackTest.kt +++ b/android/app/src/test/java/tv/orbit/classic/SqueezeBackTest.kt @@ -45,4 +45,12 @@ class SqueezeBackTest { assertNull(squeezeCast(TOTS_NUMBER)); assertNotNull(squeezeCast(106)) assertEquals(squeezeCast(106), squeezeCast(106)) } + + @Test fun aShowGoesSmallForItsLast22SecondsAFilmForItsLastMinute() { + assertEquals(SQUEEZE_SHOW_MS, squeezeLength("Episode")); assertEquals(SQUEEZE_MS, squeezeLength("Movie")) + val start = 0L; val end = 30 * 60_000L + assertFalse(squeezeBackDue(106, start, end, end, end - 30_000, 0, 0, squeezeLength("Episode"))) + assertTrue(squeezeBackDue(106, start, end, end, end - 20_000, 0, 0, squeezeLength("Episode"))) + assertTrue(squeezeBackDue(106, start, end, end, end - 50_000, 0, 0, squeezeLength("Movie"))) + } }