Resolve media period ids in multi-period windows

Ignorable ad periods are skipped to resolve the media period id with the
ad playback state of the resulting period. In case of a change in the period
position un-played ad periods are rolled forward to be played.

PiperOrigin-RevId: 428011116
This commit is contained in:
bachinger 2022-02-11 16:41:36 +00:00 committed by Ian Baker
parent 4ef007cae7
commit a72f04f9b0
12 changed files with 1177 additions and 68 deletions

View File

@ -23,6 +23,7 @@ import androidx.media3.test.utils.FakeTimeline;
import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition;
import androidx.media3.test.utils.TimelineAsserts;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -221,7 +222,7 @@ public class TimelineTest {
/* durationUs= */ 2,
/* defaultPositionUs= */ 22,
/* windowOffsetInFirstPeriodUs= */ 222,
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder().setMediaId("mediaId2").build()),
new TimelineWindowDefinition(
/* periodCount= */ 3,
@ -233,7 +234,7 @@ public class TimelineTest {
/* durationUs= */ 3,
/* defaultPositionUs= */ 33,
/* windowOffsetInFirstPeriodUs= */ 333,
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder().setMediaId("mediaId3").build()));
Timeline restoredTimeline = Timeline.CREATOR.fromBundle(timeline.toBundle());

View File

@ -1178,7 +1178,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
requestedContentPositionUs =
seekPosition.windowPositionUs == C.TIME_UNSET ? C.TIME_UNSET : resolvedContentPositionUs;
periodId =
queue.resolveMediaPeriodIdForAds(
queue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
playbackInfo.timeline, periodUid, resolvedContentPositionUs);
if (periodId.isAd()) {
playbackInfo.timeline.getPeriodByUid(periodId.periodUid, period);
@ -1492,7 +1492,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
window, period, firstWindowIndex, /* windowPositionUs= */ C.TIME_UNSET);
// Add ad metadata if any and propagate the window sequence number to new period id.
MediaPeriodId firstPeriodId =
queue.resolveMediaPeriodIdForAds(
queue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
timeline, firstPeriodAndPositionUs.first, /* positionUs= */ 0);
long positionUs = firstPeriodAndPositionUs.second;
if (firstPeriodId.isAd()) {
@ -2354,7 +2354,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
private PlaybackInfo handlePositionDiscontinuity(
MediaPeriodId mediaPeriodId,
long positionUs,
long contentPositionUs,
long requestedContentPositionUs,
long discontinuityStartPositionUs,
boolean reportDiscontinuity,
@DiscontinuityReason int discontinuityReason) {
@ -2379,9 +2379,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
staticMetadata = extractMetadataFromTrackSelectionArray(trackSelectorResult.selections);
// Ensure the media period queue requested content position matches the new playback info.
if (playingPeriodHolder != null
&& playingPeriodHolder.info.requestedContentPositionUs != contentPositionUs) {
&& playingPeriodHolder.info.requestedContentPositionUs != requestedContentPositionUs) {
playingPeriodHolder.info =
playingPeriodHolder.info.copyWithRequestedContentPositionUs(contentPositionUs);
playingPeriodHolder.info.copyWithRequestedContentPositionUs(requestedContentPositionUs);
}
} else if (!mediaPeriodId.equals(playbackInfo.periodId)) {
// Reset previously kept track info if unprepared and the period changes.
@ -2395,7 +2395,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
return playbackInfo.copyWithNewPosition(
mediaPeriodId,
positionUs,
contentPositionUs,
requestedContentPositionUs,
discontinuityStartPositionUs,
getTotalBufferedDurationUs(),
trackGroupArray,
@ -2668,7 +2668,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
// Ensure ad insertion metadata is up to date.
MediaPeriodId periodIdWithAds =
queue.resolveMediaPeriodIdForAds(timeline, newPeriodUid, contentPositionForAdResolutionUs);
queue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
timeline, newPeriodUid, contentPositionForAdResolutionUs);
boolean earliestCuePointIsUnchangedOrLater =
periodIdWithAds.nextAdGroupIndex == C.INDEX_UNSET
|| (oldPeriodId.nextAdGroupIndex != C.INDEX_UNSET

View File

@ -15,6 +15,7 @@
*/
package androidx.media3.exoplayer;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static java.lang.Math.max;
import android.os.Handler;
@ -446,21 +447,7 @@ import com.google.common.collect.ImmutableList;
Timeline timeline, Object periodUid, long positionUs) {
long windowSequenceNumber = resolvePeriodIndexToWindowSequenceNumber(timeline, periodUid);
return resolveMediaPeriodIdForAds(
timeline, periodUid, positionUs, windowSequenceNumber, period);
}
// Internal methods.
private void notifyQueueUpdate() {
ImmutableList.Builder<MediaPeriodId> builder = ImmutableList.builder();
@Nullable MediaPeriodHolder period = playing;
while (period != null) {
builder.add(period.info.id);
period = period.getNext();
}
@Nullable MediaPeriodId readingPeriodId = reading == null ? null : reading.info.id;
analyticsCollectorHandler.post(
() -> analyticsCollector.updateMediaPeriodQueueInfo(builder.build(), readingPeriodId));
timeline, periodUid, positionUs, windowSequenceNumber, window, period);
}
/**
@ -481,8 +468,21 @@ import com.google.common.collect.ImmutableList;
Object periodUid,
long positionUs,
long windowSequenceNumber,
Timeline.Window window,
Timeline.Period period) {
timeline.getPeriodByUid(periodUid, period);
timeline.getWindow(period.windowIndex, window);
int periodIndex = timeline.getIndexOfPeriod(periodUid);
// Skip ignorable server side inserted ad periods.
while ((period.durationUs == 0
&& period.getAdGroupCount() > 0
&& period.isServerSideInsertedAdGroup(period.getRemovedAdGroupCount())
&& period.getAdGroupIndexForPositionUs(0) == C.INDEX_UNSET)
&& periodIndex++ < window.lastPeriodIndex) {
timeline.getPeriod(periodIndex, period, /* setIds= */ true);
periodUid = checkNotNull(period.uid);
}
timeline.getPeriodByUid(periodUid, period);
int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs);
if (adGroupIndex == C.INDEX_UNSET) {
int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs);
@ -493,6 +493,55 @@ import com.google.common.collect.ImmutableList;
}
}
/**
* Resolves the specified timeline period and position to a {@link MediaPeriodId} that should be
* played after a period position change, returning an identifier for an ad group if one needs to
* be played before the specified position, or an identifier for a content media period if not.
*
* @param timeline The timeline the period is part of.
* @param periodUid The uid of the timeline period to play.
* @param positionUs The next content position in the period to play.
* @return The identifier for the first media period to play, taking into account unplayed ads.
*/
public MediaPeriodId resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
Timeline timeline, Object periodUid, long positionUs) {
long windowSequenceNumber = resolvePeriodIndexToWindowSequenceNumber(timeline, periodUid);
// Check for preceding ad periods in multi-period window.
timeline.getPeriodByUid(periodUid, period);
timeline.getWindow(period.windowIndex, window);
Object periodUidToPlay = periodUid;
boolean seenAdPeriod = false;
for (int i = timeline.getIndexOfPeriod(periodUid); i >= window.firstPeriodIndex; i--) {
timeline.getPeriod(/* periodIndex= */ i, period, /* setIds= */ true);
boolean isAdPeriod = period.getAdGroupCount() > 0;
seenAdPeriod |= isAdPeriod;
if (period.getAdGroupIndexForPositionUs(period.durationUs) != C.INDEX_UNSET) {
// Roll forward to preceding un-played ad period.
periodUidToPlay = checkNotNull(period.uid);
}
if (seenAdPeriod && (!isAdPeriod || period.durationUs != 0)) {
// Stop for any periods except un-played ads with no content.
break;
}
}
return resolveMediaPeriodIdForAds(
timeline, periodUidToPlay, positionUs, windowSequenceNumber, window, period);
}
// Internal methods.
private void notifyQueueUpdate() {
ImmutableList.Builder<MediaPeriodId> builder = ImmutableList.builder();
@Nullable MediaPeriodHolder period = playing;
while (period != null) {
builder.add(period.info.id);
period = period.getNext();
}
@Nullable MediaPeriodId readingPeriodId = reading == null ? null : reading.info.id;
analyticsCollectorHandler.post(
() -> analyticsCollector.updateMediaPeriodQueueInfo(builder.build(), readingPeriodId));
}
/**
* Resolves the specified period uid to a corresponding window sequence number. Either by reusing
* the window sequence number of an existing matching media period or by creating a new window
@ -647,12 +696,12 @@ import com.google.common.collect.ImmutableList;
// We can't create a next period yet.
return null;
}
long startPositionUs;
long contentPositionUs;
// We either start a new period in the same window or the first period in the next window.
long startPositionUs = 0;
long contentPositionUs = 0;
int nextWindowIndex =
timeline.getPeriod(nextPeriodIndex, period, /* setIds= */ true).windowIndex;
Object nextPeriodUid = period.uid;
Object nextPeriodUid = checkNotNull(period.uid);
long windowSequenceNumber = mediaPeriodInfo.id.windowSequenceNumber;
if (timeline.getWindow(nextWindowIndex, window).firstPeriodIndex == nextPeriodIndex) {
// We're starting to buffer a new window. When playback transitions to this window we'll
@ -672,20 +721,32 @@ import com.google.common.collect.ImmutableList;
}
nextPeriodUid = defaultPositionUs.first;
startPositionUs = defaultPositionUs.second;
MediaPeriodHolder nextMediaPeriodHolder = mediaPeriodHolder.getNext();
@Nullable MediaPeriodHolder nextMediaPeriodHolder = mediaPeriodHolder.getNext();
if (nextMediaPeriodHolder != null && nextMediaPeriodHolder.uid.equals(nextPeriodUid)) {
windowSequenceNumber = nextMediaPeriodHolder.info.id.windowSequenceNumber;
} else {
windowSequenceNumber = nextWindowSequenceNumber++;
}
} else {
// We're starting to buffer a new period within the same window.
startPositionUs = 0;
contentPositionUs = 0;
}
@Nullable
MediaPeriodId periodId =
resolveMediaPeriodIdForAds(
timeline, nextPeriodUid, startPositionUs, windowSequenceNumber, period);
timeline, nextPeriodUid, startPositionUs, windowSequenceNumber, window, period);
if (contentPositionUs != C.TIME_UNSET
&& mediaPeriodInfo.requestedContentPositionUs != C.TIME_UNSET) {
boolean isPrecedingPeriodAnAd =
timeline.getPeriodByUid(mediaPeriodInfo.id.periodUid, period).getAdGroupCount() > 0
&& period.isServerSideInsertedAdGroup(period.getRemovedAdGroupCount());
// Handle the requested content position for period transitions within the same window.
if (periodId.isAd() && isPrecedingPeriodAnAd) {
// Propagate the requested position to the following ad period in the same window.
contentPositionUs = mediaPeriodInfo.requestedContentPositionUs;
} else if (isPrecedingPeriodAnAd) {
// Use the requested content position of the preceding ad period as the start position.
startPositionUs = mediaPeriodInfo.requestedContentPositionUs;
}
}
return getMediaPeriodInfo(timeline, periodId, contentPositionUs, startPositionUs);
}

