Commit 28695d9a by andrewlewis Committed by Andrew Lewis

Move IMA SDK callbacks into inner class

The release() method was added in the recent IMA API changes for
preloading and now 'collides' with the ExoPlayer AdsLoader release
method. This led to all ads completing being treated as a call to
completely release the ads loader, which meant that the ad playback
state was not updated on resuming after all ads had completed, which
in turn led to playback getting stuck buffering on returning from the
background after all ads played.

Move the IMA callbacks into an inner class to avoid this.

Issue: #7508
PiperOrigin-RevId: 316834561
parent f85098a8
...@@ -215,6 +215,9 @@ ...@@ -215,6 +215,9 @@
* Add option to skip ads before the start position. * Add option to skip ads before the start position.
* Catch unexpected errors in `stopAd` to avoid a crash * Catch unexpected errors in `stopAd` to avoid a crash
([#7492](https://github.com/google/ExoPlayer/issues/7492)). ([#7492](https://github.com/google/ExoPlayer/issues/7492)).
* Fix a bug that caused playback to be stuck buffering on resuming from
the background after all ads had played to the end
([#7508](https://github.com/google/ExoPlayer/issues/7508)).
* Demo app: Retain previous position in list of samples. * Demo app: Retain previous position in list of samples.
* Add Guava dependency. * Add Guava dependency.
......
...@@ -86,14 +86,7 @@ import java.util.Set; ...@@ -86,14 +86,7 @@ import java.util.Set;
* For more details see {@link AdDisplayContainer#registerVideoControlsOverlay(View)} and {@link * For more details see {@link AdDisplayContainer#registerVideoControlsOverlay(View)} and {@link
* AdViewProvider#getAdOverlayViews()}. * AdViewProvider#getAdOverlayViews()}.
*/ */
public final class ImaAdsLoader public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
implements Player.EventListener,
AdsLoader,
VideoAdPlayer,
ContentProgressProvider,
AdErrorListener,
AdsLoadedListener,
AdEventListener {
static { static {
ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); ExoPlayerLibraryInfo.registerModule("goog.exo.ima");
...@@ -364,12 +357,13 @@ public final class ImaAdsLoader ...@@ -364,12 +357,13 @@ public final class ImaAdsLoader
*/ */
private static final int IMA_AD_STATE_NONE = 0; private static final int IMA_AD_STATE_NONE = 0;
/** /**
* The ad playback state when IMA has called {@link #playAd(AdMediaInfo)} and not {@link * The ad playback state when IMA has called {@link ComponentListener#playAd(AdMediaInfo)} and not
* #pauseAd(AdMediaInfo)}. * {@link ComponentListener##pauseAd(AdMediaInfo)}.
*/ */
private static final int IMA_AD_STATE_PLAYING = 1; private static final int IMA_AD_STATE_PLAYING = 1;
/** /**
* The ad playback state when IMA has called {@link #pauseAd(AdMediaInfo)} while playing an ad. * The ad playback state when IMA has called {@link ComponentListener#pauseAd(AdMediaInfo)} while
* playing an ad.
*/ */
private static final int IMA_AD_STATE_PAUSED = 2; private static final int IMA_AD_STATE_PAUSED = 2;
...@@ -386,7 +380,8 @@ public final class ImaAdsLoader ...@@ -386,7 +380,8 @@ public final class ImaAdsLoader
private final ImaFactory imaFactory; private final ImaFactory imaFactory;
private final Timeline.Period period; private final Timeline.Period period;
private final Handler handler; private final Handler handler;
private final List<VideoAdPlayerCallback> adCallbacks; private final ComponentListener componentListener;
private final List<VideoAdPlayer.VideoAdPlayerCallback> adCallbacks;
private final AdDisplayContainer adDisplayContainer; private final AdDisplayContainer adDisplayContainer;
private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
private final Runnable updateAdProgressRunnable; private final Runnable updateAdProgressRunnable;
...@@ -400,7 +395,7 @@ public final class ImaAdsLoader ...@@ -400,7 +395,7 @@ public final class ImaAdsLoader
@Nullable private Player player; @Nullable private Player player;
private VideoProgressUpdate lastContentProgress; private VideoProgressUpdate lastContentProgress;
private VideoProgressUpdate lastAdProgress; private VideoProgressUpdate lastAdProgress;
private int lastVolumePercentage; private int lastVolumePercent;
@Nullable private AdsManager adsManager; @Nullable private AdsManager adsManager;
private boolean isAdsManagerInitialized; private boolean isAdsManagerInitialized;
...@@ -443,10 +438,10 @@ public final class ImaAdsLoader ...@@ -443,10 +438,10 @@ public final class ImaAdsLoader
*/ */
@Nullable private AdInfo pendingAdPrepareErrorAdInfo; @Nullable private AdInfo pendingAdPrepareErrorAdInfo;
/** /**
* If a content period has finished but IMA has not yet called {@link #playAd(AdMediaInfo)}, * If a content period has finished but IMA has not yet called {@link
* stores the value of {@link SystemClock#elapsedRealtime()} when the content stopped playing. * ComponentListener#playAd(AdMediaInfo)}, stores the value of {@link
* This can be used to determine a fake, increasing content position. {@link C#TIME_UNSET} * SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to determine
* otherwise. * a fake, increasing content position. {@link C#TIME_UNSET} otherwise.
*/ */
private long fakeContentProgressElapsedRealtimeMs; private long fakeContentProgressElapsedRealtimeMs;
/** /**
...@@ -456,7 +451,10 @@ public final class ImaAdsLoader ...@@ -456,7 +451,10 @@ public final class ImaAdsLoader
private long fakeContentProgressOffsetMs; private long fakeContentProgressOffsetMs;
/** Stores the pending content position when a seek operation was intercepted to play an ad. */ /** Stores the pending content position when a seek operation was intercepted to play an ad. */
private long pendingContentPositionMs; private long pendingContentPositionMs;
/** Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. */ /**
* Whether {@link ComponentListener#getContentProgress()} has sent {@link
* #pendingContentPositionMs} to IMA.
*/
private boolean sentPendingContentPositionMs; private boolean sentPendingContentPositionMs;
/** /**
* Stores the real time in milliseconds at which the player started buffering, possibly due to not * Stores the real time in milliseconds at which the player started buffering, possibly due to not
...@@ -528,14 +526,15 @@ public final class ImaAdsLoader ...@@ -528,14 +526,15 @@ public final class ImaAdsLoader
imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION); imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION);
period = new Timeline.Period(); period = new Timeline.Period();
handler = Util.createHandler(getImaLooper(), /* callback= */ null); handler = Util.createHandler(getImaLooper(), /* callback= */ null);
componentListener = new ComponentListener();
adCallbacks = new ArrayList<>(/* initialCapacity= */ 1); adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
adDisplayContainer = imaFactory.createAdDisplayContainer(); adDisplayContainer = imaFactory.createAdDisplayContainer();
adDisplayContainer.setPlayer(/* videoAdPlayer= */ this); adDisplayContainer.setPlayer(/* videoAdPlayer= */ componentListener);
adsLoader = adsLoader =
imaFactory.createAdsLoader( imaFactory.createAdsLoader(
context.getApplicationContext(), imaSdkSettings, adDisplayContainer); context.getApplicationContext(), imaSdkSettings, adDisplayContainer);
adsLoader.addAdErrorListener(/* adErrorListener= */ this); adsLoader.addAdErrorListener(componentListener);
adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this); adsLoader.addAdsLoadedListener(componentListener);
updateAdProgressRunnable = this::updateAdProgress; updateAdProgressRunnable = this::updateAdProgress;
adInfoByAdMediaInfo = new HashMap<>(); adInfoByAdMediaInfo = new HashMap<>();
supportedMimeTypes = Collections.emptyList(); supportedMimeTypes = Collections.emptyList();
...@@ -596,7 +595,7 @@ public final class ImaAdsLoader ...@@ -596,7 +595,7 @@ public final class ImaAdsLoader
if (vastLoadTimeoutMs != TIMEOUT_UNSET) { if (vastLoadTimeoutMs != TIMEOUT_UNSET) {
request.setVastLoadTimeout(vastLoadTimeoutMs); request.setVastLoadTimeout(vastLoadTimeoutMs);
} }
request.setContentProgressProvider(this); request.setContentProgressProvider(componentListener);
pendingAdRequestContext = new Object(); pendingAdRequestContext = new Object();
request.setUserRequestContext(pendingAdRequestContext); request.setUserRequestContext(pendingAdRequestContext);
adsLoader.requestAds(request); adsLoader.requestAds(request);
...@@ -645,7 +644,7 @@ public final class ImaAdsLoader ...@@ -645,7 +644,7 @@ public final class ImaAdsLoader
player.addListener(this); player.addListener(this);
boolean playWhenReady = player.getPlayWhenReady(); boolean playWhenReady = player.getPlayWhenReady();
this.eventListener = eventListener; this.eventListener = eventListener;
lastVolumePercentage = 0; lastVolumePercent = 0;
lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
ViewGroup adViewGroup = adViewProvider.getAdViewGroup(); ViewGroup adViewGroup = adViewProvider.getAdViewGroup();
...@@ -682,9 +681,9 @@ public final class ImaAdsLoader ...@@ -682,9 +681,9 @@ public final class ImaAdsLoader
adPlaybackState.withAdResumePositionUs( adPlaybackState.withAdResumePositionUs(
playingAd ? C.msToUs(player.getCurrentPosition()) : 0); playingAd ? C.msToUs(player.getCurrentPosition()) : 0);
} }
lastVolumePercentage = getVolume(); lastVolumePercent = getPlayerVolumePercent();
lastAdProgress = getAdVideoProgressUpdate(); lastAdProgress = getAdVideoProgressUpdate();
lastContentProgress = getContentProgress(); lastContentProgress = getContentVideoProgressUpdate();
adDisplayContainer.unregisterAllVideoControlsOverlays(); adDisplayContainer.unregisterAllVideoControlsOverlays();
player.removeListener(this); player.removeListener(this);
this.player = null; this.player = null;
...@@ -695,8 +694,8 @@ public final class ImaAdsLoader ...@@ -695,8 +694,8 @@ public final class ImaAdsLoader
public void release() { public void release() {
pendingAdRequestContext = null; pendingAdRequestContext = null;
destroyAdsManager(); destroyAdsManager();
adsLoader.removeAdsLoadedListener(/* adsLoadedListener= */ this); adsLoader.removeAdsLoadedListener(componentListener);
adsLoader.removeAdErrorListener(/* adErrorListener= */ this); adsLoader.removeAdErrorListener(componentListener);
imaPausedContent = false; imaPausedContent = false;
imaAdState = IMA_AD_STATE_NONE; imaAdState = IMA_AD_STATE_NONE;
imaAdMediaInfo = null; imaAdMediaInfo = null;
...@@ -704,7 +703,7 @@ public final class ImaAdsLoader ...@@ -704,7 +703,7 @@ public final class ImaAdsLoader
imaAdInfo = null; imaAdInfo = null;
pendingAdLoadError = null; pendingAdLoadError = null;
adPlaybackState = AdPlaybackState.NONE; adPlaybackState = AdPlaybackState.NONE;
hasAdPlaybackState = false; hasAdPlaybackState = true;
updateAdPlaybackState(); updateAdPlaybackState();
} }
...@@ -720,275 +719,6 @@ public final class ImaAdsLoader ...@@ -720,275 +719,6 @@ public final class ImaAdsLoader
} }
} }
// com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener implementation.
@Override
public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) {
AdsManager adsManager = adsManagerLoadedEvent.getAdsManager();
if (!Util.areEqual(pendingAdRequestContext, adsManagerLoadedEvent.getUserRequestContext())) {
adsManager.destroy();
return;
}
pendingAdRequestContext = null;
this.adsManager = adsManager;
adsManager.addAdErrorListener(this);
adsManager.addAdEventListener(this);
if (adEventListener != null) {
adsManager.addAdEventListener(adEventListener);
}
if (player != null) {
// If a player is attached already, start playback immediately.
try {
adPlaybackState = AdPlaybackStateFactory.fromCuePoints(adsManager.getAdCuePoints());
hasAdPlaybackState = true;
updateAdPlaybackState();
} catch (RuntimeException e) {
maybeNotifyInternalError("onAdsManagerLoaded", e);
}
}
}
// AdEvent.AdEventListener implementation.
@Override
public void onAdEvent(AdEvent adEvent) {
AdEventType adEventType = adEvent.getType();
if (DEBUG && adEventType != AdEventType.AD_PROGRESS) {
Log.d(TAG, "onAdEvent: " + adEventType);
}
if (adsManager == null) {
// Drop events after release.
return;
}
try {
handleAdEvent(adEvent);
} catch (RuntimeException e) {
maybeNotifyInternalError("onAdEvent", e);
}
}
// AdErrorEvent.AdErrorListener implementation.
@Override
public void onAdError(AdErrorEvent adErrorEvent) {
AdError error = adErrorEvent.getError();
if (DEBUG) {
Log.d(TAG, "onAdError", error);
}
if (adsManager == null) {
// No ads were loaded, so allow playback to start without any ads.
pendingAdRequestContext = null;
adPlaybackState = AdPlaybackState.NONE;
hasAdPlaybackState = true;
updateAdPlaybackState();
} else if (isAdGroupLoadError(error)) {
try {
handleAdGroupLoadError(error);
} catch (RuntimeException e) {
maybeNotifyInternalError("onAdError", e);
}
}
if (pendingAdLoadError == null) {
pendingAdLoadError = AdLoadException.createForAllAds(error);
}
maybeNotifyPendingAdLoadError();
}
// ContentProgressProvider implementation.
@Override
public VideoProgressUpdate getContentProgress() {
VideoProgressUpdate videoProgressUpdate = getContentVideoProgressUpdate();
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;
}
// VideoAdPlayer implementation.
@Override
public VideoProgressUpdate getAdProgress() {
throw new IllegalStateException("Unexpected call to getAdProgress when using preloading");
}
@Override
public int getVolume() {
@Nullable Player player = this.player;
if (player == null) {
return lastVolumePercentage;
}
@Nullable Player.AudioComponent audioComponent = player.getAudioComponent();
if (audioComponent != null) {
return (int) (audioComponent.getVolume() * 100);
}
// Check for a selected track using an audio renderer.
TrackSelectionArray trackSelections = player.getCurrentTrackSelections();
for (int i = 0; i < player.getRendererCount() && i < trackSelections.length; i++) {
if (player.getRendererType(i) == C.TRACK_TYPE_AUDIO && trackSelections.get(i) != null) {
return 100;
}
}
return 0;
}
@Override
public void loadAd(AdMediaInfo adMediaInfo, AdPodInfo adPodInfo) {
try {
if (DEBUG) {
Log.d(TAG, "loadAd " + getAdMediaInfoString(adMediaInfo) + ", ad pod " + adPodInfo);
}
if (adsManager == null) {
// Drop events after release.
return;
}
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 =
adPlaybackState.withAdCount(
adInfo.adGroupIndex, Math.max(adPodInfo.getTotalAds(), adGroup.states.length));
adGroup = adPlaybackState.adGroups[adInfo.adGroupIndex];
}
for (int i = 0; i < adIndexInAdGroup; i++) {
// Any preceding ads that haven't loaded are not going to load.
if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) {
adPlaybackState =
adPlaybackState.withAdLoadError(
/* adGroupIndex= */ adGroupIndex, /* adIndexInAdGroup= */ i);
}
}
Uri adUri = Uri.parse(adMediaInfo.getUrl());
adPlaybackState =
adPlaybackState.withAdUri(adInfo.adGroupIndex, adInfo.adIndexInAdGroup, adUri);
updateAdPlaybackState();
} catch (RuntimeException e) {
maybeNotifyInternalError("loadAd", e);
}
}
@Override
public void addCallback(VideoAdPlayerCallback videoAdPlayerCallback) {
adCallbacks.add(videoAdPlayerCallback);
}
@Override
public void removeCallback(VideoAdPlayerCallback videoAdPlayerCallback) {
adCallbacks.remove(videoAdPlayerCallback);
}
@Override
public void playAd(AdMediaInfo adMediaInfo) {
if (DEBUG) {
Log.d(TAG, "playAd " + getAdMediaInfoString(adMediaInfo));
}
if (adsManager == null) {
// Drop events after release.
return;
}
if (imaAdState == IMA_AD_STATE_PLAYING) {
// IMA does not always call stopAd before resuming content.
// See [Internal: b/38354028].
Log.w(TAG, "Unexpected playAd without stopAd");
}
try {
if (imaAdState == IMA_AD_STATE_NONE) {
// IMA is requesting to play the ad, so stop faking the content position.
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
fakeContentProgressOffsetMs = C.TIME_UNSET;
imaAdState = IMA_AD_STATE_PLAYING;
imaAdMediaInfo = adMediaInfo;
imaAdInfo = Assertions.checkNotNull(adInfoByAdMediaInfo.get(adMediaInfo));
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onPlay(adMediaInfo);
}
if (pendingAdPrepareErrorAdInfo != null && pendingAdPrepareErrorAdInfo.equals(imaAdInfo)) {
pendingAdPrepareErrorAdInfo = null;
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onError(adMediaInfo);
}
}
updateAdProgress();
} else {
imaAdState = IMA_AD_STATE_PLAYING;
Assertions.checkState(adMediaInfo.equals(imaAdMediaInfo));
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onResume(adMediaInfo);
}
}
if (!Assertions.checkNotNull(player).getPlayWhenReady()) {
Assertions.checkNotNull(adsManager).pause();
}
} catch (RuntimeException e) {
maybeNotifyInternalError("playAd", e);
}
}
@Override
public void stopAd(AdMediaInfo adMediaInfo) {
if (DEBUG) {
Log.d(TAG, "stopAd " + getAdMediaInfoString(adMediaInfo));
}
if (adsManager == null) {
// Drop event after release.
return;
}
try {
Assertions.checkNotNull(player);
Assertions.checkState(imaAdState != IMA_AD_STATE_NONE);
stopAdInternal();
} catch (RuntimeException e) {
maybeNotifyInternalError("stopAd", e);
}
}
@Override
public void pauseAd(AdMediaInfo adMediaInfo) {
if (DEBUG) {
Log.d(TAG, "pauseAd " + getAdMediaInfoString(adMediaInfo));
}
if (imaAdState == IMA_AD_STATE_NONE) {
// This method is called after content is resumed.
return;
}
try {
Assertions.checkState(adMediaInfo.equals(imaAdMediaInfo));
imaAdState = IMA_AD_STATE_PAUSED;
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onPause(adMediaInfo);
}
} catch (RuntimeException e) {
maybeNotifyInternalError("pauseAd", e);
}
}
// Player.EventListener implementation. // Player.EventListener implementation.
@Override @Override
...@@ -1265,6 +995,27 @@ public final class ImaAdsLoader ...@@ -1265,6 +995,27 @@ public final class ImaAdsLoader
handler.removeCallbacks(updateAdProgressRunnable); handler.removeCallbacks(updateAdProgressRunnable);
} }
private int getPlayerVolumePercent() {
@Nullable Player player = this.player;
if (player == null) {
return lastVolumePercent;
}
@Nullable Player.AudioComponent audioComponent = player.getAudioComponent();
if (audioComponent != null) {
return (int) (audioComponent.getVolume() * 100);
}
// Check for a selected track using an audio renderer.
TrackSelectionArray trackSelections = player.getCurrentTrackSelections();
for (int i = 0; i < player.getRendererCount() && i < trackSelections.length; i++) {
if (player.getRendererType(i) == C.TRACK_TYPE_AUDIO && trackSelections.get(i) != null) {
return 100;
}
}
return 0;
}
private void handlePlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { private void handlePlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
if (playingAd && imaAdState == IMA_AD_STATE_PLAYING) { if (playingAd && imaAdState == IMA_AD_STATE_PLAYING) {
if (!bufferingAd && playbackState == Player.STATE_BUFFERING) { if (!bufferingAd && playbackState == Player.STATE_BUFFERING) {
...@@ -1581,8 +1332,8 @@ public final class ImaAdsLoader ...@@ -1581,8 +1332,8 @@ public final class ImaAdsLoader
private void destroyAdsManager() { private void destroyAdsManager() {
if (adsManager != null) { if (adsManager != null) {
adsManager.removeAdErrorListener(this); adsManager.removeAdErrorListener(componentListener);
adsManager.removeAdEventListener(this); adsManager.removeAdEventListener(componentListener);
if (adEventListener != null) { if (adEventListener != null) {
adsManager.removeAdEventListener(adEventListener); adsManager.removeAdEventListener(adEventListener);
} }
...@@ -1607,6 +1358,272 @@ public final class ImaAdsLoader ...@@ -1607,6 +1358,272 @@ public final class ImaAdsLoader
Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer); Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer);
} }
private final class ComponentListener
implements VideoAdPlayer,
ContentProgressProvider,
AdErrorListener,
AdsLoadedListener,
AdEventListener {
// com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener implementation.
@Override
public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) {
AdsManager adsManager = adsManagerLoadedEvent.getAdsManager();
if (!Util.areEqual(pendingAdRequestContext, adsManagerLoadedEvent.getUserRequestContext())) {
adsManager.destroy();
return;
}
pendingAdRequestContext = null;
ImaAdsLoader.this.adsManager = adsManager;
adsManager.addAdErrorListener(this);
adsManager.addAdEventListener(this);
if (adEventListener != null) {
adsManager.addAdEventListener(adEventListener);
}
if (player != null) {
// If a player is attached already, start playback immediately.
try {
adPlaybackState = AdPlaybackStateFactory.fromCuePoints(adsManager.getAdCuePoints());
hasAdPlaybackState = true;
updateAdPlaybackState();
} catch (RuntimeException e) {
maybeNotifyInternalError("onAdsManagerLoaded", e);
}
}
}
// AdEvent.AdEventListener implementation.
@Override
public void onAdEvent(AdEvent adEvent) {
AdEventType adEventType = adEvent.getType();
if (DEBUG && adEventType != AdEventType.AD_PROGRESS) {
Log.d(TAG, "onAdEvent: " + adEventType);
}
if (adsManager == null) {
// Drop events after release.
return;
}
try {
handleAdEvent(adEvent);
} catch (RuntimeException e) {
maybeNotifyInternalError("onAdEvent", e);
}
}
// AdErrorEvent.AdErrorListener implementation.
@Override
public void onAdError(AdErrorEvent adErrorEvent) {
AdError error = adErrorEvent.getError();
if (DEBUG) {
Log.d(TAG, "onAdError", error);
}
if (adsManager == null) {
// No ads were loaded, so allow playback to start without any ads.
pendingAdRequestContext = null;
adPlaybackState = AdPlaybackState.NONE;
hasAdPlaybackState = true;
updateAdPlaybackState();
} else if (isAdGroupLoadError(error)) {
try {
handleAdGroupLoadError(error);
} catch (RuntimeException e) {
maybeNotifyInternalError("onAdError", e);
}
}
if (pendingAdLoadError == null) {
pendingAdLoadError = AdLoadException.createForAllAds(error);
}
maybeNotifyPendingAdLoadError();
}
// ContentProgressProvider implementation.
@Override
public VideoProgressUpdate getContentProgress() {
VideoProgressUpdate videoProgressUpdate = getContentVideoProgressUpdate();
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;
}
// VideoAdPlayer implementation.
@Override
public VideoProgressUpdate getAdProgress() {
throw new IllegalStateException("Unexpected call to getAdProgress when using preloading");
}
@Override
public int getVolume() {
return getPlayerVolumePercent();
}
@Override
public void loadAd(AdMediaInfo adMediaInfo, AdPodInfo adPodInfo) {
try {
if (DEBUG) {
Log.d(TAG, "loadAd " + getAdMediaInfoString(adMediaInfo) + ", ad pod " + adPodInfo);
}
if (adsManager == null) {
// Drop events after release.
return;
}
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 =
adPlaybackState.withAdCount(
adInfo.adGroupIndex, Math.max(adPodInfo.getTotalAds(), adGroup.states.length));
adGroup = adPlaybackState.adGroups[adInfo.adGroupIndex];
}
for (int i = 0; i < adIndexInAdGroup; i++) {
// Any preceding ads that haven't loaded are not going to load.
if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) {
adPlaybackState =
adPlaybackState.withAdLoadError(
/* adGroupIndex= */ adGroupIndex, /* adIndexInAdGroup= */ i);
}
}
Uri adUri = Uri.parse(adMediaInfo.getUrl());
adPlaybackState =
adPlaybackState.withAdUri(adInfo.adGroupIndex, adInfo.adIndexInAdGroup, adUri);
updateAdPlaybackState();
} catch (RuntimeException e) {
maybeNotifyInternalError("loadAd", e);
}
}
@Override
public void addCallback(VideoAdPlayerCallback videoAdPlayerCallback) {
adCallbacks.add(videoAdPlayerCallback);
}
@Override
public void removeCallback(VideoAdPlayerCallback videoAdPlayerCallback) {
adCallbacks.remove(videoAdPlayerCallback);
}
@Override
public void playAd(AdMediaInfo adMediaInfo) {
if (DEBUG) {
Log.d(TAG, "playAd " + getAdMediaInfoString(adMediaInfo));
}
if (adsManager == null) {
// Drop events after release.
return;
}
if (imaAdState == IMA_AD_STATE_PLAYING) {
// IMA does not always call stopAd before resuming content.
// See [Internal: b/38354028].
Log.w(TAG, "Unexpected playAd without stopAd");
}
try {
if (imaAdState == IMA_AD_STATE_NONE) {
// IMA is requesting to play the ad, so stop faking the content position.
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
fakeContentProgressOffsetMs = C.TIME_UNSET;
imaAdState = IMA_AD_STATE_PLAYING;
imaAdMediaInfo = adMediaInfo;
imaAdInfo = Assertions.checkNotNull(adInfoByAdMediaInfo.get(adMediaInfo));
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onPlay(adMediaInfo);
}
if (pendingAdPrepareErrorAdInfo != null
&& pendingAdPrepareErrorAdInfo.equals(imaAdInfo)) {
pendingAdPrepareErrorAdInfo = null;
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onError(adMediaInfo);
}
}
updateAdProgress();
} else {
imaAdState = IMA_AD_STATE_PLAYING;
Assertions.checkState(adMediaInfo.equals(imaAdMediaInfo));
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onResume(adMediaInfo);
}
}
if (!Assertions.checkNotNull(player).getPlayWhenReady()) {
Assertions.checkNotNull(adsManager).pause();
}
} catch (RuntimeException e) {
maybeNotifyInternalError("playAd", e);
}
}
@Override
public void stopAd(AdMediaInfo adMediaInfo) {
if (DEBUG) {
Log.d(TAG, "stopAd " + getAdMediaInfoString(adMediaInfo));
}
if (adsManager == null) {
// Drop event after release.
return;
}
try {
Assertions.checkNotNull(player);
Assertions.checkState(imaAdState != IMA_AD_STATE_NONE);
stopAdInternal();
} catch (RuntimeException e) {
maybeNotifyInternalError("stopAd", e);
}
}
@Override
public void pauseAd(AdMediaInfo adMediaInfo) {
if (DEBUG) {
Log.d(TAG, "pauseAd " + getAdMediaInfoString(adMediaInfo));
}
if (imaAdState == IMA_AD_STATE_NONE) {
// This method is called after content is resumed.
return;
}
try {
Assertions.checkState(adMediaInfo.equals(imaAdMediaInfo));
imaAdState = IMA_AD_STATE_PAUSED;
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onPause(adMediaInfo);
}
} catch (RuntimeException e) {
maybeNotifyInternalError("pauseAd", e);
}
}
@Override
public void release() {
// Do nothing.
}
}
// TODO: Consider moving this into AdPlaybackState. // TODO: Consider moving this into AdPlaybackState.
private static final class AdInfo { private static final class AdInfo {
public final int adGroupIndex; public final int adGroupIndex;
......
...@@ -44,6 +44,8 @@ import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; ...@@ -44,6 +44,8 @@ import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
import com.google.ads.interactivemedia.v3.api.AdsRequest; import com.google.ads.interactivemedia.v3.api.AdsRequest;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
import com.google.ads.interactivemedia.v3.api.player.AdMediaInfo; import com.google.ads.interactivemedia.v3.api.player.AdMediaInfo;
import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider;
import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.MediaItem;
...@@ -114,6 +116,9 @@ public final class ImaAdsLoaderTest { ...@@ -114,6 +116,9 @@ public final class ImaAdsLoaderTest {
private ViewGroup adViewGroup; private ViewGroup adViewGroup;
private View adOverlayView; private View adOverlayView;
private AdsLoader.AdViewProvider adViewProvider; private AdsLoader.AdViewProvider adViewProvider;
private AdEvent.AdEventListener adEventListener;
private ContentProgressProvider contentProgressProvider;
private VideoAdPlayer videoAdPlayer;
private TestAdsLoaderListener adsLoaderListener; private TestAdsLoaderListener adsLoaderListener;
private FakePlayer fakeExoPlayer; private FakePlayer fakeExoPlayer;
private ImaAdsLoader imaAdsLoader; private ImaAdsLoader imaAdsLoader;
...@@ -192,6 +197,8 @@ public final class ImaAdsLoaderTest { ...@@ -192,6 +197,8 @@ public final class ImaAdsLoaderTest {
@Test @Test
public void startAndCallbacksAfterRelease() { public void startAndCallbacksAfterRelease() {
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS); setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
// Request ads in order to get a reference to the ad event listener.
imaAdsLoader.requestAds(adViewGroup);
imaAdsLoader.release(); imaAdsLoader.release();
imaAdsLoader.start(adsLoaderListener, adViewProvider); imaAdsLoader.start(adsLoaderListener, adViewProvider);
fakeExoPlayer.setPlayingContentPosition(/* position= */ 0); fakeExoPlayer.setPlayingContentPosition(/* position= */ 0);
...@@ -202,16 +209,16 @@ public final class ImaAdsLoaderTest { ...@@ -202,16 +209,16 @@ public final class ImaAdsLoaderTest {
// when using Robolectric and accessing VideoProgressUpdate.VIDEO_TIME_NOT_READY, due to the IMA // when using Robolectric and accessing VideoProgressUpdate.VIDEO_TIME_NOT_READY, due to the IMA
// SDK being proguarded. // SDK being proguarded.
imaAdsLoader.requestAds(adViewGroup); imaAdsLoader.requestAds(adViewGroup);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd)); adEventListener.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd));
imaAdsLoader.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo); videoAdPlayer.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd)); adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd));
imaAdsLoader.playAd(TEST_AD_MEDIA_INFO); videoAdPlayer.playAd(TEST_AD_MEDIA_INFO);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd)); adEventListener.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd));
imaAdsLoader.pauseAd(TEST_AD_MEDIA_INFO); videoAdPlayer.pauseAd(TEST_AD_MEDIA_INFO);
imaAdsLoader.stopAd(TEST_AD_MEDIA_INFO); videoAdPlayer.stopAd(TEST_AD_MEDIA_INFO);
imaAdsLoader.onPlayerError(ExoPlaybackException.createForSource(new IOException())); imaAdsLoader.onPlayerError(ExoPlaybackException.createForSource(new IOException()));
imaAdsLoader.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); imaAdsLoader.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null)); adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
imaAdsLoader.handlePrepareError( imaAdsLoader.handlePrepareError(
/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, new IOException()); /* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, new IOException());
} }
...@@ -222,27 +229,27 @@ public final class ImaAdsLoaderTest { ...@@ -222,27 +229,27 @@ public final class ImaAdsLoaderTest {
// Load the preroll ad. // Load the preroll ad.
imaAdsLoader.start(adsLoaderListener, adViewProvider); imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd)); adEventListener.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd));
imaAdsLoader.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo); videoAdPlayer.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd)); adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd));
// Play the preroll ad. // Play the preroll ad.
imaAdsLoader.playAd(TEST_AD_MEDIA_INFO); videoAdPlayer.playAd(TEST_AD_MEDIA_INFO);
fakeExoPlayer.setPlayingAdPosition( fakeExoPlayer.setPlayingAdPosition(
/* adGroupIndex= */ 0, /* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0, /* adIndexInAdGroup= */ 0,
/* position= */ 0, /* position= */ 0,
/* contentPosition= */ 0); /* contentPosition= */ 0);
fakeExoPlayer.setState(Player.STATE_READY, true); fakeExoPlayer.setState(Player.STATE_READY, true);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd)); adEventListener.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd));
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, mockPrerollSingleAd)); adEventListener.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, mockPrerollSingleAd));
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.MIDPOINT, mockPrerollSingleAd)); adEventListener.onAdEvent(getAdEvent(AdEventType.MIDPOINT, mockPrerollSingleAd));
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.THIRD_QUARTILE, mockPrerollSingleAd)); adEventListener.onAdEvent(getAdEvent(AdEventType.THIRD_QUARTILE, mockPrerollSingleAd));
// Play the content. // Play the content.
fakeExoPlayer.setPlayingContentPosition(0); fakeExoPlayer.setPlayingContentPosition(0);
imaAdsLoader.stopAd(TEST_AD_MEDIA_INFO); videoAdPlayer.stopAd(TEST_AD_MEDIA_INFO);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null)); adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
// Verify that the preroll ad has been marked as played. // Verify that the preroll ad has been marked as played.
assertThat(adsLoaderListener.adPlaybackState) assertThat(adsLoaderListener.adPlaybackState)
...@@ -262,7 +269,7 @@ public final class ImaAdsLoaderTest { ...@@ -262,7 +269,7 @@ public final class ImaAdsLoaderTest {
// Simulate loading an empty postroll ad. // Simulate loading an empty postroll ad.
imaAdsLoader.start(adsLoaderListener, adViewProvider); imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.onAdEvent(mockPostrollFetchErrorAdEvent); adEventListener.onAdEvent(mockPostrollFetchErrorAdEvent);
assertThat(adsLoaderListener.adPlaybackState) assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo( .isEqualTo(
...@@ -289,7 +296,7 @@ public final class ImaAdsLoaderTest { ...@@ -289,7 +296,7 @@ public final class ImaAdsLoaderTest {
fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true); fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true);
// Advance before the timeout and simulating polling content progress. // Advance before the timeout and simulating polling content progress.
ShadowSystemClock.advanceBy(Duration.ofSeconds(1)); ShadowSystemClock.advanceBy(Duration.ofSeconds(1));
imaAdsLoader.getContentProgress(); contentProgressProvider.getContentProgress();
assertThat(adsLoaderListener.adPlaybackState) assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo( .isEqualTo(
...@@ -313,7 +320,7 @@ public final class ImaAdsLoaderTest { ...@@ -313,7 +320,7 @@ public final class ImaAdsLoaderTest {
fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true); fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true);
// Advance past the timeout and simulate polling content progress. // Advance past the timeout and simulate polling content progress.
ShadowSystemClock.advanceBy(Duration.ofSeconds(5)); ShadowSystemClock.advanceBy(Duration.ofSeconds(5));
imaAdsLoader.getContentProgress(); contentProgressProvider.getContentProgress();
assertThat(adsLoaderListener.adPlaybackState) assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo( .isEqualTo(
...@@ -660,6 +667,8 @@ public final class ImaAdsLoaderTest { ...@@ -660,6 +667,8 @@ public final class ImaAdsLoaderTest {
.thenAnswer(invocation -> userRequestContextCaptor.getValue()); .thenAnswer(invocation -> userRequestContextCaptor.getValue());
List<com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener> adsLoadedListeners = List<com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener> adsLoadedListeners =
new ArrayList<>(); new ArrayList<>();
// Deliberately don't handle removeAdsLoadedListener to allow testing behavior if the IMA SDK
// invokes callbacks after release.
doAnswer( doAnswer(
invocation -> { invocation -> {
adsLoadedListeners.add(invocation.getArgument(0)); adsLoadedListeners.add(invocation.getArgument(0));
...@@ -667,13 +676,6 @@ public final class ImaAdsLoaderTest { ...@@ -667,13 +676,6 @@ public final class ImaAdsLoaderTest {
}) })
.when(mockAdsLoader) .when(mockAdsLoader)
.addAdsLoadedListener(any()); .addAdsLoadedListener(any());
doAnswer(
invocation -> {
adsLoadedListeners.remove(invocation.getArgument(0));
return null;
})
.when(mockAdsLoader)
.removeAdsLoadedListener(any());
when(mockAdsManagerLoadedEvent.getAdsManager()).thenReturn(mockAdsManager); when(mockAdsManagerLoadedEvent.getAdsManager()).thenReturn(mockAdsManager);
when(mockAdsManagerLoadedEvent.getUserRequestContext()) when(mockAdsManagerLoadedEvent.getUserRequestContext())
.thenAnswer(invocation -> mockAdsRequest.getUserRequestContext()); .thenAnswer(invocation -> mockAdsRequest.getUserRequestContext());
...@@ -689,6 +691,30 @@ public final class ImaAdsLoaderTest { ...@@ -689,6 +691,30 @@ public final class ImaAdsLoaderTest {
.when(mockAdsLoader) .when(mockAdsLoader)
.requestAds(mockAdsRequest); .requestAds(mockAdsRequest);
doAnswer(
invocation -> {
adEventListener = invocation.getArgument(0);
return null;
})
.when(mockAdsManager)
.addAdEventListener(any());
doAnswer(
invocation -> {
contentProgressProvider = invocation.getArgument(0);
return null;
})
.when(mockAdsRequest)
.setContentProgressProvider(any());
doAnswer(
invocation -> {
videoAdPlayer = invocation.getArgument(0);
return null;
})
.when(mockAdDisplayContainer)
.setPlayer(any());
when(mockImaFactory.createAdDisplayContainer()).thenReturn(mockAdDisplayContainer); when(mockImaFactory.createAdDisplayContainer()).thenReturn(mockAdDisplayContainer);
when(mockImaFactory.createAdsRenderingSettings()).thenReturn(mockAdsRenderingSettings); when(mockImaFactory.createAdsRenderingSettings()).thenReturn(mockAdsRenderingSettings);
when(mockImaFactory.createAdsRequest()).thenReturn(mockAdsRequest); when(mockImaFactory.createAdsRequest()).thenReturn(mockAdsRequest);
......
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