diff --git a/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java b/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java index 58c1ff1862..6e784fbc26 100644 --- a/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java +++ b/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java @@ -35,6 +35,7 @@ import androidx.annotation.VisibleForTesting; import androidx.media3.common.util.NullableType; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; +import com.google.errorprone.annotations.InlineMe; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -71,7 +72,7 @@ public final class AdPlaybackState { /** * The original number of ads in the ad group in case the ad group is only partially available, - * or {@link C#LENGTH_UNSET} if unknown. An ad can be partially available when a server side + * or {@link C#LENGTH_UNSET} if unknown. An ad can be partially available when a server-side * inserted ad live stream is joined while an ad is already playing and some ad information is * missing. */ @@ -103,6 +104,9 @@ public final class AdPlaybackState { /** Whether this ad group is server-side inserted and part of the content stream. */ public final boolean isServerSideInserted; + /** Whether this is an ignorable placeholder that must not be attempted to be played. */ + public final boolean isPlaceholder; + /** * Creates a new ad group with an unspecified number of ads. * @@ -119,7 +123,8 @@ public final class AdPlaybackState { /* durationsUs= */ new long[0], /* contentResumeOffsetUs= */ 0, /* isServerSideInserted= */ false, - /* ids= */ new String[0]); + /* ids= */ new String[0], + /* isPlaceholder= */ false); } @SuppressWarnings("deprecation") // Intentionally assigning deprecated field @@ -132,7 +137,8 @@ public final class AdPlaybackState { long[] durationsUs, long contentResumeOffsetUs, boolean isServerSideInserted, - @NullableType String[] ids) { + @NullableType String[] ids, + boolean isPlaceholder) { checkArgument(states.length == mediaItems.length); this.timeUs = timeUs; this.count = count; @@ -147,6 +153,7 @@ public final class AdPlaybackState { uris[i] = mediaItems[i] == null ? null : checkNotNull(mediaItems[i].localConfiguration).uri; } this.ids = ids; + this.isPlaceholder = isPlaceholder; } /** @@ -162,7 +169,7 @@ public final class AdPlaybackState { * lastPlayedAdIndex}, or {@link #count} if no later ads should be played. If no ads have been * played, pass -1 to get the index of the first ad to play. * - *

