From e2cf266b1c903e3311607347891df8a5ef425c0f Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 20 Jan 2022 22:47:05 +0000 Subject: [PATCH] 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 --- .../google/android/exoplayer2/util/Util.java | 14 ++++++ .../source/ads/ServerSideAdInsertionUtil.java | 24 ++++++---- .../ServerSideAdInsertionMediaSourceTest.java | 38 ++++++++-------- .../ads/ServerSideAdInsertionUtilTest.java | 44 +++++++++++++++---- 4 files changed, 85 insertions(+), 35 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java index 9dad181b91..1a961d0585 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -2537,6 +2537,20 @@ public final class Util { .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 private static String getSystemProperty(String name) { try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionUtil.java index 59ccb5c90f..06aa3ef87b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionUtil.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.ads; +import static com.google.android.exoplayer2.util.Util.sum; import static java.lang.Math.max; import androidx.annotation.CheckResult; @@ -33,23 +34,25 @@ public final class ServerSideAdInsertionUtil { /** * Adds a new server-side inserted ad group to an {@link AdPlaybackState}. * + *

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 fromPositionUs The position in the underlying server-side inserted ads stream at which * 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 * 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 * 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}. */ @CheckResult public static AdPlaybackState addAdGroupToAdPlaybackState( AdPlaybackState adPlaybackState, long fromPositionUs, - long toPositionUs, - long contentResumeOffsetUs) { + long contentResumeOffsetUs, + long... adDurationsUs) { long adGroupInsertionPositionUs = getMediaPeriodPositionUsForContent( fromPositionUs, /* nextAdGroupIndex= */ C.INDEX_UNSET, adPlaybackState); @@ -59,16 +62,21 @@ public final class ServerSideAdInsertionUtil { && adPlaybackState.getAdGroup(insertionIndex).timeUs <= adGroupInsertionPositionUs) { insertionIndex++; } - long adDurationUs = toPositionUs - fromPositionUs; adPlaybackState = adPlaybackState .withNewAdGroup(insertionIndex, adGroupInsertionPositionUs) .withIsServerSideInserted(insertionIndex, /* isServerSideInserted= */ true) - .withAdCount(insertionIndex, /* adCount= */ 1) - .withAdDurationsUs(insertionIndex, adDurationUs) + .withAdCount(insertionIndex, /* adCount= */ adDurationsUs.length) + .withAdDurationsUs(insertionIndex, adDurationsUs) .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( - adPlaybackState, insertionIndex, adDurationUs, contentResumeOffsetUs); + adPlaybackState, insertionIndex, sum(adDurationsUs), contentResumeOffsetUs); } /** diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSourceTest.java index 66d85e360f..cd9b066f69 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSourceTest.java @@ -182,20 +182,20 @@ public final class ServerSideAdInsertionMediaSourceTest { addAdGroupToAdPlaybackState( adPlaybackState, /* fromPositionUs= */ 0, - /* toPositionUs= */ 200_000, - /* contentResumeOffsetUs= */ 0); + /* contentResumeOffsetUs= */ 0, + /* adDurationsUs...= */ 200_000); adPlaybackState = addAdGroupToAdPlaybackState( adPlaybackState, /* fromPositionUs= */ 400_000, - /* toPositionUs= */ 700_000, - /* contentResumeOffsetUs= */ 1_000_000); + /* contentResumeOffsetUs= */ 1_000_000, + /* adDurationsUs...= */ 300_000); AdPlaybackState firstAdPlaybackState = addAdGroupToAdPlaybackState( adPlaybackState, /* fromPositionUs= */ 900_000, - /* toPositionUs= */ 1_000_000, - /* contentResumeOffsetUs= */ 0); + /* contentResumeOffsetUs= */ 0, + /* adDurationsUs...= */ 100_000); AtomicReference mediaSourceRef = new AtomicReference<>(); mediaSourceRef.set( @@ -252,8 +252,8 @@ public final class ServerSideAdInsertionMediaSourceTest { addAdGroupToAdPlaybackState( new AdPlaybackState(/* adsId= */ new Object()), /* fromPositionUs= */ 900_000, - /* toPositionUs= */ 1_000_000, - /* contentResumeOffsetUs= */ 0); + /* contentResumeOffsetUs= */ 0, + /* adDurationsUs...= */ 100_000); AtomicReference mediaSourceRef = new AtomicReference<>(); mediaSourceRef.set( new ServerSideAdInsertionMediaSource( @@ -280,8 +280,8 @@ public final class ServerSideAdInsertionMediaSourceTest { addAdGroupToAdPlaybackState( firstAdPlaybackState, /* fromPositionUs= */ 0, - /* toPositionUs= */ 500_000, - /* contentResumeOffsetUs= */ 0); + /* contentResumeOffsetUs= */ 0, + /* adDurationsUs...= */ 500_000); mediaSourceRef .get() .setAdPlaybackStates(ImmutableMap.of(periodUid.get(), secondAdPlaybackState)); @@ -323,8 +323,8 @@ public final class ServerSideAdInsertionMediaSourceTest { addAdGroupToAdPlaybackState( new AdPlaybackState(/* adsId= */ new Object()), /* fromPositionUs= */ 0, - /* toPositionUs= */ 500_000, - /* contentResumeOffsetUs= */ 0); + /* contentResumeOffsetUs= */ 0, + /* adDurationsUs...= */ 500_000); AtomicReference mediaSourceRef = new AtomicReference<>(); mediaSourceRef.set( new ServerSideAdInsertionMediaSource( @@ -391,20 +391,20 @@ public final class ServerSideAdInsertionMediaSourceTest { addAdGroupToAdPlaybackState( adPlaybackState, /* fromPositionUs= */ 0, - /* toPositionUs= */ 100_000, - /* contentResumeOffsetUs= */ 0); + /* contentResumeOffsetUs= */ 0, + /* adDurationsUs...= */ 100_000); adPlaybackState = addAdGroupToAdPlaybackState( adPlaybackState, /* fromPositionUs= */ 600_000, - /* toPositionUs= */ 700_000, - /* contentResumeOffsetUs= */ 1_000_000); + /* contentResumeOffsetUs= */ 1_000_000, + /* adDurationsUs...= */ 100_000); AdPlaybackState firstAdPlaybackState = addAdGroupToAdPlaybackState( adPlaybackState, /* fromPositionUs= */ 900_000, - /* toPositionUs= */ 1_000_000, - /* contentResumeOffsetUs= */ 0); + /* contentResumeOffsetUs= */ 0, + /* adDurationsUs...= */ 100_000); AtomicReference mediaSourceRef = new AtomicReference<>(); mediaSourceRef.set( @@ -427,7 +427,7 @@ public final class ServerSideAdInsertionMediaSourceTest { player.setMediaSource(mediaSourceRef.get()); player.prepare(); // 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); runUntilPendingCommandsAreFullyHandled(player); long positionAfterSeekMs = player.getCurrentPosition(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionUtilTest.java index b9bfaae7b9..c3a3c38d74 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionUtilTest.java @@ -22,6 +22,7 @@ import static com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil import static com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil.getStreamPositionUsForAd; import static com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil.getStreamPositionUsForContent; import static com.google.common.truth.Truth.assertThat; +import static java.util.Arrays.stream; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; @@ -46,8 +47,8 @@ public final class ServerSideAdInsertionUtilTest { addAdGroupToAdPlaybackState( state, /* fromPositionUs= */ 4300, - /* toPositionUs= */ 4500, - /* contentResumeOffsetUs= */ 400); + /* contentResumeOffsetUs= */ 400, + /* adDurationsUs...= */ 200); assertThat(state) .isEqualTo( @@ -64,8 +65,8 @@ public final class ServerSideAdInsertionUtilTest { addAdGroupToAdPlaybackState( state, /* fromPositionUs= */ 2100, - /* toPositionUs= */ 2400, - /* contentResumeOffsetUs= */ 0); + /* contentResumeOffsetUs= */ 0, + /* adDurationsUs...= */ 300); assertThat(state) .isEqualTo( @@ -86,8 +87,8 @@ public final class ServerSideAdInsertionUtilTest { addAdGroupToAdPlaybackState( state, /* fromPositionUs= */ 0, - /* toPositionUs= */ 100, - /* contentResumeOffsetUs= */ 50); + /* contentResumeOffsetUs= */ 50, + /* adDurationsUs...= */ 100); assertThat(state) .isEqualTo( @@ -112,8 +113,8 @@ public final class ServerSideAdInsertionUtilTest { addAdGroupToAdPlaybackState( state, /* fromPositionUs= */ 5000, - /* toPositionUs= */ 6000, - /* contentResumeOffsetUs= */ 0); + /* contentResumeOffsetUs= */ 0, + /* adDurationsUs...= */ 1000); assertThat(state) .isEqualTo( @@ -143,6 +144,33 @@ public final class ServerSideAdInsertionUtilTest { .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 public void getStreamPositionUsForAd_returnsCorrectPositions() { // stream: 0-- ad1 --200-- content --2100-- ad2 --2300-- content --4300-- ad3 --4500-- content