From 76e195ff5aa8bcfd13b664c57ed7e180ec803ba9 Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 6 Apr 2023 11:19:01 +0100 Subject: [PATCH] Correct ad durations when timeline moves more than a single period This change improves `ImaUtil.maybeCorrectPreviouslyUnknownAdDuration` to handles the case when the timeline moves forward more than a single period while an ad group with unknown period duration is being played. PiperOrigin-RevId: 522292612 --- .../ImaServerSideAdInsertionMediaSource.java | 4 +- .../media3/exoplayer/ima/ImaUtil.java | 105 ++-- .../media3/exoplayer/ima/ImaUtilTest.java | 458 +++++++++++++++--- 3 files changed, 464 insertions(+), 103 deletions(-) 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 bf318794df..6a7f3ac8e7 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 @@ -28,7 +28,7 @@ import static androidx.media3.exoplayer.ima.ImaUtil.getAdGroupAndIndexInVodMulti import static androidx.media3.exoplayer.ima.ImaUtil.getAdGroupDurationUsForLiveAdPeriodIndex; import static androidx.media3.exoplayer.ima.ImaUtil.getWindowStartTimeUs; import static androidx.media3.exoplayer.ima.ImaUtil.handleAdPeriodRemovedFromTimeline; -import static androidx.media3.exoplayer.ima.ImaUtil.maybeCorrectPreviouslyUnknownAdDuration; +import static androidx.media3.exoplayer.ima.ImaUtil.maybeCorrectPreviouslyUnknownAdDurations; import static androidx.media3.exoplayer.ima.ImaUtil.secToMsRounded; import static androidx.media3.exoplayer.ima.ImaUtil.secToUsRounded; import static androidx.media3.exoplayer.ima.ImaUtil.splitAdGroup; @@ -691,7 +691,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou // If the ad started playing while the corresponding period in the timeline had an unknown // duration, the ad duration is estimated and needs to be corrected when the actual duration // is reported. - adPlaybackState = maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState); + adPlaybackState = maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, adPlaybackState); } this.contentTimeline = contentTimeline; invalidateServerSideAdInsertionAdPlaybackState(); 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 c32d5d4f9a..90fbeea96d 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 @@ -46,6 +46,7 @@ import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSchemeDataSource; import androidx.media3.datasource.DataSourceUtil; import androidx.media3.datasource.DataSpec; +import com.google.ads.interactivemedia.v3.api.Ad; import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; import com.google.ads.interactivemedia.v3.api.AdError; import com.google.ads.interactivemedia.v3.api.AdErrorEvent; @@ -537,23 +538,30 @@ import java.util.Set; } /** - * Updates a previously estimated ad duration with the period duration from the timeline. + * Updates previously inserted ad durations with actual period durations from the timeline and + * returns the updated {@linkplain AdPlaybackState ad playback state}. * *

