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
This commit is contained in:
parent
bd4ba4c583
commit
795210d7bc
@ -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.
|
||||
* @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) {
|
||||
return !adPlaybackState.adGroups[adGroupIndex].hasUnplayedAds();
|
||||
|
@ -108,7 +108,8 @@ public final class AdPlaybackState implements Bundleable {
|
||||
public int getNextAdIndexToPlay(int lastPlayedAdIndex) {
|
||||
int nextAdIndexToPlay = lastPlayedAdIndex + 1;
|
||||
while (nextAdIndexToPlay < states.length) {
|
||||
if (states[nextAdIndexToPlay] == AD_STATE_UNAVAILABLE
|
||||
if (isServerSideInserted
|
||||
|| states[nextAdIndexToPlay] == AD_STATE_UNAVAILABLE
|
||||
|| states[nextAdIndexToPlay] == AD_STATE_AVAILABLE) {
|
||||
break;
|
||||
}
|
||||
@ -117,11 +118,26 @@ public final class AdPlaybackState implements Bundleable {
|
||||
return nextAdIndexToPlay;
|
||||
}
|
||||
|
||||
/** Returns whether the ad group has at least one ad that still needs to be played. */
|
||||
public boolean hasUnplayedAds() {
|
||||
/** Returns whether the ad group has at least one ad that should be played. */
|
||||
public boolean shouldPlayAdGroup() {
|
||||
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
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o) {
|
||||
@ -473,7 +489,7 @@ public final class AdPlaybackState implements Bundleable {
|
||||
int index = 0;
|
||||
while (index < adGroupTimesUs.length
|
||||
&& ((adGroupTimesUs[index] != C.TIME_END_OF_SOURCE && adGroupTimesUs[index] <= positionUs)
|
||||
|| !adGroups[index].hasUnplayedAds())) {
|
||||
|| !adGroups[index].shouldPlayAdGroup())) {
|
||||
index++;
|
||||
}
|
||||
return index < adGroupTimesUs.length ? index : C.INDEX_UNSET;
|
||||
@ -501,7 +517,7 @@ public final class AdPlaybackState implements Bundleable {
|
||||
* @return The updated ad playback state.
|
||||
*/
|
||||
@CheckResult
|
||||
public AdPlaybackState withAdGroupTimesUs(long[] adGroupTimesUs) {
|
||||
public AdPlaybackState withAdGroupTimesUs(long... adGroupTimesUs) {
|
||||
AdGroup[] adGroups =
|
||||
adGroupTimesUs.length < adGroupCount
|
||||
? Util.nullSafeArrayCopy(this.adGroups, adGroupTimesUs.length)
|
||||
|
@ -161,6 +161,35 @@ public class AdPlaybackStateTest {
|
||||
assertThat(state.adGroups[0].getNextAdIndexToPlay(0)).isEqualTo(2);
|
||||
}
|
||||
|
||||
@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() {
|
||||
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1);
|
||||
@ -226,4 +255,152 @@ public class AdPlaybackStateTest {
|
||||
|
||||
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;
|
||||
// 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
|
||||
// discontinuity until we reach the former next ad group position.
|
||||
boolean sameOldAndNewPeriodUid = oldPeriodId.periodUid.equals(newPeriodUid);
|
||||
boolean onlyNextAdGroupIndexIncreased =
|
||||
oldPeriodId.periodUid.equals(newPeriodUid)
|
||||
sameOldAndNewPeriodUid
|
||||
&& !oldPeriodId.isAd()
|
||||
&& !periodIdWithAds.isAd()
|
||||
&& 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;
|
||||
if (newPeriodId.isAd()) {
|
||||
|
@ -358,6 +358,7 @@ import com.google.common.collect.ImmutableList;
|
||||
: periodHolder.toRendererTime(newPeriodInfo.durationUs);
|
||||
boolean isReadingAndReadBeyondNewDuration =
|
||||
periodHolder == reading
|
||||
&& !isUsingSameStreamForNextMediaPeriod(timeline, periodHolder.info.id)
|
||||
&& (maxRendererReadPositionUs == C.TIME_END_OF_SOURCE
|
||||
|| maxRendererReadPositionUs >= newDurationInRendererTime);
|
||||
boolean readingPeriodRemoved = removeAfter(periodHolder);
|
||||
@ -858,4 +859,20 @@ import com.google.common.collect.ImmutableList;
|
||||
}
|
||||
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;
|
||||
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.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.runUntilPendingCommandsAreFullyHandled;
|
||||
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
|
||||
@ -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.FakeTrackSelection;
|
||||
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.TestExoPlayerBuilder;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
@ -10446,6 +10448,73 @@ public final class ExoPlayerTest {
|
||||
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.
|
||||
|
||||
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
|
||||
|
@ -369,9 +369,9 @@ public final class MediaPeriodQueueTest {
|
||||
updateQueuedPeriods_withDurationChangeInPlayingContent_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {
|
||||
setupAdTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US);
|
||||
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
||||
enqueueNext(); // Content before first ad.
|
||||
enqueueNext(); // First ad.
|
||||
enqueueNext(); // Content between ads.
|
||||
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);
|
||||
@ -389,6 +389,65 @@ public final class MediaPeriodQueueTest {
|
||||
.isEqualTo(FIRST_AD_START_TIME_US - 2000);
|
||||
}
|
||||
|
||||
@Test
|
||||
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() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user