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/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSource.java
index c0a1828be6..3a4a1f4182 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSource.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSource.java
@@ -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);
}
}
}
diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java
index 1f410a2e36..9929003b58 100644
--- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java
+++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java
@@ -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 {
diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java
index 5619c7f13e..9c24e62009 100644
--- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java
+++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java
@@ -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.
+ *
+ *
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