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
This commit is contained in:
bachinger 2023-04-06 11:19:01 +01:00 committed by Ian Baker
parent 14ba173dfe
commit 76e195ff5a
3 changed files with 464 additions and 103 deletions

View File

@ -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.getAdGroupDurationUsForLiveAdPeriodIndex;
import static androidx.media3.exoplayer.ima.ImaUtil.getWindowStartTimeUs; import static androidx.media3.exoplayer.ima.ImaUtil.getWindowStartTimeUs;
import static androidx.media3.exoplayer.ima.ImaUtil.handleAdPeriodRemovedFromTimeline; 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.secToMsRounded;
import static androidx.media3.exoplayer.ima.ImaUtil.secToUsRounded; import static androidx.media3.exoplayer.ima.ImaUtil.secToUsRounded;
import static androidx.media3.exoplayer.ima.ImaUtil.splitAdGroup; 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 // 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 // duration, the ad duration is estimated and needs to be corrected when the actual duration
// is reported. // is reported.
adPlaybackState = maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState); adPlaybackState = maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, adPlaybackState);
} }
this.contentTimeline = contentTimeline; this.contentTimeline = contentTimeline;
invalidateServerSideAdInsertionAdPlaybackState(); invalidateServerSideAdInsertionAdPlaybackState();

View File

@ -46,6 +46,7 @@ import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSchemeDataSource; import androidx.media3.datasource.DataSchemeDataSource;
import androidx.media3.datasource.DataSourceUtil; import androidx.media3.datasource.DataSourceUtil;
import androidx.media3.datasource.DataSpec; 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.AdDisplayContainer;
import com.google.ads.interactivemedia.v3.api.AdError; import com.google.ads.interactivemedia.v3.api.AdError;
import com.google.ads.interactivemedia.v3.api.AdErrorEvent; 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}.
* *
* <p>This method must only be called for multi period live streams and is useful in the case that * <p>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 * {@linkplain #addLiveAdBreak(long, long, int, long, int, AdPlaybackState) a live ad has been
* ad duration was used which can be corrected as soon as the {@code contentTimeline} was * inserted} while the duration of the corresponding period was still unknown. In this case the
* refreshed with the actual period duration. * {@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.
* *
* <p>The method queries the {@linkplain AdPlaybackState ad playback state} for an ad that starts * <p>Roughly, the logic checks whether an ad group of the ad playback state fits in or overlaps
* at the period start time of the last period that has a known duration. If found, the ad * one or several periods in the content timeline. Starting at the first ad inside the window, the
* duration is set to the period duration and the new ad playback state is returned. If not found * ad duration is set to the duration of the corresponding period until a period with an unknown
* or the duration is already correct the ad playback state remains unchanged. * duration or the end of the ad group is reached.
*
* <p>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 contentTimeline The live content timeline.
* @param adPlaybackState The ad playback state. * @param adPlaybackState The ad playback state.
* @return The (potentially) updated ad playback state. * @return The (potentially) updated ad playback state.
*/ */
public static AdPlaybackState maybeCorrectPreviouslyUnknownAdDuration( public static AdPlaybackState maybeCorrectPreviouslyUnknownAdDurations(
Timeline contentTimeline, AdPlaybackState adPlaybackState) { Timeline contentTimeline, AdPlaybackState adPlaybackState) {
Timeline.Window window = contentTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()); Timeline.Window window = contentTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window());
if (window.firstPeriodIndex == window.lastPeriodIndex || adPlaybackState.adGroupCount < 2) { if (window.firstPeriodIndex == window.lastPeriodIndex || adPlaybackState.adGroupCount < 2) {
@ -561,51 +569,68 @@ import java.util.Set;
return adPlaybackState; return adPlaybackState;
} }
Timeline.Period period = new Timeline.Period(); Timeline.Period period = new Timeline.Period();
// Get the first period from the end with a known duration. int lastPeriodIndex = window.lastPeriodIndex;
int periodIndex = window.lastPeriodIndex; if (contentTimeline.getPeriod(lastPeriodIndex, period).durationUs == C.TIME_UNSET) {
while (periodIndex >= window.firstPeriodIndex lastPeriodIndex--;
&& contentTimeline.getPeriod(periodIndex, period).durationUs == C.TIME_UNSET) { contentTimeline.getPeriod(lastPeriodIndex, period);
periodIndex--;
} }
// 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 = long windowStartTimeUs =
getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs); getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs);
long periodStartTimeUs = windowStartTimeUs + period.positionInWindowUs; long lastCompletePeriodStartTimeUs = windowStartTimeUs + period.positionInWindowUs;
int adGroupIndex = int adGroupIndex =
adPlaybackState.getAdGroupIndexForPositionUs( adPlaybackState.getAdGroupIndexForPositionUs(
periodStartTimeUs, /* periodDurationUs= */ C.TIME_UNSET); lastCompletePeriodStartTimeUs, /* periodDurationUs= */ C.TIME_UNSET);
if (adGroupIndex == C.INDEX_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; return adPlaybackState;
} }
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex); 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; return adPlaybackState;
} }
// Period is inside the ad group. Get ad start that matches the period start. // The ads at the start of the ad group may be out of the window already. Skip them.
long adGroupDurationUs = 0; int firstAdIndexInWindow = 0;
for (int adIndex = 0; adIndex < adGroup.durationsUs.length; adIndex++) { long adStartTimeUs = adGroup.timeUs;
long adDurationUs = adGroup.durationsUs[adIndex]; while (adStartTimeUs < periodStartTimeUs) {
if (adGroup.timeUs + adGroupDurationUs < periodStartTimeUs) { if (adGroup.states[firstAdIndexInWindow] == AD_STATE_AVAILABLE) {
adGroupDurationUs += adDurationUs; // The previously available ad is not in the timeline anymore. Discontinuity of reason
continue; // `DISCONTINUITY_REASON_REMOVE`.
}
if (period.durationUs == adDurationUs) {
// No update required.
return adPlaybackState; return adPlaybackState;
} }
// Skip ad before first period of window.
adStartTimeUs += adGroup.durationsUs[firstAdIndexInWindow++];
}
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. // Set the ad duration to the period duration.
adPlaybackState = adPlaybackState =
updateAdDurationInAdGroup( updateAdDurationInAdGroup(
adGroupIndex, /* adIndexInAdGroup= */ adIndex, period.durationUs, adPlaybackState); adGroupIndex, /* adIndexInAdGroup= */ i, 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);
} }
// Return unchanged. }
return 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);
} }
/** /**

View File

@ -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.getAdGroupAndIndexInLiveMultiPeriodTimeline;
import static androidx.media3.exoplayer.ima.ImaUtil.getAdGroupAndIndexInVodMultiPeriodTimeline; import static androidx.media3.exoplayer.ima.ImaUtil.getAdGroupAndIndexInVodMultiPeriodTimeline;
import static androidx.media3.exoplayer.ima.ImaUtil.handleAdPeriodRemovedFromTimeline; 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.secToUsRounded;
import static androidx.media3.exoplayer.ima.ImaUtil.splitAdGroup; import static androidx.media3.exoplayer.ima.ImaUtil.splitAdGroup;
import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState; import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState;
@ -1074,7 +1074,7 @@ public class ImaUtilTest {
/* populateAds= */ false, /* populateAds= */ false,
/* playedAds= */ 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).timeUs).isEqualTo(80_000_000L);
assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs)
@ -1116,7 +1116,7 @@ public class ImaUtilTest {
/* populateAds= */ false, /* populateAds= */ false,
/* playedAds= */ 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).timeUs).isEqualTo(80_000_000L);
assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs)
@ -1135,14 +1135,8 @@ public class ImaUtilTest {
addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
adPlaybackState, adPlaybackState,
/* fromPositionUs= */ 80_000_000L, /* fromPositionUs= */ 80_000_000L,
/* contentResumeOffsetUs= */ 123, /* contentResumeOffsetUs= */ 123L,
/* adDurationsUs...= */ 123); /* adDurationsUs...= */ 123L);
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ 90_000_000L,
/* contentResumeOffsetUs= */ 123,
/* adDurationsUs...= */ 123);
FakeMultiPeriodLiveTimeline contentTimeline = FakeMultiPeriodLiveTimeline contentTimeline =
new FakeMultiPeriodLiveTimeline( new FakeMultiPeriodLiveTimeline(
/* availabilityStartTimeMs= */ 0, /* availabilityStartTimeMs= */ 0,
@ -1157,7 +1151,7 @@ public class ImaUtilTest {
/* playedAds= */ false); /* playedAds= */ false);
AdPlaybackState correctedAdPlaybackState = AdPlaybackState correctedAdPlaybackState =
maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState); maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, adPlaybackState);
assertThat(contentTimeline.getWindow(/* windowIndex= */ 0, new Window()).windowStartTimeMs) assertThat(contentTimeline.getWindow(/* windowIndex= */ 0, new Window()).windowStartTimeMs)
.isEqualTo(100_000L); .isEqualTo(100_000L);
@ -1186,117 +1180,459 @@ public class ImaUtilTest {
/* playedAds= */ false); /* playedAds= */ false);
AdPlaybackState adPlaybackState = AdPlaybackState adPlaybackState =
new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended(); 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 = adPlaybackState =
addLiveAdBreak( addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 30_000_000, /* currentContentPeriodPositionUs= */ 30_000_000,
/* adDurationUs= */ 10_000_000, /* adDurationUs= */ 10_000_000,
/* adPositionInAdPod= */ 1, /* adPositionInAdPod= */ 1,
/* totalAdDurationUs= */ 40_000_123, /* totalAdDurationUs= */ 39_000_123,
/* totalAdsInAdPod= */ 4, /* totalAdsInAdPod= */ 4,
adPlaybackState); adPlaybackState);
AdPlaybackState correctedAdPlaybackState = 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).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); contentTimeline.advanceNowUs(1L);
correctedAdPlaybackState = correctedAdPlaybackState =
maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, correctedAdPlaybackState); maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, correctedAdPlaybackState);
assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs)
.asList() .asList()
.containsExactly(10_000_000L, 10_000_000L, 20_000_123L, 0L) .containsExactly(10_000_000L, 10_000_000L, 19_000_123L, 0L)
.inOrder(); .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); contentTimeline.advanceNowUs(10_000_000L);
correctedAdPlaybackState = correctedAdPlaybackState =
maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, correctedAdPlaybackState); maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, correctedAdPlaybackState);
assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs)
.asList() .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(); .inOrder();
assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).states) assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).states)
.asList() .asList()
.containsExactly(1, 0, 0, 0) .containsExactly(1, 1, 0, 0)
.inOrder(); .inOrder();
// It doesn't matter whether the live break event or the correction propagates the remainder // The event of the previously corrected ad sets the same duration and marks the ad available.
// forward. Updating the ad by ad event later only marks the ad as available.
correctedAdPlaybackState = correctedAdPlaybackState =
addLiveAdBreak( addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 40_000_000, /* currentContentPeriodPositionUs= */ 50_000_000L,
/* adDurationUs= */ 10_000_000, /* adDurationUs= */ 10_000_000L,
/* 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,
/* adPositionInAdPod= */ 3, /* adPositionInAdPod= */ 3,
/* totalAdDurationUs= */ 40_000_000, /* totalAdDurationUs= */ 39_000_123L,
/* totalAdsInAdPod= */ 4, /* totalAdsInAdPod= */ 4,
correctedAdPlaybackState); correctedAdPlaybackState);
// No change in durations. // No change in durations.
assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs)
.asList() .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(); .inOrder();
assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).states) assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).states)
.asList() .asList()
.containsExactly(1, 1, 1, 0); .containsExactly(1, 1, 1, 0);
// The last ad is inserted with ad pod duration 123 as fallback of the missing duration.
correctedAdPlaybackState = correctedAdPlaybackState =
addLiveAdBreak( addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 40_000_000, /* currentContentPeriodPositionUs= */ 40_000_000L,
/* adDurationUs= */ 9_999_999L, /* adDurationUs= */ 123L,
/* adPositionInAdPod= */ 3, /* adPositionInAdPod= */ 4,
/* totalAdDurationUs= */ 40_000_000, /* totalAdDurationUs= */ 40_000_000L,
/* totalAdsInAdPod= */ 4, /* totalAdsInAdPod= */ 4,
correctedAdPlaybackState); correctedAdPlaybackState);
// Last duration updated with ad pod duration. // Last duration updated with ad pod duration.
assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs)
.asList() .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(); .inOrder();
assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).states) assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).states)
.asList() .asList()
.containsExactly(1, 1, 1, 1); .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); contentTimeline.advanceNowUs(10_000_000L);
correctedAdPlaybackState = correctedAdPlaybackState =
maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, correctedAdPlaybackState); maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, correctedAdPlaybackState);
// Last duration corrected when period arrives.
assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs)
.asList() .asList()
.containsExactly(10_000_000L, 10_000_000L, 10_000_000L, 10_000_000L); .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 @Test
public void maybeCorrectPreviouslyUnknownAdDuration_singleContentPeriodTimeline_doNothing() { public void maybeCorrectPreviouslyUnknownAdDuration_singleContentPeriodTimeline_doNothing() {
FakeMultiPeriodLiveTimeline contentTimeline = FakeMultiPeriodLiveTimeline contentTimeline =
@ -1327,7 +1663,7 @@ public class ImaUtilTest {
/* adDurationsUs...= */ 123); /* adDurationsUs...= */ 123);
AdPlaybackState correctedAdPlaybackState = AdPlaybackState correctedAdPlaybackState =
maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState); maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, adPlaybackState);
assertThat(correctedAdPlaybackState).isSameInstanceAs(adPlaybackState); assertThat(correctedAdPlaybackState).isSameInstanceAs(adPlaybackState);
} }
@ -1356,7 +1692,7 @@ public class ImaUtilTest {
/* contentResumeOffsetUs= */ 123L, /* contentResumeOffsetUs= */ 123L,
/* adDurationsUs...= */ 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).timeUs).isEqualTo(80_000_000L);
assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs) assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs)