Commit 0767cb97 by bachinger Committed by Marc Baechinger

Add live ad breaks for DASH multi-period streams

This includes:

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

PiperOrigin-RevId: 520919236
parent b18fb368
......@@ -15,9 +15,15 @@
*/
package com.google.android.exoplayer2.ext.ima;
import static com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType.LOADED;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.addLiveAdBreak;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.expandAdGroupPlaceholder;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexInMultiPeriodWindow;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexInLiveMultiPeriodTimeline;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexInVodMultiPeriodTimeline;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupDurationUsForLiveAdPeriodIndex;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.getWindowStartTimeUs;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.handleAdPeriodRemovedFromTimeline;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.maybeCorrectPreviouslyUnknownAdDuration;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.secToMsRounded;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.secToUsRounded;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.splitAdGroup;
......@@ -545,7 +551,9 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
componentListener =
new ComponentListener(
isLiveStream
? (isDashStream ? new NoopAdEventListener() : new SinglePeriodLiveAdEventListener())
? (isDashStream
? new MultiPeriodLiveAdEventListener()
: new SinglePeriodLiveAdEventListener())
: new VodAdEventListener());
adPlaybackState = adsLoader.getAdPlaybackState(adsId);
}
......@@ -677,6 +685,12 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
if (contentTimeline.equals(this.contentTimeline)) {
return;
}
if (isLiveStream && Objects.equals(streamRequest.getFormat(), StreamFormat.DASH)) {
// If the ad started playing while the corresponding period in the timeline had an unknown
// duration, the ad duration is estimated and needs to be corrected when the actual duration
// is reported.
adPlaybackState = maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState);
}
this.contentTimeline = contentTimeline;
invalidateServerSideAdInsertionAdPlaybackState();
}
......@@ -685,7 +699,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
private void invalidateServerSideAdInsertionAdPlaybackState() {
if (!adPlaybackState.equals(AdPlaybackState.NONE) && contentTimeline != null) {
ImmutableMap<Object, AdPlaybackState> splitAdPlaybackStates;
if (streamRequest.getFormat() == StreamFormat.DASH) {
if (Objects.equals(streamRequest.getFormat(), StreamFormat.DASH)) {
// DASH ad groups are always split by period.
splitAdPlaybackStates = splitAdPlaybackStateForPeriods(adPlaybackState, contentTimeline);
} else {
......@@ -815,8 +829,9 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
@Player.DiscontinuityReason int reason) {
if (reason != Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
// Only auto transitions within the same or to the next media item are of interest.
if (!(reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION
|| (isLiveStream && reason == Player.DISCONTINUITY_REASON_REMOVE))) {
// Only auto transitions and removals of an ad period in live streams need to be handled.
return;
}
......@@ -843,9 +858,21 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
Timeline.Window window =
timeline.getWindow(oldPosition.mediaItemIndex, new Timeline.Window());
if (window.lastPeriodIndex > window.firstPeriodIndex) {
if (reason == Player.DISCONTINUITY_REASON_REMOVE) {
setAdPlaybackState(
handleAdPeriodRemovedFromTimeline(
player.getCurrentPeriodIndex(), timeline, adPlaybackState));
return;
}
// Map adGroupIndex and adIndexInAdGroup to multi-period window.
Pair<Integer, Integer> adGroupIndexAndAdIndexInAdGroup =
getAdGroupAndIndexInMultiPeriodWindow(
window.isLive()
? getAdGroupAndIndexInLiveMultiPeriodTimeline(
oldPosition.mediaItemIndex,
oldPosition.periodIndex - window.firstPeriodIndex,
timeline,
adPlaybackState)
: getAdGroupAndIndexInVodMultiPeriodTimeline(
oldPosition.periodIndex - window.firstPeriodIndex,
adPlaybackState,
checkNotNull(contentTimeline));
......@@ -926,9 +953,9 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
@Override
public boolean onAdPlaybackStateUpdateRequested(Timeline contentTimeline) {
mainHandler.post(() -> setContentTimeline(contentTimeline));
// Defer source refresh to ad playback state update for VOD. Refresh immediately when live
// with single period.
return !isLiveStream || contentTimeline.getPeriodCount() > 1;
// Defer source refresh to ad playback state update for VOD (wait for potential ad cue points)
// or DASH (split manifest).
return !isLiveStream || Objects.equals(streamRequest.getFormat(), StreamFormat.DASH);
}
}
......@@ -1368,7 +1395,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
private class SinglePeriodLiveAdEventListener implements AdEventListener {
@Override
public void onAdEvent(AdEvent event) {
if (event.getType() != AdEvent.AdEventType.LOADED) {
if (!Objects.equals(event.getType(), LOADED)) {
return;
}
AdPlaybackState newAdPlaybackState = adPlaybackState;
......@@ -1396,14 +1423,40 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
}
}
private static class NoopAdEventListener implements AdEventListener {
private class MultiPeriodLiveAdEventListener implements AdEventListener {
@Override
public void onAdEvent(AdEvent event) {
Log.w(
"ImaSSAIMediaSource",
String.format(
"Ignoring IMA ad event %s because the current stream type is not supported.",
event.getType().name()));
if (!Objects.equals(event.getType(), LOADED)) {
return;
}
AdPodInfo adPodInfo = event.getAd().getAdPodInfo();
Timeline timeline = player.getCurrentTimeline();
Timeline.Window window = new Timeline.Window();
Timeline.Period adPeriod = new Timeline.Period();
// In case all periods are in the live window, we need the correct ad group duration when
// inserting the first ad. Try calculate ad group duration from media structure.
long totalAdDurationUs =
getAdGroupDurationUsForLiveAdPeriodIndex(
timeline,
adPodInfo,
/* adPeriodIndex= */ player.getCurrentPeriodIndex(),
window,
adPeriod);
long adPeriodStartTimeUs =
getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs)
+ adPeriod.positionInWindowUs;
long adDurationUs =
adPeriod.durationUs != C.TIME_UNSET
? adPeriod.durationUs
: secToUsRounded(event.getAd().getDuration());
setAdPlaybackState(
addLiveAdBreak(
/* currentContentPeriodPositionUs= */ adPeriodStartTimeUs,
adDurationUs,
adPodInfo.getAdPosition(),
totalAdDurationUs,
adPodInfo.getTotalAds(),
adPlaybackState));
}
}
}
......@@ -70,7 +70,7 @@ public class FakeMultiPeriodLiveTimeline extends Timeline {
* availabilityStartTimeUs}. True is an ad period, and false a content period.
* @param isContentTimeline Whether the timeline is a content timeline without {@link
* AdPlaybackState}s.
* @param populateAds Whether to populate ads like after the ad event has been received. This
* @param populateAds Whether to populate ads in the same way if an ad event has been received.
* @param playedAds Whether ads should be marked as played if populated.
*/
public FakeMultiPeriodLiveTimeline(
......@@ -131,6 +131,7 @@ public class FakeMultiPeriodLiveTimeline extends Timeline {
@Override
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
checkArgument(windowIndex == 0);
MediaItem.LiveConfiguration liveConfiguration =
new MediaItem.LiveConfiguration.Builder().build();
window.set(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment