Version
Media3 main branch
More version details
1.10.1 (latest stable at the time of writing)
Devices that reproduce the issue
Any.
Reproduced deterministically in a Robolectric unit test and on physical devices (Android 14) playing a live DASH stream with AWS MediaTailor server-side ad insertion via ServerSideAdInsertionMediaSource.
Devices that do not reproduce the issue
None
Reproducible in the demo app?
Not tested
Reproduction steps
Use case: client-side ad tracking for a server-side-stitched stream (e.g. AWS MediaTailor).
The stitcher tells the client at which stream positions its tracking beacons (impression, quartiles, complete) are due; createMessage().setPosition(...) is the natural API to be notified at those positions.
Every position that falls inside an ad period is silently ignored.
Prepare a ServerSideAdInsertionMediaSource wrapping a live, dynamic, multi-period timeline.
Mark one mid-window period as a single whole-period server-side ad group
Send a PlayerMessage with a position inside that ad period.
Play from the window start to past the ad period.
The message is never delivered.
A control message positioned inside a content period of the same stream is delivered normally.
import androidx.media3.common.AdPlaybackState
import androidx.media3.common.Player
import androidx.media3.common.Timeline
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionMediaSource
import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil
import androidx.media3.test.utils.FakeMediaSource
import androidx.media3.test.utils.FakeTimeline
import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition
import androidx.media3.test.utils.TestExoPlayerBuilder
import androidx.media3.test.utils.robolectric.TestPlayerRunHelper
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.collect.ImmutableMap
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import java.util.concurrent.atomic.AtomicBoolean
@RunWith(AndroidJUnit4::class)
@Config(sdk = [33])
class Media3SsaiPlayerMessageReproTest {
private lateinit var player: ExoPlayer
@After
fun tearDown() {
player.release()
}
@Test
fun `a player message inside a server-side inserted ad period is delivered when playback passes it`() {
// window: ├─ p0 content (0..20s) ─┼─ p1 ad (20..40s) ─┼─ p2 content (40..60s) ─┤
// message at window position 30s, inside the server-side ad period.
prepareSsai()
val delivered = AtomicBoolean(false)
player
.createMessage { _, _ -> delivered.set(true) }
.setPosition(30_000)
.setDeleteAfterDelivery(true)
.send()
TestPlayerRunHelper.play(player).untilPosition(0, 50_000)
// FAILS
assertTrue("message at 30s (inside the server-side-inserted ad period) was never delivered", delivered.get())
}
@Test
fun `control - a player message in a content period is delivered when playback passes it`() {
// Identical setup; the message sits at 10s, inside content period p0. PASSES.
prepareSsai()
val delivered = AtomicBoolean(false)
player
.createMessage { _, _ -> delivered.set(true) }
.setPosition(10_000)
.setDeleteAfterDelivery(true)
.send()
TestPlayerRunHelper.play(player).untilPosition(0, 15_000)
assertTrue("message at 10s (inside a content period) was never delivered", delivered.get())
}
private fun prepareSsai() {
player = TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build()
val timeline = liveTimeline()
val content = FakeMediaSource(timeline)
val ssai = ServerSideAdInsertionMediaSource(content) { false }
ssai.setAdPlaybackStates(adPlaybackStates(timeline, adPeriodIndex = 1), timeline)
player.setMediaSource(ssai)
player.prepare()
TestPlayerRunHelper.advance(player).untilState(Player.STATE_READY)
}
/** FakeTimeline period uids are Pair(windowUid, periodIndex): stable across refreshes. */
private fun liveTimeline(): Timeline =
FakeTimeline(
TimelineWindowDefinition.Builder()
.setUid("window")
.setLive(true)
.setDynamic(true)
.setSeekable(true)
.setPeriodCount(3)
.setDurationUs(60_000_000)
.setWindowStartTimeUs(1_720_000_000_000_000)
.setWindowPositionInFirstPeriodUs(0)
.setDefaultPositionUs(0)
.build(),
)
/** One state per period, keyed by period uid (`ServerSideAdInsertionTimeline` requires full coverage). */
private fun adPlaybackStates(timeline: Timeline, adPeriodIndex: Int): ImmutableMap<Any, AdPlaybackState> {
val builder = ImmutableMap.builder<Any, AdPlaybackState>()
for (periodIndex in 0 until timeline.periodCount) {
builder.put(
timeline.getUidOfPeriod(periodIndex),
if (periodIndex == adPeriodIndex) {
ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState(
AdPlaybackState(ADS_ID),
/* fromPositionUs = */ 0,
/* contentResumeOffsetUs = */ 20_000_000,
/* adDurationsUs... = */ 20_000_000,
)
} else {
AdPlaybackState(ADS_ID)
},
)
}
return builder.build()
}
private companion object {
const val ADS_ID = "repro-ads-id"
}
}
Expected result
The message is delivered when playback passes its position.
Unlike client-side ad insertion, content time does not freeze during a server-side-inserted ad the stitched ad occupies real stream time and the playhead sweeps the message position.
Actual result
The message stays pending forever.
Media
Not applicable
Bug Report
Version
Media3 main branch
More version details
1.10.1 (latest stable at the time of writing)
Devices that reproduce the issue
Any.
Reproduced deterministically in a Robolectric unit test and on physical devices (Android 14) playing a live DASH stream with AWS MediaTailor server-side ad insertion via ServerSideAdInsertionMediaSource.
Devices that do not reproduce the issue
None
Reproducible in the demo app?
Not tested
Reproduction steps
Use case: client-side ad tracking for a server-side-stitched stream (e.g. AWS MediaTailor).
The stitcher tells the client at which stream positions its tracking beacons (impression, quartiles, complete) are due; createMessage().setPosition(...) is the natural API to be notified at those positions.
Every position that falls inside an ad period is silently ignored.
Prepare a
ServerSideAdInsertionMediaSourcewrapping a live, dynamic, multi-period timeline.Mark one mid-window period as a single whole-period server-side ad group
Send a
PlayerMessagewith a position inside that ad period.Play from the window start to past the ad period.
The message is never delivered.
A control message positioned inside a content period of the same stream is delivered normally.
Expected result
The message is delivered when playback passes its position.
Unlike client-side ad insertion, content time does not freeze during a server-side-inserted ad the stitched ad occupies real stream time and the playhead sweeps the message position.
Actual result
The message stays pending forever.
Media
Not applicable
Bug Report
adb bugreportto android-media-github@google.com after filing this issue.