diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 274e4cc4b6..0a31580488 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -266,6 +266,10 @@ * Migrate to new 'friendly obstruction' IMA SDK APIs, and allow apps to register a purpose and detail reason for overlay views via `AdsLoader.AdViewProvider`. + * Add support for audio-only ads display containers by returning `null` + from `AdsLoader.AdViewProvider.getAdViewGroup`, and allow skipping + audio-only ads via `ImaAdsLoader.skipAd` + ([#7689](https://github.com/google/ExoPlayer/issues/7689)). * Add `ImaAdsLoader.Builder.setCompanionAdSlots` so it's possible to set companion ad slots without accessing the `AdDisplayContainer`. * Add missing notification of `VideoAdPlayerCallback.onLoaded`. 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 834b7e546c..81e21bc753 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 @@ -609,15 +609,21 @@ public final class ImaAdsLoader * called, so it is only necessary to call this method if you want to request ads before preparing * the player. * - * @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + * @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI, or {@code + * null} if playing audio-only ads. */ - public void requestAds(ViewGroup adViewGroup) { + public void requestAds(@Nullable ViewGroup adViewGroup) { if (hasAdPlaybackState || adsManager != null || pendingAdRequestContext != null) { // Ads have already been requested. return; } - adDisplayContainer = - imaFactory.createAdDisplayContainer(adViewGroup, /* player= */ componentListener); + if (adViewGroup != null) { + adDisplayContainer = + imaFactory.createAdDisplayContainer(adViewGroup, /* player= */ componentListener); + } else { + adDisplayContainer = + imaFactory.createAudioAdDisplayContainer(context, /* player= */ componentListener); + } if (companionAdSlots != null) { adDisplayContainer.setCompanionSlots(companionAdSlots); } @@ -639,6 +645,19 @@ public final class ImaAdsLoader adsLoader.requestAds(request); } + /** + * Skips the current ad. + * + *

This method is intended for apps that play audio-only ads and so need to provide their own + * UI for users to skip skippable ads. Apps showing video ads should not call this method, as the + * IMA SDK provides the UI to skip ads in the ad view group passed via {@link AdViewProvider}. + */ + public void skipAd() { + if (adsManager != null) { + adsManager.skip(); + } + } + // com.google.android.exoplayer2.source.ads.AdsLoader implementation. @Override @@ -1582,6 +1601,8 @@ public final class ImaAdsLoader * non-linear ads, and slots for companion ads. */ AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player); + /** Creates an {@link AdDisplayContainer} to hold the player for audio ads. */ + AdDisplayContainer createAudioAdDisplayContainer(Context context, VideoAdPlayer player); /** * Creates a {@link FriendlyObstruction} to describe an obstruction considered "friendly" for * viewability measurement purposes. @@ -1817,6 +1838,11 @@ public final class ImaAdsLoader return ImaSdkFactory.createAdDisplayContainer(container, player); } + @Override + public AdDisplayContainer createAudioAdDisplayContainer(Context context, VideoAdPlayer player) { + return ImaSdkFactory.createAudioAdDisplayContainer(context, player); + } + // The reasonDetail parameter to createFriendlyObstruction is annotated @Nullable but the // annotation is not kept in the obfuscated dependency. @SuppressWarnings("nullness:argument.type.incompatible") 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 23b7110103..ee0ea41e47 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.ima; +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyDouble; @@ -31,7 +32,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import androidx.annotation.Nullable; -import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.ads.interactivemedia.v3.api.Ad; import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; @@ -117,6 +117,7 @@ public final class ImaAdsLoaderTest { private ViewGroup adViewGroup; private AdsLoader.AdViewProvider adViewProvider; + private AdsLoader.AdViewProvider audioAdsAdViewProvider; private AdEvent.AdEventListener adEventListener; private ContentProgressProvider contentProgressProvider; private VideoAdPlayer videoAdPlayer; @@ -127,8 +128,8 @@ public final class ImaAdsLoaderTest { @Before public void setUp() { setupMocks(); - adViewGroup = new FrameLayout(ApplicationProvider.getApplicationContext()); - View adOverlayView = new View(ApplicationProvider.getApplicationContext()); + adViewGroup = new FrameLayout(getApplicationContext()); + View adOverlayView = new View(getApplicationContext()); adViewProvider = new AdsLoader.AdViewProvider() { @Override @@ -142,6 +143,18 @@ public final class ImaAdsLoaderTest { new AdsLoader.OverlayInfo(adOverlayView, AdsLoader.OverlayInfo.PURPOSE_CLOSE_AD)); } }; + audioAdsAdViewProvider = + new AdsLoader.AdViewProvider() { + @Override + public ViewGroup getAdViewGroup() { + return null; + } + + @Override + public ImmutableList getAdOverlayInfos() { + return ImmutableList.of(); + } + }; } @After @@ -165,9 +178,21 @@ public final class ImaAdsLoaderTest { imaAdsLoader.start(adsLoaderListener, adViewProvider); verify(mockImaFactory, atLeastOnce()).createAdDisplayContainer(adViewGroup, videoAdPlayer); + verify(mockImaFactory, never()).createAudioAdDisplayContainer(any(), any()); verify(mockAdDisplayContainer).registerFriendlyObstruction(mockFriendlyObstruction); } + @Test + public void startForAudioOnlyAds_createsAudioOnlyAdDisplayContainer() { + setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS); + imaAdsLoader.start(adsLoaderListener, audioAdsAdViewProvider); + + verify(mockImaFactory, atLeastOnce()) + .createAudioAdDisplayContainer(getApplicationContext(), videoAdPlayer); + verify(mockImaFactory, never()).createAdDisplayContainer(any(), any()); + verify(mockAdDisplayContainer, never()).registerFriendlyObstruction(any()); + } + @Test public void start_withPlaceholderContent_initializedAdsLoader() { Timeline placeholderTimeline = new PlaceholderTimeline(MediaItem.fromUri(Uri.EMPTY)); @@ -470,7 +495,7 @@ public final class ImaAdsLoaderTest { setupPlayback( CONTENT_TIMELINE, cuePoints, - new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext()) + new ImaAdsLoader.Builder(getApplicationContext()) .setPlayAdBeforeStartPosition(false) .setImaFactory(mockImaFactory) .setImaSdkSettings(mockImaSdkSettings) @@ -502,7 +527,7 @@ public final class ImaAdsLoaderTest { setupPlayback( CONTENT_TIMELINE, cuePoints, - new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext()) + new ImaAdsLoader.Builder(getApplicationContext()) .setPlayAdBeforeStartPosition(false) .setImaFactory(mockImaFactory) .setImaSdkSettings(mockImaSdkSettings) @@ -534,7 +559,7 @@ public final class ImaAdsLoaderTest { setupPlayback( CONTENT_TIMELINE, cuePoints, - new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext()) + new ImaAdsLoader.Builder(getApplicationContext()) .setPlayAdBeforeStartPosition(false) .setImaFactory(mockImaFactory) .setImaSdkSettings(mockImaSdkSettings) @@ -570,7 +595,7 @@ public final class ImaAdsLoaderTest { setupPlayback( CONTENT_TIMELINE, cuePoints, - new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext()) + new ImaAdsLoader.Builder(getApplicationContext()) .setPlayAdBeforeStartPosition(false) .setImaFactory(mockImaFactory) .setImaSdkSettings(mockImaSdkSettings) @@ -609,7 +634,7 @@ public final class ImaAdsLoaderTest { setupPlayback( CONTENT_TIMELINE, cuePoints, - new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext()) + new ImaAdsLoader.Builder(getApplicationContext()) .setPlayAdBeforeStartPosition(false) .setImaFactory(mockImaFactory) .setImaSdkSettings(mockImaSdkSettings) @@ -696,7 +721,7 @@ public final class ImaAdsLoaderTest { setupPlayback( contentTimeline, cuePoints, - new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext()) + new ImaAdsLoader.Builder(getApplicationContext()) .setImaFactory(mockImaFactory) .setImaSdkSettings(mockImaSdkSettings) .buildForAdTag(TEST_URI)); @@ -765,6 +790,13 @@ public final class ImaAdsLoaderTest { }) .when(mockImaFactory) .createAdDisplayContainer(any(), any()); + doAnswer( + invocation -> { + videoAdPlayer = invocation.getArgument(1); + return mockAdDisplayContainer; + }) + .when(mockImaFactory) + .createAudioAdDisplayContainer(any(), any()); when(mockImaFactory.createAdsRenderingSettings()).thenReturn(mockAdsRenderingSettings); when(mockImaFactory.createAdsRequest()).thenReturn(mockAdsRequest); when(mockImaFactory.createAdsLoader(any(), any(), any())).thenReturn(mockAdsLoader); 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 093ca1d5c4..f1c17c1093 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 @@ -80,10 +80,12 @@ public interface AdsLoader { interface AdViewProvider { /** - * 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. + * Returns the {@link ViewGroup} on top of the player that will show any ad UI, or {@code null} + * if playing audio-only ads. Any views on top of the returned view group must be described by + * {@link OverlayInfo OverlayInfos} returned by {@link #getAdOverlayInfos()}, for accurate + * viewability measurement. */ + @Nullable ViewGroup getAdViewGroup(); /** @deprecated Use {@link #getAdOverlayInfos()} instead. */