Skip to content

Commit a5eb483

Browse files
toniheiicbaker
authored andcommitted
Clean up some preload timeout logic values in ImaAdsLoader
We used two different preload timeouts, one hardcoded to 4 seconds and one configurable (default 10 seconds) to detect slightly different versions of the same problem that IMA doesn't preload or fail an ad metadata load. This can be aligned to use the same (configurable) timeout. Secondly, this timeout should not be less than the timeout we configured on the IMA SDK itself to load the VAST data. Otherwise IMA has no chance of timing out internally first to potentially provide a better experience (e.g. loading part of the ads instead of marking the entire ad group as failed). PiperOrigin-RevId: 783298825 (cherry picked from commit f894cd5)
1 parent 5f07d7d commit a5eb483

File tree

3 files changed

+118
-11
lines changed

3 files changed

+118
-11
lines changed

libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/AdTagLoader.java

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,6 @@
104104
*/
105105
private static final long THRESHOLD_END_OF_CONTENT_MS = 5000;
106106

107-
/**
108-
* Threshold before the start of an ad at which IMA is expected to be able to preload the ad, in
109-
* milliseconds.
110-
*/
111-
private static final long THRESHOLD_AD_PRELOAD_MS = 4000;
112-
113107
/** The threshold below which ad cue points are treated as matching, in microseconds. */
114108
private static final long THRESHOLD_AD_MATCH_US = 1000;
115109

