From 56944caa2167b045f361e6fb86d40570c95e5cc0 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 11 Feb 2019 14:07:13 +0000 Subject: [PATCH] Allow registration of control overlays for ad viewability Switch from passing an ad UI ViewGroup to passing an object that can also provide information about controls overlays. Also switch to using a dedicated overlay for ads instead of the overlay frame layout, which apps have easy access to. PiperOrigin-RevId: 233393500 --- RELEASENOTES.md | 4 ++ .../exoplayer2/imademo/PlayerManager.java | 5 +- .../exoplayer2/demo/PlayerActivity.java | 9 +--- .../exoplayer2/ext/ima/ImaAdsLoader.java | 39 ++++++++++++--- .../exoplayer2/ext/ima/FakePlayer.java | 16 ++++++ .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 49 +++++++++++++++---- .../exoplayer2/source/ads/AdsLoader.java | 38 ++++++++++---- .../exoplayer2/source/ads/AdsMediaSource.java | 17 +++---- .../android/exoplayer2/ui/PlayerView.java | 20 +++++++- 9 files changed, 150 insertions(+), 47 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index df0978fa75..87b00b0e11 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -50,6 +50,10 @@ * Remove deprecated `AdsMediaSource` constructors. Listen for media source events using `AdsMediaSource.addEventListener`, and ad interaction events by adding a listener when building `ImaAdsLoader`. + * Allow apps to register playback-related obstructing views that are on top of + their ad display containers via `AdsLoader.AdViewProvider`. `PlayerView` + implements this interface and will register its control view. This makes it + possible for ad loading SDKs to calculate ad viewability accurately. * Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`. * VP9 extension: Remove RGB output mode and libyuv dependency, and switch to surface YUV output as the default. Remove constructor parameters `scaleToFit` diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java index 97c3299a4a..05c804c7a8 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -64,10 +64,7 @@ import com.google.android.exoplayer2.util.Util; // Compose the content media source into a new AdsMediaSource with both ads and content. MediaSource mediaSourceWithAds = new AdsMediaSource( - contentMediaSource, - /* adMediaSourceFactory= */ this, - adsLoader, - playerView.getOverlayFrameLayout()); + contentMediaSource, /* adMediaSourceFactory= */ this, adsLoader, playerView); // Prepare the player with the source. player.seekTo(contentPosition); diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 5284183a90..12fc5eb06f 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -26,9 +26,7 @@ import android.util.Pair; import android.view.KeyEvent; import android.view.View; import android.view.View.OnClickListener; -import android.view.ViewGroup; import android.widget.Button; -import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; @@ -146,7 +144,6 @@ public class PlayerActivity extends Activity private AdsLoader adsLoader; private Uri loadedAdTagUri; - private ViewGroup adUiViewGroup; // Activity lifecycle @@ -464,7 +461,6 @@ public class PlayerActivity extends Activity return buildMediaSource(uri, null); } - @SuppressWarnings("unchecked") private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { @ContentType int type = Util.inferContentType(uri, overrideExtension); List offlineStreamKeys = getOfflineStreamKeys(uri); @@ -584,9 +580,6 @@ public class PlayerActivity extends Activity .getConstructor(android.content.Context.class, android.net.Uri.class); // LINT.ThenChange(../../../../../../../../proguard-rules.txt) adsLoader = loaderConstructor.newInstance(this, adTagUri); - adUiViewGroup = new FrameLayout(this); - // The demo app has a non-null overlay frame layout. - playerView.getOverlayFrameLayout().addView(adUiViewGroup); } adsLoader.setPlayer(player); AdsMediaSource.MediaSourceFactory adMediaSourceFactory = @@ -601,7 +594,7 @@ public class PlayerActivity extends Activity return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER}; } }; - return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup); + return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, playerView); } catch (ClassNotFoundException e) { // IMA extension not loaded. return null; 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 4bdec23804..3bfb9aab7b 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 @@ -22,6 +22,7 @@ import android.os.SystemClock; import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; +import android.view.View; import android.view.ViewGroup; import com.google.ads.interactivemedia.v3.api.Ad; import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; @@ -79,6 +80,10 @@ import java.util.Set; *

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()}. */ public final class ImaAdsLoader implements Player.EventListener, @@ -488,13 +493,29 @@ public final class ImaAdsLoader return adsLoader; } + /** + * Returns the {@link AdDisplayContainer} used by this loader. + * + *

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. + */ + public AdDisplayContainer getAdDisplayContainer() { + return adDisplayContainer; + } + /** * Sets the slots for displaying companion ads. Individual slots can be created using {@link * ImaSdkFactory#createCompanionAdSlot()}. * * @param companionSlots Slots for displaying companion ads. * @see AdDisplayContainer#setCompanionSlots(Collection) + * @deprecated Use {@code getAdDisplayContainer().setCompanionSlots(...)}. */ + @Deprecated public void setCompanionSlots(Collection companionSlots) { adDisplayContainer.setCompanionSlots(companionSlots); } @@ -506,14 +527,14 @@ 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 adUiViewGroup 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. */ - public void requestAds(ViewGroup adUiViewGroup) { + public void requestAds(ViewGroup adViewGroup) { if (adPlaybackState != null || adsManager != null || pendingAdRequestContext != null) { // Ads have already been requested. return; } - adDisplayContainer.setAdContainer(adUiViewGroup); + adDisplayContainer.setAdContainer(adViewGroup); pendingAdRequestContext = new Object(); AdsRequest request = imaFactory.createAdsRequest(); if (adTagUri != null) { @@ -563,7 +584,7 @@ public final class ImaAdsLoader } @Override - public void start(EventListener eventListener, ViewGroup adUiViewGroup) { + public void start(EventListener eventListener, AdViewProvider adViewProvider) { Assertions.checkNotNull( nextPlayer, "Set player using adsLoader.setPlayer before preparing the player."); player = nextPlayer; @@ -571,7 +592,12 @@ public final class ImaAdsLoader lastVolumePercentage = 0; lastAdProgress = null; lastContentProgress = null; - adDisplayContainer.setAdContainer(adUiViewGroup); + ViewGroup adViewGroup = adViewProvider.getAdViewGroup(); + adDisplayContainer.setAdContainer(adViewGroup); + View[] adOverlayViews = adViewProvider.getAdOverlayViews(); + for (View view : adOverlayViews) { + adDisplayContainer.registerVideoControlsOverlay(view); + } player.addListener(this); maybeNotifyPendingAdLoadError(); if (adPlaybackState != null) { @@ -585,7 +611,7 @@ public final class ImaAdsLoader startAdPlayback(); } else { // Ads haven't loaded yet, so request them. - requestAds(adUiViewGroup); + requestAds(adViewGroup); } } @@ -600,6 +626,7 @@ public final class ImaAdsLoader lastVolumePercentage = getVolume(); lastAdProgress = getAdProgress(); lastContentProgress = getContentProgress(); + adDisplayContainer.unregisterAllVideoControlsOverlays(); player.removeListener(this); player = null; eventListener = null; diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java index b8024d6534..d20ccbd728 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.testutil.StubExoPlayer; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import java.util.ArrayList; /** A fake player for testing content/ad playback. */ @@ -109,6 +110,11 @@ import java.util.ArrayList; // ExoPlayer methods. Other methods are unsupported. + @Override + public AudioComponent getAudioComponent() { + return null; + } + @Override public Looper getApplicationLooper() { return Looper.getMainLooper(); @@ -134,6 +140,16 @@ import java.util.ArrayList; return playWhenReady; } + @Override + public int getRendererCount() { + return 0; + } + + @Override + public TrackSelectionArray getCurrentTrackSelections() { + return new TrackSelectionArray(); + } + @Override public Timeline getCurrentTimeline() { return timeline; 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 0253e7db13..dabae2de4b 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 @@ -17,11 +17,13 @@ package com.google.android.exoplayer2.ext.ima; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.net.Uri; import android.support.annotation.Nullable; +import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import com.google.ads.interactivemedia.v3.api.Ad; @@ -49,6 +51,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; @@ -73,7 +76,9 @@ public class ImaAdsLoaderTest { private @Mock AdDisplayContainer adDisplayContainer; private @Mock AdsManager adsManager; private SingletonImaFactory testImaFactory; - private ViewGroup adUiViewGroup; + private ViewGroup adViewGroup; + private View adOverlayView; + private AdsLoader.AdViewProvider adViewProvider; private TestAdsLoaderListener adsLoaderListener; private FakePlayer fakeExoPlayer; private ImaAdsLoader imaAdsLoader; @@ -90,7 +95,20 @@ public class ImaAdsLoaderTest { adDisplayContainer, fakeAdsRequest, fakeAdsLoader); - adUiViewGroup = new FrameLayout(RuntimeEnvironment.application); + adViewGroup = new FrameLayout(RuntimeEnvironment.application); + adOverlayView = new View(RuntimeEnvironment.application); + adViewProvider = + new AdsLoader.AdViewProvider() { + @Override + public ViewGroup getAdViewGroup() { + return adViewGroup; + } + + @Override + public View[] getAdOverlayViews() { + return new View[] {adOverlayView}; + } + }; } @After @@ -111,15 +129,16 @@ public class ImaAdsLoaderTest { @Test public void testStart_setsAdUiViewGroup() { setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); - imaAdsLoader.start(adsLoaderListener, adUiViewGroup); + imaAdsLoader.start(adsLoaderListener, adViewProvider); - verify(adDisplayContainer, atLeastOnce()).setAdContainer(adUiViewGroup); + verify(adDisplayContainer, atLeastOnce()).setAdContainer(adViewGroup); + verify(adDisplayContainer, atLeastOnce()).registerVideoControlsOverlay(adOverlayView); } @Test public void testStart_updatesAdPlaybackState() { setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); - imaAdsLoader.start(adsLoaderListener, adUiViewGroup); + imaAdsLoader.start(adsLoaderListener, adViewProvider); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( @@ -131,14 +150,14 @@ public class ImaAdsLoaderTest { public void testStartAfterRelease() { setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.release(); - imaAdsLoader.start(adsLoaderListener, adUiViewGroup); + imaAdsLoader.start(adsLoaderListener, adViewProvider); } @Test public void testStartAndCallbacksAfterRelease() { setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.release(); - imaAdsLoader.start(adsLoaderListener, adUiViewGroup); + imaAdsLoader.start(adsLoaderListener, adViewProvider); fakeExoPlayer.setPlayingContentPosition(/* position= */ 0); fakeExoPlayer.setState(Player.STATE_READY, true); @@ -146,7 +165,7 @@ public class ImaAdsLoaderTest { // Note: we can't currently call getContentProgress/getAdProgress as a VerifyError is thrown // when using Robolectric and accessing VideoProgressUpdate.VIDEO_TIME_NOT_READY, due to the IMA // SDK being proguarded. - imaAdsLoader.requestAds(adUiViewGroup); + imaAdsLoader.requestAds(adViewGroup); imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD)); imaAdsLoader.loadAd(TEST_URI.toString()); imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD)); @@ -166,7 +185,7 @@ public class ImaAdsLoaderTest { setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); // Load the preroll ad. - imaAdsLoader.start(adsLoaderListener, adUiViewGroup); + imaAdsLoader.start(adsLoaderListener, adViewProvider); imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD)); imaAdsLoader.loadAd(TEST_URI.toString()); imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD)); @@ -201,6 +220,18 @@ public class ImaAdsLoaderTest { .withAdResumePositionUs(/* adResumePositionUs= */ 0)); } + @Test + public void testStop_unregistersAllVideoControlOverlays() { + setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + imaAdsLoader.start(adsLoaderListener, adViewProvider); + imaAdsLoader.requestAds(adViewGroup); + imaAdsLoader.stop(); + + InOrder inOrder = inOrder(adDisplayContainer); + inOrder.verify(adDisplayContainer).registerVideoControlsOverlay(adOverlayView); + inOrder.verify(adDisplayContainer).unregisterAllVideoControlsOverlays(); + } + private void setupPlayback(Timeline contentTimeline, long[][] adDurationsUs, Float[] cuePoints) { fakeExoPlayer = new FakePlayer(); adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline, adDurationsUs); 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 48ac79f227..b3054f69a0 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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.ads; import android.support.annotation.Nullable; +import android.view.View; import android.view.ViewGroup; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; @@ -31,16 +32,16 @@ import java.io.IOException; * with a new copy of the current {@link AdPlaybackState} whenever further information about ads * becomes known (for example, when an ad media URI is available, or an ad has played to the end). * - *

{@link #start(EventListener, ViewGroup)} will be called when the ads media source first + *

{@link #start(EventListener, AdViewProvider)} will be called when the ads media source first * initializes, at which point the loader can request ads. If the player enters the background, * {@link #stop()} will be called. Loaders should maintain any ad playback state in preparation for - * a later call to {@link #start(EventListener, ViewGroup)}. If an ad is playing when the player is - * detached, update the ad playback state with the current playback position using {@link + * a later call to {@link #start(EventListener, AdViewProvider)}. If an ad is playing when the + * player is detached, update the ad playback state with the current playback position using {@link * AdPlaybackState#withAdResumePositionUs(long)}. * *

If {@link EventListener#onAdPlaybackState(AdPlaybackState)} has been called, the - * implementation of {@link #start(EventListener, ViewGroup)} should invoke the same listener to - * provide the existing playback state to the new player. + * implementation of {@link #start(EventListener, AdViewProvider)} should invoke the same listener + * to provide the existing playback state to the new player. */ public interface AdsLoader { @@ -69,6 +70,25 @@ public interface AdsLoader { default void onAdTapped() {} } + /** Provides views for the ad UI. */ + interface AdViewProvider { + + /** Returns the {@link ViewGroup} on top of the player that will show any ad UI. */ + ViewGroup getAdViewGroup(); + + /** + * 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). + * + *

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(); + } + // Methods called by the application. /** @@ -95,8 +115,8 @@ public interface AdsLoader { /** * Sets the supported content types for ad media. Must be called before the first call to {@link - * #start(EventListener, ViewGroup)}. Subsequent calls may be ignored. Called on the main thread - * by {@link AdsMediaSource}. + * #start(EventListener, AdViewProvider)}. Subsequent calls may be ignored. Called on the main + * thread by {@link AdsMediaSource}. * * @param contentTypes The supported content types for ad media. Each element must be one of * {@link C#TYPE_DASH}, {@link C#TYPE_HLS}, {@link C#TYPE_SS} and {@link C#TYPE_OTHER}. @@ -107,9 +127,9 @@ public interface AdsLoader { * Starts using the ads loader for playback. Called on the main thread by {@link AdsMediaSource}. * * @param eventListener Listener for ads loader events. - * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + * @param adViewProvider Provider of views for the ad UI. */ - void start(EventListener eventListener, ViewGroup adUiViewGroup); + void start(EventListener eventListener, AdViewProvider adViewProvider); /** * Stops using the ads loader for playback and deregisters the event listener. Called on the main diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 75c9c789cf..76fe7f8735 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -20,7 +20,6 @@ import android.os.Handler; import android.os.Looper; import android.support.annotation.IntDef; import android.support.annotation.Nullable; -import android.view.ViewGroup; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.CompositeMediaSource; @@ -146,7 +145,7 @@ public final class AdsMediaSource extends CompositeMediaSource { private final MediaSource contentMediaSource; private final MediaSourceFactory adMediaSourceFactory; private final AdsLoader adsLoader; - private final ViewGroup adUiViewGroup; + private final AdsLoader.AdViewProvider adViewProvider; private final Handler mainHandler; private final Map> deferredMediaPeriodByAdMediaSource; private final Timeline.Period period; @@ -166,18 +165,18 @@ public final class AdsMediaSource extends CompositeMediaSource { * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. * @param adsLoader The loader for ads. - * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + * @param adViewProvider Provider of views for the ad UI. */ public AdsMediaSource( MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, AdsLoader adsLoader, - ViewGroup adUiViewGroup) { + AdsLoader.AdViewProvider adViewProvider) { this( contentMediaSource, new ProgressiveMediaSource.Factory(dataSourceFactory), adsLoader, - adUiViewGroup); + adViewProvider); } /** @@ -187,17 +186,17 @@ public final class AdsMediaSource extends CompositeMediaSource { * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param adMediaSourceFactory Factory for media sources used to load ad media. * @param adsLoader The loader for ads. - * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + * @param adViewProvider Provider of views for the ad UI. */ public AdsMediaSource( MediaSource contentMediaSource, MediaSourceFactory adMediaSourceFactory, AdsLoader adsLoader, - ViewGroup adUiViewGroup) { + AdsLoader.AdViewProvider adViewProvider) { this.contentMediaSource = contentMediaSource; this.adMediaSourceFactory = adMediaSourceFactory; this.adsLoader = adsLoader; - this.adUiViewGroup = adUiViewGroup; + this.adViewProvider = adViewProvider; mainHandler = new Handler(Looper.getMainLooper()); deferredMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); @@ -218,7 +217,7 @@ public final class AdsMediaSource extends CompositeMediaSource { ComponentListener componentListener = new ComponentListener(); this.componentListener = componentListener; prepareChildSource(DUMMY_CONTENT_MEDIA_PERIOD_ID, contentMediaSource); - mainHandler.post(() -> adsLoader.start(componentListener, adUiViewGroup)); + mainHandler.post(() -> adsLoader.start(componentListener, adViewProvider)); } @Override diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index 8871767f49..00ffaa8057 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -54,6 +54,7 @@ import com.google.android.exoplayer2.Player.VideoComponent; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -244,7 +245,7 @@ import java.util.List; * PlayerView. This will cause the specified layout to be inflated instead of {@code * exo_player_view.xml} for only the instance on which the attribute is set. */ -public class PlayerView extends FrameLayout { +public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider { // LINT.IfChange /** @@ -283,7 +284,7 @@ public class PlayerView extends FrameLayout { private final SubtitleView subtitleView; @Nullable private final View bufferingView; @Nullable private final TextView errorMessageView; - private final PlayerControlView controller; + @Nullable private final PlayerControlView controller; private final ComponentListener componentListener; @Nullable private final FrameLayout adOverlayFrameLayout; @Nullable private final FrameLayout overlayFrameLayout; @@ -1106,6 +1107,21 @@ public class PlayerView extends FrameLayout { } } + // AdsLoader.AdViewProvider implementation. + + @Override + public ViewGroup getAdViewGroup() { + return Assertions.checkNotNull( + adOverlayFrameLayout, "exo_ad_overlay must be present for ad playback"); + } + + @Override + public View[] getAdOverlayViews() { + return controller != null ? new View[] {controller} : new View[0]; + } + + // Internal methods. + private boolean toggleControllerVisibility() { if (!useController || player == null) { return false;