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,595 +719,347 @@ public final class ImaAdsLoader ...@@ -720,595 +719,347 @@ public final class ImaAdsLoader
} }
} }
// com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener implementation. // Player.EventListener implementation.
@Override @Override
public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
AdsManager adsManager = adsManagerLoadedEvent.getAdsManager(); if (timeline.isEmpty()) {
if (!Util.areEqual(pendingAdRequestContext, adsManagerLoadedEvent.getUserRequestContext())) { // The player is being reset or contains no media.
adsManager.destroy();
return; return;
} }
pendingAdRequestContext = null; Assertions.checkArgument(timeline.getPeriodCount() == 1);
this.adsManager = adsManager; this.timeline = timeline;
adsManager.addAdErrorListener(this); long contentDurationUs = timeline.getPeriod(/* periodIndex= */ 0, period).durationUs;
adsManager.addAdEventListener(this); contentDurationMs = C.usToMs(contentDurationUs);
if (adEventListener != null) { if (contentDurationUs != C.TIME_UNSET) {
adsManager.addAdEventListener(adEventListener); adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs);
} }
if (player != null) { @Nullable AdsManager adsManager = this.adsManager;
// If a player is attached already, start playback immediately. if (!isAdsManagerInitialized && adsManager != null) {
try { isAdsManagerInitialized = true;
adPlaybackState = AdPlaybackStateFactory.fromCuePoints(adsManager.getAdCuePoints()); @Nullable AdsRenderingSettings adsRenderingSettings = setupAdsRendering();
hasAdPlaybackState = true; if (adsRenderingSettings == null) {
updateAdPlaybackState(); // There are no ads to play.
} catch (RuntimeException e) { destroyAdsManager();
maybeNotifyInternalError("onAdsManagerLoaded", e); } else {
adsManager.init(adsRenderingSettings);
adsManager.start();
if (DEBUG) {
Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings);
} }
} }
updateAdPlaybackState();
}
handleTimelineOrPositionChanged();
} }
// AdEvent.AdEventListener implementation.
@Override @Override
public void onAdEvent(AdEvent adEvent) { public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
AdEventType adEventType = adEvent.getType(); handleTimelineOrPositionChanged();
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 @Override
public void onAdError(AdErrorEvent adErrorEvent) { public void onPlaybackStateChanged(@Player.State int playbackState) {
AdError error = adErrorEvent.getError(); @Nullable Player player = this.player;
if (DEBUG) { if (adsManager == null || player == null) {
Log.d(TAG, "onAdError", error); return;
} }
if (adsManager == null) {
// No ads were loaded, so allow playback to start without any ads. if (playbackState == Player.STATE_BUFFERING && !player.isPlayingAd()) {
pendingAdRequestContext = null; // Check whether we are waiting for an ad to preload.
adPlaybackState = AdPlaybackState.NONE; int adGroupIndex = getLoadingAdGroupIndex();
hasAdPlaybackState = true; if (adGroupIndex == C.INDEX_UNSET) {
updateAdPlaybackState(); return;
} else if (isAdGroupLoadError(error)) {
try {
handleAdGroupLoadError(error);
} catch (RuntimeException e) {
maybeNotifyInternalError("onAdError", e);
} }
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;
} }
if (pendingAdLoadError == null) { long adGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]);
pendingAdLoadError = AdLoadException.createForAllAds(error); long contentPositionMs = getContentPeriodPositionMs(player, timeline, period);
long timeUntilAdMs = adGroupTimeMs - contentPositionMs;
if (timeUntilAdMs < adPreloadTimeoutMs) {
waitingForPreloadElapsedRealtimeMs = SystemClock.elapsedRealtime();
} }
maybeNotifyPendingAdLoadError(); } else if (playbackState == Player.STATE_READY) {
waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET;
} }
// ContentProgressProvider implementation. handlePlayerStateChanged(player.getPlayWhenReady(), playbackState);
}
@Override @Override
public VideoProgressUpdate getContentProgress() { public void onPlayWhenReadyChanged(
VideoProgressUpdate videoProgressUpdate = getContentVideoProgressUpdate(); boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) {
if (DEBUG) { if (adsManager == null || player == null) {
Log.d(TAG, "Content progress: " + videoProgressUpdate); return;
} }
if (waitingForPreloadElapsedRealtimeMs != C.TIME_UNSET) { if (imaAdState == IMA_AD_STATE_PLAYING && !playWhenReady) {
// IMA is polling the player position but we are buffering for an ad to preload, so playback adsManager.pause();
// may be stuck. Detect this case and signal an error if applicable. return;
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; if (imaAdState == IMA_AD_STATE_PAUSED && playWhenReady) {
adsManager.resume();
return;
} }
handlePlayerStateChanged(playWhenReady, player.getPlaybackState());
// VideoAdPlayer implementation.
@Override
public VideoProgressUpdate getAdProgress() {
throw new IllegalStateException("Unexpected call to getAdProgress when using preloading");
} }
@Override @Override
public int getVolume() { public void onPlayerError(ExoPlaybackException error) {
@Nullable Player player = this.player; if (imaAdState != IMA_AD_STATE_NONE) {
if (player == null) { AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo);
return lastVolumePercentage; for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onError(adMediaInfo);
}
} }
@Nullable Player.AudioComponent audioComponent = player.getAudioComponent();
if (audioComponent != null) {
return (int) (audioComponent.getVolume() * 100);
} }
// Check for a selected track using an audio renderer. // Internal methods.
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) { * Configures ads rendering for starting playback, returning the settings for the IMA SDK or
return 100; * {@code null} if no ads should play.
*/
@Nullable
private AdsRenderingSettings setupAdsRendering() {
AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings();
adsRenderingSettings.setEnablePreloading(true);
adsRenderingSettings.setMimeTypes(supportedMimeTypes);
if (mediaLoadTimeoutMs != TIMEOUT_UNSET) {
adsRenderingSettings.setLoadVideoTimeout(mediaLoadTimeoutMs);
} }
if (mediaBitrate != BITRATE_UNSET) {
adsRenderingSettings.setBitrateKbps(mediaBitrate / 1000);
} }
return 0; adsRenderingSettings.setFocusSkipButtonWhenAvailable(focusSkipButtonWhenAvailable);
if (adUiElements != null) {
adsRenderingSettings.setUiElements(adUiElements);
} }
@Override // Skip ads based on the start position as required.
public void loadAd(AdMediaInfo adMediaInfo, AdPodInfo adPodInfo) { long[] adGroupTimesUs = adPlaybackState.adGroupTimesUs;
try { long contentPositionMs =
if (DEBUG) { getContentPeriodPositionMs(Assertions.checkNotNull(player), timeline, period);
Log.d(TAG, "loadAd " + getAdMediaInfoString(adMediaInfo) + ", ad pod " + adPodInfo); int adGroupForPositionIndex =
} adPlaybackState.getAdGroupIndexForPositionUs(
if (adsManager == null) { C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
// Drop events after release. if (adGroupForPositionIndex != C.INDEX_UNSET) {
return; boolean playAdWhenStartingPlayback =
playAdBeforeStartPosition
|| adGroupTimesUs[adGroupForPositionIndex] == C.msToUs(contentPositionMs);
if (!playAdWhenStartingPlayback) {
adGroupForPositionIndex++;
} else if (hasMidrollAdGroups(adGroupTimesUs)) {
// Provide the player's initial position to trigger loading and playing the ad. If there are
// no midrolls, we are playing a preroll and any pending content position wouldn't be
// cleared.
pendingContentPositionMs = contentPositionMs;
} }
int adGroupIndex = getAdGroupIndexForAdPod(adPodInfo); if (adGroupForPositionIndex > 0) {
int adIndexInAdGroup = adPodInfo.getAdPosition() - 1; for (int i = 0; i < adGroupForPositionIndex; i++) {
AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup); adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
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 (adGroupForPositionIndex == adGroupTimesUs.length) {
if (adGroup.count == C.LENGTH_UNSET) { // We don't need to play any ads. Because setPlayAdsAfterTime does not discard non-VMAP
adPlaybackState = // ads, we signal that no ads will render so the caller can destroy the ads manager.
adPlaybackState.withAdCount( return null;
adInfo.adGroupIndex, Math.max(adPodInfo.getTotalAds(), adGroup.states.length));
adGroup = adPlaybackState.adGroups[adInfo.adGroupIndex];
} }
for (int i = 0; i < adIndexInAdGroup; i++) { long adGroupForPositionTimeUs = adGroupTimesUs[adGroupForPositionIndex];
// Any preceding ads that haven't loaded are not going to load. long adGroupBeforePositionTimeUs = adGroupTimesUs[adGroupForPositionIndex - 1];
if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) { if (adGroupForPositionTimeUs == C.TIME_END_OF_SOURCE) {
adPlaybackState = // Play the postroll by offsetting the start position just past the last non-postroll ad.
adPlaybackState.withAdLoadError( adsRenderingSettings.setPlayAdsAfterTime(
/* adGroupIndex= */ adGroupIndex, /* adIndexInAdGroup= */ i); (double) adGroupBeforePositionTimeUs / C.MICROS_PER_SECOND + 1d);
} else {
// Play ads after the midpoint between the ad to play and the one before it, to avoid
// issues with rounding one of the two ad times.
double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforePositionTimeUs) / 2d;
adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND);
} }
} }
Uri adUri = Uri.parse(adMediaInfo.getUrl());
adPlaybackState =
adPlaybackState.withAdUri(adInfo.adGroupIndex, adInfo.adIndexInAdGroup, adUri);
updateAdPlaybackState();
} catch (RuntimeException e) {
maybeNotifyInternalError("loadAd", e);
} }
return adsRenderingSettings;
} }
@Override private void handleAdEvent(AdEvent adEvent) {
public void addCallback(VideoAdPlayerCallback videoAdPlayerCallback) { switch (adEvent.getType()) {
adCallbacks.add(videoAdPlayerCallback); case AD_BREAK_FETCH_ERROR:
String adGroupTimeSecondsString =
Assertions.checkNotNull(adEvent.getAdData().get("adBreakTime"));
if (DEBUG) {
Log.d(TAG, "Fetch error for ad at " + adGroupTimeSecondsString + " seconds");
} }
int adGroupTimeSeconds = Integer.parseInt(adGroupTimeSecondsString);
@Override int adGroupIndex =
public void removeCallback(VideoAdPlayerCallback videoAdPlayerCallback) { adGroupTimeSeconds == -1
adCallbacks.remove(videoAdPlayerCallback); ? adPlaybackState.adGroupCount - 1
: Util.linearSearch(
adPlaybackState.adGroupTimesUs, C.MICROS_PER_SECOND * adGroupTimeSeconds);
AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];
if (adGroup.count == C.LENGTH_UNSET) {
adPlaybackState =
adPlaybackState.withAdCount(adGroupIndex, Math.max(1, adGroup.states.length));
adGroup = adPlaybackState.adGroups[adGroupIndex];
} }
for (int i = 0; i < adGroup.count; i++) {
@Override if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) {
public void playAd(AdMediaInfo adMediaInfo) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "playAd " + getAdMediaInfoString(adMediaInfo)); Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex);
}
if (adsManager == null) {
// Drop events after release.
return;
} }
adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i);
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)) { updateAdPlaybackState();
pendingAdPrepareErrorAdInfo = null; break;
for (int i = 0; i < adCallbacks.size(); i++) { case CONTENT_PAUSE_REQUESTED:
adCallbacks.get(i).onError(adMediaInfo); // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads
// before sending CONTENT_RESUME_REQUESTED.
imaPausedContent = true;
pauseContentInternal();
break;
case TAPPED:
if (eventListener != null) {
eventListener.onAdTapped();
} }
break;
case CLICKED:
if (eventListener != null) {
eventListener.onAdClicked();
} }
updateAdProgress(); break;
} else { case CONTENT_RESUME_REQUESTED:
imaAdState = IMA_AD_STATE_PLAYING; imaPausedContent = false;
Assertions.checkState(adMediaInfo.equals(imaAdMediaInfo)); resumeContentInternal();
for (int i = 0; i < adCallbacks.size(); i++) { break;
adCallbacks.get(i).onResume(adMediaInfo); case LOG:
Map<String, String> adData = adEvent.getAdData();
String message = "AdEvent: " + adData;
Log.i(TAG, message);
break;
default:
break;
} }
} }
if (!Assertions.checkNotNull(player).getPlayWhenReady()) {
Assertions.checkNotNull(adsManager).pause(); private VideoProgressUpdate getContentVideoProgressUpdate() {
if (player == null) {
return lastContentProgress;
} }
} catch (RuntimeException e) { boolean hasContentDuration = contentDurationMs != C.TIME_UNSET;
maybeNotifyInternalError("playAd", e); long contentPositionMs;
if (pendingContentPositionMs != C.TIME_UNSET) {
sentPendingContentPositionMs = true;
contentPositionMs = pendingContentPositionMs;
} else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {
long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;
} else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) {
contentPositionMs = getContentPeriodPositionMs(player, timeline, period);
} else {
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
} }
long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET;
return new VideoProgressUpdate(contentPositionMs, contentDurationMs);
} }
@Override private VideoProgressUpdate getAdVideoProgressUpdate() {
public void stopAd(AdMediaInfo adMediaInfo) { if (player == null) {
if (DEBUG) { return lastAdProgress;
Log.d(TAG, "stopAd " + getAdMediaInfoString(adMediaInfo)); } else if (imaAdState != IMA_AD_STATE_NONE && playingAd) {
long adDuration = player.getDuration();
return adDuration == C.TIME_UNSET
? VideoProgressUpdate.VIDEO_TIME_NOT_READY
: new VideoProgressUpdate(player.getCurrentPosition(), adDuration);
} else {
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
} }
if (adsManager == null) {
// Drop event after release.
return;
} }
try { private void updateAdProgress() {
Assertions.checkNotNull(player); VideoProgressUpdate videoProgressUpdate = getAdVideoProgressUpdate();
Assertions.checkState(imaAdState != IMA_AD_STATE_NONE); AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo);
stopAdInternal(); for (int i = 0; i < adCallbacks.size(); i++) {
} catch (RuntimeException e) { adCallbacks.get(i).onAdProgress(adMediaInfo, videoProgressUpdate);
maybeNotifyInternalError("stopAd", e);
} }
handler.removeCallbacks(updateAdProgressRunnable);
handler.postDelayed(updateAdProgressRunnable, AD_PROGRESS_UPDATE_INTERVAL_MS);
} }
@Override private void stopUpdatingAdProgress() {
public void pauseAd(AdMediaInfo adMediaInfo) { handler.removeCallbacks(updateAdProgressRunnable);
if (DEBUG) {
Log.d(TAG, "pauseAd " + getAdMediaInfoString(adMediaInfo));
} }
if (imaAdState == IMA_AD_STATE_NONE) {
// This method is called after content is resumed. private int getPlayerVolumePercent() {
return; @Nullable Player player = this.player;
if (player == null) {
return lastVolumePercent;
} }
try { @Nullable Player.AudioComponent audioComponent = player.getAudioComponent();
Assertions.checkState(adMediaInfo.equals(imaAdMediaInfo)); if (audioComponent != null) {
imaAdState = IMA_AD_STATE_PAUSED; return (int) (audioComponent.getVolume() * 100);
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onPause(adMediaInfo);
} }
} catch (RuntimeException e) {
maybeNotifyInternalError("pauseAd", e); // 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;
}
// Player.EventListener implementation. private void handlePlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
if (playingAd && imaAdState == IMA_AD_STATE_PLAYING) {
@Override if (!bufferingAd && playbackState == Player.STATE_BUFFERING) {
public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo);
if (timeline.isEmpty()) { for (int i = 0; i < adCallbacks.size(); i++) {
// The player is being reset or contains no media. adCallbacks.get(i).onBuffering(adMediaInfo);
return;
} }
Assertions.checkArgument(timeline.getPeriodCount() == 1); stopUpdatingAdProgress();
this.timeline = timeline; } else if (bufferingAd && playbackState == Player.STATE_READY) {
long contentDurationUs = timeline.getPeriod(/* periodIndex= */ 0, period).durationUs; bufferingAd = false;
contentDurationMs = C.usToMs(contentDurationUs); updateAdProgress();
if (contentDurationUs != C.TIME_UNSET) {
adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs);
} }
@Nullable AdsManager adsManager = this.adsManager; }
if (!isAdsManagerInitialized && adsManager != null) {
isAdsManagerInitialized = true; if (imaAdState == IMA_AD_STATE_NONE
@Nullable AdsRenderingSettings adsRenderingSettings = setupAdsRendering(); && playbackState == Player.STATE_BUFFERING
if (adsRenderingSettings == null) { && playWhenReady) {
// There are no ads to play. checkForContentComplete();
destroyAdsManager(); } else if (imaAdState != IMA_AD_STATE_NONE && playbackState == Player.STATE_ENDED) {
AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo);
if (adMediaInfo == null) {
Log.w(TAG, "onEnded without ad media info");
} else { } else {
adsManager.init(adsRenderingSettings); for (int i = 0; i < adCallbacks.size(); i++) {
adsManager.start(); adCallbacks.get(i).onEnded(adMediaInfo);
if (DEBUG) {
Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings);
} }
} }
updateAdPlaybackState(); if (DEBUG) {
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlaybackStateChanged");
} }
handleTimelineOrPositionChanged();
} }
@Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
handleTimelineOrPositionChanged();
} }
@Override private void handleTimelineOrPositionChanged() {
public void onPlaybackStateChanged(@Player.State int playbackState) {
@Nullable Player player = this.player; @Nullable Player player = this.player;
if (adsManager == null || player == null) { if (adsManager == null || player == null) {
return; return;
} }
if (!playingAd && !player.isPlayingAd()) {
if (playbackState == Player.STATE_BUFFERING && !player.isPlayingAd()) { checkForContentComplete();
// Check whether we are waiting for an ad to preload. if (sentContentComplete) {
int adGroupIndex = getLoadingAdGroupIndex(); for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
if (adGroupIndex == C.INDEX_UNSET) { if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) {
return; adPlaybackState = adPlaybackState.withSkippedAdGroup(/* adGroupIndex= */ i);
}
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;
}
handlePlayerStateChanged(player.getPlayWhenReady(), playbackState);
}
@Override
public void onPlayWhenReadyChanged(
boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) {
if (adsManager == null || player == null) {
return;
}
if (imaAdState == IMA_AD_STATE_PLAYING && !playWhenReady) {
adsManager.pause();
return;
}
if (imaAdState == IMA_AD_STATE_PAUSED && playWhenReady) {
adsManager.resume();
return;
}
handlePlayerStateChanged(playWhenReady, player.getPlaybackState());
}
@Override
public void onPlayerError(ExoPlaybackException error) {
if (imaAdState != IMA_AD_STATE_NONE) {
AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo);
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onError(adMediaInfo);
}
}
}
// Internal methods.
/**
* Configures ads rendering for starting playback, returning the settings for the IMA SDK or
* {@code null} if no ads should play.
*/
@Nullable
private AdsRenderingSettings setupAdsRendering() {
AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings();
adsRenderingSettings.setEnablePreloading(true);
adsRenderingSettings.setMimeTypes(supportedMimeTypes);
if (mediaLoadTimeoutMs != TIMEOUT_UNSET) {
adsRenderingSettings.setLoadVideoTimeout(mediaLoadTimeoutMs);
}
if (mediaBitrate != BITRATE_UNSET) {
adsRenderingSettings.setBitrateKbps(mediaBitrate / 1000);
}
adsRenderingSettings.setFocusSkipButtonWhenAvailable(focusSkipButtonWhenAvailable);
if (adUiElements != null) {
adsRenderingSettings.setUiElements(adUiElements);
}
// Skip ads based on the start position as required.
long[] adGroupTimesUs = adPlaybackState.adGroupTimesUs;
long contentPositionMs =
getContentPeriodPositionMs(Assertions.checkNotNull(player), timeline, period);
int adGroupForPositionIndex =
adPlaybackState.getAdGroupIndexForPositionUs(
C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
if (adGroupForPositionIndex != C.INDEX_UNSET) {
boolean playAdWhenStartingPlayback =
playAdBeforeStartPosition
|| adGroupTimesUs[adGroupForPositionIndex] == C.msToUs(contentPositionMs);
if (!playAdWhenStartingPlayback) {
adGroupForPositionIndex++;
} else if (hasMidrollAdGroups(adGroupTimesUs)) {
// Provide the player's initial position to trigger loading and playing the ad. If there are
// no midrolls, we are playing a preroll and any pending content position wouldn't be
// cleared.
pendingContentPositionMs = contentPositionMs;
}
if (adGroupForPositionIndex > 0) {
for (int i = 0; i < adGroupForPositionIndex; i++) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
}
if (adGroupForPositionIndex == adGroupTimesUs.length) {
// We don't need to play any ads. Because setPlayAdsAfterTime does not discard non-VMAP
// ads, we signal that no ads will render so the caller can destroy the ads manager.
return null;
}
long adGroupForPositionTimeUs = adGroupTimesUs[adGroupForPositionIndex];
long adGroupBeforePositionTimeUs = adGroupTimesUs[adGroupForPositionIndex - 1];
if (adGroupForPositionTimeUs == C.TIME_END_OF_SOURCE) {
// Play the postroll by offsetting the start position just past the last non-postroll ad.
adsRenderingSettings.setPlayAdsAfterTime(
(double) adGroupBeforePositionTimeUs / C.MICROS_PER_SECOND + 1d);
} else {
// Play ads after the midpoint between the ad to play and the one before it, to avoid
// issues with rounding one of the two ad times.
double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforePositionTimeUs) / 2d;
adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND);
}
}
}
return adsRenderingSettings;
}
private void handleAdEvent(AdEvent adEvent) {
switch (adEvent.getType()) {
case AD_BREAK_FETCH_ERROR:
String adGroupTimeSecondsString =
Assertions.checkNotNull(adEvent.getAdData().get("adBreakTime"));
if (DEBUG) {
Log.d(TAG, "Fetch error for ad at " + adGroupTimeSecondsString + " seconds");
}
int adGroupTimeSeconds = Integer.parseInt(adGroupTimeSecondsString);
int adGroupIndex =
adGroupTimeSeconds == -1
? adPlaybackState.adGroupCount - 1
: Util.linearSearch(
adPlaybackState.adGroupTimesUs, C.MICROS_PER_SECOND * adGroupTimeSeconds);
AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];
if (adGroup.count == C.LENGTH_UNSET) {
adPlaybackState =
adPlaybackState.withAdCount(adGroupIndex, Math.max(1, adGroup.states.length));
adGroup = adPlaybackState.adGroups[adGroupIndex];
}
for (int i = 0; i < adGroup.count; i++) {
if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) {
if (DEBUG) {
Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex);
}
adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i);
}
}
updateAdPlaybackState();
break;
case CONTENT_PAUSE_REQUESTED:
// After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads
// before sending CONTENT_RESUME_REQUESTED.
imaPausedContent = true;
pauseContentInternal();
break;
case TAPPED:
if (eventListener != null) {
eventListener.onAdTapped();
}
break;
case CLICKED:
if (eventListener != null) {
eventListener.onAdClicked();
}
break;
case CONTENT_RESUME_REQUESTED:
imaPausedContent = false;
resumeContentInternal();
break;
case LOG:
Map<String, String> adData = adEvent.getAdData();
String message = "AdEvent: " + adData;
Log.i(TAG, message);
break;
default:
break;
}
}
private VideoProgressUpdate getContentVideoProgressUpdate() {
if (player == null) {
return lastContentProgress;
}
boolean hasContentDuration = contentDurationMs != C.TIME_UNSET;
long contentPositionMs;
if (pendingContentPositionMs != C.TIME_UNSET) {
sentPendingContentPositionMs = true;
contentPositionMs = pendingContentPositionMs;
} else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {
long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;
} else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) {
contentPositionMs = getContentPeriodPositionMs(player, timeline, period);
} else {
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
}
long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET;
return new VideoProgressUpdate(contentPositionMs, contentDurationMs);
}
private VideoProgressUpdate getAdVideoProgressUpdate() {
if (player == null) {
return lastAdProgress;
} else if (imaAdState != IMA_AD_STATE_NONE && playingAd) {
long adDuration = player.getDuration();
return adDuration == C.TIME_UNSET
? VideoProgressUpdate.VIDEO_TIME_NOT_READY
: new VideoProgressUpdate(player.getCurrentPosition(), adDuration);
} else {
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
}
}
private void updateAdProgress() {
VideoProgressUpdate videoProgressUpdate = getAdVideoProgressUpdate();
AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo);
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onAdProgress(adMediaInfo, videoProgressUpdate);
}
handler.removeCallbacks(updateAdProgressRunnable);
handler.postDelayed(updateAdProgressRunnable, AD_PROGRESS_UPDATE_INTERVAL_MS);
}
private void stopUpdatingAdProgress() {
handler.removeCallbacks(updateAdProgressRunnable);
}
private void handlePlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
if (playingAd && imaAdState == IMA_AD_STATE_PLAYING) {
if (!bufferingAd && playbackState == Player.STATE_BUFFERING) {
AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo);
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onBuffering(adMediaInfo);
}
stopUpdatingAdProgress();
} else if (bufferingAd && playbackState == Player.STATE_READY) {
bufferingAd = false;
updateAdProgress();
}
}
if (imaAdState == IMA_AD_STATE_NONE
&& playbackState == Player.STATE_BUFFERING
&& playWhenReady) {
checkForContentComplete();
} else if (imaAdState != IMA_AD_STATE_NONE && playbackState == Player.STATE_ENDED) {
AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo);
if (adMediaInfo == null) {
Log.w(TAG, "onEnded without ad media info");
} else {
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onEnded(adMediaInfo);
}
}
if (DEBUG) {
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlaybackStateChanged");
}
}
}
private void handleTimelineOrPositionChanged() {
@Nullable Player player = this.player;
if (adsManager == null || player == null) {
return;
}
if (!playingAd && !player.isPlayingAd()) {
checkForContentComplete();
if (sentContentComplete) {
for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(/* adGroupIndex= */ i);
} }
} }
updateAdPlaybackState(); updateAdPlaybackState();
...@@ -1412,199 +1163,465 @@ public final class ImaAdsLoader ...@@ -1412,199 +1163,465 @@ public final class ImaAdsLoader
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex); Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex);
} }
adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i); adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i);
}
}
updateAdPlaybackState();
if (pendingAdLoadError == null) {
pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex);
}
pendingContentPositionMs = C.TIME_UNSET;
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
}
private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) {
if (DEBUG) {
Log.d(
TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception);
}
if (adsManager == null) {
Log.w(TAG, "Ignoring ad prepare error after release");
return;
}
if (imaAdState == IMA_AD_STATE_NONE) {
// Send IMA a content position at the ad group so that it will try to play it, at which point
// we can notify that it failed to load.
fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime();
fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]);
if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) {
fakeContentProgressOffsetMs = contentDurationMs;
}
pendingAdPrepareErrorAdInfo = new AdInfo(adGroupIndex, adIndexInAdGroup);
} else {
AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo);
// We're already playing an ad.
if (adIndexInAdGroup > playingAdIndexInAdGroup) {
// Mark the playing ad as ended so we can notify the error on the next ad and remove it,
// which means that the ad after will load (if any).
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onEnded(adMediaInfo);
}
}
playingAdIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay();
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onError(Assertions.checkNotNull(adMediaInfo));
}
}
adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, adIndexInAdGroup);
updateAdPlaybackState();
}
private void checkForContentComplete() {
long positionMs = getContentPeriodPositionMs(Assertions.checkNotNull(player), timeline, period);
if (!sentContentComplete
&& contentDurationMs != C.TIME_UNSET
&& pendingContentPositionMs == C.TIME_UNSET
&& positionMs + THRESHOLD_END_OF_CONTENT_MS >= contentDurationMs) {
adsLoader.contentComplete();
if (DEBUG) {
Log.d(TAG, "adsLoader.contentComplete");
}
sentContentComplete = true;
}
}
private void updateAdPlaybackState() {
// Ignore updates while detached. When a player is attached it will receive the latest state.
if (eventListener != null) {
eventListener.onAdPlaybackState(adPlaybackState);
}
}
private void maybeNotifyPendingAdLoadError() {
if (pendingAdLoadError != null && eventListener != null) {
eventListener.onAdLoadError(pendingAdLoadError, getAdsDataSpec(adTagUri));
pendingAdLoadError = null;
}
}
private void maybeNotifyInternalError(String name, Exception cause) {
String message = "Internal error in " + name;
Log.e(TAG, message, cause);
// We can't recover from an unexpected error in general, so skip all remaining ads.
for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
}
updateAdPlaybackState();
if (eventListener != null) {
eventListener.onAdLoadError(
AdLoadException.createForUnexpected(new RuntimeException(message, cause)),
getAdsDataSpec(adTagUri));
}
}
private int getAdGroupIndexForAdPod(AdPodInfo adPodInfo) {
if (adPodInfo.getPodIndex() == -1) {
// This is a postroll ad.
return adPlaybackState.adGroupCount - 1;
}
// adPodInfo.podIndex may be 0-based or 1-based, so for now look up the cue point instead.
long adGroupTimeUs = (long) (((float) adPodInfo.getTimeOffset()) * C.MICROS_PER_SECOND);
for (int adGroupIndex = 0; adGroupIndex < adPlaybackState.adGroupCount; adGroupIndex++) {
if (adPlaybackState.adGroupTimesUs[adGroupIndex] == adGroupTimeUs) {
return adGroupIndex;
}
}
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 : "") + "]";
}
private static DataSpec getAdsDataSpec(@Nullable Uri adTagUri) {
return new DataSpec(adTagUri != null ? adTagUri : Uri.EMPTY);
}
private static long getContentPeriodPositionMs(
Player player, Timeline timeline, Timeline.Period period) {
long contentWindowPositionMs = player.getContentPosition();
return contentWindowPositionMs
- (timeline.isEmpty()
? 0
: timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs());
}
private static boolean isAdGroupLoadError(AdError adError) {
// TODO: Find out what other errors need to be handled (if any), and whether each one relates to
// a single ad, ad group or the whole timeline.
return adError.getErrorCode() == AdErrorCode.VAST_LINEAR_ASSET_MISMATCH
|| adError.getErrorCode() == AdErrorCode.UNKNOWN_ERROR;
}
private static Looper getImaLooper() {
// IMA SDK callbacks occur on the main thread. This method can be used to check that the player
// is using the same looper, to ensure all interaction with this class is on the main thread.
return Looper.getMainLooper();
}
private static boolean hasMidrollAdGroups(long[] adGroupTimesUs) {
int count = adGroupTimesUs.length;
if (count == 1) {
return adGroupTimesUs[0] != 0 && adGroupTimesUs[0] != C.TIME_END_OF_SOURCE;
} else if (count == 2) {
return adGroupTimesUs[0] != 0 || adGroupTimesUs[1] != C.TIME_END_OF_SOURCE;
} else {
// There's at least one midroll ad group, as adGroupTimesUs is never empty.
return true;
}
}
private void destroyAdsManager() {
if (adsManager != null) {
adsManager.removeAdErrorListener(componentListener);
adsManager.removeAdEventListener(componentListener);
if (adEventListener != null) {
adsManager.removeAdEventListener(adEventListener);
}
adsManager.destroy();
adsManager = null;
}
}
/** Factory for objects provided by the IMA SDK. */
@VisibleForTesting
/* package */ interface ImaFactory {
/** @see ImaSdkSettings */
ImaSdkSettings createImaSdkSettings();
/** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRenderingSettings() */
AdsRenderingSettings createAdsRenderingSettings();
/** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdDisplayContainer() */
AdDisplayContainer createAdDisplayContainer();
/** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRequest() */
AdsRequest createAdsRequest();
/** @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings, AdDisplayContainer) */
com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
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;
} }
updateAdPlaybackState();
if (pendingAdLoadError == null) { // VideoAdPlayer implementation.
pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex);
@Override
public VideoProgressUpdate getAdProgress() {
throw new IllegalStateException("Unexpected call to getAdProgress when using preloading");
} }
pendingContentPositionMs = C.TIME_UNSET;
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; @Override
public int getVolume() {
return getPlayerVolumePercent();
} }
private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) { @Override
public void loadAd(AdMediaInfo adMediaInfo, AdPodInfo adPodInfo) {
try {
if (DEBUG) { if (DEBUG) {
Log.d( Log.d(TAG, "loadAd " + getAdMediaInfoString(adMediaInfo) + ", ad pod " + adPodInfo);
TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception);
} }
if (adsManager == null) { if (adsManager == null) {
Log.w(TAG, "Ignoring ad prepare error after release"); // Drop events after release.
return; return;
} }
if (imaAdState == IMA_AD_STATE_NONE) { int adGroupIndex = getAdGroupIndexForAdPod(adPodInfo);
// Send IMA a content position at the ad group so that it will try to play it, at which point int adIndexInAdGroup = adPodInfo.getAdPosition() - 1;
// we can notify that it failed to load. AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup);
fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); adInfoByAdMediaInfo.put(adMediaInfo, adInfo);
fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); if (adPlaybackState.isAdInErrorState(adGroupIndex, adIndexInAdGroup)) {
if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { // We have already marked this ad as having failed to load, so ignore the request. IMA
fakeContentProgressOffsetMs = contentDurationMs; // will timeout after its media load timeout.
} return;
pendingAdPrepareErrorAdInfo = new AdInfo(adGroupIndex, adIndexInAdGroup);
} else {
AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo);
// We're already playing an ad.
if (adIndexInAdGroup > playingAdIndexInAdGroup) {
// Mark the playing ad as ended so we can notify the error on the next ad and remove it,
// which means that the ad after will load (if any).
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onEnded(adMediaInfo);
} }
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];
} }
playingAdIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); for (int i = 0; i < adIndexInAdGroup; i++) {
for (int i = 0; i < adCallbacks.size(); i++) { // Any preceding ads that haven't loaded are not going to load.
adCallbacks.get(i).onError(Assertions.checkNotNull(adMediaInfo)); if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) {
adPlaybackState =
adPlaybackState.withAdLoadError(
/* adGroupIndex= */ adGroupIndex, /* adIndexInAdGroup= */ i);
} }
} }
adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, adIndexInAdGroup); Uri adUri = Uri.parse(adMediaInfo.getUrl());
adPlaybackState =
adPlaybackState.withAdUri(adInfo.adGroupIndex, adInfo.adIndexInAdGroup, adUri);
updateAdPlaybackState(); updateAdPlaybackState();
} } catch (RuntimeException e) {
maybeNotifyInternalError("loadAd", e);
private void checkForContentComplete() {
long positionMs = getContentPeriodPositionMs(Assertions.checkNotNull(player), timeline, period);
if (!sentContentComplete
&& contentDurationMs != C.TIME_UNSET
&& pendingContentPositionMs == C.TIME_UNSET
&& positionMs + THRESHOLD_END_OF_CONTENT_MS >= contentDurationMs) {
adsLoader.contentComplete();
if (DEBUG) {
Log.d(TAG, "adsLoader.contentComplete");
}
sentContentComplete = true;
} }
} }
private void updateAdPlaybackState() { @Override
// Ignore updates while detached. When a player is attached it will receive the latest state. public void addCallback(VideoAdPlayerCallback videoAdPlayerCallback) {
if (eventListener != null) { adCallbacks.add(videoAdPlayerCallback);
eventListener.onAdPlaybackState(adPlaybackState);
}
} }
private void maybeNotifyPendingAdLoadError() { @Override
if (pendingAdLoadError != null && eventListener != null) { public void removeCallback(VideoAdPlayerCallback videoAdPlayerCallback) {
eventListener.onAdLoadError(pendingAdLoadError, getAdsDataSpec(adTagUri)); adCallbacks.remove(videoAdPlayerCallback);
pendingAdLoadError = null;
}
} }
private void maybeNotifyInternalError(String name, Exception cause) { @Override
String message = "Internal error in " + name; public void playAd(AdMediaInfo adMediaInfo) {
Log.e(TAG, message, cause); if (DEBUG) {
// We can't recover from an unexpected error in general, so skip all remaining ads. Log.d(TAG, "playAd " + getAdMediaInfoString(adMediaInfo));
for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
}
updateAdPlaybackState();
if (eventListener != null) {
eventListener.onAdLoadError(
AdLoadException.createForUnexpected(new RuntimeException(message, cause)),
getAdsDataSpec(adTagUri));
} }
if (adsManager == null) {
// Drop events after release.
return;
} }
private int getAdGroupIndexForAdPod(AdPodInfo adPodInfo) { if (imaAdState == IMA_AD_STATE_PLAYING) {
if (adPodInfo.getPodIndex() == -1) { // IMA does not always call stopAd before resuming content.
// This is a postroll ad. // See [Internal: b/38354028].
return adPlaybackState.adGroupCount - 1; Log.w(TAG, "Unexpected playAd without stopAd");
} }
// adPodInfo.podIndex may be 0-based or 1-based, so for now look up the cue point instead. try {
long adGroupTimeUs = (long) (((float) adPodInfo.getTimeOffset()) * C.MICROS_PER_SECOND); if (imaAdState == IMA_AD_STATE_NONE) {
for (int adGroupIndex = 0; adGroupIndex < adPlaybackState.adGroupCount; adGroupIndex++) { // IMA is requesting to play the ad, so stop faking the content position.
if (adPlaybackState.adGroupTimesUs[adGroupIndex] == adGroupTimeUs) { fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
return adGroupIndex; 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);
} }
throw new IllegalStateException("Failed to find cue point");
} }
updateAdProgress();
/** } else {
* Returns the index of the ad group that will preload next, or {@link C#INDEX_UNSET} if there is imaAdState = IMA_AD_STATE_PLAYING;
* no such ad group. Assertions.checkState(adMediaInfo.equals(imaAdMediaInfo));
*/ for (int i = 0; i < adCallbacks.size(); i++) {
private int getLoadingAdGroupIndex() { adCallbacks.get(i).onResume(adMediaInfo);
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;
} }
if (!Assertions.checkNotNull(player).getPlayWhenReady()) {
private String getAdMediaInfoString(AdMediaInfo adMediaInfo) { Assertions.checkNotNull(adsManager).pause();
@Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo);
return "AdMediaInfo[" + adMediaInfo.getUrl() + (adInfo != null ? ", " + adInfo : "") + "]";
} }
} catch (RuntimeException e) {
private static DataSpec getAdsDataSpec(@Nullable Uri adTagUri) { maybeNotifyInternalError("playAd", e);
return new DataSpec(adTagUri != null ? adTagUri : Uri.EMPTY);
} }
private static long getContentPeriodPositionMs(
Player player, Timeline timeline, Timeline.Period period) {
long contentWindowPositionMs = player.getContentPosition();
return contentWindowPositionMs
- (timeline.isEmpty()
? 0
: timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs());
} }
private static boolean isAdGroupLoadError(AdError adError) { @Override
// TODO: Find out what other errors need to be handled (if any), and whether each one relates to public void stopAd(AdMediaInfo adMediaInfo) {
// a single ad, ad group or the whole timeline. if (DEBUG) {
return adError.getErrorCode() == AdErrorCode.VAST_LINEAR_ASSET_MISMATCH Log.d(TAG, "stopAd " + getAdMediaInfoString(adMediaInfo));
|| adError.getErrorCode() == AdErrorCode.UNKNOWN_ERROR; }
if (adsManager == null) {
// Drop event after release.
return;
} }
private static Looper getImaLooper() { try {
// IMA SDK callbacks occur on the main thread. This method can be used to check that the player Assertions.checkNotNull(player);
// is using the same looper, to ensure all interaction with this class is on the main thread. Assertions.checkState(imaAdState != IMA_AD_STATE_NONE);
return Looper.getMainLooper(); stopAdInternal();
} catch (RuntimeException e) {
maybeNotifyInternalError("stopAd", e);
}
} }
private static boolean hasMidrollAdGroups(long[] adGroupTimesUs) { @Override
int count = adGroupTimesUs.length; public void pauseAd(AdMediaInfo adMediaInfo) {
if (count == 1) { if (DEBUG) {
return adGroupTimesUs[0] != 0 && adGroupTimesUs[0] != C.TIME_END_OF_SOURCE; Log.d(TAG, "pauseAd " + getAdMediaInfoString(adMediaInfo));
} else if (count == 2) {
return adGroupTimesUs[0] != 0 || adGroupTimesUs[1] != C.TIME_END_OF_SOURCE;
} else {
// There's at least one midroll ad group, as adGroupTimesUs is never empty.
return true;
} }
if (imaAdState == IMA_AD_STATE_NONE) {
// This method is called after content is resumed.
return;
} }
private void destroyAdsManager() { try {
if (adsManager != null) { Assertions.checkState(adMediaInfo.equals(imaAdMediaInfo));
adsManager.removeAdErrorListener(this); imaAdState = IMA_AD_STATE_PAUSED;
adsManager.removeAdEventListener(this); for (int i = 0; i < adCallbacks.size(); i++) {
if (adEventListener != null) { adCallbacks.get(i).onPause(adMediaInfo);
adsManager.removeAdEventListener(adEventListener);
} }
adsManager.destroy(); } catch (RuntimeException e) {
adsManager = null; maybeNotifyInternalError("pauseAd", e);
} }
} }
/** Factory for objects provided by the IMA SDK. */ @Override
@VisibleForTesting public void release() {
/* package */ interface ImaFactory { // Do nothing.
/** @see ImaSdkSettings */ }
ImaSdkSettings createImaSdkSettings();
/** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRenderingSettings() */
AdsRenderingSettings createAdsRenderingSettings();
/** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdDisplayContainer() */
AdDisplayContainer createAdDisplayContainer();
/** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRequest() */
AdsRequest createAdsRequest();
/** @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings, AdDisplayContainer) */
com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer);
} }
// TODO: Consider moving this into AdPlaybackState. // TODO: Consider moving this into AdPlaybackState.
......
...@@ -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