Support splitting the ad playback state for live streams
This change does basically three things in preparation of a follow up CL that will insert ad breaks into multi-period live windows. - Make sure that only `AdPlaybackState`s of multi-period streams are passed to `ImaUtil.splitAdPlaybackStateForPeriods()` from the media source. - Enhance the splitting algorithm in ImaUtil to support live windows. Roughly, this includes calculating the window start position in the UNIX epoch context, handling the end period with an unknown duration and taking the `adResumePosition` of an ad group into account that affects the ad group start positions (`adGroup.timeUs`). - Append a placeholder postroll ad group to the ad playback state of each period in a live stream to allow immediate transition to an ad that's inserted by an ad event from the SDK. PiperOrigin-RevId: 516167958
This commit is contained in:
parent
f0d91b30d5
commit
d1534680e9
@ -678,12 +678,24 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
|
||||
@MainThread
|
||||
private void invalidateServerSideAdInsertionAdPlaybackState() {
|
||||
if (!adPlaybackState.equals(AdPlaybackState.NONE) && contentTimeline != null) {
|
||||
ImmutableMap<Object, AdPlaybackState> splitAdPlaybackStates =
|
||||
splitAdPlaybackStateForPeriods(adPlaybackState, contentTimeline);
|
||||
ImmutableMap<Object, AdPlaybackState> splitAdPlaybackStates;
|
||||
if (streamRequest.getFormat() == StreamRequest.StreamFormat.DASH) {
|
||||
// DASH ad groups are always split by period.
|
||||
splitAdPlaybackStates = splitAdPlaybackStateForPeriods(adPlaybackState, contentTimeline);
|
||||
} else {
|
||||
// The HLS single period timeline for VOD and live must not be split.
|
||||
int firstPeriodIndex =
|
||||
contentTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()).firstPeriodIndex;
|
||||
Object periodUid =
|
||||
checkNotNull(
|
||||
contentTimeline.getPeriod(
|
||||
firstPeriodIndex, new Timeline.Period(), /* setIds= */ true)
|
||||
.uid);
|
||||
splitAdPlaybackStates = ImmutableMap.of(periodUid, adPlaybackState);
|
||||
}
|
||||
streamPlayer.setAdPlaybackStates(adsId, splitAdPlaybackStates, contentTimeline);
|
||||
checkNotNull(serverSideAdInsertionMediaSource).setAdPlaybackStates(splitAdPlaybackStates);
|
||||
if (!ImaServerSideAdInsertionUriBuilder.isLiveStream(
|
||||
checkNotNull(mediaItem.localConfiguration).uri)) {
|
||||
if (!isLiveStream) {
|
||||
adsLoader.setAdPlaybackState(adsId, adPlaybackState);
|
||||
}
|
||||
}
|
||||
|
@ -20,10 +20,12 @@ import static androidx.media3.common.AdPlaybackState.AD_STATE_UNAVAILABLE;
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static androidx.media3.common.util.Assertions.checkState;
|
||||
import static androidx.media3.common.util.Util.msToUs;
|
||||
import static androidx.media3.common.util.Util.sum;
|
||||
import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState;
|
||||
import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.getMediaPeriodPositionUsForContent;
|
||||
import static java.lang.Math.max;
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Looper;
|
||||
@ -370,6 +372,19 @@ import java.util.Set;
|
||||
return adDurationsUs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the window start in microseconds since the Unix epoch for a window of a {@linkplain
|
||||
* Timeline timeline} of the {@code DashMediaSource}.
|
||||
*
|
||||
* @param windowStartTimeMs The window start time, in milliseconds.
|
||||
* @param positionInFirstPeriodUs The position of the window in the first period.
|
||||
* @return The window start time, in microseconds.
|
||||
*/
|
||||
private static long getWindowStartTimeUs(long windowStartTimeMs, long positionInFirstPeriodUs) {
|
||||
// Revert us/ms truncation introduced in `DashMediaSource.DashTimeline`.
|
||||
return msToUs(windowStartTimeMs) + (positionInFirstPeriodUs % 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits an {@link AdPlaybackState} into a separate {@link AdPlaybackState} for each period of a
|
||||
* content timeline.
|
||||
@ -387,19 +402,19 @@ import java.util.Set;
|
||||
*/
|
||||
public static ImmutableMap<Object, AdPlaybackState> splitAdPlaybackStateForPeriods(
|
||||
AdPlaybackState adPlaybackState, Timeline contentTimeline) {
|
||||
checkArgument(!contentTimeline.isEmpty());
|
||||
Timeline.Period period = new Timeline.Period();
|
||||
if (contentTimeline.getPeriodCount() == 1) {
|
||||
// A single period gets the entire ad playback state that may contain multiple ad groups.
|
||||
return ImmutableMap.of(
|
||||
checkNotNull(
|
||||
contentTimeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ true).uid),
|
||||
adPlaybackState);
|
||||
}
|
||||
|
||||
Timeline.Window window = contentTimeline.getWindow(0, new Timeline.Window());
|
||||
int periodIndex = 0;
|
||||
long totalElapsedContentDurationUs = 0;
|
||||
Object adsId = checkNotNull(adPlaybackState.adsId);
|
||||
AdPlaybackState contentOnlyAdPlaybackState = new AdPlaybackState(adsId);
|
||||
if (window.isLive()) {
|
||||
long windowStartTimeUs =
|
||||
getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs);
|
||||
totalElapsedContentDurationUs = windowStartTimeUs - window.positionInFirstPeriodUs;
|
||||
contentOnlyAdPlaybackState = contentOnlyAdPlaybackState.withLivePostrollPlaceholderAppended();
|
||||
}
|
||||
Map<Object, AdPlaybackState> adPlaybackStates = new HashMap<>();
|
||||
for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) {
|
||||
AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ i);
|
||||
@ -418,22 +433,42 @@ import java.util.Set;
|
||||
// Period starts before the ad group, so it is a content period.
|
||||
adPlaybackStates.put(checkNotNull(period.uid), contentOnlyAdPlaybackState);
|
||||
totalElapsedContentDurationUs += period.durationUs;
|
||||
// Current period added as a content period. Advance and look at the next period.
|
||||
periodIndex++;
|
||||
} else {
|
||||
long periodStartUs = totalElapsedContentDurationUs + elapsedAdGroupAdDurationUs;
|
||||
if (periodStartUs + period.durationUs <= adGroup.timeUs + adGroupDurationUs) {
|
||||
// The period ends before the end of the ad group, so it is an ad period (Note: A VOD ad
|
||||
// reported by the IMA SDK spans multiple periods before the LOADED event arrives).
|
||||
long periodDurationUs = period.durationUs;
|
||||
if ((periodDurationUs != C.TIME_UNSET
|
||||
&& periodStartUs + periodDurationUs <= adGroup.timeUs + adGroupDurationUs)
|
||||
|| (periodDurationUs == C.TIME_UNSET
|
||||
&& elapsedAdGroupAdDurationUs < adGroupDurationUs
|
||||
&& periodStartUs < adGroup.timeUs + adGroupDurationUs)) {
|
||||
// Ad period found. The period either ends before the end of the ad group, or it is the
|
||||
// last period of a live stream and it starts in the ad group.
|
||||
adPlaybackStates.put(
|
||||
checkNotNull(period.uid),
|
||||
splitAdGroupForPeriod(adsId, adGroup, periodStartUs, period.durationUs));
|
||||
elapsedAdGroupAdDurationUs += period.durationUs;
|
||||
splitAdGroupForPeriod(
|
||||
adsId, adGroup, periodStartUs, periodDurationUs, window.isLive()));
|
||||
// Current period added as an ad period. Advance and look at the next period.
|
||||
periodIndex++;
|
||||
elapsedAdGroupAdDurationUs += periodDurationUs;
|
||||
if (periodStartUs + periodDurationUs == adGroup.timeUs + adGroupDurationUs) {
|
||||
// Periods have consumed the ad group. We're at the end of the ad group.
|
||||
if (window.isLive()) {
|
||||
// Add elapsed ad duration to elapsed content duration for live streams to account
|
||||
// for the content resume offset (relevant because we above compare against
|
||||
// `adGroup.timeUs`). Instead of `adGroup.contentResumeOffsetUs` we use
|
||||
// `elapsedAdGroupAdDurationUs` that is the sum of the actual period durations.
|
||||
totalElapsedContentDurationUs += elapsedAdGroupAdDurationUs;
|
||||
}
|
||||
// Continue with next ad group.
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Period is after the current ad group. Continue with next ad group.
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Increment the period index to the next unclassified period.
|
||||
periodIndex++;
|
||||
}
|
||||
}
|
||||
// The remaining periods end after the last ad group, so these are content periods.
|
||||
@ -445,18 +480,32 @@ import java.util.Set;
|
||||
}
|
||||
|
||||
private static AdPlaybackState splitAdGroupForPeriod(
|
||||
Object adsId, AdGroup adGroup, long periodStartUs, long periodDurationUs) {
|
||||
Object adsId,
|
||||
AdGroup adGroup,
|
||||
long periodStartUs,
|
||||
long periodDurationUs,
|
||||
boolean isLiveStream) {
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(checkNotNull(adsId), /* adGroupTimesUs...= */ 0)
|
||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, true)
|
||||
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, adGroup.contentResumeOffsetUs);
|
||||
long periodEndUs = periodStartUs + periodDurationUs;
|
||||
long adDurationsUs = 0;
|
||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1);
|
||||
if (isLiveStream) {
|
||||
adPlaybackState = adPlaybackState.withLivePostrollPlaceholderAppended();
|
||||
}
|
||||
long adGroupDurationUs = 0;
|
||||
for (int i = 0; i < adGroup.count; i++) {
|
||||
adDurationsUs += adGroup.durationsUs[i];
|
||||
if (periodEndUs <= adGroup.timeUs + adDurationsUs + 10_000) {
|
||||
long sanitizedDurationUs =
|
||||
periodDurationUs != C.TIME_UNSET ? periodDurationUs : adGroup.durationsUs[i];
|
||||
long periodEndUs = periodStartUs + sanitizedDurationUs;
|
||||
adGroupDurationUs += adGroup.durationsUs[i];
|
||||
// TODO(bachinger): Remove margin constant by making sure the VOD ad group times are adjusted
|
||||
// to the actual DASH timeline periods.
|
||||
if (periodEndUs <= adGroup.timeUs + adGroupDurationUs + 10_000) {
|
||||
adPlaybackState =
|
||||
adPlaybackState
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, sanitizedDurationUs)
|
||||
.withContentResumeOffsetUs(
|
||||
/* adGroupIndex= */ 0, isLiveStream ? sanitizedDurationUs : 0);
|
||||
// Map the state of the global ad state to the period specific ad state.
|
||||
switch (adGroup.states[i]) {
|
||||
case AdPlaybackState.AD_STATE_PLAYED:
|
||||
@ -495,6 +544,12 @@ import java.util.Set;
|
||||
Timeline.Period period = new Timeline.Period();
|
||||
int periodIndex = 0;
|
||||
long totalElapsedContentDurationUs = 0;
|
||||
Timeline.Window window = contentTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window());
|
||||
if (window.isLive()) {
|
||||
long windowStartTimeUs =
|
||||
getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs);
|
||||
totalElapsedContentDurationUs = windowStartTimeUs - window.positionInFirstPeriodUs;
|
||||
}
|
||||
for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) {
|
||||
int adIndexInAdGroup = 0;
|
||||
AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ i);
|
||||
@ -516,6 +571,8 @@ import java.util.Set;
|
||||
adIndexInAdGroup++;
|
||||
} else {
|
||||
// Period is after the current ad group. Continue with next ad group.
|
||||
totalElapsedContentDurationUs +=
|
||||
min(elapsedAdGroupAdDurationUs, adGroup.contentResumeOffsetUs);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import static androidx.media3.common.AdPlaybackState.AD_STATE_UNAVAILABLE;
|
||||
import static androidx.media3.exoplayer.ima.ImaUtil.addLiveAdBreak;
|
||||
import static androidx.media3.exoplayer.ima.ImaUtil.getAdGroupAndIndexInMultiPeriodWindow;
|
||||
import static androidx.media3.exoplayer.ima.ImaUtil.splitAdGroup;
|
||||
import static androidx.media3.test.utils.FakeMultiPeriodLiveTimeline.AD_PERIOD_DURATION_US;
|
||||
import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US;
|
||||
import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
@ -31,7 +32,10 @@ import android.util.Pair;
|
||||
import androidx.media3.common.AdPlaybackState;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.Timeline;
|
||||
import androidx.media3.common.Timeline.Period;
|
||||
import androidx.media3.common.Timeline.Window;
|
||||
import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil;
|
||||
import androidx.media3.test.utils.FakeMultiPeriodLiveTimeline;
|
||||
import androidx.media3.test.utils.FakeTimeline;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
@ -44,29 +48,13 @@ import org.junit.runner.RunWith;
|
||||
public class ImaUtilTest {
|
||||
|
||||
@Test
|
||||
public void splitAdPlaybackStateForPeriods_emptyTimeline_emptyMapOfAdPlaybackStates() {
|
||||
public void splitAdPlaybackStateForPeriods_emptyTimeline_throwsIllegalArgumentException() {
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(/* adsId= */ "", 0, 20_000, C.TIME_END_OF_SOURCE);
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, Timeline.EMPTY);
|
||||
|
||||
assertThat(adPlaybackStates).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitAdPlaybackStateForPeriods_singlePeriod_doesNotSplit() {
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(/* adsId= */ "", 0, 20_000, C.TIME_END_OF_SOURCE);
|
||||
FakeTimeline singlePeriodTimeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0L));
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, singlePeriodTimeline);
|
||||
|
||||
assertThat(adPlaybackStates).hasSize(1);
|
||||
assertThat(adPlaybackStates).containsEntry(new Pair<>(0L, 0), adPlaybackState);
|
||||
Assert.assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, Timeline.EMPTY));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -562,6 +550,453 @@ public class ImaUtilTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
splitAdPlaybackStateForPeriods_liveAdGroupStartedAndMovedOutOfWindow_splitCorrectly() {
|
||||
long adPeriodDurationUs = AD_PERIOD_DURATION_US;
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(/* adsId= */ "adsId", C.TIME_END_OF_SOURCE)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
|
||||
// First window start time (UNIX epoch): 50_000_000
|
||||
// Period durations: content=30_000_000, ad=10_000_000
|
||||
FakeMultiPeriodLiveTimeline liveTimeline =
|
||||
new FakeMultiPeriodLiveTimeline(
|
||||
/* availabilityStartTimeUs= */ 0,
|
||||
/* liveWindowDurationUs= */ 100_000_000,
|
||||
/* nowUs= */ 150_000_000,
|
||||
/* adSequencePattern= */ new boolean[] {false, true, true},
|
||||
/* isContentTimeline= */ true,
|
||||
/* populateAds= */ false);
|
||||
// Ad event received from SDK around 130s.
|
||||
adPlaybackState =
|
||||
addLiveAdBreak(
|
||||
/* currentContentPeriodPositionUs= */ 130_000_000,
|
||||
adPeriodDurationUs,
|
||||
/* adPositionInAdPod= */ 1,
|
||||
/* totalAdDurationUs= */ 2 * adPeriodDurationUs,
|
||||
/* totalAdsInAdPod= */ 2,
|
||||
adPlaybackState);
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(6);
|
||||
assertThat(adPlaybackStates).hasSize(6);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(50_000L);
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(0L); // Exact.
|
||||
assertThat(adPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-4[a]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-5[a]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-6[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-7[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2);
|
||||
|
||||
// Move 1us forward to include the first us of the next period.
|
||||
liveTimeline.advanceNowUs(/* durationUs= */ 1L);
|
||||
adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(7);
|
||||
assertThat(adPlaybackStates).hasSize(7);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(50_000L);
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-1L);
|
||||
assertThat(adPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-4[a]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-5[a]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-6[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-7[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1);
|
||||
|
||||
// Move 29_999_999us forward to the last us of the first content period.
|
||||
liveTimeline.advanceNowUs(/* durationUs= */ 29_999_998L);
|
||||
adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(7);
|
||||
assertThat(adPlaybackStates).hasSize(7);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(79_999L);
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-29_999_999L);
|
||||
assertThat(adPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-4[a]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-5[a]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-6[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-7[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1);
|
||||
|
||||
// Move 1us forward to the drop the first content period at the beginning of the window.
|
||||
liveTimeline.advanceNowUs(/* durationUs= */ 1L);
|
||||
adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(6);
|
||||
assertThat(adPlaybackStates).hasSize(6);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(80_000L);
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(0L); // Exact.
|
||||
assertThat(adPlaybackStates.get("uid-4[a]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-5[a]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-6[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-7[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1);
|
||||
|
||||
// Move 1us forward to add the next ad period at the end of the window.
|
||||
liveTimeline.advanceNowUs(/* durationUs= */ 1L);
|
||||
adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(7);
|
||||
assertThat(adPlaybackStates).hasSize(7);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(80_000L);
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-1L);
|
||||
assertThat(adPlaybackStates.get("uid-4[a]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-5[a]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-6[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-7[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-10[a]").adGroupCount).isEqualTo(1);
|
||||
|
||||
// Mark previous ad group as played.
|
||||
Pair<Integer, Integer> adGroupAndAdIndex =
|
||||
ImaUtil.getAdGroupAndIndexInMultiPeriodWindow(
|
||||
/* adPeriodIndex= */ 3, adPlaybackState, liveTimeline);
|
||||
adPlaybackState =
|
||||
adPlaybackState.withPlayedAd(
|
||||
/* adGroupIndex= */ adGroupAndAdIndex.first,
|
||||
/* adIndexInAdGroup= */ adGroupAndAdIndex.second);
|
||||
adGroupAndAdIndex =
|
||||
ImaUtil.getAdGroupAndIndexInMultiPeriodWindow(
|
||||
/* adPeriodIndex= */ 4, adPlaybackState, liveTimeline);
|
||||
adPlaybackState =
|
||||
adPlaybackState.withPlayedAd(
|
||||
/* adGroupIndex= */ adGroupAndAdIndex.first,
|
||||
/* adIndexInAdGroup= */ adGroupAndAdIndex.second);
|
||||
adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
AdPlaybackState.AdGroup adGroup =
|
||||
adPlaybackStates.get("uid-7[a]").getAdGroup(/* adGroupIndex= */ 0);
|
||||
assertThat(adGroup.getFirstAdIndexToPlay()).isEqualTo(0);
|
||||
assertThat(adGroup.states[0]).isEqualTo(AD_STATE_PLAYED);
|
||||
adGroup = adPlaybackStates.get("uid-8[a]").getAdGroup(/* adGroupIndex= */ 0);
|
||||
assertThat(adGroup.getFirstAdIndexToPlay()).isEqualTo(0);
|
||||
assertThat(adGroup.states[0]).isEqualTo(AD_STATE_PLAYED);
|
||||
|
||||
// Move 9_999_998us forward to the last us of the first ad period. Same periods, shifted.
|
||||
liveTimeline.advanceNowUs(/* durationUs= */ 9_999_998L);
|
||||
adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(7);
|
||||
assertThat(adPlaybackStates).hasSize(7);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(89_999L);
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-9_999_999L);
|
||||
assertThat(adPlaybackStates.get("uid-4[a]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-5[a]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-6[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-7[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-10[a]").adGroupCount).isEqualTo(1);
|
||||
|
||||
// Ad event received from SDK around 180s.
|
||||
adPlaybackState =
|
||||
addLiveAdBreak(
|
||||
/* currentContentPeriodPositionUs= */ 180_000_000,
|
||||
adPeriodDurationUs,
|
||||
/* adPositionInAdPod= */ 1,
|
||||
/* totalAdDurationUs= */ 2 * adPeriodDurationUs,
|
||||
/* totalAdsInAdPod= */ 2,
|
||||
adPlaybackState);
|
||||
adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(7);
|
||||
assertThat(adPlaybackStates).hasSize(7);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(89_999L);
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-9_999_999L);
|
||||
assertThat(adPlaybackStates.get("uid-4[a]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-5[a]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-6[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-7[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-10[a]").adGroupCount).isEqualTo(2);
|
||||
|
||||
// Move 1us forward to drop the first ad from the beginning of the window.
|
||||
liveTimeline.advanceNowUs(/* durationUs= */ 1L);
|
||||
adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(6);
|
||||
assertThat(adPlaybackStates).hasSize(6);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(90_000L);
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(0L); // Exact.
|
||||
assertThat(adPlaybackStates.get("uid-5[a]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-6[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-7[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-10[a]").adGroupCount).isEqualTo(2);
|
||||
|
||||
// Move 1us forward to add the next ad period at the end of the window.
|
||||
liveTimeline.advanceNowUs(/* durationUs= */ 1L);
|
||||
adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(7);
|
||||
assertThat(adPlaybackStates).hasSize(7);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(90_000L);
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-1L);
|
||||
assertThat(adPlaybackStates.get("uid-5[a]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-6[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-7[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-10[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-11[a]").adGroupCount).isEqualTo(2);
|
||||
|
||||
// Move 39_999_999us to drop an ad and a content period at the beginning of the window.
|
||||
liveTimeline.advanceNowUs(/* durationUs= */ 39_999_999L);
|
||||
adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(6);
|
||||
assertThat(adPlaybackStates).hasSize(6);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(130_000L);
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(0L); // Exact.
|
||||
assertThat(adPlaybackStates.get("uid-7[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-10[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-11[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-12[c]").adGroupCount).isEqualTo(1);
|
||||
|
||||
// Move 10_000_000us to drop an ad (incomplete ad group at the beginning of the window).
|
||||
liveTimeline.advanceNowUs(/* durationUs= */ 10_000_000L);
|
||||
adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(6);
|
||||
assertThat(adPlaybackStates).hasSize(6);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(140_000L);
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(0L); // Exact.
|
||||
assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-10[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-11[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-12[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-13[a]").adGroupCount).isEqualTo(1);
|
||||
|
||||
// Mark previous ad group as played.
|
||||
adGroupAndAdIndex =
|
||||
ImaUtil.getAdGroupAndIndexInMultiPeriodWindow(
|
||||
/* adPeriodIndex= */ 2, adPlaybackState, liveTimeline);
|
||||
adPlaybackState =
|
||||
adPlaybackState.withPlayedAd(
|
||||
/* adGroupIndex= */ adGroupAndAdIndex.first,
|
||||
/* adIndexInAdGroup= */ adGroupAndAdIndex.second);
|
||||
adGroupAndAdIndex =
|
||||
ImaUtil.getAdGroupAndIndexInMultiPeriodWindow(
|
||||
/* adPeriodIndex= */ 3, adPlaybackState, liveTimeline);
|
||||
adPlaybackState =
|
||||
adPlaybackState.withPlayedAd(
|
||||
/* adGroupIndex= */ adGroupAndAdIndex.first,
|
||||
/* adIndexInAdGroup= */ adGroupAndAdIndex.second);
|
||||
// Ad event received from SDK around 230s for ad period with unknown duration.
|
||||
adPlaybackState =
|
||||
addLiveAdBreak(
|
||||
/* currentContentPeriodPositionUs= */ 230_000_000,
|
||||
adPeriodDurationUs - 1000L, // SDK fallback duration.
|
||||
/* adPositionInAdPod= */ 1,
|
||||
/* totalAdDurationUs= */ 2 * adPeriodDurationUs - 1000,
|
||||
/* totalAdsInAdPod= */ 2,
|
||||
adPlaybackState);
|
||||
adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(6);
|
||||
assertThat(adPlaybackStates).hasSize(6);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(140_000L);
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(0L); // Exact.
|
||||
assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-10[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-11[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-12[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-13[a]").adGroupCount).isEqualTo(2);
|
||||
AdPlaybackState.AdGroup actualAdGroup =
|
||||
adPlaybackStates.get("uid-13[a]").getAdGroup(/* adGroupIndex= */ 0);
|
||||
assertThat(actualAdGroup.count).isEqualTo(1);
|
||||
assertThat(actualAdGroup.durationsUs[0]).isEqualTo(adPeriodDurationUs - 1000L);
|
||||
|
||||
// Move 1us forward to add the next ad period at the end of the window.
|
||||
liveTimeline.advanceNowUs(/* durationUs= */ 1L);
|
||||
adPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(7);
|
||||
assertThat(adPlaybackStates).hasSize(7);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(140_000L);
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-1L);
|
||||
assertThat(adPlaybackStates.get("uid-8[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-9[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-10[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-11[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(adPlaybackStates.get("uid-12[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get("uid-13[a]").adGroupCount).isEqualTo(2);
|
||||
actualAdGroup = adPlaybackStates.get("uid-13[a]").getAdGroup(/* adGroupIndex= */ 0);
|
||||
assertThat(actualAdGroup.count).isEqualTo(1);
|
||||
assertThat(actualAdGroup.durationsUs[0]).isEqualTo(adPeriodDurationUs);
|
||||
assertThat(adPlaybackStates.get("uid-14[a]").adGroupCount).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
splitAdPlaybackStateForPeriods_fullAdGroupAtBeginOfWindow_adPeriodsCorrectlyDetected() {
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(/* adsId= */ "adsId", C.TIME_END_OF_SOURCE)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
|
||||
// Window start time (UNIX epoch): 29_999_999
|
||||
// Period durations: content=30_000_000, ad=10_000_000
|
||||
FakeMultiPeriodLiveTimeline liveTimeline =
|
||||
new FakeMultiPeriodLiveTimeline(
|
||||
/* availabilityStartTimeUs= */ 0,
|
||||
/* liveWindowDurationUs= */ 30_000_000,
|
||||
/* nowUs= */ 59_999_999,
|
||||
/* adSequencePattern= */ new boolean[] {false, true, true},
|
||||
/* isContentTimeline= */ true,
|
||||
/* populateAds= */ false);
|
||||
// Ad event received from SDK around 30s.
|
||||
adPlaybackState =
|
||||
addLiveAdBreak(
|
||||
/* currentContentPeriodPositionUs= */ 30_000_000,
|
||||
AD_PERIOD_DURATION_US,
|
||||
/* adPositionInAdPod= */ 1,
|
||||
/* totalAdDurationUs= */ 2 * AD_PERIOD_DURATION_US,
|
||||
/* totalAdsInAdPod= */ 2,
|
||||
adPlaybackState);
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> splitAdPlaybackStates =
|
||||
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(4);
|
||||
assertThat(splitAdPlaybackStates).hasSize(4);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(29_999L);
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-29_999_999L);
|
||||
assertThat(splitAdPlaybackStates.get("uid-0[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(splitAdPlaybackStates.get("uid-1[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(splitAdPlaybackStates.get("uid-2[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(splitAdPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1);
|
||||
|
||||
// Move window start to the first microsecond of the first ad period.
|
||||
liveTimeline.advanceNowUs(/* durationUs= */ 1L);
|
||||
splitAdPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(3);
|
||||
assertThat(splitAdPlaybackStates).hasSize(3);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(30_000L);
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(0L);
|
||||
assertThat(splitAdPlaybackStates.get("uid-1[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(splitAdPlaybackStates.get("uid-2[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(splitAdPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1);
|
||||
|
||||
// Move window start to the last microsecond of the first ad period.
|
||||
liveTimeline.advanceNowUs(/* durationUs= */ 9_999_999L);
|
||||
splitAdPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(3);
|
||||
assertThat(splitAdPlaybackStates).hasSize(3);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(39_999L);
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-9_999_999L);
|
||||
assertThat(splitAdPlaybackStates.get("uid-1[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(splitAdPlaybackStates.get("uid-2[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(splitAdPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1);
|
||||
|
||||
// Mark previous ad group as played.
|
||||
adPlaybackState =
|
||||
adPlaybackState
|
||||
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
|
||||
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1);
|
||||
|
||||
// Move first ad period out of live window.
|
||||
liveTimeline.advanceNowUs(/* durationUs= */ 1L);
|
||||
splitAdPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(2);
|
||||
assertThat(splitAdPlaybackStates).hasSize(2);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(40_000L);
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(0L);
|
||||
assertThat(splitAdPlaybackStates.get("uid-2[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(splitAdPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1);
|
||||
|
||||
// Move window start to the last microsecond of the second ad period. Same periods, shifted.
|
||||
liveTimeline.advanceNowUs(/* durationUs= */ 9_999_999L);
|
||||
splitAdPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(2);
|
||||
assertThat(splitAdPlaybackStates).hasSize(2);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(50_000L - 1L);
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-9_999_999L);
|
||||
assertThat(splitAdPlaybackStates.get("uid-2[a]").adGroupCount).isEqualTo(2);
|
||||
assertThat(splitAdPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1);
|
||||
|
||||
// Move second ad period out of live window. Only a single content period in the window.
|
||||
liveTimeline.advanceNowUs(/* durationUs= */ 1L);
|
||||
splitAdPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(1);
|
||||
assertThat(splitAdPlaybackStates).hasSize(1);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(50_000L);
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(0);
|
||||
assertThat(splitAdPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1);
|
||||
|
||||
// Move window start 1 microsecond to require 1us microsecond of the next period.
|
||||
// Note: The ad period is now the last in the window with a duration of TIME_UNSET. Also, the ad
|
||||
// playback state doesn't know yet that the period is an ad.
|
||||
liveTimeline.advanceNowUs(/* durationUs= */ 1L);
|
||||
splitAdPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(2);
|
||||
assertThat(splitAdPlaybackStates).hasSize(2);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(50_000L);
|
||||
// TODO(bachinger): Rounding inaccuracies of 1us because windowStartTimeMs is in milliseconds.
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-1L);
|
||||
assertThat(splitAdPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(splitAdPlaybackStates.get("uid-4[a]").adGroupCount).isEqualTo(1);
|
||||
|
||||
// The ad break arrives that tells the ad playback state about the ad in the timeline. We assert
|
||||
// that the same timeline now gets the period marked as an ad expected.
|
||||
adPlaybackState =
|
||||
addLiveAdBreak(
|
||||
/* currentContentPeriodPositionUs= */ 80_000_000,
|
||||
AD_PERIOD_DURATION_US - 1000L, // SDK fallback duration.
|
||||
/* adPositionInAdPod= */ 1,
|
||||
/* totalAdDurationUs= */ 2 * AD_PERIOD_DURATION_US - 1001L,
|
||||
/* totalAdsInAdPod= */ 2,
|
||||
adPlaybackState);
|
||||
splitAdPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(2);
|
||||
assertThat(splitAdPlaybackStates).hasSize(2);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(50_000L);
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-1L);
|
||||
assertThat(splitAdPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(splitAdPlaybackStates.get("uid-4[a]").adGroupCount).isEqualTo(2);
|
||||
AdPlaybackState.AdGroup actualAdGroup =
|
||||
splitAdPlaybackStates.get("uid-4[a]").getAdGroup(/* adGroupIndex= */ 0);
|
||||
assertThat(actualAdGroup.count).isEqualTo(1);
|
||||
assertThat(actualAdGroup.durationsUs[0]).isEqualTo(AD_PERIOD_DURATION_US - 1000L);
|
||||
|
||||
// Advance to make the window overlap 1 microsecond into the second ad period. Assert whether
|
||||
// both ad periods, including the last with unknown duration, are correctly marked as ad.
|
||||
liveTimeline.advanceNowUs(10_000_000L);
|
||||
splitAdPlaybackStates = ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, liveTimeline);
|
||||
|
||||
assertThat(liveTimeline.getPeriodCount()).isEqualTo(3);
|
||||
assertThat(splitAdPlaybackStates).hasSize(3);
|
||||
assertThat(liveTimeline.getWindow(0, new Window()).windowStartTimeMs).isEqualTo(60_000L);
|
||||
assertThat(liveTimeline.getPeriod(0, new Period()).positionInWindowUs).isEqualTo(-10_000_001L);
|
||||
assertThat(splitAdPlaybackStates.get("uid-3[c]").adGroupCount).isEqualTo(1);
|
||||
assertThat(splitAdPlaybackStates.get("uid-4[a]").adGroupCount).isEqualTo(2);
|
||||
actualAdGroup = splitAdPlaybackStates.get("uid-4[a]").getAdGroup(/* adGroupIndex= */ 0);
|
||||
assertThat(actualAdGroup.count).isEqualTo(1);
|
||||
assertThat(actualAdGroup.durationsUs[0]).isEqualTo(AD_PERIOD_DURATION_US);
|
||||
assertThat(splitAdPlaybackStates.get("uid-5[a]").adGroupCount).isEqualTo(2);
|
||||
actualAdGroup = splitAdPlaybackStates.get("uid-5[a]").getAdGroup(/* adGroupIndex= */ 0);
|
||||
assertThat(actualAdGroup.count).isEqualTo(1);
|
||||
assertThat(actualAdGroup.durationsUs[0]).isEqualTo(AD_PERIOD_DURATION_US - 1L); // SDK fallback.
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expandAdGroupPlaceHolder_expandWithFirstAdInGroup_correctExpansion() {
|
||||
AdPlaybackState adPlaybackState =
|
||||
@ -851,6 +1286,80 @@ public class ImaUtilTest {
|
||||
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
getAdGroupAndIndexInMultiPeriodWindow_liveWindow_correctAdGroupIndexAndAdIndexInAdGroup() {
|
||||
FakeMultiPeriodLiveTimeline contentTimeline =
|
||||
new FakeMultiPeriodLiveTimeline(
|
||||
/* availabilityStartTimeUs= */ 0,
|
||||
/* liveWindowDurationUs= */ 100_000_000,
|
||||
/* nowUs= */ 150_000_000,
|
||||
/* adSequencePattern= */ new boolean[] {false, true, true},
|
||||
/* isContentTimeline= */ true,
|
||||
/* populateAds= */ false);
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended();
|
||||
adPlaybackState =
|
||||
addLiveAdBreak(
|
||||
/* currentContentPeriodPositionUs= */ 80_000_000,
|
||||
/* adDurationUs= */ AD_PERIOD_DURATION_US,
|
||||
/* adPositionInAdPod= */ 1,
|
||||
/* totalAdDurationUs= */ AD_PERIOD_DURATION_US,
|
||||
/* totalAdsInAdPod= */ 1,
|
||||
adPlaybackState);
|
||||
adPlaybackState =
|
||||
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
|
||||
adPlaybackState =
|
||||
addLiveAdBreak(
|
||||
/* currentContentPeriodPositionUs= */ 90_000_000,
|
||||
/* adDurationUs= */ AD_PERIOD_DURATION_US,
|
||||
/* adPositionInAdPod= */ 1,
|
||||
/* totalAdDurationUs= */ AD_PERIOD_DURATION_US,
|
||||
/* totalAdsInAdPod= */ 1,
|
||||
adPlaybackState);
|
||||
adPlaybackState =
|
||||
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0);
|
||||
adPlaybackState =
|
||||
addLiveAdBreak(
|
||||
/* currentContentPeriodPositionUs= */ 130_000_000,
|
||||
/* adDurationUs= */ AD_PERIOD_DURATION_US,
|
||||
/* adPositionInAdPod= */ 1,
|
||||
/* totalAdDurationUs= */ 2 * AD_PERIOD_DURATION_US,
|
||||
/* totalAdsInAdPod= */ 2,
|
||||
adPlaybackState);
|
||||
adPlaybackState =
|
||||
addLiveAdBreak(
|
||||
/* currentContentPeriodPositionUs= */ 130_000_000,
|
||||
/* adDurationUs= */ AD_PERIOD_DURATION_US,
|
||||
/* adPositionInAdPod= */ 2,
|
||||
/* totalAdDurationUs= */ 2 * AD_PERIOD_DURATION_US,
|
||||
/* totalAdsInAdPod= */ 2,
|
||||
adPlaybackState);
|
||||
AdPlaybackState finalAdPlaybackState = adPlaybackState;
|
||||
|
||||
assertThat(
|
||||
getAdGroupAndIndexInMultiPeriodWindow(
|
||||
/* adPeriodIndex= */ 1, adPlaybackState, contentTimeline))
|
||||
.isEqualTo(new Pair<>(0, 0));
|
||||
assertThat(
|
||||
getAdGroupAndIndexInMultiPeriodWindow(
|
||||
/* adPeriodIndex= */ 2, adPlaybackState, contentTimeline))
|
||||
.isEqualTo(new Pair<>(1, 0));
|
||||
assertThat(
|
||||
getAdGroupAndIndexInMultiPeriodWindow(
|
||||
/* adPeriodIndex= */ 4, adPlaybackState, contentTimeline))
|
||||
.isEqualTo(new Pair<>(2, 0));
|
||||
assertThat(
|
||||
getAdGroupAndIndexInMultiPeriodWindow(
|
||||
/* adPeriodIndex= */ 5, adPlaybackState, contentTimeline))
|
||||
.isEqualTo(new Pair<>(2, 1));
|
||||
Assert.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() ->
|
||||
getAdGroupAndIndexInMultiPeriodWindow(
|
||||
/* adPeriodIndex= */ 0, finalAdPlaybackState, contentTimeline));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addLiveAdBreak_threeAdsHappyPath_createsNewAdGroupAndPropagates() {
|
||||
AdPlaybackState adPlaybackState =
|
||||
|
Loading…
x
Reference in New Issue
Block a user