From 0b58c33632014bf485774b0c19ce2e92ae5ef42d Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 11 Jul 2017 07:29:39 -0700 Subject: [PATCH] Handle detaching and reattaching the ads loader ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=161526026 --- .../exoplayer2/demo/PlayerActivity.java | 69 +++++++- .../exoplayer2/ext/ima/ImaAdsLoader.java | 161 ++++++++++++------ .../exoplayer2/ext/ima/ImaAdsMediaSource.java | 71 ++------ 3 files changed, 188 insertions(+), 113 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index d6b403223e..26179a66d9 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -73,6 +73,7 @@ import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.Util; import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; @@ -124,6 +125,12 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay private int resumeWindow; private long resumePosition; + // Fields used only for ad playback. The ads loader is loaded via reflection. + + private Object imaAdsLoader; // com.google.android.exoplayer2.ext.ima.ImaAdsLoader + private Uri loadedAdTagUri; + private ViewGroup adOverlayViewGroup; + // Activity lifecycle @Override @@ -190,6 +197,12 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay } } + @Override + public void onDestroy() { + super.onDestroy(); + releaseAdsLoader(); + } + @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { @@ -317,20 +330,19 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA); if (adTagUriString != null) { Uri adTagUri = Uri.parse(adTagUriString); - ViewGroup adOverlayViewGroup = new FrameLayout(this); - // Load the extension source using reflection so that demo app doesn't have to depend on it. + if (!adTagUri.equals(loadedAdTagUri)) { + releaseAdsLoader(); + loadedAdTagUri = adTagUri; + } try { - Class clazz = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsMediaSource"); - Constructor constructor = clazz.getConstructor(MediaSource.class, - DataSource.Factory.class, Context.class, Uri.class, ViewGroup.class); - mediaSource = (MediaSource) constructor.newInstance(mediaSource, - mediaDataSourceFactory, this, adTagUri, adOverlayViewGroup); + mediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString)); // The demo app has a non-null overlay frame layout. simpleExoPlayerView.getOverlayFrameLayout().addView(adOverlayViewGroup); } catch (Exception e) { - // Throw if the media source class was not found, or there was an error instantiating it. showToast(R.string.ima_not_loaded); } + } else { + releaseAdsLoader(); } boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; if (haveResumePosition) { @@ -429,6 +441,47 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay .buildHttpDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null); } + /** + * Returns an ads media source, reusing the ads loader if one exists. + * + * @throws Exception Thrown if it was not possible to create an ads media source, for example, due + * to a missing dependency. + */ + private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) throws Exception { + // Load the extension source using reflection so the demo app doesn't have to depend on it. + // The ads loader is reused for multiple playbacks, so that ad playback can resume. + Class loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader"); + if (imaAdsLoader == null) { + imaAdsLoader = loaderClass.getConstructor(Context.class, Uri.class) + .newInstance(this, adTagUri); + adOverlayViewGroup = new FrameLayout(this); + // The demo app has a non-null overlay frame layout. + simpleExoPlayerView.getOverlayFrameLayout().addView(adOverlayViewGroup); + } + Class sourceClass = + Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsMediaSource"); + Constructor constructor = sourceClass.getConstructor(MediaSource.class, + DataSource.Factory.class, loaderClass, ViewGroup.class); + return (MediaSource) constructor.newInstance(mediaSource, mediaDataSourceFactory, imaAdsLoader, + adOverlayViewGroup); + } + + private void releaseAdsLoader() { + if (imaAdsLoader != null) { + try { + Class loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader"); + Method releaseMethod = loaderClass.getMethod("release"); + releaseMethod.invoke(imaAdsLoader); + } catch (Exception e) { + // Should never happen. + throw new IllegalStateException(e); + } + imaAdsLoader = null; + loadedAdTagUri = null; + simpleExoPlayerView.getOverlayFrameLayout().removeAllViews(); + } + } + // ExoPlayer.EventListener implementation @Override 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 89c8e61b7f..b0462334dc 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 @@ -54,7 +54,7 @@ import java.util.List; /** * Loads ads using the IMA SDK. All methods are called on the main thread. */ -/* package */ final class ImaAdsLoader implements ExoPlayer.EventListener, VideoAdPlayer, +public final class ImaAdsLoader implements ExoPlayer.EventListener, VideoAdPlayer, ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener { private static final boolean DEBUG = false; @@ -95,19 +95,23 @@ import java.util.List; */ private static final long END_OF_CONTENT_POSITION_THRESHOLD_MS = 5000; - private final EventListener eventListener; - private final ExoPlayer player; + private final Uri adTagUri; private final Timeline.Period period; private final List adCallbacks; + private final ImaSdkFactory imaSdkFactory; + private final AdDisplayContainer adDisplayContainer; private final AdsLoader adsLoader; + private EventListener eventListener; + private ExoPlayer player; + private VideoProgressUpdate lastContentProgress; + private VideoProgressUpdate lastAdProgress; + private AdsManager adsManager; private Timeline timeline; private long contentDurationMs; private AdPlaybackState adPlaybackState; - private boolean released; - // Fields tracking IMA's state. /** @@ -163,46 +167,80 @@ import java.util.List; * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for * more information. - * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + */ + public ImaAdsLoader(Context context, Uri adTagUri) { + this(context, adTagUri, null); + } + + /** + * Creates a new IMA ads loader. + * + * @param context The context. + * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See + * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for + * more information. * @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to * use the default settings. If set, the player type and version fields may be overwritten. - * @param player The player instance that will play the loaded ad schedule. - * @param eventListener Listener for ad loader events. */ - public ImaAdsLoader(Context context, Uri adTagUri, ViewGroup adUiViewGroup, - ImaSdkSettings imaSdkSettings, ExoPlayer player, EventListener eventListener) { - this.eventListener = eventListener; - this.player = player; + public ImaAdsLoader(Context context, Uri adTagUri, ImaSdkSettings imaSdkSettings) { + this.adTagUri = adTagUri; period = new Timeline.Period(); adCallbacks = new ArrayList<>(1); - - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - pendingContentPositionMs = C.TIME_UNSET; - adGroupIndex = C.INDEX_UNSET; - contentDurationMs = C.TIME_UNSET; - - player.addListener(this); - - ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); - AdDisplayContainer adDisplayContainer = imaSdkFactory.createAdDisplayContainer(); + imaSdkFactory = ImaSdkFactory.getInstance(); + adDisplayContainer = imaSdkFactory.createAdDisplayContainer(); adDisplayContainer.setPlayer(this); - adDisplayContainer.setAdContainer(adUiViewGroup); - if (imaSdkSettings == null) { imaSdkSettings = imaSdkFactory.createImaSdkSettings(); } imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE); imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION); - - AdsRequest request = imaSdkFactory.createAdsRequest(); - request.setAdTagUrl(adTagUri.toString()); - request.setAdDisplayContainer(adDisplayContainer); - request.setContentProgressProvider(this); - adsLoader = imaSdkFactory.createAdsLoader(context, imaSdkSettings); adsLoader.addAdErrorListener(this); adsLoader.addAdsLoadedListener(this); - adsLoader.requestAds(request); + fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; + pendingContentPositionMs = C.TIME_UNSET; + adGroupIndex = C.INDEX_UNSET; + contentDurationMs = C.TIME_UNSET; + } + + /** + * Attaches a player that will play ads loaded using this instance. + * + * @param player The player instance that will play the loaded ads. + * @param eventListener Listener for ads loader events. + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + */ + public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) { + this.player = player; + this.eventListener = eventListener; + lastAdProgress = null; + lastContentProgress = null; + adDisplayContainer.setAdContainer(adUiViewGroup); + player.addListener(this); + if (adPlaybackState != null) { + eventListener.onAdPlaybackState(adPlaybackState); + // TODO: Call adsManager.resume if an ad is playing. + } else if (adTagUri != null) { + requestAds(); + } + } + + /** + * Detaches any attached player and event listener. To attach a new player, call + * {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)}. Call {@link #release()} to release + * all resources associated with this instance. + */ + public void detachPlayer() { + if (player != null) { + if (adsManager != null && player.isPlayingAd()) { + adsManager.pause(); + } + lastAdProgress = getAdProgress(); + lastContentProgress = getContentProgress(); + player.removeListener(this); + player = null; + } + eventListener = null; } /** @@ -212,9 +250,8 @@ import java.util.List; if (adsManager != null) { adsManager.destroy(); adsManager = null; + detachPlayer(); } - player.removeListener(this); - released = true; } // AdsLoader.AdsLoadedListener implementation. @@ -251,8 +288,8 @@ import java.util.List; if (DEBUG) { Log.d(TAG, "onAdEvent " + adEvent.getType()); } - if (released) { - // The ads manager may pass CONTENT_RESUME_REQUESTED after it is destroyed. + if (adsManager == null) { + Log.w(TAG, "Dropping ad event while detached: " + adEvent); return; } switch (adEvent.getType()) { @@ -274,11 +311,15 @@ import java.util.List; case CONTENT_PAUSE_REQUESTED: // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads // before sending CONTENT_RESUME_REQUESTED. - pauseContentInternal(); + if (player != null) { + pauseContentInternal(); + } break; case SKIPPED: // Fall through. case CONTENT_RESUME_REQUESTED: - resumeContentInternal(); + if (player != null) { + resumeContentInternal(); + } break; case ALL_ADS_COMPLETED: // Do nothing. The ads manager will be released when the source is released. @@ -294,8 +335,10 @@ import java.util.List; if (DEBUG) { Log.d(TAG, "onAdError " + adErrorEvent); } - IOException exception = new IOException("Ad error: " + adErrorEvent, adErrorEvent.getError()); - eventListener.onLoadError(exception); + if (eventListener != null) { + IOException exception = new IOException("Ad error: " + adErrorEvent, adErrorEvent.getError()); + eventListener.onLoadError(exception); + } // TODO: Provide a timeline to the player if it doesn't have one yet, so the content can play. } @@ -303,32 +346,36 @@ import java.util.List; @Override public VideoProgressUpdate getContentProgress() { - if (pendingContentPositionMs != C.TIME_UNSET) { + if (player == null) { + return lastContentProgress; + } else if (pendingContentPositionMs != C.TIME_UNSET) { sentPendingContentPositionMs = true; return new VideoProgressUpdate(pendingContentPositionMs, contentDurationMs); - } - if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { + } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { long adGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); if (adGroupTimeMs == C.TIME_END_OF_SOURCE) { adGroupTimeMs = contentDurationMs; } long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; return new VideoProgressUpdate(adGroupTimeMs + elapsedSinceEndMs, contentDurationMs); - } - if (player.isPlayingAd() || contentDurationMs == C.TIME_UNSET) { + } else if (player.isPlayingAd() || contentDurationMs == C.TIME_UNSET) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; + } else { + return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs); } - return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs); } // VideoAdPlayer implementation. @Override public VideoProgressUpdate getAdProgress() { - if (!player.isPlayingAd()) { + if (player == null) { + return lastAdProgress; + } else if (!player.isPlayingAd()) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; + } else { + return new VideoProgressUpdate(player.getCurrentPosition(), player.getDuration()); } - return new VideoProgressUpdate(player.getCurrentPosition(), player.getDuration()); } @Override @@ -352,6 +399,7 @@ import java.util.List; @Override public void playAd() { + Assertions.checkState(player != null); if (DEBUG) { Log.d(TAG, "playAd"); } @@ -379,6 +427,7 @@ import java.util.List; @Override public void stopAd() { + Assertions.checkState(player != null); if (!playingAd) { if (DEBUG) { Log.d(TAG, "Ignoring unexpected stopAd"); @@ -396,7 +445,7 @@ import java.util.List; if (DEBUG) { Log.d(TAG, "pauseAd"); } - if (released || !playingAd) { + if (player == null || !playingAd) { // This method is called after content is resumed, and may also be called after release. return; } @@ -513,9 +562,14 @@ import java.util.List; // Internal methods. - /** - * Resumes the player, ensuring the current period is a content period by seeking if necessary. - */ + private void requestAds() { + AdsRequest request = imaSdkFactory.createAdsRequest(); + request.setAdTagUrl(adTagUri.toString()); + request.setAdDisplayContainer(adDisplayContainer); + request.setContentProgressProvider(this); + adsLoader.requestAds(request); + } + private void resumeContentInternal() { if (contentDurationMs != C.TIME_UNSET) { if (playingAd) { @@ -573,7 +627,10 @@ import java.util.List; } private void updateAdPlaybackState() { - eventListener.onAdPlaybackState(adPlaybackState.copy()); + // Ignore updates while detached. When a player is attached it will receive the latest state. + if (eventListener != null) { + eventListener.onAdPlaybackState(adPlaybackState.copy()); + } } private static long[] getAdGroupTimesUs(List cuePoints) { diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index 5a54a59cac..920f294d41 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -15,12 +15,9 @@ */ package com.google.android.exoplayer2.ext.ima; -import android.content.Context; -import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.view.ViewGroup; -import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; @@ -44,10 +41,8 @@ public final class ImaAdsMediaSource implements MediaSource { private final MediaSource contentMediaSource; private final DataSource.Factory dataSourceFactory; - private final Context context; - private final Uri adTagUri; + private final ImaAdsLoader imaAdsLoader; private final ViewGroup adUiViewGroup; - private final ImaSdkSettings imaSdkSettings; private final Handler mainHandler; private final AdsLoaderListener adsLoaderListener; private final Map adMediaSourceByMediaPeriod; @@ -66,49 +61,20 @@ public final class ImaAdsMediaSource implements MediaSource { private MediaSource.Listener listener; private IOException adLoadError; - // Accessed on the main thread. - private ImaAdsLoader imaAdsLoader; - /** * Constructs a new source that inserts ads linearly with the content specified by * {@code contentMediaSource}. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. - * @param context The context. - * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See - * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for - * more information. - * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad user - * interface. + * @param imaAdsLoader The loader for ads. */ public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, - Context context, Uri adTagUri, ViewGroup adUiViewGroup) { - this(contentMediaSource, dataSourceFactory, context, adTagUri, adUiViewGroup, null); - } - - /** - * Constructs a new source that inserts ads linearly with the content specified by - * {@code contentMediaSource}. - * - * @param contentMediaSource The {@link MediaSource} providing the content to play. - * @param dataSourceFactory Factory for data sources used to load ad media. - * @param context The context. - * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See - * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for - * more information. - * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. - * @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to - * use the default settings. If set, the player type and version fields may be overwritten. - */ - public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, - Context context, Uri adTagUri, ViewGroup adUiViewGroup, ImaSdkSettings imaSdkSettings) { + ImaAdsLoader imaAdsLoader, ViewGroup adUiViewGroup) { this.contentMediaSource = contentMediaSource; this.dataSourceFactory = dataSourceFactory; - this.context = context; - this.adTagUri = adTagUri; + this.imaAdsLoader = imaAdsLoader; this.adUiViewGroup = adUiViewGroup; - this.imaSdkSettings = imaSdkSettings; mainHandler = new Handler(Looper.getMainLooper()); adsLoaderListener = new AdsLoaderListener(); adMediaSourceByMediaPeriod = new HashMap<>(); @@ -118,24 +84,23 @@ public final class ImaAdsMediaSource implements MediaSource { } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + public void prepareSource(final ExoPlayer player, boolean isTopLevelSource, Listener listener) { Assertions.checkArgument(isTopLevelSource); this.listener = listener; this.player = player; playerHandler = new Handler(); - mainHandler.post(new Runnable() { - @Override - public void run() { - imaAdsLoader = new ImaAdsLoader(context, adTagUri, adUiViewGroup, imaSdkSettings, - ImaAdsMediaSource.this.player, adsLoaderListener); - } - }); contentMediaSource.prepareSource(player, false, new Listener() { @Override public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { ImaAdsMediaSource.this.onContentSourceInfoRefreshed(timeline, manifest); } }); + mainHandler.post(new Runnable() { + @Override + public void run() { + imaAdsLoader.attachPlayer(player, adsLoaderListener, adUiViewGroup); + } + }); } @Override @@ -146,7 +111,9 @@ public final class ImaAdsMediaSource implements MediaSource { contentMediaSource.maybeThrowSourceInfoRefreshError(); for (MediaSource[] mediaSources : adGroupMediaSources) { for (MediaSource mediaSource : mediaSources) { - mediaSource.maybeThrowSourceInfoRefreshError(); + if (mediaSource != null) { + mediaSource.maybeThrowSourceInfoRefreshError(); + } } } } @@ -201,17 +168,15 @@ public final class ImaAdsMediaSource implements MediaSource { contentMediaSource.releaseSource(); for (MediaSource[] mediaSources : adGroupMediaSources) { for (MediaSource mediaSource : mediaSources) { - mediaSource.releaseSource(); + if (mediaSource != null) { + mediaSource.releaseSource(); + } } } mainHandler.post(new Runnable() { @Override public void run() { - // TODO: The source will be released when the application is paused/stopped, which can occur - // if the user taps on the ad. In this case, we should keep the ads manager alive but pause - // it, instead of destroying it. - imaAdsLoader.release(); - imaAdsLoader = null; + imaAdsLoader.detachPlayer(); } }); }