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
This commit is contained in:
parent
0183678747
commit
56944caa21
@ -50,6 +50,10 @@
|
|||||||
* Remove deprecated `AdsMediaSource` constructors. Listen for media source
|
* Remove deprecated `AdsMediaSource` constructors. Listen for media source
|
||||||
events using `AdsMediaSource.addEventListener`, and ad interaction events by
|
events using `AdsMediaSource.addEventListener`, and ad interaction events by
|
||||||
adding a listener when building `ImaAdsLoader`.
|
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`.
|
* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`.
|
||||||
* VP9 extension: Remove RGB output mode and libyuv dependency, and switch to
|
* VP9 extension: Remove RGB output mode and libyuv dependency, and switch to
|
||||||
surface YUV output as the default. Remove constructor parameters `scaleToFit`
|
surface YUV output as the default. Remove constructor parameters `scaleToFit`
|
||||||
|
@ -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.
|
// Compose the content media source into a new AdsMediaSource with both ads and content.
|
||||||
MediaSource mediaSourceWithAds =
|
MediaSource mediaSourceWithAds =
|
||||||
new AdsMediaSource(
|
new AdsMediaSource(
|
||||||
contentMediaSource,
|
contentMediaSource, /* adMediaSourceFactory= */ this, adsLoader, playerView);
|
||||||
/* adMediaSourceFactory= */ this,
|
|
||||||
adsLoader,
|
|
||||||
playerView.getOverlayFrameLayout());
|
|
||||||
|
|
||||||
// Prepare the player with the source.
|
// Prepare the player with the source.
|
||||||
player.seekTo(contentPosition);
|
player.seekTo(contentPosition);
|
||||||
|
@ -26,9 +26,7 @@ import android.util.Pair;
|
|||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
@ -146,7 +144,6 @@ public class PlayerActivity extends Activity
|
|||||||
|
|
||||||
private AdsLoader adsLoader;
|
private AdsLoader adsLoader;
|
||||||
private Uri loadedAdTagUri;
|
private Uri loadedAdTagUri;
|
||||||
private ViewGroup adUiViewGroup;
|
|
||||||
|
|
||||||
// Activity lifecycle
|
// Activity lifecycle
|
||||||
|
|
||||||
@ -464,7 +461,6 @@ public class PlayerActivity extends Activity
|
|||||||
return buildMediaSource(uri, null);
|
return buildMediaSource(uri, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
|
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
|
||||||
@ContentType int type = Util.inferContentType(uri, overrideExtension);
|
@ContentType int type = Util.inferContentType(uri, overrideExtension);
|
||||||
List<StreamKey> offlineStreamKeys = getOfflineStreamKeys(uri);
|
List<StreamKey> offlineStreamKeys = getOfflineStreamKeys(uri);
|
||||||
@ -584,9 +580,6 @@ public class PlayerActivity extends Activity
|
|||||||
.getConstructor(android.content.Context.class, android.net.Uri.class);
|
.getConstructor(android.content.Context.class, android.net.Uri.class);
|
||||||
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
|
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
|
||||||
adsLoader = loaderConstructor.newInstance(this, adTagUri);
|
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);
|
adsLoader.setPlayer(player);
|
||||||
AdsMediaSource.MediaSourceFactory adMediaSourceFactory =
|
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 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) {
|
} catch (ClassNotFoundException e) {
|
||||||
// IMA extension not loaded.
|
// IMA extension not loaded.
|
||||||
return null;
|
return null;
|
||||||
|
@ -22,6 +22,7 @@ import android.os.SystemClock;
|
|||||||
import android.support.annotation.IntDef;
|
import android.support.annotation.IntDef;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.annotation.VisibleForTesting;
|
import android.support.annotation.VisibleForTesting;
|
||||||
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import com.google.ads.interactivemedia.v3.api.Ad;
|
import com.google.ads.interactivemedia.v3.api.Ad;
|
||||||
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
|
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
|
||||||
@ -79,6 +80,10 @@ import java.util.Set;
|
|||||||
* <p>The player instance that will play the loaded ads must be set before playback using {@link
|
* <p>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
|
* #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling
|
||||||
* {@link #release()}.
|
* {@link #release()}.
|
||||||
|
*
|
||||||
|
* <p>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
|
public final class ImaAdsLoader
|
||||||
implements Player.EventListener,
|
implements Player.EventListener,
|
||||||
@ -488,13 +493,29 @@ public final class ImaAdsLoader
|
|||||||
return adsLoader;
|
return adsLoader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link AdDisplayContainer} used by this loader.
|
||||||
|
*
|
||||||
|
* <p>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
|
* Sets the slots for displaying companion ads. Individual slots can be created using {@link
|
||||||
* ImaSdkFactory#createCompanionAdSlot()}.
|
* ImaSdkFactory#createCompanionAdSlot()}.
|
||||||
*
|
*
|
||||||
* @param companionSlots Slots for displaying companion ads.
|
* @param companionSlots Slots for displaying companion ads.
|
||||||
* @see AdDisplayContainer#setCompanionSlots(Collection)
|
* @see AdDisplayContainer#setCompanionSlots(Collection)
|
||||||
|
* @deprecated Use {@code getAdDisplayContainer().setCompanionSlots(...)}.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public void setCompanionSlots(Collection<CompanionAdSlot> companionSlots) {
|
public void setCompanionSlots(Collection<CompanionAdSlot> companionSlots) {
|
||||||
adDisplayContainer.setCompanionSlots(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
|
* called, so it is only necessary to call this method if you want to request ads before preparing
|
||||||
* the player.
|
* 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) {
|
if (adPlaybackState != null || adsManager != null || pendingAdRequestContext != null) {
|
||||||
// Ads have already been requested.
|
// Ads have already been requested.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
adDisplayContainer.setAdContainer(adUiViewGroup);
|
adDisplayContainer.setAdContainer(adViewGroup);
|
||||||
pendingAdRequestContext = new Object();
|
pendingAdRequestContext = new Object();
|
||||||
AdsRequest request = imaFactory.createAdsRequest();
|
AdsRequest request = imaFactory.createAdsRequest();
|
||||||
if (adTagUri != null) {
|
if (adTagUri != null) {
|
||||||
@ -563,7 +584,7 @@ public final class ImaAdsLoader
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start(EventListener eventListener, ViewGroup adUiViewGroup) {
|
public void start(EventListener eventListener, AdViewProvider adViewProvider) {
|
||||||
Assertions.checkNotNull(
|
Assertions.checkNotNull(
|
||||||
nextPlayer, "Set player using adsLoader.setPlayer before preparing the player.");
|
nextPlayer, "Set player using adsLoader.setPlayer before preparing the player.");
|
||||||
player = nextPlayer;
|
player = nextPlayer;
|
||||||
@ -571,7 +592,12 @@ public final class ImaAdsLoader
|
|||||||
lastVolumePercentage = 0;
|
lastVolumePercentage = 0;
|
||||||
lastAdProgress = null;
|
lastAdProgress = null;
|
||||||
lastContentProgress = 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);
|
player.addListener(this);
|
||||||
maybeNotifyPendingAdLoadError();
|
maybeNotifyPendingAdLoadError();
|
||||||
if (adPlaybackState != null) {
|
if (adPlaybackState != null) {
|
||||||
@ -585,7 +611,7 @@ public final class ImaAdsLoader
|
|||||||
startAdPlayback();
|
startAdPlayback();
|
||||||
} else {
|
} else {
|
||||||
// Ads haven't loaded yet, so request them.
|
// Ads haven't loaded yet, so request them.
|
||||||
requestAds(adUiViewGroup);
|
requestAds(adViewGroup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -600,6 +626,7 @@ public final class ImaAdsLoader
|
|||||||
lastVolumePercentage = getVolume();
|
lastVolumePercentage = getVolume();
|
||||||
lastAdProgress = getAdProgress();
|
lastAdProgress = getAdProgress();
|
||||||
lastContentProgress = getContentProgress();
|
lastContentProgress = getContentProgress();
|
||||||
|
adDisplayContainer.unregisterAllVideoControlsOverlays();
|
||||||
player.removeListener(this);
|
player.removeListener(this);
|
||||||
player = null;
|
player = null;
|
||||||
eventListener = null;
|
eventListener = null;
|
||||||
|
@ -20,6 +20,7 @@ import com.google.android.exoplayer2.C;
|
|||||||
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.testutil.StubExoPlayer;
|
import com.google.android.exoplayer2.testutil.StubExoPlayer;
|
||||||
|
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
/** A fake player for testing content/ad playback. */
|
/** A fake player for testing content/ad playback. */
|
||||||
@ -109,6 +110,11 @@ import java.util.ArrayList;
|
|||||||
|
|
||||||
// ExoPlayer methods. Other methods are unsupported.
|
// ExoPlayer methods. Other methods are unsupported.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AudioComponent getAudioComponent() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Looper getApplicationLooper() {
|
public Looper getApplicationLooper() {
|
||||||
return Looper.getMainLooper();
|
return Looper.getMainLooper();
|
||||||
@ -134,6 +140,16 @@ import java.util.ArrayList;
|
|||||||
return playWhenReady;
|
return playWhenReady;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getRendererCount() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TrackSelectionArray getCurrentTrackSelections() {
|
||||||
|
return new TrackSelectionArray();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Timeline getCurrentTimeline() {
|
public Timeline getCurrentTimeline() {
|
||||||
return timeline;
|
return timeline;
|
||||||
|
@ -17,11 +17,13 @@ package com.google.android.exoplayer2.ext.ima;
|
|||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static org.mockito.Mockito.atLeastOnce;
|
import static org.mockito.Mockito.atLeastOnce;
|
||||||
|
import static org.mockito.Mockito.inOrder;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import com.google.ads.interactivemedia.v3.api.Ad;
|
import com.google.ads.interactivemedia.v3.api.Ad;
|
||||||
@ -49,6 +51,7 @@ import org.junit.After;
|
|||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.InOrder;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
import org.robolectric.RobolectricTestRunner;
|
import org.robolectric.RobolectricTestRunner;
|
||||||
@ -73,7 +76,9 @@ public class ImaAdsLoaderTest {
|
|||||||
private @Mock AdDisplayContainer adDisplayContainer;
|
private @Mock AdDisplayContainer adDisplayContainer;
|
||||||
private @Mock AdsManager adsManager;
|
private @Mock AdsManager adsManager;
|
||||||
private SingletonImaFactory testImaFactory;
|
private SingletonImaFactory testImaFactory;
|
||||||
private ViewGroup adUiViewGroup;
|
private ViewGroup adViewGroup;
|
||||||
|
private View adOverlayView;
|
||||||
|
private AdsLoader.AdViewProvider adViewProvider;
|
||||||
private TestAdsLoaderListener adsLoaderListener;
|
private TestAdsLoaderListener adsLoaderListener;
|
||||||
private FakePlayer fakeExoPlayer;
|
private FakePlayer fakeExoPlayer;
|
||||||
private ImaAdsLoader imaAdsLoader;
|
private ImaAdsLoader imaAdsLoader;
|
||||||
@ -90,7 +95,20 @@ public class ImaAdsLoaderTest {
|
|||||||
adDisplayContainer,
|
adDisplayContainer,
|
||||||
fakeAdsRequest,
|
fakeAdsRequest,
|
||||||
fakeAdsLoader);
|
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
|
@After
|
||||||
@ -111,15 +129,16 @@ public class ImaAdsLoaderTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testStart_setsAdUiViewGroup() {
|
public void testStart_setsAdUiViewGroup() {
|
||||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
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
|
@Test
|
||||||
public void testStart_updatesAdPlaybackState() {
|
public void testStart_updatesAdPlaybackState() {
|
||||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||||
imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
|
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||||
|
|
||||||
assertThat(adsLoaderListener.adPlaybackState)
|
assertThat(adsLoaderListener.adPlaybackState)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
@ -131,14 +150,14 @@ public class ImaAdsLoaderTest {
|
|||||||
public void testStartAfterRelease() {
|
public void testStartAfterRelease() {
|
||||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||||
imaAdsLoader.release();
|
imaAdsLoader.release();
|
||||||
imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
|
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testStartAndCallbacksAfterRelease() {
|
public void testStartAndCallbacksAfterRelease() {
|
||||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||||
imaAdsLoader.release();
|
imaAdsLoader.release();
|
||||||
imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
|
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||||
fakeExoPlayer.setPlayingContentPosition(/* position= */ 0);
|
fakeExoPlayer.setPlayingContentPosition(/* position= */ 0);
|
||||||
fakeExoPlayer.setState(Player.STATE_READY, true);
|
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
|
// 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
|
// when using Robolectric and accessing VideoProgressUpdate.VIDEO_TIME_NOT_READY, due to the IMA
|
||||||
// SDK being proguarded.
|
// SDK being proguarded.
|
||||||
imaAdsLoader.requestAds(adUiViewGroup);
|
imaAdsLoader.requestAds(adViewGroup);
|
||||||
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD));
|
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD));
|
||||||
imaAdsLoader.loadAd(TEST_URI.toString());
|
imaAdsLoader.loadAd(TEST_URI.toString());
|
||||||
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD));
|
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);
|
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||||
|
|
||||||
// Load the preroll ad.
|
// Load the preroll ad.
|
||||||
imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
|
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||||
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD));
|
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD));
|
||||||
imaAdsLoader.loadAd(TEST_URI.toString());
|
imaAdsLoader.loadAd(TEST_URI.toString());
|
||||||
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD));
|
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD));
|
||||||
@ -201,6 +220,18 @@ public class ImaAdsLoaderTest {
|
|||||||
.withAdResumePositionUs(/* adResumePositionUs= */ 0));
|
.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) {
|
private void setupPlayback(Timeline contentTimeline, long[][] adDurationsUs, Float[] cuePoints) {
|
||||||
fakeExoPlayer = new FakePlayer();
|
fakeExoPlayer = new FakePlayer();
|
||||||
adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline, adDurationsUs);
|
adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline, adDurationsUs);
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
package com.google.android.exoplayer2.source.ads;
|
package com.google.android.exoplayer2.source.ads;
|
||||||
|
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Player;
|
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
|
* 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).
|
* becomes known (for example, when an ad media URI is available, or an ad has played to the end).
|
||||||
*
|
*
|
||||||
* <p>{@link #start(EventListener, ViewGroup)} will be called when the ads media source first
|
* <p>{@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,
|
* 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
|
* {@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
|
* a later call to {@link #start(EventListener, AdViewProvider)}. If an ad is playing when the
|
||||||
* detached, update the ad playback state with the current playback position using {@link
|
* player is detached, update the ad playback state with the current playback position using {@link
|
||||||
* AdPlaybackState#withAdResumePositionUs(long)}.
|
* AdPlaybackState#withAdResumePositionUs(long)}.
|
||||||
*
|
*
|
||||||
* <p>If {@link EventListener#onAdPlaybackState(AdPlaybackState)} has been called, the
|
* <p>If {@link EventListener#onAdPlaybackState(AdPlaybackState)} has been called, the
|
||||||
* implementation of {@link #start(EventListener, ViewGroup)} should invoke the same listener to
|
* implementation of {@link #start(EventListener, AdViewProvider)} should invoke the same listener
|
||||||
* provide the existing playback state to the new player.
|
* to provide the existing playback state to the new player.
|
||||||
*/
|
*/
|
||||||
public interface AdsLoader {
|
public interface AdsLoader {
|
||||||
|
|
||||||
@ -69,6 +70,25 @@ public interface AdsLoader {
|
|||||||
default void onAdTapped() {}
|
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).
|
||||||
|
*
|
||||||
|
* <p>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.
|
// 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
|
* 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
|
* #start(EventListener, AdViewProvider)}. Subsequent calls may be ignored. Called on the main
|
||||||
* by {@link AdsMediaSource}.
|
* thread by {@link AdsMediaSource}.
|
||||||
*
|
*
|
||||||
* @param contentTypes The supported content types for ad media. Each element must be one of
|
* @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}.
|
* {@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}.
|
* Starts using the ads loader for playback. Called on the main thread by {@link AdsMediaSource}.
|
||||||
*
|
*
|
||||||
* @param eventListener Listener for ads loader events.
|
* @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
|
* Stops using the ads loader for playback and deregisters the event listener. Called on the main
|
||||||
|
@ -20,7 +20,6 @@ import android.os.Handler;
|
|||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.support.annotation.IntDef;
|
import android.support.annotation.IntDef;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.view.ViewGroup;
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.source.CompositeMediaSource;
|
import com.google.android.exoplayer2.source.CompositeMediaSource;
|
||||||
@ -146,7 +145,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||||||
private final MediaSource contentMediaSource;
|
private final MediaSource contentMediaSource;
|
||||||
private final MediaSourceFactory adMediaSourceFactory;
|
private final MediaSourceFactory adMediaSourceFactory;
|
||||||
private final AdsLoader adsLoader;
|
private final AdsLoader adsLoader;
|
||||||
private final ViewGroup adUiViewGroup;
|
private final AdsLoader.AdViewProvider adViewProvider;
|
||||||
private final Handler mainHandler;
|
private final Handler mainHandler;
|
||||||
private final Map<MediaSource, List<DeferredMediaPeriod>> deferredMediaPeriodByAdMediaSource;
|
private final Map<MediaSource, List<DeferredMediaPeriod>> deferredMediaPeriodByAdMediaSource;
|
||||||
private final Timeline.Period period;
|
private final Timeline.Period period;
|
||||||
@ -166,18 +165,18 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||||||
* @param contentMediaSource The {@link MediaSource} providing the content to play.
|
* @param contentMediaSource The {@link MediaSource} providing the content to play.
|
||||||
* @param dataSourceFactory Factory for data sources used to load ad media.
|
* @param dataSourceFactory Factory for data sources used to load ad media.
|
||||||
* @param adsLoader The loader for ads.
|
* @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(
|
public AdsMediaSource(
|
||||||
MediaSource contentMediaSource,
|
MediaSource contentMediaSource,
|
||||||
DataSource.Factory dataSourceFactory,
|
DataSource.Factory dataSourceFactory,
|
||||||
AdsLoader adsLoader,
|
AdsLoader adsLoader,
|
||||||
ViewGroup adUiViewGroup) {
|
AdsLoader.AdViewProvider adViewProvider) {
|
||||||
this(
|
this(
|
||||||
contentMediaSource,
|
contentMediaSource,
|
||||||
new ProgressiveMediaSource.Factory(dataSourceFactory),
|
new ProgressiveMediaSource.Factory(dataSourceFactory),
|
||||||
adsLoader,
|
adsLoader,
|
||||||
adUiViewGroup);
|
adViewProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -187,17 +186,17 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||||||
* @param contentMediaSource The {@link MediaSource} providing the content to play.
|
* @param contentMediaSource The {@link MediaSource} providing the content to play.
|
||||||
* @param adMediaSourceFactory Factory for media sources used to load ad media.
|
* @param adMediaSourceFactory Factory for media sources used to load ad media.
|
||||||
* @param adsLoader The loader for ads.
|
* @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(
|
public AdsMediaSource(
|
||||||
MediaSource contentMediaSource,
|
MediaSource contentMediaSource,
|
||||||
MediaSourceFactory adMediaSourceFactory,
|
MediaSourceFactory adMediaSourceFactory,
|
||||||
AdsLoader adsLoader,
|
AdsLoader adsLoader,
|
||||||
ViewGroup adUiViewGroup) {
|
AdsLoader.AdViewProvider adViewProvider) {
|
||||||
this.contentMediaSource = contentMediaSource;
|
this.contentMediaSource = contentMediaSource;
|
||||||
this.adMediaSourceFactory = adMediaSourceFactory;
|
this.adMediaSourceFactory = adMediaSourceFactory;
|
||||||
this.adsLoader = adsLoader;
|
this.adsLoader = adsLoader;
|
||||||
this.adUiViewGroup = adUiViewGroup;
|
this.adViewProvider = adViewProvider;
|
||||||
mainHandler = new Handler(Looper.getMainLooper());
|
mainHandler = new Handler(Looper.getMainLooper());
|
||||||
deferredMediaPeriodByAdMediaSource = new HashMap<>();
|
deferredMediaPeriodByAdMediaSource = new HashMap<>();
|
||||||
period = new Timeline.Period();
|
period = new Timeline.Period();
|
||||||
@ -218,7 +217,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||||||
ComponentListener componentListener = new ComponentListener();
|
ComponentListener componentListener = new ComponentListener();
|
||||||
this.componentListener = componentListener;
|
this.componentListener = componentListener;
|
||||||
prepareChildSource(DUMMY_CONTENT_MEDIA_PERIOD_ID, contentMediaSource);
|
prepareChildSource(DUMMY_CONTENT_MEDIA_PERIOD_ID, contentMediaSource);
|
||||||
mainHandler.post(() -> adsLoader.start(componentListener, adUiViewGroup));
|
mainHandler.post(() -> adsLoader.start(componentListener, adViewProvider));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -54,6 +54,7 @@ import com.google.android.exoplayer2.Player.VideoComponent;
|
|||||||
import com.google.android.exoplayer2.metadata.Metadata;
|
import com.google.android.exoplayer2.metadata.Metadata;
|
||||||
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
|
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
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.Cue;
|
||||||
import com.google.android.exoplayer2.text.TextOutput;
|
import com.google.android.exoplayer2.text.TextOutput;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
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
|
* 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.
|
* 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
|
// LINT.IfChange
|
||||||
/**
|
/**
|
||||||
@ -283,7 +284,7 @@ public class PlayerView extends FrameLayout {
|
|||||||
private final SubtitleView subtitleView;
|
private final SubtitleView subtitleView;
|
||||||
@Nullable private final View bufferingView;
|
@Nullable private final View bufferingView;
|
||||||
@Nullable private final TextView errorMessageView;
|
@Nullable private final TextView errorMessageView;
|
||||||
private final PlayerControlView controller;
|
@Nullable private final PlayerControlView controller;
|
||||||
private final ComponentListener componentListener;
|
private final ComponentListener componentListener;
|
||||||
@Nullable private final FrameLayout adOverlayFrameLayout;
|
@Nullable private final FrameLayout adOverlayFrameLayout;
|
||||||
@Nullable private final FrameLayout overlayFrameLayout;
|
@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() {
|
private boolean toggleControllerVisibility() {
|
||||||
if (!useController || player == null) {
|
if (!useController || player == null) {
|
||||||
return false;
|
return false;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user