Note: {@linkplain #isServerSideInserted Server side inserted ads} are always considered + *

Note: {@linkplain #isServerSideInserted server-side inserted ads} are always considered * playable. */ public int getNextAdIndexToPlay(@IntRange(from = -1) int lastPlayedAdIndex) { @@ -198,8 +205,24 @@ public final class AdPlaybackState { return false; } - private boolean isLivePostrollPlaceholder() { - return isServerSideInserted && timeUs == C.TIME_END_OF_SOURCE && count == C.LENGTH_UNSET; + /** + * Returns whether this is a is a placeholder ad group. + * + * @param isServerSideInserted Whether the postroll placeholder must be server-side inserted. + * @return true only if this ad group has a matching {@link #isServerSideInserted} flag. + */ + public boolean isLivePostrollPlaceholder(boolean isServerSideInserted) { + return (this.isServerSideInserted == isServerSideInserted) && isLivePostrollPlaceholder(); + } + + /** + * Returns whether this is a placeholder ad group. It can be server-side inserted or not. Use + * {@link #isLivePostrollPlaceholder(boolean)} if you want to differentiate. + * + * @return true only if this is a live postroll placeholder. + */ + public boolean isLivePostrollPlaceholder() { + return isPlaceholder && timeUs == C.TIME_END_OF_SOURCE && count == C.LENGTH_UNSET; } @Override @@ -219,7 +242,8 @@ public final class AdPlaybackState { && Arrays.equals(durationsUs, adGroup.durationsUs) && contentResumeOffsetUs == adGroup.contentResumeOffsetUs && isServerSideInserted == adGroup.isServerSideInserted - && Arrays.equals(ids, adGroup.ids); + && Arrays.equals(ids, adGroup.ids) + && isPlaceholder == adGroup.isPlaceholder; } @Override @@ -233,6 +257,7 @@ public final class AdPlaybackState { result = 31 * result + (int) (contentResumeOffsetUs ^ (contentResumeOffsetUs >>> 32)); result = 31 * result + (isServerSideInserted ? 1 : 0); result = 31 * result + Arrays.hashCode(ids); + result = 31 * result + (isPlaceholder ? 1 : 0); return result; } @@ -248,7 +273,8 @@ public final class AdPlaybackState { durationsUs, contentResumeOffsetUs, isServerSideInserted, - ids); + ids, + isPlaceholder); } /** Returns a new instance with the ad count set to {@code count}. */ @@ -267,7 +293,8 @@ public final class AdPlaybackState { durationsUs, contentResumeOffsetUs, isServerSideInserted, - ids); + ids, + isPlaceholder); } /** @@ -305,7 +332,8 @@ public final class AdPlaybackState { durationsUs, contentResumeOffsetUs, isServerSideInserted, - ids); + ids, + isPlaceholder); } /** @@ -346,7 +374,8 @@ public final class AdPlaybackState { durationsUs, contentResumeOffsetUs, isServerSideInserted, - ids); + ids, + isPlaceholder); } /** Returns a new instance with the specified ad durations, in microseconds. */ @@ -366,7 +395,8 @@ public final class AdPlaybackState { durationsUs, contentResumeOffsetUs, isServerSideInserted, - ids); + ids, + isPlaceholder); } /** Returns a new instance with the specified ID for the given ad index. */ @@ -395,7 +425,8 @@ public final class AdPlaybackState { durationsUs, contentResumeOffsetUs, isServerSideInserted, - ids); + ids, + isPlaceholder); } /** Returns an instance with the specified {@link #contentResumeOffsetUs}. */ @@ -410,7 +441,8 @@ public final class AdPlaybackState { durationsUs, contentResumeOffsetUs, isServerSideInserted, - ids); + ids, + isPlaceholder); } /** Returns an instance with the specified value for {@link #isServerSideInserted}. */ @@ -425,7 +457,8 @@ public final class AdPlaybackState { durationsUs, contentResumeOffsetUs, isServerSideInserted, - ids); + ids, + isPlaceholder); } /** Returns an instance with the specified value for {@link #originalCount}. */ @@ -439,7 +472,8 @@ public final class AdPlaybackState { durationsUs, contentResumeOffsetUs, isServerSideInserted, - ids); + ids, + isPlaceholder); } /** Removes the last ad from the ad group. */ @@ -461,7 +495,8 @@ public final class AdPlaybackState { newDurationsUs, /* contentResumeOffsetUs= */ Util.sum(newDurationsUs), isServerSideInserted, - newIds); + newIds, + isPlaceholder); } /** @@ -480,7 +515,8 @@ public final class AdPlaybackState { /* durationsUs= */ new long[0], contentResumeOffsetUs, isServerSideInserted, - ids); + ids, + isPlaceholder); } int count = this.states.length; @AdState int[] states = Arrays.copyOf(this.states, count); @@ -498,7 +534,8 @@ public final class AdPlaybackState { durationsUs, contentResumeOffsetUs, isServerSideInserted, - ids); + ids, + isPlaceholder); } /** @@ -528,7 +565,22 @@ public final class AdPlaybackState { durationsUs, contentResumeOffsetUs, isServerSideInserted, - ids); + ids, + isPlaceholder); + } + + private AdGroup withIsPlaceholder(boolean isPlaceholder, boolean isServerSideInserted) { + return new AdGroup( + timeUs, + count, + originalCount, + states, + mediaItems, + durationsUs, + contentResumeOffsetUs, + isServerSideInserted, + ids, + isPlaceholder); } /** @@ -572,6 +624,7 @@ public final class AdPlaybackState { private static final String FIELD_ORIGINAL_COUNT = Util.intToStringMaxRadix(7); @VisibleForTesting static final String FIELD_MEDIA_ITEMS = Util.intToStringMaxRadix(8); static final String FIELD_IDS = Util.intToStringMaxRadix(9); + static final String FIELD_IS_PLACEHOLDER = Util.intToStringMaxRadix(10); // Intentionally assigning deprecated field. // putParcelableArrayList actually supports null elements. @@ -589,6 +642,7 @@ public final class AdPlaybackState { bundle.putLong(FIELD_CONTENT_RESUME_OFFSET_US, contentResumeOffsetUs); bundle.putBoolean(FIELD_IS_SERVER_SIDE_INSERTED, isServerSideInserted); bundle.putStringArrayList(FIELD_IDS, new ArrayList<>(Arrays.asList(ids))); + bundle.putBoolean(FIELD_IS_PLACEHOLDER, isPlaceholder); return bundle; } @@ -610,6 +664,7 @@ public final class AdPlaybackState { long contentResumeOffsetUs = bundle.getLong(FIELD_CONTENT_RESUME_OFFSET_US); boolean isServerSideInserted = bundle.getBoolean(FIELD_IS_SERVER_SIDE_INSERTED); @Nullable ArrayList ids = bundle.getStringArrayList(FIELD_IDS); + boolean isPlaceholder = bundle.getBoolean(FIELD_IS_PLACEHOLDER); return new AdGroup( timeUs, count, @@ -619,7 +674,8 @@ public final class AdPlaybackState { durationsUs == null ? new long[0] : durationsUs, contentResumeOffsetUs, isServerSideInserted, - ids == null ? new String[0] : ids.toArray(new String[0])); + ids == null ? new String[0] : ids.toArray(new String[0]), + isPlaceholder); } private ArrayList<@NullableType Bundle> getMediaItemsArrayBundles() { @@ -929,11 +985,11 @@ public final class AdPlaybackState { /** * Returns an instance with the specified ad marked as {@linkplain #AD_STATE_AVAILABLE available}. * - *

Must not be called with client side inserted ad groups. Client side inserted ads should use + *

Must not be called with client-side inserted ad groups. Client-side inserted ads should use * {@link #withAvailableAdMediaItem}. * * @throws IllegalStateException in case this methods is called on an ad group that {@linkplain - * AdGroup#isServerSideInserted is not server side inserted}. + * AdGroup#isServerSideInserted is not server-side inserted}. */ @CheckResult public AdPlaybackState withAvailableAd( @@ -1160,24 +1216,55 @@ public final class AdPlaybackState { } /** - * Appends a live post roll placeholder ad group to the ad playback state. + * @deprecated Use {@link #withLivePostrollPlaceholderAppended(boolean)} and pass {@code true} + * instead. + */ + @InlineMe(replacement = "this.withLivePostrollPlaceholderAppended(true)") + @Deprecated + public AdPlaybackState withLivePostrollPlaceholderAppended() { + return withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); + } + + /** + * Appends a live postroll placeholder ad group to the ad playback state. * - *

Adding such a placeholder is only required for periods of server side ad insertion live - * streams. A player is not expected to play this placeholder. It is only used to indicate that - * another ad group with this ad group index will be inserted in the future. + *

Adding such a placeholder is only required for periods of live streams. A player is not + * expected to play this placeholder. It is only used to indicate that another ad group with this + * ad group index will be inserted in the future. * - *

See {@link #endsWithLivePostrollPlaceHolder()} also. + *

See {@link #endsWithLivePostrollPlaceHolder()} and {@link + * #endsWithLivePostrollPlaceHolder(boolean)} also. * + * @param isServerSideInserted Whether this is a server-side inserted ad (single stream). * @return The new ad playback state instance ending with a live postroll placeholder. */ - public AdPlaybackState withLivePostrollPlaceholderAppended() { + public AdPlaybackState withLivePostrollPlaceholderAppended(boolean isServerSideInserted) { return withNewAdGroup(adGroupCount, /* adGroupTimeUs= */ C.TIME_END_OF_SOURCE) - .withIsServerSideInserted(adGroupCount, true); + .withIsPlaceholder(adGroupCount, /* isPlaceholder= */ true, isServerSideInserted); + } + + @VisibleForTesting + /* package */ AdPlaybackState withIsPlaceholder( + int adGroupIndex, boolean isPlaceholder, boolean isServerSideInserted) { + int adjustedIndex = adGroupIndex - removedAdGroupCount; + if (adGroups[adjustedIndex].isPlaceholder == isPlaceholder + && adGroups[adjustedIndex].isServerSideInserted == isServerSideInserted) { + return this; + } + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); + adGroups[adjustedIndex] = + adGroups[adjustedIndex].withIsPlaceholder(isPlaceholder, isServerSideInserted); + return new AdPlaybackState( + adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount); } /** * Returns whether the last ad group is a live postroll placeholder as inserted by {@link - * #withLivePostrollPlaceholderAppended()}. + * #withLivePostrollPlaceholderAppended(boolean)}. + * + *

Note: That either server-side or client-side inserted placeholders are considered. Use + * {@link #endsWithLivePostrollPlaceHolder(boolean)} if you want to test for one or the other + * only. * * @return Whether the ad playback state ends with a live postroll placeholder. */ @@ -1187,7 +1274,22 @@ public final class AdPlaybackState { } /** - * Whether the {@link AdGroup} at the given ad group index is a live postroll placeholder. + * Returns whether the last ad group is a live postroll placeholder as inserted by {@link + * #withLivePostrollPlaceholderAppended(boolean)} . + * + * @param isServerSideInserted Whether the trailing placeholder is server-side inserted. + * @return Whether the ad playback state ends with a live postroll placeholder. + */ + public boolean endsWithLivePostrollPlaceHolder(boolean isServerSideInserted) { + int adGroupIndex = adGroupCount - 1; + return adGroupIndex >= 0 && isLivePostrollPlaceholder(adGroupIndex, isServerSideInserted); + } + + /** + * Returns whether the {@link AdGroup} at the given ad group index is a live postroll placeholder. + * + *

Note: That either server-side or client-side inserted placeholders return true. Use {@link + * #isLivePostrollPlaceholder(int, boolean)} if you want to test for one or the other only. * * @param adGroupIndex The ad group index. * @return True if the ad group at the given index is a live postroll placeholder, false if not. @@ -1196,6 +1298,19 @@ public final class AdPlaybackState { return adGroupIndex == adGroupCount - 1 && getAdGroup(adGroupIndex).isLivePostrollPlaceholder(); } + /** + * Returns whether the {@link AdGroup} at the given ad group index is a live postroll placeholder + * and either server or client-side inserted. + * + * @param adGroupIndex The ad group index. + * @param isServerSideInserted Whether the placeholder is server-side inserted. + * @return True if the ad group at the given index is a live postroll placeholder, false if not. + */ + public boolean isLivePostrollPlaceholder(int adGroupIndex, boolean isServerSideInserted) { + return adGroupIndex == adGroupCount - 1 + && getAdGroup(adGroupIndex).isLivePostrollPlaceholder(isServerSideInserted); + } + /** * Returns the index of the ad with the given ad ID in the given ad group, or {@link * C#INDEX_UNSET} if the ad ID can't be found. @@ -1230,7 +1345,8 @@ public final class AdPlaybackState { Arrays.copyOf(adGroup.durationsUs, adGroup.durationsUs.length), adGroup.contentResumeOffsetUs, adGroup.isServerSideInserted, - adGroup.ids); + adGroup.ids, + adGroup.isPlaceholder); } return new AdPlaybackState( adsId, @@ -1332,7 +1448,7 @@ public final class AdPlaybackState { // placeholder in a period of a multi-period live window, or when c) the position actually is // before the given period duration. return periodDurationUs == C.TIME_UNSET - || (adGroup.isServerSideInserted && adGroup.count == C.LENGTH_UNSET) + || adGroup.isLivePostrollPlaceholder() || positionUs < periodDurationUs; } return positionUs < adGroupPositionUs; diff --git a/libraries/common/src/test/java/androidx/media3/common/AdPlaybackStateTest.java b/libraries/common/src/test/java/androidx/media3/common/AdPlaybackStateTest.java index a1a0a8cbf6..d98f6eca70 100644 --- a/libraries/common/src/test/java/androidx/media3/common/AdPlaybackStateTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/AdPlaybackStateTest.java @@ -568,18 +568,66 @@ public class AdPlaybackStateTest { assertThat(AdPlaybackState.AdGroup.fromBundle(bundle)).isEqualTo(adGroup); } + @SuppressWarnings({"deprecation", "InlineMeInliner"}) // testing deprecated API @Test public void withLivePostrollPlaceholderAppended_emptyAdPlaybackState_insertsPlaceholder() { - AdPlaybackState adPlaybackState = - new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended(); + AdPlaybackState emptyAdPlaybackState = new AdPlaybackState("adsId"); - assertThat(adPlaybackState.adGroupCount).isEqualTo(1); - assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).timeUs) - .isEqualTo(C.TIME_END_OF_SOURCE); - assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).count).isEqualTo(C.LENGTH_UNSET); - assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).isServerSideInserted).isTrue(); + assertThat( + emptyAdPlaybackState.withLivePostrollPlaceholderAppended( + /* isServerSideInserted= */ true)) + .isEqualTo( + new AdPlaybackState("adsId", C.TIME_END_OF_SOURCE) + .withIsPlaceholder( + /* adGroupIndex= */ 0, + /* isPlaceholder= */ true, + /* isServerSideInserted= */ true)); + assertThat(emptyAdPlaybackState.withLivePostrollPlaceholderAppended()) + .isEqualTo( + new AdPlaybackState("adsId", C.TIME_END_OF_SOURCE) + .withIsPlaceholder( + /* adGroupIndex= */ 0, + /* isPlaceholder= */ true, + /* isServerSideInserted= */ true)); + assertThat( + emptyAdPlaybackState.withLivePostrollPlaceholderAppended( + /* isServerSideInserted= */ false)) + .isEqualTo( + new AdPlaybackState("adsId", C.TIME_END_OF_SOURCE) + .withIsPlaceholder( + /* adGroupIndex= */ 0, + /* isPlaceholder= */ true, + /* isServerSideInserted= */ false)); } + @SuppressWarnings({"deprecation", "InlineMeInliner"}) // testing deprecated API + @Test + public void endsWithLivePostrollPlaceHolder_emptyAdPlaybackState_insertsPlaceholder() { + AdPlaybackState emptyAdPlaybackState = new AdPlaybackState("adsId"); + + assertThat( + emptyAdPlaybackState + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true) + .endsWithLivePostrollPlaceHolder()) + .isTrue(); + assertThat( + emptyAdPlaybackState + .withLivePostrollPlaceholderAppended() + .endsWithLivePostrollPlaceHolder(/* isServerSideInserted= */ true)) + .isTrue(); + assertThat( + emptyAdPlaybackState + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false) + .endsWithLivePostrollPlaceHolder()) + .isTrue(); + assertThat( + emptyAdPlaybackState + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false) + .endsWithLivePostrollPlaceHolder(/* isServerSideInserted= */ false)) + .isTrue(); + } + + @SuppressWarnings({"deprecation", "InlineMeInliner"}) // testing deprecated API @Test public void withLivePostrollPlaceholderAppended_withExistingAdGroups_appendsPlaceholder() { AdPlaybackState adPlaybackState = @@ -591,13 +639,32 @@ public class AdPlaybackStateTest { .withAdDurationsUs(/* adGroupIndex= */ 0, /* adDurationsUs...= */ 10_000_000L) .withAdDurationsUs(/* adGroupIndex= */ 1, /* adDurationsUs...= */ 5_000_000L); - adPlaybackState = adPlaybackState.withLivePostrollPlaceholderAppended(); - - assertThat(adPlaybackState.adGroupCount).isEqualTo(3); - assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 2).timeUs) - .isEqualTo(C.TIME_END_OF_SOURCE); - assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 2).count).isEqualTo(C.LENGTH_UNSET); - assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 2).isServerSideInserted).isTrue(); + assertThat( + adPlaybackState.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true)) + .isEqualTo( + adPlaybackState + .withNewAdGroup(/* adGroupIndex= */ 2, C.TIME_END_OF_SOURCE) + .withIsPlaceholder( + /* adGroupIndex= */ 2, + /* isPlaceholder= */ true, + /* isServerSideInserted= */ true)); + assertThat(adPlaybackState.withLivePostrollPlaceholderAppended()) + .isEqualTo( + adPlaybackState + .withNewAdGroup(/* adGroupIndex= */ 2, C.TIME_END_OF_SOURCE) + .withIsPlaceholder( + /* adGroupIndex= */ 2, + /* isPlaceholder= */ true, + /* isServerSideInserted= */ true)); + assertThat( + adPlaybackState.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false)) + .isEqualTo( + adPlaybackState + .withNewAdGroup(/* adGroupIndex= */ 2, C.TIME_END_OF_SOURCE) + .withIsPlaceholder( + /* adGroupIndex= */ 2, + /* isPlaceholder= */ true, + /* isServerSideInserted= */ false)); } @Test @@ -611,14 +678,37 @@ public class AdPlaybackStateTest { .withAdDurationsUs(/* adGroupIndex= */ 0, /* adDurationsUs...= */ 10_000_000L) .withAdDurationsUs(/* adGroupIndex= */ 1, /* adDurationsUs...= */ 5_000_000L); - boolean endsWithLivePostrollPlaceHolder = adPlaybackState.endsWithLivePostrollPlaceHolder(); + assertThat( + adPlaybackState + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true) + .endsWithLivePostrollPlaceHolder()) + .isTrue(); + assertThat( + adPlaybackState + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true) + .endsWithLivePostrollPlaceHolder(/* isServerSideInserted= */ true)) + .isTrue(); + assertThat( + adPlaybackState + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true) + .endsWithLivePostrollPlaceHolder(/* isServerSideInserted= */ false)) + .isFalse(); - assertThat(endsWithLivePostrollPlaceHolder).isFalse(); - - adPlaybackState = adPlaybackState.withLivePostrollPlaceholderAppended(); - endsWithLivePostrollPlaceHolder = adPlaybackState.endsWithLivePostrollPlaceHolder(); - - assertThat(endsWithLivePostrollPlaceHolder).isTrue(); + assertThat( + adPlaybackState + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false) + .endsWithLivePostrollPlaceHolder()) + .isTrue(); + assertThat( + adPlaybackState + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false) + .endsWithLivePostrollPlaceHolder(/* isServerSideInserted= */ false)) + .isTrue(); + assertThat( + adPlaybackState + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false) + .endsWithLivePostrollPlaceHolder(/* isServerSideInserted= */ true)) + .isFalse(); } @Test @@ -783,7 +873,7 @@ public class AdPlaybackStateTest { .withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) - .withLivePostrollPlaceholderAppended(); + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); assertThat( state.getAdGroupIndexAfterPositionUs( @@ -811,13 +901,51 @@ public class AdPlaybackStateTest { public void getAdGroupIndexForPositionUs_withServerSidePostrollPlaceholderForLive_ignoresPlaceholder() { AdPlaybackState state = - new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 0L, 5_000_000L, C.TIME_END_OF_SOURCE) + new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 0L, 5_000_000L) .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) - .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); + .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); + + assertThat( + state.getAdGroupIndexForPositionUs( + /* positionUs= */ 4_999_999L, /* periodDurationUs= */ 10_000_000L)) + .isEqualTo(C.INDEX_UNSET); + assertThat( + state.getAdGroupIndexForPositionUs( + /* positionUs= */ 4_999_999L, /* periodDurationUs= */ C.TIME_UNSET)) + .isEqualTo(C.INDEX_UNSET); + assertThat( + state.getAdGroupIndexForPositionUs( + /* positionUs= */ 5_000_000L, /* periodDurationUs= */ 10_000_000L)) + .isEqualTo(1); + assertThat( + state.getAdGroupIndexForPositionUs( + /* positionUs= */ 5_000_000L, /* periodDurationUs= */ C.TIME_UNSET)) + .isEqualTo(1); + assertThat( + state.getAdGroupIndexForPositionUs( + /* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ 10_000_000L)) + .isEqualTo(1); + assertThat( + state.getAdGroupIndexForPositionUs( + /* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ C.TIME_UNSET)) + .isEqualTo(1); + } + + @Test + public void + getAdGroupIndexForPositionUs_withClientSidePostrollPlaceholderForLive_ignoresPlaceholder() { + AdPlaybackState state = + new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 0L, 5_000_000L) + .withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true) + .withIsServerSideInserted(/* adGroupIndex= */ 1, /* isServerSideInserted= */ true) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1) + .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false); assertThat( state.getAdGroupIndexForPositionUs( @@ -849,8 +977,8 @@ public class AdPlaybackStateTest { public void getAdGroupIndexForPositionUs_withOnlyServerSidePostrollPlaceholderForLive_ignoresPlaceholder() { AdPlaybackState state = - new AdPlaybackState("adsId", /* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE) - .withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true); + new AdPlaybackState("adsId") + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); assertThat( state.getAdGroupIndexForPositionUs( 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 fcb65beaea..6d005f99fc 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 @@ -198,7 +198,8 @@ public final class ServerSideAdInsertionMediaSourceTest { public void onContinueLoadingRequested(MediaPeriod source) {} }; AdPlaybackState adPlaybackState = - new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended(); + new AdPlaybackState("adsId") + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); FakeTimeline wrappedTimeline = new FakeTimeline( new FakeTimeline.TimelineWindowDefinition( diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java index facbc90f27..66c40604bb 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java @@ -861,7 +861,8 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou mainHandler.post( () -> setAdPlaybackState( - new AdPlaybackState(adsId).withLivePostrollPlaceholderAppended())); + new AdPlaybackState(adsId) + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true))); } prepareChildSource(/* id= */ null, serverSideAdInsertionMediaSource); } diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java index 5c4cf7a26a..a9650fc18a 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java @@ -425,7 +425,9 @@ import java.util.Set; long windowStartTimeUs = getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs); totalElapsedContentDurationUs = windowStartTimeUs - window.positionInFirstPeriodUs; - contentOnlyAdPlaybackState = contentOnlyAdPlaybackState.withLivePostrollPlaceholderAppended(); + contentOnlyAdPlaybackState = + contentOnlyAdPlaybackState.withLivePostrollPlaceholderAppended( + /* isServerSideInserted= */ true); } Map adPlaybackStates = new HashMap<>(); for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) { @@ -505,7 +507,8 @@ import java.util.Set; .withIsServerSideInserted(/* adGroupIndex= */ 0, true) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1); if (isLiveStream) { - adPlaybackState = adPlaybackState.withLivePostrollPlaceholderAppended(); + adPlaybackState = + adPlaybackState.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); } long adGroupDurationUs = 0; for (int i = 0; i < adGroup.count; i++) { diff --git a/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaUtilTest.java b/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaUtilTest.java index b119d86998..dce169c8f3 100644 --- a/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaUtilTest.java +++ b/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaUtilTest.java @@ -1086,7 +1086,8 @@ public class ImaUtilTest { long liveWindowDurationUs = 60_000_000L; long nowUs = 110_234_567L; AdPlaybackState adPlaybackState = - new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + new AdPlaybackState(/* adsId= */ "adsId") + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); adPlaybackState = addAdGroupToAdPlaybackState( adPlaybackState, @@ -1134,7 +1135,8 @@ public class ImaUtilTest { long liveWindowDurationUs = 60_000_000L; long nowUs = 110_234_567L; AdPlaybackState adPlaybackState = - new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + new AdPlaybackState(/* adsId= */ "adsId") + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); adPlaybackState = addAdGroupToAdPlaybackState( adPlaybackState, @@ -1170,7 +1172,8 @@ public class ImaUtilTest { public void maybeCorrectPreviouslyUnknownAdDuration_windowPastAdGroups_adPlaybackStateNotChanged() { AdPlaybackState adPlaybackState = - new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + new AdPlaybackState(/* adsId= */ "adsId") + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); adPlaybackState = addAdGroupToAdPlaybackState( adPlaybackState, @@ -1219,7 +1222,8 @@ public class ImaUtilTest { /* populateAds= */ false, /* playedAds= */ false); AdPlaybackState adPlaybackState = - new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + new AdPlaybackState(/* adsId= */ "adsId") + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); // Insert first ad resulting in group [10_000_000, 29_000_123, 0, 0] adPlaybackState = addLiveAdBreak( @@ -1322,7 +1326,8 @@ public class ImaUtilTest { public void maybeCorrectPreviouslyUnknownAdDuration_timelineMovesMultiplePeriodsForward_adDurationCorrected() { AdPlaybackState adPlaybackState = - new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + new AdPlaybackState(/* adsId= */ "adsId") + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); // Timeline window to start with: c, a, a, a, [a, c, a], a, a, a FakeMultiPeriodLiveTimeline contentTimeline = new FakeMultiPeriodLiveTimeline( @@ -1402,7 +1407,8 @@ public class ImaUtilTest { windowStartTimeUs + contentTimeline.getPeriod(/* periodIndex= */ 1, new Period()).positionInWindowUs; AdPlaybackState adPlaybackState = - new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + new AdPlaybackState(/* adsId= */ "adsId") + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); adPlaybackState = addLiveAdBreak( /* currentContentPeriodPositionUs= */ firstAdPeriodStartTimeUs, @@ -1438,7 +1444,8 @@ public class ImaUtilTest { public void maybeCorrectPreviouslyUnknownAdDuration_timelineMovesMultiplePeriodsForwardStartOfAdGroupNotInWindow_adDurationCorrected() { AdPlaybackState adPlaybackState = - new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + new AdPlaybackState(/* adsId= */ "adsId") + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); // Window with content and ad periods: c, a, a, a, a, [c, a, a], a, a, c // Supposed insertion of ad for period with unknown duration. // durationsUs: [10_000_000L, 28_000_000L, 0L, 0L] @@ -1496,7 +1503,8 @@ public class ImaUtilTest { public void maybeCorrectPreviouslyUnknownAdDuration_timelineMovesMultiplePeriodsForwardWithinAdOnlyWindow_adDurationCorrected() { AdPlaybackState adPlaybackState = - new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + new AdPlaybackState(/* adsId= */ "adsId") + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); // Supposed window when inserting ads: c, a, a, [a, a, a], a, a, a, c // durationsUs: [10_000_000L, 10_000_000L, 10_000_000L, 10_000_000L, 123L, 0, 0, 0] adPlaybackState = @@ -1619,7 +1627,8 @@ public class ImaUtilTest { long adPeriodDurationUs = msToUs(AD_PERIOD_DURATION_MS); long periodDurationUs = msToUs(PERIOD_DURATION_MS); AdPlaybackState adPlaybackState = - new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + new AdPlaybackState(/* adsId= */ "adsId") + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); // Window with content and ad periods: c, a, a, a, a, [c, a, a], a, a, c // Supposed insertion of ad for period with unknown duration. PLaying first ad. // durationsUs: [10_000_000L, 28_000_000L, 0L, 0L] @@ -1688,7 +1697,8 @@ public class ImaUtilTest { /* populateAds= */ false, /* playedAds= */ false); AdPlaybackState adPlaybackState = - new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + new AdPlaybackState(/* adsId= */ "adsId") + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); adPlaybackState = addAdGroupToAdPlaybackState( adPlaybackState, @@ -1724,7 +1734,8 @@ public class ImaUtilTest { /* populateAds= */ false, /* playedAds= */ false); AdPlaybackState adPlaybackState = - new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + new AdPlaybackState(/* adsId= */ "adsId") + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); adPlaybackState = addAdGroupToAdPlaybackState( adPlaybackState, @@ -2034,7 +2045,8 @@ public class ImaUtilTest { /* populateAds= */ false, /* playedAds= */ false); AdPlaybackState adPlaybackState = - new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended(); + new AdPlaybackState("adsId") + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); adPlaybackState = addLiveAdBreak( /* currentContentPeriodPositionUs= */ 50_000_000, @@ -2135,7 +2147,8 @@ public class ImaUtilTest { /* populateAds= */ false, /* playedAds= */ false); AdPlaybackState adPlaybackState = - new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended(); + new AdPlaybackState("adsId") + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); adPlaybackState = addLiveAdBreak( /* currentContentPeriodPositionUs= */ 50_000_000, @@ -2222,7 +2235,8 @@ public class ImaUtilTest { /* populateAds= */ false, /* playedAds= */ false); AdPlaybackState adPlaybackState = - new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended(); + new AdPlaybackState("adsId") + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); adPlaybackState = addLiveAdBreak( /* currentContentPeriodPositionUs= */ 30_000_000, @@ -2283,7 +2297,8 @@ public class ImaUtilTest { /* populateAds= */ false, /* playedAds= */ false); AdPlaybackState adPlaybackState = - new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended(); + new AdPlaybackState("adsId") + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); // Ad events of the first two ads of the group have arrived (the first of the window). adPlaybackState = addLiveAdBreak( @@ -2365,7 +2380,8 @@ public class ImaUtilTest { /* populateAds= */ true, /* playedAds= */ false); AdPlaybackState adPlaybackState = - new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended(); + new AdPlaybackState("adsId") + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); // Ad events of the first two ads of the group have arrived (the first of the window). adPlaybackState = addLiveAdBreak( diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMultiPeriodLiveTimeline.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMultiPeriodLiveTimeline.java index 1bfba42203..2acf71000a 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMultiPeriodLiveTimeline.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMultiPeriodLiveTimeline.java @@ -250,7 +250,9 @@ public class FakeMultiPeriodLiveTimeline extends Timeline { boolean isAd = adSequencePattern[lastPeriodIndex % sequencePeriodCount]; AdPlaybackState adPlaybackState = AdPlaybackState.NONE; if (!isContentTimeline) { - adPlaybackState = new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended(); + adPlaybackState = + new AdPlaybackState("adsId") + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true); if (isAd && populateAds) { adPlaybackState = adPlaybackState