View File

@ -124,6 +124,7 @@ import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
import androidx.media3.exoplayer.source.MediaSourceEventListener;
import androidx.media3.exoplayer.source.SinglePeriodTimeline;
import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionMediaSource;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.exoplayer.upstream.Allocation;
import androidx.media3.exoplayer.upstream.Allocator;
@ -4984,6 +4985,436 @@ public final class ExoPlayerTest {
runUntilPlaybackState(player, Player.STATE_ENDED);
}
@Test
public void seekTo_beyondSSAIMidRolls_seekAdjustedAndRequestedContentPositionKept()
throws Exception {
ArgumentCaptor<PositionInfo> oldPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<PositionInfo> newPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<Integer> reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class);
FakeTimeline adTimeline =
FakeTimeline.createMultiPeriodAdTimeline(
"windowId",
/* numberOfPlayedAds= */ 0,
/* isAdPeriodFlags...= */ false,
true,
true,
false);
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),
contentTimeline -> {
sourceReference
.get()
.setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0));
return true;
}));
player.setMediaSource(sourceReference.get());
player.pause();
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.seekTo(/* positionMs= */ 4000);
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
verify(listener, times(6))
.onPositionDiscontinuity(
oldPositionArgumentCaptor.capture(),
newPositionArgumentCaptor.capture(),
reasonArgumentCaptor.capture());
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
List<Integer> reasons = reasonArgumentCaptor.getAllValues();
assertThat(reasons).containsExactly(1, 2, 0, 0, 0, 0).inOrder();
// seek discontinuities
assertThat(oldPositions.get(0).periodIndex).isEqualTo(0);
assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(0).periodIndex).isEqualTo(3);
assertThat(newPositions.get(0).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(0).positionMs).isEqualTo(4000);
// seek adjustment
assertThat(oldPositions.get(1).periodIndex).isEqualTo(3);
assertThat(oldPositions.get(1).adGroupIndex).isEqualTo(-1);
assertThat(oldPositions.get(1).positionMs).isEqualTo(4000);
assertThat(newPositions.get(1).periodIndex).isEqualTo(1);
assertThat(newPositions.get(1).adGroupIndex).isEqualTo(0);
assertThat(newPositions.get(1).adIndexInAdGroup).isEqualTo(0);
assertThat(newPositions.get(1).positionMs).isEqualTo(0);
assertThat(newPositions.get(1).contentPositionMs).isEqualTo(4000);
// auto transition from ad to end of period
assertThat(oldPositions.get(2).periodIndex).isEqualTo(1);
assertThat(oldPositions.get(2).adGroupIndex).isEqualTo(0);
assertThat(oldPositions.get(2).adIndexInAdGroup).isEqualTo(0);
assertThat(oldPositions.get(2).positionMs).isEqualTo(2500);
assertThat(oldPositions.get(2).contentPositionMs).isEqualTo(4000);
assertThat(newPositions.get(2).periodIndex).isEqualTo(1);
assertThat(newPositions.get(2).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(2).positionMs).isEqualTo(2500);
// auto transition to next ad period
assertThat(oldPositions.get(3).periodIndex).isEqualTo(1);
assertThat(oldPositions.get(3).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(3).periodIndex).isEqualTo(2);
assertThat(newPositions.get(3).adGroupIndex).isEqualTo(0);
assertThat(newPositions.get(3).adIndexInAdGroup).isEqualTo(0);
assertThat(newPositions.get(3).contentPositionMs).isEqualTo(4000);
// auto transition from ad to end of period
assertThat(oldPositions.get(4).periodIndex).isEqualTo(2);
assertThat(oldPositions.get(4).adGroupIndex).isEqualTo(0);
assertThat(oldPositions.get(4).adIndexInAdGroup).isEqualTo(0);
assertThat(newPositions.get(4).periodIndex).isEqualTo(2);
assertThat(newPositions.get(4).adGroupIndex).isEqualTo(-1);
// auto transition to final content period with seek position
assertThat(oldPositions.get(5).periodIndex).isEqualTo(2);
assertThat(oldPositions.get(5).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(5).periodIndex).isEqualTo(3);
assertThat(newPositions.get(5).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(5).contentPositionMs).isEqualTo(4000);
}
@Test
public void seekTo_beyondSSAIMidRollsConsecutiveContentPeriods_seekAdjusted() throws Exception {
ArgumentCaptor<PositionInfo> oldPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<PositionInfo> newPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<Integer> reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class);
FakeTimeline adTimeline =
FakeTimeline.createMultiPeriodAdTimeline(
"windowId",
/* numberOfPlayedAds= */ 0,
/* isAdPeriodFlags...= */ false,
true,
false,
false);
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),
contentTimeline -> {
sourceReference
.get()
.setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0));
return true;
}));
player.setMediaSource(sourceReference.get());
player.pause();
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.seekTo(/* positionMs= */ 7000);
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
verify(listener, times(5))
.onPositionDiscontinuity(
oldPositionArgumentCaptor.capture(),
newPositionArgumentCaptor.capture(),
reasonArgumentCaptor.capture());
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
List<Integer> reasons = reasonArgumentCaptor.getAllValues();
assertThat(reasons).containsExactly(1, 2, 0, 0, 0).inOrder();
// seek
assertThat(oldPositions.get(0).periodIndex).isEqualTo(0);
assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(0).periodIndex).isEqualTo(3);
assertThat(newPositions.get(0).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(0).positionMs).isEqualTo(7000);
// seek adjustment
assertThat(oldPositions.get(1).periodIndex).isEqualTo(3);
assertThat(oldPositions.get(1).adGroupIndex).isEqualTo(-1);
assertThat(oldPositions.get(1).positionMs).isEqualTo(7000);
assertThat(newPositions.get(1).periodIndex).isEqualTo(1);
assertThat(newPositions.get(1).adGroupIndex).isEqualTo(0);
assertThat(newPositions.get(1).positionMs).isEqualTo(0);
}
@Test
public void seekTo_beforeSSAIMidRolls_requestedContentPositionNotPropagatedIntoAds()
throws Exception {
ArgumentCaptor<PositionInfo> oldPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<PositionInfo> newPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<Integer> reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class);
FakeTimeline adTimeline =
FakeTimeline.createMultiPeriodAdTimeline(
"windowId",
/* numberOfPlayedAds= */ 0,
/* isAdPeriodFlags...= */ false,
true,
true,
false);
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),
contentTimeline -> {
sourceReference
.get()
.setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0));
return true;
}));
player.setMediaSource(sourceReference.get());
player.pause();
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.play();
player.seekTo(1600);
runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
verify(listener, times(6))
.onPositionDiscontinuity(
oldPositionArgumentCaptor.capture(),
newPositionArgumentCaptor.capture(),
reasonArgumentCaptor.capture());
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
List<Integer> reasons = reasonArgumentCaptor.getAllValues();
assertThat(reasons).containsExactly(1, 0, 0, 0, 0, 0).inOrder();
// seek discontinuity
assertThat(oldPositions.get(0).periodIndex).isEqualTo(0);
assertThat(newPositions.get(0).periodIndex).isEqualTo(0);
assertThat(newPositions.get(0).positionMs).isEqualTo(1600);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(1600);
// auto discontinuities through ads has correct content position that is not the seek position.
assertThat(newPositions.get(1).periodIndex).isEqualTo(1);
assertThat(newPositions.get(1).adGroupIndex).isEqualTo(0);
assertThat(newPositions.get(1).adIndexInAdGroup).isEqualTo(0);
assertThat(newPositions.get(1).positionMs).isEqualTo(0);
assertThat(newPositions.get(1).contentPositionMs).isEqualTo(2500);
assertThat(newPositions.get(2).contentPositionMs).isEqualTo(2500);
assertThat(newPositions.get(3).contentPositionMs).isEqualTo(2500);
assertThat(newPositions.get(4).contentPositionMs).isEqualTo(2500);
// Content resumes at expected position that is not the seek position.
assertThat(newPositions.get(5).periodIndex).isEqualTo(3);
assertThat(newPositions.get(5).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(5).positionMs).isEqualTo(2500);
assertThat(newPositions.get(5).contentPositionMs).isEqualTo(2500);
}
@Test
public void seekTo_toSAIMidRolls_playsMidRolls() throws Exception {
ArgumentCaptor<PositionInfo> oldPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<PositionInfo> newPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<Integer> reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class);
FakeTimeline adTimeline =
FakeTimeline.createMultiPeriodAdTimeline(
"windowId",
/* numberOfPlayedAds= */ 0,
/* isAdPeriodFlags...= */ false,
true,
true,
false);
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),
contentTimeline -> {
sourceReference
.get()
.setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0));
return true;
}));
player.setMediaSource(sourceReference.get());
player.pause();
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.seekTo(2500);
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
verify(listener, times(6))
.onPositionDiscontinuity(
oldPositionArgumentCaptor.capture(),
newPositionArgumentCaptor.capture(),
reasonArgumentCaptor.capture());
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
List<Integer> reasons = reasonArgumentCaptor.getAllValues();
assertThat(reasons).containsExactly(1, 2, 0, 0, 0, 0).inOrder();
// seek discontinuity
assertThat(oldPositions.get(0).periodIndex).isEqualTo(0);
assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(0).periodIndex).isEqualTo(1);
assertThat(newPositions.get(0).adGroupIndex).isEqualTo(-1);
// seek adjustment discontinuity
assertThat(oldPositions.get(1).periodIndex).isEqualTo(1);
assertThat(oldPositions.get(1).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(1).periodIndex).isEqualTo(1);
assertThat(newPositions.get(1).adGroupIndex).isEqualTo(0);
// auto transition to last frame of first ad period
assertThat(oldPositions.get(2).periodIndex).isEqualTo(1);
assertThat(oldPositions.get(2).adGroupIndex).isEqualTo(0);
assertThat(newPositions.get(2).periodIndex).isEqualTo(1);
assertThat(newPositions.get(2).adGroupIndex).isEqualTo(-1);
// auto transition to second ad period
assertThat(oldPositions.get(3).periodIndex).isEqualTo(1);
assertThat(oldPositions.get(3).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(3).periodIndex).isEqualTo(2);
assertThat(newPositions.get(3).adGroupIndex).isEqualTo(0);
// auto transition to last frame of second ad period
assertThat(oldPositions.get(4).periodIndex).isEqualTo(2);
assertThat(oldPositions.get(4).adGroupIndex).isEqualTo(0);
assertThat(newPositions.get(4).periodIndex).isEqualTo(2);
assertThat(newPositions.get(4).adGroupIndex).isEqualTo(-1);
// auto transition to the final content period
assertThat(oldPositions.get(5).periodIndex).isEqualTo(2);
assertThat(oldPositions.get(5).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(5).periodIndex).isEqualTo(3);
assertThat(newPositions.get(5).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(5).positionMs).isEqualTo(2500);
assertThat(newPositions.get(5).contentPositionMs).isEqualTo(2500);
}
@Test
public void seekTo_toPlayedSAIMidRolls_requestedContentPositionNotPropagatedIntoAds()
throws Exception {
ArgumentCaptor<PositionInfo> oldPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<PositionInfo> newPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<Integer> reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class);
FakeTimeline adTimeline =
FakeTimeline.createMultiPeriodAdTimeline(
"windowId",
/* numberOfPlayedAds= */ 2,
/* isAdPeriodFlags...= */ false,
true,
true,
false);
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),
contentTimeline -> {
sourceReference
.get()
.setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0));
return true;
}));
player.setMediaSource(sourceReference.get());
player.pause();
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.seekTo(2500);
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
verify(listener, times(1))
.onPositionDiscontinuity(
oldPositionArgumentCaptor.capture(),
newPositionArgumentCaptor.capture(),
reasonArgumentCaptor.capture());
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
List<Integer> reasons = reasonArgumentCaptor.getAllValues();
assertThat(reasons).containsExactly(1).inOrder();
// seek discontinuity
assertThat(oldPositions.get(0).periodIndex).isEqualTo(0);
assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(-1);
// TODO(bachinger): Incorrect masking. Skipped played prerolls not taken into account by masking
assertThat(newPositions.get(0).periodIndex).isEqualTo(1);
assertThat(newPositions.get(0).adGroupIndex).isEqualTo(-1);
}
@Test
public void play_playedSSAIPreMidPostRolls_skipsAllAds() throws Exception {
ArgumentCaptor<PositionInfo> oldPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<PositionInfo> newPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<Integer> reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class);
FakeTimeline adTimeline =
FakeTimeline.createMultiPeriodAdTimeline(
"windowId",
/* numberOfPlayedAds= */ Integer.MAX_VALUE,
/* isAdPeriodFlags...= */ true,
false,
true,
true,
false,
true,
true,
true);
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),
contentTimeline -> {
sourceReference
.get()
.setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0));
return true;
}));
player.setMediaSource(sourceReference.get());
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
verify(listener, times(3))
.onPositionDiscontinuity(
oldPositionArgumentCaptor.capture(),
newPositionArgumentCaptor.capture(),
reasonArgumentCaptor.capture());
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
List<Integer> reasons = reasonArgumentCaptor.getAllValues();
assertThat(reasons).containsExactly(0, 0, 0).inOrder();
// Auto discontinuity from the empty ad period to the first content period.
assertThat(oldPositions.get(0).periodIndex).isEqualTo(0);
assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(-1);
assertThat(oldPositions.get(0).positionMs).isEqualTo(0);
assertThat(newPositions.get(0).periodIndex).isEqualTo(1);
assertThat(newPositions.get(0).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(0).positionMs).isEqualTo(0);
// Auto discontinuity from the first content to the second content period.
assertThat(oldPositions.get(1).periodIndex).isEqualTo(1);
assertThat(oldPositions.get(1).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(1).periodIndex).isEqualTo(4);
assertThat(newPositions.get(1).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(1).positionMs).isEqualTo(1250);
// Auto discontinuity from the second content period to the last frame of the last postroll.
assertThat(oldPositions.get(2).periodIndex).isEqualTo(4);
assertThat(oldPositions.get(2).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(2).periodIndex).isEqualTo(7);
assertThat(newPositions.get(2).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(2).positionMs).isEqualTo(2500);
}
@Test
public void becomingNoisyIgnoredIfBecomingNoisyHandlingIsDisabled() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
@ -8036,7 +8467,7 @@ public final class ExoPlayerTest {
/* durationUs = */ 100_000,
/* defaultPositionUs = */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
MediaItem.fromUri("http://foo.bar/fake1"));
FakeMediaSource fakeMediaSource1 = new FakeMediaSource(new FakeTimeline(window1));
TimelineWindowDefinition window2 =
@ -8050,7 +8481,7 @@ public final class ExoPlayerTest {
/* durationUs = */ 100_000,
/* defaultPositionUs = */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
MediaItem.fromUri("http://foo.bar/fake2"));
FakeMediaSource fakeMediaSource2 = new FakeMediaSource(new FakeTimeline(window2));
TimelineWindowDefinition window3 =
@ -8064,7 +8495,7 @@ public final class ExoPlayerTest {
/* durationUs = */ 100_000,
/* defaultPositionUs = */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
MediaItem.fromUri("http://foo.bar/fake3"));
FakeMediaSource fakeMediaSource3 = new FakeMediaSource(new FakeTimeline(window3));
final Player[] playerHolder = {null};
@ -8422,7 +8853,7 @@ public final class ExoPlayerTest {
/* durationUs= */ 10_000_000,
/* defaultPositionUs= */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
initialMediaItem);
TimelineWindowDefinition secondWindow =
new TimelineWindowDefinition(
@ -8435,7 +8866,7 @@ public final class ExoPlayerTest {
/* durationUs= */ 10_000_000,
/* defaultPositionUs= */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
initialMediaItem.buildUpon().setTag(1).build());
FakeTimeline timeline = new FakeTimeline(initialWindow);
FakeTimeline newTimeline = new FakeTimeline(secondWindow);
@ -9269,7 +9700,7 @@ public final class ExoPlayerTest {
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
@ -9319,7 +9750,7 @@ public final class ExoPlayerTest {
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
@ -9365,7 +9796,7 @@ public final class ExoPlayerTest {
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
@ -9413,7 +9844,7 @@ public final class ExoPlayerTest {
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
@ -9431,7 +9862,7 @@ public final class ExoPlayerTest {
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs + 50_000),
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
@ -9576,7 +10007,7 @@ public final class ExoPlayerTest {
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 20 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
@ -9630,7 +10061,7 @@ public final class ExoPlayerTest {
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
@ -9675,7 +10106,7 @@ public final class ExoPlayerTest {
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
@ -9693,7 +10124,7 @@ public final class ExoPlayerTest {
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
@ -9742,7 +10173,7 @@ public final class ExoPlayerTest {
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
@ -9760,7 +10191,7 @@ public final class ExoPlayerTest {
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
@ -9850,7 +10281,7 @@ public final class ExoPlayerTest {
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder().setUri(Uri.EMPTY).build()));
player.pause();
player.setMediaSource(new FakeMediaSource(liveTimelineWithoutTargetLiveOffset));

View File

@ -15,6 +15,7 @@
*/
package androidx.media3.exoplayer;
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 org.mockito.Mockito.mock;
import static org.robolectric.Shadows.shadowOf;
@ -22,6 +23,7 @@ import static org.robolectric.Shadows.shadowOf;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.util.Pair;
import androidx.media3.common.AdPlaybackState;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
@ -479,8 +481,7 @@ public final class MediaPeriodQueueTest {
/* startPositionUs= */ 0,
/* requestedContentPositionUs= */ C.TIME_UNSET,
/* endPositionUs= */ C.TIME_UNSET,
/* durationUs= */ CONTENT_DURATION_US
+ TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
/* durationUs= */ CONTENT_DURATION_US + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
/* isFollowedByTransitionToSameStream= */ false,
/* isLastInPeriod= */ true,
/* isLastInWindow= */ false,
@ -740,6 +741,320 @@ public final class MediaPeriodQueueTest {
assertThat(getQueueLength()).isEqualTo(3);
}
@Test
public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_behindAdPositionInSinglePeriodTimeline_resolvesToAd() {
long adPositionUs = DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + 10_000;
AdPlaybackState adPlaybackState = new AdPlaybackState("adsId", adPositionUs);
adPlaybackState = adPlaybackState.withAdDurationsUs(/* adGroupIndex= */ 0, 5_000);
Object windowUid = new Object();
FakeTimeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ windowUid,
/* isSeekable= */ true,
/* isDynamic= */ false,
TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US,
adPlaybackState));
MediaPeriodId mediaPeriodId =
mediaPeriodQueue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
timeline, /* periodUid= */ new Pair<>(windowUid, 0), adPositionUs + 1);
assertThat(mediaPeriodId.adGroupIndex).isEqualTo(0);
assertThat(mediaPeriodId.adIndexInAdGroup).isEqualTo(0);
assertThat(mediaPeriodId.nextAdGroupIndex).isEqualTo(-1);
assertThat(mediaPeriodId.periodUid).isEqualTo(new Pair<>(windowUid, 0));
}
@Test
public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_toAdPositionInSinglePeriodTimeline_resolvesToAd() {
long adPositionUs = DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + 10_000;
AdPlaybackState adPlaybackState = new AdPlaybackState("adsId", adPositionUs);
adPlaybackState = adPlaybackState.withAdDurationsUs(/* adGroupIndex= */ 0, 5_000);
Object windowUid = new Object();
FakeTimeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ windowUid,
/* isSeekable= */ true,
/* isDynamic= */ false,
TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US,
adPlaybackState));
MediaPeriodId mediaPeriodId =
mediaPeriodQueue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
timeline, /* periodUid= */ new Pair<>(windowUid, 0), adPositionUs);
assertThat(mediaPeriodId.periodUid).isEqualTo(new Pair<>(windowUid, 0));
assertThat(mediaPeriodId.adGroupIndex).isEqualTo(0);
assertThat(mediaPeriodId.adIndexInAdGroup).isEqualTo(0);
assertThat(mediaPeriodId.nextAdGroupIndex).isEqualTo(-1);
}
@Test
public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_beforeAdPositionInSinglePeriodTimeline_seekNotAdjusted() {
long adPositionUs = DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + 10_000;
AdPlaybackState adPlaybackState =
new AdPlaybackState("adsId", adPositionUs).withAdDurationsUs(/* adGroupIndex= */ 0, 5_000);
Object windowUid = new Object();
FakeTimeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ windowUid,
/* isSeekable= */ true,
/* isDynamic= */ false,
TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US,
adPlaybackState));
MediaPeriodId mediaPeriodId =
mediaPeriodQueue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
timeline, new Pair<>(windowUid, 0), adPositionUs - 1);
assertThat(mediaPeriodId.periodUid).isEqualTo(new Pair<>(windowUid, 0));
assertThat(mediaPeriodId.adGroupIndex).isEqualTo(-1);
assertThat(mediaPeriodId.adIndexInAdGroup).isEqualTo(-1);
assertThat(mediaPeriodId.nextAdGroupIndex).isEqualTo(0);
}
@Test
public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_behindAdInMultiPeriodTimeline_rollForward() {
Object windowId = new Object();
FakeTimeline timeline =
FakeTimeline.createMultiPeriodAdTimeline(
windowId,
/* numberOfPlayedAds= */ 0,
/* isAdPeriodFlags...= */ true,
false,
true,
true,
true,
false);
MediaPeriodId mediaPeriodId =
mediaPeriodQueue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
timeline, new Pair<>(windowId, 1), /* positionUs= */ 1);
assertThat(mediaPeriodId.periodUid).isEqualTo(new Pair<>(windowId, 0));
assertThat(mediaPeriodId.adGroupIndex).isEqualTo(0);
assertThat(mediaPeriodId.adIndexInAdGroup).isEqualTo(0);
assertThat(mediaPeriodId.nextAdGroupIndex).isEqualTo(-1);
mediaPeriodId =
mediaPeriodQueue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
timeline, new Pair<>(windowId, 5), /* positionUs= */ 0);
assertThat(mediaPeriodId.periodUid).isEqualTo(new Pair<>(windowId, 2));
assertThat(mediaPeriodId.adGroupIndex).isEqualTo(0);
assertThat(mediaPeriodId.adIndexInAdGroup).isEqualTo(0);
assertThat(mediaPeriodId.nextAdGroupIndex).isEqualTo(-1);
}
@Test
public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_behindAdInMultiPeriodAllAdsPlayed_seekNotAdjusted() {
Object windowId = new Object();
FakeTimeline timeline =
FakeTimeline.createMultiPeriodAdTimeline(
windowId,
/* numberOfPlayedAds= */ 4,
/* isAdPeriodFlags...= */ true,
false,
true,
true,
true,
false);
MediaPeriodId mediaPeriodId =
mediaPeriodQueue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
timeline, new Pair<>(windowId, 1), /* positionUs= */ 11);
assertThat(mediaPeriodId.adGroupIndex).isEqualTo(-1);
assertThat(mediaPeriodId.adIndexInAdGroup).isEqualTo(-1);
assertThat(mediaPeriodId.nextAdGroupIndex).isEqualTo(-1);
assertThat(mediaPeriodId.periodUid).isEqualTo(new Pair<>(windowId, 1));
mediaPeriodId =
mediaPeriodQueue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
timeline, new Pair<>(windowId, 5), /* positionUs= */ 33);
assertThat(mediaPeriodId.adGroupIndex).isEqualTo(-1);
assertThat(mediaPeriodId.adIndexInAdGroup).isEqualTo(-1);
assertThat(mediaPeriodId.nextAdGroupIndex).isEqualTo(-1);
assertThat(mediaPeriodId.periodUid).isEqualTo(new Pair<>(windowId, 5));
}
@Test
public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_behindAdInMultiPeriodFirstTwoAdsPlayed_rollForward() {
Object windowId = new Object();
FakeTimeline timeline =
FakeTimeline.createMultiPeriodAdTimeline(
windowId,
/* numberOfPlayedAds= */ 2,
/* isAdPeriodFlags...= */ true,
false,
true,
true,
true,
false);
MediaPeriodId mediaPeriodId =
mediaPeriodQueue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
timeline, new Pair<>(windowId, 5), /* positionUs= */ 33);
assertThat(mediaPeriodId.adGroupIndex).isEqualTo(0);
assertThat(mediaPeriodId.adIndexInAdGroup).isEqualTo(0);
assertThat(mediaPeriodId.nextAdGroupIndex).isEqualTo(-1);
assertThat(mediaPeriodId.periodUid).isEqualTo(new Pair<>(windowId, 3));
}
@Test
public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_beforeAdInMultiPeriodTimeline_seekNotAdjusted() {
Object windowId = new Object();
FakeTimeline timeline =
FakeTimeline.createMultiPeriodAdTimeline(
windowId, /* numberOfPlayedAds= */ 0, /* isAdPeriodFlags...= */ false, true);
MediaPeriodId mediaPeriodId =
mediaPeriodQueue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
timeline, new Pair<>(windowId, 0), /* positionUs= */ 33);
assertThat(mediaPeriodId.adGroupIndex).isEqualTo(-1);
assertThat(mediaPeriodId.adIndexInAdGroup).isEqualTo(-1);
assertThat(mediaPeriodId.nextAdGroupIndex).isEqualTo(-1);
assertThat(mediaPeriodId.periodUid).isEqualTo(new Pair<>(windowId, 0));
}
@Test
public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_toUnplayedAdInMultiPeriodTimeline_resolvedAsAd() {
Object windowId = new Object();
FakeTimeline timeline =
FakeTimeline.createMultiPeriodAdTimeline(
windowId, /* numberOfPlayedAds= */ 0, /* isAdPeriodFlags...= */ false, true, false);
MediaPeriodId mediaPeriodId =
mediaPeriodQueue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
timeline, new Pair<>(windowId, 1), /* positionUs= */ 0);
assertThat(mediaPeriodId.adGroupIndex).isEqualTo(0);
assertThat(mediaPeriodId.adIndexInAdGroup).isEqualTo(0);
assertThat(mediaPeriodId.nextAdGroupIndex).isEqualTo(-1);
assertThat(mediaPeriodId.periodUid).isEqualTo(new Pair<>(windowId, 1));
}
@Test
public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_toPlayedAdInMultiPeriodTimeline_skipPlayedAd() {
Object windowId = new Object();
FakeTimeline timeline =
FakeTimeline.createMultiPeriodAdTimeline(
windowId, /* numberOfPlayedAds= */ 1, /* isAdPeriodFlags...= */ false, true, false);
MediaPeriodId mediaPeriodId =
mediaPeriodQueue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
timeline, new Pair<>(windowId, 1), /* positionUs= */ 0);
assertThat(mediaPeriodId.adGroupIndex).isEqualTo(-1);
assertThat(mediaPeriodId.adIndexInAdGroup).isEqualTo(-1);
assertThat(mediaPeriodId.nextAdGroupIndex).isEqualTo(-1);
assertThat(mediaPeriodId.periodUid).isEqualTo(new Pair<>(windowId, 2));
}
@Test
public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_toStartOfWindowPlayedAdPreroll_skipsPlayedPrerolls() {
Object windowId = new Object();
FakeTimeline timeline =
FakeTimeline.createMultiPeriodAdTimeline(
windowId, /* numberOfPlayedAds= */ 2, /* isAdPeriodFlags...= */ true, true, false);
MediaPeriodId mediaPeriodId =
mediaPeriodQueue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
timeline, new Pair<>(windowId, 0), /* positionUs= */ 0);
assertThat(mediaPeriodId.adGroupIndex).isEqualTo(-1);
assertThat(mediaPeriodId.adIndexInAdGroup).isEqualTo(-1);
assertThat(mediaPeriodId.nextAdGroupIndex).isEqualTo(-1);
assertThat(mediaPeriodId.periodUid).isEqualTo(new Pair<>(windowId, 2));
}
@Test
public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_toPlayedPostrolls_skipsAllButLastPostroll() {
Object windowId = new Object();
FakeTimeline timeline =
FakeTimeline.createMultiPeriodAdTimeline(
windowId,
/* numberOfPlayedAds= */ 4,
/* isAdPeriodFlags...= */ false,
true,
true,
true,
true);
MediaPeriodId mediaPeriodId =
mediaPeriodQueue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
timeline, new Pair<>(windowId, 1), /* positionUs= */ 0);
assertThat(mediaPeriodId.periodUid).isEqualTo(new Pair<>(windowId, 4));
assertThat(mediaPeriodId.adGroupIndex).isEqualTo(-1);
assertThat(mediaPeriodId.adIndexInAdGroup).isEqualTo(-1);
assertThat(mediaPeriodId.nextAdGroupIndex).isEqualTo(-1);
}
@Test
public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_consecutiveContentPeriods_rollForward() {
Object windowId = new Object();
FakeTimeline timeline =
FakeTimeline.createMultiPeriodAdTimeline(
windowId,
/* numberOfPlayedAds= */ 0,
/* isAdPeriodFlags...= */ true,
false,
false,
false);
MediaPeriodId mediaPeriodId =
mediaPeriodQueue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
timeline, new Pair<>(windowId, 3), /* positionUs= */ 10_000);
assertThat(mediaPeriodId.periodUid).isEqualTo(new Pair<>(windowId, 0));
assertThat(mediaPeriodId.adGroupIndex).isEqualTo(0);
assertThat(mediaPeriodId.adIndexInAdGroup).isEqualTo(0);
assertThat(mediaPeriodId.nextAdGroupIndex).isEqualTo(-1);
}
@Test
public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_onlyConsecutiveContentPeriods_seekNotAdjusted() {
Object windowId = new Object();
FakeTimeline timeline =
FakeTimeline.createMultiPeriodAdTimeline(
windowId,
/* numberOfPlayedAds= */ 0,
/* isAdPeriodFlags...= */ false,
false,
false,
false);
MediaPeriodId mediaPeriodId =
mediaPeriodQueue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
timeline, new Pair<>(windowId, 3), /* positionUs= */ 10_000);
assertThat(mediaPeriodId.periodUid).isEqualTo(new Pair<>(windowId, 3));
assertThat(mediaPeriodId.adGroupIndex).isEqualTo(-1);
}
private void setupAdTimeline(long... adGroupTimesUs) {
adPlaybackState =
new AdPlaybackState(/* adsId= */ new Object(), adGroupTimesUs)

View File

@ -526,6 +526,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
adPlaybackState,
/* fromPositionUs= */ secToUs(cuePoint.getStartTime()),
/* contentResumeOffsetUs= */ 0,
// TODO(b/192231683) Use getEndTimeMs()/getStartTimeMs() after jar target was removed
/* adDurationsUs...= */ secToUs(cuePoint.getEndTime() - cuePoint.getStartTime()));
}
return adPlaybackState;

View File

@ -438,12 +438,13 @@ import java.util.Set;
new AdPlaybackState(checkNotNull(adsId), /* adGroupTimesUs...= */ 0)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
.withIsServerSideInserted(/* adGroupIndex= */ 0, true)
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, adGroup.contentResumeOffsetUs);
long periodEndUs = periodStartUs + periodDurationUs;
long adDurationsUs = 0;
for (int i = 0; i < adGroup.count; i++) {
adDurationsUs += adGroup.durationsUs[i];
if (periodEndUs == adGroup.timeUs + adDurationsUs) {
if (periodEndUs <= adGroup.timeUs + adDurationsUs + 10_000) {
// Map the state of the global ad state to the period specific ad state.
switch (adGroup.states[i]) {
case AdPlaybackState.AD_STATE_PLAYED:

View File

@ -1365,7 +1365,9 @@ public final class ImaAdsLoaderTest {
}
private AdPlaybackState getAdPlaybackState(int periodIndex) {
return timelineWindowDefinitions[periodIndex].adPlaybackState;
int adPlaybackStateCount = timelineWindowDefinitions[periodIndex].adPlaybackStates.size();
return timelineWindowDefinitions[periodIndex].adPlaybackStates.get(
periodIndex % adPlaybackStateCount);
}
private static AdEvent getAdEvent(AdEventType adEventType, @Nullable Ad ad) {
@ -1408,7 +1410,11 @@ public final class ImaAdsLoaderTest {
adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs);
TimelineWindowDefinition timelineWindowDefinition = timelineWindowDefinitions[periodIndex];
assertThat(adPlaybackState.adsId).isEqualTo(timelineWindowDefinition.adPlaybackState.adsId);
assertThat(adPlaybackState.adsId)
.isEqualTo(
timelineWindowDefinition.adPlaybackStates.get(
periodIndex % timelineWindowDefinition.adPlaybackStates.size())
.adsId);
timelineWindowDefinitions[periodIndex] =
new TimelineWindowDefinition(
timelineWindowDefinition.periodCount,

View File

@ -458,6 +458,52 @@ public class ImaUtilTest {
.isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
}
@Test
public void splitAdPlaybackStateForPeriods_singleAdOfAdGroupSpansMultiplePeriods_correctState() {
int periodCount = 8;
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
AdPlaybackState adPlaybackState =
new AdPlaybackState(/* adsId= */ "adsId", 0, periodDurationUs, 2 * periodDurationUs)
.withAdCount(/* adGroupIndex= */ 0, 1)
.withAdCount(/* adGroupIndex= */ 1, 1)
.withAdCount(/* adGroupIndex= */ 2, 1)
.withAdDurationsUs(
/* adGroupIndex= */ 0, /* adDurationsUs...= */
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + (2 * periodDurationUs))
.withAdDurationsUs(
/* adGroupIndex= */ 1, /* adDurationsUs...= */ (2 * periodDurationUs))
.withAdDurationsUs(
/* adGroupIndex= */ 2, /* adDurationsUs...= */ (2 * periodDurationUs))
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true)
.withIsServerSideInserted(/* adGroupIndex= */ 1, true)
.withIsServerSideInserted(/* adGroupIndex= */ 2, true);
FakeTimeline timeline =
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(
/* periodCount= */ periodCount, /* id= */ 0L));
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
assertThat(adPlaybackStates).hasSize(periodCount);
assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).getAdGroup(/* adGroupIndex= */ 0).states[0])
.isEqualTo(AdPlaybackState.AD_STATE_PLAYED);
assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).getAdGroup(/* adGroupIndex= */ 0).states[0])
.isEqualTo(AdPlaybackState.AD_STATE_PLAYED);
assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).getAdGroup(/* adGroupIndex= */ 0).states[0])
.isEqualTo(AdPlaybackState.AD_STATE_PLAYED);
assertThat(adPlaybackStates.get(new Pair<>(0L, 4)).getAdGroup(/* adGroupIndex= */ 0).states[0])
.isEqualTo(AdPlaybackState.AD_STATE_PLAYED);
assertThat(adPlaybackStates.get(new Pair<>(0L, 5)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 6)).getAdGroup(/* adGroupIndex= */ 0).states[0])
.isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
assertThat(adPlaybackStates.get(new Pair<>(0L, 7)).getAdGroup(/* adGroupIndex= */ 0).states[0])
.isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
}
@Test
public void splitAdPlaybackStateForPeriods_lateMidrollAdGroupStartTimeUs_adGroupIgnored() {
int periodCount = 4;

View File

@ -26,6 +26,7 @@ import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.MediaSourceFactory;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition;
import com.google.common.collect.ImmutableList;
/** Fake {@link MediaSourceFactory} that creates a {@link FakeMediaSource}. */
@UnstableApi
@ -66,7 +67,7 @@ public final class FakeMediaSourceFactory implements MediaSourceFactory {
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 2 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(123456789),
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
mediaItem);
return new FakeMediaSource(new FakeTimeline(timelineWindowDefinition));
}

