Improve playback, preroll and TV experience

This commit is contained in:
ponzischeme89
2026-08-10 07:11:14 +12:00
parent 598a4f5c75
commit d2f2eb62be
30 changed files with 1380 additions and 142 deletions
@@ -97,5 +97,25 @@ class CastMetadataTest {
assertEquals("portrait-tag", item.cast.single().primaryImageTag)
}
@Test
fun `decodes person biography and life dates`() {
val person = Json { ignoreUnknownKeys = true }.decodeFromString<BaseItem>(
"""
{
"Id":"person-1",
"Name":"Alex Actor",
"Type":"Person",
"Overview":"A stage and screen performer.",
"PremiereDate":"1940-01-02T00:00:00.0000000Z",
"EndDate":"2020-03-04T00:00:00.0000000Z"
}
""".trimIndent(),
)
assertEquals("A stage and screen performer.", person.overview)
assertEquals("1940-01-02T00:00:00.0000000Z", person.premiereDate)
assertEquals("2020-03-04T00:00:00.0000000Z", person.endDate)
}
private fun actor(id: String) = EmbyPerson(id = id, name = id, type = "Actor")
}
@@ -3,6 +3,9 @@ package com.ponzischeme89.memby.data
import com.ponzischeme89.memby.data.playback.DevicePlaybackCapabilities
import com.ponzischeme89.memby.data.playback.VideoDecoderCapabilities
import com.ponzischeme89.memby.data.playback.gatewayCapabilityTokens
import com.ponzischeme89.memby.data.model.DeviceProfile
import com.ponzischeme89.memby.data.model.h264TranscodeFallback
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -45,4 +48,14 @@ class DevicePlaybackCapabilitiesTest {
assertTrue("video_h264_decode" in tokens)
assertTrue("video_hevc_decode" !in tokens)
}
@Test
fun decoderFallbackCannotReturnHevcStreamCopy() {
val fallback = DeviceProfile.embyAndroidTv(supportsHevc = true)
.h264TranscodeFallback()
assertTrue(fallback.directPlayProfiles.isEmpty())
assertEquals(listOf("h264"), fallback.transcodingProfiles.map { it.videoCodec })
assertTrue(fallback.codecProfiles.none { it.codec.equals("hevc", ignoreCase = true) })
}
}
@@ -0,0 +1,44 @@
package com.ponzischeme89.memby.ui
import com.ponzischeme89.memby.data.launchResumePositionMs
import com.ponzischeme89.memby.data.model.BaseItem
import com.ponzischeme89.memby.data.model.UserItemData
import com.ponzischeme89.memby.ui.detail.primaryActionLabel
import org.junit.Assert.assertEquals
import org.junit.Test
class ContinueWatchingResumeTest {
@Test
fun `detail metadata keeps the continue watching playhead and resume label`() {
val card = BaseItem(
id = "episode",
name = "The Episode",
type = "Episode",
indexNumber = 3,
parentIndexNumber = 1,
userData = UserItemData(playbackPositionTicks = 36_000_000L),
)
val detailsWithoutUserData = card.copy(
overview = "The richer record fetched after focus.",
userData = null,
)
val focused = focusedItemWithMetadata(card, detailsWithoutUserData)
assertEquals(36_000_000L, focused.userData?.playbackPositionTicks)
assertEquals("Resume S01E03", primaryActionLabel(focused))
assertEquals("The richer record fetched after focus.", focused.overview)
}
@Test
fun `pressed card resume point outranks a prefetched zero`() {
assertEquals(
36_000L,
launchResumePositionMs(resolvedPositionMs = 0L, requestedPositionMs = 36_000L),
)
assertEquals(
42_000L,
launchResumePositionMs(resolvedPositionMs = 42_000L, requestedPositionMs = 0L),
)
}
}
@@ -5,6 +5,13 @@ import org.junit.Assert.assertNull
import org.junit.Test
class AlertsFormatTest {
@Test
fun `dismissed alert hands focus to a stable neighbour`() {
assertEquals(1, alertFocusIndexAfterRemoval(removedIndex = 1, remainingCount = 3))
assertEquals(2, alertFocusIndexAfterRemoval(removedIndex = 3, remainingCount = 3))
assertNull(alertFocusIndexAfterRemoval(removedIndex = 0, remainingCount = 0))
}
@Test
fun `no alerts wears no badge`() {
assertNull(alertBadgeLabel(0))
@@ -43,7 +43,11 @@ class CastPanelScreenshotTest {
title = "The Lives of Others",
loaded = true,
members = listOf(
CastMember("Ulrich Mühe", "Hauptmann Gerd Wiesler"),
CastMember(
"Ulrich Mühe",
"Hauptmann Gerd Wiesler",
deathDate = "2007-07-22T00:00:00.0000000Z",
),
CastMember("Martina Gedeck", "Christa-Maria Sieland"),
CastMember("Sebastian Koch", "Georg Dreyman"),
CastMember("Ulrich Tukur", "Oberstleutnant Anton Grubitz"),
@@ -94,6 +98,33 @@ class CastPanelScreenshotTest {
)
}
@Test
fun `selected actor shows biography and filmography`() {
capture(
name = "cast-panel-person-profile",
state = CastPanelState(
title = "The Lives of Others",
loaded = true,
members = listOf(CastMember("Ulrich Mühe", id = "person-1")),
selectedPerson = CastPersonPanel(
id = "person-1",
name = "Ulrich Mühe",
role = "Hauptmann Gerd Wiesler",
birthDate = "1953-06-20T00:00:00.0000000Z",
deathDate = "2007-07-22T00:00:00.0000000Z",
overview = "A celebrated German actor known for quiet, exacting performances on stage and screen. His portrayal of Gerd Wiesler earned international acclaim.",
filmography = listOf(
FilmographyCredit("The Lives of Others", 2006),
FilmographyCredit("Funny Games", 1997),
FilmographyCredit("The Castle", 1997),
FilmographyCredit("Benny's Video", 1992),
),
loaded = true,
),
),
)
}
/** Still fetching. The panel opens instantly and says so rather than showing nothing. */
@Test
fun `still loading`() {
@@ -118,6 +149,10 @@ class CastPanelScreenshotTest {
assertEquals("C", castInitials("Cher"))
assertEquals("AA", castInitials(" amy adams "))
assertEquals("", castInitials(" "))
assertEquals(
"20 June 1953 22 July 2007",
personLifeDates("1953-06-20T00:00:00Z", "2007-07-22T00:00:00Z"),
)
}
private fun capture(name: String, state: CastPanelState, focused: Int? = null) {
@@ -53,4 +53,13 @@ class PlaybackRecoveryTest {
assertEquals(3_000L, automaticRetryDelayMs(2))
assertNull(automaticRetryDelayMs(3))
}
@Test
fun prolongedMidProgrammeRebufferGetsOneCompatibilityFallback() {
assertTrue(shouldRecoverProlongedRebuffer(true, false, false, false))
assertFalse(shouldRecoverProlongedRebuffer(false, false, false, false))
assertFalse(shouldRecoverProlongedRebuffer(true, true, false, false))
assertFalse(shouldRecoverProlongedRebuffer(true, false, true, false))
assertFalse(shouldRecoverProlongedRebuffer(true, false, false, true))
}
}
@@ -1,5 +1,7 @@
package com.ponzischeme89.memby.ui.player
import androidx.media3.common.C
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -40,4 +42,11 @@ class PrerollSequenceTest {
assertFalse(prerollCountdownAdvances(lifecycleStarted = false, playbackReady = true))
assertFalse(prerollCountdownAdvances(lifecycleStarted = true, playbackReady = false))
}
@Test
fun `packaged clip duration replaces the configured fallback`() {
assertEquals(4_133L, effectivePrerollDurationMs(4_133L, 7_000L))
assertEquals(7_000L, effectivePrerollDurationMs(C.TIME_UNSET, 7_000L))
assertEquals(7_000L, effectivePrerollDurationMs(0L, 7_000L))
}
}