diff --git a/libraries/common/src/test/java/androidx/media3/common/TimelineTest.java b/libraries/common/src/test/java/androidx/media3/common/TimelineTest.java index 876e8284bc..bc7686d0ff 100644 --- a/libraries/common/src/test/java/androidx/media3/common/TimelineTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/TimelineTest.java @@ -441,7 +441,8 @@ public class TimelineTest { /* nowUs= */ 60_000_000, /* adSequencePattern= */ new boolean[] {false, true, true}, /* isContentTimeline= */ false, - /* populateAds= */ true); + /* populateAds= */ true, + /* playedAds= */ false); assertThat(timeline.getPeriodCount()).isEqualTo(4); assertThat( diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java index 4affc2fd28..fbbf84b42c 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java @@ -474,13 +474,10 @@ import com.google.common.collect.ImmutableList; Timeline.Period period) { timeline.getPeriodByUid(periodUid, period); timeline.getWindow(period.windowIndex, window); - int periodIndex = timeline.getIndexOfPeriod(periodUid); // Skip ignorable server side inserted ad periods. - while ((period.durationUs == 0 - && period.getAdGroupCount() > 0 - && period.isServerSideInsertedAdGroup(period.getRemovedAdGroupCount()) - && period.getAdGroupIndexForPositionUs(0) == C.INDEX_UNSET) - && periodIndex++ < window.lastPeriodIndex) { + for (int periodIndex = timeline.getIndexOfPeriod(periodUid); + isSkippableAdPeriod(period) && periodIndex <= window.lastPeriodIndex; + periodIndex++) { timeline.getPeriod(periodIndex, period, /* setIds= */ true); periodUid = checkNotNull(period.uid); } @@ -495,6 +492,26 @@ import com.google.common.collect.ImmutableList; } } + private static boolean isSkippableAdPeriod(Timeline.Period period) { + int adGroupCount = period.getAdGroupCount(); + if (adGroupCount == 0 + || (adGroupCount == 1 && period.isLivePostrollPlaceholder(/* adGroupIndex= */ 0)) + || !period.isServerSideInsertedAdGroup(period.getRemovedAdGroupCount()) + || period.getAdGroupIndexForPositionUs(0L) != C.INDEX_UNSET) { + return false; + } + if (period.durationUs == 0) { + return true; + } + long contentResumeOffsetUs = 0; + int lastIndexInclusive = + adGroupCount - (period.isLivePostrollPlaceholder(adGroupCount - 1) ? 2 : 1); + for (int i = 0; i <= lastIndexInclusive; i++) { + contentResumeOffsetUs += period.getContentResumeOffsetUs(/* adGroupIndex= */ i); + } + return period.durationUs <= contentResumeOffsetUs; + } + /** * Resolves the specified timeline period and position to a {@link MediaPeriodId} that should be * played after a period position change, returning an identifier for an ad group if one needs to diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java index 218460b894..d9e9720116 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java @@ -438,7 +438,8 @@ public final class MediaPeriodQueueTest { /* nowUs= */ 110_000_000, new boolean[] {false, true, true}, /* isContentTimeline= */ true, - /* populateAds= */ false); + /* populateAds= */ false, + /* playedAds= */ false); setupTimeline(multiPeriodLiveTimeline); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* periodUid= */ firstPeriodUid, @@ -504,7 +505,8 @@ public final class MediaPeriodQueueTest { /* nowUs= */ 110_000_000, new boolean[] {false, true, true}, /* isContentTimeline= */ false, - /* populateAds= */ false); + /* populateAds= */ false, + /* playedAds= */ false); setupTimeline(multiPeriodLiveTimeline); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* periodUid= */ firstPeriodUid, @@ -569,7 +571,8 @@ public final class MediaPeriodQueueTest { /* nowUs= */ 110_000_000, new boolean[] {false, true, true}, /* isContentTimeline= */ false, - /* populateAds= */ true); + /* populateAds= */ true, + /* playedAds= */ false); setupTimeline(multiPeriodLiveTimeline); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* periodUid= */ firstPeriodUid, @@ -638,6 +641,47 @@ public final class MediaPeriodQueueTest { assertThat(getNextMediaPeriodInfo()).isNull(); } + @Test + @SuppressWarnings("unchecked") + public void getNextMediaPeriodInfo_multiPeriodTimelineWithPlayedAdsAndWithPostRollPlaceHolder() { + long contentPeriodDurationUs = FakeMultiPeriodLiveTimeline.PERIOD_DURATION_US; + FakeMultiPeriodLiveTimeline multiPeriodLiveTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0, + /* liveWindowDurationUs= */ 60_000_000, + /* nowUs= */ 110_000_000, + new boolean[] {false, true, true}, + /* isContentTimeline= */ false, + /* populateAds= */ true, + /* playedAds= */ true); + setupTimeline(multiPeriodLiveTimeline); + assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + /* periodUid= */ firstPeriodUid, + /* startPositionUs= */ 0, + /* requestedContentPositionUs= */ C.TIME_UNSET, + /* endPositionUs= */ C.TIME_UNSET, + /* durationUs= */ contentPeriodDurationUs, + /* isFollowedByTransitionToSameStream= */ false, + /* isLastInPeriod= */ false, + /* isLastInWindow= */ false, + /* isFinal= */ false, + /* nextAdGroupIndex= */ 0); + advance(); + assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + new Pair(((Pair) firstPeriodUid).first, "uid-6[c]"), + /* startPositionUs= */ 0, + /* requestedContentPositionUs= */ 0, + /* endPositionUs= */ C.TIME_UNSET, + /* durationUs= */ C.TIME_UNSET, // Last period in stream. + /* isFollowedByTransitionToSameStream= */ false, + /* isLastInPeriod= */ false, + /* isLastInWindow= */ false, + /* isFinal= */ false, + /* nextAdGroupIndex= */ 0); + advance(); + assertThat(getNextMediaPeriodInfo()).isNull(); + } + @Test public void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaPeriodInfo() { setupAdTimeline(/* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE); 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 9e726f4146..ad7620a3fa 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 @@ -566,7 +566,8 @@ public class ImaUtilTest { /* nowUs= */ 150_000_000, /* adSequencePattern= */ new boolean[] {false, true, true}, /* isContentTimeline= */ true, - /* populateAds= */ false); + /* populateAds= */ false, + /* playedAds= */ false); // Ad event received from SDK around 130s. adPlaybackState = addLiveAdBreak( @@ -855,7 +856,8 @@ public class ImaUtilTest { /* nowUs= */ 59_999_999, /* adSequencePattern= */ new boolean[] {false, true, true}, /* isContentTimeline= */ true, - /* populateAds= */ false); + /* populateAds= */ false, + /* playedAds= */ false); // Ad event received from SDK around 30s. adPlaybackState = addLiveAdBreak( @@ -1296,7 +1298,8 @@ public class ImaUtilTest { /* nowUs= */ 150_000_000, /* adSequencePattern= */ new boolean[] {false, true, true}, /* isContentTimeline= */ true, - /* populateAds= */ false); + /* populateAds= */ false, + /* playedAds= */ false); AdPlaybackState adPlaybackState = new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended(); adPlaybackState = 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 a00b8679a7..7eb0d9fd61 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 @@ -57,6 +57,7 @@ public class FakeMultiPeriodLiveTimeline extends Timeline { private final long liveWindowDurationUs; private final boolean isContentTimeline; private final boolean populateAds; + private final boolean playedAds; private long nowUs; private ImmutableList periods; @@ -72,7 +73,7 @@ public class FakeMultiPeriodLiveTimeline extends Timeline { * @param isContentTimeline Whether the timeline is a content timeline without {@link * AdPlaybackState}s. * @param populateAds Whether to populate ads like after the ad event has been received. This - * parameter is ignored if the timeline is a content timeline. + * @param playedAds Whether ads should be marked as played if populated. */ public FakeMultiPeriodLiveTimeline( long availabilityStartTimeUs, @@ -80,7 +81,8 @@ public class FakeMultiPeriodLiveTimeline extends Timeline { long nowUs, boolean[] adSequencePattern, boolean isContentTimeline, - boolean populateAds) { + boolean populateAds, + boolean playedAds) { checkArgument(nowUs - liveWindowDurationUs >= availabilityStartTimeUs); this.availabilityStartTimeUs = availabilityStartTimeUs; this.liveWindowDurationUs = liveWindowDurationUs; @@ -88,6 +90,7 @@ public class FakeMultiPeriodLiveTimeline extends Timeline { this.adSequencePattern = Arrays.copyOf(adSequencePattern, adSequencePattern.length); this.isContentTimeline = isContentTimeline; this.populateAds = populateAds; + this.playedAds = playedAds; mediaItem = new MediaItem.Builder().build(); periods = invalidate( @@ -96,7 +99,8 @@ public class FakeMultiPeriodLiveTimeline extends Timeline { nowUs, adSequencePattern, isContentTimeline, - populateAds); + populateAds, + playedAds); } /** Calculates the total duration of the given ad period sequence. */ @@ -118,7 +122,8 @@ public class FakeMultiPeriodLiveTimeline extends Timeline { nowUs, adSequencePattern, isContentTimeline, - populateAds); + populateAds, + playedAds); } @Override @@ -188,7 +193,8 @@ public class FakeMultiPeriodLiveTimeline extends Timeline { long now, boolean[] adSequencePattern, boolean isContentTimeline, - boolean populateAds) { + boolean populateAds, + boolean playedAds) { long windowStartTimeUs = now - liveWindowDurationUs; int sequencePeriodCount = adSequencePattern.length; long sequenceDurationUs = calculateAdSequencePatternDurationUs(adSequencePattern); @@ -226,6 +232,10 @@ public class FakeMultiPeriodLiveTimeline extends Timeline { /* adGroupIndex= */ 0, /* adDurationsUs...= */ periodDurationUs) .withContentResumeOffsetUs( /* adGroupIndex= */ 0, /* contentResumeOffsetUs= */ periodDurationUs); + if (playedAds) { + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); + } } } liveWindow.add( diff --git a/libraries/test_utils/src/test/java/androidx/media3/test/utils/FakeMultiPeriodLiveTimelineTest.java b/libraries/test_utils/src/test/java/androidx/media3/test/utils/FakeMultiPeriodLiveTimelineTest.java index a76b7a9ff3..05cc89787a 100644 --- a/libraries/test_utils/src/test/java/androidx/media3/test/utils/FakeMultiPeriodLiveTimelineTest.java +++ b/libraries/test_utils/src/test/java/androidx/media3/test/utils/FakeMultiPeriodLiveTimelineTest.java @@ -43,7 +43,8 @@ public class FakeMultiPeriodLiveTimelineTest { /* nowUs= */ 60_000_000L, adSequencePattern, /* isContentTimeline= */ true, - /* populateAds= */ false); + /* populateAds= */ false, + /* playedAds= */ false); Timeline.Period period = new Timeline.Period(); Timeline.Window window = new Timeline.Window(); @@ -79,7 +80,8 @@ public class FakeMultiPeriodLiveTimelineTest { /* nowUs= */ 100_000_000L, adSequencePattern, /* isContentTimeline= */ false, - /* populateAds= */ true); + /* populateAds= */ true, + /* playedAds= */ false); Timeline.Period period = new Timeline.Period(); Timeline.Window window = new Timeline.Window(); @@ -138,7 +140,8 @@ public class FakeMultiPeriodLiveTimelineTest { /* nowUs= */ 100_000_000L, adSequencePattern, /* isContentTimeline= */ false, - /* populateAds= */ false); + /* populateAds= */ false, + /* playedAds= */ false); Timeline.Period period = new Timeline.Period(); Timeline.Window window = new Timeline.Window(); @@ -189,7 +192,8 @@ public class FakeMultiPeriodLiveTimelineTest { /* nowUs= */ 60_000_123L, adSequencePattern, /* isContentTimeline= */ true, - /* populateAds= */ false); + /* populateAds= */ false, + /* playedAds= */ false); Timeline.Period period = new Timeline.Period(); Timeline.Window window = new Timeline.Window(); @@ -273,7 +277,8 @@ public class FakeMultiPeriodLiveTimelineTest { nowUs, adSequencePattern, /* isContentTimeline= */ true, - /* populateAds= */ false); + /* populateAds= */ false, + /* playedAds= */ true); assertThat(timeline.getWindow(0, new Timeline.Window()).windowStartTimeMs) .isEqualTo(Util.usToMs(nowUs - liveWindowDurationUs)); @@ -333,7 +338,8 @@ public class FakeMultiPeriodLiveTimelineTest { nowUs, adSequencePattern, /* isContentTimeline= */ true, - /* populateAds= */ false); + /* populateAds= */ false, + /* playedAds= */ false); assertThat(timeline.getWindow(0, new Timeline.Window()).windowStartTimeMs) .isEqualTo(Util.usToMs(nowUs - liveWindowDurationUs)); @@ -353,7 +359,8 @@ public class FakeMultiPeriodLiveTimelineTest { /* nowUs= */ 120_000_000L, new boolean[] {false, true, true, true}, /* isContentTimeline= */ true, - /* populateAds= */ false); + /* populateAds= */ false, + /* playedAds= */ false); Timeline.Period period = new Timeline.Period(); Timeline.Window window = new Timeline.Window(); @@ -390,7 +397,8 @@ public class FakeMultiPeriodLiveTimelineTest { /* nowUs= */ 250_000_000L, new boolean[] {false, true, false, true, false}, /* isContentTimeline= */ true, - /* populateAds= */ false); + /* populateAds= */ false, + /* playedAds= */ false); assertThat(timeline.getPeriodCount()).isEqualTo(10); assertThat(timeline.getWindow(0, window).windowStartTimeMs).isEqualTo(30_000L);