Skip to content

Commit 8b80e40

Browse files
authored
Merge pull request #9 from bitmovin/atomic-ad-playback-tracker
Atomic ad playback tracker state updates
2 parents 738a70f + f9ba104 commit 8b80e40

File tree

3 files changed

+60
-21
lines changed

3 files changed

+60
-21
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Race condition that could cause `IndexOutOfBoundsException` when `MediaTailorAdBreak`s got removed during playback
13+
1014
## [0.1.1] - 2025-07-11
1115

1216
### Fixed

mediatailor/src/main/java/com/bitmovin/player/integration/mediatailor/AdPlaybackTracker.kt

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import kotlinx.coroutines.Dispatchers
1111
import kotlinx.coroutines.cancel
1212
import kotlinx.coroutines.flow.MutableStateFlow
1313
import kotlinx.coroutines.flow.StateFlow
14+
import kotlinx.coroutines.flow.collectLatest
1415
import kotlinx.coroutines.flow.merge
1516
import kotlinx.coroutines.flow.update
1617
import kotlinx.coroutines.launch
@@ -47,33 +48,36 @@ internal class DefaultAdPlaybackTracker(
4748

4849
init {
4950
scope.launch {
50-
player.eventFlow<PlayerEvent.TimeChanged>().collect {
51-
trackAdBreaks()
52-
}
53-
}
54-
scope.launch {
55-
merge(
56-
// When seeking or time shifting, the calculated ad breaks are most likely outdated
57-
player.eventFlow<PlayerEvent.Seeked>(),
58-
player.eventFlow<PlayerEvent.TimeShifted>(),
59-
// In case ad breaks are updated before the tail the indices might point to an invalid ad break
60-
mediaTailorSession.adBreaks,
61-
).collect {
51+
stateShouldBeInvalidatedFlow().collectLatest {
6252
currentAdBreakIndex = 0
6353
currentAdIndex = 0
64-
trackAdBreaks()
54+
val adBreaks = mediaTailorSession.adBreaks.value
55+
56+
player.eventFlow<PlayerEvent.TimeChanged>().collect {
57+
trackAdBreaks(adBreaks)
58+
}
6559
}
6660
}
6761
}
6862

63+
private fun stateShouldBeInvalidatedFlow() = merge(
64+
// When seeking or time shifting, the calculated ad breaks are most likely outdated
65+
player.eventFlow<PlayerEvent.Seeked>(),
66+
player.eventFlow<PlayerEvent.TimeShifted>(),
67+
// In case ad breaks are updated before the tail the indices might point to an invalid ad break
68+
mediaTailorSession.adBreaks,
69+
)
70+
6971
/**
7072
* This function uses [currentAdBreakIndex] and [currentAdIndex] to track the current position
7173
* of the [playingAdBreak] and [nextAdBreak] to avoid unnecessary iterations over all ad breaks.
7274
*
7375
* It assumes that ad breaks are sorted by schedule time.
7476
*/
75-
private fun trackAdBreaks() {
76-
val adBreaks = mediaTailorSession.adBreaks.value.takeIf { it.isNotEmpty() } ?: return
77+
private fun trackAdBreaks(adBreaks: List<MediaTailorAdBreak>) {
78+
if (adBreaks.isEmpty()) {
79+
return
80+
}
7781

7882
while (currentAdBreakIndex < adBreaks.lastIndex &&
7983
player.currentTime >= adBreaks[currentAdBreakIndex].endTime

mediatailor/src/test/java/com/bitmovin/player/integration/mediatailor/AdPlaybackTrackerSpec.kt

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,11 @@ class AdPlaybackTrackerSpec : DescribeSpec({
8888

8989
describe("with an ad break in the present") {
9090
lateinit var adBreak: MediaTailorAdBreak
91+
lateinit var timeChangedFlow: MutableSharedFlow<PlayerEvent.TimeChanged>
92+
9193
beforeEach {
92-
createPlayer(currentTime = 5.0)
94+
timeChangedFlow = MutableSharedFlow()
95+
createPlayer(currentTime = 5.0, timeChangedFlow)
9396
adBreak = MediaTailorAdBreak(
9497
id = "adBreak1",
9598
ads = listOf(
@@ -107,6 +110,7 @@ class AdPlaybackTrackerSpec : DescribeSpec({
107110
adMarkerDuration = "5",
108111
)
109112
createAdPlaybackTracker(listOf(adBreak))
113+
timeChangedFlow.emit(PlayerEvent.TimeChanged(5.0))
110114
}
111115

112116
it("currentAdBreak is the correct adBreak") {
@@ -116,6 +120,7 @@ class AdPlaybackTrackerSpec : DescribeSpec({
116120

117121
describe("with an ad break in the past") {
118122
lateinit var adBreak: MediaTailorAdBreak
123+
119124
beforeEach {
120125
createPlayer(currentTime = 5.0)
121126
adBreak = MediaTailorAdBreak(
@@ -136,8 +141,11 @@ class AdPlaybackTrackerSpec : DescribeSpec({
136141

137142
describe("with an ad break in the future") {
138143
lateinit var adBreak: MediaTailorAdBreak
144+
lateinit var timeChangedFlow: MutableSharedFlow<PlayerEvent.TimeChanged>
145+
139146
beforeEach {
140-
createPlayer(currentTime = 5.0)
147+
timeChangedFlow = MutableSharedFlow()
148+
createPlayer(currentTime = 5.0, timeChangedFlow)
141149
adBreak = MediaTailorAdBreak(
142150
id = "adBreak1",
143151
ads = listOf(),
@@ -147,6 +155,7 @@ class AdPlaybackTrackerSpec : DescribeSpec({
147155
adMarkerDuration = "5",
148156
)
149157
createAdPlaybackTracker(listOf(adBreak))
158+
timeChangedFlow.emit(PlayerEvent.TimeChanged(5.0))
150159
}
151160

152161
it("currentAdBreak is null") {
@@ -161,8 +170,11 @@ class AdPlaybackTrackerSpec : DescribeSpec({
161170
describe("with ad break in the past future and current ad break") {
162171
lateinit var currentAdBreak: MediaTailorAdBreak
163172
lateinit var nextAdBreak: MediaTailorAdBreak
173+
lateinit var timeChangedFlow: MutableSharedFlow<PlayerEvent.TimeChanged>
174+
164175
beforeEach {
165-
createPlayer(currentTime = 5.0)
176+
timeChangedFlow = MutableSharedFlow()
177+
createPlayer(currentTime = 5.0, timeChangedFlow)
166178
currentAdBreak = MediaTailorAdBreak(
167179
id = "adBreak1",
168180
ads = listOf(
@@ -196,6 +208,7 @@ class AdPlaybackTrackerSpec : DescribeSpec({
196208
adMarkerDuration = "5",
197209
)
198210
createAdPlaybackTracker(listOf(currentAdBreak, nextAdBreak))
211+
timeChangedFlow.emit(PlayerEvent.TimeChanged(5.0))
199212
}
200213

201214
it("currentAdBreak is the correct adBreak") {
@@ -239,9 +252,13 @@ class AdPlaybackTrackerSpec : DescribeSpec({
239252
)
240253

241254
describe("when the player time is at first ad") {
255+
lateinit var timeChangedFlow: MutableSharedFlow<PlayerEvent.TimeChanged>
256+
242257
beforeEach {
243-
createPlayer(currentTime = 5.0)
258+
timeChangedFlow = MutableSharedFlow()
259+
createPlayer(currentTime = 5.0, timeChangedFlow)
244260
createAdPlaybackTracker(listOf(adBreak))
261+
timeChangedFlow.emit(PlayerEvent.TimeChanged(5.0))
245262
}
246263

247264
it("current ad is the first ad") {
@@ -254,9 +271,13 @@ class AdPlaybackTrackerSpec : DescribeSpec({
254271
}
255272

256273
describe("when the player time is at second ad") {
274+
lateinit var timeChangedFlow: MutableSharedFlow<PlayerEvent.TimeChanged>
275+
257276
beforeEach {
258-
createPlayer(currentTime = 10.0)
277+
timeChangedFlow = MutableSharedFlow()
278+
createPlayer(currentTime = 10.0, timeChangedFlow)
259279
createAdPlaybackTracker(listOf(adBreak))
280+
timeChangedFlow.emit(PlayerEvent.TimeChanged(10.0))
260281
}
261282

262283
it("current ad is the second ad") {
@@ -269,9 +290,13 @@ class AdPlaybackTrackerSpec : DescribeSpec({
269290
}
270291

271292
describe("when the player time is at third ad") {
293+
lateinit var timeChangedFlow: MutableSharedFlow<PlayerEvent.TimeChanged>
294+
272295
beforeEach {
273-
createPlayer(currentTime = 15.0)
296+
timeChangedFlow = MutableSharedFlow()
297+
createPlayer(currentTime = 15.0, timeChangedFlow)
274298
createAdPlaybackTracker(listOf(adBreak))
299+
timeChangedFlow.emit(PlayerEvent.TimeChanged(15.0))
275300
}
276301

277302
it("current ad is the third ad") {
@@ -287,6 +312,7 @@ class AdPlaybackTrackerSpec : DescribeSpec({
287312

288313
describe("when player receives seeking and time shifting events") {
289314
val timeShiftedFlow = MutableSharedFlow<PlayerEvent.TimeShifted>()
315+
val timeChangedFlow = MutableSharedFlow<PlayerEvent.TimeChanged>()
290316
val seekedFlow = MutableSharedFlow<PlayerEvent.Seeked>()
291317
lateinit var firstAdBreak: MediaTailorAdBreak
292318
lateinit var secondAdBreak: MediaTailorAdBreak
@@ -296,6 +322,7 @@ class AdPlaybackTrackerSpec : DescribeSpec({
296322
currentTime = 5.0,
297323
timeShiftedFlow = timeShiftedFlow,
298324
seekedFlow = seekedFlow,
325+
timeChangedFlow = timeChangedFlow,
299326
)
300327
firstAdBreak = MediaTailorAdBreak(
301328
id = "adBreak1",
@@ -330,6 +357,7 @@ class AdPlaybackTrackerSpec : DescribeSpec({
330357
adMarkerDuration = "5",
331358
)
332359
createAdPlaybackTracker(listOf(firstAdBreak, secondAdBreak))
360+
timeChangedFlow.emit(PlayerEvent.TimeChanged(5.0))
333361
}
334362

335363
describe("before the time shift or seek") {
@@ -346,6 +374,7 @@ class AdPlaybackTrackerSpec : DescribeSpec({
346374
beforeEach {
347375
// Time has to change otherwise the same ad breaks will be calculated again
348376
every { player.currentTime } returns 50.0
377+
timeChangedFlow.emit(PlayerEvent.TimeChanged(50.0))
349378
}
350379

351380
it("resets the state when time shifted") {
@@ -503,6 +532,8 @@ class AdPlaybackTrackerSpec : DescribeSpec({
503532

504533
describe("when the player starts in the second ad in the first ad break") {
505534
it("selects the correct ad index initially") {
535+
timeChangedFlow.emit(PlayerEvent.TimeChanged(1.5))
536+
506537
expectThat(adPlaybackTracker.playingAdBreak.value?.adBreak).isEqualTo(firstAdBreak)
507538
expectThat(adPlaybackTracker.playingAdBreak.value?.adIndex).isEqualTo(1)
508539
}

0 commit comments

Comments
 (0)