diff --git a/android/app/src/main/java/tv/orbit/classic/OrbitCompanion.kt b/android/app/src/main/java/tv/orbit/classic/OrbitCompanion.kt index a4c21ed..04b6f48 100644 --- a/android/app/src/main/java/tv/orbit/classic/OrbitCompanion.kt +++ b/android/app/src/main/java/tv/orbit/classic/OrbitCompanion.kt @@ -139,6 +139,8 @@ internal object OrbitCompanion { catchUpShelves(screen.channels(), nowMs).flatMap { it.entries }.firstOrNull { it.channel.number == number && it.show.startMs == startMs } const val QUEUE_MAX = 5 + /** How long before the programme's end the queue takes over. */ + const val QUEUE_LEAD_MS = 2_500L var queue by mutableStateOf>(emptyList()) /** Queue [number] live after the programme on now, or a Catch Up programme ([catchUpStartMs]); null when it can't be. */ @@ -169,8 +171,12 @@ internal object OrbitCompanion { fun follow(screen: CompanionScreen, anchor: Pair?, nowMs: Long): Pair? { val head = queue.firstOrNull() ?: return null val channel = screen.channels().firstOrNull { it.number == screen.selected() } ?: return anchor - val on = screen.selected() to channel.at(nowMs + screen.offset()).startMs - if (anchor == null || !screen.watching() || anchor.first != on.first || anchor == on) return on + val programme = channel.at(nowMs + screen.offset()) + val on = screen.selected() to programme.startMs + // It switches a moment before the end (Paul, 6 November 2026: the next programme began loading first, then the queue + // took over), so the channel's own next programme never starts. + val ending = anchor == on && programme.endMs - (nowMs + screen.offset()) <= QUEUE_LEAD_MS + if (anchor == null || !screen.watching() || anchor.first != on.first || (anchor == on && !ending)) return on queue = queue.drop(1) val offset = head.catchUpStartMs?.let { start -> catchUpEntry(screen, head.number, start, nowMs)?.offsetFrom(nowMs) } if (head.catchUpStartMs == null || offset != null) screen.tune(head.number, offset ?: 0) diff --git a/android/app/src/test/java/tv/orbit/classic/PhoneLinkTest.kt b/android/app/src/test/java/tv/orbit/classic/PhoneLinkTest.kt index 1a5d658..8fa9aad 100644 --- a/android/app/src/test/java/tv/orbit/classic/PhoneLinkTest.kt +++ b/android/app/src/test/java/tv/orbit/classic/PhoneLinkTest.kt @@ -210,15 +210,17 @@ class PhoneLinkTest { assertEquals(guideTitleParts(companionChannels[1].at(end).media.item.title).first, OrbitCompanion.queue[0].title) var anchor = OrbitCompanion.follow(screen, null, now) assertEquals(101 to start, anchor) - anchor = OrbitCompanion.follow(screen, anchor, end - 1_000); assertTrue(tuned.isEmpty()) + anchor = OrbitCompanion.follow(screen, anchor, end - 10_000); assertTrue(tuned.isEmpty()) // Changing channel moves the wait to the new programme instead of playing. - selected = 308; anchor = OrbitCompanion.follow(screen, anchor, end - 500); assertTrue(tuned.isEmpty()) - selected = 101; anchor = OrbitCompanion.follow(screen, anchor, end - 400); assertTrue(tuned.isEmpty()) + selected = 308; anchor = OrbitCompanion.follow(screen, anchor, end - 9_000); assertTrue(tuned.isEmpty()) + selected = 101; anchor = OrbitCompanion.follow(screen, anchor, end - 8_000); assertTrue(tuned.isEmpty()) // Not watching: it waits. watching = false; anchor = OrbitCompanion.follow(screen, anchor, end + 1_000); assertTrue(tuned.isEmpty()) watching = true; anchor = OrbitCompanion.follow(screen, anchor, end + 2_000); assertTrue(tuned.isEmpty()) val nextEnd = companionChannels[0].at(end + 2_000).endMs - OrbitCompanion.follow(screen, anchor, nextEnd + 1_000) + anchor = OrbitCompanion.follow(screen, anchor, nextEnd - 5_000); assertTrue(tuned.isEmpty()) + // A moment before the end, so the channel's own next programme never starts. + OrbitCompanion.follow(screen, anchor, nextEnd - 2_000) assertEquals(listOf(308 to 0L), tuned); assertTrue(OrbitCompanion.queue.isEmpty()) // Five at most. repeat(5) { assertNotNull(OrbitCompanion.enqueue(screen, 101, null, now)) }