Set the next live ad in ad group to avoid rebuffering

To avoid the `MediaPeriodQueue`to discard the reading period, we can set the next ad of an ad group early and then (possibly) only change it's duration once we receive the actual duration. This way we avoid a rebuffering as a result of the reading period being discarded.

The change also takes care to properly set ad break and their durations when we join the live stream at the moment when an ad is playing.

PiperOrigin-RevId: 423163467
This commit is contained in:
bachinger 2022-01-20 22:47:05 +00:00 committed by Ian Baker
parent a9e6bc60cb
commit a1424c834f
4 changed files with 85 additions and 35 deletions

View File

@ -2539,6 +2539,20 @@ public final class Util {
.build(); .build();
} }
/**
* Returns the sum of all summands of the given array.
*
* @param summands The summands to calculate the sum from.
* @return The sum of all summands.
*/
public static long sum(long... summands) {
long sum = 0;
for (long summand : summands) {
sum += summand;
}
return sum;
}
@Nullable @Nullable
private static String getSystemProperty(String name) { private static String getSystemProperty(String name) {
try { try {

View File

@ -15,6 +15,7 @@
*/ */
package androidx.media3.exoplayer.source.ads; package androidx.media3.exoplayer.source.ads;
import static androidx.media3.common.util.Util.sum;
import static java.lang.Math.max; import static java.lang.Math.max;
import androidx.annotation.CheckResult; import androidx.annotation.CheckResult;
@ -36,23 +37,25 @@ public final class ServerSideAdInsertionUtil {
/** /**
* Adds a new server-side inserted ad group to an {@link AdPlaybackState}. * Adds a new server-side inserted ad group to an {@link AdPlaybackState}.
* *
* <p>If the first ad with a non-zero duration is not the first ad in the group, all ads before
* that ad are marked as skipped.
*
* @param adPlaybackState The existing {@link AdPlaybackState}. * @param adPlaybackState The existing {@link AdPlaybackState}.
* @param fromPositionUs The position in the underlying server-side inserted ads stream at which * @param fromPositionUs The position in the underlying server-side inserted ads stream at which
* the ad group starts, in microseconds. * the ad group starts, in microseconds.
* @param toPositionUs The position in the underlying server-side inserted ads stream at which the
* ad group ends, in microseconds.
* @param contentResumeOffsetUs The timestamp offset which should be added to the content stream * @param contentResumeOffsetUs The timestamp offset which should be added to the content stream
* when resuming playback after the ad group. An offset of 0 collapses the ad group to a * when resuming playback after the ad group. An offset of 0 collapses the ad group to a
* single insertion point, an offset of {@code toPositionUs-fromPositionUs} keeps the original * single insertion point, an offset of {@code toPositionUs-fromPositionUs} keeps the original
* stream timestamps after the ad group. * stream timestamps after the ad group.
* @param adDurationsUs The durations of the ads to be added to the group, in microseconds.
* @return The updated {@link AdPlaybackState}. * @return The updated {@link AdPlaybackState}.
*/ */
@CheckResult @CheckResult
public static AdPlaybackState addAdGroupToAdPlaybackState( public static AdPlaybackState addAdGroupToAdPlaybackState(
AdPlaybackState adPlaybackState, AdPlaybackState adPlaybackState,
long fromPositionUs, long fromPositionUs,
long toPositionUs, long contentResumeOffsetUs,
long contentResumeOffsetUs) { long... adDurationsUs) {
long adGroupInsertionPositionUs = long adGroupInsertionPositionUs =
getMediaPeriodPositionUsForContent( getMediaPeriodPositionUsForContent(
fromPositionUs, /* nextAdGroupIndex= */ C.INDEX_UNSET, adPlaybackState); fromPositionUs, /* nextAdGroupIndex= */ C.INDEX_UNSET, adPlaybackState);
@ -62,16 +65,21 @@ public final class ServerSideAdInsertionUtil {
&& adPlaybackState.getAdGroup(insertionIndex).timeUs <= adGroupInsertionPositionUs) { && adPlaybackState.getAdGroup(insertionIndex).timeUs <= adGroupInsertionPositionUs) {
insertionIndex++; insertionIndex++;
} }
long adDurationUs = toPositionUs - fromPositionUs;
adPlaybackState = adPlaybackState =
adPlaybackState adPlaybackState
.withNewAdGroup(insertionIndex, adGroupInsertionPositionUs) .withNewAdGroup(insertionIndex, adGroupInsertionPositionUs)
.withIsServerSideInserted(insertionIndex, /* isServerSideInserted= */ true) .withIsServerSideInserted(insertionIndex, /* isServerSideInserted= */ true)
.withAdCount(insertionIndex, /* adCount= */ 1) .withAdCount(insertionIndex, /* adCount= */ adDurationsUs.length)
.withAdDurationsUs(insertionIndex, adDurationUs) .withAdDurationsUs(insertionIndex, adDurationsUs)
.withContentResumeOffsetUs(insertionIndex, contentResumeOffsetUs); .withContentResumeOffsetUs(insertionIndex, contentResumeOffsetUs);
// Mark all ads as skipped that are before the first ad with a non-zero duration.
int adIndex = 0;
while (adIndex < adDurationsUs.length && adDurationsUs[adIndex] == 0) {
adPlaybackState =
adPlaybackState.withSkippedAd(insertionIndex, /* adIndexInAdGroup= */ adIndex++);
}
return correctFollowingAdGroupTimes( return correctFollowingAdGroupTimes(
adPlaybackState, insertionIndex, adDurationUs, contentResumeOffsetUs); adPlaybackState, insertionIndex, sum(adDurationsUs), contentResumeOffsetUs);
} }
/** /**

View File

@ -183,20 +183,20 @@ public final class ServerSideAdInsertionMediaSourceTest {
addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
adPlaybackState, adPlaybackState,
/* fromPositionUs= */ 0, /* fromPositionUs= */ 0,
/* toPositionUs= */ 200_000, /* contentResumeOffsetUs= */ 0,
/* contentResumeOffsetUs= */ 0); /* adDurationsUs...= */ 200_000);
adPlaybackState = adPlaybackState =
addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
adPlaybackState, adPlaybackState,
/* fromPositionUs= */ 400_000, /* fromPositionUs= */ 400_000,
/* toPositionUs= */ 700_000, /* contentResumeOffsetUs= */ 1_000_000,
/* contentResumeOffsetUs= */ 1_000_000); /* adDurationsUs...= */ 300_000);
AdPlaybackState firstAdPlaybackState = AdPlaybackState firstAdPlaybackState =
addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
adPlaybackState, adPlaybackState,
/* fromPositionUs= */ 900_000, /* fromPositionUs= */ 900_000,
/* toPositionUs= */ 1_000_000, /* contentResumeOffsetUs= */ 0,
/* contentResumeOffsetUs= */ 0); /* adDurationsUs...= */ 100_000);
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>(); AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
mediaSourceRef.set( mediaSourceRef.set(
@ -253,8 +253,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
new AdPlaybackState(/* adsId= */ new Object()), new AdPlaybackState(/* adsId= */ new Object()),
/* fromPositionUs= */ 900_000, /* fromPositionUs= */ 900_000,
/* toPositionUs= */ 1_000_000, /* contentResumeOffsetUs= */ 0,
/* contentResumeOffsetUs= */ 0); /* adDurationsUs...= */ 100_000);
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>(); AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
mediaSourceRef.set( mediaSourceRef.set(
new ServerSideAdInsertionMediaSource( new ServerSideAdInsertionMediaSource(
@ -281,8 +281,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
firstAdPlaybackState, firstAdPlaybackState,
/* fromPositionUs= */ 0, /* fromPositionUs= */ 0,
/* toPositionUs= */ 500_000, /* contentResumeOffsetUs= */ 0,
/* contentResumeOffsetUs= */ 0); /* adDurationsUs...= */ 500_000);
mediaSourceRef mediaSourceRef
.get() .get()
.setAdPlaybackStates(ImmutableMap.of(periodUid.get(), secondAdPlaybackState)); .setAdPlaybackStates(ImmutableMap.of(periodUid.get(), secondAdPlaybackState));
@ -324,8 +324,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
new AdPlaybackState(/* adsId= */ new Object()), new AdPlaybackState(/* adsId= */ new Object()),
/* fromPositionUs= */ 0, /* fromPositionUs= */ 0,
/* toPositionUs= */ 500_000, /* contentResumeOffsetUs= */ 0,
/* contentResumeOffsetUs= */ 0); /* adDurationsUs...= */ 500_000);
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>(); AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
mediaSourceRef.set( mediaSourceRef.set(
new ServerSideAdInsertionMediaSource( new ServerSideAdInsertionMediaSource(
@ -392,20 +392,20 @@ public final class ServerSideAdInsertionMediaSourceTest {
addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
adPlaybackState, adPlaybackState,
/* fromPositionUs= */ 0, /* fromPositionUs= */ 0,
/* toPositionUs= */ 100_000, /* contentResumeOffsetUs= */ 0,
/* contentResumeOffsetUs= */ 0); /* adDurationsUs...= */ 100_000);
adPlaybackState = adPlaybackState =
addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
adPlaybackState, adPlaybackState,
/* fromPositionUs= */ 600_000, /* fromPositionUs= */ 600_000,
/* toPositionUs= */ 700_000, /* contentResumeOffsetUs= */ 1_000_000,
/* contentResumeOffsetUs= */ 1_000_000); /* adDurationsUs...= */ 100_000);
AdPlaybackState firstAdPlaybackState = AdPlaybackState firstAdPlaybackState =
addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
adPlaybackState, adPlaybackState,
/* fromPositionUs= */ 900_000, /* fromPositionUs= */ 900_000,
/* toPositionUs= */ 1_000_000, /* contentResumeOffsetUs= */ 0,
/* contentResumeOffsetUs= */ 0); /* adDurationsUs...= */ 100_000);
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>(); AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
mediaSourceRef.set( mediaSourceRef.set(
@ -428,7 +428,7 @@ public final class ServerSideAdInsertionMediaSourceTest {
player.setMediaSource(mediaSourceRef.get()); player.setMediaSource(mediaSourceRef.get());
player.prepare(); player.prepare();
// Play to the first content part, then seek past the midroll. // Play to the first content part, then seek past the midroll.
playUntilPosition(player, /* windowIndex= */ 0, /* positionMs= */ 100); playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ 100);
player.seekTo(/* positionMs= */ 1_600); player.seekTo(/* positionMs= */ 1_600);
runUntilPendingCommandsAreFullyHandled(player); runUntilPendingCommandsAreFullyHandled(player);
long positionAfterSeekMs = player.getCurrentPosition(); long positionAfterSeekMs = player.getCurrentPosition();

View File

@ -22,6 +22,7 @@ import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.get
import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.getStreamPositionUsForAd; import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.getStreamPositionUsForAd;
import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.getStreamPositionUsForContent; import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.getStreamPositionUsForContent;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static java.util.Arrays.stream;
import androidx.media3.common.AdPlaybackState; import androidx.media3.common.AdPlaybackState;
import androidx.media3.common.C; import androidx.media3.common.C;
@ -47,8 +48,8 @@ public final class ServerSideAdInsertionUtilTest {
addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
state, state,
/* fromPositionUs= */ 4300, /* fromPositionUs= */ 4300,
/* toPositionUs= */ 4500, /* contentResumeOffsetUs= */ 400,
/* contentResumeOffsetUs= */ 400); /* adDurationsUs...= */ 200);
assertThat(state) assertThat(state)
.isEqualTo( .isEqualTo(
@ -65,8 +66,8 @@ public final class ServerSideAdInsertionUtilTest {
addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
state, state,
/* fromPositionUs= */ 2100, /* fromPositionUs= */ 2100,
/* toPositionUs= */ 2400, /* contentResumeOffsetUs= */ 0,
/* contentResumeOffsetUs= */ 0); /* adDurationsUs...= */ 300);
assertThat(state) assertThat(state)
.isEqualTo( .isEqualTo(
@ -87,8 +88,8 @@ public final class ServerSideAdInsertionUtilTest {
addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
state, state,
/* fromPositionUs= */ 0, /* fromPositionUs= */ 0,
/* toPositionUs= */ 100, /* contentResumeOffsetUs= */ 50,
/* contentResumeOffsetUs= */ 50); /* adDurationsUs...= */ 100);
assertThat(state) assertThat(state)
.isEqualTo( .isEqualTo(
@ -113,8 +114,8 @@ public final class ServerSideAdInsertionUtilTest {
addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
state, state,
/* fromPositionUs= */ 5000, /* fromPositionUs= */ 5000,
/* toPositionUs= */ 6000, /* contentResumeOffsetUs= */ 0,
/* contentResumeOffsetUs= */ 0); /* adDurationsUs...= */ 1000);
assertThat(state) assertThat(state)
.isEqualTo( .isEqualTo(
@ -144,6 +145,33 @@ public final class ServerSideAdInsertionUtilTest {
.withAdDurationsUs(/* adGroupIndex= */ 5, /* adDurationsUs...= */ 1000)); .withAdDurationsUs(/* adGroupIndex= */ 5, /* adDurationsUs...= */ 1000));
} }
@Test
public void addAdGroupToAdPlaybackState_emptyLeadingAds_markedAsSkipped() {
AdPlaybackState state = new AdPlaybackState(ADS_ID);
state =
addAdGroupToAdPlaybackState(
state,
/* fromPositionUs= */ 0,
/* contentResumeOffsetUs= */ 50_000,
/* adDurationsUs...= */ 0,
0,
10_000,
40_000,
0);
AdPlaybackState.AdGroup adGroup = state.getAdGroup(/* adGroupIndex= */ 0);
assertThat(adGroup.durationsUs[0]).isEqualTo(0);
assertThat(adGroup.states[0]).isEqualTo(AdPlaybackState.AD_STATE_SKIPPED);
assertThat(adGroup.durationsUs[1]).isEqualTo(0);
assertThat(adGroup.states[1]).isEqualTo(AdPlaybackState.AD_STATE_SKIPPED);
assertThat(adGroup.durationsUs[2]).isEqualTo(10_000);
assertThat(adGroup.states[2]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
assertThat(adGroup.durationsUs[4]).isEqualTo(0);
assertThat(adGroup.states[4]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
assertThat(stream(adGroup.durationsUs).sum()).isEqualTo(50_000);
}
@Test @Test
public void getStreamPositionUsForAd_returnsCorrectPositions() { public void getStreamPositionUsForAd_returnsCorrectPositions() {
// stream: 0-- ad1 --200-- content --2100-- ad2 --2300-- content --4300-- ad3 --4500-- content // stream: 0-- ad1 --200-- content --2100-- ad2 --2300-- content --4300-- ad3 --4500-- content