View File

@ -15,6 +15,9 @@
*/
package androidx.media3.test.utils;
import static androidx.media3.common.util.Util.sum;
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 java.lang.Math.min;
import android.net.Uri;
@ -27,7 +30,13 @@ import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Fake {@link Timeline} which can be setup to return custom {@link TimelineWindowDefinition}s. */
@UnstableApi
@ -52,7 +61,7 @@ public final class FakeTimeline extends Timeline {
public final long durationUs;
public final long defaultPositionUs;
public final long windowOffsetInFirstPeriodUs;
public final AdPlaybackState adPlaybackState;
public final List<AdPlaybackState> adPlaybackStates;
/**
* Creates a window definition that corresponds to a placeholder timeline using the given tag.
@ -179,10 +188,41 @@ public final class FakeTimeline extends Timeline {
durationUs,
defaultPositionUs,
windowOffsetInFirstPeriodUs,
adPlaybackState,
ImmutableList.of(adPlaybackState),
FAKE_MEDIA_ITEM.buildUpon().setTag(id).build());
}
/**
* @deprecated Use {@link #TimelineWindowDefinition(int, Object, boolean, boolean, boolean,
* boolean, long, long, long, List, MediaItem)} instead.
*/
@Deprecated
public TimelineWindowDefinition(
int periodCount,
Object id,
boolean isSeekable,
boolean isDynamic,
boolean isLive,
boolean isPlaceholder,
long durationUs,
long defaultPositionUs,
long windowOffsetInFirstPeriodUs,
AdPlaybackState adPlaybackState,
MediaItem mediaItem) {
this(
periodCount,
id,
isSeekable,
isDynamic,
isLive,
isPlaceholder,
durationUs,
defaultPositionUs,
windowOffsetInFirstPeriodUs,
ImmutableList.of(adPlaybackState),
mediaItem);
}
/**
* Creates a window definition with ad groups and a custom media item.
*
@ -197,7 +237,7 @@ public final class FakeTimeline extends Timeline {
* @param defaultPositionUs The default position of the window in microseconds.
* @param windowOffsetInFirstPeriodUs The offset of the window in the first period, in
* microseconds.
* @param adPlaybackState The ad playback state.
* @param adPlaybackStates The ad playback states for the periods.
* @param mediaItem The media item to include in the timeline.
*/
public TimelineWindowDefinition(
@ -210,7 +250,7 @@ public final class FakeTimeline extends Timeline {
long durationUs,
long defaultPositionUs,
long windowOffsetInFirstPeriodUs,
AdPlaybackState adPlaybackState,
List<AdPlaybackState> adPlaybackStates,
MediaItem mediaItem) {
Assertions.checkArgument(durationUs != C.TIME_UNSET || periodCount == 1);
this.periodCount = periodCount;
@ -223,7 +263,7 @@ public final class FakeTimeline extends Timeline {
this.durationUs = durationUs;
this.defaultPositionUs = defaultPositionUs;
this.windowOffsetInFirstPeriodUs = windowOffsetInFirstPeriodUs;
this.adPlaybackState = adPlaybackState;
this.adPlaybackStates = adPlaybackStates;
}
}
@ -268,6 +308,59 @@ public final class FakeTimeline extends Timeline {
return adPlaybackState;
}
/**
* Creates a multi-period timeline with ad and content periods specified by the flags passed as
* var-arg arguments.
*
* <p>Period uid end up being a {@code new Pair<>(windowId, periodIndex)}.
*
* @param windowId The window ID.
* @param numberOfPlayedAds The number of ads that should be marked as played.
* @param isAdPeriodFlags A value of true indicates an ad period. A value of false indicated a
* content period.
* @return A timeline with a single window with as many periods as var-arg arguments.
*/
public static FakeTimeline createMultiPeriodAdTimeline(
Object windowId, int numberOfPlayedAds, boolean... isAdPeriodFlags) {
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / isAdPeriodFlags.length;
AdPlaybackState firstAdPeriodState =
new AdPlaybackState(/* adsId= */ "adsId", /* adGroupTimesUs... */ 0)
.withAdCount(/* adGroupIndex= */ 0, 1)
.withAdDurationsUs(
/* adGroupIndex= */ 0, DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
AdPlaybackState commonAdPeriodState = firstAdPeriodState.withAdDurationsUs(0, periodDurationUs);
AdPlaybackState contentPeriodState = new AdPlaybackState(/* adsId= */ "adsId");
List<AdPlaybackState> adPlaybackStates = new ArrayList<>();
int playedAdsCounter = 0;
for (boolean isAd : isAdPeriodFlags) {
AdPlaybackState periodAdPlaybackState =
isAd
? (adPlaybackStates.isEmpty() ? firstAdPeriodState : commonAdPeriodState)
: contentPeriodState;
if (isAd && playedAdsCounter < numberOfPlayedAds) {
periodAdPlaybackState =
periodAdPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
playedAdsCounter++;
}
adPlaybackStates.add(periodAdPlaybackState);
}
return new FakeTimeline(
new FakeTimeline.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= */ adPlaybackStates,
MediaItem.EMPTY));
}
/**
* Create a fake timeline with one seekable, non-dynamic window with one period and a duration of
* {@link TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US}.
@ -363,6 +456,19 @@ public final class FakeTimeline extends Timeline {
@Override
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex];
long windowDurationUs = 0;
Period period = new Period();
for (int i = periodOffsets[windowIndex]; i < periodOffsets[windowIndex + 1]; i++) {
long periodDurationUs = getPeriod(/* periodIndex= */ i, period).durationUs;
if (i == periodOffsets[windowIndex] && periodDurationUs != 0) {
windowDurationUs -= windowDefinition.windowOffsetInFirstPeriodUs;
}
if (periodDurationUs == C.TIME_UNSET) {
windowDurationUs = C.TIME_UNSET;
break;
}
windowDurationUs += periodDurationUs;
}
window.set(
/* uid= */ windowDefinition.id,
windowDefinition.mediaItem,
@ -376,7 +482,7 @@ public final class FakeTimeline extends Timeline {
windowDefinition.isDynamic,
windowDefinition.isLive ? windowDefinition.mediaItem.liveConfiguration : null,
windowDefinition.defaultPositionUs,
windowDefinition.durationUs,
windowDurationUs,
periodOffsets[windowIndex],
periodOffsets[windowIndex + 1] - 1,
windowDefinition.windowOffsetInFirstPeriodUs);
@ -396,11 +502,15 @@ public final class FakeTimeline extends Timeline {
TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex];
Object id = setIds ? windowPeriodIndex : null;
Object uid = setIds ? Pair.create(windowDefinition.id, windowPeriodIndex) : null;
AdPlaybackState adPlaybackState =
windowDefinition.adPlaybackStates.get(
periodIndex % windowDefinition.adPlaybackStates.size());
// Arbitrarily set period duration by distributing window duration equally among all periods.
long periodDurationUs =
windowDefinition.durationUs == C.TIME_UNSET
periodIndex == windowDefinition.periodCount - 1
&& windowDefinition.durationUs == C.TIME_UNSET
? C.TIME_UNSET
: windowDefinition.durationUs / windowDefinition.periodCount;
: (windowDefinition.durationUs / windowDefinition.periodCount);
long positionInWindowUs;
if (windowPeriodIndex == 0) {
if (windowDefinition.durationUs != C.TIME_UNSET) {
@ -414,9 +524,11 @@ public final class FakeTimeline extends Timeline {
id,
uid,
windowIndex,
periodDurationUs,
periodDurationUs == C.TIME_UNSET
? C.TIME_UNSET
: periodDurationUs - getServerSideAdInsertionAdDurationUs(adPlaybackState),
positionInWindowUs,
windowDefinition.adPlaybackState,
adPlaybackState,
windowDefinition.isPlaceholder);
return period;
}
@ -442,6 +554,22 @@ public final class FakeTimeline extends Timeline {
return Pair.create(windowDefinition.id, windowPeriodIndex);
}
/**
* Returns a map of ad playback states keyed by the period UID.
*
* @param windowIndex The window index of the window to get the map of ad playback states from.
* @return The map of {@link AdPlaybackState ad playback states}.
*/
public ImmutableMap<Object, AdPlaybackState> getAdPlaybackStates(int windowIndex) {
Map<Object, AdPlaybackState> adPlaybackStateMap = new HashMap<>();
TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex];
for (int i = 0; i < windowDefinition.adPlaybackStates.size(); i++) {
adPlaybackStateMap.put(
new Pair<>(windowDefinition.id, i), windowDefinition.adPlaybackStates.get(i));
}
return ImmutableMap.copyOf(adPlaybackStateMap);
}
private static TimelineWindowDefinition[] createDefaultWindowDefinitions(int windowCount) {
TimelineWindowDefinition[] windowDefinitions = new TimelineWindowDefinition[windowCount];
for (int i = 0; i < windowCount; i++) {
@ -449,4 +577,15 @@ public final class FakeTimeline extends Timeline {
}
return windowDefinitions;
}
private static long getServerSideAdInsertionAdDurationUs(AdPlaybackState adPlaybackState) {
long adDurationUs = 0;
for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(i);
if (adGroup.isServerSideInserted) {
adDurationUs += sum(adGroup.durationsUs);
}
}
return adDurationUs;
}
}

View File

@ -0,0 +1,106 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.test.utils;
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 androidx.media3.common.AdPlaybackState;
import androidx.media3.common.Timeline;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link FakeTimeline}. */
@RunWith(AndroidJUnit4.class)
public class FakeTimelineTest {
@Test
public void createMultiPeriodAdTimeline_firstPeriodIsAd() {
Timeline.Window window = new Timeline.Window();
Timeline.Period period = new Timeline.Period();
Object windowId = new Object();
int numberOfPlayedAds = 2;
FakeTimeline timeline =
FakeTimeline.createMultiPeriodAdTimeline(
windowId,
numberOfPlayedAds,
/* isAdPeriodFlags...= */ true,
false,
true,
true,
true,
false,
true);
assertThat(timeline.getWindowCount()).isEqualTo(1);
assertThat(timeline.getPeriodCount()).isEqualTo(7);
// Assert content periods and window duration.
Timeline.Period contentPeriod1 = timeline.getPeriod(/* periodIndex= */ 1, period);
Timeline.Period contentPeriod5 = timeline.getPeriod(/* periodIndex= */ 5, period);
assertThat(contentPeriod1.durationUs).isEqualTo(DEFAULT_WINDOW_DURATION_US / 7);
assertThat(contentPeriod5.durationUs).isEqualTo(DEFAULT_WINDOW_DURATION_US / 7);
assertThat(contentPeriod1.getAdGroupCount()).isEqualTo(0);
assertThat(contentPeriod5.getAdGroupCount()).isEqualTo(0);
timeline.getWindow(/* windowIndex= */ 0, window);
assertThat(window.uid).isEqualTo(windowId);
assertThat(window.durationUs).isEqualTo(contentPeriod1.durationUs + contentPeriod5.durationUs);
assertThat(window.positionInFirstPeriodUs).isEqualTo(DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US);
// Assert ad periods.
int[] adIndices = {0, 2, 3, 4, 6};
int adCounter = 0;
for (int periodIndex : adIndices) {
Timeline.Period adPeriod = timeline.getPeriod(periodIndex, period);
assertThat(adPeriod.isServerSideInsertedAdGroup(0)).isTrue();
assertThat(adPeriod.getAdGroupCount()).isEqualTo(1);
assertThat(adPeriod.durationUs).isEqualTo(0);
if (adPeriod.getAdGroupCount() > 0) {
if (adCounter < numberOfPlayedAds) {
assertThat(adPeriod.getAdState(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0))
.isEqualTo(AdPlaybackState.AD_STATE_PLAYED);
} else {
assertThat(adPeriod.getAdState(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0))
.isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
}
adCounter++;
}
long expectedDurationUs =
(DEFAULT_WINDOW_DURATION_US / 7)
+ (periodIndex == 0 ? DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US : 0);
assertThat(adPeriod.getAdDurationUs(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0))
.isEqualTo(expectedDurationUs);
}
}
@Test
public void createMultiPeriodAdTimeline_firstPeriodIsContent_correctWindowDurationUs() {
Timeline.Window window = new Timeline.Window();
FakeTimeline timeline =
FakeTimeline.createMultiPeriodAdTimeline(
/* windowId= */ new Object(),
/* numberOfPlayedAds= */ 0,
/* isAdPeriodFlags...= */ false,
true,
true,
false);
timeline.getWindow(/* windowIndex= */ 0, window);
// Assert content periods and window duration.
assertThat(window.durationUs).isEqualTo(DEFAULT_WINDOW_DURATION_US / 2);
assertThat(window.positionInFirstPeriodUs).isEqualTo(DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US);
}
}