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