diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json
index 09688fa73a..ac7b5ce749 100644
--- a/demos/main/src/main/assets/media.exolist.json
+++ b/demos/main/src/main/assets/media.exolist.json
@@ -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"
},
{
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaServerSideAdInsertionMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaServerSideAdInsertionMediaSource.java
index c05a91b0b8..634e036de9 100644
--- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaServerSideAdInsertionMediaSource.java
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaServerSideAdInsertionMediaSource.java
@@ -15,20 +15,21 @@
*/
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.expandAdGroupPlaceholder;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexInMultiPeriodWindow;
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.splitAdGroup;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.splitAdPlaybackStateForPeriods;
-import static com.google.android.exoplayer2.ext.ima.ImaUtil.updateAdDurationAndPropagate;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.updateAdDurationInAdGroup;
+import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_AVAILABLE;
+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.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Util.msToUs;
-import static com.google.android.exoplayer2.util.Util.sum;
import static com.google.android.exoplayer2.util.Util.usToMs;
-import static java.lang.Math.min;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.content.Context;
@@ -89,6 +90,7 @@ import com.google.android.exoplayer2.upstream.Loader.Loadable;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ConditionVariable;
+import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -449,6 +451,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;
@@ -470,7 +474,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,
@@ -718,46 +721,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();
@@ -813,11 +776,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);
}
}
}
@@ -885,12 +864,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 {
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java
index 966efd1020..7a5e90c149 100644
--- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java
@@ -15,6 +15,10 @@
*/
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_UNAVAILABLE;
+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.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
@@ -44,8 +48,10 @@ import com.google.ads.interactivemedia.v3.api.UiElement;
import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
+import com.google.android.exoplayer2.source.ads.AdPlaybackState.AdGroup;
import com.google.android.exoplayer2.ui.AdOverlayInfo;
import com.google.android.exoplayer2.ui.AdViewProvider;
import com.google.android.exoplayer2.upstream.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.
+ *
+ *
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.
*
*
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 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.
+ *
+ * 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.
+ *
+ *
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:
+ *
+ *
+ * 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.
+ * 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.
+ * 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.
+ * 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}.
+ *
+ *
+ * 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).
+ *
+ *
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.
+ *
+ *
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.
*
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaUtilTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaUtilTest.java
index 922ddace34..ad094ced27 100644
--- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaUtilTest.java
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaUtilTest.java
@@ -15,7 +15,14 @@
*/
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.getAdGroupAndIndexInMultiPeriodWindow;
+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_ERROR;
+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_UNAVAILABLE;
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.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();
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSource.java
index 436b57dce0..ab1cf9c9ba 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSource.java
@@ -158,16 +158,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);
}
}
}