diff --git a/build.gradle.kts b/build.gradle.kts index dc2ac0db4..b4427b29d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,7 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.parcelize) apply false alias(libs.plugins.detekt) alias(libs.plugins.dependency.analysis.gradle.plugin) alias(libs.plugins.kotlinx.kover) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 09ceae463..e7a18ea3a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -66,6 +66,7 @@ coil = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } coil-base = { module = "io.coil-kt:coil-compose-base", version.ref = "coil" } json = { module = "org.json:json", version.ref = "json" } kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +kotlin-parcelize-runtime = { module = "org.jetbrains.kotlin:kotlin-parcelize-runtime", version.ref = "kotlin" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlinx-kover-gradle = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kotlinx-kover" } @@ -139,6 +140,7 @@ android-library = { id = "com.android.library", version.ref = "android-gradle-pl dependency-analysis-gradle-plugin = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependency-analysis-gradle-plugin" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kotlinx-kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kotlinx-kover" } pillarbox-android-application = { id = "ch.srgssr.pillarbox.gradle.android_application", version = "unspecified" } diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/ChapterAdapter.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/ChapterAdapter.kt index a929f6f72..023389ea7 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/ChapterAdapter.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/ChapterAdapter.kt @@ -10,14 +10,15 @@ import ch.srgssr.pillarbox.core.business.integrationlayer.ImageScalingService import ch.srgssr.pillarbox.core.business.integrationlayer.data.Chapter import ch.srgssr.pillarbox.core.business.integrationlayer.data.MediaComposition import ch.srgssr.pillarbox.core.business.integrationlayer.data.MediaType +import ch.srgssr.pillarbox.player.asset.timeRange.Chapter as TimeRangeChapter internal object ChapterAdapter { private val imageScalingService = ImageScalingService() - fun toChapter(chapter: Chapter): ch.srgssr.pillarbox.player.asset.Chapter { + fun toChapter(chapter: Chapter): TimeRangeChapter { requireNotNull(chapter.fullLengthMarkIn) requireNotNull(chapter.fullLengthMarkOut) - return ch.srgssr.pillarbox.player.asset.Chapter( + return TimeRangeChapter( id = chapter.urn, start = chapter.fullLengthMarkIn, end = chapter.fullLengthMarkOut, @@ -29,7 +30,7 @@ internal object ChapterAdapter { ) } - fun getChapters(mediaComposition: MediaComposition): List { + fun getChapters(mediaComposition: MediaComposition): List { val mainChapter = mediaComposition.mainChapter if (!mainChapter.isFullLengthChapter && mainChapter.mediaType == MediaType.AUDIO) return emptyList() return mediaComposition.listChapter diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SRGAssetLoader.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SRGAssetLoader.kt index 8e4c7286c..ddf6015bd 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SRGAssetLoader.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SRGAssetLoader.kt @@ -167,9 +167,9 @@ class SRGAssetLoader( mediaComposition = result, ) }.build(), - chapters = ChapterAdapter.getChapters(result), - blockedTimeRanges = SegmentAdapter.getBlockedTimeRanges(chapter.listSegment), - timeRanges = TimeIntervalAdapter.getTimeIntervals(result.mainChapter.timeIntervalList), + timeRanges = ChapterAdapter.getChapters(result) + + TimeIntervalAdapter.getCredits(result.mainChapter.timeIntervalList) + + SegmentAdapter.getBlockedTimeRanges(chapter.listSegment), ) } diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SegmentAdapter.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SegmentAdapter.kt index 9089013b8..156eab9a8 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SegmentAdapter.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SegmentAdapter.kt @@ -5,13 +5,18 @@ package ch.srgssr.pillarbox.core.business.source import ch.srgssr.pillarbox.core.business.integrationlayer.data.Segment -import ch.srgssr.pillarbox.player.asset.BlockedTimeRange +import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange internal object SegmentAdapter { fun getBlockedTimeRange(segment: Segment): BlockedTimeRange { requireNotNull(segment.blockReason) - return BlockedTimeRange(segment.urn, segment.markIn, segment.markOut, segment.blockReason.toString()) + return BlockedTimeRange( + id = segment.urn, + start = segment.markIn, + end = segment.markOut, + reason = segment.blockReason.toString(), + ) } fun getBlockedTimeRanges(listSegment: List?): List { diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/TimeIntervalAdapter.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/TimeIntervalAdapter.kt index ad3c8f13e..5aea413f7 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/TimeIntervalAdapter.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/TimeIntervalAdapter.kt @@ -5,24 +5,24 @@ package ch.srgssr.pillarbox.core.business.source import ch.srgssr.pillarbox.core.business.integrationlayer.data.TimeInterval -import ch.srgssr.pillarbox.player.asset.SkipableTimeRange +import ch.srgssr.pillarbox.core.business.integrationlayer.data.TimeIntervalType +import ch.srgssr.pillarbox.player.asset.timeRange.Credit internal object TimeIntervalAdapter { - internal fun getTimeIntervals(timeIntervals: List?): List { + internal fun getCredits(timeIntervals: List?): List { return timeIntervals .orEmpty() - .mapNotNull { it.toSkipableTimeInterval() } + .mapNotNull { it.toCredit() } } - internal fun TimeInterval.toSkipableTimeInterval(): SkipableTimeRange? { + internal fun TimeInterval.toCredit(): Credit? { return if (type == null || markIn == null || markOut == null) { null } else { - SkipableTimeRange( - id = type.name, - start = markIn, - end = markOut, - ) + when (type) { + TimeIntervalType.CLOSING_CREDITS -> Credit.Closing(start = markIn, end = markOut) + TimeIntervalType.OPENING_CREDITS -> Credit.Opening(start = markIn, end = markOut) + } } } } diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/ChapterAdapterTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/ChapterAdapterTest.kt index f3f08885b..5a1f6aa16 100644 --- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/ChapterAdapterTest.kt +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/ChapterAdapterTest.kt @@ -15,6 +15,7 @@ import ch.srgssr.pillarbox.core.business.source.ChapterAdapter import org.junit.runner.RunWith import kotlin.test.Test import kotlin.test.assertEquals +import ch.srgssr.pillarbox.player.asset.timeRange.Chapter as TimeRangeChapter @RunWith(AndroidJUnit4::class) class ChapterAdapterTest { @@ -58,7 +59,7 @@ class ChapterAdapterTest { fullLengthMarkOut = 100, mediaType = MediaType.VIDEO, ) - val expected = ch.srgssr.pillarbox.player.asset.Chapter( + val expected = TimeRangeChapter( id = "urn", start = 10, end = 100, diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/SRGAssetLoaderTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/SRGAssetLoaderTest.kt index c378b6d1c..5e3cd28f6 100644 --- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/SRGAssetLoaderTest.kt +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/SRGAssetLoaderTest.kt @@ -148,8 +148,8 @@ class SRGAssetLoaderTest { val asset = assetLoader.loadAsset( SRGMediaItemBuilder(DummyMediaCompositionProvider.URN_SEGMENT_BLOCK_REASON).build() ) - val expectedBlockIntervals = listOf(SegmentAdapter.getBlockedTimeRange(DummyMediaCompositionProvider.BLOCKED_SEGMENT)) - assertEquals(expectedBlockIntervals, asset.blockedTimeRanges) + val expectedBlockTimeRanges = listOf(SegmentAdapter.getBlockedTimeRange(DummyMediaCompositionProvider.BLOCKED_SEGMENT)) + assertEquals(expectedBlockTimeRanges, asset.timeRanges) } @Test @@ -157,10 +157,10 @@ class SRGAssetLoaderTest { val asset = assetLoader.loadAsset( SRGMediaItemBuilder(DummyMediaCompositionProvider.URN_TIME_INTERVALS).build() ) - val expectedTimeIntervals = TimeIntervalAdapter.getTimeIntervals( + val expectedCredits = TimeIntervalAdapter.getCredits( listOf(DummyMediaCompositionProvider.TIME_INTERVAL_1, DummyMediaCompositionProvider.TIME_INTERVAL_2) ) - assertEquals(expectedTimeIntervals, asset.timeRanges) + assertEquals(expectedCredits, asset.timeRanges) } internal class DummyMediaCompositionProvider : MediaCompositionService { diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/SegmentAdapterTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/SegmentAdapterTest.kt index 8ef138f32..004db84ca 100644 --- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/SegmentAdapterTest.kt +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/SegmentAdapterTest.kt @@ -7,7 +7,7 @@ package ch.srgssr.pillarbox.core.business import ch.srgssr.pillarbox.core.business.integrationlayer.data.BlockReason import ch.srgssr.pillarbox.core.business.integrationlayer.data.Segment import ch.srgssr.pillarbox.core.business.source.SegmentAdapter -import ch.srgssr.pillarbox.player.asset.BlockedTimeRange +import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange import kotlin.test.Test import kotlin.test.assertEquals diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/TimeIntervalAdapterTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/TimeIntervalAdapterTest.kt new file mode 100644 index 000000000..f52cba216 --- /dev/null +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/TimeIntervalAdapterTest.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business + +import ch.srgssr.pillarbox.core.business.integrationlayer.data.TimeInterval +import ch.srgssr.pillarbox.core.business.integrationlayer.data.TimeIntervalType +import ch.srgssr.pillarbox.core.business.source.TimeIntervalAdapter +import ch.srgssr.pillarbox.core.business.source.TimeIntervalAdapter.toCredit +import ch.srgssr.pillarbox.player.asset.timeRange.Credit +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class TimeIntervalAdapterTest { + + @Test + fun `get credits, source is null`() { + val credits = TimeIntervalAdapter.getCredits(null) + + assertTrue(credits.isEmpty()) + } + + @Test + fun `get credits, source is empty`() { + val credits = TimeIntervalAdapter.getCredits(emptyList()) + + assertTrue(credits.isEmpty()) + } + + @Test + fun `get credits, source is not empty`() { + val timeIntervals = listOf( + // Valid time intervals + TimeInterval(markIn = 10L, markOut = 20L, type = TimeIntervalType.OPENING_CREDITS), + TimeInterval(markIn = 30L, markOut = 100L, type = TimeIntervalType.CLOSING_CREDITS), + + // Invalid time intervals + TimeInterval(markIn = null, markOut = null, type = null), + TimeInterval(markIn = 10L, markOut = null, type = null), + TimeInterval(markIn = 10L, markOut = 20L, type = null), + TimeInterval(markIn = 10L, markOut = null, type = TimeIntervalType.OPENING_CREDITS), + TimeInterval(markIn = null, markOut = 20L, type = null), + TimeInterval(markIn = null, markOut = 20L, type = TimeIntervalType.CLOSING_CREDITS), + TimeInterval(markIn = null, markOut = null, type = TimeIntervalType.OPENING_CREDITS), + ) + val credits = TimeIntervalAdapter.getCredits(timeIntervals) + val expectedCredits = listOf( + timeIntervals[0].toCredit(), + timeIntervals[1].toCredit(), + ) + + assertEquals(expectedCredits, credits) + } + + @Test + fun `empty time interval produces null Credit`() { + val timeInterval = TimeInterval(markIn = null, markOut = null, type = null) + assertNull(timeInterval.toCredit()) + } + + @Test + fun `null markOut produces null Credit`() { + val timeInterval = TimeInterval(markIn = 100, markOut = null, type = TimeIntervalType.CLOSING_CREDITS) + assertNull(timeInterval.toCredit()) + } + + @Test + fun `null markIn produces null Credit`() { + val timeInterval = TimeInterval(markIn = null, markOut = 100, type = TimeIntervalType.CLOSING_CREDITS) + assertNull(timeInterval.toCredit()) + } + + @Test + fun `null type produces null Credit`() { + val timeInterval = TimeInterval(markIn = 100, markOut = 200, type = null) + assertNull(timeInterval.toCredit()) + } + + @Test + fun `OPENING_CREDITS type produces Opening`() { + val timeInterval = TimeInterval(markIn = 100, markOut = 200, type = TimeIntervalType.OPENING_CREDITS) + assertEquals(Credit.Opening(start = 100, end = 200), timeInterval.toCredit()) + } + + @Test + fun `CLOSING_CREDITS type produces Opening`() { + val timeInterval = TimeInterval(markIn = 100, markOut = 200, type = TimeIntervalType.CLOSING_CREDITS) + assertEquals(Credit.Closing(start = 100, end = 200), timeInterval.toCredit()) + } +} diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/source/TimeRangeAdapterTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/source/TimeRangeAdapterTest.kt deleted file mode 100644 index f46c79dec..000000000 --- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/source/TimeRangeAdapterTest.kt +++ /dev/null @@ -1,49 +0,0 @@ -package ch.srgssr.pillarbox.core.business.source - -import ch.srgssr.pillarbox.core.business.integrationlayer.data.TimeInterval -import ch.srgssr.pillarbox.core.business.integrationlayer.data.TimeIntervalType -import ch.srgssr.pillarbox.core.business.source.TimeIntervalAdapter.toSkipableTimeInterval -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class TimeRangeAdapterTest { - @Test - fun `get time intervals, source is null`() { - val timeIntervals = TimeIntervalAdapter.getTimeIntervals(null) - - assertTrue(timeIntervals.isEmpty()) - } - - @Test - fun `get time intervals, source is empty`() { - val timeIntervals = TimeIntervalAdapter.getTimeIntervals(emptyList()) - - assertTrue(timeIntervals.isEmpty()) - } - - @Test - fun `get time intervals, source is not empty`() { - val originalTimeIntervals = listOf( - // Valid time intervals - TimeInterval(markIn = 10L, markOut = 20L, type = TimeIntervalType.OPENING_CREDITS), - TimeInterval(markIn = 30L, markOut = 100L, type = TimeIntervalType.CLOSING_CREDITS), - - // Invalid time intervals - TimeInterval(markIn = null, markOut = null, type = null), - TimeInterval(markIn = 10L, markOut = null, type = null), - TimeInterval(markIn = 10L, markOut = 20L, type = null), - TimeInterval(markIn = 10L, markOut = null, type = TimeIntervalType.OPENING_CREDITS), - TimeInterval(markIn = null, markOut = 20L, type = null), - TimeInterval(markIn = null, markOut = 20L, type = TimeIntervalType.CLOSING_CREDITS), - TimeInterval(markIn = null, markOut = null, type = TimeIntervalType.OPENING_CREDITS), - ) - val timeIntervals = TimeIntervalAdapter.getTimeIntervals(originalTimeIntervals) - val expectedTimeIntervals = listOf( - originalTimeIntervals[0].toSkipableTimeInterval(), - originalTimeIntervals[1].toSkipableTimeInterval(), - ) - - assertEquals(expectedTimeIntervals, timeIntervals) - } -} diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/PlayerView.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/PlayerView.kt index 62bc79f5a..d17c198e7 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/PlayerView.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/PlayerView.kt @@ -47,7 +47,7 @@ import ch.srgssr.pillarbox.demo.tv.ui.player.compose.settings.PlaybackSettingsDr import ch.srgssr.pillarbox.demo.tv.ui.theme.paddings import ch.srgssr.pillarbox.ui.extension.currentMediaMetadataAsState import ch.srgssr.pillarbox.ui.extension.getCurrentChapterAsState -import ch.srgssr.pillarbox.ui.extension.getCurrentTimeRangeAsState +import ch.srgssr.pillarbox.ui.extension.getCurrentCreditAsState import ch.srgssr.pillarbox.ui.extension.playerErrorAsState import ch.srgssr.pillarbox.ui.widget.maintainVisibleOnFocus import ch.srgssr.pillarbox.ui.widget.player.PlayerSurface @@ -69,7 +69,7 @@ fun PlayerView( ) { val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val visibilityState = rememberDelayedVisibilityState(player = player, visible = true) - val timeInterval by player.getCurrentTimeRangeAsState() + val currentCredit by player.getCurrentCreditAsState() LaunchedEffect(drawerState.currentValue) { when (drawerState.currentValue) { @@ -128,9 +128,9 @@ fun PlayerView( } } } - AnimatedVisibility(timeInterval != null) { + AnimatedVisibility(currentCredit != null) { Button( - onClick = { player.seekTo(timeInterval?.end ?: 0L) }, + onClick = { player.seekTo(currentCredit?.end ?: 0L) }, modifier = Modifier.padding(MaterialTheme.paddings.baseline), ) { Text(text = stringResource(R.string.skip)) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/BlockedIntervalWarning.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/BlockedTimeRangeWarning.kt similarity index 70% rename from pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/BlockedIntervalWarning.kt rename to pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/BlockedTimeRangeWarning.kt index 5b5237fb7..3e82ee2ee 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/BlockedIntervalWarning.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/BlockedTimeRangeWarning.kt @@ -24,31 +24,31 @@ import androidx.media3.common.Player import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme import ch.srgssr.pillarbox.demo.ui.theme.paddings import ch.srgssr.pillarbox.player.PillarboxPlayer -import ch.srgssr.pillarbox.player.asset.BlockedTimeRange +import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange import kotlinx.coroutines.delay import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds /** - * Display a message when the player reaches a blocked interval. + * Display a message when the player reaches a blocked time range. * * @param player * @param modifier * @param visibilityDelay */ @Composable -fun BlockedIntervalWarning( +fun BlockedTimeRangeWarning( player: Player, modifier: Modifier = Modifier, visibilityDelay: Duration = 5.seconds, ) { - var currentBlockedInterval: BlockedTimeRange? by remember(player) { + var currentBlockedTimeRange: BlockedTimeRange? by remember(player) { mutableStateOf(null) } DisposableEffect(player) { val listener = object : PillarboxPlayer.Listener { override fun onBlockedTimeRangeReached(blockedTimeRange: BlockedTimeRange) { - currentBlockedInterval = blockedTimeRange + currentBlockedTimeRange = blockedTimeRange } } player.addListener(listener) @@ -56,32 +56,32 @@ fun BlockedIntervalWarning( player.removeListener(listener) } } - LaunchedEffect(currentBlockedInterval) { - if (currentBlockedInterval != null) { + LaunchedEffect(currentBlockedTimeRange) { + if (currentBlockedTimeRange != null) { delay(visibilityDelay) - currentBlockedInterval = null + currentBlockedTimeRange = null } } AnimatedVisibility( modifier = modifier, - visible = currentBlockedInterval != null + visible = currentBlockedTimeRange != null ) { - currentBlockedInterval?.let { - BlockedSegmentInfo(modifier = Modifier.fillMaxWidth(), blockedInterval = it) + currentBlockedTimeRange?.let { + BlockedTimeRangeInfo(modifier = Modifier.fillMaxWidth(), blockedTimeRange = it) } } } @Composable -private fun BlockedSegmentInfo( - blockedInterval: BlockedTimeRange, +private fun BlockedTimeRangeInfo( + blockedTimeRange: BlockedTimeRange, modifier: Modifier = Modifier ) { Text( modifier = modifier .background(color = Color.Blue.copy(0.8f)) .padding(MaterialTheme.paddings.baseline), - text = blockedInterval.reason, + text = "Reached a blocked segment! ${blockedTimeRange.reason}", color = Color.White, style = MaterialTheme.typography.labelSmall ) @@ -89,12 +89,12 @@ private fun BlockedSegmentInfo( @Preview(showBackground = true) @Composable -private fun BlockedSegmentPreview() { - val blockedSection = BlockedTimeRange("", 0, 0, "GeoBlock") +private fun BlockedTimeRangeInfoPreview() { + val blockedTimeRange = BlockedTimeRange(start = 0, end = 0, reason = "GeoBlock") PillarboxTheme { - BlockedSegmentInfo( + BlockedTimeRangeInfo( modifier = Modifier.fillMaxWidth(), - blockedInterval = blockedSection + blockedTimeRange = blockedTimeRange ) } } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/ChapterInfo.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/ChapterInfo.kt index c064119a9..58755f2d5 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/ChapterInfo.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/ChapterInfo.kt @@ -14,7 +14,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.media3.common.Player import ch.srgssr.pillarbox.demo.ui.player.controls.MediaMetadataView -import ch.srgssr.pillarbox.player.asset.Chapter +import ch.srgssr.pillarbox.player.asset.timeRange.Chapter import ch.srgssr.pillarbox.ui.extension.getCurrentChapterAsState import kotlinx.coroutines.delay import kotlin.time.Duration diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt index 3702481cb..e9eb2b610 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt @@ -30,7 +30,7 @@ import ch.srgssr.pillarbox.demo.ui.theme.paddings import ch.srgssr.pillarbox.ui.ProgressTrackerState import ch.srgssr.pillarbox.ui.ScaleMode import ch.srgssr.pillarbox.ui.exoplayer.ExoPlayerSubtitleView -import ch.srgssr.pillarbox.ui.extension.getCurrentTimeRangeAsState +import ch.srgssr.pillarbox.ui.extension.getCurrentCreditAsState import ch.srgssr.pillarbox.ui.extension.hasMediaItemsAsState import ch.srgssr.pillarbox.ui.extension.playbackStateAsState import ch.srgssr.pillarbox.ui.extension.playerErrorAsState @@ -85,7 +85,7 @@ fun PlayerView( autoHideEnabled = !isSliderDragged, visible = controlsVisible ) - val timeInterval by player.getCurrentTimeRangeAsState() + val currentCredit by player.getCurrentCreditAsState() ToggleableBox( modifier = modifier, @@ -96,7 +96,7 @@ fun PlayerView( player = player, interactionSource = interactionSource, progressTracker = progressTracker, - timeInterval = timeInterval, + credit = currentCredit, content = content ) } @@ -118,16 +118,16 @@ fun PlayerView( ExoPlayerSubtitleView(player = player) } - if (timeInterval != null && !visibilityState.isVisible) { + if (currentCredit != null && !visibilityState.isVisible) { SkipButton( modifier = Modifier .align(Alignment.BottomEnd) .padding(MaterialTheme.paddings.baseline), - onClick = { player.seekTo(timeInterval?.end ?: 0L) }, + onClick = { player.seekTo(currentCredit?.end ?: 0L) }, ) } - BlockedIntervalWarning( + BlockedTimeRangeWarning( player = player, modifier = Modifier .align(Alignment.TopStart) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerViewModel.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerViewModel.kt index b2a60a611..7733be243 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerViewModel.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerViewModel.kt @@ -20,8 +20,8 @@ import androidx.media3.common.VideoSize import ch.srgssr.pillarbox.demo.shared.data.DemoItem import ch.srgssr.pillarbox.demo.shared.di.PlayerModule import ch.srgssr.pillarbox.player.PillarboxPlayer -import ch.srgssr.pillarbox.player.asset.Chapter -import ch.srgssr.pillarbox.player.asset.SkipableTimeRange +import ch.srgssr.pillarbox.player.asset.timeRange.Chapter +import ch.srgssr.pillarbox.player.asset.timeRange.Credit import ch.srgssr.pillarbox.player.extension.setHandleAudioFocus import ch.srgssr.pillarbox.player.extension.toRational import kotlinx.coroutines.flow.MutableStateFlow @@ -150,12 +150,12 @@ class SimplePlayerViewModel( Log.d(TAG, "onPlaybackParametersChanged ${playbackParameters.speed}") } - override fun onCurrentChapterChanged(chapter: Chapter?) { - Log.i(TAG, "onCurrentChapterChanged $chapter") + override fun onChapterChanged(chapter: Chapter?) { + Log.i(TAG, "onChapterChanged $chapter") } - override fun onSkipableTimeRangeChanged(timeRange: SkipableTimeRange?) { - Log.i(TAG, "onTimeIntervalChanged $timeRange") + override fun onCreditChanged(credit: Credit?) { + Log.i(TAG, "onCreditChanged $credit") } companion object { diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerControls.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerControls.kt index 4e531ba76..3ea6eb0b3 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerControls.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerControls.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.graphics.Color import androidx.media3.common.Player import ch.srgssr.pillarbox.demo.ui.player.LiveIndicator import ch.srgssr.pillarbox.demo.ui.theme.paddings -import ch.srgssr.pillarbox.player.asset.SkipableTimeRange +import ch.srgssr.pillarbox.player.asset.timeRange.Credit import ch.srgssr.pillarbox.player.extension.canSeek import ch.srgssr.pillarbox.player.extension.getChapterAtPosition import ch.srgssr.pillarbox.player.extension.isAtLiveEdge @@ -44,7 +44,7 @@ import kotlinx.coroutines.flow.map * @param backgroundColor The background color to apply behind the controls. * @param interactionSource The interaction source of the slider. * @param progressTracker The progress tracker. - * @param timeInterval The current time interval, or `null`. + * @param credit The current credit, or `null`. * @param content The content to display under the slider. * @receiver */ @@ -55,7 +55,7 @@ fun PlayerControls( backgroundColor: Color = Color.Black.copy(0.5f), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, progressTracker: ProgressTrackerState = rememberProgressTrackerState(player = player, smoothTracker = true), - timeInterval: SkipableTimeRange? = null, + credit: Credit? = null, content: @Composable ColumnScope.() -> Unit, ) { val currentMediaMetadata by player.currentMediaMetadataAsState() @@ -88,12 +88,12 @@ fun PlayerControls( modifier = Modifier .align(Alignment.BottomCenter) ) { - if (timeInterval != null) { + if (credit != null) { SkipButton( modifier = Modifier .align(Alignment.End) .padding(MaterialTheme.paddings.baseline), - onClick = { player.seekTo(timeInterval.end) }, + onClick = { player.seekTo(credit.end) }, ) } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/ChapterShowcase.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/ChapterShowcase.kt index ff28a50ad..c81dd1714 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/ChapterShowcase.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/ChapterShowcase.kt @@ -50,7 +50,7 @@ import androidx.media3.common.MediaMetadata import ch.srgssr.pillarbox.demo.ui.player.PlayerView import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme import ch.srgssr.pillarbox.demo.ui.theme.paddings -import ch.srgssr.pillarbox.player.asset.Chapter +import ch.srgssr.pillarbox.player.asset.timeRange.Chapter import coil.compose.AsyncImage import kotlin.time.Duration.Companion.minutes diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/ChaptersShowcaseViewModel.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/ChaptersShowcaseViewModel.kt index fcc515697..ab84b8e91 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/ChaptersShowcaseViewModel.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/ChaptersShowcaseViewModel.kt @@ -10,7 +10,7 @@ import androidx.lifecycle.viewModelScope import androidx.media3.common.Player import ch.srgssr.pillarbox.core.business.DefaultPillarbox import ch.srgssr.pillarbox.demo.shared.data.DemoItem -import ch.srgssr.pillarbox.player.asset.Chapter +import ch.srgssr.pillarbox.player.asset.timeRange.Chapter import ch.srgssr.pillarbox.player.currentMediaItemAsFlow import ch.srgssr.pillarbox.player.extension.getChapterAtPosition import ch.srgssr.pillarbox.player.extension.getCurrentChapters diff --git a/pillarbox-player/build.gradle.kts b/pillarbox-player/build.gradle.kts index 8968c7773..fb81daf13 100644 --- a/pillarbox-player/build.gradle.kts +++ b/pillarbox-player/build.gradle.kts @@ -7,6 +7,7 @@ plugins { alias(libs.plugins.pillarbox.android.library) alias(libs.plugins.pillarbox.android.library.publishing) alias(libs.plugins.pillarbox.android.library.tested.module) + alias(libs.plugins.kotlin.parcelize) } android { @@ -37,6 +38,7 @@ dependencies { api(libs.androidx.media3.session) api(libs.androidx.media3.ui) api(libs.guava) + implementation(libs.kotlin.parcelize.runtime) implementation(libs.kotlinx.coroutines.guava) runtimeOnly(libs.kotlinx.coroutines.android) api(libs.kotlinx.coroutines.core) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt index b7745e9e7..eb0c90fa2 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt @@ -19,18 +19,14 @@ import androidx.media3.exoplayer.LoadControl import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter import androidx.media3.exoplayer.util.EventLogger -import ch.srgssr.pillarbox.player.asset.BlockedTimeRange -import ch.srgssr.pillarbox.player.asset.Chapter -import ch.srgssr.pillarbox.player.asset.PillarboxData -import ch.srgssr.pillarbox.player.asset.SkipableTimeRange -import ch.srgssr.pillarbox.player.extension.getChapterAtPosition +import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange +import ch.srgssr.pillarbox.player.asset.timeRange.Chapter +import ch.srgssr.pillarbox.player.asset.timeRange.Credit import ch.srgssr.pillarbox.player.extension.getPlaybackSpeed -import ch.srgssr.pillarbox.player.extension.getSkipableTimeRangeAtPosition import ch.srgssr.pillarbox.player.extension.setPreferredAudioRoleFlagsToAccessibilityManagerSettings import ch.srgssr.pillarbox.player.extension.setSeekIncrements import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory import ch.srgssr.pillarbox.player.tracker.AnalyticsMediaItemTracker -import ch.srgssr.pillarbox.player.tracker.BlockedTimeRangeTracker import ch.srgssr.pillarbox.player.tracker.CurrentMediaItemPillarboxDataTracker import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerProvider import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerRepository @@ -84,26 +80,28 @@ class PillarboxExoPlayer internal constructor( } get() = analyticsTracker.enabled - private val blockedTimeRangeTracker = BlockedTimeRangeTracker(this) - private val chapterTracker = TimeRangeTracker( - player = this, - getTimeRangeAt = Player::getChapterAtPosition, - getAllTimeRanges = PillarboxData::chapters, - notifyTimeRangeChanged = { notifyCurrentChapterChanged(it) }, - ) private val timeRangeTracker = TimeRangeTracker( - player = this, - getTimeRangeAt = Player::getSkipableTimeRangeAtPosition, - getAllTimeRanges = PillarboxData::timeRanges, - notifyTimeRangeChanged = { notifyTimeRangeChanged(it) }, + this, + object : TimeRangeTracker.Callback { + override fun onBlockedTimeRange(blockedTimeRange: BlockedTimeRange) { + notifyBlockedTimeRangeReached(blockedTimeRange) + handleBlockedTimeRange(blockedTimeRange) + } + + override fun onChapterChanged(chapter: Chapter?) { + notifyChapterChanged(chapter) + } + + override fun onCreditChanged(credit: Credit?) { + notifyCreditChanged(credit) + } + } ) init { exoPlayer.addListener(ComponentListener()) - itemPillarboxDataTracker.addCallback(blockedTimeRangeTracker) - itemPillarboxDataTracker.addCallback(analyticsTracker) - itemPillarboxDataTracker.addCallback(chapterTracker) itemPillarboxDataTracker.addCallback(timeRangeTracker) + itemPillarboxDataTracker.addCallback(analyticsTracker) if (BuildConfig.DEBUG) { addAnalyticsListener(EventLogger()) } @@ -172,21 +170,21 @@ class PillarboxExoPlayer internal constructor( } } - internal fun notifyCurrentChapterChanged(chapter: Chapter?) { + private fun notifyChapterChanged(chapter: Chapter?) { HashSet(listeners).forEach { - it.onCurrentChapterChanged(chapter) + it.onChapterChanged(chapter) } } - internal fun notifyBlockedTimeRangeReached(blockedTimeRange: BlockedTimeRange) { + private fun notifyBlockedTimeRangeReached(blockedTimeRange: BlockedTimeRange) { HashSet(listeners).forEach { it.onBlockedTimeRangeReached(blockedTimeRange) } } - internal fun notifyTimeRangeChanged(timeRange: SkipableTimeRange?) { + private fun notifyCreditChanged(timeRange: Credit?) { HashSet(listeners).forEach { - it.onSkipableTimeRangeChanged(timeRange) + it.onCreditChanged(timeRange) } } @@ -238,9 +236,9 @@ class PillarboxExoPlayer internal constructor( exoPlayer.replaceMediaItems(fromIndex, toIndex, mediaItems.map { it.clearTag() }) } - internal fun seekToWithoutSmoothSeeking(positionMs: Long) { + private fun handleBlockedTimeRange(timeRange: BlockedTimeRange) { clearSeeking() - exoPlayer.seekTo(positionMs) + exoPlayer.seekTo(timeRange.end + 1) } override fun seekTo(positionMs: Long) { diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt index 947f3a482..6375016cf 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt @@ -7,9 +7,9 @@ package ch.srgssr.pillarbox.player import androidx.media3.common.Player import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.SeekParameters -import ch.srgssr.pillarbox.player.asset.BlockedTimeRange -import ch.srgssr.pillarbox.player.asset.Chapter -import ch.srgssr.pillarbox.player.asset.SkipableTimeRange +import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange +import ch.srgssr.pillarbox.player.asset.timeRange.Chapter +import ch.srgssr.pillarbox.player.asset.timeRange.Credit /** * Pillarbox [Player] interface extension. @@ -34,14 +34,14 @@ interface PillarboxPlayer : Player { fun onTrackingEnabledChanged(trackingEnabled: Boolean) {} /** - * `onCurrentChapterChanged` is called when either: + * `onChapterChanged` is called when either: * - The player position changes while playing automatically. * - The use seeks to a new position. * - The playlist changes. * * @param chapter `null` when the current position is not in a chapter. */ - fun onCurrentChapterChanged(chapter: Chapter?) {} + fun onChapterChanged(chapter: Chapter?) {} /** * On blocked time range reached @@ -51,14 +51,14 @@ interface PillarboxPlayer : Player { fun onBlockedTimeRangeReached(blockedTimeRange: BlockedTimeRange) {} /** - * `onSkipableTimeRangeChanged` is called when either: + * `onCreditChanged` is called when either: * - The player position changes while playing automatically. * - The use seeks to a new position. * - The playlist changes. * - * @param timeRange `null` when the current position is not in a time interval. + * @param credit `null` when the current position is not in a Credit. */ - fun onSkipableTimeRangeChanged(timeRange: SkipableTimeRange?) {} + fun onCreditChanged(credit: Credit?) {} } /** diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PlayerCallbackFlow.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PlayerCallbackFlow.kt index 3d95f4550..bf46d3085 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PlayerCallbackFlow.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PlayerCallbackFlow.kt @@ -15,13 +15,13 @@ import androidx.media3.common.Timeline import androidx.media3.common.TrackSelectionParameters import androidx.media3.common.Tracks import androidx.media3.common.VideoSize -import ch.srgssr.pillarbox.player.asset.Chapter -import ch.srgssr.pillarbox.player.asset.SkipableTimeRange +import ch.srgssr.pillarbox.player.asset.timeRange.Chapter +import ch.srgssr.pillarbox.player.asset.timeRange.Credit import ch.srgssr.pillarbox.player.extension.computeAspectRatioOrNull import ch.srgssr.pillarbox.player.extension.getChapterAtPosition +import ch.srgssr.pillarbox.player.extension.getCreditAtPosition import ch.srgssr.pillarbox.player.extension.getCurrentMediaItems import ch.srgssr.pillarbox.player.extension.getPlaybackSpeed -import ch.srgssr.pillarbox.player.extension.getSkipableTimeRangeAtPosition import ch.srgssr.pillarbox.player.tracks.videoTracks import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.ProducerScope @@ -298,7 +298,6 @@ fun Player.videoSizeAsFlow(): Flow = callbackFlow { * * @param defaultAspectRatio The aspect ratio when the video size is unknown, or for audio content. */ -@OptIn(ExperimentalCoroutinesApi::class) fun Player.getAspectRatioAsFlow(defaultAspectRatio: Float): Flow { return combine( getCurrentTracksAsFlow(), @@ -397,7 +396,7 @@ fun Player.getCurrentDefaultPositionAsFlow(): Flow = callbackFlow { */ fun Player.getCurrentChapterAsFlow(): Flow = callbackFlow { val listener = object : PillarboxPlayer.Listener { - override fun onCurrentChapterChanged(chapter: Chapter?) { + override fun onChapterChanged(chapter: Chapter?) { trySend(chapter) } } @@ -406,16 +405,16 @@ fun Player.getCurrentChapterAsFlow(): Flow = callbackFlow { } /** - * @return Get the current time range as flow, when the time interval changes. + * @return Get the current credit as flow, when the credit changes. */ -fun Player.getCurrentSkipableTimeRangeAsFlow(): Flow = callbackFlow { +fun Player.getCurrentCreditAsFlow(): Flow = callbackFlow { val listener = object : PillarboxPlayer.Listener { - override fun onSkipableTimeRangeChanged(timeRange: SkipableTimeRange?) { - trySend(timeRange) + override fun onCreditChanged(credit: Credit?) { + trySend(credit) } } - trySend(getSkipableTimeRangeAtPosition()) - addPlayerListener(this@getCurrentSkipableTimeRangeAsFlow, listener) + trySend(getCreditAtPosition()) + addPlayerListener(this@getCurrentCreditAsFlow, listener) } private suspend fun ProducerScope.addPlayerListener(player: Player, listener: Listener) { diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/Asset.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/Asset.kt index e279a02e3..d4cfda6f5 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/Asset.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/Asset.kt @@ -6,6 +6,7 @@ package ch.srgssr.pillarbox.player.asset import androidx.media3.common.MediaMetadata import androidx.media3.exoplayer.source.MediaSource +import ch.srgssr.pillarbox.player.asset.timeRange.TimeRange import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerData /** @@ -14,15 +15,11 @@ import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerData * @property mediaSource The [MediaSource] used by the player to play something. * @property trackersData The [MediaItemTrackerData] to set to the [PillarboxData]. * @property mediaMetadata The [MediaMetadata] to set to the player media item. - * @property blockedTimeRanges The [BlockedTimeRange] list to set to the [PillarboxData]. - * @property chapters The [Chapter] list to set to the [PillarboxData]. - * @property timeRanges The [SkipableTimeRange] list to set to the [PillarboxData]. + * @property timeRanges The [TimeRange] list to set to the [PillarboxData]. */ data class Asset( val mediaSource: MediaSource, val trackersData: MediaItemTrackerData = MediaItemTrackerData.EMPTY, val mediaMetadata: MediaMetadata = MediaMetadata.EMPTY, - val blockedTimeRanges: List = emptyList(), - val chapters: List = emptyList(), - val timeRanges: List = emptyList(), + val timeRanges: List = emptyList(), ) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/BlockedTimeRange.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/BlockedTimeRange.kt deleted file mode 100644 index f6946bf0b..000000000 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/BlockedTimeRange.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player.asset - -import android.os.Parcel -import android.os.Parcelable - -/** - * Blocked time range - * - * @property id The id of the chapter. - * @property start The start position, in milliseconds. - * @property end The end position, in milliseconds. - * @property reason The block reason. - */ -data class BlockedTimeRange( - override val id: String, - override val start: Long, - override val end: Long, - val reason: String -) : TimeRange, Parcelable { - constructor(parcel: Parcel) : this( - parcel.readString()!!, - parcel.readLong(), - parcel.readLong(), - parcel.readString()!! - ) - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(id) - parcel.writeLong(start) - parcel.writeLong(end) - parcel.writeString(reason) - } - - override fun describeContents(): Int { - return 0 - } - - /** - * Creator create a [BlockedTimeRange] - */ - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): BlockedTimeRange { - return BlockedTimeRange(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } -} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/Chapter.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/Chapter.kt deleted file mode 100644 index 52ff552ea..000000000 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/Chapter.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player.asset - -import android.os.Parcel -import android.os.Parcelable -import androidx.media3.common.MediaMetadata - -/** - * Chapter - * - * @property id The id of the chapter. - * @property start The start position, in milliseconds. - * @property end The end position, in milliseconds. - * @property mediaMetadata The [MediaMetadata]. - */ -data class Chapter( - override val id: String, - override val start: Long, - override val end: Long, - val mediaMetadata: MediaMetadata -) : TimeRange, Parcelable { - constructor(parcel: Parcel) : this( - parcel.readString()!!, - parcel.readLong(), - parcel.readLong(), - MediaMetadata.fromBundle(parcel.readBundle(Chapter::class.java.classLoader)!!) - ) - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(id) - parcel.writeLong(start) - parcel.writeLong(end) - parcel.writeBundle(mediaMetadata.toBundle()) - } - - override fun describeContents(): Int { - return 0 - } - - /** - * Creator create a [Chapter] - */ - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): Chapter { - return Chapter(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } -} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/PillarboxData.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/PillarboxData.kt index f179a2b67..70a4a8f7c 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/PillarboxData.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/PillarboxData.kt @@ -4,6 +4,9 @@ */ package ch.srgssr.pillarbox.player.asset +import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange +import ch.srgssr.pillarbox.player.asset.timeRange.Chapter +import ch.srgssr.pillarbox.player.asset.timeRange.Credit import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerData /** @@ -12,13 +15,13 @@ import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerData * @property trackersData The [MediaItemTrackerData]. * @property blockedTimeRanges The [BlockedTimeRange] list. * @property chapters The [Chapter] list. - * @property timeRanges The [SkipableTimeRange] list. + * @property credits The [Credit] list. */ data class PillarboxData( val trackersData: MediaItemTrackerData = MediaItemTrackerData.EMPTY, val blockedTimeRanges: List = emptyList(), val chapters: List = emptyList(), - val timeRanges: List = emptyList(), + val credits: List = emptyList(), ) { companion object { /** diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/SkipableTimeRange.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/SkipableTimeRange.kt deleted file mode 100644 index 358521180..000000000 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/SkipableTimeRange.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player.asset - -import android.os.Parcel -import android.os.Parcelable - -/** - * Skipable time interval - * - * @property id The id of the time interval. - * @property start The start time, in milliseconds, of the time interval. - * @property end The end time, in milliseconds, of the time interval. - */ -data class SkipableTimeRange( - override val id: String, - override val start: Long, - override val end: Long, -) : TimeRange, Parcelable { - /** - * The type of time interval. - */ - val type: SkipableTimeRangeType? - get() = runCatching { - enumValueOf(id) - }.getOrNull() - - constructor(parcel: Parcel) : this( - id = parcel.readString()!!, - start = parcel.readLong(), - end = parcel.readLong(), - ) - - override fun writeToParcel(dest: Parcel, flags: Int) { - dest.writeString(id) - dest.writeLong(start) - dest.writeLong(end) - } - - override fun describeContents(): Int { - return 0 - } - - /** - * Creator create a [SkipableTimeRange] - */ - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(source: Parcel): SkipableTimeRange { - return SkipableTimeRange(source) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } -} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/SkipableTimeRangeType.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/SkipableTimeRangeType.kt deleted file mode 100644 index 4d4ca038d..000000000 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/SkipableTimeRangeType.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player.asset - -/** - * The type of skipable time range. - */ -enum class SkipableTimeRangeType { - CLOSING_CREDITS, - OPENING_CREDITS, -} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/timeRange/BlockedTimeRange.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/timeRange/BlockedTimeRange.kt new file mode 100644 index 000000000..b49b36c1b --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/timeRange/BlockedTimeRange.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.asset.timeRange + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * Blocked time range. The player will always seek to [end] when reaching [start], regardless of the [reason] or [id]. + * + * @property start The start position, in milliseconds. + * @property end The end position, in milliseconds. + * @property reason The optional block reason. + * @property id The optional id. + */ +@Parcelize +data class BlockedTimeRange( + override val start: Long, + override val end: Long, + val reason: String? = null, + val id: String? = null, +) : TimeRange, Parcelable diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/timeRange/Chapter.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/timeRange/Chapter.kt new file mode 100644 index 000000000..37507bc96 --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/timeRange/Chapter.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.asset.timeRange + +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable +import androidx.media3.common.MediaMetadata +import kotlinx.parcelize.Parceler +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.TypeParceler + +/** + * Chapter + * + * @property id The id of the chapter. + * @property start The start position, in milliseconds. + * @property end The end position, in milliseconds. + * @property mediaMetadata The [MediaMetadata]. + */ +@Parcelize +data class Chapter( + val id: String, + override val start: Long, + override val end: Long, + @TypeParceler() + val mediaMetadata: MediaMetadata +) : TimeRange, Parcelable + +internal object MediaMetadataParceler : Parceler { + override fun create(parcel: Parcel): MediaMetadata { + return MediaMetadata.fromBundle(parcel.readBundle(MediaMetadata::class.java.classLoader) ?: Bundle()) + } + + override fun MediaMetadata.write(parcel: Parcel, flags: Int) { + parcel.writeBundle(toBundle()) + } +} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/timeRange/Credit.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/timeRange/Credit.kt new file mode 100644 index 000000000..28ade8f9c --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/timeRange/Credit.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.asset.timeRange + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * Credit + */ +sealed interface Credit : TimeRange, Parcelable { + /** + * Opening credits + */ + @Parcelize + data class Opening( + override val start: Long, + override val end: Long + ) : Credit + + /** + * Closing credits + */ + @Parcelize + data class Closing( + override val start: Long, + override val end: Long + ) : Credit +} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/TimeRange.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/timeRange/TimeRange.kt similarity index 68% rename from pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/TimeRange.kt rename to pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/timeRange/TimeRange.kt index 4f8216a2b..85d830232 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/TimeRange.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/timeRange/TimeRange.kt @@ -2,19 +2,15 @@ * Copyright (c) SRG SSR. All rights reserved. * License information is available from the LICENSE file. */ -package ch.srgssr.pillarbox.player.asset +package ch.srgssr.pillarbox.player.asset.timeRange +import androidx.media3.common.C import kotlin.math.abs /** * Time range */ -interface TimeRange { - /** - * The id. - */ - val id: String - +sealed interface TimeRange { /** * The start position, in milliseconds, in the player timeline. */ @@ -34,7 +30,7 @@ interface TimeRange { } /** - * Check if the provided [position][positionMs] is in this [interval][TimeRange]. + * Check if the provided [position][positionMs] is in this [TimeRange]. * * @param positionMs The position, in milliseconds. * @return `true` if [positionMs] is between [start] (included) and [end] (excluded). @@ -43,3 +39,14 @@ interface TimeRange { return positionMs in start.. List.firstOrNullAtPosition(position: Long): T? { + return if (position == C.TIME_UNSET) { + null + } else { + firstOrNull { position in it } + } +} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Player.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Player.kt index 27dd3974c..a4c4eab51 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Player.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Player.kt @@ -4,15 +4,14 @@ */ package ch.srgssr.pillarbox.player.extension -import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.Timeline.Window import androidx.media3.exoplayer.dash.manifest.DashManifest import androidx.media3.exoplayer.hls.HlsManifest -import ch.srgssr.pillarbox.player.asset.BlockedTimeRange -import ch.srgssr.pillarbox.player.asset.Chapter -import ch.srgssr.pillarbox.player.asset.SkipableTimeRange +import ch.srgssr.pillarbox.player.asset.timeRange.Chapter +import ch.srgssr.pillarbox.player.asset.timeRange.Credit +import ch.srgssr.pillarbox.player.asset.timeRange.firstOrNullAtPosition import kotlin.time.Duration.Companion.microseconds import kotlin.time.Duration.Companion.milliseconds @@ -65,10 +64,10 @@ fun Player.getCurrentChapters(): List { } /** - * @return The current media item time intervals or an empty list. + * @return The current media item credits or an empty list. */ -fun Player.getSkipableTimeRange(): List { - return currentMediaItem?.pillarboxData?.timeRanges.orEmpty() +fun Player.getCurrentCredits(): List { + return currentMediaItem?.pillarboxData?.credits.orEmpty() } /** @@ -78,22 +77,17 @@ fun Player.getSkipableTimeRange(): List { * @return `null` if there is no chapter at [positionMs]. */ fun Player.getChapterAtPosition(positionMs: Long = currentPosition): Chapter? { - if (positionMs == C.TIME_UNSET) return null - return getCurrentChapters().firstOrNull { positionMs in it } + return getCurrentChapters().firstOrNullAtPosition(positionMs) } /** - * Get the time interval at [position][positionMs]. + * Get the credit at [position][positionMs]. * - * @param positionMs The position, in milliseconds, to find the time interval from. - * @return `null` if there is no time interval at [positionMs]. + * @param positionMs The position, in milliseconds, to find the credit from. + * @return `null` if there is no credit at [positionMs]. */ -fun Player.getSkipableTimeRangeAtPosition(positionMs: Long = currentPosition): SkipableTimeRange? { - return if (positionMs == C.TIME_UNSET) { - null - } else { - getSkipableTimeRange().firstOrNull { positionMs in it } - } +fun Player.getCreditAtPosition(positionMs: Long = currentPosition): Credit? { + return getCurrentCredits().firstOrNullAtPosition(positionMs) } /** @@ -121,21 +115,3 @@ fun Player.isAtLiveEdge(positionMs: Long = currentPosition, window: Window = Win } return playWhenReady && positionMs.milliseconds.inWholeSeconds >= window.defaultPositionMs.milliseconds.inWholeSeconds - offsetSeconds } - -/** - * @return The current media item blocked intervals or an empty list. - */ -fun Player.getCurrentBlockedIntervals(): List { - return currentMediaItem?.pillarboxData?.blockedTimeRanges ?: emptyList() -} - -/** - * Get the blocked interval at [position][positionMs]. - * - * @param positionMs The position, in milliseconds, to find the block interval from. - * @return `null` if there is no [BlockedTimeRange] at [positionMs]. - */ -fun Player.getBlockedIntervalAtPosition(positionMs: Long = currentPosition): BlockedTimeRange? { - if (positionMs == C.TIME_UNSET) return null - return getCurrentBlockedIntervals().firstOrNull { positionMs in it } -} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt index df2157aaa..8012b63c5 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt @@ -39,9 +39,9 @@ import androidx.media3.session.SessionCommands import androidx.media3.session.SessionResult import androidx.media3.session.SessionToken import ch.srgssr.pillarbox.player.PillarboxPlayer -import ch.srgssr.pillarbox.player.asset.BlockedTimeRange -import ch.srgssr.pillarbox.player.asset.Chapter -import ch.srgssr.pillarbox.player.asset.SkipableTimeRange +import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange +import ch.srgssr.pillarbox.player.asset.timeRange.Chapter +import ch.srgssr.pillarbox.player.asset.timeRange.Credit import ch.srgssr.pillarbox.player.utils.DebugLogger import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures @@ -255,24 +255,24 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { PillarboxSessionCommands.CHAPTER_CHANGED -> { val chapter: Chapter? = BundleCompat.getParcelable(args, PillarboxSessionCommands.ARG_CHAPTER_CHANGED, Chapter::class.java) listeners.forEach { - it.onCurrentChapterChanged(chapter) + it.onChapterChanged(chapter) } } - PillarboxSessionCommands.BLOCKED_INTERVAL_CHANGED -> { - val blockedInterval = BundleCompat.getParcelable(args, PillarboxSessionCommands.ARG_BLOCKED_INTERVAL, BlockedTimeRange::class.java) - blockedInterval?.let { + PillarboxSessionCommands.BLOCKED_CHANGED -> { + val blockedTimeRange = BundleCompat.getParcelable(args, PillarboxSessionCommands.ARG_BLOCKED, BlockedTimeRange::class.java) + blockedTimeRange?.let { listeners.forEach { listener -> - listener.onBlockedTimeRangeReached(blockedInterval) + listener.onBlockedTimeRangeReached(blockedTimeRange) } } } - PillarboxSessionCommands.TIME_INTERVAL_CHANGED -> { - val timeInterval = BundleCompat.getParcelable(args, PillarboxSessionCommands.ARG_TIME_INTERVAL, SkipableTimeRange::class.java) - timeInterval?.let { + PillarboxSessionCommands.CREDIT_CHANGED -> { + val credit = BundleCompat.getParcelable(args, PillarboxSessionCommands.ARG_CREDIT, Credit::class.java) + credit?.let { listeners.forEach { listener -> - listener.onSkipableTimeRangeChanged(timeInterval) + listener.onCreditChanged(credit) } } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt index 38602bfb4..298267a48 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt @@ -18,9 +18,9 @@ import androidx.media3.session.SessionCommand import androidx.media3.session.SessionCommands import androidx.media3.session.SessionResult import ch.srgssr.pillarbox.player.PillarboxPlayer -import ch.srgssr.pillarbox.player.asset.BlockedTimeRange -import ch.srgssr.pillarbox.player.asset.Chapter -import ch.srgssr.pillarbox.player.asset.SkipableTimeRange +import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange +import ch.srgssr.pillarbox.player.asset.timeRange.Chapter +import ch.srgssr.pillarbox.player.asset.timeRange.Credit import ch.srgssr.pillarbox.player.utils.DebugLogger import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture @@ -198,32 +198,32 @@ open class PillarboxMediaSession internal constructor() { } } - override fun onCurrentChapterChanged(chapter: Chapter?) { + override fun onChapterChanged(chapter: Chapter?) { val commandArg = Bundle().apply { putParcelable(PillarboxSessionCommands.ARG_CHAPTER_CHANGED, chapter) } _mediaSession.connectedControllers.forEach { - Log.d(TAG, "onCurrentChapterChanged $chapter") + Log.d(TAG, "onChapterChanged $chapter") _mediaSession.sendCustomCommand(it, PillarboxSessionCommands.COMMAND_CHAPTER_CHANGED, commandArg) } } override fun onBlockedTimeRangeReached(blockedTimeRange: BlockedTimeRange) { val commandArg = Bundle().apply { - putParcelable(PillarboxSessionCommands.ARG_BLOCKED_INTERVAL, blockedTimeRange) + putParcelable(PillarboxSessionCommands.ARG_BLOCKED, blockedTimeRange) } _mediaSession.connectedControllers.forEach { - _mediaSession.sendCustomCommand(it, PillarboxSessionCommands.COMMAND_BLOCK_INTERVAL_CHANGED, commandArg) + _mediaSession.sendCustomCommand(it, PillarboxSessionCommands.COMMAND_BLOCKED_CHANGED, commandArg) } } - override fun onSkipableTimeRangeChanged(timeRange: SkipableTimeRange?) { + override fun onCreditChanged(credit: Credit?) { val commandArg = Bundle().apply { - putParcelable(PillarboxSessionCommands.ARG_TIME_INTERVAL, timeRange) + putParcelable(PillarboxSessionCommands.ARG_CREDIT, credit) } _mediaSession.connectedControllers.forEach { - Log.d("TAG", "onTimeIntervalChanged $timeRange") - _mediaSession.sendCustomCommand(it, PillarboxSessionCommands.COMMAND_TIME_INTERVAL_CHANGED, commandArg) + Log.d("TAG", "onCreditChanged $credit") + _mediaSession.sendCustomCommand(it, PillarboxSessionCommands.COMMAND_CREDIT_CHANGED, commandArg) } } @@ -252,8 +252,8 @@ open class PillarboxMediaSession internal constructor() { add(PillarboxSessionCommands.COMMAND_SMOOTH_SEEKING_ENABLED) add(PillarboxSessionCommands.COMMAND_TRACKER_ENABLED) add(PillarboxSessionCommands.COMMAND_CHAPTER_CHANGED) - add(PillarboxSessionCommands.COMMAND_BLOCK_INTERVAL_CHANGED) - add(PillarboxSessionCommands.COMMAND_TIME_INTERVAL_CHANGED) + add(PillarboxSessionCommands.COMMAND_BLOCKED_CHANGED) + add(PillarboxSessionCommands.COMMAND_CREDIT_CHANGED) }.build() val pillarboxPlayer = session.player as PillarboxPlayer val playerSessionState = PlayerSessionState(pillarboxPlayer) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommands.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommands.kt index 266a99e7b..8a82c2048 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommands.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommands.kt @@ -10,13 +10,13 @@ import androidx.media3.session.SessionCommand internal object PillarboxSessionCommands { const val SMOOTH_SEEKING_ARG = "pillarbox.smoothSeekingEnabled" const val TRACKER_ENABLED_ARG = "pillarbox.trackerEnabled" - const val ARG_CHAPTER_CHANGED = "pillarbox.interval.chapter" - const val ARG_BLOCKED_INTERVAL = "pillarbox.interval.blocked" - const val ARG_TIME_INTERVAL = "pillarbox.interval.time" + const val ARG_CHAPTER_CHANGED = "pillarbox.range.chapter" + const val ARG_BLOCKED = "pillarbox.range.blocked" + const val ARG_CREDIT = "pillarbox.range.credit" const val CHAPTER_CHANGED = "pillarbox.chapter.changed" - const val BLOCKED_INTERVAL_CHANGED = "pillarbox.blockedInterval.changed" - const val TIME_INTERVAL_CHANGED = "pillarbox.time_interval.changed" + const val BLOCKED_CHANGED = "pillarbox.blocked.changed" + const val CREDIT_CHANGED = "pillarbox.credit.changed" const val SMOOTH_SEEKING_ENABLED = "pillarbox.smooth.seeking.enabled" const val TRACKER_ENABLED = "pillarbox.tracker.enabled" @@ -32,9 +32,9 @@ internal object PillarboxSessionCommands { val COMMAND_CHAPTER_CHANGED = SessionCommand(CHAPTER_CHANGED, Bundle.EMPTY) - val COMMAND_BLOCK_INTERVAL_CHANGED = SessionCommand(BLOCKED_INTERVAL_CHANGED, Bundle.EMPTY) + val COMMAND_BLOCKED_CHANGED = SessionCommand(BLOCKED_CHANGED, Bundle.EMPTY) - val COMMAND_TIME_INTERVAL_CHANGED = SessionCommand(TIME_INTERVAL_CHANGED, Bundle.EMPTY) + val COMMAND_CREDIT_CHANGED = SessionCommand(CREDIT_CHANGED, Bundle.EMPTY) fun setSmoothSeekingEnabled(smoothSeekingEnabled: Boolean): SessionCommand { return SessionCommand(SMOOTH_SEEKING_ENABLED, Bundle().apply { putBoolean(SMOOTH_SEEKING_ARG, smoothSeekingEnabled) }) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/source/PillarboxMediaSource.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/source/PillarboxMediaSource.kt index ec152e13f..3f358db1d 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/source/PillarboxMediaSource.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/source/PillarboxMediaSource.kt @@ -15,6 +15,9 @@ import androidx.media3.exoplayer.source.TimelineWithUpdatedMediaItem import androidx.media3.exoplayer.upstream.Allocator import ch.srgssr.pillarbox.player.asset.AssetLoader import ch.srgssr.pillarbox.player.asset.PillarboxData +import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange +import ch.srgssr.pillarbox.player.asset.timeRange.Chapter +import ch.srgssr.pillarbox.player.asset.timeRange.Credit import ch.srgssr.pillarbox.player.utils.DebugLogger import kotlinx.coroutines.runBlocking @@ -50,9 +53,9 @@ class PillarboxMediaSource internal constructor( .setTag( PillarboxData( trackersData = asset.trackersData, - blockedTimeRanges = asset.blockedTimeRanges, - chapters = asset.chapters, - timeRanges = asset.timeRanges, + blockedTimeRanges = asset.timeRanges.filterIsInstance(), + chapters = asset.timeRanges.filterIsInstance(), + credits = asset.timeRanges.filterIsInstance(), ) ) .build() diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/BlockedTimeRangeTracker.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/BlockedTimeRangeTracker.kt deleted file mode 100644 index bc4126b9e..000000000 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/BlockedTimeRangeTracker.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player.tracker - -import android.util.Log -import androidx.media3.common.MediaItem -import androidx.media3.common.Player -import androidx.media3.exoplayer.PlayerMessage -import ch.srgssr.pillarbox.player.PillarboxExoPlayer -import ch.srgssr.pillarbox.player.asset.BlockedTimeRange -import ch.srgssr.pillarbox.player.asset.PillarboxData -import ch.srgssr.pillarbox.player.extension.pillarboxData - -/** - * Blocked range tracker that seeks to [BlockedTimeRange.end] when the player reaches the segment. - */ -internal class BlockedTimeRangeTracker( - private val pillarboxExoPlayer: PillarboxExoPlayer -) : CurrentMediaItemPillarboxDataTracker.Callback { - private val listPlayerMessage = mutableListOf() - private var listBlockedIntervals = emptyList() - private val listener = Listener() - - override fun onPillarboxDataChanged(mediaItem: MediaItem?, data: PillarboxData?) { - clearPlayerMessage() - pillarboxExoPlayer.removeListener(listener) - if (data == null || mediaItem == null) return - listBlockedIntervals = mediaItem.pillarboxData.blockedTimeRanges - pillarboxExoPlayer.addListener(listener) - createMessages() - } - - private fun notifyBlockedSegment(blockedSection: BlockedTimeRange) { - Log.i(TAG, "Blocked segment reached $blockedSection") - pillarboxExoPlayer.notifyBlockedTimeRangeReached(blockedSection) - pillarboxExoPlayer.seekToWithoutSmoothSeeking(blockedSection.end + 1) - } - - private fun createMessages() { - listBlockedIntervals.forEach { - val message = pillarboxExoPlayer.createMessage { _, message -> - val segment = message as BlockedTimeRange - notifyBlockedSegment(segment) - }.apply { - deleteAfterDelivery = false - looper = pillarboxExoPlayer.applicationLooper - payload = it - setPosition(it.start) - } - message.send() - listPlayerMessage.add(message) - } - } - - private fun clearPlayerMessage() { - listPlayerMessage.forEach { - it.cancel() - } - listPlayerMessage.clear() - } - - private inner class Listener : Player.Listener { - override fun onEvents(player: Player, events: Player.Events) { - val blockedInterval = listBlockedIntervals.firstOrNull { player.currentPosition in it } - blockedInterval?.let { - notifyBlockedSegment(it) - } - } - } - - companion object { - private const val TAG = "BlockedSegmentTracker" - } -} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/TimeRangeTracker.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/TimeRangeTracker.kt index 2969c0e07..5747b1508 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/TimeRangeTracker.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/TimeRangeTracker.kt @@ -9,112 +9,201 @@ import androidx.media3.common.Player import androidx.media3.common.Player.DiscontinuityReason import androidx.media3.exoplayer.PlayerMessage import ch.srgssr.pillarbox.player.PillarboxExoPlayer +import ch.srgssr.pillarbox.player.PillarboxPlayer import ch.srgssr.pillarbox.player.asset.PillarboxData -import ch.srgssr.pillarbox.player.asset.TimeRange -import ch.srgssr.pillarbox.player.extension.pillarboxData - -internal class TimeRangeTracker( - private val player: PillarboxExoPlayer, - private val getTimeRangeAt: PillarboxExoPlayer.(position: Long) -> T?, - private val getAllTimeRanges: PillarboxData.() -> List, - private val notifyTimeRangeChanged: PillarboxExoPlayer.(timeInterval: T?) -> Unit, +import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange +import ch.srgssr.pillarbox.player.asset.timeRange.Chapter +import ch.srgssr.pillarbox.player.asset.timeRange.Credit +import ch.srgssr.pillarbox.player.asset.timeRange.TimeRange +import ch.srgssr.pillarbox.player.asset.timeRange.firstOrNullAtPosition + +internal class TimeRangeTracker( + private val pillarboxPlayer: PillarboxExoPlayer, + private val callback: Callback ) : CurrentMediaItemPillarboxDataTracker.Callback { + + interface Callback { + fun onChapterChanged(chapter: Chapter?) + fun onCreditChanged(credit: Credit?) + fun onBlockedTimeRange(blockedTimeRange: BlockedTimeRange) + } + private val playerMessages = mutableListOf() - private var timeRanges = emptyList() - private val listener = Listener() + private val listTrackers = mutableListOf>() + + override fun onPillarboxDataChanged(mediaItem: MediaItem?, data: PillarboxData?) { + clearPlayerMessages() + + if (data == null || mediaItem == null) { + // set current item to null + return + } + createMessages(data) + } - private var lastTimeRange: T? = player.getTimeRangeAt(player.currentPosition) + private fun createMessages(data: PillarboxData) { + val position = pillarboxPlayer.currentPosition + if (data.blockedTimeRanges.isNotEmpty()) { + listTrackers.add( + BlockedTimeRangeTracker( + initialPosition = position, + timeRanges = data.blockedTimeRanges, + callback = callback::onBlockedTimeRange + ) + ) + } + if (data.chapters.isNotEmpty()) { + listTrackers.add( + ChapterCreditsTracker( + initialPosition = position, + timeRanges = data.chapters, + callback = callback::onChapterChanged + ) + ) + } + + if (data.credits.isNotEmpty()) { + listTrackers.add( + ChapterCreditsTracker( + initialPosition = position, + timeRanges = data.credits, + callback = callback::onCreditChanged + ) + ) + } + + listTrackers.forEach { + pillarboxPlayer.addListener(it) + playerMessages.addAll(it.createMessages(pillarboxPlayer)) + } + } + + private fun clearPlayerMessages() { + playerMessages.forEach { playerMessage -> + playerMessage.cancel() + } + playerMessages.clear() + + listTrackers.forEach { + pillarboxPlayer.removeListener(it) + } + listTrackers.clear() + } +} + +private sealed interface PlayerTimeRangeTracker : PillarboxPlayer.Listener { + fun createMessages(player: PillarboxExoPlayer): List +} + +private class ChapterCreditsTracker( + initialPosition: Long, + private val timeRanges: List, + private val callback: (T?) -> Unit, +) : PlayerTimeRangeTracker { + + private var currentTimeRange: T? = null set(value) { if (field != value) { + callback(value) field = value - player.notifyTimeRangeChanged(field) } } - override fun onPillarboxDataChanged(mediaItem: MediaItem?, data: PillarboxData?) { - clearPlayerMessages() - lastTimeRange = player.getTimeRangeAt(player.currentPosition) - player.removeListener(listener) - if (data == null || mediaItem == null) { - return - } + init { + currentTimeRange = timeRanges.firstOrNullAtPosition(initialPosition) + } - timeRanges = mediaItem.pillarboxData.getAllTimeRanges() - player.addListener(listener) - createPlayerMessages() + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + @DiscontinuityReason reason: Int, + ) { + if ( + (reason == Player.DISCONTINUITY_REASON_SEEK || reason == Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT) && + oldPosition.mediaItemIndex == newPosition.mediaItemIndex + ) { + val currentPosition = oldPosition.positionMs + val currentTimeRange = currentTimeRange + ?.takeIf { timeRange -> currentPosition in timeRange } + ?: timeRanges.firstOrNullAtPosition(currentPosition) + + this.currentTimeRange = currentTimeRange + } } - private fun createPlayerMessages() { + override fun createMessages(player: PillarboxExoPlayer): List { val messageHandler = PlayerMessage.Target { messageType, message -> @Suppress("UNCHECKED_CAST") - val timeInterval = message as? T ?: return@Target - + val timeRange = message as? T ?: return@Target when (messageType) { - TYPE_ENTER -> { - if (timeInterval != lastTimeRange) { - lastTimeRange = timeInterval - } - } - - TYPE_EXIT -> lastTimeRange = null + TYPE_ENTER -> currentTimeRange = timeRange + TYPE_EXIT -> currentTimeRange = null } } - - timeRanges.forEach { timeInterval -> + val playerMessages = mutableListOf() + timeRanges.forEach { timeRange -> val messageEnter = player.createMessage(messageHandler).apply { deleteAfterDelivery = false looper = player.applicationLooper - payload = timeInterval - setPosition(timeInterval.start) + payload = timeRange + setPosition(timeRange.start) type = TYPE_ENTER + send() } val messageExit = player.createMessage(messageHandler).apply { deleteAfterDelivery = false looper = player.applicationLooper - payload = timeInterval - setPosition(timeInterval.end) + payload = timeRange + setPosition(timeRange.end) type = TYPE_EXIT + send() } - - messageEnter.send() - messageExit.send() - playerMessages.add(messageEnter) playerMessages.add(messageExit) } + return playerMessages } - private fun clearPlayerMessages() { - playerMessages.forEach { playerMessage -> - playerMessage.cancel() + private companion object { + private const val TYPE_ENTER = 1 + private const val TYPE_EXIT = 2 + } +} + +/** + * Whenever a [BlockedTimeRange] is reached, [callback] has to be called because the player will skip the content to the end. + */ +private class BlockedTimeRangeTracker( + initialPosition: Long, + private val timeRanges: List, + private val callback: (BlockedTimeRange) -> Unit +) : PlayerTimeRangeTracker { + + init { + timeRanges.firstOrNullAtPosition(initialPosition)?.let { + callback(it) } - playerMessages.clear() } - private inner class Listener : Player.Listener { - override fun onPositionDiscontinuity( - oldPosition: Player.PositionInfo, - newPosition: Player.PositionInfo, - @DiscontinuityReason reason: Int, - ) { - if ( - (reason == Player.DISCONTINUITY_REASON_SEEK || reason == Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT) && - oldPosition.mediaItemIndex == newPosition.mediaItemIndex - ) { - val currentPosition = player.currentPosition - val currentTimeInterval = lastTimeRange - ?.takeIf { timeInterval -> currentPosition in timeInterval } - ?: player.getTimeRangeAt(currentPosition) - - if (currentTimeInterval != lastTimeRange) { - lastTimeRange = currentTimeInterval - } - } + override fun onEvents(player: Player, events: Player.Events) { + val blockedInterval = timeRanges.firstOrNullAtPosition(player.currentPosition) + blockedInterval?.let { + callback(it) } } - private companion object { - private const val TYPE_ENTER = 1 - private const val TYPE_EXIT = 2 + override fun createMessages(player: PillarboxExoPlayer): List { + val target = PlayerMessage.Target { _, message -> + callback(message as BlockedTimeRange) + } + return timeRanges.map { + player.createMessage(target).apply { + deleteAfterDelivery = false + looper = player.applicationLooper + payload = it + setPosition(it.start) + send() + } + } } } diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/asset/SkipableTimeRangeTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/asset/SkipableTimeRangeTest.kt deleted file mode 100644 index 23eb92d60..000000000 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/asset/SkipableTimeRangeTest.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player.asset - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull - -class SkipableTimeRangeTest { - @Test - fun `SkipableTimeInterval#type with empty id`() { - val timeInterval = SkipableTimeRange( - id = "", - start = 0L, - end = 10L, - ) - - assertNull(timeInterval.type) - } - - @Test - fun `SkipableTimeInterval#type with unknown id`() { - val timeInterval = SkipableTimeRange( - id = "CLOSING", - start = 0L, - end = 10L, - ) - - assertNull(timeInterval.type) - } - - @Test - fun `SkipableTimeInterval#type with id=CLOSING_CREDITS`() { - val timeInterval = SkipableTimeRange( - id = "CLOSING_CREDITS", - start = 0L, - end = 10L, - ) - - assertEquals(SkipableTimeRangeType.CLOSING_CREDITS, timeInterval.type) - } - - @Test - fun `SkipableTimeInterval#type with id=OPENING_CREDITS`() { - val timeInterval = SkipableTimeRange( - id = "OPENING_CREDITS", - start = 0L, - end = 10L, - ) - - assertEquals(SkipableTimeRangeType.OPENING_CREDITS, timeInterval.type) - } -} diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/extension/PlayerTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/extension/PlayerTest.kt index 4b8051dfd..a9d82690a 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/extension/PlayerTest.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/extension/PlayerTest.kt @@ -9,7 +9,7 @@ import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player import androidx.test.ext.junit.runners.AndroidJUnit4 import ch.srgssr.pillarbox.player.asset.PillarboxData -import ch.srgssr.pillarbox.player.asset.SkipableTimeRange +import ch.srgssr.pillarbox.player.asset.timeRange.Credit import io.mockk.every import io.mockk.mockk import org.junit.runner.RunWith @@ -94,20 +94,20 @@ class PlayerTest { every { currentMediaItem } returns null } - assertEquals(emptyList(), player.getSkipableTimeRange()) + assertEquals(emptyList(), player.getCurrentCredits()) } @Test - fun `getTimeIntervals, with MediaItem, without PillarboxData`() { + fun `getCurrentCredits, with MediaItem, without PillarboxData`() { val player = mockk { every { currentMediaItem } returns MediaItem.Builder().build() } - assertEquals(emptyList(), player.getSkipableTimeRange()) + assertEquals(emptyList(), player.getCurrentCredits()) } @Test - fun `getTimeIntervals, with MediaItem, with PillarboxData, without time intervals`() { + fun `getCurrentCredits, with MediaItem, with PillarboxData, without credits`() { val player = mockk { every { currentMediaItem } returns MediaItem.Builder() .setUri("https://example.com/") @@ -115,19 +115,19 @@ class PlayerTest { .build() } - assertEquals(emptyList(), player.getSkipableTimeRange()) + assertEquals(emptyList(), player.getCurrentCredits()) } @Test - fun `getTimeIntervals, with MediaItem, with PillarboxData, with time intervals`() { - val timeIntervals = listOf(mockk()) + fun `getTimeIntervals, with MediaItem, with PillarboxData, with credits`() { + val credits = listOf(mockk()) val player = mockk { every { currentMediaItem } returns MediaItem.Builder() .setUri("https://example.com/") - .setTag(PillarboxData(timeRanges = timeIntervals)) + .setTag(PillarboxData(credits = credits)) .build() } - assertEquals(timeIntervals, player.getSkipableTimeRange()) + assertEquals(credits, player.getCurrentCredits()) } } diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommandsTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommandsTest.kt index 32b18de51..eaf97266a 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommandsTest.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommandsTest.kt @@ -38,18 +38,18 @@ class PillarboxSessionCommandsTest { } @Test - fun `empty block interval changed command`() { - val command = PillarboxSessionCommands.COMMAND_BLOCK_INTERVAL_CHANGED + fun `empty blocked changed command`() { + val command = PillarboxSessionCommands.COMMAND_BLOCKED_CHANGED - assertEquals(PillarboxSessionCommands.BLOCKED_INTERVAL_CHANGED, command.customAction) + assertEquals(PillarboxSessionCommands.BLOCKED_CHANGED, command.customAction) assertTrue(command.customExtras.isEmpty) } @Test - fun `empty time interval changed command`() { - val command = PillarboxSessionCommands.COMMAND_TIME_INTERVAL_CHANGED + fun `empty credit changed command`() { + val command = PillarboxSessionCommands.COMMAND_CREDIT_CHANGED - assertEquals(PillarboxSessionCommands.TIME_INTERVAL_CHANGED, command.customAction) + assertEquals(PillarboxSessionCommands.CREDIT_CHANGED, command.customAction) assertTrue(command.customExtras.isEmpty) } diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/BlockedTimeRangeTrackerTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/BlockedTimeRangeTrackerTest.kt index 9f6ca8f21..386fc31c2 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/BlockedTimeRangeTrackerTest.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/BlockedTimeRangeTrackerTest.kt @@ -18,7 +18,7 @@ import ch.srgssr.pillarbox.player.PillarboxPlayer import ch.srgssr.pillarbox.player.SeekIncrement import ch.srgssr.pillarbox.player.asset.Asset import ch.srgssr.pillarbox.player.asset.AssetLoader -import ch.srgssr.pillarbox.player.asset.BlockedTimeRange +import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory import io.mockk.clearAllMocks import io.mockk.spyk @@ -64,22 +64,22 @@ class BlockedTimeRangeTrackerTest { } @Test - fun `test block interval while playing`() { + fun `test block time range while playing`() { val expectedBlockedIntervals = listOf(BlockedAssetLoader.START_SEGMENT, BlockedAssetLoader.SEGMENT) player.addMediaItem(BlockedAssetLoader.MEDIA_START_BLOCKED_SEGMENT) TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) - val receivedBlockedIntervals = mutableListOf() + val receivedBlockedTimeRanges = mutableListOf() verifyOrder { - listener.onBlockedTimeRangeReached(capture(receivedBlockedIntervals)) - listener.onBlockedTimeRangeReached(capture(receivedBlockedIntervals)) + listener.onBlockedTimeRangeReached(capture(receivedBlockedTimeRanges)) + listener.onBlockedTimeRangeReached(capture(receivedBlockedTimeRanges)) } - assertEquals(expectedBlockedIntervals, receivedBlockedIntervals.reversed()) + assertEquals(expectedBlockedIntervals, receivedBlockedTimeRanges.reversed()) } @Test - fun `test block interval when player seek`() { + fun `test block time range when player seek`() { player.pause() val expectedBlockedIntervals = listOf(BlockedAssetLoader.SEGMENT) player.setMediaItem(BlockedAssetLoader.MEDIA_ONE_SEGMENT, BlockedAssetLoader.SEGMENT.start - 10) @@ -89,11 +89,11 @@ class BlockedTimeRangeTrackerTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) - val receivedBlockedIntervals = mutableListOf() + val receivedBlockedTimeRanges = mutableListOf() verify { - listener.onBlockedTimeRangeReached(capture(receivedBlockedIntervals)) + listener.onBlockedTimeRangeReached(capture(receivedBlockedTimeRanges)) } - assertEquals(expectedBlockedIntervals, receivedBlockedIntervals.reversed()) + assertEquals(expectedBlockedIntervals, receivedBlockedTimeRanges.reversed()) } } @@ -105,7 +105,7 @@ private class BlockedAssetLoader(context: Context) : AssetLoader(DefaultMediaSou override suspend fun loadAsset(mediaItem: MediaItem): Asset { val itemBuilder = mediaItem.buildUpon() - val blockedIntervals = when (mediaItem.mediaId) { + val timeRanges = when (mediaItem.mediaId) { MEDIA_ONE_SEGMENT.mediaId -> { listOf(SEGMENT) } @@ -121,7 +121,7 @@ private class BlockedAssetLoader(context: Context) : AssetLoader(DefaultMediaSou return Asset( mediaSource = mediaSourceFactory.createMediaSource(itemBuilder.build()), mediaMetadata = mediaItem.mediaMetadata, - blockedTimeRanges = blockedIntervals, + timeRanges = timeRanges, ) } @@ -132,8 +132,8 @@ private class BlockedAssetLoader(context: Context) : AssetLoader(DefaultMediaSou private const val URL = "https://rts-vod-amd.akamaized.net/ww/13317145/f1d49f18-f302-37ce-866c-1c1c9b76a824/master.m3u8" const val NEAR_END_POSITION_MS = 15_000L // the video has 17 sec duration - val START_SEGMENT = BlockedTimeRange("id:1", start = 0, end = 5, reason = "reason") - val SEGMENT = BlockedTimeRange("id:2", start = 10, end = 13, reason = "reason") + val START_SEGMENT = BlockedTimeRange(id = "id:1", start = 0, end = 5, reason = "reason") + val SEGMENT = BlockedTimeRange(id = "id:2", start = 10, end = 13, reason = "reason") private fun createMediaItem(mediaId: String) = MediaItem.Builder() .setUri(URL) diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/TimeRangeTrackerTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/ChapterTrackerTest.kt similarity index 90% rename from pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/TimeRangeTrackerTest.kt rename to pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/ChapterTrackerTest.kt index 7eb19027c..b45b2e4fb 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/TimeRangeTrackerTest.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/ChapterTrackerTest.kt @@ -19,7 +19,7 @@ import ch.srgssr.pillarbox.player.PillarboxPlayer import ch.srgssr.pillarbox.player.SeekIncrement import ch.srgssr.pillarbox.player.asset.Asset import ch.srgssr.pillarbox.player.asset.AssetLoader -import ch.srgssr.pillarbox.player.asset.Chapter +import ch.srgssr.pillarbox.player.asset.timeRange.Chapter import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory import io.mockk.clearAllMocks import io.mockk.spyk @@ -31,7 +31,7 @@ import kotlin.test.Test import kotlin.test.assertEquals @RunWith(AndroidJUnit4::class) -class TimeRangeTrackerTest { +class ChapterTrackerTest { private lateinit var player: PillarboxExoPlayer private lateinit var fakeClock: FakeClock private lateinit var listener: PillarboxPlayer.Listener @@ -70,10 +70,10 @@ class TimeRangeTrackerTest { val expectedChapters = listOf(ChapterAssetLoader.CHAPTER_1, ChapterAssetLoader.CHAPTER_2) val receivedChapters = mutableListOf() verifyOrder { - listener.onCurrentChapterChanged(capture(receivedChapters)) - listener.onCurrentChapterChanged(null) - listener.onCurrentChapterChanged(capture(receivedChapters)) - listener.onCurrentChapterChanged(null) + listener.onChapterChanged(capture(receivedChapters)) + listener.onChapterChanged(null) + listener.onChapterChanged(capture(receivedChapters)) + listener.onChapterChanged(null) } assertEquals(expectedChapters, receivedChapters.reversed()) } @@ -93,7 +93,7 @@ class TimeRangeTrackerTest { val expectedChapters = listOf(ChapterAssetLoader.CHAPTER_2) val receivedChapters = mutableListOf() verifyOrder { - listener.onCurrentChapterChanged(capture(receivedChapters)) + listener.onChapterChanged(capture(receivedChapters)) } assertEquals(expectedChapters, receivedChapters.reversed()) } @@ -110,7 +110,7 @@ private class ChapterAssetLoader(context: Context) : AssetLoader(DefaultMediaSou return Asset( mediaSource = mediaSourceFactory.createMediaSource(itemBuilder.build()), mediaMetadata = mediaItem.mediaMetadata, - chapters = listOf(CHAPTER_1, CHAPTER_2) + timeRanges = listOf(CHAPTER_1, CHAPTER_2) ) } diff --git a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/extension/ComposablePlayer.kt b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/extension/ComposablePlayer.kt index b7bb2e997..1fc91668b 100644 --- a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/extension/ComposablePlayer.kt +++ b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/extension/ComposablePlayer.kt @@ -23,22 +23,22 @@ import androidx.media3.common.Player import androidx.media3.common.Player.Commands import androidx.media3.common.VideoSize import ch.srgssr.pillarbox.player.DefaultUpdateInterval -import ch.srgssr.pillarbox.player.asset.Chapter -import ch.srgssr.pillarbox.player.asset.SkipableTimeRange +import ch.srgssr.pillarbox.player.asset.timeRange.Chapter +import ch.srgssr.pillarbox.player.asset.timeRange.Credit import ch.srgssr.pillarbox.player.availableCommandsAsFlow import ch.srgssr.pillarbox.player.currentBufferedPercentageAsFlow import ch.srgssr.pillarbox.player.currentMediaMetadataAsFlow import ch.srgssr.pillarbox.player.currentPositionAsFlow import ch.srgssr.pillarbox.player.durationAsFlow import ch.srgssr.pillarbox.player.extension.getChapterAtPosition +import ch.srgssr.pillarbox.player.extension.getCreditAtPosition import ch.srgssr.pillarbox.player.extension.getCurrentMediaItems import ch.srgssr.pillarbox.player.extension.getPlaybackSpeed -import ch.srgssr.pillarbox.player.extension.getSkipableTimeRangeAtPosition import ch.srgssr.pillarbox.player.getAspectRatioAsFlow import ch.srgssr.pillarbox.player.getCurrentChapterAsFlow +import ch.srgssr.pillarbox.player.getCurrentCreditAsFlow import ch.srgssr.pillarbox.player.getCurrentMediaItemIndexAsFlow import ch.srgssr.pillarbox.player.getCurrentMediaItemsAsFlow -import ch.srgssr.pillarbox.player.getCurrentSkipableTimeRangeAsFlow import ch.srgssr.pillarbox.player.getPlaybackSpeedAsFlow import ch.srgssr.pillarbox.player.isCurrentMediaItemLiveAsFlow import ch.srgssr.pillarbox.player.isPlayingAsFlow @@ -267,12 +267,12 @@ fun Player.getCurrentChapterAsState(): State { } /** - * @return Get the current time range as state, when the time interval changes. + * @return Get the current credit as state, when the current credit changes. */ @Composable -fun Player.getCurrentTimeRangeAsState(): State { +fun Player.getCurrentCreditAsState(): State { val flow = remember(this) { - getCurrentSkipableTimeRangeAsFlow() + getCurrentCreditAsFlow() } - return flow.collectAsState(initial = getSkipableTimeRangeAtPosition()) + return flow.collectAsState(initial = getCreditAtPosition()) }