This method must only be called for multi period live streams and is useful in the case that - * an ad started playing while its period duration was still unknown. In this case the estimated - * ad duration was used which can be corrected as soon as the {@code contentTimeline} was - * refreshed with the actual period duration. + * {@linkplain #addLiveAdBreak(long, long, int, long, int, AdPlaybackState) a live ad has been + * inserted} while the duration of the corresponding period was still unknown. In this case the + * {@linkplain Ad#getDuration() estimated ad duration} was used which must be corrected as soon as + * the live window of the {@code contentTimeline} advances and the previously unknown period + * duration is available. * - *

The method queries the {@linkplain AdPlaybackState ad playback state} for an ad that starts - * at the period start time of the last period that has a known duration. If found, the ad - * duration is set to the period duration and the new ad playback state is returned. If not found - * or the duration is already correct the ad playback state remains unchanged. + *

Roughly, the logic checks whether an ad group of the ad playback state fits in or overlaps + * one or several periods in the content timeline. Starting at the first ad inside the window, the + * ad duration is set to the duration of the corresponding period until a period with an unknown + * duration or the end of the ad group is reached. + * + *

If the previously playing ad period isn't available in the content timeline anymore, no + * correction is applied. The resulting position discontinuity of {@link + * Player#DISCONTINUITY_REASON_REMOVE} needs to be handled accordingly elsewhere. * * @param contentTimeline The live content timeline. * @param adPlaybackState The ad playback state. * @return The (potentially) updated ad playback state. */ - public static AdPlaybackState maybeCorrectPreviouslyUnknownAdDuration( + public static AdPlaybackState maybeCorrectPreviouslyUnknownAdDurations( Timeline contentTimeline, AdPlaybackState adPlaybackState) { Timeline.Window window = contentTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()); if (window.firstPeriodIndex == window.lastPeriodIndex || adPlaybackState.adGroupCount < 2) { @@ -561,51 +569,68 @@ import java.util.Set; return adPlaybackState; } Timeline.Period period = new Timeline.Period(); - // Get the first period from the end with a known duration. - int periodIndex = window.lastPeriodIndex; - while (periodIndex >= window.firstPeriodIndex - && contentTimeline.getPeriod(periodIndex, period).durationUs == C.TIME_UNSET) { - periodIndex--; + int lastPeriodIndex = window.lastPeriodIndex; + if (contentTimeline.getPeriod(lastPeriodIndex, period).durationUs == C.TIME_UNSET) { + lastPeriodIndex--; + contentTimeline.getPeriod(lastPeriodIndex, period); } - // Search for an ad group at or before the period start. + // Search for an unplayed ad group at or before the period start. long windowStartTimeUs = getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs); - long periodStartTimeUs = windowStartTimeUs + period.positionInWindowUs; + long lastCompletePeriodStartTimeUs = windowStartTimeUs + period.positionInWindowUs; int adGroupIndex = adPlaybackState.getAdGroupIndexForPositionUs( - periodStartTimeUs, /* periodDurationUs= */ C.TIME_UNSET); + lastCompletePeriodStartTimeUs, /* periodDurationUs= */ C.TIME_UNSET); if (adGroupIndex == C.INDEX_UNSET) { - // No ad group at or before the period start. + // No unplayed ads before the last period with a duration. Nothing to do. return adPlaybackState; } - AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex); - if (adGroup.timeUs + adGroup.contentResumeOffsetUs < periodStartTimeUs) { - // Ad group ends before the period starts. + + long periodStartTimeUs = windowStartTimeUs - window.positionInFirstPeriodUs; + if (adGroup.timeUs + adGroup.contentResumeOffsetUs <= periodStartTimeUs) { + // Ad group ends before first period in window. Discontinuity of reason REMOVE. return adPlaybackState; } - // Period is inside the ad group. Get ad start that matches the period start. - long adGroupDurationUs = 0; - for (int adIndex = 0; adIndex < adGroup.durationsUs.length; adIndex++) { - long adDurationUs = adGroup.durationsUs[adIndex]; - if (adGroup.timeUs + adGroupDurationUs < periodStartTimeUs) { - adGroupDurationUs += adDurationUs; - continue; - } - if (period.durationUs == adDurationUs) { - // No update required. + // The ads at the start of the ad group may be out of the window already. Skip them. + int firstAdIndexInWindow = 0; + long adStartTimeUs = adGroup.timeUs; + while (adStartTimeUs < periodStartTimeUs) { + if (adGroup.states[firstAdIndexInWindow] == AD_STATE_AVAILABLE) { + // The previously available ad is not in the timeline anymore. Discontinuity of reason + // `DISCONTINUITY_REASON_REMOVE`. return adPlaybackState; } - // Set the ad duration to the period duration. - adPlaybackState = - updateAdDurationInAdGroup( - adGroupIndex, /* adIndexInAdGroup= */ adIndex, period.durationUs, adPlaybackState); - // Get the ad group again and set the new content resume offset after update. - adGroupDurationUs = sum(adPlaybackState.getAdGroup(adGroupIndex).durationsUs); - return adPlaybackState.withContentResumeOffsetUs(adGroupIndex, adGroupDurationUs); + // Skip ad before first period of window. + adStartTimeUs += adGroup.durationsUs[firstAdIndexInWindow++]; } - // Return unchanged. - return adPlaybackState; + int firstPeriodIndexInAdGroup = C.INDEX_UNSET; + for (int i = window.firstPeriodIndex; i <= lastPeriodIndex; i++) { + if (adGroup.timeUs <= periodStartTimeUs) { + firstPeriodIndexInAdGroup = i; + break; + } + periodStartTimeUs += contentTimeline.getPeriod(/* periodIndex= */ i, period).durationUs; + } + checkState(firstPeriodIndexInAdGroup != C.INDEX_UNSET); + + // Update all ad durations that we know and are not yet correct. + for (int i = firstAdIndexInWindow; i < adGroup.durationsUs.length; i++) { + int adPeriodIndex = firstPeriodIndexInAdGroup + (i - firstAdIndexInWindow); + if (adPeriodIndex > lastPeriodIndex) { + break; + } + contentTimeline.getPeriod(adPeriodIndex, period); + if (period.durationUs != adGroup.durationsUs[i]) { + // Set the ad duration to the period duration. + adPlaybackState = + updateAdDurationInAdGroup( + adGroupIndex, /* adIndexInAdGroup= */ i, period.durationUs, adPlaybackState); + } + } + // Get the ad group again and set the new content resume offset after update. + long adGroupDurationUs = sum(adPlaybackState.getAdGroup(adGroupIndex).durationsUs); + return adPlaybackState.withContentResumeOffsetUs(adGroupIndex, adGroupDurationUs); } /** 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 43a3cc532e..d0bf367819 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 @@ -25,7 +25,7 @@ import static androidx.media3.exoplayer.ima.ImaUtil.addLiveAdBreak; import static androidx.media3.exoplayer.ima.ImaUtil.getAdGroupAndIndexInLiveMultiPeriodTimeline; import static androidx.media3.exoplayer.ima.ImaUtil.getAdGroupAndIndexInVodMultiPeriodTimeline; import static androidx.media3.exoplayer.ima.ImaUtil.handleAdPeriodRemovedFromTimeline; -import static androidx.media3.exoplayer.ima.ImaUtil.maybeCorrectPreviouslyUnknownAdDuration; +import static androidx.media3.exoplayer.ima.ImaUtil.maybeCorrectPreviouslyUnknownAdDurations; import static androidx.media3.exoplayer.ima.ImaUtil.secToUsRounded; import static androidx.media3.exoplayer.ima.ImaUtil.splitAdGroup; import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState; @@ -1074,7 +1074,7 @@ public class ImaUtilTest { /* populateAds= */ false, /* playedAds= */ false); - adPlaybackState = maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState); + adPlaybackState = maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, adPlaybackState); assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).timeUs).isEqualTo(80_000_000L); assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) @@ -1116,7 +1116,7 @@ public class ImaUtilTest { /* populateAds= */ false, /* playedAds= */ false); - adPlaybackState = maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState); + adPlaybackState = maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, adPlaybackState); assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).timeUs).isEqualTo(80_000_000L); assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) @@ -1135,14 +1135,8 @@ public class ImaUtilTest { addAdGroupToAdPlaybackState( adPlaybackState, /* fromPositionUs= */ 80_000_000L, - /* contentResumeOffsetUs= */ 123, - /* adDurationsUs...= */ 123); - adPlaybackState = - addAdGroupToAdPlaybackState( - adPlaybackState, - /* fromPositionUs= */ 90_000_000L, - /* contentResumeOffsetUs= */ 123, - /* adDurationsUs...= */ 123); + /* contentResumeOffsetUs= */ 123L, + /* adDurationsUs...= */ 123L); FakeMultiPeriodLiveTimeline contentTimeline = new FakeMultiPeriodLiveTimeline( /* availabilityStartTimeMs= */ 0, @@ -1157,7 +1151,7 @@ public class ImaUtilTest { /* playedAds= */ false); AdPlaybackState correctedAdPlaybackState = - maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState); + maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, adPlaybackState); assertThat(contentTimeline.getWindow(/* windowIndex= */ 0, new Window()).windowStartTimeMs) .isEqualTo(100_000L); @@ -1186,117 +1180,459 @@ public class ImaUtilTest { /* playedAds= */ false); AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); - // Insert first ad resulting in group [10_000, 20_000, 0] + // Insert first ad resulting in group [10_000_000, 29_000_123, 0, 0] adPlaybackState = addLiveAdBreak( /* currentContentPeriodPositionUs= */ 30_000_000, /* adDurationUs= */ 10_000_000, /* adPositionInAdPod= */ 1, - /* totalAdDurationUs= */ 40_000_123, + /* totalAdDurationUs= */ 39_000_123, /* totalAdsInAdPod= */ 4, adPlaybackState); AdPlaybackState correctedAdPlaybackState = - maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState); + maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, adPlaybackState); - // Assert starting point: no change because the second ad period is still last of window. + // Assert no change because the second ad period is still last of window. assertThat(correctedAdPlaybackState).isSameInstanceAs(adPlaybackState); - assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) - .asList() - .containsExactly(10_000_000L, 30_000_123L, 0L, 0L) - .inOrder(); - // Get third ad period into timeline so the second ad period gets a duration: [c, a, a, a] + // Get third ad period into timeline so the second ad period gets a duration: [c, a, a, a], a contentTimeline.advanceNowUs(1L); correctedAdPlaybackState = - maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, correctedAdPlaybackState); + maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, correctedAdPlaybackState); assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) .asList() - .containsExactly(10_000_000L, 10_000_000L, 20_000_123L, 0L) + .containsExactly(10_000_000L, 10_000_000L, 19_000_123L, 0L) .inOrder(); - // Get next ad period into timeline so the third ad period gets a duration: [c, a, a, a, a] + // Second ad event resulting in group [10_000_000, 10_000_000, 19_000_123, 0] + correctedAdPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 40_000_000L, + /* adDurationUs= */ 10_000_000L, + /* adPositionInAdPod= */ 2, + /* totalAdDurationUs= */ 39_000_123L, + /* totalAdsInAdPod= */ 4, + correctedAdPlaybackState); + + // Get last ad period into timeline so the third ad period gets a duration: [c, a, a, a, a] contentTimeline.advanceNowUs(10_000_000L); correctedAdPlaybackState = - maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, correctedAdPlaybackState); + maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, correctedAdPlaybackState); assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) .asList() - .containsExactly(10_000_000L, 10_000_000L, 10_000_000L, 10_000_123L) + .containsExactly(10_000_000L, 10_000_000L, 10_000_000L, 9_000_123L) .inOrder(); assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).states) .asList() - .containsExactly(1, 0, 0, 0) + .containsExactly(1, 1, 0, 0) .inOrder(); - // It doesn't matter whether the live break event or the correction propagates the remainder - // forward. Updating the ad by ad event later only marks the ad as available. + // The event of the previously corrected ad sets the same duration and marks the ad available. correctedAdPlaybackState = addLiveAdBreak( - /* currentContentPeriodPositionUs= */ 40_000_000, - /* adDurationUs= */ 10_000_000, - /* adPositionInAdPod= */ 2, - /* totalAdDurationUs= */ 40_000_000, - /* totalAdsInAdPod= */ 4, - correctedAdPlaybackState); - - // No change in durations. - assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) - .asList() - .containsExactly(10_000_000L, 10_000_000L, 10_000_000L, 10_000_123L) - .inOrder(); - assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).states) - .asList() - .containsExactly(1, 1, 0, 0); - - correctedAdPlaybackState = - addLiveAdBreak( - /* currentContentPeriodPositionUs= */ 40_000_000, - /* adDurationUs= */ 10_000_000, + /* currentContentPeriodPositionUs= */ 50_000_000L, + /* adDurationUs= */ 10_000_000L, /* adPositionInAdPod= */ 3, - /* totalAdDurationUs= */ 40_000_000, + /* totalAdDurationUs= */ 39_000_123L, /* totalAdsInAdPod= */ 4, correctedAdPlaybackState); // No change in durations. assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) .asList() - .containsExactly(10_000_000L, 10_000_000L, 10_000_000L, 10_000_123L) + .containsExactly(10_000_000L, 10_000_000L, 10_000_000L, 9_000_123L) .inOrder(); assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).states) .asList() .containsExactly(1, 1, 1, 0); + // The last ad is inserted with ad pod duration 123 as fallback of the missing duration. correctedAdPlaybackState = addLiveAdBreak( - /* currentContentPeriodPositionUs= */ 40_000_000, - /* adDurationUs= */ 9_999_999L, - /* adPositionInAdPod= */ 3, - /* totalAdDurationUs= */ 40_000_000, + /* currentContentPeriodPositionUs= */ 40_000_000L, + /* adDurationUs= */ 123L, + /* adPositionInAdPod= */ 4, + /* totalAdDurationUs= */ 40_000_000L, /* totalAdsInAdPod= */ 4, correctedAdPlaybackState); // Last duration updated with ad pod duration. assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) .asList() - .containsExactly(10_000_000L, 10_000_000L, 10_000_000L, 9_999_999L) + .containsExactly(10_000_000L, 10_000_000L, 10_000_000L, 123L) .inOrder(); assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).states) .asList() .containsExactly(1, 1, 1, 1); - // Get next period into timeline so the 4th ad period gets a duration: [..., a, a, c] + // Get period after the ad group into timeline. All ad periods have a duration: [..., a, a, c] contentTimeline.advanceNowUs(10_000_000L); correctedAdPlaybackState = - maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, correctedAdPlaybackState); + maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, correctedAdPlaybackState); - // Last duration corrected when period arrives. assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) .asList() .containsExactly(10_000_000L, 10_000_000L, 10_000_000L, 10_000_000L); } + @Test + public void + maybeCorrectPreviouslyUnknownAdDuration_timelineMovesMultiplePeriodsForward_adDurationCorrected() { + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + // Timeline window to start with: c, a, a, a, [a, c, a], a, a, a + FakeMultiPeriodLiveTimeline contentTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeMs= */ 0, + /* liveWindowDurationUs= */ 40_000_321L, + /* nowUs= */ 109_234_000L, + /* adSequencePattern= */ new boolean[] {false, true, true, true, true}, + /* periodDurationMsPattern= */ new long[] { + PERIOD_DURATION_MS, 10_123L, 10_457L, AD_PERIOD_DURATION_MS, AD_PERIOD_DURATION_MS + }, + /* isContentTimeline= */ true, + /* populateAds= */ false, + /* playedAds= */ false); + // Get the period start position at which to insert the ad. + long adPeriodStartTimeUs = + contentTimeline.getWindowStartTimeUs() + + contentTimeline.getPeriod(/* periodIndex= */ 2, new Period()).positionInWindowUs; + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ adPeriodStartTimeUs, + /* adDurationUs= */ 123L, // Incorrect duration to be corrected. + /* adPositionInAdPod= */ 1, + /* totalAdDurationUs= */ 28_000_000L, + /* totalAdsInAdPod= */ 4, + adPlaybackState); + + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(123L, 27_999_877L, 0L, 0L) + .inOrder(); + + // Advance the live window in timeline: c, a, a, a, a, [c, a, a, a], a, c + contentTimeline.advanceNowUs(20_000_000L); + adPlaybackState = maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, adPlaybackState); + + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(10_123_000L, 10_457_000L, 17_542_877L, 0L) + .inOrder(); + } + + @Test + public void + maybeCorrectPreviouslyUnknownAdDuration_allPeriodsInWindowWithKnownDuration_adDurationCorrected() { + // Timeline with window: c, a, a, a, a, [c, a, a, a], a, c + long nowUs = 38_064_000L + 38_064_000L - 3_333_000L; + long liveWindowDurationUs = 4_731_351L; + FakeMultiPeriodLiveTimeline contentTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeMs= */ 0, + /* liveWindowDurationUs= */ liveWindowDurationUs, + nowUs, + /* adSequencePattern= */ new boolean[] {false, true, true, true, true}, + /* periodDurationMsPattern= */ new long[] { + PERIOD_DURATION_MS, 1_231L, 2_000L, 1_500L, 3_333L + }, + /* isContentTimeline= */ true, + /* populateAds= */ false, + /* playedAds= */ false) { + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + super.getPeriod(periodIndex, period, setIds); + if (periodIndex == 3 && period.durationUs == C.TIME_UNSET) { + // Normally the FakeMultiPeriodLiveTimeline sets the last period to an unknown + // duration. Make sure that the correct duration is used when overriding. + long positionInFirstPeriodUs = + getWindow(period.windowIndex, new Window()).positionInFirstPeriodUs; + period.durationUs = positionInFirstPeriodUs != 0 ? 1_500_000L : 3_333_000L; + } + return period; + } + }; + Window window = contentTimeline.getWindow(0, new Window()); + long windowStartTimeUs = + ImaUtil.getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs); + long firstAdPeriodStartTimeUs = + windowStartTimeUs + + contentTimeline.getPeriod(/* periodIndex= */ 1, new Period()).positionInWindowUs; + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ firstAdPeriodStartTimeUs, + /* adDurationUs= */ 753L, + /* adPositionInAdPod= */ 1, + /* totalAdDurationUs= */ 8_000_000L, + /* totalAdsInAdPod= */ 4, + adPlaybackState); + + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(753L, 7_999_247L, 0L, 0L) + .inOrder(); + + adPlaybackState = maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, adPlaybackState); + + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(1_231_000L, 2_000_000L, 1_500_000L, 4_499_247L) + .inOrder(); + + // After advancing: c, a, a, a, a, c, [a, a, a, a], c + contentTimeline.advanceNowUs(351L); + adPlaybackState = maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, adPlaybackState); + + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(1_231_000L, 2_000_000L, 1_500_000L, 3_333_000L) + .inOrder(); + } + + @Test + public void + maybeCorrectPreviouslyUnknownAdDuration_timelineMovesMultiplePeriodsForwardStartOfAdGroupNotInWindow_adDurationCorrected() { + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + // 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] + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 100_000_000L, + /* adDurationUs= */ 10_000_000L, + /* adPositionInAdPod= */ 1, + /* totalAdDurationUs= */ 38_000_000L, + /* totalAdsInAdPod= */ 4, + adPlaybackState); + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); + // durationsUs: [10_000_000L, 123L, 27_999_877L, 0L] + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 110_000_000L, + /* adDurationUs= */ 123L, + /* adPositionInAdPod= */ 2, + /* totalAdDurationUs= */ 38_000_000L, + /* totalAdsInAdPod= */ 4, + adPlaybackState); + // Correct with window that move more than a single period: c, a, a, a, a, c, a, [a, a, a, c] + FakeMultiPeriodLiveTimeline contentTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeMs= */ 0, + /* liveWindowDurationUs= */ 40_000_000L, + /* nowUs= */ 159_234_567L, + /* adSequencePattern= */ new boolean[] {false, true, true, true, true}, + /* periodDurationMsPattern= */ new long[] { + PERIOD_DURATION_MS, + AD_PERIOD_DURATION_MS, + AD_PERIOD_DURATION_MS, + AD_PERIOD_DURATION_MS, + AD_PERIOD_DURATION_MS + }, + /* isContentTimeline= */ true, + /* populateAds= */ false, + /* playedAds= */ false); + + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(10_000_000L, 123L, 27_999_877L, 0L) + .inOrder(); + + adPlaybackState = maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, adPlaybackState); + + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(10_000_000L, 10_000_000L, 10_000_000L, 10_000_000L) + .inOrder(); + } + + @Test + public void + maybeCorrectPreviouslyUnknownAdDuration_timelineMovesMultiplePeriodsForwardWithinAdOnlyWindow_adDurationCorrected() { + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + // 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 = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 30_000_000L, + /* adDurationUs= */ 10_000_000L, + /* adPositionInAdPod= */ 1, + /* totalAdDurationUs= */ 78_000_000L, + /* totalAdsInAdPod= */ 8, + adPlaybackState); + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 40_000_000L, + /* adDurationUs= */ 10_000_000L, + /* adPositionInAdPod= */ 2, + /* totalAdDurationUs= */ 78_000_000L, + /* totalAdsInAdPod= */ 8, + adPlaybackState); + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1); + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 50_000_000L, + /* adDurationUs= */ 10_000_000L, + /* adPositionInAdPod= */ 3, + /* totalAdDurationUs= */ 78_000_000L, + /* totalAdsInAdPod= */ 8, + adPlaybackState); + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2); + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 60_000_000L, + /* adDurationUs= */ 10_000_000L, + /* adPositionInAdPod= */ 4, + /* totalAdDurationUs= */ 78_000_000L, + /* totalAdsInAdPod= */ 8, + adPlaybackState); + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 3); + // Ad event for the ad period that is last in the window. + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 70_000_000L, + /* adDurationUs= */ 123L, + /* adPositionInAdPod= */ 4, + /* totalAdDurationUs= */ 78_000_000L, + /* totalAdsInAdPod= */ 8, + adPlaybackState); + // Correct with window that move more than a single period: c, a, a, a, a, [a, a, a, a], c + // Still playing at adIndex=4 + FakeMultiPeriodLiveTimeline contentTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeMs= */ 0, + /* liveWindowDurationUs= */ 30_000_000L, + /* nowUs= */ 109_234_567L, + /* adSequencePattern= */ new boolean[] { + false, true, true, true, true, true, true, true, true + }, + /* periodDurationMsPattern= */ new long[] { + PERIOD_DURATION_MS, + AD_PERIOD_DURATION_MS, + AD_PERIOD_DURATION_MS, + AD_PERIOD_DURATION_MS, + AD_PERIOD_DURATION_MS, + AD_PERIOD_DURATION_MS, + AD_PERIOD_DURATION_MS, + AD_PERIOD_DURATION_MS, + AD_PERIOD_DURATION_MS + }, + /* isContentTimeline= */ true, + /* populateAds= */ false, + /* playedAds= */ false); + + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly( + 10_000_000L, 10_000_000L, 10_000_000L, 10_000_000L, 123L, 37_999_877L, 0L, 0L) + .inOrder(); + + adPlaybackState = maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, adPlaybackState); + + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly( + 10_000_000L, + 10_000_000L, + 10_000_000L, + 10_000_000L, + 10_000_000L, + 10_000_000L, + 10_000_000L, + 17_999_877L) + .inOrder(); + + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 4); + // Advance to get a duration for the last ad period: c, a, a, a, a, a, [a, a, a, c] + contentTimeline.advanceNowUs(/* durationUs= */ 10_000_000L); + adPlaybackState = maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, adPlaybackState); + + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly( + 10_000_000L, + 10_000_000L, + 10_000_000L, + 10_000_000L, + 10_000_000L, + 10_000_000L, + 10_000_000L, + 10_000_000L) + .inOrder(); + } + + @Test + public void maybeCorrectPreviouslyUnknownAdDuration_playingAdPeriodRemoved_doNothing() { + long adPeriodDurationUs = msToUs(AD_PERIOD_DURATION_MS); + long periodDurationUs = msToUs(PERIOD_DURATION_MS); + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); + // 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] + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 100_000_000L, + /* adDurationUs= */ 10_000_000L, + /* adPositionInAdPod= */ 1, + /* totalAdDurationUs= */ 38_000_000L, + /* totalAdsInAdPod= */ 4, + adPlaybackState); + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); + // Playback advances to second ad. Insert second ad break. Playing on last period of window. + // durationsUs: [10_000_000L, 123L, 27_999_877L, 0L] + adPlaybackState = + addLiveAdBreak( + /* currentContentPeriodPositionUs= */ 110_000_000L, + /* adDurationUs= */ 123L, + /* adPositionInAdPod= */ 2, + /* totalAdDurationUs= */ 38_000_000L, + /* totalAdsInAdPod= */ 4, + adPlaybackState); + // Window advances to a state where the playing ad period has been removed: + // c, a, a, a, a, c, a, a, [a, a, c] + FakeMultiPeriodLiveTimeline contentTimeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeMs= */ 0, + /* liveWindowDurationUs= */ 40_000_000L, + /* nowUs= */ 169_234_567L, + /* adSequencePattern= */ new boolean[] {false, true, true, true, true}, + /* periodDurationMsPattern= */ new long[] { + periodDurationUs, + adPeriodDurationUs, + adPeriodDurationUs, + adPeriodDurationUs, + adPeriodDurationUs + }, + /* isContentTimeline= */ true, + /* populateAds= */ false, + /* playedAds= */ false); + + assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) + .asList() + .containsExactly(10_000_000L, 123L, 27_999_877L, 0L) + .inOrder(); + + AdPlaybackState correctedAdPlaybackState = + maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, adPlaybackState); + + assertThat(correctedAdPlaybackState).isSameInstanceAs(adPlaybackState); + } + @Test public void maybeCorrectPreviouslyUnknownAdDuration_singleContentPeriodTimeline_doNothing() { FakeMultiPeriodLiveTimeline contentTimeline = @@ -1327,7 +1663,7 @@ public class ImaUtilTest { /* adDurationsUs...= */ 123); AdPlaybackState correctedAdPlaybackState = - maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState); + maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, adPlaybackState); assertThat(correctedAdPlaybackState).isSameInstanceAs(adPlaybackState); } @@ -1356,7 +1692,7 @@ public class ImaUtilTest { /* contentResumeOffsetUs= */ 123L, /* adDurationsUs...= */ 123L); - adPlaybackState = maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState); + adPlaybackState = maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, adPlaybackState); assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).timeUs).isEqualTo(80_000_000L); assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs)