From 0a2a20d4fbbf04812386142a833c2d8a7593c75c Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 20 Mar 2023 20:55:43 +0000 Subject: [PATCH] Skip played live ad media periods This change makes sure that live ad periods that are played are skip when attempted to be added to the queue. To make this work the existing filter logic had to be take into account the content resume offset that live periods use. PiperOrigin-RevId: 518068138 --- .../androidx/media3/common/TimelineTest.java | 3 +- .../media3/exoplayer/MediaPeriodQueue.java | 29 ++++++++--- .../exoplayer/MediaPeriodQueueTest.java | 50 +++++++++++++++++-- .../media3/exoplayer/ima/ImaUtilTest.java | 9 ++-- .../utils/FakeMultiPeriodLiveTimeline.java | 20 ++++++-- .../FakeMultiPeriodLiveTimelineTest.java | 24 ++++++--- 6 files changed, 109 insertions(+), 26 deletions(-) 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);