Skip to content

Commit

Permalink
Add live ad breaks for DASH multi-period streams
Browse files Browse the repository at this point in the history
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
  • Loading branch information
marcbaechinger committed Apr 5, 2023
1 parent b18fb36 commit 0767cb9
Show file tree
Hide file tree
Showing 4 changed files with 1,213 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -545,7 +551,9 @@ private ImaServerSideAdInsertionMediaSource(
componentListener =
new ComponentListener(
isLiveStream
? (isDashStream ? new NoopAdEventListener() : new SinglePeriodLiveAdEventListener())
? (isDashStream
? new MultiPeriodLiveAdEventListener()
: new SinglePeriodLiveAdEventListener())
: new VodAdEventListener());
adPlaybackState = adsLoader.getAdPlaybackState(adsId);
}
Expand Down Expand Up @@ -677,6 +685,12 @@ private void setContentTimeline(Timeline contentTimeline) {
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();
}
Expand All @@ -685,7 +699,7 @@ private void setContentTimeline(Timeline contentTimeline) {
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 {
Expand Down Expand Up @@ -815,8 +829,9 @@ public void onPositionDiscontinuity(
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;
}

Expand All @@ -843,12 +858,24 @@ public void onPositionDiscontinuity(
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(
oldPosition.periodIndex - window.firstPeriodIndex,
adPlaybackState,
checkNotNull(contentTimeline));
window.isLive()
? getAdGroupAndIndexInLiveMultiPeriodTimeline(
oldPosition.mediaItemIndex,
oldPosition.periodIndex - window.firstPeriodIndex,
timeline,
adPlaybackState)
: getAdGroupAndIndexInVodMultiPeriodTimeline(
oldPosition.periodIndex - window.firstPeriodIndex,
adPlaybackState,
checkNotNull(contentTimeline));
adGroupIndex = adGroupIndexAndAdIndexInAdGroup.first;
adIndexInAdGroup = adGroupIndexAndAdIndexInAdGroup.second;
}
Expand Down Expand Up @@ -926,9 +953,9 @@ public void onAdEvent(AdEvent event) {
@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);
}
}

Expand Down Expand Up @@ -1368,7 +1395,7 @@ public void onAdEvent(AdEvent event) {
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;
Expand Down Expand Up @@ -1396,14 +1423,40 @@ public void onAdEvent(AdEvent event) {
}
}

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));
}
}
}
Loading

0 comments on commit 0767cb9

Please sign in to comment.