Commit 0767cb97 by bachinger Committed by Marc Baechinger

Add live ad breaks for DASH multi-period streams

This includes:

- Add an ad for each LOADED event of the SDK by taking the duration
  of the ad from the media structure to exactly match the start position
  of ads and then use `addLiveAdBreak()` that is used for HLS live already.
- When the refreshed content timeline arrives, possibly correct
  the duration of an ad that has been inserted while the period duration was
  still unknown (last period of the live timeline).
- When an ad period is removed the ad group needs to be put into a condition
  that allows continuing playback.

PiperOrigin-RevId: 520919236
parent b18fb368
...@@ -15,9 +15,15 @@ ...@@ -15,9 +15,15 @@
*/ */
package com.google.android.exoplayer2.ext.ima; package com.google.android.exoplayer2.ext.ima;
import static com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType.LOADED;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.addLiveAdBreak; import static com.google.android.exoplayer2.ext.ima.ImaUtil.addLiveAdBreak;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.expandAdGroupPlaceholder; import static com.google.android.exoplayer2.ext.ima.ImaUtil.expandAdGroupPlaceholder;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexInMultiPeriodWindow; import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexInLiveMultiPeriodTimeline;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexInVodMultiPeriodTimeline;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupDurationUsForLiveAdPeriodIndex;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.getWindowStartTimeUs;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.handleAdPeriodRemovedFromTimeline;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.maybeCorrectPreviouslyUnknownAdDuration;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.secToMsRounded; import static com.google.android.exoplayer2.ext.ima.ImaUtil.secToMsRounded;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.secToUsRounded; import static com.google.android.exoplayer2.ext.ima.ImaUtil.secToUsRounded;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.splitAdGroup; import static com.google.android.exoplayer2.ext.ima.ImaUtil.splitAdGroup;
...@@ -545,7 +551,9 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou ...@@ -545,7 +551,9 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
componentListener = componentListener =
new ComponentListener( new ComponentListener(
isLiveStream isLiveStream
? (isDashStream ? new NoopAdEventListener() : new SinglePeriodLiveAdEventListener()) ? (isDashStream
? new MultiPeriodLiveAdEventListener()
: new SinglePeriodLiveAdEventListener())
: new VodAdEventListener()); : new VodAdEventListener());
adPlaybackState = adsLoader.getAdPlaybackState(adsId); adPlaybackState = adsLoader.getAdPlaybackState(adsId);
} }
...@@ -677,6 +685,12 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou ...@@ -677,6 +685,12 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
if (contentTimeline.equals(this.contentTimeline)) { if (contentTimeline.equals(this.contentTimeline)) {
return; return;
} }
if (isLiveStream && Objects.equals(streamRequest.getFormat(), StreamFormat.DASH)) {
// 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);
}
this.contentTimeline = contentTimeline; this.contentTimeline = contentTimeline;
invalidateServerSideAdInsertionAdPlaybackState(); invalidateServerSideAdInsertionAdPlaybackState();
} }
...@@ -685,7 +699,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou ...@@ -685,7 +699,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
private void invalidateServerSideAdInsertionAdPlaybackState() { private void invalidateServerSideAdInsertionAdPlaybackState() {
if (!adPlaybackState.equals(AdPlaybackState.NONE) && contentTimeline != null) { if (!adPlaybackState.equals(AdPlaybackState.NONE) && contentTimeline != null) {
ImmutableMap<Object, AdPlaybackState> splitAdPlaybackStates; ImmutableMap<Object, AdPlaybackState> splitAdPlaybackStates;
if (streamRequest.getFormat() == StreamFormat.DASH) { if (Objects.equals(streamRequest.getFormat(), StreamFormat.DASH)) {
// DASH ad groups are always split by period. // DASH ad groups are always split by period.
splitAdPlaybackStates = splitAdPlaybackStateForPeriods(adPlaybackState, contentTimeline); splitAdPlaybackStates = splitAdPlaybackStateForPeriods(adPlaybackState, contentTimeline);
} else { } else {
...@@ -815,8 +829,9 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou ...@@ -815,8 +829,9 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
Player.PositionInfo oldPosition, Player.PositionInfo oldPosition,
Player.PositionInfo newPosition, Player.PositionInfo newPosition,
@Player.DiscontinuityReason int reason) { @Player.DiscontinuityReason int reason) {
if (reason != Player.DISCONTINUITY_REASON_AUTO_TRANSITION) { if (!(reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION
// Only auto transitions within the same or to the next media item are of interest. || (isLiveStream && reason == Player.DISCONTINUITY_REASON_REMOVE))) {
// Only auto transitions and removals of an ad period in live streams need to be handled.
return; return;
} }
...@@ -843,12 +858,24 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou ...@@ -843,12 +858,24 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
Timeline.Window window = Timeline.Window window =
timeline.getWindow(oldPosition.mediaItemIndex, new Timeline.Window()); timeline.getWindow(oldPosition.mediaItemIndex, new Timeline.Window());
if (window.lastPeriodIndex > window.firstPeriodIndex) { if (window.lastPeriodIndex > window.firstPeriodIndex) {
if (reason == Player.DISCONTINUITY_REASON_REMOVE) {
setAdPlaybackState(
handleAdPeriodRemovedFromTimeline(
player.getCurrentPeriodIndex(), timeline, adPlaybackState));
return;
}
// Map adGroupIndex and adIndexInAdGroup to multi-period window. // Map adGroupIndex and adIndexInAdGroup to multi-period window.
Pair<Integer, Integer> adGroupIndexAndAdIndexInAdGroup = Pair<Integer, Integer> adGroupIndexAndAdIndexInAdGroup =
getAdGroupAndIndexInMultiPeriodWindow( window.isLive()
oldPosition.periodIndex - window.firstPeriodIndex, ? getAdGroupAndIndexInLiveMultiPeriodTimeline(
adPlaybackState, oldPosition.mediaItemIndex,
checkNotNull(contentTimeline)); oldPosition.periodIndex - window.firstPeriodIndex,
timeline,
adPlaybackState)
: getAdGroupAndIndexInVodMultiPeriodTimeline(
oldPosition.periodIndex - window.firstPeriodIndex,
adPlaybackState,
checkNotNull(contentTimeline));
adGroupIndex = adGroupIndexAndAdIndexInAdGroup.first; adGroupIndex = adGroupIndexAndAdIndexInAdGroup.first;
adIndexInAdGroup = adGroupIndexAndAdIndexInAdGroup.second; adIndexInAdGroup = adGroupIndexAndAdIndexInAdGroup.second;
} }
...@@ -926,9 +953,9 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou ...@@ -926,9 +953,9 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
@Override @Override
public boolean onAdPlaybackStateUpdateRequested(Timeline contentTimeline) { public boolean onAdPlaybackStateUpdateRequested(Timeline contentTimeline) {
mainHandler.post(() -> setContentTimeline(contentTimeline)); mainHandler.post(() -> setContentTimeline(contentTimeline));
// Defer source refresh to ad playback state update for VOD. Refresh immediately when live // Defer source refresh to ad playback state update for VOD (wait for potential ad cue points)
// with single period. // or DASH (split manifest).
return !isLiveStream || contentTimeline.getPeriodCount() > 1; return !isLiveStream || Objects.equals(streamRequest.getFormat(), StreamFormat.DASH);
} }
} }
...@@ -1368,7 +1395,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou ...@@ -1368,7 +1395,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
private class SinglePeriodLiveAdEventListener implements AdEventListener { private class SinglePeriodLiveAdEventListener implements AdEventListener {
@Override @Override
public void onAdEvent(AdEvent event) { public void onAdEvent(AdEvent event) {
if (event.getType() != AdEvent.AdEventType.LOADED) { if (!Objects.equals(event.getType(), LOADED)) {
return; return;
} }
AdPlaybackState newAdPlaybackState = adPlaybackState; AdPlaybackState newAdPlaybackState = adPlaybackState;
...@@ -1396,14 +1423,40 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou ...@@ -1396,14 +1423,40 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
} }
} }
private static class NoopAdEventListener implements AdEventListener { private class MultiPeriodLiveAdEventListener implements AdEventListener {
@Override @Override
public void onAdEvent(AdEvent event) { public void onAdEvent(AdEvent event) {
Log.w( if (!Objects.equals(event.getType(), LOADED)) {
"ImaSSAIMediaSource", return;
String.format( }
"Ignoring IMA ad event %s because the current stream type is not supported.", AdPodInfo adPodInfo = event.getAd().getAdPodInfo();
event.getType().name())); Timeline timeline = player.getCurrentTimeline();
Timeline.Window window = new Timeline.Window();
Timeline.Period adPeriod = new Timeline.Period();
// In case all periods are in the live window, we need the correct ad group duration when
// inserting the first ad. Try calculate ad group duration from media structure.
long totalAdDurationUs =
getAdGroupDurationUsForLiveAdPeriodIndex(
timeline,
adPodInfo,
/* adPeriodIndex= */ player.getCurrentPeriodIndex(),
window,
adPeriod);
long adPeriodStartTimeUs =
getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs)
+ adPeriod.positionInWindowUs;
long adDurationUs =
adPeriod.durationUs != C.TIME_UNSET
? adPeriod.durationUs
: secToUsRounded(event.getAd().getDuration());
setAdPlaybackState(
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ adPeriodStartTimeUs,
adDurationUs,
adPodInfo.getAdPosition(),
totalAdDurationUs,
adPodInfo.getTotalAds(),
adPlaybackState));
} }
} }
} }
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.ima; package com.google.android.exoplayer2.ext.ima;
import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_AVAILABLE; import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_AVAILABLE;
import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_PLAYED;
import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_UNAVAILABLE; import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_UNAVAILABLE;
import static com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState; import static com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState;
import static com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil.getMediaPeriodPositionUsForContent; import static com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil.getMediaPeriodPositionUsForContent;
...@@ -38,6 +39,7 @@ import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; ...@@ -38,6 +39,7 @@ 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;
import com.google.ads.interactivemedia.v3.api.AdEvent; import com.google.ads.interactivemedia.v3.api.AdEvent;
import com.google.ads.interactivemedia.v3.api.AdPodInfo;
import com.google.ads.interactivemedia.v3.api.AdsLoader; import com.google.ads.interactivemedia.v3.api.AdsLoader;
import com.google.ads.interactivemedia.v3.api.AdsManager; import com.google.ads.interactivemedia.v3.api.AdsManager;
import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
...@@ -380,7 +382,7 @@ import java.util.Set; ...@@ -380,7 +382,7 @@ import java.util.Set;
* @param positionInFirstPeriodUs The position of the window in the first period. * @param positionInFirstPeriodUs The position of the window in the first period.
* @return The window start time, in microseconds. * @return The window start time, in microseconds.
*/ */
private static long getWindowStartTimeUs(long windowStartTimeMs, long positionInFirstPeriodUs) { public static long getWindowStartTimeUs(long windowStartTimeMs, long positionInFirstPeriodUs) {
// Revert us/ms truncation introduced in `DashMediaSource.DashTimeline`. // Revert us/ms truncation introduced in `DashMediaSource.DashTimeline`.
return msToUs(windowStartTimeMs) + (positionInFirstPeriodUs % 1000); return msToUs(windowStartTimeMs) + (positionInFirstPeriodUs % 1000);
} }
...@@ -508,6 +510,10 @@ import java.util.Set; ...@@ -508,6 +510,10 @@ import java.util.Set;
/* adGroupIndex= */ 0, isLiveStream ? sanitizedDurationUs : 0); /* adGroupIndex= */ 0, isLiveStream ? sanitizedDurationUs : 0);
// Map the state of the global ad state to the period specific ad state. // Map the state of the global ad state to the period specific ad state.
switch (adGroup.states[i]) { switch (adGroup.states[i]) {
case AD_STATE_AVAILABLE:
adPlaybackState =
adPlaybackState.withAvailableAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
break;
case AdPlaybackState.AD_STATE_PLAYED: case AdPlaybackState.AD_STATE_PLAYED:
adPlaybackState = adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
...@@ -531,31 +537,298 @@ import java.util.Set; ...@@ -531,31 +537,298 @@ import java.util.Set;
} }
/** /**
* Updates a previously estimated ad duration with the period duration from the timeline.
*
* <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
* ad duration was used which can be corrected as soon as the {@code contentTimeline} was
* refreshed with the actual period duration.
*
* <p>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.
*
* @param contentTimeline The live content timeline.
* @param adPlaybackState The ad playback state.
* @return The (potentially) updated ad playback state.
*/
public static AdPlaybackState maybeCorrectPreviouslyUnknownAdDuration(
Timeline contentTimeline, AdPlaybackState adPlaybackState) {
Timeline.Window window = contentTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window());
if (window.firstPeriodIndex == window.lastPeriodIndex || adPlaybackState.adGroupCount < 2) {
// Single period with unknown duration, or only a postroll placeholder.
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--;
}
// Search for an ad group at or before the period start.
long windowStartTimeUs =
getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs);
long periodStartTimeUs = windowStartTimeUs + period.positionInWindowUs;
int adGroupIndex =
adPlaybackState.getAdGroupIndexForPositionUs(
periodStartTimeUs, /* periodDurationUs= */ C.TIME_UNSET);
if (adGroupIndex == C.INDEX_UNSET) {
// No ad group at or before the period start.
return adPlaybackState;
}
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
if (adGroup.timeUs + adGroup.contentResumeOffsetUs < periodStartTimeUs) {
// Ad group ends before the period starts.
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.
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);
}
// Return unchanged.
return adPlaybackState;
}
/**
* Returns the sum of the durations of all the ads in an ad pod, in microseconds.
*
* <p>If all periods are contained in the current window and have a known duration, the sum of the
* period durations is returned. If this is not possible, {@link AdPodInfo#getMaxDuration() the
* SDK provided group duration} is used instead.
*
* <p>Do not use this method with public timelines of VOD multi-period streams that have the ad
* duration subtracted from period durations.
*
* @param timeline The timeline.
* @param adPodInfo The {@link AdPodInfo} from the SDK event.
* @param adPeriodIndex The period index of the period corresponding to the {@linkplain
* AdPodInfo#getAdPosition() ad position in the ad pod info}.
* @param window The window to be populated as the window containing the ad period.
* @param period The period to be populated as the ad period.
* @return The duration of the ad group, in microseconds.
*/
public static long getAdGroupDurationUsForLiveAdPeriodIndex(
Timeline timeline,
AdPodInfo adPodInfo,
int adPeriodIndex,
Timeline.Window window,
Timeline.Period period) {
timeline.getPeriod(adPeriodIndex, period);
timeline.getWindow(period.windowIndex, window);
checkArgument(window.isLive());
int adIndexInAdGroup = adPodInfo.getAdPosition() - 1;
int firstAdPeriodIndex = adPeriodIndex - adIndexInAdGroup;
int lastAdPeriodIndex = adPeriodIndex + (adPodInfo.getTotalAds() - adIndexInAdGroup - 1);
long adGroupDurationUs = C.TIME_UNSET;
if (window.firstPeriodIndex <= firstAdPeriodIndex
&& lastAdPeriodIndex < window.lastPeriodIndex) {
adGroupDurationUs = 0L;
Timeline.Period currentPeriod = new Timeline.Period();
for (int periodIndex = firstAdPeriodIndex; periodIndex <= lastAdPeriodIndex; periodIndex++) {
long durationUs = timeline.getPeriod(periodIndex, currentPeriod).durationUs;
if (durationUs == C.TIME_UNSET) {
adGroupDurationUs = C.TIME_UNSET;
break;
}
adGroupDurationUs += durationUs;
}
}
return adGroupDurationUs != C.TIME_UNSET
? adGroupDurationUs
: secToUsRounded(adPodInfo.getMaxDuration());
}
/**
* Handles the case when an ad period has been removed from the timeline while being played.
*
* <p>This makes sure no unplayed ads are left in the ad playback state after buffering or after
* the player has been paused in a live stream. In such a case periods may be removed from the
* timeline before playback transitions to the next period automatically.
*
* @param currentPeriodIndex The index of the {@linkplain Player#getCurrentPeriodIndex() current
* period}.
* @param timeline The timeline containing the current period.
* @param adPlaybackState The ad playback state to query.
* @return The updated ad playback state.
*/
@CheckResult
public static AdPlaybackState handleAdPeriodRemovedFromTimeline(
int currentPeriodIndex, Timeline timeline, AdPlaybackState adPlaybackState) {
Timeline.Period currentPeriod = timeline.getPeriod(currentPeriodIndex, new Timeline.Period());
Timeline.Window window = timeline.getWindow(currentPeriod.windowIndex, new Timeline.Window());
long currentPeriodStartTimeUs =
getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs)
+ currentPeriod.positionInWindowUs;
int adGroupIndex =
adPlaybackState.getAdGroupIndexForPositionUs(currentPeriodStartTimeUs, C.TIME_UNSET);
if (adGroupIndex != C.INDEX_UNSET) {
AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
if (adGroup.timeUs + adGroup.contentResumeOffsetUs <= currentPeriodStartTimeUs) {
// current period starts after ad group.
return markAdGroupAsPlayed(adGroupIndex, /* preserveDurations= */ true, adPlaybackState);
}
int lastAvailableAdIndex = C.INDEX_UNSET;
long adGroupDurationUs = 0;
for (int i = 0; i < adGroup.states.length; i++) {
int state = adGroup.states[i];
if (state == AD_STATE_AVAILABLE) {
lastAvailableAdIndex = i;
}
if (currentPeriodStartTimeUs <= adGroup.timeUs + adGroupDurationUs) {
if (currentPeriodStartTimeUs == adGroup.timeUs + adGroupDurationUs) {
// Found the ad matching the current period.
if (state == AD_STATE_AVAILABLE || state == AD_STATE_PLAYED) {
return adPlaybackState;
}
if (state == AD_STATE_UNAVAILABLE && lastAvailableAdIndex == i - 1) {
// First unavailable ad in the group.
if (currentPeriod.durationUs == C.TIME_UNSET) {
// All ad data available except the last period. Will be corrected as normal.
return adPlaybackState;
}
// Else we can set the duration in the ad group as if we received the ad event.
adPlaybackState =
updateAdDurationInAdGroup(
adGroupIndex,
/* adIndexInAdGroup= */ i,
/* adDurationUs= */ currentPeriod.durationUs,
adPlaybackState);
adGroup = adPlaybackState.getAdGroup(adGroupIndex);
return adPlaybackState.withContentResumeOffsetUs(
adGroupIndex, sum(adGroup.durationsUs));
}
}
// We can't calculate the index position of the current period within the ad group after
// an ad period has been removed from the timeline. Give up.
adPlaybackState =
markAdGroupAsPlayed(adGroupIndex, /* preserveDurations= */ false, adPlaybackState);
// Future SDK events belonging to the skipped ad group are handled in #addLiveAdBreak()
// like when joining the live stream while an ad is playing and will insert a new separate
// partial ad group.
if (currentPeriod.durationUs != C.TIME_UNSET) {
// Insert the current ad as a single ad group instead of the skipped ad group.
return addLiveAdBreak(
currentPeriodStartTimeUs,
/* adDurationUs= */ currentPeriod.durationUs,
/* adPositionInAdPod= */ 1,
/* totalAdDurationUs= */ currentPeriod.durationUs,
/* totalAdsInAdPod= */ 1,
adPlaybackState);
}
return adPlaybackState;
}
if (state == AD_STATE_AVAILABLE || state == AD_STATE_UNAVAILABLE) {
adPlaybackState = adPlaybackState.withPlayedAd(adGroupIndex, /* adIndexInAdGroup= */ i);
}
adGroupDurationUs += adGroup.durationsUs[i];
}
}
return adPlaybackState;
}
private static AdPlaybackState markAdGroupAsPlayed(
int adGroupIndex, boolean preserveDurations, AdPlaybackState adPlaybackState) {
AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
long[] newDurationsUs = new long[adGroup.durationsUs.length];
for (int adIndex = 0; adIndex < newDurationsUs.length; adIndex++) {
newDurationsUs[adIndex] = preserveDurations ? adGroup.durationsUs[adIndex] : 0;
adPlaybackState = adPlaybackState.withPlayedAd(adGroupIndex, /* adIndexInAdGroup= */ adIndex);
}
return adPlaybackState
.withAdDurationsUs(adGroupIndex, newDurationsUs)
.withContentResumeOffsetUs(adGroupIndex, sum(newDurationsUs));
}
/**
* Returns the {@code adGroupIndex} in the ad playback state corresponding to the given period
* index in the timeline and the {@code adIndexInAdGroup} of the first unplayed ad in that ad
* group.
*
* @param currentMediaItemIndex The {@linkplain Player#getCurrentMediaItemIndex() current media
* item index}.
* @param adPeriodIndex The index of the ad period in the timeline.
* @param timeline The timeline that contains the period for the ad period index.
* @param adPlaybackState The ad playback state that holds the ad group and ad information.
* @return A pair with the ad group index (first) and the ad index in that ad group (second).
* @exception IllegalStateException If no unplayed ad is found before or at the start time of the
* ad period.
*/
public static Pair<Integer, Integer> getAdGroupAndIndexInLiveMultiPeriodTimeline(
int currentMediaItemIndex,
int adPeriodIndex,
Timeline timeline,
AdPlaybackState adPlaybackState) {
Timeline.Window window =
timeline.getWindow(/* windowIndex= */ currentMediaItemIndex, new Timeline.Window());
checkArgument(window.isLive());
Timeline.Period period = new Timeline.Period();
timeline.getPeriod(adPeriodIndex, period, /* setIds= */ true);
long adPeriodStartTimeUs =
getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs)
+ period.positionInWindowUs;
int adGroupIndex =
adPlaybackState.getAdGroupIndexForPositionUs(adPeriodStartTimeUs, C.TIME_UNSET);
if (adGroupIndex != C.INDEX_UNSET) {
AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
for (int k = 0; k < adGroup.states.length; k++) {
if (adGroup.states[k] == AD_STATE_AVAILABLE || adGroup.states[k] == AD_STATE_UNAVAILABLE) {
return new Pair<>(/* adGroupIndex= */ adGroupIndex, /* adIndexInAdGroup= */ k);
}
}
}
throw new IllegalStateException(
String.format(
"No unplayed ad group found before or at the start time us %d of the period with index"
+ " %d",
adPeriodStartTimeUs, adPeriodIndex));
}
/**
* Returns the {@code adGroupIndex} and the {@code adIndexInAdGroup} for the given period index of * Returns the {@code adGroupIndex} and the {@code adIndexInAdGroup} for the given period index of
* an ad period. * an ad period in a VOD multi-period timeline.
* *
* @param adPeriodIndex The period index of the ad period. * @param adPeriodIndex The period index of the ad period.
* @param adPlaybackState The ad playback state that holds the ad group and ad information. * @param adPlaybackState The ad playback state that holds the ad group and ad information.
* @param contentTimeline The timeline that contains the ad period. * @param contentTimeline The timeline that contains the ad period.
* @return A pair with the ad group index (first) and the ad index in that ad group (second). * @return A pair with the ad group index (first) and the ad index in that ad group (second).
*/ */
public static Pair<Integer, Integer> getAdGroupAndIndexInMultiPeriodWindow( public static Pair<Integer, Integer> getAdGroupAndIndexInVodMultiPeriodTimeline(
int adPeriodIndex, AdPlaybackState adPlaybackState, Timeline contentTimeline) { int adPeriodIndex, AdPlaybackState adPlaybackState, Timeline contentTimeline) {
Timeline.Period period = new Timeline.Period(); Timeline.Window window = contentTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window());
checkArgument(contentTimeline.getWindowCount() == 1);
int periodIndex = 0; int periodIndex = 0;
long totalElapsedContentDurationUs = 0; long totalElapsedContentDurationUs = 0;
Timeline.Window window = contentTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window());
if (window.isLive()) { if (window.isLive()) {
long windowStartTimeUs = long windowStartTimeUs =
getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs); getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs);
totalElapsedContentDurationUs = windowStartTimeUs - window.positionInFirstPeriodUs; totalElapsedContentDurationUs = windowStartTimeUs - window.positionInFirstPeriodUs;
} }
Timeline.Period period = new Timeline.Period();
for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) { for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) {
int adIndexInAdGroup = 0; int adIndexInAdGroup = 0;
AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ i); AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ i);
long adGroupDurationUs = sum(adGroup.durationsUs); long adGroupDurationUs = sum(adGroup.durationsUs);
long elapsedAdGroupAdDurationUs = 0; long elapsedAdGroupAdDurationUs = 0;
for (int j = periodIndex; j < contentTimeline.getPeriodCount(); j++) { for (int j = periodIndex; j < min(contentTimeline.getPeriodCount(), adPeriodIndex + 1); j++) {
contentTimeline.getPeriod(j, period, /* setIds= */ true); contentTimeline.getPeriod(j, period, /* setIds= */ true);
if (totalElapsedContentDurationUs < adGroup.timeUs) { if (totalElapsedContentDurationUs < adGroup.timeUs) {
// Period starts before the ad group, so it is a content period. // Period starts before the ad group, so it is a content period.
......
...@@ -16,26 +16,33 @@ ...@@ -16,26 +16,33 @@
package com.google.android.exoplayer2.ext.ima; package com.google.android.exoplayer2.ext.ima;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.addLiveAdBreak; import static com.google.android.exoplayer2.ext.ima.ImaUtil.addLiveAdBreak;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexInMultiPeriodWindow; import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexInLiveMultiPeriodTimeline;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexInVodMultiPeriodTimeline;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.handleAdPeriodRemovedFromTimeline;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.maybeCorrectPreviouslyUnknownAdDuration;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.secToUsRounded;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.splitAdGroup; import static com.google.android.exoplayer2.ext.ima.ImaUtil.splitAdGroup;
import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_AVAILABLE; import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_AVAILABLE;
import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_ERROR; import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_ERROR;
import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_PLAYED; import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_PLAYED;
import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_SKIPPED; import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_SKIPPED;
import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_UNAVAILABLE; import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_UNAVAILABLE;
import static com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState;
import static com.google.android.exoplayer2.testutil.FakeMultiPeriodLiveTimeline.AD_PERIOD_DURATION_US; import static com.google.android.exoplayer2.testutil.FakeMultiPeriodLiveTimeline.AD_PERIOD_DURATION_US;
import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US; import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US;
import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.util.Pair; import android.util.Pair;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.ads.interactivemedia.v3.api.AdPodInfo;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.Timeline.Window;
import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil;
import com.google.android.exoplayer2.testutil.FakeMultiPeriodLiveTimeline; import com.google.android.exoplayer2.testutil.FakeMultiPeriodLiveTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
...@@ -657,14 +664,14 @@ public class ImaUtilTest { ...@@ -657,14 +664,14 @@ public class ImaUtilTest {
// Mark previous ad group as played. // Mark previous ad group as played.
Pair<Integer, Integer> adGroupAndAdIndex = Pair<Integer, Integer> adGroupAndAdIndex =
ImaUtil.getAdGroupAndIndexInMultiPeriodWindow( ImaUtil.getAdGroupAndIndexInVodMultiPeriodTimeline(
/* adPeriodIndex= */ 3, adPlaybackState, liveTimeline); /* adPeriodIndex= */ 3, adPlaybackState, liveTimeline);
adPlaybackState = adPlaybackState =
adPlaybackState.withPlayedAd( adPlaybackState.withPlayedAd(
/* adGroupIndex= */ adGroupAndAdIndex.first, /* adGroupIndex= */ adGroupAndAdIndex.first,
/* adIndexInAdGroup= */ adGroupAndAdIndex.second); /* adIndexInAdGroup= */ adGroupAndAdIndex.second);
adGroupAndAdIndex = adGroupAndAdIndex =
ImaUtil.getAdGroupAndIndexInMultiPeriodWindow( ImaUtil.getAdGroupAndIndexInVodMultiPeriodTimeline(
/* adPeriodIndex= */ 4, adPlaybackState, liveTimeline); /* adPeriodIndex= */ 4, adPlaybackState, liveTimeline);
adPlaybackState = adPlaybackState =
adPlaybackState.withPlayedAd( adPlaybackState.withPlayedAd(
...@@ -782,14 +789,14 @@ public class ImaUtilTest { ...@@ -782,14 +789,14 @@ public class ImaUtilTest {
// Mark previous ad group as played. // Mark previous ad group as played.
adGroupAndAdIndex = adGroupAndAdIndex =
ImaUtil.getAdGroupAndIndexInMultiPeriodWindow( ImaUtil.getAdGroupAndIndexInVodMultiPeriodTimeline(
/* adPeriodIndex= */ 2, adPlaybackState, liveTimeline); /* adPeriodIndex= */ 2, adPlaybackState, liveTimeline);
adPlaybackState = adPlaybackState =
adPlaybackState.withPlayedAd( adPlaybackState.withPlayedAd(
/* adGroupIndex= */ adGroupAndAdIndex.first, /* adGroupIndex= */ adGroupAndAdIndex.first,
/* adIndexInAdGroup= */ adGroupAndAdIndex.second); /* adIndexInAdGroup= */ adGroupAndAdIndex.second);
adGroupAndAdIndex = adGroupAndAdIndex =
ImaUtil.getAdGroupAndIndexInMultiPeriodWindow( ImaUtil.getAdGroupAndIndexInVodMultiPeriodTimeline(
/* adPeriodIndex= */ 3, adPlaybackState, liveTimeline); /* adPeriodIndex= */ 3, adPlaybackState, liveTimeline);
adPlaybackState = adPlaybackState =
adPlaybackState.withPlayedAd( adPlaybackState.withPlayedAd(
...@@ -1002,7 +1009,7 @@ public class ImaUtilTest { ...@@ -1002,7 +1009,7 @@ public class ImaUtilTest {
@Test @Test
public void expandAdGroupPlaceHolder_expandWithFirstAdInGroup_correctExpansion() { public void expandAdGroupPlaceHolder_expandWithFirstAdInGroup_correctExpansion() {
AdPlaybackState adPlaybackState = AdPlaybackState adPlaybackState =
ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
AdPlaybackState.NONE, AdPlaybackState.NONE,
/* fromPositionUs= */ 0, /* fromPositionUs= */ 0,
/* contentResumeOffsetUs= */ 0, /* contentResumeOffsetUs= */ 0,
...@@ -1025,9 +1032,312 @@ public class ImaUtilTest { ...@@ -1025,9 +1032,312 @@ public class ImaUtilTest {
} }
@Test @Test
public void maybeCorrectPreviouslyUnknownAdDuration_singleAdInAdGroup_adDurationCorrected() {
long liveWindowDurationUs = 60_000_000L;
long nowUs = 110_234_567L;
AdPlaybackState adPlaybackState =
new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended();
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ 80_000_000L,
/* contentResumeOffsetUs= */ 123,
/* adDurationsUs...= */ 123);
// Second ad group was inserted when the period duration was unknown and can be corrected with
// the new content timeline.
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ 90_000_000L,
/* contentResumeOffsetUs= */ 123,
/* adDurationsUs...= */ 123);
FakeMultiPeriodLiveTimeline contentTimeline =
new FakeMultiPeriodLiveTimeline(
/* availabilityStartTimeUs= */ 0,
liveWindowDurationUs,
nowUs,
/* adSequencePattern= */ new boolean[] {false, true, true},
/* isContentTimeline= */ true,
/* populateAds= */ false,
/* playedAds= */ false);
adPlaybackState = maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState);
assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).timeUs).isEqualTo(80_000_000L);
assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs)
.asList()
.containsExactly(123L);
assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 1).timeUs).isEqualTo(90_000_000L);
assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 1).durationsUs)
.asList()
.containsExactly(AD_PERIOD_DURATION_US);
assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 1).contentResumeOffsetUs)
.isEqualTo(AD_PERIOD_DURATION_US);
}
@Test
public void maybeCorrectPreviouslyUnknownAdDuration_multipleAdsInAdGroup_adDurationCorrected() {
long liveWindowDurationUs = 60_000_000L;
long nowUs = 110_234_567L;
AdPlaybackState adPlaybackState =
new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended();
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ 80_000_000L,
/* contentResumeOffsetUs= */ 10_000_123L,
/* adDurationsUs...= */ AD_PERIOD_DURATION_US,
123L);
// Content timeline: [content, ad, ad, content]
FakeMultiPeriodLiveTimeline contentTimeline =
new FakeMultiPeriodLiveTimeline(
/* availabilityStartTimeUs= */ 0,
liveWindowDurationUs,
nowUs,
/* adSequencePattern= */ new boolean[] {false, true, true},
/* isContentTimeline= */ true,
/* populateAds= */ false,
/* playedAds= */ false);
adPlaybackState = maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState);
assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).timeUs).isEqualTo(80_000_000L);
assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs)
.asList()
.containsExactly(AD_PERIOD_DURATION_US, AD_PERIOD_DURATION_US);
assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).contentResumeOffsetUs)
.isEqualTo(2 * AD_PERIOD_DURATION_US);
}
@Test
public void
maybeCorrectPreviouslyUnknownAdDuration_windowPastAdGroups_adPlaybackStateNotChanged() {
AdPlaybackState adPlaybackState =
new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended();
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ 80_000_000L,
/* contentResumeOffsetUs= */ 123,
/* adDurationsUs...= */ 123);
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ 90_000_000L,
/* contentResumeOffsetUs= */ 123,
/* adDurationsUs...= */ 123);
FakeMultiPeriodLiveTimeline contentTimeline =
new FakeMultiPeriodLiveTimeline(
/* availabilityStartTimeUs= */ 0,
/* liveWindowDurationUs= */ 60_000_000L,
/* nowUs= */ 160_000_000L,
/* adSequencePattern= */ new boolean[] {false, true, true},
/* isContentTimeline= */ true,
/* populateAds= */ false,
/* playedAds= */ false);
AdPlaybackState correctedAdPlaybackState =
maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState);
assertThat(contentTimeline.getWindow(/* windowIndex= */ 0, new Window()).windowStartTimeMs)
.isEqualTo(100_000L);
assertThat(correctedAdPlaybackState).isSameInstanceAs(adPlaybackState);
}
@Test
public void
maybeCorrectPreviouslyUnknownAdDuration_withInsertionRemainder_preserveRemainderDuration() {
// Content and ad period in window at the beginning: [c, a, a]
FakeMultiPeriodLiveTimeline contentTimeline =
new FakeMultiPeriodLiveTimeline(
/* availabilityStartTimeUs= */ 0,
/* liveWindowDurationUs= */ 50_000_000L,
/* nowUs= */ 50_000_000L,
/* adSequencePattern= */ new boolean[] {false, true, true, true, true},
/* isContentTimeline= */ true,
/* populateAds= */ false,
/* playedAds= */ false);
AdPlaybackState adPlaybackState =
new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended();
// Insert first ad resulting in group [10_000, 20_000, 0]
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 30_000_000,
/* adDurationUs= */ 10_000_000,
/* adPositionInAdPod= */ 1,
/* totalAdDurationUs= */ 40_000_123,
/* totalAdsInAdPod= */ 4,
adPlaybackState);
AdPlaybackState correctedAdPlaybackState =
maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState);
// Assert starting point: 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]
contentTimeline.advanceNowUs(1L);
correctedAdPlaybackState =
maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, correctedAdPlaybackState);
assertThat(correctedAdPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs)
.asList()
.containsExactly(10_000_000L, 10_000_000L, 20_000_123L, 0L)
.inOrder();
// Get next 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);
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, 0, 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.
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,
/* adPositionInAdPod= */ 3,
/* 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, 1, 0);
correctedAdPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 40_000_000,
/* adDurationUs= */ 9_999_999L,
/* adPositionInAdPod= */ 3,
/* totalAdDurationUs= */ 40_000_000,
/* 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)
.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]
contentTimeline.advanceNowUs(10_000_000L);
correctedAdPlaybackState =
maybeCorrectPreviouslyUnknownAdDuration(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_singleContentPeriodTimeline_adPlaybackStateNotChanged() {
FakeMultiPeriodLiveTimeline contentTimeline =
new FakeMultiPeriodLiveTimeline(
/* availabilityStartTimeUs= */ 0,
/* liveWindowDurationUs= */ 30_000_000L,
/* nowUs= */ 80_000_000L,
/* adSequencePattern= */ new boolean[] {false, true, true},
/* isContentTimeline= */ true,
/* populateAds= */ false,
/* playedAds= */ false);
AdPlaybackState adPlaybackState =
new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended();
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ 30_000_000L,
/* contentResumeOffsetUs= */ 123,
/* adDurationsUs...= */ 123);
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ 40_000_000L,
/* contentResumeOffsetUs= */ 123,
/* adDurationsUs...= */ 123);
AdPlaybackState correctedAdPlaybackState =
maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState);
assertThat(correctedAdPlaybackState).isSameInstanceAs(adPlaybackState);
}
@Test
public void
maybeCorrectPreviouslyUnknownAdDuration_singleAdPeriodTimeline_doesNotOverrideWithTimeUnset() {
FakeMultiPeriodLiveTimeline contentTimeline =
new FakeMultiPeriodLiveTimeline(
/* availabilityStartTimeUs= */ 0,
/* liveWindowDurationUs= */ 10_000_000L,
/* nowUs= */ 90_000_000L,
/* adSequencePattern= */ new boolean[] {false, true, true},
/* isContentTimeline= */ true,
/* populateAds= */ false,
/* playedAds= */ false);
AdPlaybackState adPlaybackState =
new AdPlaybackState(/* adsId= */ "adsId").withLivePostrollPlaceholderAppended();
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ 80_000_000L,
/* contentResumeOffsetUs= */ 123L,
/* adDurationsUs...= */ 123L);
adPlaybackState = maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState);
assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).timeUs).isEqualTo(80_000_000L);
assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).durationsUs)
.asList()
.containsExactly(123L);
assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).contentResumeOffsetUs)
.isEqualTo(123L);
}
@Test
public void expandAdGroupPlaceHolder_expandWithMiddleAdInGroup_correctExpansion() { public void expandAdGroupPlaceHolder_expandWithMiddleAdInGroup_correctExpansion() {
AdPlaybackState adPlaybackState = AdPlaybackState adPlaybackState =
ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
AdPlaybackState.NONE, AdPlaybackState.NONE,
/* fromPositionUs= */ 0, /* fromPositionUs= */ 0,
/* contentResumeOffsetUs= */ 0, /* contentResumeOffsetUs= */ 0,
...@@ -1052,7 +1362,7 @@ public class ImaUtilTest { ...@@ -1052,7 +1362,7 @@ public class ImaUtilTest {
@Test @Test
public void expandAdGroupPlaceHolder_expandWithLastAdInGroup_correctDurationWrappedAround() { public void expandAdGroupPlaceHolder_expandWithLastAdInGroup_correctDurationWrappedAround() {
AdPlaybackState adPlaybackState = AdPlaybackState adPlaybackState =
ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
AdPlaybackState.NONE, AdPlaybackState.NONE,
/* fromPositionUs= */ 0, /* fromPositionUs= */ 0,
/* contentResumeOffsetUs= */ 0, /* contentResumeOffsetUs= */ 0,
...@@ -1077,7 +1387,7 @@ public class ImaUtilTest { ...@@ -1077,7 +1387,7 @@ public class ImaUtilTest {
@Test @Test
public void expandAdGroupPlaceHolder_expandSingleAdInAdGroup_noExpansionCorrectDuration() { public void expandAdGroupPlaceHolder_expandSingleAdInAdGroup_noExpansionCorrectDuration() {
AdPlaybackState adPlaybackState = AdPlaybackState adPlaybackState =
ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
AdPlaybackState.NONE, AdPlaybackState.NONE,
/* fromPositionUs= */ 0, /* fromPositionUs= */ 0,
/* contentResumeOffsetUs= */ 0, /* contentResumeOffsetUs= */ 0,
...@@ -1100,7 +1410,7 @@ public class ImaUtilTest { ...@@ -1100,7 +1410,7 @@ public class ImaUtilTest {
@Test @Test
public void expandAdGroupPlaceHolder_singleAdInAdGroupOverLength_correctsAdDuration() { public void expandAdGroupPlaceHolder_singleAdInAdGroupOverLength_correctsAdDuration() {
AdPlaybackState adPlaybackState = AdPlaybackState adPlaybackState =
ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
AdPlaybackState.NONE, AdPlaybackState.NONE,
/* fromPositionUs= */ 0, /* fromPositionUs= */ 0,
/* contentResumeOffsetUs= */ 0, /* contentResumeOffsetUs= */ 0,
...@@ -1123,7 +1433,7 @@ public class ImaUtilTest { ...@@ -1123,7 +1433,7 @@ public class ImaUtilTest {
@Test @Test
public void expandAdGroupPlaceHolder_initialDurationTooLarge_overriddenWhenExpanded() { public void expandAdGroupPlaceHolder_initialDurationTooLarge_overriddenWhenExpanded() {
AdPlaybackState adPlaybackState = AdPlaybackState adPlaybackState =
ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
AdPlaybackState.NONE, AdPlaybackState.NONE,
/* fromPositionUs= */ 0, /* fromPositionUs= */ 0,
/* contentResumeOffsetUs= */ 0, /* contentResumeOffsetUs= */ 0,
...@@ -1147,7 +1457,7 @@ public class ImaUtilTest { ...@@ -1147,7 +1457,7 @@ public class ImaUtilTest {
@Test @Test
public void insertAdDurationInAdGroup_correctDurationAndPropagation() { public void insertAdDurationInAdGroup_correctDurationAndPropagation() {
AdPlaybackState adPlaybackState = AdPlaybackState adPlaybackState =
ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
AdPlaybackState.NONE, AdPlaybackState.NONE,
/* fromPositionUs= */ 0, /* fromPositionUs= */ 0,
/* contentResumeOffsetUs= */ 0, /* contentResumeOffsetUs= */ 0,
...@@ -1172,7 +1482,7 @@ public class ImaUtilTest { ...@@ -1172,7 +1482,7 @@ public class ImaUtilTest {
@Test @Test
public void insertAdDurationInAdGroup_insertLast_correctDurationAndPropagation() { public void insertAdDurationInAdGroup_insertLast_correctDurationAndPropagation() {
AdPlaybackState adPlaybackState = AdPlaybackState adPlaybackState =
ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
AdPlaybackState.NONE, AdPlaybackState.NONE,
/* fromPositionUs= */ 0, /* fromPositionUs= */ 0,
/* contentResumeOffsetUs= */ 0, /* contentResumeOffsetUs= */ 0,
...@@ -1197,7 +1507,7 @@ public class ImaUtilTest { ...@@ -1197,7 +1507,7 @@ public class ImaUtilTest {
@Test @Test
public void insertAdDurationInAdGroup_allDurationsSetAlready_setDurationNoPropagation() { public void insertAdDurationInAdGroup_allDurationsSetAlready_setDurationNoPropagation() {
AdPlaybackState adPlaybackState = AdPlaybackState adPlaybackState =
ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( addAdGroupToAdPlaybackState(
AdPlaybackState.NONE, AdPlaybackState.NONE,
/* fromPositionUs= */ 0, /* fromPositionUs= */ 0,
/* contentResumeOffsetUs= */ 0, /* contentResumeOffsetUs= */ 0,
...@@ -1220,7 +1530,7 @@ public class ImaUtilTest { ...@@ -1220,7 +1530,7 @@ public class ImaUtilTest {
} }
@Test @Test
public void getAdGroupAndIndexInMultiPeriodWindow_correctAdGroupIndexAndAdIndexInAdGroup() { public void getAdGroupAndIndexInVodMultiPeriodTimeline_correctAdGroupIndexAndAdIndexInAdGroup() {
FakeTimeline timeline = FakeTimeline timeline =
new FakeTimeline( new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(/* periodCount= */ 9, new Object())); new FakeTimeline.TimelineWindowDefinition(/* periodCount= */ 9, new Object()));
...@@ -1241,62 +1551,70 @@ public class ImaUtilTest { ...@@ -1241,62 +1551,70 @@ public class ImaUtilTest {
.withIsServerSideInserted(/* adGroupIndex= */ 0, true); .withIsServerSideInserted(/* adGroupIndex= */ 0, true);
Pair<Integer, Integer> adGroupIndexAndAdIndexInAdGroup = Pair<Integer, Integer> adGroupIndexAndAdIndexInAdGroup =
getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 0, adPlaybackState, timeline); getAdGroupAndIndexInVodMultiPeriodTimeline(
/* adPeriodIndex= */ 0, adPlaybackState, timeline);
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(0); assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(0);
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(0); assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(0);
adGroupIndexAndAdIndexInAdGroup = adGroupIndexAndAdIndexInAdGroup =
getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 1, adPlaybackState, timeline); getAdGroupAndIndexInVodMultiPeriodTimeline(
/* adPeriodIndex= */ 1, adPlaybackState, timeline);
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(0); assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(0);
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(1); assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(1);
Assert.assertThrows( Assert.assertThrows(
IllegalStateException.class, IllegalStateException.class,
() -> () ->
getAdGroupAndIndexInMultiPeriodWindow( getAdGroupAndIndexInVodMultiPeriodTimeline(
/* adPeriodIndex= */ 2, adPlaybackState, timeline)); /* adPeriodIndex= */ 2, adPlaybackState, timeline));
adGroupIndexAndAdIndexInAdGroup = adGroupIndexAndAdIndexInAdGroup =
getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 3, adPlaybackState, timeline); getAdGroupAndIndexInVodMultiPeriodTimeline(
/* adPeriodIndex= */ 3, adPlaybackState, timeline);
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(1); assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(1);
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(0); assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(0);
adGroupIndexAndAdIndexInAdGroup = adGroupIndexAndAdIndexInAdGroup =
getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 4, adPlaybackState, timeline); getAdGroupAndIndexInVodMultiPeriodTimeline(
/* adPeriodIndex= */ 4, adPlaybackState, timeline);
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(1); assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(1);
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(1); assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(1);
adGroupIndexAndAdIndexInAdGroup = adGroupIndexAndAdIndexInAdGroup =
getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 5, adPlaybackState, timeline); getAdGroupAndIndexInVodMultiPeriodTimeline(
/* adPeriodIndex= */ 5, adPlaybackState, timeline);
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(1); assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(1);
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(2); assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(2);
Assert.assertThrows( Assert.assertThrows(
IllegalStateException.class, IllegalStateException.class,
() -> () ->
getAdGroupAndIndexInMultiPeriodWindow( getAdGroupAndIndexInVodMultiPeriodTimeline(
/* adPeriodIndex= */ 6, adPlaybackState, timeline)); /* adPeriodIndex= */ 6, adPlaybackState, timeline));
adGroupIndexAndAdIndexInAdGroup = adGroupIndexAndAdIndexInAdGroup =
getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 7, adPlaybackState, timeline); getAdGroupAndIndexInVodMultiPeriodTimeline(
/* adPeriodIndex= */ 7, adPlaybackState, timeline);
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(2); assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(2);
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(0); assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(0);
adGroupIndexAndAdIndexInAdGroup = adGroupIndexAndAdIndexInAdGroup =
getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 8, adPlaybackState, timeline); getAdGroupAndIndexInVodMultiPeriodTimeline(
/* adPeriodIndex= */ 8, adPlaybackState, timeline);
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(2); assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(2);
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(1); assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(1);
} }
@Test @Test
public void public void
getAdGroupAndIndexInMultiPeriodWindow_liveWindow_correctAdGroupIndexAndAdIndexInAdGroup() { getAdGroupAndIndexInLiveMultiPeriodTimeline_calledForPeriodsAfterUnplayedAdGroup_correctAdGroupIndexAndAdIndexInAdGroup() {
// Content live window with content and ad periods: c, [a, c, a, a, a, c, a], a, a
FakeMultiPeriodLiveTimeline contentTimeline = FakeMultiPeriodLiveTimeline contentTimeline =
new FakeMultiPeriodLiveTimeline( new FakeMultiPeriodLiveTimeline(
/* availabilityStartTimeUs= */ 0, /* availabilityStartTimeUs= */ 0,
/* liveWindowDurationUs= */ 100_000_000, /* liveWindowDurationUs= */ 100_000_000,
/* nowUs= */ 150_000_000, /* nowUs= */ 159_000_123,
/* adSequencePattern= */ new boolean[] {false, true, true}, /* adSequencePattern= */ new boolean[] {false, true, true, true},
/* isContentTimeline= */ true, /* isContentTimeline= */ true,
/* populateAds= */ false, /* populateAds= */ false,
/* playedAds= */ false); /* playedAds= */ false);
...@@ -1304,12 +1622,21 @@ public class ImaUtilTest { ...@@ -1304,12 +1622,21 @@ public class ImaUtilTest {
new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended(); new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended();
adPlaybackState = adPlaybackState =
addLiveAdBreak( addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 80_000_000, /* currentContentPeriodPositionUs= */ 50_000_000,
/* adDurationUs= */ AD_PERIOD_DURATION_US, /* adDurationUs= */ AD_PERIOD_DURATION_US,
/* adPositionInAdPod= */ 1, /* adPositionInAdPod= */ 1,
/* totalAdDurationUs= */ AD_PERIOD_DURATION_US, /* totalAdDurationUs= */ AD_PERIOD_DURATION_US,
/* totalAdsInAdPod= */ 1, /* totalAdsInAdPod= */ 1,
adPlaybackState); adPlaybackState);
assertThat(
getAdGroupAndIndexInLiveMultiPeriodTimeline(
/* currentMediaItemIndex= */ 0,
/* adPeriodIndex= */ 0,
contentTimeline,
adPlaybackState))
.isEqualTo(new Pair<>(0, 0));
adPlaybackState = adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
adPlaybackState = adPlaybackState =
...@@ -1317,50 +1644,531 @@ public class ImaUtilTest { ...@@ -1317,50 +1644,531 @@ public class ImaUtilTest {
/* currentContentPeriodPositionUs= */ 90_000_000, /* currentContentPeriodPositionUs= */ 90_000_000,
/* adDurationUs= */ AD_PERIOD_DURATION_US, /* adDurationUs= */ AD_PERIOD_DURATION_US,
/* adPositionInAdPod= */ 1, /* adPositionInAdPod= */ 1,
/* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US,
/* totalAdsInAdPod= */ 3,
adPlaybackState);
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 100_000_000,
/* adDurationUs= */ AD_PERIOD_DURATION_US,
/* adPositionInAdPod= */ 2,
/* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US,
/* totalAdsInAdPod= */ 3,
adPlaybackState);
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 110_000_000,
/* adDurationUs= */ AD_PERIOD_DURATION_US,
/* adPositionInAdPod= */ 3,
/* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US,
/* totalAdsInAdPod= */ 3,
adPlaybackState);
AdPlaybackState finalAdPlaybackState = adPlaybackState;
assertThat(
getAdGroupAndIndexInLiveMultiPeriodTimeline(
/* currentMediaItemIndex= */ 0,
/* adPeriodIndex= */ 2,
contentTimeline,
adPlaybackState))
.isEqualTo(new Pair<>(1, 0));
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0);
assertThat(
getAdGroupAndIndexInLiveMultiPeriodTimeline(
/* currentMediaItemIndex= */ 0,
/* adPeriodIndex= */ 3,
contentTimeline,
adPlaybackState))
.isEqualTo(new Pair<>(1, 1));
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1);
assertThat(
getAdGroupAndIndexInLiveMultiPeriodTimeline(
/* currentMediaItemIndex= */ 0,
/* adPeriodIndex= */ 4,
contentTimeline,
adPlaybackState))
.isEqualTo(new Pair<>(1, 2));
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2);
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 150_000_000,
/* adDurationUs= */ AD_PERIOD_DURATION_US,
/* adPositionInAdPod= */ 2,
/* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US,
/* totalAdsInAdPod= */ 3,
adPlaybackState);
assertThat(
getAdGroupAndIndexInLiveMultiPeriodTimeline(
/* currentMediaItemIndex= */ 0,
/* adPeriodIndex= */ 6,
contentTimeline,
adPlaybackState))
.isEqualTo(new Pair<>(2, 0));
}
@Test
public void
getAdGroupAndIndexInLiveMultiPeriodTimeline_calledForPeriodsBeforeUnplayedAdGroup_throwsWhenCalledForNonAdPeriods() {
// Content live window with content and ad periods: c, [a, c, a, a, a, c, a], a, a
FakeMultiPeriodLiveTimeline contentTimeline =
new FakeMultiPeriodLiveTimeline(
/* availabilityStartTimeUs= */ 0,
/* liveWindowDurationUs= */ 100_000_000,
/* nowUs= */ 159_000_123,
/* adSequencePattern= */ new boolean[] {false, true, true, true},
/* isContentTimeline= */ true,
/* populateAds= */ false,
/* playedAds= */ false);
AdPlaybackState adPlaybackState =
new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended();
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 50_000_000,
/* adDurationUs= */ AD_PERIOD_DURATION_US,
/* adPositionInAdPod= */ 1,
/* totalAdDurationUs= */ AD_PERIOD_DURATION_US, /* totalAdDurationUs= */ AD_PERIOD_DURATION_US,
/* totalAdsInAdPod= */ 1, /* totalAdsInAdPod= */ 1,
adPlaybackState); adPlaybackState);
adPlaybackState = adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 90_000_000,
/* adDurationUs= */ AD_PERIOD_DURATION_US,
/* adPositionInAdPod= */ 1,
/* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US,
/* totalAdsInAdPod= */ 3,
adPlaybackState);
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 100_000_000,
/* adDurationUs= */ AD_PERIOD_DURATION_US,
/* adPositionInAdPod= */ 2,
/* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US,
/* totalAdsInAdPod= */ 3,
adPlaybackState);
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 110_000_000,
/* adDurationUs= */ AD_PERIOD_DURATION_US,
/* adPositionInAdPod= */ 3,
/* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US,
/* totalAdsInAdPod= */ 3,
adPlaybackState);
AdPlaybackState finalAdPlaybackState = adPlaybackState;
Assert.assertThrows(
IllegalStateException.class,
() ->
getAdGroupAndIndexInLiveMultiPeriodTimeline(
/* currentMediaItemIndex= */ 0,
/* adPeriodIndex= */ 1,
contentTimeline,
finalAdPlaybackState));
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0); adPlaybackState.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0);
adPlaybackState = adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1);
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2);
adPlaybackState =
addLiveAdBreak( addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 130_000_000, /* currentContentPeriodPositionUs= */ 150_000_000,
/* adDurationUs= */ AD_PERIOD_DURATION_US,
/* adPositionInAdPod= */ 2,
/* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US,
/* totalAdsInAdPod= */ 3,
adPlaybackState);
AdPlaybackState anotherFinalAdPlaybackState = adPlaybackState;
Assert.assertThrows(
IllegalStateException.class,
() ->
getAdGroupAndIndexInLiveMultiPeriodTimeline(
/* currentMediaItemIndex= */ 0,
/* adPeriodIndex= */ 5,
contentTimeline,
anotherFinalAdPlaybackState));
}
@Test
public void
getAdGroupAndIndexInLiveMultiPeriodTimeline_partialAdGroupAtTimelineStart_correctAdGroupIndexAndAdIndexInAdGroup() {
// Content timeline with content and ad periods: c, a, a, [a, c, a, a, a, c]
FakeMultiPeriodLiveTimeline contentTimeline =
new FakeMultiPeriodLiveTimeline(
/* availabilityStartTimeUs= */ 0,
/* liveWindowDurationUs= */ 100_000_000,
/* nowUs= */ 151_000_123,
/* adSequencePattern= */ new boolean[] {false, true, true, true},
/* isContentTimeline= */ true,
/* populateAds= */ false,
/* playedAds= */ false);
AdPlaybackState adPlaybackState =
new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended();
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 30_000_000,
/* adDurationUs= */ AD_PERIOD_DURATION_US, /* adDurationUs= */ AD_PERIOD_DURATION_US,
/* adPositionInAdPod= */ 1, /* adPositionInAdPod= */ 1,
/* totalAdDurationUs= */ 2 * AD_PERIOD_DURATION_US, /* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US,
/* totalAdsInAdPod= */ 2, /* totalAdsInAdPod= */ 3,
adPlaybackState);
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 40_000_000,
/* adDurationUs= */ AD_PERIOD_DURATION_US,
/* adPositionInAdPod= */ 2,
/* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US,
/* totalAdsInAdPod= */ 3,
adPlaybackState);
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 50_000_000,
/* adDurationUs= */ AD_PERIOD_DURATION_US,
/* adPositionInAdPod= */ 3,
/* totalAdDurationUs= */ 3 * AD_PERIOD_DURATION_US,
/* totalAdsInAdPod= */ 3,
adPlaybackState);
adPlaybackState =
adPlaybackState
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1);
assertThat(
getAdGroupAndIndexInLiveMultiPeriodTimeline(
/* currentMediaItemIndex= */ 0,
/* adPeriodIndex= */ 0,
contentTimeline,
adPlaybackState))
.isEqualTo(new Pair<>(0, 2));
}
@Test
public void
getAdGroupAndIndexInLiveMultiPeriodTimeline_onlyPartialAdGroupInWindow_correctAdGroupIndexAndAdIndexInAdGroup() {
// Content timeline with content and ad periods: c, a, [a, a, a, a], a, c
// First three ad periods of the ad group already outside of the live window.
FakeMultiPeriodLiveTimeline contentTimeline =
new FakeMultiPeriodLiveTimeline(
/* availabilityStartTimeUs= */ 0,
/* liveWindowDurationUs= */ 30_000_000,
/* nowUs= */ 71_000_123,
/* adSequencePattern= */ new boolean[] {false, true, true, true, true, true, true},
/* isContentTimeline= */ true,
/* populateAds= */ false,
/* playedAds= */ false);
AdPlaybackState adPlaybackState =
new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended();
// Ad events of the first two ads of the group have arrived (the first of the window).
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 30_000_000,
/* adDurationUs= */ AD_PERIOD_DURATION_US,
/* adPositionInAdPod= */ 1,
/* totalAdDurationUs= */ 6 * AD_PERIOD_DURATION_US,
/* totalAdsInAdPod= */ 6,
adPlaybackState);
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 40_000_000,
/* adDurationUs= */ AD_PERIOD_DURATION_US,
/* adPositionInAdPod= */ 2,
/* totalAdDurationUs= */ 6 * AD_PERIOD_DURATION_US,
/* totalAdsInAdPod= */ 6,
adPlaybackState);
assertThat(
getAdGroupAndIndexInLiveMultiPeriodTimeline(
/* currentMediaItemIndex= */ 0,
/* adPeriodIndex= */ 0,
contentTimeline,
adPlaybackState))
.isEqualTo(new Pair<>(0, 1));
// Ad event for second ad in window arrives.
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1);
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 50_000_000,
/* adDurationUs= */ AD_PERIOD_DURATION_US,
/* adPositionInAdPod= */ 3,
/* totalAdDurationUs= */ 6 * AD_PERIOD_DURATION_US,
/* totalAdsInAdPod= */ 6,
adPlaybackState);
assertThat(
getAdGroupAndIndexInLiveMultiPeriodTimeline(
/* currentMediaItemIndex= */ 0,
/* adPeriodIndex= */ 1,
contentTimeline,
adPlaybackState))
.isEqualTo(new Pair<>(0, 2));
// Move one ad period forward: c, a, a, [a, a, a, a], c
contentTimeline.advanceNowUs(10_000_000L);
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2);
assertThat(
getAdGroupAndIndexInLiveMultiPeriodTimeline(
/* currentMediaItemIndex= */ 0,
/* adPeriodIndex= */ 1,
contentTimeline,
adPlaybackState))
.isEqualTo(new Pair<>(0, 3));
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 3);
assertThat(
getAdGroupAndIndexInLiveMultiPeriodTimeline(
/* currentMediaItemIndex= */ 0,
/* adPeriodIndex= */ 2,
contentTimeline,
adPlaybackState))
.isEqualTo(new Pair<>(0, 4));
}
@Test
public void handleAdPeriodRemovedFromTimeline_removalCorrectlyHandled() {
// Content timeline with content and ad periods: a,[c, a, a, a, a, a, a, c], a
FakeMultiPeriodLiveTimeline contentTimeline =
new FakeMultiPeriodLiveTimeline(
/* availabilityStartTimeUs= */ 0,
/* liveWindowDurationUs= */ 70_000_123,
/* nowUs= */ 189_453_123,
/* adSequencePattern= */ new boolean[] {false, true, true, true, true, true, true},
/* isContentTimeline= */ false,
/* populateAds= */ true,
/* playedAds= */ false);
AdPlaybackState adPlaybackState =
new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended();
// Ad events of the first two ads of the group have arrived (the first of the window).
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 120_000_000,
/* adDurationUs= */ AD_PERIOD_DURATION_US,
/* adPositionInAdPod= */ 1,
/* totalAdDurationUs= */ 6 * AD_PERIOD_DURATION_US,
/* totalAdsInAdPod= */ 6,
adPlaybackState); adPlaybackState);
adPlaybackState = adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
adPlaybackState =
addLiveAdBreak( addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 130_000_000, /* currentContentPeriodPositionUs= */ 130_000_000,
/* adDurationUs= */ AD_PERIOD_DURATION_US, /* adDurationUs= */ AD_PERIOD_DURATION_US,
/* adPositionInAdPod= */ 2, /* adPositionInAdPod= */ 2,
/* totalAdDurationUs= */ 2 * AD_PERIOD_DURATION_US, /* totalAdDurationUs= */ 6 * AD_PERIOD_DURATION_US,
/* totalAdsInAdPod= */ 2, /* totalAdsInAdPod= */ 6,
adPlaybackState); adPlaybackState);
AdPlaybackState finalAdPlaybackState = adPlaybackState;
// Current period is content period before ad group.
AdPlaybackState adPlaybackState1 =
handleAdPeriodRemovedFromTimeline(
/* currentPeriodIndex= */ 0, contentTimeline, adPlaybackState);
assertThat(adPlaybackState1.adGroupCount).isEqualTo(2);
assertThat(adPlaybackState1.getAdGroup(/* adGroupIndex= */ 0).states)
.asList()
.containsExactly(
AD_STATE_PLAYED,
AD_STATE_AVAILABLE,
AD_STATE_UNAVAILABLE,
AD_STATE_UNAVAILABLE,
AD_STATE_UNAVAILABLE,
AD_STATE_UNAVAILABLE)
.inOrder();
assertThat(adPlaybackState1.getAdGroup(/* adGroupIndex= */ 0).durationsUs)
.asList()
.containsExactly(10_000_000L, 10_000_000L, 40_000_000L, 0L, 0L, 0L)
.inOrder();
// Current period is played ad.
AdPlaybackState adPlaybackState2 =
handleAdPeriodRemovedFromTimeline(
/* currentPeriodIndex= */ 1, contentTimeline, adPlaybackState);
assertThat(adPlaybackState2.adGroupCount).isEqualTo(2);
assertThat(adPlaybackState2.getAdGroup(/* adGroupIndex= */ 0).states)
.asList()
.containsExactly(
AD_STATE_PLAYED,
AD_STATE_AVAILABLE,
AD_STATE_UNAVAILABLE,
AD_STATE_UNAVAILABLE,
AD_STATE_UNAVAILABLE,
AD_STATE_UNAVAILABLE)
.inOrder();
assertThat(adPlaybackState2.getAdGroup(/* adGroupIndex= */ 0).durationsUs)
.asList()
.containsExactly(10_000_000L, 10_000_000L, 40_000_000L, 0L, 0L, 0L)
.inOrder();
// Current period is available ad.
AdPlaybackState adPlaybackState3 =
handleAdPeriodRemovedFromTimeline(
/* currentPeriodIndex= */ 2, contentTimeline, adPlaybackState);
assertThat(adPlaybackState3.adGroupCount).isEqualTo(2);
assertThat(adPlaybackState3.getAdGroup(/* adGroupIndex= */ 0).states)
.asList()
.containsExactly(
AD_STATE_PLAYED,
AD_STATE_AVAILABLE,
AD_STATE_UNAVAILABLE,
AD_STATE_UNAVAILABLE,
AD_STATE_UNAVAILABLE,
AD_STATE_UNAVAILABLE)
.inOrder();
assertThat(adPlaybackState3.getAdGroup(/* adGroupIndex= */ 0).durationsUs)
.asList()
.containsExactly(10_000_000L, 10_000_000L, 40_000_000L, 0L, 0L, 0L)
.inOrder();
// Current period is first unavailable ad.
AdPlaybackState adPlaybackState4 =
handleAdPeriodRemovedFromTimeline(
/* currentPeriodIndex= */ 3, contentTimeline, adPlaybackState);
assertThat(adPlaybackState4.adGroupCount).isEqualTo(2);
assertThat(adPlaybackState4.getAdGroup(/* adGroupIndex= */ 0).states)
.asList()
.containsExactly(
AD_STATE_PLAYED,
AD_STATE_PLAYED,
AD_STATE_UNAVAILABLE,
AD_STATE_UNAVAILABLE,
AD_STATE_UNAVAILABLE,
AD_STATE_UNAVAILABLE)
.inOrder();
assertThat(adPlaybackState4.getAdGroup(/* adGroupIndex= */ 0).durationsUs)
.asList()
.containsExactly(10_000_000L, 10_000_000L, 10_000_000L, 30_000_000L, 0L, 0L)
.inOrder();
// Current period is unavailable ad. Give up case.
AdPlaybackState adPlaybackState5 =
handleAdPeriodRemovedFromTimeline(
/* currentPeriodIndex= */ 4, contentTimeline, adPlaybackState);
assertThat(adPlaybackState5.adGroupCount).isEqualTo(3);
assertThat(adPlaybackState5.getAdGroup(/* adGroupIndex= */ 0).states)
.asList()
.containsExactly(
AD_STATE_PLAYED,
AD_STATE_PLAYED,
AD_STATE_PLAYED,
AD_STATE_PLAYED,
AD_STATE_PLAYED,
AD_STATE_PLAYED)
.inOrder();
assertThat(adPlaybackState5.getAdGroup(/* adGroupIndex= */ 0).durationsUs)
.asList()
.containsExactly(0L, 0L, 0L, 0L, 0L, 0L)
.inOrder();
assertThat(adPlaybackState5.getAdGroup(/* adGroupIndex= */ 1).states)
.asList()
.containsExactly(AD_STATE_AVAILABLE);
assertThat(adPlaybackState5.getAdGroup(/* adGroupIndex= */ 1).durationsUs)
.asList()
.containsExactly(10_000_000L)
.inOrder();
// Current period is after ad group.
AdPlaybackState adPlaybackState6 =
handleAdPeriodRemovedFromTimeline(
/* currentPeriodIndex= */ 7, contentTimeline, adPlaybackState);
assertThat(adPlaybackState6.adGroupCount).isEqualTo(2);
assertThat(adPlaybackState6.getAdGroup(/* adGroupIndex= */ 0).states)
.asList()
.containsExactly(
AD_STATE_PLAYED,
AD_STATE_PLAYED,
AD_STATE_PLAYED,
AD_STATE_PLAYED,
AD_STATE_PLAYED,
AD_STATE_PLAYED)
.inOrder();
assertThat(adPlaybackState6.getAdGroup(/* adGroupIndex= */ 0).durationsUs)
.asList()
.containsExactly(10_000_000L, 10_000_000L, 40_000_000L, 0L, 0L, 0L)
.inOrder();
}
@Test
public void getAdGroupDurationUsForLiveAdPeriodIndex_allAdsInTimeline_correctAdGroupDuration() {
int adPodTotalAdCount = 2;
// Content and ad periods in timeline: [c, a, a, c, a, a].
FakeMultiPeriodLiveTimeline timeline =
new FakeMultiPeriodLiveTimeline(
/* availabilityStartTimeUs= */ 0,
/* liveWindowDurationUs= */ 75_007_123,
/* nowUs= */ 99_321_457,
/* adSequencePattern= */ new boolean[] {false, true, true},
/* isContentTimeline= */ false,
/* populateAds= */ true,
/* playedAds= */ false);
AdPodInfo firstAdPodInfo = mock(AdPodInfo.class);
when(firstAdPodInfo.getAdPosition()).thenReturn(1);
when(firstAdPodInfo.getTotalAds()).thenReturn(adPodTotalAdCount);
when(firstAdPodInfo.getMaxDuration()).thenReturn(0.3D);
AdPodInfo secondAdPodInfo = mock(AdPodInfo.class);
when(secondAdPodInfo.getAdPosition()).thenReturn(2);
when(secondAdPodInfo.getTotalAds()).thenReturn(adPodTotalAdCount);
when(secondAdPodInfo.getMaxDuration()).thenReturn(0.3D);
assertThat( assertThat(
getAdGroupAndIndexInMultiPeriodWindow( ImaUtil.getAdGroupDurationUsForLiveAdPeriodIndex(
/* adPeriodIndex= */ 1, adPlaybackState, contentTimeline)) timeline, firstAdPodInfo, /* adPeriodIndex= */ 1, new Window(), new Period()))
.isEqualTo(new Pair<>(0, 0)); .isEqualTo(2 * AD_PERIOD_DURATION_US);
assertThat( assertThat(
getAdGroupAndIndexInMultiPeriodWindow( ImaUtil.getAdGroupDurationUsForLiveAdPeriodIndex(
/* adPeriodIndex= */ 2, adPlaybackState, contentTimeline)) timeline, secondAdPodInfo, /* adPeriodIndex= */ 2, new Window(), new Period()))
.isEqualTo(new Pair<>(1, 0)); .isEqualTo(2 * AD_PERIOD_DURATION_US);
// The second ad group has the last ad with an unknown duration.
assertThat( assertThat(
getAdGroupAndIndexInMultiPeriodWindow( ImaUtil.getAdGroupDurationUsForLiveAdPeriodIndex(
/* adPeriodIndex= */ 4, adPlaybackState, contentTimeline)) timeline, firstAdPodInfo, /* adPeriodIndex= */ 4, new Window(), new Period()))
.isEqualTo(new Pair<>(2, 0)); .isEqualTo(secToUsRounded(0.3D));
assertThat( assertThat(
getAdGroupAndIndexInMultiPeriodWindow( ImaUtil.getAdGroupDurationUsForLiveAdPeriodIndex(
/* adPeriodIndex= */ 5, adPlaybackState, contentTimeline)) timeline, secondAdPodInfo, /* adPeriodIndex= */ 5, new Window(), new Period()))
.isEqualTo(new Pair<>(2, 1)); .isEqualTo(secToUsRounded(0.3D));
Assert.assertThrows( }
IllegalStateException.class,
() -> @Test
getAdGroupAndIndexInMultiPeriodWindow( public void
/* adPeriodIndex= */ 0, finalAdPlaybackState, contentTimeline)); getAdGroupDurationUsForLiveAdPeriodIndex_missingAdPeriodInTimeline_fallbackAdGroupDuration() {
int adPodTotalAdCount = 3;
// Content and ad periods in timeline: [a, a, c]. First two ads not in window.
FakeMultiPeriodLiveTimeline timeline =
new FakeMultiPeriodLiveTimeline(
/* availabilityStartTimeUs= */ 0,
/* liveWindowDurationUs= */ 49_321_753,
/* nowUs= */ 85_007_123,
/* adSequencePattern= */ new boolean[] {false, true, true},
/* isContentTimeline= */ false,
/* populateAds= */ true,
/* playedAds= */ false);
AdPodInfo firstAdPodInfo = mock(AdPodInfo.class);
when(firstAdPodInfo.getAdPosition()).thenReturn(3);
when(firstAdPodInfo.getTotalAds()).thenReturn(adPodTotalAdCount);
when(firstAdPodInfo.getMaxDuration()).thenReturn(0.3D);
AdPodInfo secondAdPodInfo = mock(AdPodInfo.class);
when(secondAdPodInfo.getAdPosition()).thenReturn(4);
when(secondAdPodInfo.getTotalAds()).thenReturn(adPodTotalAdCount);
when(secondAdPodInfo.getMaxDuration()).thenReturn(0.3D);
assertThat(
ImaUtil.getAdGroupDurationUsForLiveAdPeriodIndex(
timeline, firstAdPodInfo, /* adPeriodIndex= */ 0, new Window(), new Period()))
.isEqualTo(secToUsRounded(0.3D));
assertThat(
ImaUtil.getAdGroupDurationUsForLiveAdPeriodIndex(
timeline, secondAdPodInfo, /* adPeriodIndex= */ 1, new Window(), new Period()))
.isEqualTo(secToUsRounded(0.3D));
} }
@Test @Test
......
...@@ -70,7 +70,7 @@ public class FakeMultiPeriodLiveTimeline extends Timeline { ...@@ -70,7 +70,7 @@ public class FakeMultiPeriodLiveTimeline extends Timeline {
* availabilityStartTimeUs}. True is an ad period, and false a content period. * availabilityStartTimeUs}. True is an ad period, and false a content period.
* @param isContentTimeline Whether the timeline is a content timeline without {@link * @param isContentTimeline Whether the timeline is a content timeline without {@link
* AdPlaybackState}s. * AdPlaybackState}s.
* @param populateAds Whether to populate ads like after the ad event has been received. This * @param populateAds Whether to populate ads in the same way if an ad event has been received.
* @param playedAds Whether ads should be marked as played if populated. * @param playedAds Whether ads should be marked as played if populated.
*/ */
public FakeMultiPeriodLiveTimeline( public FakeMultiPeriodLiveTimeline(
...@@ -131,6 +131,7 @@ public class FakeMultiPeriodLiveTimeline extends Timeline { ...@@ -131,6 +131,7 @@ public class FakeMultiPeriodLiveTimeline extends Timeline {
@Override @Override
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
checkArgument(windowIndex == 0);
MediaItem.LiveConfiguration liveConfiguration = MediaItem.LiveConfiguration liveConfiguration =
new MediaItem.LiveConfiguration.Builder().build(); new MediaItem.LiveConfiguration.Builder().build();
window.set( window.set(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment