From a1424c834fd7a4f9fee39053a26a204eb0e8a09d 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 --- .../androidx/media3/common/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/libraries/common/src/main/java/androidx/media3/common/util/Util.java b/libraries/common/src/main/java/androidx/media3/common/util/Util.java index 69c78eeb6d..84a9a570be 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/Util.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/Util.java @@ -2539,6 +2539,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/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionUtil.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionUtil.java index 052181ec33..53e3f814ba 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionUtil.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionUtil.java @@ -15,6 +15,7 @@ */ package androidx.media3.exoplayer.source.ads; +import static androidx.media3.common.util.Util.sum; import static java.lang.Math.max; import androidx.annotation.CheckResult; @@ -36,23 +37,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); @@ -62,16 +65,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/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSourceTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSourceTest.java index 4db0210883..ef8d30a56c 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSourceTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSourceTest.java @@ -183,20 +183,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( @@ -253,8 +253,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( @@ -281,8 +281,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)); @@ -324,8 +324,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( @@ -392,20 +392,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( @@ -428,7 +428,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/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionUtilTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionUtilTest.java index 2453caa5f9..8ed0eb90fa 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionUtilTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionUtilTest.java @@ -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.getStreamPositionUsForContent; import static com.google.common.truth.Truth.assertThat; +import static java.util.Arrays.stream; import androidx.media3.common.AdPlaybackState; import androidx.media3.common.C; @@ -47,8 +48,8 @@ public final class ServerSideAdInsertionUtilTest { addAdGroupToAdPlaybackState( state, /* fromPositionUs= */ 4300, - /* toPositionUs= */ 4500, - /* contentResumeOffsetUs= */ 400); + /* contentResumeOffsetUs= */ 400, + /* adDurationsUs...= */ 200); assertThat(state) .isEqualTo( @@ -65,8 +66,8 @@ public final class ServerSideAdInsertionUtilTest { addAdGroupToAdPlaybackState( state, /* fromPositionUs= */ 2100, - /* toPositionUs= */ 2400, - /* contentResumeOffsetUs= */ 0); + /* contentResumeOffsetUs= */ 0, + /* adDurationsUs...= */ 300); assertThat(state) .isEqualTo( @@ -87,8 +88,8 @@ public final class ServerSideAdInsertionUtilTest { addAdGroupToAdPlaybackState( state, /* fromPositionUs= */ 0, - /* toPositionUs= */ 100, - /* contentResumeOffsetUs= */ 50); + /* contentResumeOffsetUs= */ 50, + /* adDurationsUs...= */ 100); assertThat(state) .isEqualTo( @@ -113,8 +114,8 @@ public final class ServerSideAdInsertionUtilTest { addAdGroupToAdPlaybackState( state, /* fromPositionUs= */ 5000, - /* toPositionUs= */ 6000, - /* contentResumeOffsetUs= */ 0); + /* contentResumeOffsetUs= */ 0, + /* adDurationsUs...= */ 1000); assertThat(state) .isEqualTo( @@ -144,6 +145,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