From b125d45a63dcca237535ccc67e741f950312dc52 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 11 Aug 2022 14:59:21 +0000 Subject: [PATCH] Add timeout for ad to load. In some cases, the IMA SDK fails to call the expected loadAd event to load the next ad to play. This is (potentially) the only remaining case where playback can get stuck due to missing calls from IMA as the player doesn't even have a MediaSource at this stage and is only waiting for IMA to provide the ad URL. We can reuse the existing adPreloadTimeoutMs that was added for a similar purpose (when preloading the first ad in the group). The JavaDoc matches this purpose as well and the default timeout is appropriate since we expect to get the loadAd call immediately. Issue: google/ExoPlayer#10510 PiperOrigin-RevId: 466953617 --- RELEASENOTES.md | 4 ++ .../media3/exoplayer/ima/AdTagLoader.java | 41 +++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e067f9ddfc..a8f008dc8f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -46,6 +46,10 @@ * RTSP: * Add H263 fragmented packet handling ([#119](https://github.com/androidx/media/pull/119)). +* IMA: + * Add timeout for loading ad information to handle cases where the IMA SDK + gets stuck loading an ad + ([#10510](https://github.com/google/ExoPlayer/issues/10510)). ### 1.0.0-beta02 (2022-07-21) diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/AdTagLoader.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/AdTagLoader.java index 6196e933e4..4f958e3d55 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/AdTagLoader.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/AdTagLoader.java @@ -143,6 +143,7 @@ private final BiMap adInfoByAdMediaInfo; private final AdDisplayContainer adDisplayContainer; private final AdsLoader adsLoader; + private final Runnable adLoadTimeoutRunnable; @Nullable private Object pendingAdRequestContext; @Nullable private Player player; @@ -256,6 +257,7 @@ public AdTagLoader( contentDurationMs = C.TIME_UNSET; timeline = Timeline.EMPTY; adPlaybackState = AdPlaybackState.NONE; + adLoadTimeoutRunnable = this::handleAdLoadTimeout; if (adViewGroup != null) { adDisplayContainer = imaFactory.createAdDisplayContainer(adViewGroup, /* player= */ componentListener); @@ -488,7 +490,7 @@ public void onPlaybackStateChanged(@Player.State int playbackState) { if (playbackState == Player.STATE_BUFFERING && !player.isPlayingAd() - && isWaitingForAdToLoad()) { + && isWaitingForFirstAdToPreload()) { waitingForPreloadElapsedRealtimeMs = SystemClock.elapsedRealtime(); } else if (playbackState == Player.STATE_READY) { waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET; @@ -780,7 +782,7 @@ private void resumeContentInternal() { * Returns whether this instance is expecting the first ad in an the upcoming ad group to load * within the {@link ImaUtil.Configuration#adPreloadTimeoutMs preload timeout}. */ - private boolean isWaitingForAdToLoad() { + private boolean isWaitingForFirstAdToPreload() { @Nullable Player player = this.player; if (player == null) { return false; @@ -802,6 +804,23 @@ private boolean isWaitingForAdToLoad() { return timeUntilAdMs < configuration.adPreloadTimeoutMs; } + private boolean isWaitingForCurrentAdToLoad() { + @Nullable Player player = this.player; + if (player == null) { + return false; + } + int adGroupIndex = player.getCurrentAdGroupIndex(); + if (adGroupIndex == C.INDEX_UNSET) { + return false; + } + AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex); + int adIndexInAdGroup = player.getCurrentAdIndexInAdGroup(); + if (adGroup.count == C.LENGTH_UNSET || adGroup.count <= adIndexInAdGroup) { + return true; + } + return adGroup.states[adIndexInAdGroup] == AdPlaybackState.AD_STATE_UNAVAILABLE; + } + private void handlePlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { if (playingAd && imaAdState == IMA_AD_STATE_PLAYING) { if (!bufferingAd && playbackState == Player.STATE_BUFFERING) { @@ -892,6 +911,10 @@ private void handleTimelineOrPositionChanged() { } } } + if (isWaitingForCurrentAdToLoad()) { + handler.removeCallbacks(adLoadTimeoutRunnable); + handler.postDelayed(adLoadTimeoutRunnable, configuration.adPreloadTimeoutMs); + } } private void loadAdInternal(AdMediaInfo adMediaInfo, AdPodInfo adPodInfo) { @@ -918,6 +941,12 @@ private void loadAdInternal(AdMediaInfo adMediaInfo, AdPodInfo adPodInfo) { // timeout after its media load timeout. return; } + if (player != null + && player.getCurrentAdGroupIndex() == adGroupIndex + && player.getCurrentAdIndexInAdGroup() == adIndexInAdGroup) { + // Loaded ad info the player is currently waiting for. + handler.removeCallbacks(adLoadTimeoutRunnable); + } // The ad count may increase on successive loads of ads in the same ad pod, for example, due to // separate requests for ad tags with multiple ads within the ad pod completing after an earlier @@ -1063,6 +1092,12 @@ private void handleAdGroupLoadError(Exception error) { } } + private void handleAdLoadTimeout() { + // IMA got stuck and didn't load an ad in time, so skip the entire group. + handleAdGroupLoadError(new IOException("Ad loading timed out")); + maybeNotifyPendingAdLoadError(); + } + private void markAdGroupInErrorStateAndClearPendingContentPosition(int adGroupIndex) { // Update the ad playback state so all ads in the ad group are in the error state. AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex); @@ -1334,7 +1369,7 @@ public VideoProgressUpdate getContentProgress() { } else if (pendingContentPositionMs != C.TIME_UNSET && player != null && player.getPlaybackState() == Player.STATE_BUFFERING - && isWaitingForAdToLoad()) { + && isWaitingForFirstAdToPreload()) { // Prepare to timeout the load of an ad for the pending seek operation. waitingForPreloadElapsedRealtimeMs = SystemClock.elapsedRealtime(); }