Commit d31ff0f9 by bachinger Committed by Ian Baker

Correct ad durations when timeline moves more than a single period

This change improves `ImaUtil.maybeCorrectPreviouslyUnknownAdDuration` to
handles the case when the timeline moves forward more than a single period
while an ad group with unknown period duration is being played.

PiperOrigin-RevId: 522292612
parent 5e9b0c2f
...@@ -23,7 +23,7 @@ import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexIn ...@@ -23,7 +23,7 @@ import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexIn
import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupDurationUsForLiveAdPeriodIndex; 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.getWindowStartTimeUs;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.handleAdPeriodRemovedFromTimeline; 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.maybeCorrectPreviouslyUnknownAdDurations;
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.splitAdGroup;
...@@ -689,7 +689,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou ...@@ -689,7 +689,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
// If the ad started playing while the corresponding period in the timeline had an unknown // 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 // duration, the ad duration is estimated and needs to be corrected when the actual duration
// is reported. // is reported.
adPlaybackState = maybeCorrectPreviouslyUnknownAdDuration(contentTimeline, adPlaybackState); adPlaybackState = maybeCorrectPreviouslyUnknownAdDurations(contentTimeline, adPlaybackState);
} }
this.contentTimeline = contentTimeline; this.contentTimeline = contentTimeline;
invalidateServerSideAdInsertionAdPlaybackState(); invalidateServerSideAdInsertionAdPlaybackState();
......
...@@ -35,6 +35,7 @@ import android.view.View; ...@@ -35,6 +35,7 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.annotation.CheckResult; import androidx.annotation.CheckResult;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.ads.interactivemedia.v3.api.Ad;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
import com.google.ads.interactivemedia.v3.api.AdError; import com.google.ads.interactivemedia.v3.api.AdError;
import com.google.ads.interactivemedia.v3.api.AdErrorEvent; import com.google.ads.interactivemedia.v3.api.AdErrorEvent;
...@@ -537,23 +538,30 @@ import java.util.Set; ...@@ -537,23 +538,30 @@ import java.util.Set;
} }
/** /**
* Updates a previously estimated ad duration with the period duration from the timeline. * Updates previously inserted ad durations with actual period durations from the timeline and
* returns the updated {@linkplain AdPlaybackState ad playback state}.
* *
* <p>This method must only be called for multi period live streams and is useful in the case that * <p>This method must only be called for multi period live streams and is useful in the case that
* an ad started playing while its period duration was still unknown. In this case the estimated * {@linkplain #addLiveAdBreak(long, long, int, long, int, AdPlaybackState) a live ad has been
* ad duration was used which can be corrected as soon as the {@code contentTimeline} was * inserted} while the duration of the corresponding period was still unknown. In this case the
* refreshed with the actual period duration. * {@linkplain Ad#getDuration() estimated ad duration} was used which must be corrected as soon as
* the live window of the {@code contentTimeline} advances and the previously unknown period
* duration is available.
* *
* <p>The method queries the {@linkplain AdPlaybackState ad playback state} for an ad that starts * <p>Roughly, the logic checks whether an ad group of the ad playback state fits in or overlaps
* at the period start time of the last period that has a known duration. If found, the ad * one or several periods in the content timeline. Starting at the first ad inside the window, the
* duration is set to the period duration and the new ad playback state is returned. If not found * ad duration is set to the duration of the corresponding period until a period with an unknown
* or the duration is already correct the ad playback state remains unchanged. * duration or the end of the ad group is reached.
*
* <p>If the previously playing ad period isn't available in the content timeline anymore, no
* correction is applied. The resulting position discontinuity of {@link
* Player#DISCONTINUITY_REASON_REMOVE} needs to be handled accordingly elsewhere.
* *
* @param contentTimeline The live content timeline. * @param contentTimeline The live content timeline.
* @param adPlaybackState The ad playback state. * @param adPlaybackState The ad playback state.
* @return The (potentially) updated ad playback state. * @return The (potentially) updated ad playback state.
*/ */
public static AdPlaybackState maybeCorrectPreviouslyUnknownAdDuration( public static AdPlaybackState maybeCorrectPreviouslyUnknownAdDurations(
Timeline contentTimeline, AdPlaybackState adPlaybackState) { Timeline contentTimeline, AdPlaybackState adPlaybackState) {
Timeline.Window window = contentTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()); Timeline.Window window = contentTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window());
if (window.firstPeriodIndex == window.lastPeriodIndex || adPlaybackState.adGroupCount < 2) { if (window.firstPeriodIndex == window.lastPeriodIndex || adPlaybackState.adGroupCount < 2) {
...@@ -561,52 +569,69 @@ import java.util.Set; ...@@ -561,52 +569,69 @@ import java.util.Set;
return adPlaybackState; return adPlaybackState;
} }
Timeline.Period period = new Timeline.Period(); Timeline.Period period = new Timeline.Period();
// Get the first period from the end with a known duration. int lastPeriodIndex = window.lastPeriodIndex;
int periodIndex = window.lastPeriodIndex; if (contentTimeline.getPeriod(lastPeriodIndex, period).durationUs == C.TIME_UNSET) {
while (periodIndex >= window.firstPeriodIndex lastPeriodIndex--;
&& contentTimeline.getPeriod(periodIndex, period).durationUs == C.TIME_UNSET) { contentTimeline.getPeriod(lastPeriodIndex, period);
periodIndex--;
} }
// Search for an ad group at or before the period start. // Search for an unplayed ad group at or before the period start.
long windowStartTimeUs = long windowStartTimeUs =
getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs); getWindowStartTimeUs(window.windowStartTimeMs, window.positionInFirstPeriodUs);
long periodStartTimeUs = windowStartTimeUs + period.positionInWindowUs; long lastCompletePeriodStartTimeUs = windowStartTimeUs + period.positionInWindowUs;
int adGroupIndex = int adGroupIndex =
adPlaybackState.getAdGroupIndexForPositionUs( adPlaybackState.getAdGroupIndexForPositionUs(
periodStartTimeUs, /* periodDurationUs= */ C.TIME_UNSET); lastCompletePeriodStartTimeUs, /* periodDurationUs= */ C.TIME_UNSET);
if (adGroupIndex == C.INDEX_UNSET) { if (adGroupIndex == C.INDEX_UNSET) {
// No ad group at or before the period start. // No unplayed ads before the last period with a duration. Nothing to do.
return adPlaybackState; return adPlaybackState;
} }
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex); AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
if (adGroup.timeUs + adGroup.contentResumeOffsetUs < periodStartTimeUs) {
// Ad group ends before the period starts. long periodStartTimeUs = windowStartTimeUs - window.positionInFirstPeriodUs;
if (adGroup.timeUs + adGroup.contentResumeOffsetUs <= periodStartTimeUs) {
// Ad group ends before first period in window. Discontinuity of reason REMOVE.
return adPlaybackState; return adPlaybackState;
} }
// Period is inside the ad group. Get ad start that matches the period start. // The ads at the start of the ad group may be out of the window already. Skip them.
long adGroupDurationUs = 0; int firstAdIndexInWindow = 0;
for (int adIndex = 0; adIndex < adGroup.durationsUs.length; adIndex++) { long adStartTimeUs = adGroup.timeUs;
long adDurationUs = adGroup.durationsUs[adIndex]; while (adStartTimeUs < periodStartTimeUs) {
if (adGroup.timeUs + adGroupDurationUs < periodStartTimeUs) { if (adGroup.states[firstAdIndexInWindow] == AD_STATE_AVAILABLE) {
adGroupDurationUs += adDurationUs; // The previously available ad is not in the timeline anymore. Discontinuity of reason
continue; // `DISCONTINUITY_REASON_REMOVE`.
}
if (period.durationUs == adDurationUs) {
// No update required.
return adPlaybackState; return adPlaybackState;
} }
// Skip ad before first period of window.
adStartTimeUs += adGroup.durationsUs[firstAdIndexInWindow++];
}
int firstPeriodIndexInAdGroup = C.INDEX_UNSET;
for (int i = window.firstPeriodIndex; i <= lastPeriodIndex; i++) {
if (adGroup.timeUs <= periodStartTimeUs) {
firstPeriodIndexInAdGroup = i;
break;
}
periodStartTimeUs += contentTimeline.getPeriod(/* periodIndex= */ i, period).durationUs;
}
checkState(firstPeriodIndexInAdGroup != C.INDEX_UNSET);
// Update all ad durations that we know and are not yet correct.
for (int i = firstAdIndexInWindow; i < adGroup.durationsUs.length; i++) {
int adPeriodIndex = firstPeriodIndexInAdGroup + (i - firstAdIndexInWindow);
if (adPeriodIndex > lastPeriodIndex) {
break;
}
contentTimeline.getPeriod(adPeriodIndex, period);
if (period.durationUs != adGroup.durationsUs[i]) {
// Set the ad duration to the period duration. // Set the ad duration to the period duration.
adPlaybackState = adPlaybackState =
updateAdDurationInAdGroup( updateAdDurationInAdGroup(
adGroupIndex, /* adIndexInAdGroup= */ adIndex, period.durationUs, adPlaybackState); adGroupIndex, /* adIndexInAdGroup= */ i, period.durationUs, adPlaybackState);
}
}
// Get the ad group again and set the new content resume offset after update. // Get the ad group again and set the new content resume offset after update.
adGroupDurationUs = sum(adPlaybackState.getAdGroup(adGroupIndex).durationsUs); long adGroupDurationUs = sum(adPlaybackState.getAdGroup(adGroupIndex).durationsUs);
return adPlaybackState.withContentResumeOffsetUs(adGroupIndex, adGroupDurationUs); return adPlaybackState.withContentResumeOffsetUs(adGroupIndex, adGroupDurationUs);
} }
// Return unchanged.
return adPlaybackState;
}
/** /**
* Returns the sum of the durations of all the ads in an ad pod, in microseconds. * Returns the sum of the durations of all the ads in an ad pod, in microseconds.
......
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