Commit d5a934aa by bachinger Committed by Tianyi Feng

Keep content timeline and ad playback states together

For multi-period live streams the content timeline for
which the global ad playback state has been split needs
to be kept together to not run into a race between
timeline refreshes and ad events.

PiperOrigin-RevId: 520358964
parent 975c2e72
...@@ -700,7 +700,8 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou ...@@ -700,7 +700,8 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
splitAdPlaybackStates = ImmutableMap.of(periodUid, adPlaybackState); splitAdPlaybackStates = ImmutableMap.of(periodUid, adPlaybackState);
} }
streamPlayer.setAdPlaybackStates(adsId, splitAdPlaybackStates, contentTimeline); streamPlayer.setAdPlaybackStates(adsId, splitAdPlaybackStates, contentTimeline);
checkNotNull(serverSideAdInsertionMediaSource).setAdPlaybackStates(splitAdPlaybackStates); checkNotNull(serverSideAdInsertionMediaSource)
.setAdPlaybackStates(splitAdPlaybackStates, contentTimeline);
if (!isLiveStream) { if (!isLiveStream) {
adsLoader.setAdPlaybackState(adsId, adPlaybackState); adsLoader.setAdPlaybackState(adsId, adPlaybackState);
} }
......
...@@ -89,8 +89,8 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource ...@@ -89,8 +89,8 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
* Called when the content source has refreshed the timeline. * Called when the content source has refreshed the timeline.
* *
* <p>If true is returned the source refresh publication is deferred, to wait for an {@link * <p>If true is returned the source refresh publication is deferred, to wait for an {@link
* #setAdPlaybackStates(ImmutableMap)} ad playback state update}. If false is returned, the * #setAdPlaybackStates(ImmutableMap, Timeline)} ad playback state update}. If false is
* source refresh is immediately published. * returned, the source refresh is immediately published.
* *
* <p>Called on the playback thread. * <p>Called on the playback thread.
* *
...@@ -112,7 +112,6 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource ...@@ -112,7 +112,6 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
private Handler playbackHandler; private Handler playbackHandler;
@Nullable private SharedMediaPeriod lastUsedMediaPeriod; @Nullable private SharedMediaPeriod lastUsedMediaPeriod;
@Nullable private Timeline contentTimeline;
private ImmutableMap<Object, AdPlaybackState> adPlaybackStates; private ImmutableMap<Object, AdPlaybackState> adPlaybackStates;
/** /**
...@@ -136,8 +135,7 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource ...@@ -136,8 +135,7 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
/** /**
* Sets the map of {@link AdPlaybackState ad playback states} published by this source. The key is * Sets the map of {@link AdPlaybackState ad playback states} published by this source. The key is
* the period UID of a period in the {@link * the period UID of a period in the {@code contentTimeline}.
* AdPlaybackStateUpdater#onAdPlaybackStateUpdateRequested(Timeline)} content timeline}.
* *
* <p>Each period has an {@link AdPlaybackState} that tells where in the period the ad groups * <p>Each period has an {@link AdPlaybackState} that tells where in the period the ad groups
* start and end. Must only contain server-side inserted ad groups. The number of ad groups and * start and end. Must only contain server-side inserted ad groups. The number of ad groups and
...@@ -148,8 +146,11 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource ...@@ -148,8 +146,11 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
* <p>May be called from any thread. * <p>May be called from any thread.
* *
* @param adPlaybackStates The map of {@link AdPlaybackState} keyed by their period UID. * @param adPlaybackStates The map of {@link AdPlaybackState} keyed by their period UID.
* @param contentTimeline The content timeline containing the periods with the UIDs used as keys
* in the map of playback states.
*/ */
public void setAdPlaybackStates(ImmutableMap<Object, AdPlaybackState> adPlaybackStates) { public void setAdPlaybackStates(
ImmutableMap<Object, AdPlaybackState> adPlaybackStates, Timeline contentTimeline) {
checkArgument(!adPlaybackStates.isEmpty()); checkArgument(!adPlaybackStates.isEmpty());
Object adsId = checkNotNull(adPlaybackStates.values().asList().get(0).adsId); Object adsId = checkNotNull(adPlaybackStates.values().asList().get(0).adsId);
for (Map.Entry<Object, AdPlaybackState> entry : adPlaybackStates.entrySet()) { for (Map.Entry<Object, AdPlaybackState> entry : adPlaybackStates.entrySet()) {
...@@ -185,7 +186,6 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource ...@@ -185,7 +186,6 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
if (playbackHandler == null) { if (playbackHandler == null) {
this.adPlaybackStates = adPlaybackStates; this.adPlaybackStates = adPlaybackStates;
} else { } else {
Timeline finalContentTimeline = contentTimeline;
playbackHandler.post( playbackHandler.post(
() -> { () -> {
for (SharedMediaPeriod mediaPeriod : mediaPeriods.values()) { for (SharedMediaPeriod mediaPeriod : mediaPeriods.values()) {
...@@ -204,10 +204,8 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource ...@@ -204,10 +204,8 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
} }
} }
this.adPlaybackStates = adPlaybackStates; this.adPlaybackStates = adPlaybackStates;
if (finalContentTimeline != null) { refreshSourceInfo(
refreshSourceInfo( new ServerSideAdInsertionTimeline(contentTimeline, adPlaybackStates));
new ServerSideAdInsertionTimeline(finalContentTimeline, adPlaybackStates));
}
}); });
} }
} }
...@@ -247,7 +245,6 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource ...@@ -247,7 +245,6 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
@Override @Override
public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) {
this.contentTimeline = timeline;
if ((adPlaybackStateUpdater == null if ((adPlaybackStateUpdater == null
|| !adPlaybackStateUpdater.onAdPlaybackStateUpdateRequested(timeline)) || !adPlaybackStateUpdater.onAdPlaybackStateUpdateRequested(timeline))
&& !adPlaybackStates.isEmpty()) { && !adPlaybackStates.isEmpty()) {
...@@ -258,7 +255,6 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource ...@@ -258,7 +255,6 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
@Override @Override
protected void releaseSourceInternal() { protected void releaseSourceInternal() {
releaseLastUsedMediaPeriod(); releaseLastUsedMediaPeriod();
contentTimeline = null;
synchronized (this) { synchronized (this) {
playbackHandler = null; playbackHandler = null;
} }
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2;
import static com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.AUDIO_FORMAT; import static com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.AUDIO_FORMAT;
import static com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.VIDEO_FORMAT; import static com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.VIDEO_FORMAT;
import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US;
import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
...@@ -51,6 +52,7 @@ import com.google.android.exoplayer2.upstream.Allocator; ...@@ -51,6 +52,7 @@ import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.HandlerWrapper;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before; import org.junit.Before;
...@@ -1562,13 +1564,28 @@ public final class MediaPeriodQueueTest { ...@@ -1562,13 +1564,28 @@ public final class MediaPeriodQueueTest {
private static Timeline createMultiPeriodServerSideInsertedTimeline( private static Timeline createMultiPeriodServerSideInsertedTimeline(
Object windowId, int numberOfPlayedAds, boolean... isAdPeriodFlags) Object windowId, int numberOfPlayedAds, boolean... isAdPeriodFlags)
throws InterruptedException { throws InterruptedException {
FakeTimeline timeline = FakeTimeline fakeContentTimeline =
FakeTimeline.createMultiPeriodAdTimeline(windowId, numberOfPlayedAds, isAdPeriodFlags); new FakeTimeline(
new TimelineWindowDefinition(
isAdPeriodFlags.length,
windowId,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ DEFAULT_WINDOW_DURATION_US,
/* defaultPositionUs= */ 0,
/* windowOffsetInFirstPeriodUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
/* adPlaybackStates= */ ImmutableList.of(AdPlaybackState.NONE),
MediaItem.EMPTY));
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
FakeTimeline.createMultiPeriodAdTimeline(windowId, numberOfPlayedAds, isAdPeriodFlags)
.getAdPlaybackStates(/* windowIndex= */ 0);
ServerSideAdInsertionMediaSource serverSideAdInsertionMediaSource = ServerSideAdInsertionMediaSource serverSideAdInsertionMediaSource =
new ServerSideAdInsertionMediaSource( new ServerSideAdInsertionMediaSource(
new FakeMediaSource(timeline, VIDEO_FORMAT, AUDIO_FORMAT), contentTimeline -> false); new FakeMediaSource(fakeContentTimeline, VIDEO_FORMAT, AUDIO_FORMAT),
serverSideAdInsertionMediaSource.setAdPlaybackStates( contentTimeline -> false);
timeline.getAdPlaybackStates(/* windowIndex= */ 0)); serverSideAdInsertionMediaSource.setAdPlaybackStates(adPlaybackStates, fakeContentTimeline);
AtomicReference<Timeline> serverSideAdInsertionTimelineRef = new AtomicReference<>(); AtomicReference<Timeline> serverSideAdInsertionTimelineRef = new AtomicReference<>();
CountDownLatch countDownLatch = new CountDownLatch(/* count= */ 1); CountDownLatch countDownLatch = new CountDownLatch(/* count= */ 1);
serverSideAdInsertionMediaSource.prepareSource( serverSideAdInsertionMediaSource.prepareSource(
......
...@@ -54,6 +54,7 @@ import com.google.android.exoplayer2.testutil.FakeClock; ...@@ -54,6 +54,7 @@ import com.google.android.exoplayer2.testutil.FakeClock;
import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Rule; import org.junit.Rule;
...@@ -109,7 +110,8 @@ public final class ServerSideAdInsertionMediaSourceTest { ...@@ -109,7 +110,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
.withContentResumeOffsetUs(/* adGroupIndex= */ 1, /* contentResumeOffsetUs= */ 400_000) .withContentResumeOffsetUs(/* adGroupIndex= */ 1, /* contentResumeOffsetUs= */ 400_000)
.withContentResumeOffsetUs(/* adGroupIndex= */ 2, /* contentResumeOffsetUs= */ 200_000); .withContentResumeOffsetUs(/* adGroupIndex= */ 2, /* contentResumeOffsetUs= */ 200_000);
AtomicReference<Timeline> timelineReference = new AtomicReference<>(); AtomicReference<Timeline> timelineReference = new AtomicReference<>();
mediaSource.setAdPlaybackStates(ImmutableMap.of(new Pair<>(0, 0), adPlaybackState)); mediaSource.setAdPlaybackStates(
ImmutableMap.of(new Pair<>(0, 0), adPlaybackState), wrappedTimeline);
mediaSource.prepareSource( mediaSource.prepareSource(
(source, timeline) -> timelineReference.set(timeline), (source, timeline) -> timelineReference.set(timeline),
...@@ -190,7 +192,8 @@ public final class ServerSideAdInsertionMediaSourceTest { ...@@ -190,7 +192,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
wrappedTimeline.getPeriod( wrappedTimeline.getPeriod(
/* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true) /* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true)
.uid, .uid,
adPlaybackState)); adPlaybackState),
wrappedTimeline);
mediaSource.prepareSource( mediaSource.prepareSource(
(source, timeline) -> timelineReference.set(timeline), (source, timeline) -> timelineReference.set(timeline),
...@@ -230,12 +233,13 @@ public final class ServerSideAdInsertionMediaSourceTest { ...@@ -230,12 +233,13 @@ public final class ServerSideAdInsertionMediaSourceTest {
@Test @Test
public void timeline_missingAdPlaybackStateByPeriodUid_isAssertedAndThrows() { public void timeline_missingAdPlaybackStateByPeriodUid_isAssertedAndThrows() {
FakeMediaSource contentSource = new FakeMediaSource();
ServerSideAdInsertionMediaSource mediaSource = ServerSideAdInsertionMediaSource mediaSource =
new ServerSideAdInsertionMediaSource( new ServerSideAdInsertionMediaSource(contentSource, /* adPlaybackStateUpdater= */ null);
new FakeMediaSource(), /* adPlaybackStateUpdater= */ null);
// The map of adPlaybackStates does not contain a valid period UID as key. // The map of adPlaybackStates does not contain a valid period UID as key.
mediaSource.setAdPlaybackStates( mediaSource.setAdPlaybackStates(
ImmutableMap.of(new Object(), new AdPlaybackState(/* adsId= */ new Object()))); ImmutableMap.of(new Object(), new AdPlaybackState(/* adsId= */ new Object())),
contentSource.getInitialTimeline());
Assert.assertThrows( Assert.assertThrows(
IllegalStateException.class, IllegalStateException.class,
...@@ -291,7 +295,8 @@ public final class ServerSideAdInsertionMediaSourceTest { ...@@ -291,7 +295,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
.uid); .uid);
mediaSourceRef mediaSourceRef
.get() .get()
.setAdPlaybackStates(ImmutableMap.of(periodUid, firstAdPlaybackState)); .setAdPlaybackStates(
ImmutableMap.of(periodUid, firstAdPlaybackState), contentTimeline);
return true; return true;
})); }));
...@@ -337,6 +342,7 @@ public final class ServerSideAdInsertionMediaSourceTest { ...@@ -337,6 +342,7 @@ public final class ServerSideAdInsertionMediaSourceTest {
/* contentResumeOffsetUs= */ 0, /* contentResumeOffsetUs= */ 0,
/* adDurationsUs...= */ 100_000); /* adDurationsUs...= */ 100_000);
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>(); AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
ArrayList<Timeline> contentTimelines = new ArrayList<>();
mediaSourceRef.set( mediaSourceRef.set(
new ServerSideAdInsertionMediaSource( new ServerSideAdInsertionMediaSource(
new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)),
...@@ -346,9 +352,11 @@ public final class ServerSideAdInsertionMediaSourceTest { ...@@ -346,9 +352,11 @@ public final class ServerSideAdInsertionMediaSourceTest {
contentTimeline.getPeriod( contentTimeline.getPeriod(
/* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true) /* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true)
.uid)); .uid));
contentTimelines.add(contentTimeline);
mediaSourceRef mediaSourceRef
.get() .get()
.setAdPlaybackStates(ImmutableMap.of(periodUid.get(), firstAdPlaybackState)); .setAdPlaybackStates(
ImmutableMap.of(periodUid.get(), firstAdPlaybackState), contentTimeline);
return true; return true;
})); }));
AnalyticsListener listener = mock(AnalyticsListener.class); AnalyticsListener listener = mock(AnalyticsListener.class);
...@@ -366,7 +374,8 @@ public final class ServerSideAdInsertionMediaSourceTest { ...@@ -366,7 +374,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
/* adDurationsUs...= */ 500_000); /* adDurationsUs...= */ 500_000);
mediaSourceRef mediaSourceRef
.get() .get()
.setAdPlaybackStates(ImmutableMap.of(periodUid.get(), secondAdPlaybackState)); .setAdPlaybackStates(
ImmutableMap.of(periodUid.get(), secondAdPlaybackState), contentTimelines.get(1));
runUntilPendingCommandsAreFullyHandled(player); runUntilPendingCommandsAreFullyHandled(player);
player.play(); player.play();
...@@ -375,6 +384,7 @@ public final class ServerSideAdInsertionMediaSourceTest { ...@@ -375,6 +384,7 @@ public final class ServerSideAdInsertionMediaSourceTest {
// Assert all samples have been played. // Assert all samples have been played.
DumpFileAsserts.assertOutput(context, playbackOutput, TEST_ASSET_DUMP); DumpFileAsserts.assertOutput(context, playbackOutput, TEST_ASSET_DUMP);
assertThat(contentTimelines).hasSize(2);
// Assert playback has been reported with ads: [content][ad0][content][ad1][content] // Assert playback has been reported with ads: [content][ad0][content][ad1][content]
// 5*2(audio+video) format changes, 4 discontinuities between parts. // 5*2(audio+video) format changes, 4 discontinuities between parts.
verify(listener, times(4)) verify(listener, times(4))
...@@ -408,10 +418,12 @@ public final class ServerSideAdInsertionMediaSourceTest { ...@@ -408,10 +418,12 @@ public final class ServerSideAdInsertionMediaSourceTest {
/* contentResumeOffsetUs= */ 0, /* contentResumeOffsetUs= */ 0,
/* adDurationsUs...= */ 500_000); /* adDurationsUs...= */ 500_000);
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>(); AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
ArrayList<Timeline> contentTimelines = new ArrayList<>();
mediaSourceRef.set( mediaSourceRef.set(
new ServerSideAdInsertionMediaSource( new ServerSideAdInsertionMediaSource(
new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)),
/* adPlaybackStateUpdater= */ contentTimeline -> { /* adPlaybackStateUpdater= */ contentTimeline -> {
contentTimelines.add(contentTimeline);
if (periodUid.get() == null) { if (periodUid.get() == null) {
periodUid.set( periodUid.set(
checkNotNull( checkNotNull(
...@@ -420,7 +432,8 @@ public final class ServerSideAdInsertionMediaSourceTest { ...@@ -420,7 +432,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
.uid)); .uid));
mediaSourceRef mediaSourceRef
.get() .get()
.setAdPlaybackStates(ImmutableMap.of(periodUid.get(), firstAdPlaybackState)); .setAdPlaybackStates(
ImmutableMap.of(periodUid.get(), firstAdPlaybackState), contentTimeline);
} }
return true; return true;
})); }));
...@@ -439,7 +452,8 @@ public final class ServerSideAdInsertionMediaSourceTest { ...@@ -439,7 +452,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
/* adGroupIndex= */ 0, /* adDurationsUs...= */ 50_000, 250_000, 200_000); /* adGroupIndex= */ 0, /* adDurationsUs...= */ 50_000, 250_000, 200_000);
mediaSourceRef mediaSourceRef
.get() .get()
.setAdPlaybackStates(ImmutableMap.of(periodUid.get(), secondAdPlaybackState)); .setAdPlaybackStates(
ImmutableMap.of(periodUid.get(), secondAdPlaybackState), contentTimelines.get(1));
runUntilPendingCommandsAreFullyHandled(player); runUntilPendingCommandsAreFullyHandled(player);
player.play(); player.play();
...@@ -448,6 +462,7 @@ public final class ServerSideAdInsertionMediaSourceTest { ...@@ -448,6 +462,7 @@ public final class ServerSideAdInsertionMediaSourceTest {
// Assert all samples have been played. // Assert all samples have been played.
DumpFileAsserts.assertOutput(context, playbackOutput, TEST_ASSET_DUMP); DumpFileAsserts.assertOutput(context, playbackOutput, TEST_ASSET_DUMP);
assertThat(contentTimelines).hasSize(2);
// Assert playback has been reported with ads: [ad0][ad1][ad2][content] // Assert playback has been reported with ads: [ad0][ad1][ad2][content]
// 4*2(audio+video) format changes, 3 discontinuities between parts. // 4*2(audio+video) format changes, 3 discontinuities between parts.
verify(listener, times(3)) verify(listener, times(3))
...@@ -500,7 +515,8 @@ public final class ServerSideAdInsertionMediaSourceTest { ...@@ -500,7 +515,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
.uid); .uid);
mediaSourceRef mediaSourceRef
.get() .get()
.setAdPlaybackStates(ImmutableMap.of(periodUid, firstAdPlaybackState)); .setAdPlaybackStates(
ImmutableMap.of(periodUid, firstAdPlaybackState), contentTimeline);
return true; return true;
})); }));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment