Skip to content

Commit c2a4330

Browse files
StaehliJMGaetan89
andauthored
451 handle chapters and blocked segments (#501)
Co-authored-by: Gaëtan Muller <[email protected]>
1 parent 7bfecc3 commit c2a4330

File tree

46 files changed

+1752
-194
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1752
-194
lines changed

pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/Chapter.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@ import kotlinx.serialization.Serializable
1616
* @property lead
1717
* @property description
1818
* @property blockReason
19+
* @property fullLengthUrn
20+
* @property fullLengthMarkIn
21+
* @property fullLengthMarkOut
1922
* @property listSegment
2023
* @property listResource
2124
* @property comScoreAnalyticsLabels
2225
* @property analyticsLabels
26+
* @constructor Create empty Chapter
2327
*/
2428
@Serializable
2529
data class Chapter(
@@ -29,11 +33,19 @@ data class Chapter(
2933
val lead: String? = null,
3034
val description: String? = null,
3135
val blockReason: BlockReason? = null,
36+
val fullLengthUrn: String? = null,
37+
val fullLengthMarkIn: Long? = null,
38+
val fullLengthMarkOut: Long? = null,
3239
@SerialName("segmentList")
3340
val listSegment: List<Segment>? = null,
3441
@SerialName("resourceList") val listResource: List<Resource>? = null,
3542
@SerialName("analyticsData")
3643
override val comScoreAnalyticsLabels: Map<String, String>? = null,
3744
@SerialName("analyticsMetadata")
3845
override val analyticsLabels: Map<String, String>? = null,
39-
) : DataWithAnalytics
46+
) : DataWithAnalytics {
47+
/**
48+
* If it is a full length chapter.
49+
*/
50+
val isFullLengthChapter: Boolean = fullLengthUrn.isNullOrBlank()
51+
}

pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/Segment.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,18 @@ import kotlinx.serialization.Serializable
99
/**
1010
* Segment
1111
*
12+
* @property urn
13+
* @property title
14+
* @property markIn
15+
* @property markOut
1216
* @property blockReason
17+
* @constructor Create empty Segment
1318
*/
1419
@Serializable
15-
data class Segment(val blockReason: BlockReason? = null)
20+
data class Segment(
21+
val urn: String,
22+
val title: String,
23+
val markIn: Long,
24+
val markOut: Long,
25+
val blockReason: BlockReason? = null
26+
)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright (c) SRG SSR. All rights reserved.
3+
* License information is available from the LICENSE file.
4+
*/
5+
package ch.srgssr.pillarbox.core.business.source
6+
7+
import android.net.Uri
8+
import androidx.media3.common.MediaMetadata
9+
import ch.srgssr.pillarbox.core.business.integrationlayer.ImageScalingService
10+
import ch.srgssr.pillarbox.core.business.integrationlayer.data.Chapter
11+
import ch.srgssr.pillarbox.core.business.integrationlayer.data.MediaComposition
12+
13+
internal object ChapterAdapter {
14+
private val imageScalingService = ImageScalingService()
15+
16+
fun toChapter(chapter: Chapter): ch.srgssr.pillarbox.player.asset.Chapter {
17+
requireNotNull(chapter.fullLengthMarkIn)
18+
requireNotNull(chapter.fullLengthMarkOut)
19+
return ch.srgssr.pillarbox.player.asset.Chapter(
20+
id = chapter.urn,
21+
start = chapter.fullLengthMarkIn,
22+
end = chapter.fullLengthMarkOut,
23+
mediaMetadata = MediaMetadata.Builder()
24+
.setTitle(chapter.title)
25+
.setArtworkUri(Uri.parse(imageScalingService.getScaledImageUrl(chapter.imageUrl)))
26+
.setDescription(chapter.lead)
27+
.build()
28+
)
29+
}
30+
31+
fun getChapters(mediaComposition: MediaComposition): List<ch.srgssr.pillarbox.player.asset.Chapter> {
32+
val mainChapter = mediaComposition.mainChapter
33+
if (!mainChapter.isFullLengthChapter) return emptyList()
34+
return mediaComposition.listChapter
35+
.filter {
36+
it != mediaComposition.mainChapter
37+
}
38+
.map {
39+
toChapter(it)
40+
}
41+
.sortedBy { it.start }
42+
}
43+
}

pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SRGAssetLoader.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import ch.srgssr.pillarbox.core.business.tracker.commandersact.CommandersActTrac
3131
import ch.srgssr.pillarbox.core.business.tracker.comscore.ComScoreTracker
3232
import ch.srgssr.pillarbox.player.asset.Asset
3333
import ch.srgssr.pillarbox.player.asset.AssetLoader
34-
import ch.srgssr.pillarbox.player.extension.getMediaItemTrackerData
34+
import ch.srgssr.pillarbox.player.extension.pillarboxData
3535
import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerData
3636
import io.ktor.client.plugins.ClientRequestException
3737
import kotlinx.serialization.SerializationException
@@ -134,16 +134,14 @@ class SRGAssetLoader(
134134
chapter.blockReason?.let {
135135
throw BlockReasonException(it)
136136
}
137-
chapter.listSegment?.firstNotNullOfOrNull { it.blockReason }?.let {
138-
throw BlockReasonException(it)
139-
}
140137

141138
val resource = resourceSelector.selectResourceFromChapter(chapter) ?: throw ResourceNotFoundException()
142139
var uri = Uri.parse(resource.url)
143140
if (resource.tokenType == Resource.TokenType.AKAMAI) {
144141
uri = AkamaiTokenDataSource.appendTokenQueryToUri(uri)
145142
}
146-
val trackerData = mediaItem.getMediaItemTrackerData().buildUpon().apply {
143+
// TODO Shouldn't we always recreate trackers data?
144+
val trackerData = mediaItem.pillarboxData.trackersData.buildUpon().apply {
147145
trackerDataProvider?.provide(this, resource, chapter, result)
148146
putData(SRGEventLoggerTracker::class.java)
149147
getComScoreData(result, chapter, resource)?.let {
@@ -168,7 +166,9 @@ class SRGAssetLoader(
168166
resource = resource,
169167
mediaComposition = result,
170168
)
171-
}.build()
169+
}.build(),
170+
chapters = ChapterAdapter.getChapters(result),
171+
blockedIntervals = SegmentAdapter.getBlockedIntervals(chapter.listSegment)
172172
)
173173
}
174174

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright (c) SRG SSR. All rights reserved.
3+
* License information is available from the LICENSE file.
4+
*/
5+
package ch.srgssr.pillarbox.core.business.source
6+
7+
import ch.srgssr.pillarbox.core.business.integrationlayer.data.Segment
8+
import ch.srgssr.pillarbox.player.asset.BlockedInterval
9+
10+
internal object SegmentAdapter {
11+
12+
fun getBlockedInterval(segment: Segment): BlockedInterval {
13+
requireNotNull(segment.blockReason)
14+
return BlockedInterval(segment.urn, segment.markIn, segment.markOut, segment.blockReason.toString())
15+
}
16+
17+
fun getBlockedIntervals(listSegment: List<Segment>?): List<BlockedInterval> {
18+
return listSegment?.filter { it.blockReason != null }?.map {
19+
getBlockedInterval(it)
20+
} ?: emptyList()
21+
}
22+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Copyright (c) SRG SSR. All rights reserved.
3+
* License information is available from the LICENSE file.
4+
*/
5+
package ch.srgssr.pillarbox.core.business
6+
7+
import android.net.Uri
8+
import androidx.media3.common.MediaMetadata
9+
import androidx.test.ext.junit.runners.AndroidJUnit4
10+
import ch.srgssr.pillarbox.core.business.integrationlayer.ImageScalingService
11+
import ch.srgssr.pillarbox.core.business.integrationlayer.data.Chapter
12+
import ch.srgssr.pillarbox.core.business.integrationlayer.data.MediaComposition
13+
import ch.srgssr.pillarbox.core.business.source.ChapterAdapter
14+
import org.junit.runner.RunWith
15+
import kotlin.test.Test
16+
import kotlin.test.assertEquals
17+
18+
@RunWith(AndroidJUnit4::class)
19+
class ChapterAdapterTest {
20+
21+
@Test(expected = IllegalArgumentException::class)
22+
fun `main chapter to asset chapter throw exception`() {
23+
val chapter = Chapter(
24+
urn = "urn",
25+
title = "title",
26+
lead = "lead",
27+
description = "description",
28+
imageUrl = "https://www.rts.ch/image.png"
29+
)
30+
ChapterAdapter.toChapter(chapter)
31+
}
32+
33+
@Test(expected = IllegalArgumentException::class)
34+
fun `main chapter with fullLengthMarkIn only to asset chapter throw exception`() {
35+
val chapter = Chapter(
36+
urn = "urn",
37+
title = "title",
38+
lead = "lead",
39+
description = "description",
40+
imageUrl = "https://www.rts.ch/image.png",
41+
fullLengthMarkIn = 10
42+
)
43+
ChapterAdapter.toChapter(chapter)
44+
}
45+
46+
@Test
47+
fun `chapter to asset chapter`() {
48+
val chapter = Chapter(
49+
urn = "urn",
50+
title = "title",
51+
lead = "lead",
52+
description = "description",
53+
imageUrl = "https://www.rts.ch/image.png",
54+
fullLengthMarkIn = 10,
55+
fullLengthMarkOut = 100
56+
)
57+
val expected = ch.srgssr.pillarbox.player.asset.Chapter(
58+
id = "urn",
59+
start = 10,
60+
end = 100,
61+
mediaMetadata = MediaMetadata.Builder()
62+
.setTitle("title")
63+
.setDescription("lead")
64+
.setArtworkUri(Uri.parse(ImageScalingService().getScaledImageUrl("https://www.rts.ch/image.png")))
65+
.build()
66+
)
67+
assertEquals(expected, ChapterAdapter.toChapter(chapter))
68+
}
69+
70+
@Test
71+
fun `only main chapter return empty asset chapter list`() {
72+
val mainChapter = Chapter(
73+
urn = "urn",
74+
title = "title",
75+
lead = "lead",
76+
description = "description",
77+
imageUrl = "https://www.rts.ch/image.png"
78+
)
79+
val mediaComposition = MediaComposition(
80+
chapterUrn = mainChapter.urn,
81+
listChapter = listOf(mainChapter)
82+
)
83+
assertEquals(emptyList(), ChapterAdapter.getChapters(mediaComposition))
84+
}
85+
86+
@Test
87+
fun `main chapter with chapters return asset chapter list without main chapter`() {
88+
val mainChapter = Chapter(
89+
urn = "urn",
90+
title = "title",
91+
lead = "lead",
92+
description = "description",
93+
imageUrl = "https://www.rts.ch/image.png"
94+
)
95+
val chapter1 = mainChapter.copy(urn = "urn:chapitre1", fullLengthMarkIn = 0, fullLengthMarkOut = 10, fullLengthUrn = "urn")
96+
val chapter2 = mainChapter.copy(urn = "urn:chapitre2", fullLengthMarkIn = 30, fullLengthMarkOut = 60, fullLengthUrn = "urn")
97+
val mediaComposition = MediaComposition(
98+
chapterUrn = mainChapter.urn,
99+
listChapter = listOf(mainChapter, chapter1, chapter2)
100+
)
101+
val expected = listOf(ChapterAdapter.toChapter(chapter1), ChapterAdapter.toChapter(chapter2))
102+
assertEquals(expected, ChapterAdapter.getChapters(mediaComposition))
103+
}
104+
105+
@Test
106+
fun `chapter with chapters return empty asset chapter list`() {
107+
val fullLengthChapter = Chapter(
108+
urn = "urn",
109+
title = "title",
110+
lead = "lead",
111+
description = "description",
112+
imageUrl = "https://www.rts.ch/image.png"
113+
)
114+
val chapter1 = fullLengthChapter.copy(urn = "urn:chapitre1", fullLengthMarkIn = 0, fullLengthMarkOut = 10, fullLengthUrn = "urn")
115+
val chapter2 = fullLengthChapter.copy(urn = "urn:chapitre2", fullLengthMarkIn = 30, fullLengthMarkOut = 60, fullLengthUrn = "urn")
116+
val mediaComposition = MediaComposition(
117+
chapterUrn = "urn:chapitre1",
118+
listChapter = listOf(fullLengthChapter, chapter1, chapter2)
119+
)
120+
assertEquals(emptyList(), ChapterAdapter.getChapters(mediaComposition))
121+
}
122+
}

pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/SRGAssetLoaderTest.kt

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import ch.srgssr.pillarbox.core.business.integrationlayer.data.Resource
2121
import ch.srgssr.pillarbox.core.business.integrationlayer.data.Segment
2222
import ch.srgssr.pillarbox.core.business.integrationlayer.service.MediaCompositionService
2323
import ch.srgssr.pillarbox.core.business.source.SRGAssetLoader
24+
import ch.srgssr.pillarbox.core.business.source.SegmentAdapter
2425
import kotlinx.coroutines.test.runTest
2526
import org.junit.Assert.assertEquals
2627
import org.junit.runner.RunWith
@@ -138,11 +139,13 @@ class SRGAssetLoaderTest {
138139
)
139140
}
140141

141-
@Test(expected = BlockReasonException::class)
142-
fun testBlockedSegment() = runTest {
143-
assetLoader.loadAsset(
142+
@Test
143+
fun testBlockedSegmentFillAssetBlockedIntervals() = runTest {
144+
val asset = assetLoader.loadAsset(
144145
SRGMediaItemBuilder(DummyMediaCompositionProvider.URN_SEGMENT_BLOCK_REASON).build()
145146
)
147+
val expectedBlockIntervals = listOf(SegmentAdapter.getBlockedInterval(DummyMediaCompositionProvider.BLOCKED_SEGMENT))
148+
assertEquals(expectedBlockIntervals, asset.blockedIntervals)
146149
}
147150

148151
internal class DummyMediaCompositionProvider : MediaCompositionService {
@@ -166,33 +169,34 @@ class SRGAssetLoaderTest {
166169
description = "Description",
167170
listResource = listOf(createResource(Resource.Type.HLS)),
168171
imageUrl = DUMMY_IMAGE_URL,
169-
listSegment = listOf(Segment(), Segment())
172+
listSegment = listOf(SEGMENT_1, SEGMENT_2)
170173
)
171174
Result.success(MediaComposition(chapterUrn = urn, listChapter = listOf(chapter)))
172175
}
173176

174177
URN_BLOCK_REASON -> {
175-
val chapter = Chapter(
178+
val mainChapter = Chapter(
176179
urn = urn,
177180
title = "Blocked media",
178181
blockReason = BlockReason.UNKNOWN,
179182
listResource = listOf(createResource(Resource.Type.HLS)),
180183
imageUrl = DUMMY_IMAGE_URL,
181-
listSegment = listOf(Segment(), Segment())
184+
listSegment = listOf(SEGMENT_1, SEGMENT_2)
182185
)
183-
Result.success(MediaComposition(chapterUrn = urn, listChapter = listOf(chapter)))
186+
187+
Result.success(MediaComposition(chapterUrn = urn, listChapter = listOf(mainChapter)))
184188
}
185189

186190
URN_SEGMENT_BLOCK_REASON -> {
187-
val chapter = Chapter(
191+
val mainChapter = Chapter(
188192
urn = urn,
189193
title = "Blocked segment media",
190194
blockReason = null,
191195
listResource = listOf(createResource(Resource.Type.HLS)),
192196
imageUrl = DUMMY_IMAGE_URL,
193-
listSegment = listOf(Segment(), Segment(blockReason = BlockReason.UNKNOWN))
197+
listSegment = listOf(SEGMENT_1, BLOCKED_SEGMENT)
194198
)
195-
Result.success(MediaComposition(chapterUrn = urn, listChapter = listOf(chapter)))
199+
Result.success(MediaComposition(chapterUrn = urn, listChapter = listOf(mainChapter, CHAPTER_1, CHAPTER_2)))
196200
}
197201

198202
else -> Result.failure(IllegalArgumentException("No resource found"))
@@ -208,6 +212,47 @@ class SRGAssetLoaderTest {
208212
const val URN_BLOCK_REASON = "urn:rts:video:block_reason"
209213
const val URN_SEGMENT_BLOCK_REASON = "urn:rts:video:segment_block_reason"
210214
const val DUMMY_IMAGE_URL = "https://image.png"
215+
val SEGMENT_1 = Segment(
216+
urn = "s1",
217+
title = "title",
218+
markIn = 0,
219+
markOut = 1
220+
)
221+
val SEGMENT_2 = Segment(
222+
urn = "s2",
223+
title = "title",
224+
markIn = 2,
225+
markOut = 3
226+
)
227+
val BLOCKED_SEGMENT = Segment(
228+
urn = "blocked",
229+
title = "Blocked segment",
230+
markIn = 4,
231+
markOut = 5,
232+
blockReason = BlockReason.UNKNOWN,
233+
)
234+
235+
val CHAPTER_1 = Chapter(
236+
urn = "urn:chapter1",
237+
title = "Blocked segment media",
238+
blockReason = null,
239+
listResource = listOf(createResource(Resource.Type.HLS)),
240+
imageUrl = DUMMY_IMAGE_URL,
241+
fullLengthUrn = "urn:full_length",
242+
fullLengthMarkIn = 0,
243+
fullLengthMarkOut = 10
244+
)
245+
246+
val CHAPTER_2 = Chapter(
247+
urn = "urn:chapter2",
248+
title = "Blocked segment media",
249+
blockReason = null,
250+
listResource = listOf(createResource(Resource.Type.HLS)),
251+
imageUrl = DUMMY_IMAGE_URL,
252+
fullLengthUrn = "urn:full_length",
253+
fullLengthMarkIn = 20,
254+
fullLengthMarkOut = 30
255+
)
211256

212257
fun createMediaComposition(urn: String, listResource: List<Resource>?): MediaComposition {
213258
return MediaComposition(urn, listOf(Chapter(urn = urn, title = urn, listResource = listResource, imageUrl = DUMMY_IMAGE_URL)))

0 commit comments

Comments
 (0)