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
This commit is contained in:
parent
4b7875fe21
commit
f599a9b8f9
@ -16,6 +16,11 @@
|
||||
* Add parameters to `LoadControl` methods `shouldStartPlayback` and
|
||||
`onTracksSelected` that allow associating these methods with the
|
||||
relevant `MediaPeriod`.
|
||||
* Change signature of
|
||||
`ServerSideAdInsertionMediaSource.setAdPlaybackStates(Map<Object,
|
||||
AdPlaybackState>)` by adding a timeline parameter that contains the
|
||||
periods with the UIDs used as keys in the map. This is required to avoid
|
||||
concurrency issues with multi-period live streams.
|
||||
* Audio:
|
||||
* Fix bug where some playbacks fail when tunneling is enabled and
|
||||
`AudioProcessors` are active, e.g. for gapless trimming
|
||||
|
@ -92,8 +92,8 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
|
||||
* 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
|
||||
* #setAdPlaybackStates(ImmutableMap)} ad playback state update}. If false is returned, the
|
||||
* source refresh is immediately published.
|
||||
* #setAdPlaybackStates(ImmutableMap, Timeline)} ad playback state update}. If false is
|
||||
* returned, the source refresh is immediately published.
|
||||
*
|
||||
* <p>Called on the playback thread.
|
||||
*
|
||||
@ -115,7 +115,6 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
|
||||
private Handler playbackHandler;
|
||||
|
||||
@Nullable private SharedMediaPeriod lastUsedMediaPeriod;
|
||||
@Nullable private Timeline contentTimeline;
|
||||
private ImmutableMap<Object, AdPlaybackState> adPlaybackStates;
|
||||
|
||||
/**
|
||||
@ -139,8 +138,7 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
|
||||
|
||||
/**
|
||||
* 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
|
||||
* AdPlaybackStateUpdater#onAdPlaybackStateUpdateRequested(Timeline)} content timeline}.
|
||||
* the period UID of a period in the {@code contentTimeline}.
|
||||
*
|
||||
* <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
|
||||
@ -151,8 +149,11 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
|
||||
* <p>May be called from any thread.
|
||||
*
|
||||
* @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());
|
||||
Object adsId = checkNotNull(adPlaybackStates.values().asList().get(0).adsId);
|
||||
for (Map.Entry<Object, AdPlaybackState> entry : adPlaybackStates.entrySet()) {
|
||||
@ -188,7 +189,6 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
|
||||
if (playbackHandler == null) {
|
||||
this.adPlaybackStates = adPlaybackStates;
|
||||
} else {
|
||||
Timeline finalContentTimeline = contentTimeline;
|
||||
playbackHandler.post(
|
||||
() -> {
|
||||
for (SharedMediaPeriod mediaPeriod : mediaPeriods.values()) {
|
||||
@ -207,10 +207,8 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
|
||||
}
|
||||
}
|
||||
this.adPlaybackStates = adPlaybackStates;
|
||||
if (finalContentTimeline != null) {
|
||||
refreshSourceInfo(
|
||||
new ServerSideAdInsertionTimeline(finalContentTimeline, adPlaybackStates));
|
||||
}
|
||||
refreshSourceInfo(
|
||||
new ServerSideAdInsertionTimeline(contentTimeline, adPlaybackStates));
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -250,7 +248,6 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
|
||||
|
||||
@Override
|
||||
public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) {
|
||||
this.contentTimeline = timeline;
|
||||
if ((adPlaybackStateUpdater == null
|
||||
|| !adPlaybackStateUpdater.onAdPlaybackStateUpdateRequested(timeline))
|
||||
&& !adPlaybackStates.isEmpty()) {
|
||||
@ -261,7 +258,6 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
|
||||
@Override
|
||||
protected void releaseSourceInternal() {
|
||||
releaseLastUsedMediaPeriod();
|
||||
contentTimeline = null;
|
||||
synchronized (this) {
|
||||
playbackHandler = null;
|
||||
}
|
||||
|
@ -171,6 +171,7 @@ 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.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Range;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
@ -5050,25 +5051,40 @@ public final class ExoPlayerTest {
|
||||
ArgumentCaptor<PositionInfo> newPositionArgumentCaptor =
|
||||
ArgumentCaptor.forClass(PositionInfo.class);
|
||||
ArgumentCaptor<Integer> reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class);
|
||||
FakeTimeline adTimeline =
|
||||
// Create a multi-period timeline without ads.
|
||||
FakeTimeline fakeContentTimeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(
|
||||
/* periodCount= */ 4,
|
||||
"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));
|
||||
// Create the ad playback state matching to the periods in the content timeline.
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
FakeTimeline.createMultiPeriodAdTimeline(
|
||||
"windowId",
|
||||
/* numberOfPlayedAds= */ 0,
|
||||
/* isAdPeriodFlags...= */ false,
|
||||
true,
|
||||
true,
|
||||
false);
|
||||
"windowId",
|
||||
/* numberOfPlayedAds= */ 0,
|
||||
/* isAdPeriodFlags...= */ false,
|
||||
true,
|
||||
true,
|
||||
false)
|
||||
.getAdPlaybackStates(/* windowIndex= */ 0);
|
||||
Listener listener = mock(Listener.class);
|
||||
ExoPlayer player = new TestExoPlayerBuilder(context).build();
|
||||
player.addListener(listener);
|
||||
AtomicReference<ServerSideAdInsertionMediaSource> sourceReference = new AtomicReference<>();
|
||||
sourceReference.set(
|
||||
new ServerSideAdInsertionMediaSource(
|
||||
new FakeMediaSource(adTimeline),
|
||||
new FakeMediaSource(fakeContentTimeline),
|
||||
contentTimeline -> {
|
||||
sourceReference
|
||||
.get()
|
||||
.setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0));
|
||||
sourceReference.get().setAdPlaybackStates(adPlaybackStates, contentTimeline);
|
||||
return true;
|
||||
}));
|
||||
player.setMediaSource(sourceReference.get());
|
||||
@ -5141,25 +5157,40 @@ public final class ExoPlayerTest {
|
||||
ArgumentCaptor<PositionInfo> newPositionArgumentCaptor =
|
||||
ArgumentCaptor.forClass(PositionInfo.class);
|
||||
ArgumentCaptor<Integer> reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class);
|
||||
FakeTimeline adTimeline =
|
||||
// Create a multi-period timeline without ads.
|
||||
FakeTimeline fakeContentTimeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(
|
||||
/* periodCount= */ 4,
|
||||
"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));
|
||||
// Create the ad playback state matching to the periods in the content timeline.
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
FakeTimeline.createMultiPeriodAdTimeline(
|
||||
"windowId",
|
||||
/* numberOfPlayedAds= */ 0,
|
||||
/* isAdPeriodFlags...= */ false,
|
||||
true,
|
||||
false,
|
||||
false);
|
||||
"windowId",
|
||||
/* numberOfPlayedAds= */ 0,
|
||||
/* isAdPeriodFlags...= */ false,
|
||||
true,
|
||||
false,
|
||||
false)
|
||||
.getAdPlaybackStates(/* windowIndex= */ 0);
|
||||
Listener listener = mock(Listener.class);
|
||||
ExoPlayer player = new TestExoPlayerBuilder(context).build();
|
||||
player.addListener(listener);
|
||||
AtomicReference<ServerSideAdInsertionMediaSource> sourceReference = new AtomicReference<>();
|
||||
sourceReference.set(
|
||||
new ServerSideAdInsertionMediaSource(
|
||||
new FakeMediaSource(adTimeline),
|
||||
new FakeMediaSource(fakeContentTimeline),
|
||||
contentTimeline -> {
|
||||
sourceReference
|
||||
.get()
|
||||
.setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0));
|
||||
sourceReference.get().setAdPlaybackStates(adPlaybackStates, contentTimeline);
|
||||
return true;
|
||||
}));
|
||||
player.setMediaSource(sourceReference.get());
|
||||
@ -5203,25 +5234,40 @@ public final class ExoPlayerTest {
|
||||
ArgumentCaptor<PositionInfo> newPositionArgumentCaptor =
|
||||
ArgumentCaptor.forClass(PositionInfo.class);
|
||||
ArgumentCaptor<Integer> reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class);
|
||||
FakeTimeline adTimeline =
|
||||
// Create a multi-period timeline without ads.
|
||||
FakeTimeline fakeContentTimeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(
|
||||
/* periodCount= */ 4,
|
||||
"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));
|
||||
// Create the ad playback state matching to the periods in the content timeline.
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
FakeTimeline.createMultiPeriodAdTimeline(
|
||||
"windowId",
|
||||
/* numberOfPlayedAds= */ 0,
|
||||
/* isAdPeriodFlags...= */ false,
|
||||
true,
|
||||
true,
|
||||
false);
|
||||
"windowId",
|
||||
/* numberOfPlayedAds= */ 0,
|
||||
/* isAdPeriodFlags...= */ false,
|
||||
true,
|
||||
true,
|
||||
false)
|
||||
.getAdPlaybackStates(/* windowIndex= */ 0);
|
||||
Listener listener = mock(Listener.class);
|
||||
ExoPlayer player = new TestExoPlayerBuilder(context).build();
|
||||
player.addListener(listener);
|
||||
AtomicReference<ServerSideAdInsertionMediaSource> sourceReference = new AtomicReference<>();
|
||||
sourceReference.set(
|
||||
new ServerSideAdInsertionMediaSource(
|
||||
new FakeMediaSource(adTimeline),
|
||||
new FakeMediaSource(fakeContentTimeline),
|
||||
contentTimeline -> {
|
||||
sourceReference
|
||||
.get()
|
||||
.setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0));
|
||||
sourceReference.get().setAdPlaybackStates(adPlaybackStates, contentTimeline);
|
||||
return true;
|
||||
}));
|
||||
player.setMediaSource(sourceReference.get());
|
||||
@ -5270,25 +5316,40 @@ public final class ExoPlayerTest {
|
||||
ArgumentCaptor<PositionInfo> newPositionArgumentCaptor =
|
||||
ArgumentCaptor.forClass(PositionInfo.class);
|
||||
ArgumentCaptor<Integer> reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class);
|
||||
FakeTimeline adTimeline =
|
||||
// Create a multi-period timeline without ads.
|
||||
FakeTimeline fakeContentTimeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(
|
||||
/* periodCount= */ 4,
|
||||
"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));
|
||||
// Create the ad playback state matching to the periods in the content timeline.
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
FakeTimeline.createMultiPeriodAdTimeline(
|
||||
"windowId",
|
||||
/* numberOfPlayedAds= */ 0,
|
||||
/* isAdPeriodFlags...= */ false,
|
||||
true,
|
||||
true,
|
||||
false);
|
||||
"windowId",
|
||||
/* numberOfPlayedAds= */ 0,
|
||||
/* isAdPeriodFlags...= */ false,
|
||||
true,
|
||||
true,
|
||||
false)
|
||||
.getAdPlaybackStates(/* windowIndex= */ 0);
|
||||
Listener listener = mock(Listener.class);
|
||||
ExoPlayer player = new TestExoPlayerBuilder(context).build();
|
||||
player.addListener(listener);
|
||||
AtomicReference<ServerSideAdInsertionMediaSource> sourceReference = new AtomicReference<>();
|
||||
sourceReference.set(
|
||||
new ServerSideAdInsertionMediaSource(
|
||||
new FakeMediaSource(adTimeline),
|
||||
new FakeMediaSource(fakeContentTimeline),
|
||||
contentTimeline -> {
|
||||
sourceReference
|
||||
.get()
|
||||
.setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0));
|
||||
sourceReference.get().setAdPlaybackStates(adPlaybackStates, contentTimeline);
|
||||
return true;
|
||||
}));
|
||||
player.setMediaSource(sourceReference.get());
|
||||
@ -5351,25 +5412,40 @@ public final class ExoPlayerTest {
|
||||
ArgumentCaptor<PositionInfo> newPositionArgumentCaptor =
|
||||
ArgumentCaptor.forClass(PositionInfo.class);
|
||||
ArgumentCaptor<Integer> reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class);
|
||||
FakeTimeline adTimeline =
|
||||
// Create a multi-period timeline without ads.
|
||||
FakeTimeline fakeContentTimeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(
|
||||
/* periodCount= */ 4,
|
||||
"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));
|
||||
// Create the ad playback state matching to the periods in the content timeline.
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
FakeTimeline.createMultiPeriodAdTimeline(
|
||||
"windowId",
|
||||
/* numberOfPlayedAds= */ 2,
|
||||
/* isAdPeriodFlags...= */ false,
|
||||
true,
|
||||
true,
|
||||
false);
|
||||
"windowId",
|
||||
/* numberOfPlayedAds= */ 2,
|
||||
/* isAdPeriodFlags...= */ false,
|
||||
true,
|
||||
true,
|
||||
false)
|
||||
.getAdPlaybackStates(/* windowIndex= */ 0);
|
||||
Listener listener = mock(Listener.class);
|
||||
ExoPlayer player = new TestExoPlayerBuilder(context).build();
|
||||
player.addListener(listener);
|
||||
AtomicReference<ServerSideAdInsertionMediaSource> sourceReference = new AtomicReference<>();
|
||||
sourceReference.set(
|
||||
new ServerSideAdInsertionMediaSource(
|
||||
new FakeMediaSource(adTimeline),
|
||||
new FakeMediaSource(fakeContentTimeline),
|
||||
contentTimeline -> {
|
||||
sourceReference
|
||||
.get()
|
||||
.setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0));
|
||||
sourceReference.get().setAdPlaybackStates(adPlaybackStates, contentTimeline);
|
||||
return true;
|
||||
}));
|
||||
player.setMediaSource(sourceReference.get());
|
||||
@ -5406,29 +5482,44 @@ public final class ExoPlayerTest {
|
||||
ArgumentCaptor<PositionInfo> newPositionArgumentCaptor =
|
||||
ArgumentCaptor.forClass(PositionInfo.class);
|
||||
ArgumentCaptor<Integer> reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class);
|
||||
FakeTimeline adTimeline =
|
||||
// Create a multi-period timeline without ads.
|
||||
FakeTimeline fakeContentTimeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(
|
||||
/* periodCount= */ 8,
|
||||
"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));
|
||||
// Create the ad playback state matching to the periods in the content timeline.
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
FakeTimeline.createMultiPeriodAdTimeline(
|
||||
"windowId",
|
||||
/* numberOfPlayedAds= */ Integer.MAX_VALUE,
|
||||
/* isAdPeriodFlags...= */ true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true);
|
||||
"windowId",
|
||||
/* numberOfPlayedAds= */ Integer.MAX_VALUE,
|
||||
/* isAdPeriodFlags...= */ true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true)
|
||||
.getAdPlaybackStates(/* windowIndex= */ 0);
|
||||
Listener listener = mock(Listener.class);
|
||||
ExoPlayer player = new TestExoPlayerBuilder(context).build();
|
||||
player.addListener(listener);
|
||||
AtomicReference<ServerSideAdInsertionMediaSource> sourceReference = new AtomicReference<>();
|
||||
sourceReference.set(
|
||||
new ServerSideAdInsertionMediaSource(
|
||||
new FakeMediaSource(adTimeline, ExoPlayerTestRunner.AUDIO_FORMAT),
|
||||
new FakeMediaSource(fakeContentTimeline, ExoPlayerTestRunner.AUDIO_FORMAT),
|
||||
contentTimeline -> {
|
||||
sourceReference
|
||||
.get()
|
||||
.setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0));
|
||||
sourceReference.get().setAdPlaybackStates(adPlaybackStates, contentTimeline);
|
||||
return true;
|
||||
}));
|
||||
player.setMediaSource(sourceReference.get());
|
||||
@ -5508,9 +5599,10 @@ public final class ExoPlayerTest {
|
||||
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 2, /* adIndexInAdGroup+ */ 0);
|
||||
adPlaybackState =
|
||||
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 3, /* adIndexInAdGroup+ */ 0);
|
||||
FakeTimeline adTimeline =
|
||||
// Create a multi-period timeline without ads.
|
||||
FakeTimeline fakeContentTimeline =
|
||||
new FakeTimeline(
|
||||
new TimelineWindowDefinition(
|
||||
new FakeTimeline.TimelineWindowDefinition(
|
||||
/* periodCount= */ 1,
|
||||
"windowId",
|
||||
/* isSeekable= */ true,
|
||||
@ -5520,20 +5612,19 @@ public final class ExoPlayerTest {
|
||||
/* durationUs= */ DEFAULT_WINDOW_DURATION_US,
|
||||
/* defaultPositionUs= */ 0,
|
||||
/* windowOffsetInFirstPeriodUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
|
||||
/* adPlaybackStates= */ ImmutableList.of(adPlaybackState),
|
||||
/* adPlaybackStates= */ ImmutableList.of(AdPlaybackState.NONE),
|
||||
MediaItem.EMPTY));
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
ImmutableMap.of(/* period.uid */ new Pair<>("windowId", 0), adPlaybackState);
|
||||
Listener listener = mock(Listener.class);
|
||||
ExoPlayer player = new TestExoPlayerBuilder(context).build();
|
||||
player.addListener(listener);
|
||||
AtomicReference<ServerSideAdInsertionMediaSource> sourceReference = new AtomicReference<>();
|
||||
sourceReference.set(
|
||||
new ServerSideAdInsertionMediaSource(
|
||||
new FakeMediaSource(adTimeline, ExoPlayerTestRunner.AUDIO_FORMAT),
|
||||
new FakeMediaSource(fakeContentTimeline, ExoPlayerTestRunner.AUDIO_FORMAT),
|
||||
contentTimeline -> {
|
||||
sourceReference
|
||||
.get()
|
||||
.setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0));
|
||||
sourceReference.get().setAdPlaybackStates(adPlaybackStates, contentTimeline);
|
||||
return true;
|
||||
}));
|
||||
player.setMediaSource(sourceReference.get());
|
||||
|
@ -17,6 +17,7 @@ package androidx.media3.exoplayer;
|
||||
|
||||
import static androidx.media3.test.utils.ExoPlayerTestRunner.AUDIO_FORMAT;
|
||||
import static androidx.media3.test.utils.ExoPlayerTestRunner.VIDEO_FORMAT;
|
||||
import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US;
|
||||
import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
@ -57,6 +58,7 @@ import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.Before;
|
||||
@ -1568,13 +1570,28 @@ public final class MediaPeriodQueueTest {
|
||||
private static Timeline createMultiPeriodServerSideInsertedTimeline(
|
||||
Object windowId, int numberOfPlayedAds, boolean... isAdPeriodFlags)
|
||||
throws InterruptedException {
|
||||
FakeTimeline timeline =
|
||||
FakeTimeline.createMultiPeriodAdTimeline(windowId, numberOfPlayedAds, isAdPeriodFlags);
|
||||
FakeTimeline fakeContentTimeline =
|
||||
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 =
|
||||
new ServerSideAdInsertionMediaSource(
|
||||
new FakeMediaSource(timeline, VIDEO_FORMAT, AUDIO_FORMAT), contentTimeline -> false);
|
||||
serverSideAdInsertionMediaSource.setAdPlaybackStates(
|
||||
timeline.getAdPlaybackStates(/* windowIndex= */ 0));
|
||||
new FakeMediaSource(fakeContentTimeline, VIDEO_FORMAT, AUDIO_FORMAT),
|
||||
contentTimeline -> false);
|
||||
serverSideAdInsertionMediaSource.setAdPlaybackStates(adPlaybackStates, fakeContentTimeline);
|
||||
AtomicReference<Timeline> serverSideAdInsertionTimelineRef = new AtomicReference<>();
|
||||
CountDownLatch countDownLatch = new CountDownLatch(/* count= */ 1);
|
||||
serverSideAdInsertionMediaSource.prepareSource(
|
||||
|
@ -55,6 +55,7 @@ import androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Rule;
|
||||
@ -110,7 +111,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
||||
.withContentResumeOffsetUs(/* adGroupIndex= */ 1, /* contentResumeOffsetUs= */ 400_000)
|
||||
.withContentResumeOffsetUs(/* adGroupIndex= */ 2, /* contentResumeOffsetUs= */ 200_000);
|
||||
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(
|
||||
(source, timeline) -> timelineReference.set(timeline),
|
||||
@ -191,7 +193,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
||||
wrappedTimeline.getPeriod(
|
||||
/* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true)
|
||||
.uid,
|
||||
adPlaybackState));
|
||||
adPlaybackState),
|
||||
wrappedTimeline);
|
||||
|
||||
mediaSource.prepareSource(
|
||||
(source, timeline) -> timelineReference.set(timeline),
|
||||
@ -231,12 +234,13 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
||||
|
||||
@Test
|
||||
public void timeline_missingAdPlaybackStateByPeriodUid_isAssertedAndThrows() {
|
||||
FakeMediaSource contentSource = new FakeMediaSource();
|
||||
ServerSideAdInsertionMediaSource mediaSource =
|
||||
new ServerSideAdInsertionMediaSource(
|
||||
new FakeMediaSource(), /* adPlaybackStateUpdater= */ null);
|
||||
new ServerSideAdInsertionMediaSource(contentSource, /* adPlaybackStateUpdater= */ null);
|
||||
// The map of adPlaybackStates does not contain a valid period UID as key.
|
||||
mediaSource.setAdPlaybackStates(
|
||||
ImmutableMap.of(new Object(), new AdPlaybackState(/* adsId= */ new Object())));
|
||||
ImmutableMap.of(new Object(), new AdPlaybackState(/* adsId= */ new Object())),
|
||||
contentSource.getInitialTimeline());
|
||||
|
||||
Assert.assertThrows(
|
||||
IllegalStateException.class,
|
||||
@ -292,7 +296,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
||||
.uid);
|
||||
mediaSourceRef
|
||||
.get()
|
||||
.setAdPlaybackStates(ImmutableMap.of(periodUid, firstAdPlaybackState));
|
||||
.setAdPlaybackStates(
|
||||
ImmutableMap.of(periodUid, firstAdPlaybackState), contentTimeline);
|
||||
return true;
|
||||
}));
|
||||
|
||||
@ -338,6 +343,7 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
||||
/* contentResumeOffsetUs= */ 0,
|
||||
/* adDurationsUs...= */ 100_000);
|
||||
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
|
||||
ArrayList<Timeline> contentTimelines = new ArrayList<>();
|
||||
mediaSourceRef.set(
|
||||
new ServerSideAdInsertionMediaSource(
|
||||
new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)),
|
||||
@ -347,9 +353,11 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
||||
contentTimeline.getPeriod(
|
||||
/* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true)
|
||||
.uid));
|
||||
contentTimelines.add(contentTimeline);
|
||||
mediaSourceRef
|
||||
.get()
|
||||
.setAdPlaybackStates(ImmutableMap.of(periodUid.get(), firstAdPlaybackState));
|
||||
.setAdPlaybackStates(
|
||||
ImmutableMap.of(periodUid.get(), firstAdPlaybackState), contentTimeline);
|
||||
return true;
|
||||
}));
|
||||
AnalyticsListener listener = mock(AnalyticsListener.class);
|
||||
@ -367,7 +375,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
||||
/* adDurationsUs...= */ 500_000);
|
||||
mediaSourceRef
|
||||
.get()
|
||||
.setAdPlaybackStates(ImmutableMap.of(periodUid.get(), secondAdPlaybackState));
|
||||
.setAdPlaybackStates(
|
||||
ImmutableMap.of(periodUid.get(), secondAdPlaybackState), contentTimelines.get(1));
|
||||
runUntilPendingCommandsAreFullyHandled(player);
|
||||
|
||||
player.play();
|
||||
@ -376,6 +385,7 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
||||
|
||||
// Assert all samples have been played.
|
||||
DumpFileAsserts.assertOutput(context, playbackOutput, TEST_ASSET_DUMP);
|
||||
assertThat(contentTimelines).hasSize(2);
|
||||
// Assert playback has been reported with ads: [content][ad0][content][ad1][content]
|
||||
// 5*2(audio+video) format changes, 4 discontinuities between parts.
|
||||
verify(listener, times(4))
|
||||
@ -409,10 +419,12 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
||||
/* contentResumeOffsetUs= */ 0,
|
||||
/* adDurationsUs...= */ 500_000);
|
||||
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
|
||||
ArrayList<Timeline> contentTimelines = new ArrayList<>();
|
||||
mediaSourceRef.set(
|
||||
new ServerSideAdInsertionMediaSource(
|
||||
new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)),
|
||||
/* adPlaybackStateUpdater= */ contentTimeline -> {
|
||||
contentTimelines.add(contentTimeline);
|
||||
if (periodUid.get() == null) {
|
||||
periodUid.set(
|
||||
checkNotNull(
|
||||
@ -421,7 +433,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
||||
.uid));
|
||||
mediaSourceRef
|
||||
.get()
|
||||
.setAdPlaybackStates(ImmutableMap.of(periodUid.get(), firstAdPlaybackState));
|
||||
.setAdPlaybackStates(
|
||||
ImmutableMap.of(periodUid.get(), firstAdPlaybackState), contentTimeline);
|
||||
}
|
||||
return true;
|
||||
}));
|
||||
@ -440,7 +453,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
||||
/* adGroupIndex= */ 0, /* adDurationsUs...= */ 50_000, 250_000, 200_000);
|
||||
mediaSourceRef
|
||||
.get()
|
||||
.setAdPlaybackStates(ImmutableMap.of(periodUid.get(), secondAdPlaybackState));
|
||||
.setAdPlaybackStates(
|
||||
ImmutableMap.of(periodUid.get(), secondAdPlaybackState), contentTimelines.get(1));
|
||||
runUntilPendingCommandsAreFullyHandled(player);
|
||||
|
||||
player.play();
|
||||
@ -449,6 +463,7 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
||||
|
||||
// Assert all samples have been played.
|
||||
DumpFileAsserts.assertOutput(context, playbackOutput, TEST_ASSET_DUMP);
|
||||
assertThat(contentTimelines).hasSize(2);
|
||||
// Assert playback has been reported with ads: [ad0][ad1][ad2][content]
|
||||
// 4*2(audio+video) format changes, 3 discontinuities between parts.
|
||||
verify(listener, times(3))
|
||||
@ -501,7 +516,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
||||
.uid);
|
||||
mediaSourceRef
|
||||
.get()
|
||||
.setAdPlaybackStates(ImmutableMap.of(periodUid, firstAdPlaybackState));
|
||||
.setAdPlaybackStates(
|
||||
ImmutableMap.of(periodUid, firstAdPlaybackState), contentTimeline);
|
||||
return true;
|
||||
}));
|
||||
|
||||
|
@ -702,7 +702,8 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
|
||||
splitAdPlaybackStates = ImmutableMap.of(periodUid, adPlaybackState);
|
||||
}
|
||||
streamPlayer.setAdPlaybackStates(adsId, splitAdPlaybackStates, contentTimeline);
|
||||
checkNotNull(serverSideAdInsertionMediaSource).setAdPlaybackStates(splitAdPlaybackStates);
|
||||
checkNotNull(serverSideAdInsertionMediaSource)
|
||||
.setAdPlaybackStates(splitAdPlaybackStates, contentTimeline);
|
||||
if (!isLiveStream) {
|
||||
adsLoader.setAdPlaybackState(adsId, adPlaybackState);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user