diff --git a/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/extensions/CueExtensions.kt b/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/extensions/CueExtensions.kt new file mode 100644 index 000000000..c733a2bf8 --- /dev/null +++ b/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/extensions/CueExtensions.kt @@ -0,0 +1,20 @@ +package dev.anilbeesetti.nextplayer.feature.player.extensions + +import androidx.media3.common.text.Cue + +fun List.stackUnpositionedCues(): List { + if (size <= 1) return this + + var nextLine = -1 + return map { cue -> + val text = cue.text + if (cue.line != Cue.DIMEN_UNSET || text == null) { + cue + } else { + cue.buildUpon() + .setLine(nextLine.toFloat(), Cue.LINE_TYPE_NUMBER) + .build() + .also { nextLine -= text.count { it == '\n' } + 1 } + } + } +} diff --git a/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/state/CuesState.kt b/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/state/CuesState.kt index 79fa60574..497efd978 100644 --- a/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/state/CuesState.kt +++ b/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/state/CuesState.kt @@ -10,6 +10,7 @@ import androidx.media3.common.Player import androidx.media3.common.listen import androidx.media3.common.text.Cue import androidx.media3.common.util.UnstableApi +import dev.anilbeesetti.nextplayer.feature.player.extensions.stackUnpositionedCues @UnstableApi @Composable @@ -26,10 +27,10 @@ class CuesState( private set suspend fun observe() { - cues = player.currentCues.cues + cues = player.currentCues.cues.stackUnpositionedCues() player.listen { events -> if (events.contains(Player.EVENT_CUES)) { - cues = player.currentCues.cues + cues = player.currentCues.cues.stackUnpositionedCues() } } } diff --git a/feature/player/src/test/java/dev/anilbeesetti/nextplayer/feature/player/extensions/CueExtensionsTest.kt b/feature/player/src/test/java/dev/anilbeesetti/nextplayer/feature/player/extensions/CueExtensionsTest.kt new file mode 100644 index 000000000..1f0ff7461 --- /dev/null +++ b/feature/player/src/test/java/dev/anilbeesetti/nextplayer/feature/player/extensions/CueExtensionsTest.kt @@ -0,0 +1,71 @@ +package dev.anilbeesetti.nextplayer.feature.player.extensions + +import androidx.media3.common.text.Cue +import org.junit.Assert.assertEquals +import org.junit.Assert.assertSame +import org.junit.Test + +class CueExtensionsTest { + + @Test + fun `empty cue list remains empty`() { + assertEquals(emptyList(), emptyList().stackUnpositionedCues()) + } + + @Test + fun `single unpositioned cue remains unchanged`() { + val cue = textCue("only cue") + + val result = listOf(cue).stackUnpositionedCues() + + assertSame(cue, result.single()) + assertEquals(Cue.DIMEN_UNSET, result.single().line) + } + + @Test + fun `two unpositioned cues are stacked from the bottom`() { + val result = listOf(textCue("first"), textCue("second")).stackUnpositionedCues() + + assertEquals(-1f, result[0].line) + assertEquals(Cue.LINE_TYPE_NUMBER, result[0].lineType) + assertEquals(-2f, result[1].line) + assertEquals(Cue.LINE_TYPE_NUMBER, result[1].lineType) + } + + @Test + fun `multiline lower cue offsets the cue above by its rendered line count`() { + val result = listOf(textCue("a\nb"), textCue("above")).stackUnpositionedCues() + + assertEquals(-1f, result[0].line) + assertEquals(-3f, result[1].line) + } + + @Test + fun `explicitly positioned cue remains unchanged while siblings are stacked`() { + val first = textCue("first") + val positioned = Cue.Builder() + .setText("positioned") + .setLine(0.25f, Cue.LINE_TYPE_FRACTION) + .build() + val second = textCue("second") + + val result = listOf(first, positioned, second).stackUnpositionedCues() + + assertEquals(-1f, result[0].line) + assertSame(positioned, result[1]) + assertEquals(0.25f, result[1].line) + assertEquals(Cue.LINE_TYPE_FRACTION, result[1].lineType) + assertEquals(-2f, result[2].line) + } + + @Test + fun `stacking preserves cue text identity`() { + val cue = textCue("styled") + + val result = listOf(cue, textCue("second")).stackUnpositionedCues() + + assertSame(cue.text, result[0].text) + } + + private fun textCue(text: CharSequence): Cue = Cue.Builder().setText(text).build() +}