@@ -144,7 +138,6 @@
144138
private final Handler handler;
145139
private final ComponentListener componentListener;
146140
private final ContentPlaybackAdapter contentPlaybackAdapter;
147-
private final VideoAdPlayerImpl videoAdPlayerImpl;
148141
private final List<EventListener> eventListeners;
149142
private final List<VideoAdPlayer.VideoAdPlayerCallback> adCallbacks;
150143
private final Runnable updateAdProgressRunnable;
@@ -265,7 +258,6 @@ public AdTagLoader(
265258
handler = Util.createHandler(getImaLooper(), /* callback= */ null);
266259
componentListener = new ComponentListener();
267260
contentPlaybackAdapter = new ContentPlaybackAdapter();
268-
videoAdPlayerImpl = new VideoAdPlayerImpl();
269261
eventListeners = new ArrayList<>();
270262
adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
271263
if (configuration.applicationVideoAdPlayerCallback != null) {
@@ -283,6 +275,7 @@ public AdTagLoader(
283275
timeline = Timeline.EMPTY;
284276
adPlaybackState = AdPlaybackState.NONE;
285277
adLoadTimeoutRunnable = this::handleAdLoadTimeout;
278+
VideoAdPlayerImpl videoAdPlayerImpl = new VideoAdPlayerImpl();
286279
if (adViewGroup != null) {
287280
adDisplayContainer =
288281
imaFactory.createAdDisplayContainer(adViewGroup, /* player= */ videoAdPlayerImpl);
@@ -1375,7 +1368,7 @@ public VideoProgressUpdate getContentProgress() {
13751368
// may be stuck. Detect this case and signal an error if applicable.
13761369
long stuckElapsedRealtimeMs =
13771370
SystemClock.elapsedRealtime() - waitingForPreloadElapsedRealtimeMs;
1378-
if (stuckElapsedRealtimeMs >= THRESHOLD_AD_PRELOAD_MS) {
1371+
if (stuckElapsedRealtimeMs >= configuration.adPreloadTimeoutMs) {
13791372
waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET;
13801373
handleAdGroupLoadError(new IOException("Ad preloading timed out"));
13811374
maybeNotifyPendingAdLoadError();

libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaAdsLoader.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,9 @@ public Builder setEnableContinuousPlayback(boolean enableContinuousPlayback) {
280280
* <p>The purpose of this timeout is to avoid playback getting stuck in the unexpected case that
281281
* the IMA SDK does not load an ad break based on the player's reported content position.
282282
*
283+
* <p>The value will be adjusted to be greater or equal to the one in {@link
284+
* #setVastLoadTimeoutMs(int)} if provided.
285+
*
283286
* @param adPreloadTimeoutMs The timeout buffering duration in milliseconds, or {@link
284287
* C#TIME_UNSET} for no timeout.
285288
* @return This builder, for convenience.
@@ -396,6 +399,9 @@ public Builder setDebugModeEnabled(boolean debugModeEnabled) {
396399

397400
/** Returns a new {@link ImaAdsLoader}. */
398401
public ImaAdsLoader build() {
402+
if (vastLoadTimeoutMs != TIMEOUT_UNSET && adPreloadTimeoutMs < vastLoadTimeoutMs) {
403+
adPreloadTimeoutMs = vastLoadTimeoutMs;
404+
}
399405
return new ImaAdsLoader(
400406
context,
401407
new ImaUtil.Configuration(

libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaAdsLoaderTest.java

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ public void playback_withAdNotPreloadingAfterTimeout_hasErrorAdGroup() {
503503
/* periodIndex= */ 0, Util.usToMs(adGroupPositionInWindowUs));
504504
fakePlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true);
505505
// Advance past the timeout and simulate polling content progress.
506-
ShadowSystemClock.advanceBy(Duration.ofSeconds(5));
506+
ShadowSystemClock.advanceBy(Duration.ofSeconds(11));
507507
contentProgressProvider.getContentProgress();
508508

509509
assertThat(getAdPlaybackState(/* periodIndex= */ 0))
@@ -515,6 +515,114 @@ public void playback_withAdNotPreloadingAfterTimeout_hasErrorAdGroup() {
515515
.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0));
516516
}
517517

518+
@Test
519+
public void playback_withCustomAdPreloadTimeout_triggersErrorAfterTimeout() {
520+
imaAdsLoader =
521+
new ImaAdsLoader.Builder(getApplicationContext())
522+
.setImaFactory(mockImaFactory)
523+
.setImaSdkSettings(mockImaSdkSettings)
524+
.setAdPreloadTimeoutMs(3_000)
525+
.build();
526+
imaAdsLoader.setPlayer(fakePlayer);
527+
adsMediaSource =
528+
new AdsMediaSource(
529+
new FakeMediaSource(CONTENT_TIMELINE),
530+
TEST_DATA_SPEC,
531+
TEST_ADS_ID,
532+
new DefaultMediaSourceFactory((Context) getApplicationContext()),
533+
imaAdsLoader,
534+
adViewProvider,
535+
/* useLazyContentSourcePreparation= */ true);
536+
537+
// Simulate an ad at 2 seconds.
538+
long adGroupPositionInWindowUs = 2 * C.MICROS_PER_SECOND;
539+
long adGroupTimeUs =
540+
adGroupPositionInWindowUs
541+
+ TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
542+
ImmutableList<Float> cuePoints = ImmutableList.of((float) adGroupTimeUs / C.MICROS_PER_SECOND);
543+
when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints);
544+
545+
// Advance playback to just before the midroll and simulate buffering.
546+
imaAdsLoader.start(
547+
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
548+
fakePlayer.setPlayingContentPosition(
549+
/* periodIndex= */ 0, Util.usToMs(adGroupPositionInWindowUs));
550+
fakePlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true);
551+
552+
// Advance past the custom timeout.
553+
ShadowSystemClock.advanceBy(Duration.ofSeconds(4));
554+
contentProgressProvider.getContentProgress();
555+
556+
// Verify that the ad group is in an error state.
557+
assertThat(getAdPlaybackState(/* periodIndex= */ 0))
558+
.isEqualTo(
559+
new AdPlaybackState(TEST_ADS_ID, getAdGroupTimesUsForCuePoints(cuePoints))
560+
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
561+
.withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}})
562+
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
563+
.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0));
564+
}
565+
566+
@Test
567+
public void
568+
playback_withAdPreloadTimeoutLessThanVastLoadTimeout_adPreloadTimeoutIncreasedToVastLoadTimeout() {
569+
imaAdsLoader =
570+
new ImaAdsLoader.Builder(getApplicationContext())
571+
.setImaFactory(mockImaFactory)
572+
.setImaSdkSettings(mockImaSdkSettings)
573+
.setVastLoadTimeoutMs(5_000)
574+
.setAdPreloadTimeoutMs(3_000)
575+
.build();
576+
imaAdsLoader.setPlayer(fakePlayer);
577+
adsMediaSource =
578+
new AdsMediaSource(
579+
new FakeMediaSource(CONTENT_TIMELINE),
580+
TEST_DATA_SPEC,
581+
TEST_ADS_ID,
582+
new DefaultMediaSourceFactory((Context) getApplicationContext()),
583+
imaAdsLoader,
584+
adViewProvider,
585+
/* useLazyContentSourcePreparation= */ true);
586+
587+
// Simulate an ad at 2 seconds.
588+
long adGroupPositionInWindowUs = 2 * C.MICROS_PER_SECOND;
589+
long adGroupTimeUs =
590+
adGroupPositionInWindowUs
591+
+ TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
592+
ImmutableList<Float> cuePoints = ImmutableList.of((float) adGroupTimeUs / C.MICROS_PER_SECOND);
593+
when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints);
594+
595+
// Advance playback to just before the midroll and simulate buffering.
596+
imaAdsLoader.start(
597+
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
598+
fakePlayer.setPlayingContentPosition(
599+
/* periodIndex= */ 0, Util.usToMs(adGroupPositionInWindowUs));
600+
fakePlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true);
601+
602+
// Advance past the original adPreloadTimeout (3s) but not the vastLoadTimeout (5s).
603+
ShadowSystemClock.advanceBy(Duration.ofSeconds(4));
604+
contentProgressProvider.getContentProgress();
605+
606+
// Verify that the ad has not errored out, as the timeout should be 5s.
607+
assertThat(getAdPlaybackState(/* periodIndex= */ 0))
608+
.isEqualTo(
609+
new AdPlaybackState(TEST_ADS_ID, getAdGroupTimesUsForCuePoints(cuePoints))
610+
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
611+
612+
// Advance past the vastLoadTimeout.
613+
ShadowSystemClock.advanceBy(Duration.ofSeconds(2)); // Total advance is 6s.
614+
contentProgressProvider.getContentProgress();
615+
616+
// Verify that the ad group is now in an error state.
617+
assertThat(getAdPlaybackState(/* periodIndex= */ 0))
618+
.isEqualTo(
619+
new AdPlaybackState(TEST_ADS_ID, getAdGroupTimesUsForCuePoints(cuePoints))
620+
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
621+
.withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}})
622+
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
623+
.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0));
624+
}
625+
518626
@Test
519627
public void startPlaybackAfterMidroll_doesNotSkipMidroll() {
520628
// Simulate an ad at 2 seconds, and starting playback with an initial seek position at the ad.
@@ -561,7 +669,7 @@ public void startPlaybackAfterMidroll_withAdNotPreloadingAfterTimeout_hasErrorAd
561669
imaAdsLoader.start(
562670
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
563671
contentProgressProvider.getContentProgress();
564-
ShadowSystemClock.advanceBy(Duration.ofSeconds(5));
672+
ShadowSystemClock.advanceBy(Duration.ofSeconds(11));
565673
contentProgressProvider.getContentProgress();
566674

567675
assertThat(getAdPlaybackState(/* periodIndex= */ 0))

0 commit comments

Comments
 (0)