Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for "Skip intro"/"Skip credits" #516

Merged
merged 8 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config/detekt/detekt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,7 @@ style:
UnusedPrivateMember:
active: true
allowedNames: '(_|ignored|expected|serialVersionUID)'
ignoreAnnotated: [ 'Preview' ]
ignoreAnnotated: [ 'Preview', 'PreviewLightDark' ]
UseAnyOrNoneInsteadOfFind:
active: true
UseArrayLiteralsInAnnotations:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import kotlinx.serialization.Serializable
* @property listResource
* @property comScoreAnalyticsLabels
* @property analyticsLabels
* @property timeIntervalList
* @constructor Create empty Chapter
*/
@Serializable
Expand All @@ -45,6 +46,7 @@ data class Chapter(
override val comScoreAnalyticsLabels: Map<String, String>? = null,
@SerialName("analyticsMetadata")
override val analyticsLabels: Map<String, String>? = null,
val timeIntervalList: List<TimeInterval>? = null,
) : DataWithAnalytics {
/**
* If it is a full length chapter.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.core.business.integrationlayer.data

import kotlinx.serialization.Serializable

/**
* Time interval
*
* @property markIn
* @property markOut
* @property type
*/
@Serializable
data class TimeInterval(
val markIn: Long?,
val markOut: Long?,
val type: TimeIntervalType?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.core.business.integrationlayer.data

/**
* Time interval type
*/
enum class TimeIntervalType {
CLOSING_CREDITS,
OPENING_CREDITS,
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ class SRGAssetLoader(
)
}.build(),
chapters = ChapterAdapter.getChapters(result),
blockedIntervals = SegmentAdapter.getBlockedIntervals(chapter.listSegment)
blockedTimeRanges = SegmentAdapter.getBlockedTimeRanges(chapter.listSegment),
timeRanges = TimeIntervalAdapter.getTimeIntervals(result.mainChapter.timeIntervalList),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@
package ch.srgssr.pillarbox.core.business.source

import ch.srgssr.pillarbox.core.business.integrationlayer.data.Segment
import ch.srgssr.pillarbox.player.asset.BlockedInterval
import ch.srgssr.pillarbox.player.asset.BlockedTimeRange

internal object SegmentAdapter {

fun getBlockedInterval(segment: Segment): BlockedInterval {
fun getBlockedTimeRange(segment: Segment): BlockedTimeRange {
requireNotNull(segment.blockReason)
return BlockedInterval(segment.urn, segment.markIn, segment.markOut, segment.blockReason.toString())
return BlockedTimeRange(segment.urn, segment.markIn, segment.markOut, segment.blockReason.toString())
}

fun getBlockedIntervals(listSegment: List<Segment>?): List<BlockedInterval> {
fun getBlockedTimeRanges(listSegment: List<Segment>?): List<BlockedTimeRange> {
return listSegment?.filter { it.blockReason != null }?.map {
getBlockedInterval(it)
getBlockedTimeRange(it)
} ?: emptyList()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.core.business.source

import ch.srgssr.pillarbox.core.business.integrationlayer.data.TimeInterval
import ch.srgssr.pillarbox.player.asset.SkipableTimeRange

internal object TimeIntervalAdapter {
internal fun getTimeIntervals(timeIntervals: List<TimeInterval>?): List<SkipableTimeRange> {
return timeIntervals
.orEmpty()
.mapNotNull { it.toSkipableTimeInterval() }
}

internal fun TimeInterval.toSkipableTimeInterval(): SkipableTimeRange? {
return if (type == null || markIn == null || markOut == null) {
null
} else {
SkipableTimeRange(
id = type.name,
start = markIn,
end = markOut,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ import ch.srgssr.pillarbox.core.business.integrationlayer.data.MediaComposition
import ch.srgssr.pillarbox.core.business.integrationlayer.data.MediaType
import ch.srgssr.pillarbox.core.business.integrationlayer.data.Resource
import ch.srgssr.pillarbox.core.business.integrationlayer.data.Segment
import ch.srgssr.pillarbox.core.business.integrationlayer.data.TimeInterval
import ch.srgssr.pillarbox.core.business.integrationlayer.data.TimeIntervalType
import ch.srgssr.pillarbox.core.business.integrationlayer.service.MediaCompositionService
import ch.srgssr.pillarbox.core.business.source.SRGAssetLoader
import ch.srgssr.pillarbox.core.business.source.SegmentAdapter
import ch.srgssr.pillarbox.core.business.source.TimeIntervalAdapter
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.runner.RunWith
Expand Down Expand Up @@ -145,8 +148,19 @@ class SRGAssetLoaderTest {
val asset = assetLoader.loadAsset(
SRGMediaItemBuilder(DummyMediaCompositionProvider.URN_SEGMENT_BLOCK_REASON).build()
)
val expectedBlockIntervals = listOf(SegmentAdapter.getBlockedInterval(DummyMediaCompositionProvider.BLOCKED_SEGMENT))
assertEquals(expectedBlockIntervals, asset.blockedIntervals)
val expectedBlockIntervals = listOf(SegmentAdapter.getBlockedTimeRange(DummyMediaCompositionProvider.BLOCKED_SEGMENT))
assertEquals(expectedBlockIntervals, asset.blockedTimeRanges)
}

@Test
fun testTimeIntervals() = runTest {
val asset = assetLoader.loadAsset(
SRGMediaItemBuilder(DummyMediaCompositionProvider.URN_TIME_INTERVALS).build()
)
val expectedTimeIntervals = TimeIntervalAdapter.getTimeIntervals(
listOf(DummyMediaCompositionProvider.TIME_INTERVAL_1, DummyMediaCompositionProvider.TIME_INTERVAL_2)
)
assertEquals(expectedTimeIntervals, asset.timeRanges)
}

internal class DummyMediaCompositionProvider : MediaCompositionService {
Expand Down Expand Up @@ -203,6 +217,19 @@ class SRGAssetLoaderTest {
Result.success(MediaComposition(chapterUrn = urn, listChapter = listOf(mainChapter, CHAPTER_1, CHAPTER_2)))
}

URN_TIME_INTERVALS -> {
val mainChapter = Chapter(
urn = urn,
title = "Time intervals",
listResource = listOf(createResource(Resource.Type.HLS)),
imageUrl = DUMMY_IMAGE_URL,
listSegment = null,
mediaType = MediaType.VIDEO,
timeIntervalList = listOf(TIME_INTERVAL_1, TIME_INTERVAL_2),
)
Result.success(MediaComposition(chapterUrn = urn, listChapter = listOf(mainChapter)))
}

else -> Result.failure(IllegalArgumentException("No resource found"))
}
}
Expand All @@ -215,6 +242,7 @@ class SRGAssetLoaderTest {
const val URN_INCOMPATIBLE_RESOURCE = "urn:rts:video:resource_incompatible"
const val URN_BLOCK_REASON = "urn:rts:video:block_reason"
const val URN_SEGMENT_BLOCK_REASON = "urn:rts:video:segment_block_reason"
const val URN_TIME_INTERVALS = "urn:rts:video:time_intervals"
const val DUMMY_IMAGE_URL = "https://image.png"
val SEGMENT_1 = Segment(
urn = "s1",
Expand All @@ -236,6 +264,17 @@ class SRGAssetLoaderTest {
blockReason = BlockReason.UNKNOWN,
)

val TIME_INTERVAL_1 = TimeInterval(
markIn = 10L,
markOut = 20L,
type = TimeIntervalType.OPENING_CREDITS,
)
val TIME_INTERVAL_2 = TimeInterval(
markIn = 40L,
markOut = 100L,
type = TimeIntervalType.CLOSING_CREDITS,
)

val CHAPTER_1 = Chapter(
urn = "urn:chapter1",
title = "Blocked segment media",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,28 @@ 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.BlockedInterval
import ch.srgssr.pillarbox.player.asset.BlockedTimeRange
import kotlin.test.Test
import kotlin.test.assertEquals

class SegmentAdapterTest {
@Test(expected = IllegalArgumentException::class)
fun `getBlockedInterval of a non blocked segment`() {
val segmentIl = Segment(urn = "urn1", title = "title 1", markIn = 1, markOut = 2, blockReason = null)
SegmentAdapter.getBlockedInterval(segmentIl)
SegmentAdapter.getBlockedTimeRange(segmentIl)
}

@Test
fun `getBlockedInterval of a blocked segment`() {
val segmentIl = Segment(urn = "urn1", title = "title 1", markIn = 1, markOut = 2, blockReason = BlockReason.UNKNOWN)
val expected = BlockedInterval(id = "urn1", start = 1, end = 2, reason = "UNKNOWN")
assertEquals(expected, SegmentAdapter.getBlockedInterval(segmentIl))
val expected = BlockedTimeRange(id = "urn1", start = 1, end = 2, reason = "UNKNOWN")
assertEquals(expected, SegmentAdapter.getBlockedTimeRange(segmentIl))
}

@Test
fun `empty segment list return empty blocked interval`() {
assertEquals(emptyList(), SegmentAdapter.getBlockedIntervals(null))
assertEquals(emptyList(), SegmentAdapter.getBlockedIntervals(emptyList()))
assertEquals(emptyList(), SegmentAdapter.getBlockedTimeRanges(null))
assertEquals(emptyList(), SegmentAdapter.getBlockedTimeRanges(emptyList()))
}

@Test
Expand All @@ -37,7 +37,7 @@ class SegmentAdapterTest {
Segment(urn = "urn1", title = "title 1", markIn = 1, markOut = 2),
Segment(urn = "urn2", title = "title 2", markIn = 3, markOut = 4),
)
assertEquals(emptyList(), SegmentAdapter.getBlockedIntervals(listSegments))
assertEquals(emptyList(), SegmentAdapter.getBlockedTimeRanges(listSegments))
}

@Test
Expand All @@ -49,9 +49,9 @@ class SegmentAdapterTest {
Segment(urn = "urn3", title = "title 3", markIn = 5, markOut = 56, blockReason = BlockReason.UNKNOWN),
)
val expected = listOf(
BlockedInterval(id = "urn1_blocked", start = 1, end = 4, reason = "LEGAL"),
BlockedInterval(id = "urn3", start = 5, end = 56, reason = "UNKNOWN"),
BlockedTimeRange(id = "urn1_blocked", start = 1, end = 4, reason = "LEGAL"),
BlockedTimeRange(id = "urn3", start = 5, end = 56, reason = "UNKNOWN"),
)
assertEquals(expected, SegmentAdapter.getBlockedIntervals(listSegments))
assertEquals(expected, SegmentAdapter.getBlockedTimeRanges(listSegments))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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)
}
}
1 change: 1 addition & 0 deletions pillarbox-demo-shared/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@
<string name="speed_value"><xliff:g example="1.5" id="speed">%1$s</xliff:g>×</string>
<string name="reset_to_default">Reset to default</string>
<string name="disabled">Disabled</string>
<string name="skip">Skip</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.media3.common.Player
import androidx.tv.material3.Button
import androidx.tv.material3.DrawerValue
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon
import androidx.tv.material3.IconButton
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import androidx.tv.material3.rememberDrawerState
import ch.srgssr.pillarbox.demo.shared.R
import ch.srgssr.pillarbox.demo.tv.extension.onDpadEvent
Expand All @@ -40,6 +42,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.playerErrorAsState
import ch.srgssr.pillarbox.ui.widget.maintainVisibleOnFocus
import ch.srgssr.pillarbox.ui.widget.player.PlayerSurface
Expand All @@ -61,6 +64,7 @@ fun PlayerView(
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val visibilityState = rememberDelayedVisibilityState(player = player, visible = true)
val timeInterval by player.getCurrentTimeRangeAsState()

LaunchedEffect(drawerState.currentValue) {
when (drawerState.currentValue) {
Expand Down Expand Up @@ -118,6 +122,14 @@ fun PlayerView(
}
}
}
AnimatedVisibility(timeInterval != null) {
Button(
onClick = { player.seekTo(timeInterval?.end ?: 0L) },
modifier = Modifier.padding(MaterialTheme.paddings.baseline),
) {
Text(text = stringResource(R.string.skip))
}
}
AnimatedVisibility(
visible = visibilityState.isVisible,
enter = expandVertically { it },
Expand Down
Loading
Loading