diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c975d64f89..47babde849 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -239,9 +239,13 @@ `androidx.media2.session.MediaSession`. * Cast extension: Implement playlist API and deprecate the old queue manipulation API. -* IMA extension: Upgrade to IMA SDK 3.19.4, bringing in a fix for setting the - media load timeout - ([#7170](https://github.com/google/ExoPlayer/issues/7170)). +* IMA extension: + * Upgrade to IMA SDK 3.19.4, bringing in a fix for setting the + media load timeout + ([#7170](https://github.com/google/ExoPlayer/issues/7170)). + * Migrate to new 'friendly obstruction' IMA SDK APIs, and allow apps to + register a purpose and detail reason for overlay views via + `AdsLoader.AdViewProvider`. * Demo app: Retain previous position in list of samples. * Add Guava dependency. diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 9e4eddf073..33985b2639 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0' + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index b564d345b3..a581c6825b 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -40,11 +40,14 @@ import com.google.ads.interactivemedia.v3.api.AdEvent; import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener; import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType; import com.google.ads.interactivemedia.v3.api.AdPodInfo; +import com.google.ads.interactivemedia.v3.api.AdsLoader; import com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener; import com.google.ads.interactivemedia.v3.api.AdsManager; import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; import com.google.ads.interactivemedia.v3.api.AdsRequest; +import com.google.ads.interactivemedia.v3.api.FriendlyObstruction; +import com.google.ads.interactivemedia.v3.api.FriendlyObstructionPurpose; import com.google.ads.interactivemedia.v3.api.ImaSdkFactory; import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; import com.google.ads.interactivemedia.v3.api.UiElement; @@ -58,7 +61,6 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; -import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DataSpec; @@ -77,19 +79,25 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** - * {@link AdsLoader} using the IMA SDK. All methods must be called on the main thread. + * {@link com.google.android.exoplayer2.source.ads.AdsLoader} using the IMA SDK. All methods must be + * called on the main thread. * *
The player instance that will play the loaded ads must be set before playback using {@link * #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling * {@link #release()}. * - *
The IMA SDK can take into account video control overlay views when calculating ad viewability. - * For more details see {@link AdDisplayContainer#registerVideoControlsOverlay(View)} and {@link - * AdViewProvider#getAdOverlayViews()}. + *
The IMA SDK can report obstructions to the ad view for accurate viewability measurement. This
+ * means that any overlay views that obstruct the ad overlay but are essential for playback need to
+ * be registered via the {@link AdViewProvider} passed to the {@link
+ * com.google.android.exoplayer2.source.ads.AdsMediaSource}. See the
+ * IMA SDK Open Measurement documentation for more information.
*/
-public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
+public final class ImaAdsLoader
+ implements Player.EventListener, com.google.android.exoplayer2.source.ads.AdsLoader {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.ima");
@@ -329,7 +337,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
* 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
+ * @see VideoAdPlayer.VideoAdPlayerCallback
*/
private static final int AD_PROGRESS_UPDATE_INTERVAL_MS = 100;
@@ -370,6 +378,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
*/
private static final int IMA_AD_STATE_PAUSED = 2;
+ private final Context context;
@Nullable private final Uri adTagUri;
@Nullable private final String adsResponse;
private final long adPreloadTimeoutMs;
@@ -381,15 +390,16 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
@Nullable private final Set Note: any video controls overlays registered via {@link
- * AdDisplayContainer#registerVideoControlsOverlay(View)} will be unregistered automatically when
- * the media source detaches from this instance. It is therefore necessary to re-register views
- * each time the ads loader is reused. Alternatively, provide overlay views via the {@link
- * AdsLoader.AdViewProvider} when creating the media source to benefit from automatic
- * registration.
+ * AdDisplayContainer#registerFriendlyObstruction(FriendlyObstruction)} will be unregistered
+ * automatically when the media source detaches from this instance. It is therefore necessary to
+ * re-register views each time the ads loader is reused. Alternatively, provide overlay views via
+ * the {@link com.google.android.exoplayer2.source.ads.AdsLoader.AdViewProvider} when creating the
+ * media source to benefit from automatic registration.
*/
+ @Nullable
public AdDisplayContainer getAdDisplayContainer() {
return adDisplayContainer;
}
@@ -588,7 +593,11 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
// Ads have already been requested.
return;
}
- adDisplayContainer.setAdContainer(adViewGroup);
+ adDisplayContainer =
+ imaFactory.createAdDisplayContainer(adViewGroup, /* player= */ componentListener);
+ adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer);
+ adsLoader.addAdErrorListener(componentListener);
+ adsLoader.addAdsLoadedListener(componentListener);
AdsRequest request = imaFactory.createAdsRequest();
if (adTagUri != null) {
request.setAdTagUrl(adTagUri.toString());
@@ -604,7 +613,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
adsLoader.requestAds(request);
}
- // AdsLoader implementation.
+ // com.google.android.exoplayer2.source.ads.AdsLoader implementation.
@Override
public void setPlayer(@Nullable Player player) {
@@ -650,12 +659,6 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
lastVolumePercent = 0;
lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
- ViewGroup adViewGroup = adViewProvider.getAdViewGroup();
- adDisplayContainer.setAdContainer(adViewGroup);
- View[] adOverlayViews = adViewProvider.getAdOverlayViews();
- for (View view : adOverlayViews) {
- adDisplayContainer.registerVideoControlsOverlay(view);
- }
maybeNotifyPendingAdLoadError();
if (hasAdPlaybackState) {
// Pass the ad playback state to the player, and resume ads if necessary.
@@ -668,7 +671,16 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
updateAdPlaybackState();
} else {
// Ads haven't loaded yet, so request them.
- requestAds(adViewGroup);
+ requestAds(adViewProvider.getAdViewGroup());
+ }
+ if (adDisplayContainer != null) {
+ for (OverlayInfo overlayInfo : adViewProvider.getAdOverlayInfos()) {
+ adDisplayContainer.registerFriendlyObstruction(
+ imaFactory.createFriendlyObstruction(
+ overlayInfo.view,
+ getFriendlyObstructionPurpose(overlayInfo.purpose),
+ overlayInfo.reasonDetail));
+ }
}
}
@@ -687,7 +699,9 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
lastVolumePercent = getPlayerVolumePercent();
lastAdProgress = getAdVideoProgressUpdate();
lastContentProgress = getContentVideoProgressUpdate();
- adDisplayContainer.unregisterAllVideoControlsOverlays();
+ if (adDisplayContainer != null) {
+ adDisplayContainer.unregisterAllFriendlyObstructions();
+ }
player.removeListener(this);
this.player = null;
eventListener = null;
@@ -697,8 +711,10 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
public void release() {
pendingAdRequestContext = null;
destroyAdsManager();
- adsLoader.removeAdsLoadedListener(componentListener);
- adsLoader.removeAdErrorListener(componentListener);
+ if (adsLoader != null) {
+ adsLoader.removeAdsLoadedListener(componentListener);
+ adsLoader.removeAdErrorListener(componentListener);
+ }
imaPausedContent = false;
imaAdState = IMA_AD_STATE_NONE;
imaAdMediaInfo = null;
@@ -1356,7 +1372,9 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
}
private void sendContentComplete() {
- adsLoader.contentComplete();
+ for (int i = 0; i < adCallbacks.size(); i++) {
+ adCallbacks.get(i).onContentComplete();
+ }
sentContentComplete = true;
if (DEBUG) {
Log.d(TAG, "adsLoader.contentComplete");
@@ -1442,6 +1460,21 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
return "AdMediaInfo[" + adMediaInfo.getUrl() + (adInfo != null ? ", " + adInfo : "") + "]";
}
+ private static FriendlyObstructionPurpose getFriendlyObstructionPurpose(
+ @OverlayInfo.Purpose int purpose) {
+ switch (purpose) {
+ case OverlayInfo.PURPOSE_CONTROLS:
+ return FriendlyObstructionPurpose.VIDEO_CONTROLS;
+ case OverlayInfo.PURPOSE_CLOSE_AD:
+ return FriendlyObstructionPurpose.CLOSE_AD;
+ case OverlayInfo.PURPOSE_NOT_VISIBLE:
+ return FriendlyObstructionPurpose.NOT_VISIBLE;
+ case OverlayInfo.PURPOSE_OTHER:
+ default:
+ return FriendlyObstructionPurpose.OTHER;
+ }
+ }
+
private static DataSpec getAdsDataSpec(@Nullable Uri adTagUri) {
return new DataSpec(adTagUri != null ? adTagUri : Uri.EMPTY);
}
@@ -1495,16 +1528,30 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
/** Factory for objects provided by the IMA SDK. */
@VisibleForTesting
/* package */ interface ImaFactory {
- /** @see ImaSdkSettings */
+ /** Creates {@link ImaSdkSettings} for configuring the IMA SDK. */
ImaSdkSettings createImaSdkSettings();
- /** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRenderingSettings() */
+ /**
+ * Creates {@link AdsRenderingSettings} for giving the {@link AdsManager} parameters that
+ * control rendering of ads.
+ */
AdsRenderingSettings createAdsRenderingSettings();
- /** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdDisplayContainer() */
- AdDisplayContainer createAdDisplayContainer();
- /** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRequest() */
+ /**
+ * Creates an {@link AdDisplayContainer} to hold the player for video ads, a container for
+ * non-linear ads, and slots for companion ads.
+ */
+ AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player);
+ /**
+ * Creates a {@link FriendlyObstruction} to describe an obstruction considered "friendly" for
+ * viewability measurement purposes.
+ */
+ FriendlyObstruction createFriendlyObstruction(
+ View view,
+ FriendlyObstructionPurpose friendlyObstructionPurpose,
+ @Nullable String reasonDetail);
+ /** Creates an {@link AdsRequest} to contain the data used to request ads. */
AdsRequest createAdsRequest();
- /** @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings, AdDisplayContainer) */
- com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
+ /** Creates an {@link AdsLoader} for requesting ads using the specified settings. */
+ AdsLoader createAdsLoader(
Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer);
}
@@ -1515,7 +1562,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
AdErrorListener,
VideoAdPlayer {
- // com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener implementation.
+ // AdsLoader.AdsLoadedListener implementation.
@Override
public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) {
@@ -1724,8 +1771,20 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
}
@Override
- public AdDisplayContainer createAdDisplayContainer() {
- return ImaSdkFactory.getInstance().createAdDisplayContainer();
+ public AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player) {
+ return ImaSdkFactory.createAdDisplayContainer(container, player);
+ }
+
+ // The reasonDetail parameter to createFriendlyObstruction is annotated @Nullable but the
+ // annotation is not kept in the obfuscated dependency.
+ @SuppressWarnings("nullness:argument.type.incompatible")
+ @Override
+ public FriendlyObstruction createFriendlyObstruction(
+ View view,
+ FriendlyObstructionPurpose friendlyObstructionPurpose,
+ @Nullable String reasonDetail) {
+ return ImaSdkFactory.getInstance()
+ .createFriendlyObstruction(view, friendlyObstructionPurpose, reasonDetail);
}
@Override
@@ -1734,7 +1793,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
}
@Override
- public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
+ public AdsLoader createAdsLoader(
Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) {
return ImaSdkFactory.getInstance()
.createAdsLoader(context, imaSdkSettings, adDisplayContainer);
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
index 5a59690b44..e313535e9a 100644
--- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
@@ -42,6 +42,7 @@ import com.google.ads.interactivemedia.v3.api.AdsManager;
import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent;
import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
import com.google.ads.interactivemedia.v3.api.AdsRequest;
+import com.google.ads.interactivemedia.v3.api.FriendlyObstruction;
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.ContentProgressProvider;
@@ -108,6 +109,7 @@ public final class ImaAdsLoaderTest {
@Mock private AdsRequest mockAdsRequest;
@Mock private AdsManagerLoadedEvent mockAdsManagerLoadedEvent;
@Mock private com.google.ads.interactivemedia.v3.api.AdsLoader mockAdsLoader;
+ @Mock private FriendlyObstruction mockFriendlyObstruction;
@Mock private ImaFactory mockImaFactory;
@Mock private AdPodInfo mockAdPodInfo;
@Mock private Ad mockPrerollSingleAd;
@@ -162,8 +164,8 @@ public final class ImaAdsLoaderTest {
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
- verify(mockAdDisplayContainer, atLeastOnce()).setAdContainer(adViewGroup);
- verify(mockAdDisplayContainer, atLeastOnce()).registerVideoControlsOverlay(adOverlayView);
+ verify(mockImaFactory, atLeastOnce()).createAdDisplayContainer(adViewGroup, videoAdPlayer);
+ verify(mockAdDisplayContainer).registerFriendlyObstruction(mockFriendlyObstruction);
}
@Test
@@ -637,8 +639,8 @@ public final class ImaAdsLoaderTest {
imaAdsLoader.stop();
InOrder inOrder = inOrder(mockAdDisplayContainer);
- inOrder.verify(mockAdDisplayContainer).registerVideoControlsOverlay(adOverlayView);
- inOrder.verify(mockAdDisplayContainer).unregisterAllVideoControlsOverlays();
+ inOrder.verify(mockAdDisplayContainer).registerFriendlyObstruction(mockFriendlyObstruction);
+ inOrder.verify(mockAdDisplayContainer).unregisterAllFriendlyObstructions();
}
@Test
@@ -758,16 +760,16 @@ public final class ImaAdsLoaderTest {
doAnswer(
invocation -> {
- videoAdPlayer = invocation.getArgument(0);
- return null;
+ videoAdPlayer = invocation.getArgument(1);
+ return mockAdDisplayContainer;
})
- .when(mockAdDisplayContainer)
- .setPlayer(any());
-
- when(mockImaFactory.createAdDisplayContainer()).thenReturn(mockAdDisplayContainer);
+ .when(mockImaFactory)
+ .createAdDisplayContainer(any(), any());
when(mockImaFactory.createAdsRenderingSettings()).thenReturn(mockAdsRenderingSettings);
when(mockImaFactory.createAdsRequest()).thenReturn(mockAdsRequest);
when(mockImaFactory.createAdsLoader(any(), any(), any())).thenReturn(mockAdsLoader);
+ when(mockImaFactory.createFriendlyObstruction(any(), any(), any()))
+ .thenReturn(mockFriendlyObstruction);
when(mockAdPodInfo.getPodIndex()).thenReturn(0);
when(mockAdPodInfo.getTotalAds()).thenReturn(1);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java
index 11947218a3..df50a31d74 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java
@@ -17,12 +17,18 @@ package com.google.android.exoplayer2.source.ads;
import android.view.View;
import android.view.ViewGroup;
+import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.common.collect.ImmutableList;
import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
/**
* Interface for loaders of ads, which can be used with {@link AdsMediaSource}.
@@ -70,23 +76,90 @@ public interface AdsLoader {
default void onAdTapped() {}
}
- /** Provides views for the ad UI. */
+ /** Provides information about views for the ad playback UI. */
interface AdViewProvider {
- /** Returns the {@link ViewGroup} on top of the player that will show any ad UI. */
+ /**
+ * Returns the {@link ViewGroup} on top of the player that will show any ad UI. Any views on top
+ * of the returned view group must be described by {@link OverlayInfo OverlayInfos} returned by
+ * {@link #getAdOverlayInfos()}, for accurate viewability measurement.
+ */
ViewGroup getAdViewGroup();
+ /** @deprecated Use {@link #getAdOverlayInfos()} instead. */
+ @Deprecated
+ default View[] getAdOverlayViews() {
+ return new View[0];
+ }
+
/**
- * Returns an array of views that are shown on top of the ad view group, but that are essential
- * for controlling playback and should be excluded from ad viewability measurements by the
- * {@link AdsLoader} (if it supports this).
+ * Returns a list of {@link OverlayInfo} instances describing views that are on top of the ad
+ * view group, but that are essential for controlling playback and should be excluded from ad
+ * viewability measurements by the {@link AdsLoader} (if it supports this).
*
* Each view must be either a fully transparent overlay (for capturing touch events), or a
* small piece of transient UI that is essential to the user experience of playback (such as a
* button to pause/resume playback or a transient full-screen or cast button). For more
* information see the documentation for your ads loader.
*/
- View[] getAdOverlayViews();
+ @SuppressWarnings("deprecation")
+ default List