Migrate to new IMA preloading APIs

issue:#6429
PiperOrigin-RevId: 309906760
This commit is contained in:
andrewlewis 2020-05-05 10:09:01 +01:00 committed by Oliver Woodman
parent a8d1de5198
commit fa7d26dd9f
4 changed files with 292 additions and 225 deletions

View File

@ -172,6 +172,8 @@
([#7234](https://github.com/google/ExoPlayer/issues/7234)). ([#7234](https://github.com/google/ExoPlayer/issues/7234)).
* AV1 extension: Add a heuristic to determine the default number of threads * AV1 extension: Add a heuristic to determine the default number of threads
used for AV1 playback using the extension. used for AV1 playback using the extension.
* IMA extension: Upgrade to IMA SDK version 3.18.1, and migrate to new
preloading APIs ([#6429](https://github.com/google/ExoPlayer/issues/6429)).
### 2.11.4 (2020-04-08) ### 2.11.4 (2020-04-08)

View File

@ -39,7 +39,7 @@ android {
} }
dependencies { dependencies {
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.3' api 'com.google.ads.interactivemedia.v3:interactivemedia:3.18.1'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0' implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0'

View File

@ -19,6 +19,7 @@ import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.SystemClock; import android.os.SystemClock;
import android.view.View; import android.view.View;
@ -26,7 +27,6 @@ import android.view.ViewGroup;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.google.ads.interactivemedia.v3.api.Ad;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
import com.google.ads.interactivemedia.v3.api.AdError; import com.google.ads.interactivemedia.v3.api.AdError;
import com.google.ads.interactivemedia.v3.api.AdError.AdErrorCode; import com.google.ads.interactivemedia.v3.api.AdError.AdErrorCode;
@ -45,6 +45,7 @@ import com.google.ads.interactivemedia.v3.api.CompanionAdSlot;
import com.google.ads.interactivemedia.v3.api.ImaSdkFactory; import com.google.ads.interactivemedia.v3.api.ImaSdkFactory;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
import com.google.ads.interactivemedia.v3.api.UiElement; import com.google.ads.interactivemedia.v3.api.UiElement;
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.ContentProgressProvider;
import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate;
@ -54,7 +55,6 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.source.ads.AdPlaybackState.AdState;
import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
@ -71,6 +71,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -277,6 +278,14 @@ public final class ImaAdsLoader
private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima"; private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima";
private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION; private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION;
/**
* Interval at which ad progress updates are provided to the IMA SDK, in milliseconds. 100 ms is
* the interval recommended by the IMA documentation.
*
* @see com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer.VideoAdPlayerCallback
*/
private static final int AD_PROGRESS_UPDATE_INTERVAL_MS = 100;
/** The value used in {@link VideoProgressUpdate}s to indicate an unset duration. */ /** The value used in {@link VideoProgressUpdate}s to indicate an unset duration. */
private static final long IMA_DURATION_UNSET = -1L; private static final long IMA_DURATION_UNSET = -1L;
@ -286,9 +295,6 @@ public final class ImaAdsLoader
*/ */
private static final long END_OF_CONTENT_THRESHOLD_MS = 5000; private static final long END_OF_CONTENT_THRESHOLD_MS = 5000;
/** The maximum duration before an ad break that IMA may start preloading the next ad. */
private static final long MAXIMUM_PRELOAD_DURATION_MS = 8000;
private static final int TIMEOUT_UNSET = -1; private static final int TIMEOUT_UNSET = -1;
private static final int BITRATE_UNSET = -1; private static final int BITRATE_UNSET = -1;
@ -302,11 +308,12 @@ 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()} and not {@link #pauseAd()}. * The ad playback state when IMA has called {@link #playAd(AdMediaInfo)} and not {@link
* #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()} while playing an ad. * The ad playback state when IMA has called {@link #pauseAd(AdMediaInfo)} while playing an ad.
*/ */
private static final int IMA_AD_STATE_PAUSED = 2; private static final int IMA_AD_STATE_PAUSED = 2;
@ -320,9 +327,12 @@ public final class ImaAdsLoader
@Nullable private final AdEventListener adEventListener; @Nullable private final AdEventListener adEventListener;
private final ImaFactory imaFactory; private final ImaFactory imaFactory;
private final Timeline.Period period; private final Timeline.Period period;
private final Handler handler;
private final List<VideoAdPlayerCallback> adCallbacks; private final List<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 Map<AdMediaInfo, AdInfo> adInfoByAdMediaInfo;
private boolean wasSetPlayerCalled; private boolean wasSetPlayerCalled;
@Nullable private Player nextPlayer; @Nullable private Player nextPlayer;
@ -340,19 +350,18 @@ public final class ImaAdsLoader
@Nullable private AdLoadException pendingAdLoadError; @Nullable private AdLoadException pendingAdLoadError;
private Timeline timeline; private Timeline timeline;
private long contentDurationMs; private long contentDurationMs;
private int podIndexOffset;
private AdPlaybackState adPlaybackState; private AdPlaybackState adPlaybackState;
// Fields tracking IMA's state. // Fields tracking IMA's state.
/** The expected ad group index that IMA should load next. */
private int expectedAdGroupIndex;
/** The index of the current ad group that IMA is loading. */
private int adGroupIndex;
/** Whether IMA has sent an ad event to pause content since the last resume content event. */ /** Whether IMA has sent an ad event to pause content since the last resume content event. */
private boolean imaPausedContent; private boolean imaPausedContent;
/** The current ad playback state. */ /** The current ad playback state. */
private @ImaAdState int imaAdState; private @ImaAdState int imaAdState;
/** The current ad media info, or {@code null} if in state {@link #IMA_AD_STATE_NONE}. */
@Nullable private AdMediaInfo imaAdMediaInfo;
/** The current ad info, or {@code null} if in state {@link #IMA_AD_STATE_NONE}. */
@Nullable private AdInfo imaAdInfo;
/** /**
* Whether {@link com.google.ads.interactivemedia.v3.api.AdsLoader#contentComplete()} has been * Whether {@link com.google.ads.interactivemedia.v3.api.AdsLoader#contentComplete()} has been
* called since starting ad playback. * called since starting ad playback.
@ -363,20 +372,23 @@ public final class ImaAdsLoader
/** Whether the player is playing an ad. */ /** Whether the player is playing an ad. */
private boolean playingAd; private boolean playingAd;
/** Whether the player is buffering an ad. */
private boolean bufferingAd;
/** /**
* If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET} * If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET}
* otherwise. * otherwise.
*/ */
private int playingAdIndexInAdGroup; private int playingAdIndexInAdGroup;
/** /**
* Whether there's a pending ad preparation error which IMA needs to be notified of when it * The ad info for a pending ad for which the media failed preparation, or {@code null} if no
* transitions from playing content to playing the ad. * pending ads have failed to prepare.
*/ */
private boolean shouldNotifyAdPrepareError; @Nullable private AdInfo pendingAdPrepareErrorAdInfo;
/** /**
* If a content period has finished but IMA has not yet called {@link #playAd()}, stores the value * If a content period has finished but IMA has not yet called {@link #playAd(AdMediaInfo)},
* of {@link SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to * stores the value of {@link SystemClock#elapsedRealtime()} when the content stopped playing.
* determine a fake, increasing content position. {@link C#TIME_UNSET} otherwise. * This can be used to determine a fake, increasing content position. {@link C#TIME_UNSET}
* otherwise.
*/ */
private long fakeContentProgressElapsedRealtimeMs; private long fakeContentProgressElapsedRealtimeMs;
/** /**
@ -441,7 +453,7 @@ public final class ImaAdsLoader
/* imaFactory= */ new DefaultImaFactory()); /* imaFactory= */ new DefaultImaFactory());
} }
@SuppressWarnings("nullness:argument.type.incompatible") @SuppressWarnings({"nullness:argument.type.incompatible", "methodref.receiver.bound.invalid"})
private ImaAdsLoader( private ImaAdsLoader(
Context context, Context context,
@Nullable Uri adTagUri, @Nullable Uri adTagUri,
@ -473,6 +485,7 @@ public final class ImaAdsLoader
imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE); imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE);
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);
adCallbacks = new ArrayList<>(/* initialCapacity= */ 1); adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
adDisplayContainer = imaFactory.createAdDisplayContainer(); adDisplayContainer = imaFactory.createAdDisplayContainer();
adDisplayContainer.setPlayer(/* videoAdPlayer= */ this); adDisplayContainer.setPlayer(/* videoAdPlayer= */ this);
@ -481,13 +494,14 @@ public final class ImaAdsLoader
context.getApplicationContext(), imaSdkSettings, adDisplayContainer); context.getApplicationContext(), imaSdkSettings, adDisplayContainer);
adsLoader.addAdErrorListener(/* adErrorListener= */ this); adsLoader.addAdErrorListener(/* adErrorListener= */ this);
adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this); adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this);
updateAdProgressRunnable = this::updateAdProgress;
adInfoByAdMediaInfo = new HashMap<>();
supportedMimeTypes = Collections.emptyList(); supportedMimeTypes = Collections.emptyList();
lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
fakeContentProgressOffsetMs = C.TIME_UNSET; fakeContentProgressOffsetMs = C.TIME_UNSET;
pendingContentPositionMs = C.TIME_UNSET; pendingContentPositionMs = C.TIME_UNSET;
adGroupIndex = C.INDEX_UNSET;
contentDurationMs = C.TIME_UNSET; contentDurationMs = C.TIME_UNSET;
timeline = Timeline.EMPTY; timeline = Timeline.EMPTY;
adPlaybackState = AdPlaybackState.NONE; adPlaybackState = AdPlaybackState.NONE;
@ -562,9 +576,8 @@ public final class ImaAdsLoader
@Override @Override
public void setPlayer(@Nullable Player player) { public void setPlayer(@Nullable Player player) {
Assertions.checkState(Looper.getMainLooper() == Looper.myLooper()); Assertions.checkState(Looper.myLooper() == getImaLooper());
Assertions.checkState( Assertions.checkState(player == null || player.getApplicationLooper() == getImaLooper());
player == null || player.getApplicationLooper() == Looper.getMainLooper());
nextPlayer = player; nextPlayer = player;
wasSetPlayerCalled = true; wasSetPlayerCalled = true;
} }
@ -640,7 +653,7 @@ public final class ImaAdsLoader
playingAd ? C.msToUs(player.getCurrentPosition()) : 0); playingAd ? C.msToUs(player.getCurrentPosition()) : 0);
} }
lastVolumePercentage = getVolume(); lastVolumePercentage = getVolume();
lastAdProgress = getAdProgress(); lastAdProgress = getAdVideoProgressUpdate();
lastContentProgress = getContentProgress(); lastContentProgress = getContentProgress();
adDisplayContainer.unregisterAllVideoControlsOverlays(); adDisplayContainer.unregisterAllVideoControlsOverlays();
player.removeListener(this); player.removeListener(this);
@ -664,6 +677,8 @@ public final class ImaAdsLoader
adsLoader.removeAdErrorListener(/* adErrorListener= */ this); adsLoader.removeAdErrorListener(/* adErrorListener= */ this);
imaPausedContent = false; imaPausedContent = false;
imaAdState = IMA_AD_STATE_NONE; imaAdState = IMA_AD_STATE_NONE;
imaAdMediaInfo = null;
imaAdInfo = null;
pendingAdLoadError = null; pendingAdLoadError = null;
adPlaybackState = AdPlaybackState.NONE; adPlaybackState = AdPlaybackState.NONE;
hasAdPlaybackState = false; hasAdPlaybackState = false;
@ -768,32 +783,11 @@ public final class ImaAdsLoader
if (pendingContentPositionMs != C.TIME_UNSET) { if (pendingContentPositionMs != C.TIME_UNSET) {
sentPendingContentPositionMs = true; sentPendingContentPositionMs = true;
contentPositionMs = pendingContentPositionMs; contentPositionMs = pendingContentPositionMs;
expectedAdGroupIndex =
adPlaybackState.getAdGroupIndexForPositionUs(
C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
} else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {
long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;
expectedAdGroupIndex =
adPlaybackState.getAdGroupIndexForPositionUs(
C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
} else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) { } else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) {
contentPositionMs = getContentPeriodPositionMs(player, timeline, period); contentPositionMs = getContentPeriodPositionMs(player, timeline, period);
// Update the expected ad group index for the current content position. The update is delayed
// until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered
// just after an ad group isn't incorrectly attributed to the next ad group.
int nextAdGroupIndex =
adPlaybackState.getAdGroupIndexAfterPositionUs(
C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
if (nextAdGroupIndex != expectedAdGroupIndex && nextAdGroupIndex != C.INDEX_UNSET) {
long nextAdGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[nextAdGroupIndex]);
if (nextAdGroupTimeMs == C.TIME_END_OF_SOURCE) {
nextAdGroupTimeMs = contentDurationMs;
}
if (nextAdGroupTimeMs - contentPositionMs < MAXIMUM_PRELOAD_DURATION_MS) {
expectedAdGroupIndex = nextAdGroupIndex;
}
}
} else { } else {
return VideoProgressUpdate.VIDEO_TIME_NOT_READY; return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
} }
@ -805,15 +799,7 @@ public final class ImaAdsLoader
@Override @Override
public VideoProgressUpdate getAdProgress() { public VideoProgressUpdate getAdProgress() {
if (player == null) { throw new IllegalStateException("Unexpected call to getAdProgress when using preloading");
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;
}
} }
@Override @Override
@ -839,30 +825,37 @@ public final class ImaAdsLoader
} }
@Override @Override
public void loadAd(String adUriString) { public void loadAd(AdMediaInfo adMediaInfo, AdPodInfo adPodInfo) {
try { try {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "loadAd in ad group " + adGroupIndex); Log.d(TAG, "loadAd " + getAdMediaInfoString(adMediaInfo) + ", ad pod " + adPodInfo);
} }
if (adsManager == null) { if (adsManager == null) {
// Drop events after release. // Drop events after release.
return; return;
} }
if (adGroupIndex == C.INDEX_UNSET) { int adGroupIndex = getAdGroupIndex(adPodInfo);
adGroupIndex = expectedAdGroupIndex; int adIndexInAdGroup = adPodInfo.getAdPosition() - 1;
adsManager.start(); AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup);
Log.w( adInfoByAdMediaInfo.put(adMediaInfo, adInfo);
TAG, AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adInfo.adGroupIndex];
"Unexpected loadAd without LOADED event; assuming ad group index is actually " if (adGroup.count == C.LENGTH_UNSET) {
+ expectedAdGroupIndex); adPlaybackState =
adPlaybackState.withAdCount(
adInfo.adGroupIndex, Math.max(adPodInfo.getTotalAds(), adGroup.states.length));
adGroup = adPlaybackState.adGroups[adInfo.adGroupIndex];
} }
int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex); for (int i = 0; i < adIndexInAdGroup; i++) {
if (adIndexInAdGroup == C.INDEX_UNSET) { // Any preceding ads that haven't loaded are not going to load.
Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads"); if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) {
return; adPlaybackState =
adPlaybackState.withAdLoadError(
/* adGroupIndex= */ adGroupIndex, /* adIndexInAdGroup= */ i);
}
} }
Uri adUri = Uri.parse(adMediaInfo.getUrl());
adPlaybackState = adPlaybackState =
adPlaybackState.withAdUri(adGroupIndex, adIndexInAdGroup, Uri.parse(adUriString)); adPlaybackState.withAdUri(adInfo.adGroupIndex, adInfo.adIndexInAdGroup, adUri);
updateAdPlaybackState(); updateAdPlaybackState();
} catch (Exception e) { } catch (Exception e) {
maybeNotifyInternalError("loadAd", e); maybeNotifyInternalError("loadAd", e);
@ -880,69 +873,62 @@ public final class ImaAdsLoader
} }
@Override @Override
public void playAd() { public void playAd(AdMediaInfo adMediaInfo) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "playAd"); Log.d(TAG, "playAd " + getAdMediaInfoString(adMediaInfo));
} }
if (adsManager == null) { if (adsManager == null) {
// Drop events after release. // Drop events after release.
return; return;
} }
switch (imaAdState) {
case IMA_AD_STATE_PLAYING: if (imaAdState == IMA_AD_STATE_PLAYING) {
// IMA does not always call stopAd before resuming content. // IMA does not always call stopAd before resuming content.
// See [Internal: b/38354028, b/63320878]. // See [Internal: b/38354028].
Log.w(TAG, "Unexpected playAd without stopAd"); Log.w(TAG, "Unexpected playAd without stopAd");
break;
case 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;
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onPlay();
}
if (shouldNotifyAdPrepareError) {
shouldNotifyAdPrepareError = false;
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onError();
}
}
break;
case IMA_AD_STATE_PAUSED:
imaAdState = IMA_AD_STATE_PLAYING;
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onResume();
}
break;
default:
throw new IllegalStateException();
} }
if (player == null) {
// Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642]. if (imaAdState == IMA_AD_STATE_NONE) {
Log.w(TAG, "Unexpected playAd while detached"); // IMA is requesting to play the ad, so stop faking the content position.
} else if (!player.getPlayWhenReady()) { 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(); Assertions.checkNotNull(adsManager).pause();
} }
} }
@Override @Override
public void stopAd() { public void stopAd(AdMediaInfo adMediaInfo) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "stopAd"); Log.d(TAG, "stopAd " + getAdMediaInfoString(adMediaInfo));
} }
if (adsManager == null) { if (adsManager == null) {
// Drop event after release. // Drop event after release.
return; return;
} }
if (player == null) {
// Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642]. Assertions.checkNotNull(player);
Log.w(TAG, "Unexpected stopAd while detached"); Assertions.checkState(imaAdState != IMA_AD_STATE_NONE);
}
if (imaAdState == IMA_AD_STATE_NONE) {
Log.w(TAG, "Unexpected stopAd");
return;
}
try { try {
stopAdInternal(); stopAdInternal();
} catch (Exception e) { } catch (Exception e) {
@ -951,26 +937,21 @@ public final class ImaAdsLoader
} }
@Override @Override
public void pauseAd() { public void pauseAd(AdMediaInfo adMediaInfo) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "pauseAd"); Log.d(TAG, "pauseAd " + getAdMediaInfoString(adMediaInfo));
} }
if (imaAdState == IMA_AD_STATE_NONE) { if (imaAdState == IMA_AD_STATE_NONE) {
// This method is called after content is resumed. // This method is called after content is resumed.
return; return;
} }
Assertions.checkState(adMediaInfo.equals(imaAdMediaInfo));
imaAdState = IMA_AD_STATE_PAUSED; imaAdState = IMA_AD_STATE_PAUSED;
for (int i = 0; i < adCallbacks.size(); i++) { for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onPause(); adCallbacks.get(i).onPause(adMediaInfo);
} }
} }
@Override
public void resumeAd() {
// This method is never called. See [Internal: b/18931719].
maybeNotifyInternalError("resumeAd", new IllegalStateException("Unexpected call to resumeAd"));
}
// Player.EventListener implementation. // Player.EventListener implementation.
@Override @Override
@ -1028,8 +1009,9 @@ public final class ImaAdsLoader
@Override @Override
public void onPlayerError(ExoPlaybackException error) { public void onPlayerError(ExoPlaybackException error) {
if (imaAdState != IMA_AD_STATE_NONE) { if (imaAdState != IMA_AD_STATE_NONE) {
AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo);
for (int i = 0; i < adCallbacks.size(); i++) { for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onError(); adCallbacks.get(i).onError(adMediaInfo);
} }
} }
} }
@ -1071,25 +1053,13 @@ public final class ImaAdsLoader
adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND);
} }
// IMA indexes any remaining midroll ad pods from 1. A preroll (if present) has index 0.
// Store an index offset as we want to index all ads (including skipped ones) from 0.
if (adGroupIndexForPosition == 0 && adGroupTimesUs[0] == 0) {
// We are playing a preroll.
podIndexOffset = 0;
} else if (adGroupIndexForPosition == C.INDEX_UNSET) {
// There's no ad to play which means there's no preroll.
podIndexOffset = -1;
} else {
// We are playing a midroll and any ads before it were skipped.
podIndexOffset = adGroupIndexForPosition - 1;
}
if (adGroupIndexForPosition != C.INDEX_UNSET && hasMidrollAdGroups(adGroupTimesUs)) { if (adGroupIndexForPosition != C.INDEX_UNSET && hasMidrollAdGroups(adGroupTimesUs)) {
// Provide the player's initial position to trigger loading and playing the ad. // Provide the player's initial position to trigger loading and playing the ad.
pendingContentPositionMs = contentPositionMs; pendingContentPositionMs = contentPositionMs;
} }
adsManager.init(adsRenderingSettings); adsManager.init(adsRenderingSettings);
adsManager.start();
updateAdPlaybackState(); updateAdPlaybackState();
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings);
@ -1097,39 +1067,32 @@ public final class ImaAdsLoader
} }
private void handleAdEvent(AdEvent adEvent) { private void handleAdEvent(AdEvent adEvent) {
Ad ad = adEvent.getAd();
switch (adEvent.getType()) { switch (adEvent.getType()) {
case LOADED: case AD_BREAK_FETCH_ERROR:
// The ad position is not always accurate when using preloading. See [Internal: b/62613240]. String adGroupTimeSecondsString =
AdPodInfo adPodInfo = ad.getAdPodInfo(); Assertions.checkNotNull(adEvent.getAdData().get("adBreakTime"));
int podIndex = adPodInfo.getPodIndex();
adGroupIndex =
podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset);
int adPosition = adPodInfo.getAdPosition();
int adCount = adPodInfo.getTotalAds();
Assertions.checkNotNull(adsManager).start();
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex); Log.d(TAG, "Fetch error for ad at " + adGroupTimeSecondsString + " seconds");
} }
int oldAdCount = adPlaybackState.adGroups[adGroupIndex].count; int adGroupTimeSeconds = Integer.parseInt(adGroupTimeSecondsString);
if (adCount != oldAdCount) { int adGroupIndex =
if (oldAdCount == C.LENGTH_UNSET) { Arrays.binarySearch(
adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, adCount); adPlaybackState.adGroupTimesUs, C.MICROS_PER_SECOND * adGroupTimeSeconds);
updateAdPlaybackState(); AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];
} else { if (adGroup.count == C.LENGTH_UNSET) {
// IMA sometimes unexpectedly decreases the ad count in an ad group. adPlaybackState =
Log.w(TAG, "Unexpected ad count in LOADED, " + adCount + ", expected " + oldAdCount); 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);
} }
} }
if (adGroupIndex != expectedAdGroupIndex) { updateAdPlaybackState();
Log.w(
TAG,
"Expected ad group index "
+ expectedAdGroupIndex
+ ", actual ad group index "
+ adGroupIndex);
expectedAdGroupIndex = adGroupIndex;
}
break; break;
case CONTENT_PAUSE_REQUESTED: case CONTENT_PAUSE_REQUESTED:
// After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads
@ -1155,23 +1118,65 @@ public final class ImaAdsLoader
Map<String, String> adData = adEvent.getAdData(); Map<String, String> adData = adEvent.getAdData();
String message = "AdEvent: " + adData; String message = "AdEvent: " + adData;
Log.i(TAG, message); Log.i(TAG, message);
if ("adLoadError".equals(adData.get("type"))) {
handleAdGroupLoadError(new IOException(message));
}
break; break;
default: default:
break; break;
} }
} }
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) { 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 if (imaAdState == IMA_AD_STATE_NONE
&& playbackState == Player.STATE_BUFFERING && playbackState == Player.STATE_BUFFERING
&& playWhenReady) { && playWhenReady) {
checkForContentComplete(); checkForContentComplete();
} else if (imaAdState != IMA_AD_STATE_NONE && playbackState == Player.STATE_ENDED) { } else if (imaAdState != IMA_AD_STATE_NONE && playbackState == Player.STATE_ENDED) {
for (int i = 0; i < adCallbacks.size(); i++) { AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo);
adCallbacks.get(i).onEnded(); 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) { if (DEBUG) {
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlaybackStateChanged"); Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlaybackStateChanged");
@ -1200,9 +1205,6 @@ public final class ImaAdsLoader
if (newAdGroupIndex != C.INDEX_UNSET) { if (newAdGroupIndex != C.INDEX_UNSET) {
sentPendingContentPositionMs = false; sentPendingContentPositionMs = false;
pendingContentPositionMs = positionMs; pendingContentPositionMs = positionMs;
if (newAdGroupIndex != adGroupIndex) {
shouldNotifyAdPrepareError = false;
}
} }
} }
} }
@ -1215,8 +1217,13 @@ public final class ImaAdsLoader
if (adFinished) { if (adFinished) {
// IMA is waiting for the ad playback to finish so invoke the callback now. // IMA is waiting for the ad playback to finish so invoke the callback now.
// Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again.
for (int i = 0; i < adCallbacks.size(); i++) { @Nullable AdMediaInfo adMediaInfo = imaAdMediaInfo;
adCallbacks.get(i).onEnded(); 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) { if (DEBUG) {
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity"); Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity");
@ -1234,15 +1241,8 @@ public final class ImaAdsLoader
} }
private void resumeContentInternal() { private void resumeContentInternal() {
if (imaAdState != IMA_AD_STATE_NONE) { if (imaAdInfo != null) {
imaAdState = IMA_AD_STATE_NONE; adPlaybackState = adPlaybackState.withSkippedAdGroup(imaAdInfo.adGroupIndex);
if (DEBUG) {
Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd");
}
}
if (adGroupIndex != C.INDEX_UNSET) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(adGroupIndex);
adGroupIndex = C.INDEX_UNSET;
updateAdPlaybackState(); updateAdPlaybackState();
} }
} }
@ -1257,23 +1257,40 @@ public final class ImaAdsLoader
private void stopAdInternal() { private void stopAdInternal() {
imaAdState = IMA_AD_STATE_NONE; imaAdState = IMA_AD_STATE_NONE;
int adIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); stopUpdatingAdProgress();
// TODO: Handle the skipped event so the ad can be marked as skipped rather than played. // TODO: Handle the skipped event so the ad can be marked as skipped rather than played.
Assertions.checkNotNull(imaAdInfo);
int adGroupIndex = imaAdInfo.adGroupIndex;
int adIndexInAdGroup = imaAdInfo.adIndexInAdGroup;
adPlaybackState = adPlaybackState =
adPlaybackState.withPlayedAd(adGroupIndex, adIndexInAdGroup).withAdResumePositionUs(0); adPlaybackState.withPlayedAd(adGroupIndex, adIndexInAdGroup).withAdResumePositionUs(0);
updateAdPlaybackState(); updateAdPlaybackState();
if (!playingAd) { if (!playingAd) {
adGroupIndex = C.INDEX_UNSET; imaAdMediaInfo = null;
imaAdInfo = null;
} }
} }
private void handleAdGroupLoadError(Exception error) { private void handleAdGroupLoadError(Exception error) {
int adGroupIndex = if (player == null) {
this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex;
if (adGroupIndex == C.INDEX_UNSET) {
// Drop the error, as we don't know which ad group it relates to.
return; return;
} }
// TODO: Once IMA signals which ad group failed to load, clean up this code.
long playerPositionMs = player.getContentPosition();
int adGroupIndex =
adPlaybackState.getAdGroupIndexForPositionUs(
C.msToUs(playerPositionMs), C.msToUs(contentDurationMs));
if (adGroupIndex == C.INDEX_UNSET) {
adGroupIndex =
adPlaybackState.getAdGroupIndexAfterPositionUs(
C.msToUs(playerPositionMs), C.msToUs(contentDurationMs));
if (adGroupIndex == C.INDEX_UNSET) {
// The error doesn't seem to relate to any ad group so give up handling it.
return;
}
}
AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];
if (adGroup.count == C.LENGTH_UNSET) { if (adGroup.count == C.LENGTH_UNSET) {
adPlaybackState = adPlaybackState =
@ -1313,19 +1330,20 @@ public final class ImaAdsLoader
if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) {
fakeContentProgressOffsetMs = contentDurationMs; fakeContentProgressOffsetMs = contentDurationMs;
} }
shouldNotifyAdPrepareError = true; pendingAdPrepareErrorAdInfo = new AdInfo(adGroupIndex, adIndexInAdGroup);
} else { } else {
AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo);
// We're already playing an ad. // We're already playing an ad.
if (adIndexInAdGroup > playingAdIndexInAdGroup) { if (adIndexInAdGroup > playingAdIndexInAdGroup) {
// Mark the playing ad as ended so we can notify the error on the next ad and remove it, // 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). // which means that the ad after will load (if any).
for (int i = 0; i < adCallbacks.size(); i++) { for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onEnded(); adCallbacks.get(i).onEnded(adMediaInfo);
} }
} }
playingAdIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); playingAdIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay();
for (int i = 0; i < adCallbacks.size(); i++) { for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onError(); adCallbacks.get(i).onError(Assertions.checkNotNull(adMediaInfo));
} }
} }
adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, adIndexInAdGroup); adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, adIndexInAdGroup);
@ -1343,11 +1361,6 @@ public final class ImaAdsLoader
Log.d(TAG, "adsLoader.contentComplete"); Log.d(TAG, "adsLoader.contentComplete");
} }
sentContentComplete = true; sentContentComplete = true;
// After sending content complete IMA will not poll the content position, so set the expected
// ad group index.
expectedAdGroupIndex =
adPlaybackState.getAdGroupIndexForPositionUs(
C.msToUs(contentDurationMs), C.msToUs(contentDurationMs));
} }
} }
@ -1358,21 +1371,6 @@ public final class ImaAdsLoader
} }
} }
/**
* Returns the next ad index in the specified ad group to load, or {@link C#INDEX_UNSET} if all
* ads in the ad group have loaded.
*/
private int getAdIndexInAdGroupToLoad(int adGroupIndex) {
@AdState int[] states = adPlaybackState.adGroups[adGroupIndex].states;
int adIndexInAdGroup = 0;
// IMA loads ads in order.
while (adIndexInAdGroup < states.length
&& states[adIndexInAdGroup] != AdPlaybackState.AD_STATE_UNAVAILABLE) {
adIndexInAdGroup++;
}
return adIndexInAdGroup == states.length ? C.INDEX_UNSET : adIndexInAdGroup;
}
private void maybeNotifyPendingAdLoadError() { private void maybeNotifyPendingAdLoadError() {
if (pendingAdLoadError != null && eventListener != null) { if (pendingAdLoadError != null && eventListener != null) {
eventListener.onAdLoadError(pendingAdLoadError, getAdsDataSpec(adTagUri)); eventListener.onAdLoadError(pendingAdLoadError, getAdsDataSpec(adTagUri));
@ -1406,6 +1404,22 @@ public final class ImaAdsLoader
- timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs(); - timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs();
} }
private int getAdGroupIndex(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");
}
private static long[] getAdGroupTimesUs(List<Float> cuePoints) { private static long[] getAdGroupTimesUs(List<Float> cuePoints) {
if (cuePoints.isEmpty()) { if (cuePoints.isEmpty()) {
// If no cue points are specified, there is a preroll ad. // If no cue points are specified, there is a preroll ad.
@ -1435,6 +1449,12 @@ public final class ImaAdsLoader
|| adError.getErrorCode() == AdErrorCode.UNKNOWN_ERROR; || 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) { private static boolean hasMidrollAdGroups(long[] adGroupTimesUs) {
int count = adGroupTimesUs.length; int count = adGroupTimesUs.length;
if (count == 1) { if (count == 1) {
@ -1463,6 +1483,49 @@ public final class ImaAdsLoader
Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer); Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer);
} }
private String getAdMediaInfoString(AdMediaInfo adMediaInfo) {
@Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo);
return "AdMediaInfo[" + adMediaInfo.getUrl() + (adInfo != null ? ", " + adInfo : "") + "]";
}
// TODO: Consider moving this into AdPlaybackState.
private static final class AdInfo {
public final int adGroupIndex;
public final int adIndexInAdGroup;
public AdInfo(int adGroupIndex, int adIndexInAdGroup) {
this.adGroupIndex = adGroupIndex;
this.adIndexInAdGroup = adIndexInAdGroup;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AdInfo adInfo = (AdInfo) o;
if (adGroupIndex != adInfo.adGroupIndex) {
return false;
}
return adIndexInAdGroup == adInfo.adIndexInAdGroup;
}
@Override
public int hashCode() {
int result = adGroupIndex;
result = 31 * result + adIndexInAdGroup;
return result;
}
@Override
public String toString() {
return "(" + adGroupIndex + ", " + adIndexInAdGroup + ')';
}
}
/** Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. */ /** Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. */
private static final class DefaultImaFactory implements ImaFactory { private static final class DefaultImaFactory implements ImaFactory {
@Override @Override

View File

@ -41,6 +41,7 @@ import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent;
import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; 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.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.Player; import com.google.android.exoplayer2.Player;
@ -85,6 +86,7 @@ public final class ImaAdsLoaderTest {
private static final long CONTENT_PERIOD_DURATION_US = private static final long CONTENT_PERIOD_DURATION_US =
CONTENT_TIMELINE.getPeriod(/* periodIndex= */ 0, new Period()).durationUs; CONTENT_TIMELINE.getPeriod(/* periodIndex= */ 0, new Period()).durationUs;
private static final Uri TEST_URI = Uri.EMPTY; private static final Uri TEST_URI = Uri.EMPTY;
private static final AdMediaInfo TEST_AD_MEDIA_INFO = new AdMediaInfo(TEST_URI.toString());
private static final long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND; private static final long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND;
private static final long[][] PREROLL_ADS_DURATIONS_US = new long[][] {{TEST_AD_DURATION_US}}; private static final long[][] PREROLL_ADS_DURATIONS_US = new long[][] {{TEST_AD_DURATION_US}};
private static final Float[] PREROLL_CUE_POINTS_SECONDS = new Float[] {0f}; private static final Float[] PREROLL_CUE_POINTS_SECONDS = new Float[] {0f};
@ -99,7 +101,7 @@ public final class ImaAdsLoaderTest {
@Mock private AdsManagerLoadedEvent mockAdsManagerLoadedEvent; @Mock private AdsManagerLoadedEvent mockAdsManagerLoadedEvent;
@Mock private com.google.ads.interactivemedia.v3.api.AdsLoader mockAdsLoader; @Mock private com.google.ads.interactivemedia.v3.api.AdsLoader mockAdsLoader;
@Mock private ImaFactory mockImaFactory; @Mock private ImaFactory mockImaFactory;
@Mock private AdPodInfo mockPrerollSingleAdAdPodInfo; @Mock private AdPodInfo mockAdPodInfo;
@Mock private Ad mockPrerollSingleAd; @Mock private Ad mockPrerollSingleAd;
private ViewGroup adViewGroup; private ViewGroup adViewGroup;
@ -195,12 +197,12 @@ public final class ImaAdsLoaderTest {
// SDK being proguarded. // SDK being proguarded.
imaAdsLoader.requestAds(adViewGroup); imaAdsLoader.requestAds(adViewGroup);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd)); imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd));
imaAdsLoader.loadAd(TEST_URI.toString()); imaAdsLoader.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd)); imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd));
imaAdsLoader.playAd(); imaAdsLoader.playAd(TEST_AD_MEDIA_INFO);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd)); imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd));
imaAdsLoader.pauseAd(); imaAdsLoader.pauseAd(TEST_AD_MEDIA_INFO);
imaAdsLoader.stopAd(); imaAdsLoader.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)); imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
@ -215,11 +217,11 @@ 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)); imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd));
imaAdsLoader.loadAd(TEST_URI.toString()); imaAdsLoader.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd)); imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd));
// Play the preroll ad. // Play the preroll ad.
imaAdsLoader.playAd(); imaAdsLoader.playAd(TEST_AD_MEDIA_INFO);
fakeExoPlayer.setPlayingAdPosition( fakeExoPlayer.setPlayingAdPosition(
/* adGroupIndex= */ 0, /* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0, /* adIndexInAdGroup= */ 0,
@ -233,7 +235,7 @@ public final class ImaAdsLoaderTest {
// Play the content. // Play the content.
fakeExoPlayer.setPlayingContentPosition(0); fakeExoPlayer.setPlayingContentPosition(0);
imaAdsLoader.stopAd(); imaAdsLoader.stopAd(TEST_AD_MEDIA_INFO);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null)); imaAdsLoader.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.
@ -313,11 +315,11 @@ public final class ImaAdsLoaderTest {
when(mockImaFactory.createAdsRequest()).thenReturn(mockAdsRequest); when(mockImaFactory.createAdsRequest()).thenReturn(mockAdsRequest);
when(mockImaFactory.createAdsLoader(any(), any(), any())).thenReturn(mockAdsLoader); when(mockImaFactory.createAdsLoader(any(), any(), any())).thenReturn(mockAdsLoader);
when(mockPrerollSingleAdAdPodInfo.getPodIndex()).thenReturn(0); when(mockAdPodInfo.getPodIndex()).thenReturn(0);
when(mockPrerollSingleAdAdPodInfo.getTotalAds()).thenReturn(1); when(mockAdPodInfo.getTotalAds()).thenReturn(1);
when(mockPrerollSingleAdAdPodInfo.getAdPosition()).thenReturn(1); when(mockAdPodInfo.getAdPosition()).thenReturn(1);
when(mockPrerollSingleAd.getAdPodInfo()).thenReturn(mockPrerollSingleAdAdPodInfo); when(mockPrerollSingleAd.getAdPodInfo()).thenReturn(mockAdPodInfo);
} }
private static AdEvent getAdEvent(AdEventType adEventType, @Nullable Ad ad) { private static AdEvent getAdEvent(AdEventType adEventType, @Nullable Ad ad) {