mirror of
https://github.com/androidx/media.git
synced 2025-05-15 19:49:50 +08:00
Add support for timing out ad preloading
Detect stuck buffering cases in ImaAdsLoader, and discard the ad group after a timeout. This is intended to make the IMA extension more robust in the case where an ad group unexpectedly doesn't load. The timing out behavior is enabled by default but apps can choose to retain the old behavior by setting an unset timeout on ImaAdsLoader.Builder. PiperOrigin-RevId: 311729798
This commit is contained in:
parent
e8c7405545
commit
51b2a0f7a9
@ -52,8 +52,12 @@
|
||||
([#7234](https://github.com/google/ExoPlayer/issues/7234)).
|
||||
* AV1 extension: Add a heuristic to determine the default number of threads
|
||||
used for AV1 playback using the extension.
|
||||
* IMA extension: Upgrade to IMA SDK version 3.19.0, and migrate to new
|
||||
preloading APIs ([#6429](https://github.com/google/ExoPlayer/issues/6429)).
|
||||
* IMA extension:
|
||||
* Upgrade to IMA SDK version 3.19.0, and migrate to new
|
||||
preloading APIs
|
||||
([#6429](https://github.com/google/ExoPlayer/issues/6429)).
|
||||
* Add support for timing out ad preloading, to avoid playback getting
|
||||
stuck if an ad group unexpectedly fails to load.
|
||||
|
||||
### 2.11.4 (2020-04-08) ###
|
||||
|
||||
|
@ -102,11 +102,23 @@ public final class ImaAdsLoader
|
||||
/** Builder for {@link ImaAdsLoader}. */
|
||||
public static final class Builder {
|
||||
|
||||
/**
|
||||
* The default duration in milliseconds for which the player must buffer while preloading an ad
|
||||
* group before that ad group is skipped and marked as having failed to load.
|
||||
*
|
||||
* <p>This value should be large enough not to trigger discarding the ad when it actually might
|
||||
* load soon, but small enough so that user is not waiting for too long.
|
||||
*
|
||||
* @see #setAdPreloadTimeoutMs(long)
|
||||
*/
|
||||
public static final long DEFAULT_AD_PRELOAD_TIMEOUT_MS = 10 * C.MILLIS_PER_SECOND;
|
||||
|
||||
private final Context context;
|
||||
|
||||
@Nullable private ImaSdkSettings imaSdkSettings;
|
||||
@Nullable private AdEventListener adEventListener;
|
||||
@Nullable private Set<UiElement> adUiElements;
|
||||
private long adPreloadTimeoutMs;
|
||||
private int vastLoadTimeoutMs;
|
||||
private int mediaLoadTimeoutMs;
|
||||
private int mediaBitrate;
|
||||
@ -120,6 +132,7 @@ public final class ImaAdsLoader
|
||||
*/
|
||||
public Builder(Context context) {
|
||||
this.context = Assertions.checkNotNull(context);
|
||||
adPreloadTimeoutMs = DEFAULT_AD_PRELOAD_TIMEOUT_MS;
|
||||
vastLoadTimeoutMs = TIMEOUT_UNSET;
|
||||
mediaLoadTimeoutMs = TIMEOUT_UNSET;
|
||||
mediaBitrate = BITRATE_UNSET;
|
||||
@ -165,6 +178,25 @@ public final class ImaAdsLoader
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the duration in milliseconds for which the player must buffer while preloading an ad
|
||||
* group before that ad group is skipped and marked as having failed to load. Pass {@link
|
||||
* C#TIME_UNSET} if there should be no such timeout. The default value is {@value
|
||||
* DEFAULT_AD_PRELOAD_TIMEOUT_MS} ms.
|
||||
*
|
||||
* <p>The purpose of this timeout is to avoid playback getting stuck in the unexpected case that
|
||||
* the IMA SDK does not load an ad break based on the player's reported content position.
|
||||
*
|
||||
* @param adPreloadTimeoutMs The timeout buffering duration in milliseconds, or {@link
|
||||
* C#TIME_UNSET} for no timeout.
|
||||
* @return This builder, for convenience.
|
||||
*/
|
||||
public Builder setAdPreloadTimeoutMs(long adPreloadTimeoutMs) {
|
||||
Assertions.checkArgument(adPreloadTimeoutMs == C.TIME_UNSET || adPreloadTimeoutMs > 0);
|
||||
this.adPreloadTimeoutMs = adPreloadTimeoutMs;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the VAST load timeout, in milliseconds.
|
||||
*
|
||||
@ -238,6 +270,7 @@ public final class ImaAdsLoader
|
||||
adTagUri,
|
||||
imaSdkSettings,
|
||||
/* adsResponse= */ null,
|
||||
adPreloadTimeoutMs,
|
||||
vastLoadTimeoutMs,
|
||||
mediaLoadTimeoutMs,
|
||||
mediaBitrate,
|
||||
@ -260,6 +293,7 @@ public final class ImaAdsLoader
|
||||
/* adTagUri= */ null,
|
||||
imaSdkSettings,
|
||||
adsResponse,
|
||||
adPreloadTimeoutMs,
|
||||
vastLoadTimeoutMs,
|
||||
mediaLoadTimeoutMs,
|
||||
mediaBitrate,
|
||||
@ -291,7 +325,12 @@ public final class ImaAdsLoader
|
||||
* Threshold before the end of content at which IMA is notified that content is complete if the
|
||||
* player buffers, in milliseconds.
|
||||
*/
|
||||
private static final long END_OF_CONTENT_THRESHOLD_MS = 5000;
|
||||
private static final long THRESHOLD_END_OF_CONTENT_MS = 5000;
|
||||
/**
|
||||
* Threshold before the start of an ad at which IMA is expected to be able to preload the ad, in
|
||||
* milliseconds.
|
||||
*/
|
||||
private static final long THRESHOLD_AD_PRELOAD_MS = 4000;
|
||||
|
||||
private static final int TIMEOUT_UNSET = -1;
|
||||
private static final int BITRATE_UNSET = -1;
|
||||
@ -317,6 +356,7 @@ public final class ImaAdsLoader
|
||||
|
||||
@Nullable private final Uri adTagUri;
|
||||
@Nullable private final String adsResponse;
|
||||
private final long adPreloadTimeoutMs;
|
||||
private final int vastLoadTimeoutMs;
|
||||
private final int mediaLoadTimeoutMs;
|
||||
private final boolean focusSkipButtonWhenAvailable;
|
||||
@ -398,6 +438,11 @@ public final class ImaAdsLoader
|
||||
private long pendingContentPositionMs;
|
||||
/** Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. */
|
||||
private boolean sentPendingContentPositionMs;
|
||||
/**
|
||||
* Stores the real time in milliseconds at which the player started buffering, possibly due to not
|
||||
* having preloaded an ad, or {@link C#TIME_UNSET} if not applicable.
|
||||
*/
|
||||
private long waitingForPreloadElapsedRealtimeMs;
|
||||
|
||||
/**
|
||||
* Creates a new IMA ads loader.
|
||||
@ -415,6 +460,7 @@ public final class ImaAdsLoader
|
||||
adTagUri,
|
||||
/* imaSdkSettings= */ null,
|
||||
/* adsResponse= */ null,
|
||||
/* adPreloadTimeoutMs= */ Builder.DEFAULT_AD_PRELOAD_TIMEOUT_MS,
|
||||
/* vastLoadTimeoutMs= */ TIMEOUT_UNSET,
|
||||
/* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,
|
||||
/* mediaBitrate= */ BITRATE_UNSET,
|
||||
@ -430,6 +476,7 @@ public final class ImaAdsLoader
|
||||
@Nullable Uri adTagUri,
|
||||
@Nullable ImaSdkSettings imaSdkSettings,
|
||||
@Nullable String adsResponse,
|
||||
long adPreloadTimeoutMs,
|
||||
int vastLoadTimeoutMs,
|
||||
int mediaLoadTimeoutMs,
|
||||
int mediaBitrate,
|
||||
@ -440,6 +487,7 @@ public final class ImaAdsLoader
|
||||
Assertions.checkArgument(adTagUri != null || adsResponse != null);
|
||||
this.adTagUri = adTagUri;
|
||||
this.adsResponse = adsResponse;
|
||||
this.adPreloadTimeoutMs = adPreloadTimeoutMs;
|
||||
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
|
||||
this.mediaLoadTimeoutMs = mediaLoadTimeoutMs;
|
||||
this.mediaBitrate = mediaBitrate;
|
||||
@ -473,6 +521,7 @@ public final class ImaAdsLoader
|
||||
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
|
||||
fakeContentProgressOffsetMs = C.TIME_UNSET;
|
||||
pendingContentPositionMs = C.TIME_UNSET;
|
||||
waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET;
|
||||
contentDurationMs = C.TIME_UNSET;
|
||||
timeline = Timeline.EMPTY;
|
||||
adPlaybackState = AdPlaybackState.NONE;
|
||||
@ -636,6 +685,7 @@ public final class ImaAdsLoader
|
||||
imaPausedContent = false;
|
||||
imaAdState = IMA_AD_STATE_NONE;
|
||||
imaAdMediaInfo = null;
|
||||
stopUpdatingAdProgress();
|
||||
imaAdInfo = null;
|
||||
pendingAdLoadError = null;
|
||||
adPlaybackState = AdPlaybackState.NONE;
|
||||
@ -737,6 +787,19 @@ public final class ImaAdsLoader
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Content progress: " + videoProgressUpdate);
|
||||
}
|
||||
|
||||
if (waitingForPreloadElapsedRealtimeMs != C.TIME_UNSET) {
|
||||
// IMA is polling the player position but we are buffering for an ad to preload, so playback
|
||||
// may be stuck. Detect this case and signal an error if applicable.
|
||||
long stuckElapsedRealtimeMs =
|
||||
SystemClock.elapsedRealtime() - waitingForPreloadElapsedRealtimeMs;
|
||||
if (stuckElapsedRealtimeMs >= THRESHOLD_AD_PRELOAD_MS) {
|
||||
waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET;
|
||||
handleAdGroupLoadError(new IOException("Ad preloading timed out"));
|
||||
maybeNotifyPendingAdLoadError();
|
||||
}
|
||||
}
|
||||
|
||||
return videoProgressUpdate;
|
||||
}
|
||||
|
||||
@ -779,10 +842,15 @@ public final class ImaAdsLoader
|
||||
// Drop events after release.
|
||||
return;
|
||||
}
|
||||
int adGroupIndex = getAdGroupIndex(adPodInfo);
|
||||
int adGroupIndex = getAdGroupIndexForAdPod(adPodInfo);
|
||||
int adIndexInAdGroup = adPodInfo.getAdPosition() - 1;
|
||||
AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup);
|
||||
adInfoByAdMediaInfo.put(adMediaInfo, adInfo);
|
||||
if (adPlaybackState.isAdInErrorState(adGroupIndex, adIndexInAdGroup)) {
|
||||
// We have already marked this ad as having failed to load, so ignore the request. IMA will
|
||||
// timeout after its media load timeout.
|
||||
return;
|
||||
}
|
||||
AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adInfo.adGroupIndex];
|
||||
if (adGroup.count == C.LENGTH_UNSET) {
|
||||
adPlaybackState =
|
||||
@ -926,10 +994,34 @@ public final class ImaAdsLoader
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
||||
@Nullable Player player = this.player;
|
||||
if (adsManager == null || player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (playbackState == Player.STATE_BUFFERING && !player.isPlayingAd()) {
|
||||
// Check whether we are waiting for an ad to preload.
|
||||
int adGroupIndex = getLoadingAdGroupIndex();
|
||||
if (adGroupIndex == C.INDEX_UNSET) {
|
||||
return;
|
||||
}
|
||||
AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];
|
||||
if (adGroup.count != C.LENGTH_UNSET
|
||||
&& adGroup.count != 0
|
||||
&& adGroup.states[0] != AdPlaybackState.AD_STATE_UNAVAILABLE) {
|
||||
// An ad is available already so we must be buffering for some other reason.
|
||||
return;
|
||||
}
|
||||
long adGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]);
|
||||
long contentPositionMs = getContentPeriodPositionMs(player, timeline, period);
|
||||
long timeUntilAdMs = adGroupTimeMs - contentPositionMs;
|
||||
if (timeUntilAdMs < adPreloadTimeoutMs) {
|
||||
waitingForPreloadElapsedRealtimeMs = SystemClock.elapsedRealtime();
|
||||
}
|
||||
} else if (playbackState == Player.STATE_READY) {
|
||||
waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
if (imaAdState == IMA_AD_STATE_PLAYING && !playWhenReady) {
|
||||
adsManager.pause();
|
||||
return;
|
||||
@ -939,6 +1031,7 @@ public final class ImaAdsLoader
|
||||
adsManager.resume();
|
||||
return;
|
||||
}
|
||||
|
||||
handlePlayerStateChanged(playWhenReady, playbackState);
|
||||
}
|
||||
|
||||
@ -1219,6 +1312,10 @@ public final class ImaAdsLoader
|
||||
Assertions.checkNotNull(imaAdInfo);
|
||||
int adGroupIndex = imaAdInfo.adGroupIndex;
|
||||
int adIndexInAdGroup = imaAdInfo.adIndexInAdGroup;
|
||||
if (adPlaybackState.isAdInErrorState(adGroupIndex, adIndexInAdGroup)) {
|
||||
// We have already marked this ad as having failed to load, so ignore the request.
|
||||
return;
|
||||
}
|
||||
adPlaybackState =
|
||||
adPlaybackState.withPlayedAd(adGroupIndex, adIndexInAdGroup).withAdResumePositionUs(0);
|
||||
updateAdPlaybackState();
|
||||
@ -1233,19 +1330,11 @@ public final class ImaAdsLoader
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Once IMA signals which ad group failed to load, clean up this code.
|
||||
long playerPositionMs = player.getContentPosition();
|
||||
int adGroupIndex =
|
||||
adPlaybackState.getAdGroupIndexForPositionUs(
|
||||
C.msToUs(playerPositionMs), C.msToUs(contentDurationMs));
|
||||
// TODO: Once IMA signals which ad group failed to load, remove this call.
|
||||
int adGroupIndex = getLoadingAdGroupIndex();
|
||||
if (adGroupIndex == C.INDEX_UNSET) {
|
||||
adGroupIndex =
|
||||
adPlaybackState.getAdGroupIndexAfterPositionUs(
|
||||
C.msToUs(playerPositionMs), C.msToUs(contentDurationMs));
|
||||
if (adGroupIndex == C.INDEX_UNSET) {
|
||||
// The error doesn't seem to relate to any ad group so give up handling it.
|
||||
return;
|
||||
}
|
||||
Log.w(TAG, "Unable to determine ad group index for ad group load error", error);
|
||||
return;
|
||||
}
|
||||
|
||||
AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];
|
||||
@ -1312,7 +1401,7 @@ public final class ImaAdsLoader
|
||||
if (!sentContentComplete
|
||||
&& contentDurationMs != C.TIME_UNSET
|
||||
&& pendingContentPositionMs == C.TIME_UNSET
|
||||
&& positionMs + END_OF_CONTENT_THRESHOLD_MS >= contentDurationMs) {
|
||||
&& positionMs + THRESHOLD_END_OF_CONTENT_MS >= contentDurationMs) {
|
||||
adsLoader.contentComplete();
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "adsLoader.contentComplete");
|
||||
@ -1350,7 +1439,7 @@ public final class ImaAdsLoader
|
||||
}
|
||||
}
|
||||
|
||||
private int getAdGroupIndex(AdPodInfo adPodInfo) {
|
||||
private int getAdGroupIndexForAdPod(AdPodInfo adPodInfo) {
|
||||
if (adPodInfo.getPodIndex() == -1) {
|
||||
// This is a postroll ad.
|
||||
return adPlaybackState.adGroupCount - 1;
|
||||
@ -1366,6 +1455,23 @@ public final class ImaAdsLoader
|
||||
throw new IllegalStateException("Failed to find cue point");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the ad group that will preload next, or {@link C#INDEX_UNSET} if there is
|
||||
* no such ad group.
|
||||
*/
|
||||
private int getLoadingAdGroupIndex() {
|
||||
long playerPositionUs =
|
||||
C.msToUs(getContentPeriodPositionMs(Assertions.checkNotNull(player), timeline, period));
|
||||
int adGroupIndex =
|
||||
adPlaybackState.getAdGroupIndexForPositionUs(playerPositionUs, C.msToUs(contentDurationMs));
|
||||
if (adGroupIndex == C.INDEX_UNSET) {
|
||||
adGroupIndex =
|
||||
adPlaybackState.getAdGroupIndexAfterPositionUs(
|
||||
playerPositionUs, C.msToUs(contentDurationMs));
|
||||
}
|
||||
return adGroupIndex;
|
||||
}
|
||||
|
||||
private String getAdMediaInfoString(AdMediaInfo adMediaInfo) {
|
||||
@Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo);
|
||||
return "AdMediaInfo[" + adMediaInfo.getUrl() + (adInfo != null ? ", " + adInfo : "") + "]";
|
||||
@ -1379,7 +1485,9 @@ public final class ImaAdsLoader
|
||||
Player player, Timeline timeline, Timeline.Period period) {
|
||||
long contentWindowPositionMs = player.getContentPosition();
|
||||
return contentWindowPositionMs
|
||||
- timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs();
|
||||
- (timeline.isEmpty()
|
||||
? 0
|
||||
: timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs());
|
||||
}
|
||||
|
||||
private static long[] getAdGroupTimesUs(List<Float> cuePoints) {
|
||||
|
@ -48,7 +48,7 @@ import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.Timeline.Period;
|
||||
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader.ImaFactory;
|
||||
import com.google.android.exoplayer2.source.MaskingMediaSource;
|
||||
import com.google.android.exoplayer2.source.MaskingMediaSource.DummyTimeline;
|
||||
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
|
||||
@ -57,6 +57,7 @@ import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@ -73,22 +74,23 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.robolectric.shadows.ShadowSystemClock;
|
||||
|
||||
/** Test for {@link ImaAdsLoader}. */
|
||||
/** Tests for {@link ImaAdsLoader}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class ImaAdsLoaderTest {
|
||||
|
||||
private static final long CONTENT_DURATION_US = 10 * C.MICROS_PER_SECOND;
|
||||
private static final Timeline CONTENT_TIMELINE =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(
|
||||
new TimelineWindowDefinition(
|
||||
/* isSeekable= */ true, /* isDynamic= */ false, CONTENT_DURATION_US));
|
||||
private static final long CONTENT_PERIOD_DURATION_US =
|
||||
CONTENT_TIMELINE.getPeriod(/* periodIndex= */ 0, new Period()).durationUs;
|
||||
private static final Uri TEST_URI = Uri.EMPTY;
|
||||
private static final AdMediaInfo TEST_AD_MEDIA_INFO = new AdMediaInfo(TEST_URI.toString());
|
||||
private static final long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND;
|
||||
private static final long[][] PREROLL_ADS_DURATIONS_US = new long[][] {{TEST_AD_DURATION_US}};
|
||||
private static final long[][] ADS_DURATIONS_US = new long[][] {{TEST_AD_DURATION_US}};
|
||||
private static final Float[] PREROLL_CUE_POINTS_SECONDS = new Float[] {0f};
|
||||
|
||||
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
|
||||
@ -140,14 +142,14 @@ public final class ImaAdsLoaderTest {
|
||||
@Test
|
||||
public void builder_overridesPlayerType() {
|
||||
when(mockImaSdkSettings.getPlayerType()).thenReturn("test player type");
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
|
||||
verify(mockImaSdkSettings).setPlayerType("google/exo.ext.ima");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void start_setsAdUiViewGroup() {
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
|
||||
verify(mockAdDisplayContainer, atLeastOnce()).setAdContainer(adViewGroup);
|
||||
@ -156,8 +158,8 @@ public final class ImaAdsLoaderTest {
|
||||
|
||||
@Test
|
||||
public void start_withPlaceholderContent_initializedAdsLoader() {
|
||||
Timeline placeholderTimeline = new MaskingMediaSource.DummyTimeline(/* tag= */ null);
|
||||
setupPlayback(placeholderTimeline, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
Timeline placeholderTimeline = new DummyTimeline(/* tag= */ null);
|
||||
setupPlayback(placeholderTimeline, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
|
||||
// We'll only create the rendering settings when initializing the ads loader.
|
||||
@ -166,26 +168,26 @@ public final class ImaAdsLoaderTest {
|
||||
|
||||
@Test
|
||||
public void start_updatesAdPlaybackState() {
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
|
||||
assertThat(adsLoaderListener.adPlaybackState)
|
||||
.isEqualTo(
|
||||
new AdPlaybackState(/* adGroupTimesUs...= */ 0)
|
||||
.withAdDurationsUs(PREROLL_ADS_DURATIONS_US)
|
||||
.withAdDurationsUs(ADS_DURATIONS_US)
|
||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startAfterRelease() {
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
imaAdsLoader.release();
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startAndCallbacksAfterRelease() {
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
imaAdsLoader.release();
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
fakeExoPlayer.setPlayingContentPosition(/* position= */ 0);
|
||||
@ -212,7 +214,7 @@ public final class ImaAdsLoaderTest {
|
||||
|
||||
@Test
|
||||
public void playback_withPrerollAd_marksAdAsPlayed() {
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
|
||||
// Load the preroll ad.
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
@ -245,14 +247,64 @@ public final class ImaAdsLoaderTest {
|
||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
||||
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, /* uri= */ TEST_URI)
|
||||
.withAdDurationsUs(PREROLL_ADS_DURATIONS_US)
|
||||
.withAdDurationsUs(ADS_DURATIONS_US)
|
||||
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
|
||||
.withAdResumePositionUs(/* adResumePositionUs= */ 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void playback_withAdNotPreloadingBeforeTimeout_hasNoError() {
|
||||
// Simulate an ad at 2 seconds.
|
||||
long adGroupPositionInWindowUs = 2 * C.MICROS_PER_SECOND;
|
||||
setupPlayback(
|
||||
CONTENT_TIMELINE,
|
||||
ADS_DURATIONS_US,
|
||||
new Float[] {(float) adGroupPositionInWindowUs / C.MICROS_PER_SECOND});
|
||||
|
||||
// Advance playback to just before the midroll and simulate buffering.
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
fakeExoPlayer.setPlayingContentPosition(C.usToMs(adGroupPositionInWindowUs));
|
||||
fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true);
|
||||
// Advance before the timeout and simulating polling content progress.
|
||||
ShadowSystemClock.advanceBy(Duration.ofSeconds(1));
|
||||
imaAdsLoader.getContentProgress();
|
||||
|
||||
assertThat(adsLoaderListener.adPlaybackState)
|
||||
.isEqualTo(
|
||||
new AdPlaybackState(/* adGroupTimesUs...= */ adGroupPositionInWindowUs)
|
||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||
.withAdDurationsUs(ADS_DURATIONS_US));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void playback_withAdNotPreloadingAfterTimeout_hasErrorAdGroup() {
|
||||
// Simulate an ad at 2 seconds.
|
||||
long adGroupPositionInWindowUs = 2 * C.MICROS_PER_SECOND;
|
||||
setupPlayback(
|
||||
CONTENT_TIMELINE,
|
||||
ADS_DURATIONS_US,
|
||||
new Float[] {(float) adGroupPositionInWindowUs / C.MICROS_PER_SECOND});
|
||||
|
||||
// Advance playback to just before the midroll and simulate buffering.
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
fakeExoPlayer.setPlayingContentPosition(C.usToMs(adGroupPositionInWindowUs));
|
||||
fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true);
|
||||
// Advance past the timeout and simulate polling content progress.
|
||||
ShadowSystemClock.advanceBy(Duration.ofSeconds(5));
|
||||
imaAdsLoader.getContentProgress();
|
||||
|
||||
assertThat(adsLoaderListener.adPlaybackState)
|
||||
.isEqualTo(
|
||||
new AdPlaybackState(/* adGroupTimesUs...= */ adGroupPositionInWindowUs)
|
||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||
.withAdDurationsUs(ADS_DURATIONS_US)
|
||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
||||
.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stop_unregistersAllVideoControlOverlays() {
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
imaAdsLoader.requestAds(adViewGroup);
|
||||
imaAdsLoader.stop();
|
||||
@ -282,31 +334,31 @@ public final class ImaAdsLoaderTest {
|
||||
List<com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener> adsLoadedListeners =
|
||||
new ArrayList<>();
|
||||
doAnswer(
|
||||
invocation -> {
|
||||
adsLoadedListeners.add(invocation.getArgument(0));
|
||||
return null;
|
||||
})
|
||||
invocation -> {
|
||||
adsLoadedListeners.add(invocation.getArgument(0));
|
||||
return null;
|
||||
})
|
||||
.when(mockAdsLoader)
|
||||
.addAdsLoadedListener(any());
|
||||
doAnswer(
|
||||
invocation -> {
|
||||
adsLoadedListeners.remove(invocation.getArgument(0));
|
||||
return null;
|
||||
})
|
||||
invocation -> {
|
||||
adsLoadedListeners.remove(invocation.getArgument(0));
|
||||
return null;
|
||||
})
|
||||
.when(mockAdsLoader)
|
||||
.removeAdsLoadedListener(any());
|
||||
when(mockAdsManagerLoadedEvent.getAdsManager()).thenReturn(mockAdsManager);
|
||||
when(mockAdsManagerLoadedEvent.getUserRequestContext())
|
||||
.thenAnswer(invocation -> mockAdsRequest.getUserRequestContext());
|
||||
doAnswer(
|
||||
(Answer<Object>)
|
||||
invocation -> {
|
||||
for (com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener listener :
|
||||
adsLoadedListeners) {
|
||||
listener.onAdsManagerLoaded(mockAdsManagerLoadedEvent);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
(Answer<Object>)
|
||||
invocation -> {
|
||||
for (com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener listener :
|
||||
adsLoadedListeners) {
|
||||
listener.onAdsManagerLoaded(mockAdsManagerLoadedEvent);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.when(mockAdsLoader)
|
||||
.requestAds(mockAdsRequest);
|
||||
|
||||
|
@ -360,6 +360,18 @@ public final class AdPlaybackState {
|
||||
return index < adGroupTimesUs.length ? index : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
/** Returns whether the specified ad has been marked as in {@link #AD_STATE_ERROR}. */
|
||||
public boolean isAdInErrorState(int adGroupIndex, int adIndexInAdGroup) {
|
||||
if (adGroupIndex >= adGroups.length) {
|
||||
return false;
|
||||
}
|
||||
AdGroup adGroup = adGroups[adGroupIndex];
|
||||
if (adGroup.count == C.LENGTH_UNSET || adIndexInAdGroup >= adGroup.count) {
|
||||
return false;
|
||||
}
|
||||
return adGroup.states[adIndexInAdGroup] == AdPlaybackState.AD_STATE_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance with the number of ads in {@code adGroupIndex} resolved to {@code adCount}.
|
||||
* The ad count must be greater than zero.
|
||||
|
@ -64,7 +64,9 @@ public final class AdPlaybackStateTest {
|
||||
|
||||
assertThat(state.adGroups[0].uris[0]).isNull();
|
||||
assertThat(state.adGroups[0].states[0]).isEqualTo(AdPlaybackState.AD_STATE_ERROR);
|
||||
assertThat(state.isAdInErrorState(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)).isTrue();
|
||||
assertThat(state.adGroups[0].states[1]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
|
||||
assertThat(state.isAdInErrorState(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
Loading…
x
Reference in New Issue
Block a user