Commit 795210d7 by tonihei Committed by Oliver Woodman

Update player logic to handle server-side inserted ads.

There are two main changes that need to be made:
 1. Whenever we determine the next ad to play, we need to select a
    server-side inserted ad even if it has been played already (because
    it's part of the stream).
 2. When the Timeline is updated in the player, we need to avoid changes
    that would unnecessarily reset the renderers. Whenever a Timeline
    change replaces content with a server-side inserted ad at the same
    position we can just keep the existing MediaPeriod and also if the
    duration of the current MediaPeriod is reduced but it is followed by
    a MediaPeriod in the same SSAI stream, we can don't need to reset
    the renderers as we keep playing the same stream.

PiperOrigin-RevId: 373745031
parent bd4ba4c5
...@@ -738,10 +738,12 @@ public abstract class Timeline implements Bundleable { ...@@ -738,10 +738,12 @@ public abstract class Timeline implements Bundleable {
} }
/** /**
* Returns whether the ad group at index {@code adGroupIndex} has been played. * Returns whether all ads in the ad group at index {@code adGroupIndex} have been played,
* skipped or failed.
* *
* @param adGroupIndex The ad group index. * @param adGroupIndex The ad group index.
* @return Whether the ad group at index {@code adGroupIndex} has been played. * @return Whether all ads in the ad group at index {@code adGroupIndex} have been played,
* skipped or failed.
*/ */
public boolean hasPlayedAdGroup(int adGroupIndex) { public boolean hasPlayedAdGroup(int adGroupIndex) {
return !adPlaybackState.adGroups[adGroupIndex].hasUnplayedAds(); return !adPlaybackState.adGroups[adGroupIndex].hasUnplayedAds();
......
...@@ -108,7 +108,8 @@ public final class AdPlaybackState implements Bundleable { ...@@ -108,7 +108,8 @@ public final class AdPlaybackState implements Bundleable {
public int getNextAdIndexToPlay(int lastPlayedAdIndex) { public int getNextAdIndexToPlay(int lastPlayedAdIndex) {
int nextAdIndexToPlay = lastPlayedAdIndex + 1; int nextAdIndexToPlay = lastPlayedAdIndex + 1;
while (nextAdIndexToPlay < states.length) { while (nextAdIndexToPlay < states.length) {
if (states[nextAdIndexToPlay] == AD_STATE_UNAVAILABLE if (isServerSideInserted
|| states[nextAdIndexToPlay] == AD_STATE_UNAVAILABLE
|| states[nextAdIndexToPlay] == AD_STATE_AVAILABLE) { || states[nextAdIndexToPlay] == AD_STATE_AVAILABLE) {
break; break;
} }
...@@ -117,11 +118,26 @@ public final class AdPlaybackState implements Bundleable { ...@@ -117,11 +118,26 @@ public final class AdPlaybackState implements Bundleable {
return nextAdIndexToPlay; return nextAdIndexToPlay;
} }
/** Returns whether the ad group has at least one ad that still needs to be played. */ /** Returns whether the ad group has at least one ad that should be played. */
public boolean hasUnplayedAds() { public boolean shouldPlayAdGroup() {
return count == C.LENGTH_UNSET || getFirstAdIndexToPlay() < count; return count == C.LENGTH_UNSET || getFirstAdIndexToPlay() < count;
} }
/**
* Returns whether the ad group has at least one ad that is neither played, skipped, nor failed.
*/
public boolean hasUnplayedAds() {
if (count == C.LENGTH_UNSET) {
return true;
}
for (int i = 0; i < count; i++) {
if (states[i] == AD_STATE_UNAVAILABLE || states[i] == AD_STATE_AVAILABLE) {
return true;
}
}
return false;
}
@Override @Override
public boolean equals(@Nullable Object o) { public boolean equals(@Nullable Object o) {
if (this == o) { if (this == o) {
...@@ -473,7 +489,7 @@ public final class AdPlaybackState implements Bundleable { ...@@ -473,7 +489,7 @@ public final class AdPlaybackState implements Bundleable {
int index = 0; int index = 0;
while (index < adGroupTimesUs.length while (index < adGroupTimesUs.length
&& ((adGroupTimesUs[index] != C.TIME_END_OF_SOURCE && adGroupTimesUs[index] <= positionUs) && ((adGroupTimesUs[index] != C.TIME_END_OF_SOURCE && adGroupTimesUs[index] <= positionUs)
|| !adGroups[index].hasUnplayedAds())) { || !adGroups[index].shouldPlayAdGroup())) {
index++; index++;
} }
return index < adGroupTimesUs.length ? index : C.INDEX_UNSET; return index < adGroupTimesUs.length ? index : C.INDEX_UNSET;
...@@ -501,7 +517,7 @@ public final class AdPlaybackState implements Bundleable { ...@@ -501,7 +517,7 @@ public final class AdPlaybackState implements Bundleable {
* @return The updated ad playback state. * @return The updated ad playback state.
*/ */
@CheckResult @CheckResult
public AdPlaybackState withAdGroupTimesUs(long[] adGroupTimesUs) { public AdPlaybackState withAdGroupTimesUs(long... adGroupTimesUs) {
AdGroup[] adGroups = AdGroup[] adGroups =
adGroupTimesUs.length < adGroupCount adGroupTimesUs.length < adGroupCount
? Util.nullSafeArrayCopy(this.adGroups, adGroupTimesUs.length) ? Util.nullSafeArrayCopy(this.adGroups, adGroupTimesUs.length)
......
...@@ -162,6 +162,35 @@ public class AdPlaybackStateTest { ...@@ -162,6 +162,35 @@ public class AdPlaybackStateTest {
} }
@Test @Test
public void getFirstAdIndexToPlay_withPlayedServerSideInsertedAds_returnsFirstIndex() {
state = state.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true);
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, TEST_URI);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, TEST_URI);
state = state.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
assertThat(state.adGroups[0].getFirstAdIndexToPlay()).isEqualTo(0);
}
@Test
public void getNextAdIndexToPlay_withPlayedServerSideInsertedAds_returnsNextIndex() {
state = state.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true);
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, TEST_URI);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, TEST_URI);
state = state.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
state = state.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1);
state = state.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2);
assertThat(state.adGroups[0].getNextAdIndexToPlay(/* lastPlayedAdIndex= */ 0)).isEqualTo(1);
assertThat(state.adGroups[0].getNextAdIndexToPlay(/* lastPlayedAdIndex= */ 1)).isEqualTo(2);
}
@Test
public void setAdStateTwiceThrows() { public void setAdStateTwiceThrows() {
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1); state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1);
state = state.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); state = state.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
...@@ -226,4 +255,152 @@ public class AdPlaybackStateTest { ...@@ -226,4 +255,152 @@ public class AdPlaybackStateTest {
assertThat(AdPlaybackState.AdGroup.CREATOR.fromBundle(adGroup.toBundle())).isEqualTo(adGroup); assertThat(AdPlaybackState.AdGroup.CREATOR.fromBundle(adGroup.toBundle())).isEqualTo(adGroup);
} }
@Test
public void
getAdGroupIndexAfterPositionUs_withClientSideInsertedAds_returnsNextAdGroupWithUnplayedAds() {
AdPlaybackState state =
new AdPlaybackState(
/* adsId= */ new Object(),
/* adGroupTimesUs...= */ 0,
1000,
2000,
3000,
4000,
C.TIME_END_OF_SOURCE)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1)
.withAdCount(/* adGroupIndex= */ 2, /* adCount= */ 1)
.withAdCount(/* adGroupIndex= */ 3, /* adCount= */ 1)
.withAdCount(/* adGroupIndex= */ 4, /* adCount= */ 1)
.withAdCount(/* adGroupIndex= */ 5, /* adCount= */ 1)
.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0)
.withPlayedAd(/* adGroupIndex= */ 3, /* adIndexInAdGroup= */ 0);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 0, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(2);
assertThat(
state.getAdGroupIndexAfterPositionUs(/* positionUs= */ 0, /* periodDurationUs= */ 5000))
.isEqualTo(2);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 1999, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(2);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 1999, /* periodDurationUs= */ 5000))
.isEqualTo(2);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 2000, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(4);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 2000, /* periodDurationUs= */ 5000))
.isEqualTo(4);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 3999, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(4);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 3999, /* periodDurationUs= */ 5000))
.isEqualTo(4);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 4000, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(5);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 4000, /* periodDurationUs= */ 5000))
.isEqualTo(5);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 4999, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(5);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 4999, /* periodDurationUs= */ 5000))
.isEqualTo(5);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 5000, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(5);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 5000, /* periodDurationUs= */ 5000))
.isEqualTo(C.INDEX_UNSET);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(C.INDEX_UNSET);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ 5000))
.isEqualTo(C.INDEX_UNSET);
}
@Test
public void getAdGroupIndexAfterPositionUs_withServerSideInsertedAds_returnsNextAdGroup() {
AdPlaybackState state =
new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs...= */ 0, 1000, 2000)
.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true)
.withIsServerSideInserted(/* adGroupIndex= */ 1, /* isServerSideInserted= */ true)
.withIsServerSideInserted(/* adGroupIndex= */ 2, /* isServerSideInserted= */ true)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1)
.withAdCount(/* adGroupIndex= */ 2, /* adCount= */ 1)
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
.withPlayedAd(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 0, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(1);
assertThat(
state.getAdGroupIndexAfterPositionUs(/* positionUs= */ 0, /* periodDurationUs= */ 5000))
.isEqualTo(1);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 999, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(1);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 999, /* periodDurationUs= */ 5000))
.isEqualTo(1);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 1000, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(2);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 1000, /* periodDurationUs= */ 5000))
.isEqualTo(2);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 1999, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(2);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 1999, /* periodDurationUs= */ 5000))
.isEqualTo(2);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 2000, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(C.INDEX_UNSET);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 2000, /* periodDurationUs= */ 5000))
.isEqualTo(C.INDEX_UNSET);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(C.INDEX_UNSET);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ 5000))
.isEqualTo(C.INDEX_UNSET);
}
} }
...@@ -2601,12 +2601,25 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -2601,12 +2601,25 @@ import java.util.concurrent.atomic.AtomicBoolean;
// Drop update if we keep playing the same content (MediaPeriod.periodUid are identical) and // Drop update if we keep playing the same content (MediaPeriod.periodUid are identical) and
// the only change is that MediaPeriodId.nextAdGroupIndex increased. This postpones a potential // the only change is that MediaPeriodId.nextAdGroupIndex increased. This postpones a potential
// discontinuity until we reach the former next ad group position. // discontinuity until we reach the former next ad group position.
boolean sameOldAndNewPeriodUid = oldPeriodId.periodUid.equals(newPeriodUid);
boolean onlyNextAdGroupIndexIncreased = boolean onlyNextAdGroupIndexIncreased =
oldPeriodId.periodUid.equals(newPeriodUid) sameOldAndNewPeriodUid
&& !oldPeriodId.isAd() && !oldPeriodId.isAd()
&& !periodIdWithAds.isAd() && !periodIdWithAds.isAd()
&& earliestCuePointIsUnchangedOrLater; && earliestCuePointIsUnchangedOrLater;
MediaPeriodId newPeriodId = onlyNextAdGroupIndexIncreased ? oldPeriodId : periodIdWithAds; // Drop update if the change is from/to server-side inserted ads at the same content position to
// avoid any unintentional renderer reset.
timeline.getPeriodByUid(newPeriodUid, period);
boolean isInStreamAdChange =
sameOldAndNewPeriodUid
&& !isUsingPlaceholderPeriod
&& oldContentPositionUs == newContentPositionUs
&& ((periodIdWithAds.isAd()
&& period.isServerSideInsertedAdGroup(periodIdWithAds.adGroupIndex))
|| (oldPeriodId.isAd()
&& period.isServerSideInsertedAdGroup(oldPeriodId.adGroupIndex)));
MediaPeriodId newPeriodId =
onlyNextAdGroupIndexIncreased || isInStreamAdChange ? oldPeriodId : periodIdWithAds;
long periodPositionUs = contentPositionForAdResolutionUs; long periodPositionUs = contentPositionForAdResolutionUs;
if (newPeriodId.isAd()) { if (newPeriodId.isAd()) {
......
...@@ -358,6 +358,7 @@ import com.google.common.collect.ImmutableList; ...@@ -358,6 +358,7 @@ import com.google.common.collect.ImmutableList;
: periodHolder.toRendererTime(newPeriodInfo.durationUs); : periodHolder.toRendererTime(newPeriodInfo.durationUs);
boolean isReadingAndReadBeyondNewDuration = boolean isReadingAndReadBeyondNewDuration =
periodHolder == reading periodHolder == reading
&& !isUsingSameStreamForNextMediaPeriod(timeline, periodHolder.info.id)
&& (maxRendererReadPositionUs == C.TIME_END_OF_SOURCE && (maxRendererReadPositionUs == C.TIME_END_OF_SOURCE
|| maxRendererReadPositionUs >= newDurationInRendererTime); || maxRendererReadPositionUs >= newDurationInRendererTime);
boolean readingPeriodRemoved = removeAfter(periodHolder); boolean readingPeriodRemoved = removeAfter(periodHolder);
...@@ -858,4 +859,20 @@ import com.google.common.collect.ImmutableList; ...@@ -858,4 +859,20 @@ import com.google.common.collect.ImmutableList;
} }
return startPositionUs + period.getContentResumeOffsetUs(adGroupIndex); return startPositionUs + period.getContentResumeOffsetUs(adGroupIndex);
} }
private boolean isUsingSameStreamForNextMediaPeriod(
Timeline timeline, MediaPeriodId mediaPeriodId) {
// Server-side inserted ads or content after them will use the same underlying stream.
if (mediaPeriodId.isAd()) {
return timeline
.getPeriodByUid(mediaPeriodId.periodUid, period)
.isServerSideInsertedAdGroup(mediaPeriodId.adGroupIndex);
} else if (mediaPeriodId.nextAdGroupIndex == C.INDEX_UNSET) {
return false;
} else {
return timeline
.getPeriodByUid(mediaPeriodId.periodUid, period)
.isServerSideInsertedAdGroup(mediaPeriodId.nextAdGroupIndex);
}
}
} }
...@@ -39,6 +39,7 @@ import static com.google.android.exoplayer2.Player.COMMAND_SET_SPEED_AND_PITCH; ...@@ -39,6 +39,7 @@ import static com.google.android.exoplayer2.Player.COMMAND_SET_SPEED_AND_PITCH;
import static com.google.android.exoplayer2.Player.COMMAND_SET_VIDEO_SURFACE; import static com.google.android.exoplayer2.Player.COMMAND_SET_VIDEO_SURFACE;
import static com.google.android.exoplayer2.Player.COMMAND_SET_VOLUME; import static com.google.android.exoplayer2.Player.COMMAND_SET_VOLUME;
import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runMainLooperUntil; import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runMainLooperUntil;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.playUntilPosition;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.playUntilStartOfWindow; import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.playUntilStartOfWindow;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled; import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPlaybackState; import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
...@@ -120,6 +121,7 @@ import com.google.android.exoplayer2.testutil.FakeTimeline; ...@@ -120,6 +121,7 @@ import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelection;
import com.google.android.exoplayer2.testutil.FakeTrackSelector; import com.google.android.exoplayer2.testutil.FakeTrackSelector;
import com.google.android.exoplayer2.testutil.FakeVideoRenderer;
import com.google.android.exoplayer2.testutil.NoUidTimeline; import com.google.android.exoplayer2.testutil.NoUidTimeline;
import com.google.android.exoplayer2.testutil.TestExoPlayerBuilder; import com.google.android.exoplayer2.testutil.TestExoPlayerBuilder;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
...@@ -10446,6 +10448,73 @@ public final class ExoPlayerTest { ...@@ -10446,6 +10448,73 @@ public final class ExoPlayerTest {
player.release(); player.release();
} }
@Test
public void newServerSideInsertedAdAtPlaybackPosition_keepsRenderersEnabled() throws Exception {
// Injecting renderer to count number of renderer resets.
AtomicReference<FakeVideoRenderer> videoRenderer = new AtomicReference<>();
SimpleExoPlayer player =
new TestExoPlayerBuilder(context)
.setRenderersFactory(
(handler, videoListener, audioListener, textOutput, metadataOutput) -> {
videoRenderer.set(new FakeVideoRenderer(handler, videoListener));
return new Renderer[] {videoRenderer.get()};
})
.build();
// Live stream timeline with unassigned next ad group.
AdPlaybackState initialAdPlaybackState =
new AdPlaybackState(
/* adsId= */ new Object(), /* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE)
.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdDurationsUs(new long[][] {new long[] {10 * C.MICROS_PER_SECOND}});
// Updated timeline with ad group at 18 seconds.
long firstSampleTimeUs = TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
Timeline initialTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* durationUs= */ C.TIME_UNSET,
initialAdPlaybackState));
AdPlaybackState updatedAdPlaybackState =
initialAdPlaybackState.withAdGroupTimesUs(
/* adGroupTimesUs...= */ firstSampleTimeUs + 18 * C.MICROS_PER_SECOND);
Timeline updatedTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* durationUs= */ C.TIME_UNSET,
updatedAdPlaybackState));
// Add samples to allow player to load and start playing (but no EOS as this is a live stream).
FakeMediaSource mediaSource =
new FakeMediaSource(
initialTimeline,
DrmSessionManager.DRM_UNSUPPORTED,
(format, mediaPeriodId) ->
ImmutableList.of(
oneByteSample(firstSampleTimeUs, C.BUFFER_FLAG_KEY_FRAME),
oneByteSample(firstSampleTimeUs + 40 * C.MICROS_PER_SECOND)),
ExoPlayerTestRunner.VIDEO_FORMAT);
// Set updated ad group once we reach 20 seconds, and then continue playing until 40 seconds.
player
.createMessage((message, payload) -> mediaSource.setNewSourceInfo(updatedTimeline))
.setPosition(20_000)
.send();
player.setMediaSource(mediaSource);
player.prepare();
playUntilPosition(player, /* windowIndex= */ 0, /* positionMs= */ 40_000);
player.release();
// Assert that the renderer hasn't been reset despite the inserted ad group.
assertThat(videoRenderer.get().positionResetCount).isEqualTo(1);
}
// Internal methods. // Internal methods.
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
......
...@@ -369,9 +369,9 @@ public final class MediaPeriodQueueTest { ...@@ -369,9 +369,9 @@ public final class MediaPeriodQueueTest {
updateQueuedPeriods_withDurationChangeInPlayingContent_handlesChangeAndRemovesPeriodsAfterChangedPeriod() { updateQueuedPeriods_withDurationChangeInPlayingContent_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {
setupAdTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US); setupAdTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US);
setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0);
enqueueNext(); // Content before first ad. enqueueNext(); // Content before ad.
enqueueNext(); // First ad. enqueueNext(); // Ad.
enqueueNext(); // Content between ads. enqueueNext(); // Content after ad.
// Change position of first ad (= change duration of playing content before first ad). // Change position of first ad (= change duration of playing content before first ad).
updateAdPlaybackStateAndTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US - 2000); updateAdPlaybackStateAndTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US - 2000);
...@@ -391,6 +391,65 @@ public final class MediaPeriodQueueTest { ...@@ -391,6 +391,65 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void public void
updateQueuedPeriods_withDurationChangeInPlayingContentAfterReadingPosition_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() {
setupAdTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US);
setAdGroupLoaded(/* adGroupIndex= */ 0);
enqueueNext(); // Content before ad.
enqueueNext(); // Ad.
enqueueNext(); // Content after ad.
// Change position of first ad (= change duration of playing content before first ad).
updateAdPlaybackStateAndTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US - 2000);
setAdGroupLoaded(/* adGroupIndex= */ 0);
long maxRendererReadPositionUs = FIRST_AD_START_TIME_US - 1000;
boolean changeHandled =
mediaPeriodQueue.updateQueuedPeriods(
playbackInfo.timeline, /* rendererPositionUs= */ 0, maxRendererReadPositionUs);
assertThat(changeHandled).isFalse();
assertThat(getQueueLength()).isEqualTo(1);
assertThat(mediaPeriodQueue.getPlayingPeriod().info.endPositionUs)
.isEqualTo(FIRST_AD_START_TIME_US - 2000);
assertThat(mediaPeriodQueue.getPlayingPeriod().info.durationUs)
.isEqualTo(FIRST_AD_START_TIME_US - 2000);
}
@Test
public void
updateQueuedPeriods_withDurationChangeInPlayingContentAfterReadingPositionInServerSideInsertedAd_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {
adPlaybackState =
new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimes... */ FIRST_AD_START_TIME_US)
.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true);
SinglePeriodAdTimeline adTimeline =
new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState);
setupTimeline(adTimeline);
setAdGroupLoaded(/* adGroupIndex= */ 0);
enqueueNext(); // Content before ad.
enqueueNext(); // Ad.
enqueueNext(); // Content after ad.
// Change position of first ad (= change duration of playing content before first ad).
adPlaybackState =
new AdPlaybackState(
/* adsId= */ new Object(), /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US - 2000)
.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true);
updateTimeline();
setAdGroupLoaded(/* adGroupIndex= */ 0);
long maxRendererReadPositionUs = FIRST_AD_START_TIME_US - 1000;
boolean changeHandled =
mediaPeriodQueue.updateQueuedPeriods(
playbackInfo.timeline, /* rendererPositionUs= */ 0, maxRendererReadPositionUs);
assertThat(changeHandled).isTrue();
assertThat(getQueueLength()).isEqualTo(1);
assertThat(mediaPeriodQueue.getPlayingPeriod().info.endPositionUs)
.isEqualTo(FIRST_AD_START_TIME_US - 2000);
assertThat(mediaPeriodQueue.getPlayingPeriod().info.durationUs)
.isEqualTo(FIRST_AD_START_TIME_US - 2000);
}
@Test
public void
updateQueuedPeriods_withDurationChangeAfterReadingPeriod_handlesChangeAndRemovesPeriodsAfterChangedPeriod() { updateQueuedPeriods_withDurationChangeAfterReadingPeriod_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {
setupAdTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); setupAdTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US);
setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment