mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
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
7 changed files with 365 additions and 12 deletions
|
|
@ -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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
* @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)
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,35 @@ public class AdPlaybackStateTest {
|
||||||
assertThat(state.adGroups[0].getNextAdIndexToPlay(0)).isEqualTo(2);
|
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
|
@Test
|
||||||
public void setAdStateTwiceThrows() {
|
public void setAdStateTwiceThrows() {
|
||||||
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1);
|
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1);
|
||||||
|
|
@ -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;
|
||||||
// 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;
|
||||||
: 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;
|
||||||
}
|
}
|
||||||
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;
|
||||||
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;
|
||||||
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 {
|
||||||
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 {
|
||||||
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);
|
||||||
|
|
@ -389,6 +389,65 @@ public final class MediaPeriodQueueTest {
|
||||||
.isEqualTo(FIRST_AD_START_TIME_US - 2000);
|
.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
|
@Test
|
||||||
public void
|
public void
|
||||||
updateQueuedPeriods_withDurationChangeAfterReadingPeriod_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {
|
updateQueuedPeriods_withDurationChangeAfterReadingPeriod_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue