Make adding ad live breaks more robust

This change makes adding ad events in live streams more robust by allowing ad
groups to grow in number of ads if more ad events are received than initially
announced by the SDK.

With the IMA prefetch feature, an AdPod can grow in size in certain conditions
like from initially 2 ads to 4 ads being part of the ad group. With this change,
if an additional ad event arrives while the ad group is still being played,
the ad group is expanded. If the event arrives late and the ad group is already
completed, a new group is created for the remaining ads.

This also covers the case where we join the live stream while an ad is being
played and we missed at least one LOADED event from the SDK. Ads of the group
before the first LOADED event are ignored in such a case.

PiperOrigin-RevId: 484214760
This commit is contained in:
bachinger 2022-10-27 11:10:10 +00:00 committed by microkatz
parent 16cb5cbc1f
commit 64e9e88823
5 changed files with 793 additions and 82 deletions

View File

@ -399,7 +399,7 @@
"uri": "ssai://dai.google.com/?contentSourceId=2528370&videoId=tears-of-steel&format=2&adsId=1"
},
{
"name": "HLS Live: Big Buck Bunny (mid), 3 ads each [10 s]",
"name": "HLS Live: Big Buck Bunny (mid), 3 ads [10/10/10s]",
"uri": "ssai://dai.google.com/?assetKey=sN_IYUG8STe1ZzhIIE_ksA&format=2&adsId=3"
},
{

View File

@ -161,16 +161,25 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
checkArgument(Util.areEqual(adsId, adPlaybackState.adsId));
@Nullable AdPlaybackState oldAdPlaybackState = this.adPlaybackStates.get(periodUid);
if (oldAdPlaybackState != null) {
for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) {
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(i);
for (int adGroupIndex = adPlaybackState.removedAdGroupCount;
adGroupIndex < adPlaybackState.adGroupCount;
adGroupIndex++) {
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
checkArgument(adGroup.isServerSideInserted);
if (i < oldAdPlaybackState.adGroupCount) {
checkArgument(
getAdCountInGroup(adPlaybackState, /* adGroupIndex= */ i)
>= getAdCountInGroup(oldAdPlaybackState, /* adGroupIndex= */ i));
if (adGroupIndex < oldAdPlaybackState.adGroupCount
&& getAdCountInGroup(adPlaybackState, /* adGroupIndex= */ adGroupIndex)
< getAdCountInGroup(oldAdPlaybackState, /* adGroupIndex= */ adGroupIndex)) {
// Removing ads from an ad group is only allowed when the group has been split.
AdPlaybackState.AdGroup nextAdGroup = adPlaybackState.getAdGroup(adGroupIndex + 1);
long sumOfSplitContentResumeOffsetUs =
adGroup.contentResumeOffsetUs + nextAdGroup.contentResumeOffsetUs;
AdPlaybackState.AdGroup oldAdGroup = oldAdPlaybackState.getAdGroup(adGroupIndex);
checkArgument(sumOfSplitContentResumeOffsetUs == oldAdGroup.contentResumeOffsetUs);
checkArgument(adGroup.timeUs + adGroup.contentResumeOffsetUs == nextAdGroup.timeUs);
}
if (adGroup.timeUs == C.TIME_END_OF_SOURCE) {
checkArgument(getAdCountInGroup(adPlaybackState, /* adGroupIndex= */ i) == 0);
checkArgument(
getAdCountInGroup(adPlaybackState, /* adGroupIndex= */ adGroupIndex) == 0);
}
}
}

View File

@ -15,20 +15,21 @@
*/
package androidx.media3.exoplayer.ima;
import static androidx.media3.common.AdPlaybackState.AD_STATE_AVAILABLE;
import static androidx.media3.common.AdPlaybackState.AD_STATE_UNAVAILABLE;
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.common.util.Util.usToMs;
import static androidx.media3.exoplayer.ima.ImaUtil.addLiveAdBreak;
import static androidx.media3.exoplayer.ima.ImaUtil.expandAdGroupPlaceholder;
import static androidx.media3.exoplayer.ima.ImaUtil.getAdGroupAndIndexInMultiPeriodWindow;
import static androidx.media3.exoplayer.ima.ImaUtil.secToMsRounded;
import static androidx.media3.exoplayer.ima.ImaUtil.secToUsRounded;
import static androidx.media3.exoplayer.ima.ImaUtil.splitAdGroup;
import static androidx.media3.exoplayer.ima.ImaUtil.splitAdPlaybackStateForPeriods;
import static androidx.media3.exoplayer.ima.ImaUtil.updateAdDurationAndPropagate;
import static androidx.media3.exoplayer.ima.ImaUtil.updateAdDurationInAdGroup;
import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState;
import static java.lang.Math.min;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.content.Context;
@ -52,6 +53,7 @@ import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.ConditionVariable;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.TransferListener;
@ -451,6 +453,8 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
}
}
private static final String TAG = "ImaSSAIMediaSource";
private final MediaItem mediaItem;
private final Player player;
private final MediaSource.Factory contentMediaSourceFactory;
@ -472,7 +476,6 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
@Nullable private IOException loadError;
private @MonotonicNonNull Timeline contentTimeline;
private AdPlaybackState adPlaybackState;
private int firstSeenAdIndexInAdGroup;
private ImaServerSideAdInsertionMediaSource(
MediaItem mediaItem,
@ -720,46 +723,6 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
return adPlaybackState;
}
private AdPlaybackState addLiveAdBreak(
Ad ad, long currentPeriodPositionUs, AdPlaybackState adPlaybackState) {
AdPodInfo adPodInfo = ad.getAdPodInfo();
long adDurationUs = secToUsRounded(ad.getDuration());
int adIndexInAdGroup = adPodInfo.getAdPosition() - 1;
// TODO(b/208398934) Support seeking backwards.
if (adIndexInAdGroup == 0 || adPlaybackState.adGroupCount == 1) {
firstSeenAdIndexInAdGroup = adIndexInAdGroup;
// Adjust count and ad index in case we joined the live stream within an ad group.
int adCount = adPodInfo.getTotalAds() - firstSeenAdIndexInAdGroup;
adIndexInAdGroup -= firstSeenAdIndexInAdGroup;
// First ad of group. Create a new group with all ads.
long[] adDurationsUs =
updateAdDurationAndPropagate(
new long[adCount],
adIndexInAdGroup,
adDurationUs,
msToUs(secToMsRounded(adPodInfo.getMaxDuration())));
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ currentPeriodPositionUs,
/* contentResumeOffsetUs= */ sum(adDurationsUs),
/* adDurationsUs...= */ adDurationsUs);
} else {
int adGroupIndex = adPlaybackState.adGroupCount - 2;
adIndexInAdGroup -= firstSeenAdIndexInAdGroup;
if (adPodInfo.getTotalAds() == adPodInfo.getAdPosition()) {
// Reset the ad index whe we are at the last ad in the group.
firstSeenAdIndexInAdGroup = 0;
}
adPlaybackState =
updateAdDurationInAdGroup(adGroupIndex, adIndexInAdGroup, adDurationUs, adPlaybackState);
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
return adPlaybackState.withContentResumeOffsetUs(
adGroupIndex, min(adGroup.contentResumeOffsetUs, sum(adGroup.durationsUs)));
}
return adPlaybackState;
}
private static AdPlaybackState skipAd(Ad ad, AdPlaybackState adPlaybackState) {
AdPodInfo adPodInfo = ad.getAdPodInfo();
int adGroupIndex = adPodInfo.getPodIndex();
@ -815,11 +778,27 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
adGroupIndex = adGroupIndexAndAdIndexInAdGroup.first;
adIndexInAdGroup = adGroupIndexAndAdIndexInAdGroup.second;
}
int adState = adPlaybackState.getAdGroup(adGroupIndex).states[adIndexInAdGroup];
if (adState == AdPlaybackState.AD_STATE_AVAILABLE
|| adState == AdPlaybackState.AD_STATE_UNAVAILABLE) {
setAdPlaybackState(
adPlaybackState.withPlayedAd(adGroupIndex, /* adIndexInAdGroup= */ adIndexInAdGroup));
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
int adState = adGroup.states[adIndexInAdGroup];
if (adState == AD_STATE_AVAILABLE || adState == AD_STATE_UNAVAILABLE) {
AdPlaybackState newAdPlaybackState =
adPlaybackState.withPlayedAd(adGroupIndex, /* adIndexInAdGroup= */ adIndexInAdGroup);
adGroup = newAdPlaybackState.getAdGroup(adGroupIndex);
if (isLiveStream
&& newPosition.adGroupIndex == C.INDEX_UNSET
&& adIndexInAdGroup < adGroup.states.length - 1
&& adGroup.states[adIndexInAdGroup + 1] == AD_STATE_AVAILABLE) {
// There is an available ad after the ad period that just ended being played!
Log.w(TAG, "Detected late ad event. Regrouping trailing ads into separate ad group.");
newAdPlaybackState =
splitAdGroup(
adGroup,
adGroupIndex,
/* splitIndexExclusive= */ adIndexInAdGroup + 1,
newAdPlaybackState);
}
setAdPlaybackState(newAdPlaybackState);
}
}
}
@ -887,12 +866,18 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
long positionInWindowUs =
timeline.getPeriod(player.getCurrentPeriodIndex(), new Timeline.Period())
.positionInWindowUs;
long currentPeriodPosition = msToUs(player.getContentPosition()) - positionInWindowUs;
long currentContentPeriodPositionUs =
msToUs(player.getContentPosition()) - positionInWindowUs;
Ad ad = event.getAd();
AdPodInfo adPodInfo = ad.getAdPodInfo();
newAdPlaybackState =
addLiveAdBreak(
event.getAd(),
currentPeriodPosition,
newAdPlaybackState.equals(AdPlaybackState.NONE)
currentContentPeriodPositionUs,
/* adDurationUs= */ secToUsRounded(ad.getDuration()),
/* adPositionInAdPod= */ adPodInfo.getAdPosition(),
/* totalAdDurationUs= */ secToUsRounded(adPodInfo.getMaxDuration()),
/* totalAdsInAdPod= */ adPodInfo.getTotalAds(),
/* adPlaybackState= */ newAdPlaybackState.equals(AdPlaybackState.NONE)
? new AdPlaybackState(adsId)
: newAdPlaybackState);
} else {

View File

@ -15,10 +15,14 @@
*/
package androidx.media3.exoplayer.ima;
import static androidx.media3.common.AdPlaybackState.AD_STATE_AVAILABLE;
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.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 android.content.Context;
@ -30,8 +34,10 @@ import androidx.annotation.CheckResult;
import androidx.annotation.Nullable;
import androidx.media3.common.AdOverlayInfo;
import androidx.media3.common.AdPlaybackState;
import androidx.media3.common.AdPlaybackState.AdGroup;
import androidx.media3.common.AdViewProvider;
import androidx.media3.common.C;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSchemeDataSource;
@ -322,7 +328,7 @@ import java.util.Set;
@CheckResult
public static AdPlaybackState updateAdDurationInAdGroup(
int adGroupIndex, int adIndexInAdGroup, long adDurationUs, AdPlaybackState adPlaybackState) {
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
checkArgument(adIndexInAdGroup < adGroup.durationsUs.length);
long[] adDurationsUs =
updateAdDurationAndPropagate(
@ -334,25 +340,28 @@ import java.util.Set;
}
/**
* Updates the duration of the given ad in the array and propagates the difference to the total
* duration to the next ad. If the updated ad is the last ad, the remaining duration is wrapped
* around to the first ad in the group.
* Updates the duration of the given ad in the array.
*
* <p>The remaining difference when subtracting {@code adDurationUs} from {@code
* remainingDurationUs} is used as the duration of the next ad after {@code adIndex}. If the
* updated ad is the last ad, the remaining duration is wrapped around to the first ad of the
* group.
*
* <p>The remaining ad duration is only propagated if the destination ad has a duration of 0.
*
* @param adDurationsUs The array to edit.
* @param adIndex The index of the ad in the durations array.
* @param adDurationUs The new ad duration.
* @param totalDurationUs The total duration the difference of which to propagate to the next ad.
* @param remainingDurationUs The remaining ad duration before updating the new ad duration.
* @return The updated input array, for convenience.
*/
/* package */ static long[] updateAdDurationAndPropagate(
long[] adDurationsUs, int adIndex, long adDurationUs, long totalDurationUs) {
private static long[] updateAdDurationAndPropagate(
long[] adDurationsUs, int adIndex, long adDurationUs, long remainingDurationUs) {
adDurationsUs[adIndex] = adDurationUs;
int nextAdIndex = (adIndex + 1) % adDurationsUs.length;
if (adDurationsUs[nextAdIndex] == 0) {
// Propagate the remaining duration to the next ad.
adDurationsUs[nextAdIndex] = max(0, totalDurationUs - adDurationUs);
adDurationsUs[nextAdIndex] = max(0, remainingDurationUs - adDurationUs);
}
return adDurationsUs;
}
@ -389,7 +398,7 @@ import java.util.Set;
AdPlaybackState contentOnlyAdPlaybackState = new AdPlaybackState(adsId);
Map<Object, AdPlaybackState> adPlaybackStates = new HashMap<>();
for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) {
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ i);
AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ i);
if (adGroup.timeUs == C.TIME_END_OF_SOURCE) {
checkState(i == adPlaybackState.adGroupCount - 1);
// The last ad group is a placeholder for a potential post roll. We can just stop here.
@ -432,7 +441,7 @@ import java.util.Set;
}
private static AdPlaybackState splitAdGroupForPeriod(
Object adsId, AdPlaybackState.AdGroup adGroup, long periodStartUs, long periodDurationUs) {
Object adsId, AdGroup adGroup, long periodStartUs, long periodDurationUs) {
AdPlaybackState adPlaybackState =
new AdPlaybackState(checkNotNull(adsId), /* adGroupTimesUs...= */ 0)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
@ -484,7 +493,7 @@ import java.util.Set;
long totalElapsedContentDurationUs = 0;
for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) {
int adIndexInAdGroup = 0;
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ i);
AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ i);
long adGroupDurationUs = sum(adGroup.durationsUs);
long elapsedAdGroupAdDurationUs = 0;
for (int j = periodIndex; j < contentTimeline.getPeriodCount(); j++) {
@ -513,6 +522,181 @@ import java.util.Set;
throw new IllegalStateException();
}
/**
* Called when the SDK emits a {@code LOADED} event of an IMA SSAI live stream.
*
* <p>For each ad, the SDK emits a {@code LOADED} event at the start of the ad. The {@code LOADED}
* event provides the information of a certain ad (index and duration) and its ad pod (number of
* ads and total ad duration) that is mapped to an ad in an {@linkplain AdGroup ad group} of an
* {@linkplain AdPlaybackState ad playback state} to reflect ads in the ExoPlayer media structure.
*
* <p>In the normal case (when all ad information is available completely and in time), the
* life-cycle of a live ad group and its ads has these phases:
*
* <ol>
* <li>When playing content and a {@code LOADED} event arrives, an ad group is inserted at the
* current position with the number of ads reported by the ad pod. The duration of the first
* ad is set and its state is set to {@link AdPlaybackState#AD_STATE_AVAILABLE}. The
* duration of the 2nd ad is set to the remaining duration of the total ad group duration.
* This pads out the duration of the ad group, so it doesn't end before the next ad event
* arrives. When inserting the ad group at the current position, the player immediately
* advances to play the inserted ad period.
* <li>When playing an ad group and a further {@code LOADED} event arrives, the ad state is
* inspected to find the {@linkplain AdPlaybackState#getAdGroupIndexForPositionUs(long,
* long) ad group currently being played}. We query for the first {@linkplain
* AdPlaybackState#AD_STATE_UNAVAILABLE unavailable ad} of that ad group, override its
* placeholder duration, mark it {@linkplain AdPlaybackState#AD_STATE_AVAILABLE available}
* and propagate the remainder of the placeholder duration to the next ad. Repeating this
* step until all ads are configured and marked as available.
* <li>When playing an ad and a {@code LOADED} event arrives but no more ads are in {@link
* AdPlaybackState#AD_STATE_UNAVAILABLE}, the group is expanded by inserting a new ad at the
* end of the ad group.
* <li>After playing an ad: When playback exits from an ad period to the next ad or back to
* content, {@link ImaServerSideAdInsertionMediaSource} detects {@linkplain
* Player.Listener#onPositionDiscontinuity(Player.PositionInfo, Player.PositionInfo, int) a
* position discontinuity}, identifies {@linkplain Player.PositionInfo#adIndexInAdGroup the
* ad being exited} and {@linkplain AdPlaybackState#AD_STATE_PLAYED marks the ad as played}.
* </ol>
*
* <p>Some edge-cases need consideration. When a user joins a live stream during an ad being
* played, ad information previous to the first received {@code LOADED} event is missing. Only ads
* starting from the first ad with full information are inserted into the group (back to happy
* path step 2).
*
* <p>There is further a chance, that a (pre-fetch) event arrives after the ad group has already
* ended. In such a case, the pre-fetch ad starts a new ad group with the remaining ads in the
* same way as the during-ad-joiner case that can afterwards be expanded again (back to end of
* happy path step 2).
*
* @param currentContentPeriodPositionUs The current public content position, in microseconds.
* @param adDurationUs The duration of the ad to be inserted, in microseconds.
* @param adPositionInAdPod The ad position in the ad pod (Note: starts with index 1).
* @param totalAdDurationUs The total duration of all ads as declared by the ad pod.
* @param totalAdsInAdPod The total number of ads declared by the ad pod.
* @param adPlaybackState The ad playback state with the current ad information.
* @return The updated {@link AdPlaybackState}.
*/
@CheckResult
public static AdPlaybackState addLiveAdBreak(
long currentContentPeriodPositionUs,
long adDurationUs,
int adPositionInAdPod,
long totalAdDurationUs,
int totalAdsInAdPod,
AdPlaybackState adPlaybackState) {
checkArgument(adPositionInAdPod > 0);
long mediaPeriodPositionUs =
getMediaPeriodPositionUsForContent(
currentContentPeriodPositionUs, /* nextAdGroupIndex= */ C.INDEX_UNSET, adPlaybackState);
// TODO(b/217187518) Support seeking backwards.
int adGroupIndex =
adPlaybackState.getAdGroupIndexForPositionUs(
mediaPeriodPositionUs, /* periodDurationUs= */ C.TIME_UNSET);
if (adGroupIndex == C.INDEX_UNSET) {
int adIndexInAdGroup = adPositionInAdPod - 1;
long[] adDurationsUs =
updateAdDurationAndPropagate(
new long[totalAdsInAdPod - adIndexInAdGroup],
/* adIndex= */ 0,
adDurationUs,
totalAdDurationUs);
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ currentContentPeriodPositionUs,
/* contentResumeOffsetUs= */ sum(adDurationsUs),
/* adDurationsUs...= */ adDurationsUs);
adGroupIndex =
adPlaybackState.getAdGroupIndexForPositionUs(
mediaPeriodPositionUs, /* periodDurationUs= */ C.TIME_UNSET);
if (adGroupIndex != C.INDEX_UNSET) {
adPlaybackState =
adPlaybackState
.withAvailableAd(adGroupIndex, /* adIndexInAdGroup= */ 0)
.withOriginalAdCount(adGroupIndex, /* originalAdCount= */ totalAdsInAdPod);
}
} else {
AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
long[] newDurationsUs = Arrays.copyOf(adGroup.durationsUs, adGroup.count);
int nextUnavailableAdIndex = getNextUnavailableAdIndex(adGroup);
if (adGroup.originalCount < totalAdsInAdPod || nextUnavailableAdIndex == adGroup.count) {
int adInAdGroupCount = max(totalAdsInAdPod, nextUnavailableAdIndex + 1);
adPlaybackState =
adPlaybackState
.withAdCount(adGroupIndex, adInAdGroupCount)
.withOriginalAdCount(adGroupIndex, /* originalAdCount= */ adInAdGroupCount);
newDurationsUs = Arrays.copyOf(newDurationsUs, adInAdGroupCount);
newDurationsUs[nextUnavailableAdIndex] = totalAdDurationUs;
Arrays.fill(
newDurationsUs,
/* fromIndex= */ nextUnavailableAdIndex + 1,
/* toIndex= */ adInAdGroupCount,
/* val= */ 0L);
}
long remainingDurationUs = max(adDurationUs, newDurationsUs[nextUnavailableAdIndex]);
updateAdDurationAndPropagate(
newDurationsUs, nextUnavailableAdIndex, adDurationUs, remainingDurationUs);
adPlaybackState =
adPlaybackState
.withAdDurationsUs(adGroupIndex, newDurationsUs)
.withAvailableAd(adGroupIndex, nextUnavailableAdIndex)
.withContentResumeOffsetUs(adGroupIndex, sum(newDurationsUs));
}
return adPlaybackState;
}
/**
* Splits the ad group at an available ad at a given split index.
*
* <p>When splitting, the ads from and after the split index are removed from the existing ad
* group. Then the ad events of all removed available ads are replicated to get the exact same
* result as if the new ad group was created by SDK ad events.
*
* @param adGroup The ad group to split.
* @param adGroupIndex The index of the ad group in the ad playback state.
* @param splitIndexExclusive The first index that should be part of the newly created ad group.
* @param adPlaybackState The ad playback state to modify.
* @return The ad playback state with the split ad group.
*/
@CheckResult
public static AdPlaybackState splitAdGroup(
AdGroup adGroup, int adGroupIndex, int splitIndexExclusive, AdPlaybackState adPlaybackState) {
checkArgument(splitIndexExclusive > 0 && splitIndexExclusive < adGroup.count);
// Remove the ads from the ad group.
for (int i = 0; i < adGroup.count - splitIndexExclusive; i++) {
adPlaybackState = adPlaybackState.withLastAdRemoved(adGroupIndex);
}
AdGroup previousAdGroup = adPlaybackState.getAdGroup(adGroupIndex);
long newAdGroupTimeUs = previousAdGroup.timeUs + previousAdGroup.contentResumeOffsetUs;
// Replicate ad events for each available ad that has been removed.
@AdPlaybackState.AdState
int[] removedStates = Arrays.copyOfRange(adGroup.states, splitIndexExclusive, adGroup.count);
long[] removedDurationsUs =
Arrays.copyOfRange(adGroup.durationsUs, splitIndexExclusive, adGroup.count);
long remainingAdDurationUs = sum(removedDurationsUs);
for (int i = 0; i < removedStates.length && removedStates[i] == AD_STATE_AVAILABLE; i++) {
adPlaybackState =
addLiveAdBreak(
newAdGroupTimeUs,
/* adDurationUs= */ removedDurationsUs[i],
/* adPositionInAdPod= */ i + 1,
/* totalAdDurationUs= */ remainingAdDurationUs,
/* totalAdsInAdPod= */ removedDurationsUs.length,
adPlaybackState);
remainingAdDurationUs -= removedDurationsUs[i];
}
return adPlaybackState;
}
private static int getNextUnavailableAdIndex(AdGroup adGroup) {
for (int i = 0; i < adGroup.states.length; i++) {
if (adGroup.states[i] == AD_STATE_UNAVAILABLE) {
return i;
}
}
return adGroup.states.length;
}
/**
* Converts a time in seconds to the corresponding time in microseconds.
*

View File

@ -15,7 +15,14 @@
*/
package androidx.media3.exoplayer.ima;
import static androidx.media3.common.AdPlaybackState.AD_STATE_AVAILABLE;
import static androidx.media3.common.AdPlaybackState.AD_STATE_ERROR;
import static androidx.media3.common.AdPlaybackState.AD_STATE_PLAYED;
import static androidx.media3.common.AdPlaybackState.AD_STATE_SKIPPED;
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.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;
@ -449,13 +456,13 @@ public class ImaUtilTest {
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).getAdGroup(/* adGroupIndex= */ 0).states[0])
.isEqualTo(AdPlaybackState.AD_STATE_PLAYED);
.isEqualTo(AD_STATE_PLAYED);
assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).getAdGroup(/* adGroupIndex= */ 0).states[0])
.isEqualTo(AdPlaybackState.AD_STATE_SKIPPED);
.isEqualTo(AD_STATE_SKIPPED);
assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).getAdGroup(/* adGroupIndex= */ 0).states[0])
.isEqualTo(AdPlaybackState.AD_STATE_ERROR);
.isEqualTo(AD_STATE_ERROR);
assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).getAdGroup(/* adGroupIndex= */ 0).states[0])
.isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
.isEqualTo(AD_STATE_UNAVAILABLE);
}
@Test
@ -489,19 +496,19 @@ public class ImaUtilTest {
assertThat(adPlaybackStates).hasSize(periodCount);
assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).getAdGroup(/* adGroupIndex= */ 0).states[0])
.isEqualTo(AdPlaybackState.AD_STATE_PLAYED);
.isEqualTo(AD_STATE_PLAYED);
assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).getAdGroup(/* adGroupIndex= */ 0).states[0])
.isEqualTo(AdPlaybackState.AD_STATE_PLAYED);
.isEqualTo(AD_STATE_PLAYED);
assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).getAdGroup(/* adGroupIndex= */ 0).states[0])
.isEqualTo(AdPlaybackState.AD_STATE_PLAYED);
.isEqualTo(AD_STATE_PLAYED);
assertThat(adPlaybackStates.get(new Pair<>(0L, 4)).getAdGroup(/* adGroupIndex= */ 0).states[0])
.isEqualTo(AdPlaybackState.AD_STATE_PLAYED);
.isEqualTo(AD_STATE_PLAYED);
assertThat(adPlaybackStates.get(new Pair<>(0L, 5)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 6)).getAdGroup(/* adGroupIndex= */ 0).states[0])
.isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
.isEqualTo(AD_STATE_UNAVAILABLE);
assertThat(adPlaybackStates.get(new Pair<>(0L, 7)).getAdGroup(/* adGroupIndex= */ 0).states[0])
.isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
.isEqualTo(AD_STATE_UNAVAILABLE);
}
@Test
@ -843,4 +850,530 @@ public class ImaUtilTest {
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(2);
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(1);
}
@Test
public void addLiveAdBreak_threeAdsHappyPath_createsNewAdGroupAndPropagates() {
AdPlaybackState adPlaybackState =
new AdPlaybackState("adsId")
.withNewAdGroup(/* adGroupIndex= */ 0, /* adGroupTimeUs= */ C.TIME_END_OF_SOURCE)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
// Initial LOADED event while playing in content, makes the player advancing to the first ad
// period: [/* adGroupIndex= */ 0, /* adIndexInAdGroup */ 0, /* nextAdGroupIndex= */ -1].
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 123_000_000L,
/* adDurationUs= */ 10_000_001L,
/* adPositionInAdPod= */ 1,
/* totalAdDurationUs= */ 30_000_001L,
/* totalAdsInAdPod= */ 3,
adPlaybackState);
assertThat(adPlaybackState.adGroupCount).isEqualTo(2);
assertThat(adPlaybackState.getAdGroup(0).count).isEqualTo(3);
assertThat(adPlaybackState.getAdGroup(0).originalCount).isEqualTo(3);
assertThat(adPlaybackState.getAdGroup(0).timeUs).isEqualTo(123_000_000L);
assertThat(adPlaybackState.getAdGroup(0).isServerSideInserted).isTrue();
assertThat(adPlaybackState.getAdGroup(0).states)
.asList()
.containsExactly(AD_STATE_AVAILABLE, AD_STATE_UNAVAILABLE, AD_STATE_UNAVAILABLE)
.inOrder();
assertThat(adPlaybackState.getAdGroup(0).durationsUs)
.asList()
.containsExactly(10_000_001L, 20_000_000L, 0L)
.inOrder();
assertThat(adPlaybackState.getAdGroup(0).contentResumeOffsetUs).isEqualTo(30_000_001);
// Second load event while first ad is playing.
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 123_000_000L + 10_000_001L,
/* adDurationUs= */ 10_000_010L,
/* adPositionInAdPod= */ 2,
/* totalAdDurationUs= */ 30_000_011L,
/* totalAdsInAdPod= */ 3,
adPlaybackState);
// Player advances to the second ad period:
// [/* adGroupIndex= */ 0, /* adIndexInAdGroup */ 1, /* nextAdGroupIndex= */ -1].
// The first ad period is marked as played.
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
assertThat(adPlaybackState.adGroupCount).isEqualTo(2);
assertThat(adPlaybackState.getAdGroup(0).count).isEqualTo(3);
assertThat(adPlaybackState.getAdGroup(0).originalCount).isEqualTo(3);
assertThat(adPlaybackState.getAdGroup(0).states)
.asList()
.containsExactly(AD_STATE_PLAYED, AD_STATE_AVAILABLE, AD_STATE_UNAVAILABLE)
.inOrder();
assertThat(adPlaybackState.getAdGroup(0).durationsUs)
.asList()
.containsExactly(10_000_001L, 10_000_010L, 9_999_990L)
.inOrder();
assertThat(adPlaybackState.getAdGroup(0).contentResumeOffsetUs).isEqualTo(30_000_001L);
// Player advances to the third ad period:
// [/* adGroupIndex= */ 0, /* adIndexInAdGroup */ 2, /* nextAdGroupIndex= */ -1].
// The 2nd ad period is marked as played.
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1);
// Third LOADED event while already playing on the last ad period.
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 123_000_000L + 10_000_001L + 10_000_010L,
/* adDurationUs= */ 10_000_100L,
/* adPositionInAdPod= */ 3,
/* totalAdDurationUs= */ 30_000_111L,
/* totalAdsInAdPod= */ 3,
adPlaybackState);
assertThat(adPlaybackState.adGroupCount).isEqualTo(2);
assertThat(adPlaybackState.getAdGroup(0).count).isEqualTo(3);
assertThat(adPlaybackState.getAdGroup(0).originalCount).isEqualTo(3);
assertThat(adPlaybackState.getAdGroup(0).states)
.asList()
.containsExactly(AD_STATE_PLAYED, AD_STATE_PLAYED, AD_STATE_AVAILABLE)
.inOrder();
assertThat(adPlaybackState.getAdGroup(0).durationsUs)
.asList()
.containsExactly(10_000_001L, 10_000_010L, 10_000_100L)
.inOrder();
assertThat(adPlaybackState.getAdGroup(0).contentResumeOffsetUs).isEqualTo(30_000_111L);
// Additional pre-fetch LOADED event with no remaining unavailable ad slot increases ad count.
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 123_000_000
+ 10_000_001L
+ 10_000_010L
+ 10_000_100L,
/* adDurationUs= */ 10_001_000L,
/* adPositionInAdPod= */ 4,
/* totalAdDurationUs= */ 29_001_111L,
/* totalAdsInAdPod= */ 4,
adPlaybackState);
// Player advances to the content period:
// [/* adGroupIndex= */ -1, /* adIndexInAdGroup */ -1, /* nextAdGroupIndex= */ 1].
// The 3rd ad period is marked as played.
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2);
assertThat(adPlaybackState.adGroupCount).isEqualTo(2);
assertThat(adPlaybackState.getAdGroup(0).count).isEqualTo(4);
assertThat(adPlaybackState.getAdGroup(0).originalCount).isEqualTo(4);
assertThat(adPlaybackState.getAdGroup(0).states)
.asList()
.containsExactly(AD_STATE_PLAYED, AD_STATE_PLAYED, AD_STATE_PLAYED, AD_STATE_AVAILABLE)
.inOrder();
assertThat(adPlaybackState.getAdGroup(0).durationsUs)
.asList()
.containsExactly(10_000_001L, 10_000_010L, 10_000_100L, 10_001_000L)
.inOrder();
assertThat(adPlaybackState.getAdGroup(0).contentResumeOffsetUs).isEqualTo(40_001_111L);
}
@Test
public void addLiveAdBreak_groupExpandsFromTwoAdsToFourAds_createsNewAdGroupAndExpands() {
AdPlaybackState adPlaybackState =
new AdPlaybackState("adsId")
.withNewAdGroup(/* adGroupIndex= */ 0, /* adGroupTimeUs= */ C.TIME_END_OF_SOURCE)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
// Initial LOADED event while playing in content.
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 123_000_000L,
/* adDurationUs= */ 10_000_001L,
/* adPositionInAdPod= */ 1,
/* totalAdDurationUs= */ 19_000_011L,
/* totalAdsInAdPod= */ 2,
adPlaybackState);
assertThat(adPlaybackState.adGroupCount).isEqualTo(2);
assertThat(adPlaybackState.getAdGroup(0).count).isEqualTo(2);
assertThat(adPlaybackState.getAdGroup(0).originalCount).isEqualTo(2);
assertThat(adPlaybackState.getAdGroup(0).timeUs).isEqualTo(123_000_000L);
assertThat(adPlaybackState.getAdGroup(0).isServerSideInserted).isTrue();
assertThat(adPlaybackState.getAdGroup(0).states)
.asList()
.containsExactly(AD_STATE_AVAILABLE, AD_STATE_UNAVAILABLE)
.inOrder();
assertThat(adPlaybackState.getAdGroup(0).durationsUs)
.asList()
.containsExactly(10_000_001L, 9_000_010L)
.inOrder();
assertThat(adPlaybackState.getAdGroup(0).contentResumeOffsetUs).isEqualTo(19_000_011);
// Second LOADED event: switch to a ad pod with 4 ads
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 123_000_000L + 10_000_001L,
/* adDurationUs= */ 10_000_010L,
/* adPositionInAdPod= */ 2,
/* totalAdDurationUs= */ 40_000_011L,
/* totalAdsInAdPod= */ 4,
adPlaybackState);
assertThat(adPlaybackState.adGroupCount).isEqualTo(2);
assertThat(adPlaybackState.getAdGroup(0).count).isEqualTo(4);
assertThat(adPlaybackState.getAdGroup(0).originalCount).isEqualTo(4);
assertThat(adPlaybackState.getAdGroup(0).timeUs).isEqualTo(123_000_000L);
assertThat(adPlaybackState.getAdGroup(0).states)
.asList()
.containsExactly(
AD_STATE_AVAILABLE, AD_STATE_AVAILABLE, AD_STATE_UNAVAILABLE, AD_STATE_UNAVAILABLE)
.inOrder();
assertThat(adPlaybackState.getAdGroup(0).durationsUs)
.asList()
.containsExactly(10_000_001L, 10_000_010L, 30_000_001L, 0L)
.inOrder();
assertThat(adPlaybackState.getAdGroup(0).contentResumeOffsetUs).isEqualTo(50_000_012L);
// Third LOADED event
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 123_000_000L + 10_000_001L + 10_000_010L,
/* adDurationUs= */ 10_000_100L,
/* adPositionInAdPod= */ 3,
/* totalAdDurationUs= */ 40_000_111L,
/* totalAdsInAdPod= */ 4,
adPlaybackState);
assertThat(adPlaybackState.adGroupCount).isEqualTo(2);
assertThat(adPlaybackState.getAdGroup(0).count).isEqualTo(4);
assertThat(adPlaybackState.getAdGroup(0).originalCount).isEqualTo(4);
assertThat(adPlaybackState.getAdGroup(0).states)
.asList()
.containsExactly(
AD_STATE_AVAILABLE, AD_STATE_AVAILABLE, AD_STATE_AVAILABLE, AD_STATE_UNAVAILABLE)
.inOrder();
assertThat(adPlaybackState.getAdGroup(0).durationsUs)
.asList()
.containsExactly(10_000_001L, 10_000_010L, 10_000_100L, 19999901L)
.inOrder();
assertThat(adPlaybackState.getAdGroup(0).contentResumeOffsetUs).isEqualTo(50_000_012L);
// Last LOADED event
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 123_000_000L
+ 10_000_001L
+ 10_000_010L
+ 10_000_100L,
/* adDurationUs= */ 10_001_000L,
/* adPositionInAdPod= */ 4,
/* totalAdDurationUs= */ 40_001_111L,
/* totalAdsInAdPod= */ 4,
adPlaybackState);
assertThat(adPlaybackState.adGroupCount).isEqualTo(2);
assertThat(adPlaybackState.getAdGroup(0).count).isEqualTo(4);
assertThat(adPlaybackState.getAdGroup(0).originalCount).isEqualTo(4);
assertThat(adPlaybackState.getAdGroup(0).states)
.asList()
.containsExactly(
AD_STATE_AVAILABLE, AD_STATE_AVAILABLE, AD_STATE_AVAILABLE, AD_STATE_AVAILABLE)
.inOrder();
assertThat(adPlaybackState.getAdGroup(0).durationsUs)
.asList()
.containsExactly(10_000_001L, 10_000_010L, 10_000_100L, 10_001_000L)
.inOrder();
assertThat(adPlaybackState.getAdGroup(0).contentResumeOffsetUs).isEqualTo(40_001_111);
}
@Test
public void addLiveAdBreak_groupExpandsFromOneToTwoAdsAfterAdGroupCompletion_createsNewAdGroup() {
AdPlaybackState adPlaybackState =
new AdPlaybackState("adsId")
.withNewAdGroup(/* adGroupIndex= */ 0, /* adGroupTimeUs= */ C.TIME_END_OF_SOURCE)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
// Initial LOADED event while playing in content.
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 123_000_000L,
/* adDurationUs= */ 10_000_001L,
/* adPositionInAdPod= */ 1,
/* totalAdDurationUs= */ 10_000_001L,
/* totalAdsInAdPod= */ 1,
adPlaybackState);
assertThat(adPlaybackState.adGroupCount).isEqualTo(2);
assertThat(adPlaybackState.getAdGroup(0).count).isEqualTo(1);
assertThat(adPlaybackState.getAdGroup(0).originalCount).isEqualTo(1);
assertThat(adPlaybackState.getAdGroup(0).timeUs).isEqualTo(123_000_000L);
assertThat(adPlaybackState.getAdGroup(0).states).asList().containsExactly(AD_STATE_AVAILABLE);
assertThat(adPlaybackState.getAdGroup(0).durationsUs).asList().containsExactly(10_000_001L);
assertThat(adPlaybackState.getAdGroup(0).contentResumeOffsetUs).isEqualTo(10_000_001L);
// Player advances to the content period:
// [/* adGroupIndex= */ -1, /* adIndexInAdGroup */ -1, /* nextAdGroupIndex= */ 1]
// The ad group is completely played.
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
// A 'late LOADED event' at the end of the completed ad group.
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 123_000_000L + 10_000_001L,
/* adDurationUs= */ 10_000_010L,
/* adPositionInAdPod= */ 2,
/* totalAdDurationUs= */ 20_000_011L,
/* totalAdsInAdPod= */ 2,
adPlaybackState);
assertThat(adPlaybackState.adGroupCount).isEqualTo(3);
assertThat(adPlaybackState.getAdGroup(1).count).isEqualTo(1);
assertThat(adPlaybackState.getAdGroup(1).originalCount).isEqualTo(2);
assertThat(adPlaybackState.getAdGroup(1).timeUs).isEqualTo(123_000_000L + 10_000_001L);
assertThat(adPlaybackState.getAdGroup(1).states).asList().containsExactly(AD_STATE_AVAILABLE);
assertThat(adPlaybackState.getAdGroup(1).durationsUs).asList().containsExactly(10_000_010L);
assertThat(adPlaybackState.getAdGroup(1).contentResumeOffsetUs).isEqualTo(10_000_010L);
}
@Test
public void addLiveAdBreak_joinInSecondAd_createsNewAdGroupAndExpands() {
AdPlaybackState adPlaybackState =
new AdPlaybackState("adsId")
.withNewAdGroup(/* adGroupIndex= */ 0, /* adGroupTimeUs= */ C.TIME_END_OF_SOURCE)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
// First LOADED event arrives with position 2 like when joining during an ad.
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 123_000_000,
/* adDurationUs= */ 10_000_000,
/* adPositionInAdPod= */ 2,
/* totalAdDurationUs= */ 30_000_000,
/* totalAdsInAdPod= */ 3,
adPlaybackState);
assertThat(adPlaybackState.adGroupCount).isEqualTo(2);
assertThat(adPlaybackState.getAdGroup(0).count).isEqualTo(2);
assertThat(adPlaybackState.getAdGroup(0).originalCount).isEqualTo(3);
assertThat(adPlaybackState.getAdGroup(0).timeUs).isEqualTo(123_000_000L);
assertThat(adPlaybackState.getAdGroup(0).isServerSideInserted).isTrue();
assertThat(adPlaybackState.getAdGroup(0).states)
.asList()
.containsExactly(AD_STATE_AVAILABLE, AD_STATE_UNAVAILABLE)
.inOrder();
assertThat(adPlaybackState.getAdGroup(0).durationsUs)
.asList()
.containsExactly(10_000_000L, 20_000_000L) // Placeholder duration.
.inOrder();
assertThat(adPlaybackState.getAdGroup(0).contentResumeOffsetUs).isEqualTo(30_000_000L);
// Second LOADED event overrides placeholder duration.
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 123_000_000L + 10_000_000L,
/* adDurationUs= */ 10_000_000,
/* adPositionInAdPod= */ 3,
/* totalAdDurationUs= */ 30_000_000,
/* totalAdsInAdPod= */ 3,
adPlaybackState);
assertThat(adPlaybackState.adGroupCount).isEqualTo(2);
assertThat(adPlaybackState.getAdGroup(0).count).isEqualTo(2);
assertThat(adPlaybackState.getAdGroup(0).originalCount).isEqualTo(3);
assertThat(adPlaybackState.getAdGroup(0).states)
.asList()
.containsExactly(AD_STATE_AVAILABLE, AD_STATE_AVAILABLE);
assertThat(adPlaybackState.getAdGroup(0).durationsUs)
.asList()
.containsExactly(10_000_000L, 10_000_000L);
assertThat(adPlaybackState.getAdGroup(0).contentResumeOffsetUs).isEqualTo(20_000_000L);
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1);
// Delayed pre-fetch LOADED event in content (creates new ad group).
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 123_000_000L + 10_000_000L + 10_000_000L,
/* adDurationUs= */ 10_000_000,
/* adPositionInAdPod= */ 4,
/* totalAdDurationUs= */ 30_000_000,
/* totalAdsInAdPod= */ 4,
adPlaybackState);
assertThat(adPlaybackState.adGroupCount).isEqualTo(3);
assertThat(adPlaybackState.getAdGroup(1).count).isEqualTo(1);
assertThat(adPlaybackState.getAdGroup(1).originalCount).isEqualTo(4);
assertThat(adPlaybackState.getAdGroup(1).states).asList().containsExactly(AD_STATE_AVAILABLE);
assertThat(adPlaybackState.getAdGroup(1).durationsUs).asList().containsExactly(10_000_000L);
assertThat(adPlaybackState.getAdGroup(1).contentResumeOffsetUs).isEqualTo(10_000_000L);
}
@Test
public void splitAdGroup_singleTrailingAdInCompletedGroup_correctlySplit() {
AdPlaybackState adPlaybackState =
new AdPlaybackState("adsId")
.withNewAdGroup(/* adGroupIndex= */ 0, /* adGroupTimeUs= */ C.TIME_END_OF_SOURCE)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 123_000_000,
/* adDurationUs= */ 10_000_000,
/* adPositionInAdPod= */ 1,
/* totalAdDurationUs= */ 10_000_000,
/* totalAdsInAdPod= */ 2,
adPlaybackState);
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 123_000_000 + 10_000_000,
/* adDurationUs= */ 10_000_000,
/* adPositionInAdPod= */ 2,
/* totalAdDurationUs= */ 10_000_000,
/* totalAdsInAdPod= */ 2,
adPlaybackState);
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 123_000_000 + 10_000_000 + 10_000_000,
/* adDurationUs= */ 15_000_000,
/* adPositionInAdPod= */ 3,
/* totalAdDurationUs= */ 45_000_000,
/* totalAdsInAdPod= */ 3,
adPlaybackState);
adPlaybackState =
adPlaybackState
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1);
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ 0);
// Split the current adGroup at ad index 2:
// [AD_STATE_PLAYED, AD_STATE_PLAYED, AD_STATE_AVAILABLE]
adPlaybackState =
splitAdGroup(adGroup, /* adGroupIndex= */ 0, /* splitIndexExclusive= */ 2, adPlaybackState);
assertThat(adPlaybackState.adGroupCount).isEqualTo(3);
assertThat(adPlaybackState.getAdGroup(0).timeUs).isEqualTo(123_000_000);
assertThat(adPlaybackState.getAdGroup(0).count).isEqualTo(2);
assertThat(adPlaybackState.getAdGroup(0).originalCount).isEqualTo(3);
assertThat(adPlaybackState.getAdGroup(0).contentResumeOffsetUs).isEqualTo(20_000_000L);
assertThat(adPlaybackState.getAdGroup(0).durationsUs)
.asList()
.containsExactly(10_000_000L, 10_000_000L);
assertThat(adPlaybackState.getAdGroup(0).states)
.asList()
.containsExactly(AD_STATE_PLAYED, AD_STATE_PLAYED);
assertThat(adPlaybackState.getAdGroup(1).timeUs).isEqualTo(123_000_000 + 20_000_000);
assertThat(adPlaybackState.getAdGroup(1).count).isEqualTo(1);
assertThat(adPlaybackState.getAdGroup(1).originalCount).isEqualTo(1);
assertThat(adPlaybackState.getAdGroup(1).contentResumeOffsetUs).isEqualTo(15_000_000L);
assertThat(adPlaybackState.getAdGroup(1).durationsUs).asList().containsExactly(15_000_000L);
assertThat(adPlaybackState.getAdGroup(1).states).asList().containsExactly(AD_STATE_AVAILABLE);
}
@Test
public void splitAdGroup_multipleTrailingAds_correctlySplit() {
AdPlaybackState adPlaybackState =
new AdPlaybackState("adsId")
.withNewAdGroup(/* adGroupIndex= */ 0, /* adGroupTimeUs= */ C.TIME_END_OF_SOURCE)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 123_000_000,
/* adDurationUs= */ 10_000_000,
/* adPositionInAdPod= */ 1,
/* totalAdDurationUs= */ 10_000_000,
/* totalAdsInAdPod= */ 1,
adPlaybackState);
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 123_000_000 + 10_000_000,
/* adDurationUs= */ 20_000_000,
/* adPositionInAdPod= */ 2,
/* totalAdDurationUs= */ 100_000_000,
/* totalAdsInAdPod= */ 4,
adPlaybackState);
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 123_000_000 + 10_000_000 + 10_000_000,
/* adDurationUs= */ 30_000_000,
/* adPositionInAdPod= */ 3,
/* totalAdDurationUs= */ 100_000_000,
/* totalAdsInAdPod= */ 4,
adPlaybackState);
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ 0);
// Split the current adGroup at ad index 1:
// [AD_STATE_PLAYED, AD_STATE_AVAILABLE, AD_STATE_AVAILABLE, AD_STATE_UNAVAILABLE]
adPlaybackState =
splitAdGroup(adGroup, /* adGroupIndex= */ 0, /* splitIndexExclusive= */ 1, adPlaybackState);
assertThat(adPlaybackState.adGroupCount).isEqualTo(3);
assertThat(adPlaybackState.getAdGroup(0).timeUs).isEqualTo(123_000_000);
assertThat(adPlaybackState.getAdGroup(0).count).isEqualTo(1);
assertThat(adPlaybackState.getAdGroup(0).originalCount).isEqualTo(4);
assertThat(adPlaybackState.getAdGroup(0).contentResumeOffsetUs).isEqualTo(10_000_000L);
assertThat(adPlaybackState.getAdGroup(0).durationsUs).asList().containsExactly(10_000_000L);
assertThat(adPlaybackState.getAdGroup(0).states).asList().containsExactly(AD_STATE_PLAYED);
assertThat(adPlaybackState.getAdGroup(1).timeUs).isEqualTo(123_000_000 + 10_000_000);
assertThat(adPlaybackState.getAdGroup(1).count).isEqualTo(3);
assertThat(adPlaybackState.getAdGroup(1).originalCount).isEqualTo(3);
assertThat(adPlaybackState.getAdGroup(1).contentResumeOffsetUs).isEqualTo(100_000_000L);
assertThat(adPlaybackState.getAdGroup(1).durationsUs)
.asList()
.containsExactly(20_000_000L, 30_000_000L, 50_000_000L)
.inOrder();
assertThat(adPlaybackState.getAdGroup(1).states)
.asList()
.containsExactly(AD_STATE_AVAILABLE, AD_STATE_AVAILABLE, AD_STATE_UNAVAILABLE)
.inOrder();
}
@Test
public void splitAdGroup_lastAdWithZeroDuration_correctlySplit() {
AdPlaybackState adPlaybackState =
new AdPlaybackState("adsId")
.withNewAdGroup(/* adGroupIndex= */ 0, /* adGroupTimeUs= */ C.TIME_END_OF_SOURCE)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 123_000_000,
/* adDurationUs= */ 10_000_000,
/* adPositionInAdPod= */ 1,
/* totalAdDurationUs= */ 10_000_000,
/* totalAdsInAdPod= */ 1,
adPlaybackState);
adPlaybackState =
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ 123_000_000 + 10_000_000,
/* adDurationUs= */ 20_000_000,
/* adPositionInAdPod= */ 2,
/* totalAdDurationUs= */ 100_000_000,
/* totalAdsInAdPod= */ 4,
adPlaybackState);
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ 0);
// Split the current adGroup at ad index 1:
// [AD_STATE_PLAYED, AD_STATE_AVAILABLE, AD_STATE_UNAVAILABLE]
adPlaybackState =
splitAdGroup(adGroup, /* adGroupIndex= */ 0, /* splitIndexExclusive= */ 1, adPlaybackState);
assertThat(adPlaybackState.adGroupCount).isEqualTo(3);
assertThat(adPlaybackState.getAdGroup(0).timeUs).isEqualTo(123_000_000);
assertThat(adPlaybackState.getAdGroup(0).count).isEqualTo(1);
assertThat(adPlaybackState.getAdGroup(0).originalCount).isEqualTo(4);
assertThat(adPlaybackState.getAdGroup(0).contentResumeOffsetUs).isEqualTo(10_000_000L);
assertThat(adPlaybackState.getAdGroup(0).durationsUs).asList().containsExactly(10_000_000L);
assertThat(adPlaybackState.getAdGroup(0).states).asList().containsExactly(AD_STATE_PLAYED);
assertThat(adPlaybackState.getAdGroup(1).timeUs).isEqualTo(123_000_000 + 10_000_000);
assertThat(adPlaybackState.getAdGroup(1).count).isEqualTo(3);
assertThat(adPlaybackState.getAdGroup(1).originalCount).isEqualTo(3);
assertThat(adPlaybackState.getAdGroup(1).contentResumeOffsetUs).isEqualTo(100_000_000L);
assertThat(adPlaybackState.getAdGroup(1).durationsUs)
.asList()
.containsExactly(20_000_000L, 80_000_000L, 0L)
.inOrder();
assertThat(adPlaybackState.getAdGroup(1).states)
.asList()
.containsExactly(AD_STATE_AVAILABLE, AD_STATE_UNAVAILABLE, AD_STATE_UNAVAILABLE)
.inOrder();
}
}