Commit 136addf6 by bachinger Committed by microkatz

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
parent 376ee77f
...@@ -399,7 +399,7 @@ ...@@ -399,7 +399,7 @@
"uri": "ssai://dai.google.com/?contentSourceId=2528370&videoId=tears-of-steel&format=2&adsId=1" "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" "uri": "ssai://dai.google.com/?assetKey=sN_IYUG8STe1ZzhIIE_ksA&format=2&adsId=3"
}, },
{ {
......
...@@ -15,20 +15,21 @@ ...@@ -15,20 +15,21 @@
*/ */
package com.google.android.exoplayer2.ext.ima; package com.google.android.exoplayer2.ext.ima;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.addLiveAdBreak;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.expandAdGroupPlaceholder; import static com.google.android.exoplayer2.ext.ima.ImaUtil.expandAdGroupPlaceholder;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexInMultiPeriodWindow; import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexInMultiPeriodWindow;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.secToMsRounded; import static com.google.android.exoplayer2.ext.ima.ImaUtil.secToMsRounded;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.secToUsRounded; import static com.google.android.exoplayer2.ext.ima.ImaUtil.secToUsRounded;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.splitAdGroup;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.splitAdPlaybackStateForPeriods; 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.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.source.ads.ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull; 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.Assertions.checkState;
import static com.google.android.exoplayer2.util.Util.msToUs; 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 com.google.android.exoplayer2.util.Util.usToMs;
import static java.lang.Math.min;
import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.ElementType.TYPE_USE;
import android.content.Context; import android.content.Context;
...@@ -89,6 +90,7 @@ import com.google.android.exoplayer2.upstream.Loader.Loadable; ...@@ -89,6 +90,7 @@ import com.google.android.exoplayer2.upstream.Loader.Loadable;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.ConditionVariable;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
...@@ -449,6 +451,8 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou ...@@ -449,6 +451,8 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
} }
} }
private static final String TAG = "ImaSSAIMediaSource";
private final MediaItem mediaItem; private final MediaItem mediaItem;
private final Player player; private final Player player;
private final MediaSource.Factory contentMediaSourceFactory; private final MediaSource.Factory contentMediaSourceFactory;
...@@ -470,7 +474,6 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou ...@@ -470,7 +474,6 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
@Nullable private IOException loadError; @Nullable private IOException loadError;
private @MonotonicNonNull Timeline contentTimeline; private @MonotonicNonNull Timeline contentTimeline;
private AdPlaybackState adPlaybackState; private AdPlaybackState adPlaybackState;
private int firstSeenAdIndexInAdGroup;
private ImaServerSideAdInsertionMediaSource( private ImaServerSideAdInsertionMediaSource(
MediaItem mediaItem, MediaItem mediaItem,
...@@ -718,46 +721,6 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou ...@@ -718,46 +721,6 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
return adPlaybackState; 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) { private static AdPlaybackState skipAd(Ad ad, AdPlaybackState adPlaybackState) {
AdPodInfo adPodInfo = ad.getAdPodInfo(); AdPodInfo adPodInfo = ad.getAdPodInfo();
int adGroupIndex = adPodInfo.getPodIndex(); int adGroupIndex = adPodInfo.getPodIndex();
...@@ -813,11 +776,27 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou ...@@ -813,11 +776,27 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
adGroupIndex = adGroupIndexAndAdIndexInAdGroup.first; adGroupIndex = adGroupIndexAndAdIndexInAdGroup.first;
adIndexInAdGroup = adGroupIndexAndAdIndexInAdGroup.second; adIndexInAdGroup = adGroupIndexAndAdIndexInAdGroup.second;
} }
int adState = adPlaybackState.getAdGroup(adGroupIndex).states[adIndexInAdGroup];
if (adState == AdPlaybackState.AD_STATE_AVAILABLE AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
|| adState == AdPlaybackState.AD_STATE_UNAVAILABLE) { int adState = adGroup.states[adIndexInAdGroup];
setAdPlaybackState( if (adState == AD_STATE_AVAILABLE || adState == AD_STATE_UNAVAILABLE) {
adPlaybackState.withPlayedAd(adGroupIndex, /* adIndexInAdGroup= */ adIndexInAdGroup)); 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 ...@@ -885,12 +864,18 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
long positionInWindowUs = long positionInWindowUs =
timeline.getPeriod(player.getCurrentPeriodIndex(), new Timeline.Period()) timeline.getPeriod(player.getCurrentPeriodIndex(), new Timeline.Period())
.positionInWindowUs; .positionInWindowUs;
long currentPeriodPosition = msToUs(player.getContentPosition()) - positionInWindowUs; long currentContentPeriodPositionUs =
msToUs(player.getContentPosition()) - positionInWindowUs;
Ad ad = event.getAd();
AdPodInfo adPodInfo = ad.getAdPodInfo();
newAdPlaybackState = newAdPlaybackState =
addLiveAdBreak( addLiveAdBreak(
event.getAd(), currentContentPeriodPositionUs,
currentPeriodPosition, /* adDurationUs= */ secToUsRounded(ad.getDuration()),
newAdPlaybackState.equals(AdPlaybackState.NONE) /* adPositionInAdPod= */ adPodInfo.getAdPosition(),
/* totalAdDurationUs= */ secToUsRounded(adPodInfo.getMaxDuration()),
/* totalAdsInAdPod= */ adPodInfo.getTotalAds(),
/* adPlaybackState= */ newAdPlaybackState.equals(AdPlaybackState.NONE)
? new AdPlaybackState(adsId) ? new AdPlaybackState(adsId)
: newAdPlaybackState); : newAdPlaybackState);
} else { } else {
......
...@@ -158,16 +158,25 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource ...@@ -158,16 +158,25 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
checkArgument(Util.areEqual(adsId, adPlaybackState.adsId)); checkArgument(Util.areEqual(adsId, adPlaybackState.adsId));
@Nullable AdPlaybackState oldAdPlaybackState = this.adPlaybackStates.get(periodUid); @Nullable AdPlaybackState oldAdPlaybackState = this.adPlaybackStates.get(periodUid);
if (oldAdPlaybackState != null) { if (oldAdPlaybackState != null) {
for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) { for (int adGroupIndex = adPlaybackState.removedAdGroupCount;
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(i); adGroupIndex < adPlaybackState.adGroupCount;
adGroupIndex++) {
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
checkArgument(adGroup.isServerSideInserted); checkArgument(adGroup.isServerSideInserted);
if (i < oldAdPlaybackState.adGroupCount) { if (adGroupIndex < oldAdPlaybackState.adGroupCount
checkArgument( && getAdCountInGroup(adPlaybackState, /* adGroupIndex= */ adGroupIndex)
getAdCountInGroup(adPlaybackState, /* adGroupIndex= */ i) < getAdCountInGroup(oldAdPlaybackState, /* adGroupIndex= */ adGroupIndex)) {
>= getAdCountInGroup(oldAdPlaybackState, /* adGroupIndex= */ i)); // 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) { if (adGroup.timeUs == C.TIME_END_OF_SOURCE) {
checkArgument(getAdCountInGroup(adPlaybackState, /* adGroupIndex= */ i) == 0); checkArgument(
getAdCountInGroup(adPlaybackState, /* adGroupIndex= */ adGroupIndex) == 0);
} }
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment