From 58293abc11a92eb90a0b634e879129d001eec15f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 14 Sep 2017 10:34:22 -0700 Subject: [PATCH] Remove IMA dependency and add AdsMediaSource AdsMediaSource lives in the core library so only ImaAdsLoader remains in the ima extension. AdsMediaSource takes an AdsLoader implementation. ImaAdsMediaSource is deprecated rather than removed for now. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168707921 --- .../exoplayer2/demo/PlayerActivity.java | 36 +- .../exoplayer2/ext/ima/ImaAdsLoader.java | 66 +--- .../exoplayer2/ext/ima/ImaAdsMediaSource.java | 282 +------------- .../source/ads}/AdPlaybackState.java | 4 +- .../exoplayer2/source/ads/AdsLoader.java | 96 +++++ .../exoplayer2/source/ads/AdsMediaSource.java | 349 ++++++++++++++++++ .../source/ads}/SinglePeriodAdTimeline.java | 2 +- 7 files changed, 486 insertions(+), 349 deletions(-) rename {extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima => library/core/src/main/java/com/google/android/exoplayer2/source/ads}/AdPlaybackState.java (98%) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java rename {extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima => library/core/src/main/java/com/google/android/exoplayer2/source/ads}/SinglePeriodAdTimeline.java (98%) 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 071e724053..0efb04782d 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 @@ -56,6 +56,8 @@ import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.ads.AdsLoader; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; @@ -73,8 +75,6 @@ import com.google.android.exoplayer2.upstream.DataSource; 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; @@ -129,9 +129,9 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi // Fields used only for ad playback. The ads loader is loaded via reflection. - private Object imaAdsLoader; // com.google.android.exoplayer2.ext.ima.ImaAdsLoader + private AdsLoader adsLoader; private Uri loadedAdTagUri; - private ViewGroup adOverlayViewGroup; + private ViewGroup adUiViewGroup; // Activity lifecycle @@ -453,32 +453,20 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi // 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) + if (adsLoader == null) { + adsLoader = (AdsLoader) loaderClass.getConstructor(Context.class, Uri.class) .newInstance(this, adTagUri); - adOverlayViewGroup = new FrameLayout(this); + adUiViewGroup = new FrameLayout(this); // The demo app has a non-null overlay frame layout. - simpleExoPlayerView.getOverlayFrameLayout().addView(adOverlayViewGroup); + simpleExoPlayerView.getOverlayFrameLayout().addView(adUiViewGroup); } - 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); + return new AdsMediaSource(mediaSource, mediaDataSourceFactory, adsLoader, adUiViewGroup); } 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; + if (adsLoader != null) { + adsLoader.release(); + adsLoader = null; loadedAdTagUri = null; simpleExoPlayerView.getOverlayFrameLayout().removeAllViews(); } 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 e9641fc4d3..7e5912ed28 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 @@ -29,7 +29,6 @@ import com.google.ads.interactivemedia.v3.api.AdEvent; import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener; import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType; import com.google.ads.interactivemedia.v3.api.AdPodInfo; -import com.google.ads.interactivemedia.v3.api.AdsLoader; import com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener; import com.google.ads.interactivemedia.v3.api.AdsManager; import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; @@ -48,6 +47,8 @@ import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.ads.AdPlaybackState; +import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -58,40 +59,9 @@ import java.util.Map; /** * Loads ads using the IMA SDK. All methods are called on the main thread. */ -public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, +public final class ImaAdsLoader implements AdsLoader, Player.EventListener, VideoAdPlayer, ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener { - /** - * Listener for ad loader events. All methods are called on the main thread. - */ - /* package */ interface EventListener { - - /** - * Called when the ad playback state has been updated. - * - * @param adPlaybackState The new ad playback state. - */ - void onAdPlaybackState(AdPlaybackState adPlaybackState); - - /** - * Called when there was an error loading ads. - * - * @param error The error. - */ - void onLoadError(IOException error); - - /** - * Called when the user clicks through an ad (for example, following a 'learn more' link). - */ - void onAdClicked(); - - /** - * Called when the user taps a non-clickthrough part of an ad. - */ - void onAdTapped(); - - } - static { ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); } @@ -126,7 +96,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, private final List adCallbacks; private final ImaSdkFactory imaSdkFactory; private final AdDisplayContainer adDisplayContainer; - private final AdsLoader adsLoader; + private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; private EventListener eventListener; private Player player; @@ -160,7 +130,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, */ private boolean imaPausedInAd; /** - * Whether {@link AdsLoader#contentComplete()} has been called since starting ad playback. + * Whether {@link com.google.ads.interactivemedia.v3.api.AdsLoader#contentComplete()} has been + * called since starting ad playback. */ private boolean sentContentComplete; @@ -248,15 +219,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, 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. - */ - /* package */ void attachPlayer(ExoPlayer player, EventListener eventListener, - ViewGroup adUiViewGroup) { + @Override + public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) { this.player = player; this.eventListener = eventListener; this.adUiViewGroup = adUiViewGroup; @@ -265,7 +229,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, adDisplayContainer.setAdContainer(adUiViewGroup); player.addListener(this); if (adPlaybackState != null) { - eventListener.onAdPlaybackState(adPlaybackState); + eventListener.onAdPlaybackState(adPlaybackState.copy()); if (imaPausedContent) { adsManager.resume(); } @@ -274,12 +238,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, } } - /** - * Detaches the 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. - */ - /* package */ void detachPlayer() { + @Override + public void detachPlayer() { if (adsManager != null && imaPausedContent) { adPlaybackState.setAdResumePositionUs(C.msToUs(player.getCurrentPosition())); adsManager.pause(); @@ -292,9 +252,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, adUiViewGroup = null; } - /** - * Releases the loader. Must be called when the instance is no longer needed. - */ + @Override public void release() { released = true; if (adsManager != null) { 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 d56a3ad41f..c3574c414b 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 @@ -16,83 +16,25 @@ package com.google.android.exoplayer2.ext.ima; import android.os.Handler; -import android.os.Looper; import android.support.annotation.Nullable; -import android.util.Log; import android.view.ViewGroup; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; /** - * A {@link MediaSource} that inserts ads linearly with a provided content media source using the - * Interactive Media Ads SDK for ad loading and tracking. + * A {@link MediaSource} that inserts ads linearly with a provided content media source. + * + * @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader. */ +@Deprecated public final class ImaAdsMediaSource implements MediaSource { - /** - * Listener for events relating to ad loading. - */ - public interface AdsListener { - - /** - * Called if there was an error loading ads. The media source will load the content without ads - * if ads can't be loaded, so listen for this event if you need to implement additional handling - * (for example, stopping the player). - * - * @param error The error. - */ - void onAdLoadError(IOException error); - - /** - * Called when the user clicks through an ad (for example, following a 'learn more' link). - */ - void onAdClicked(); - - /** - * Called when the user taps a non-clickthrough part of an ad. - */ - void onAdTapped(); - - } - - private static final String TAG = "ImaAdsMediaSource"; - - private final MediaSource contentMediaSource; - private final DataSource.Factory dataSourceFactory; - private final ImaAdsLoader imaAdsLoader; - private final ViewGroup adUiViewGroup; - private final Handler mainHandler; - private final AdsLoaderListener adsLoaderListener; - private final Map adMediaSourceByMediaPeriod; - private final Timeline.Period period; - @Nullable - private final Handler eventHandler; - @Nullable - private final AdsListener eventListener; - - private Handler playerHandler; - private ExoPlayer player; - private volatile boolean released; - - // Accessed on the player thread. - private Timeline contentTimeline; - private Object contentManifest; - private AdPlaybackState adPlaybackState; - private MediaSource[][] adGroupMediaSources; - private long[][] adDurationsUs; - private MediaSource.Listener listener; + private final AdsMediaSource adsMediaSource; /** * Constructs a new source that inserts ads linearly with the content specified by @@ -121,230 +63,34 @@ public final class ImaAdsMediaSource implements MediaSource { */ public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, ImaAdsLoader imaAdsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler, - @Nullable AdsListener eventListener) { - this.contentMediaSource = contentMediaSource; - this.dataSourceFactory = dataSourceFactory; - this.imaAdsLoader = imaAdsLoader; - this.adUiViewGroup = adUiViewGroup; - this.eventHandler = eventHandler; - this.eventListener = eventListener; - mainHandler = new Handler(Looper.getMainLooper()); - adsLoaderListener = new AdsLoaderListener(); - adMediaSourceByMediaPeriod = new HashMap<>(); - period = new Timeline.Period(); - adGroupMediaSources = new MediaSource[0][]; - adDurationsUs = new long[0][]; + @Nullable AdsMediaSource.AdsListener eventListener) { + adsMediaSource = new AdsMediaSource(contentMediaSource, dataSourceFactory, imaAdsLoader, + adUiViewGroup, eventHandler, eventListener); } @Override public void prepareSource(final ExoPlayer player, boolean isTopLevelSource, Listener listener) { - Assertions.checkArgument(isTopLevelSource); - this.listener = listener; - this.player = player; - playerHandler = new Handler(); - 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); - } - }); + adsMediaSource.prepareSource(player, isTopLevelSource, listener); } @Override public void maybeThrowSourceInfoRefreshError() throws IOException { - contentMediaSource.maybeThrowSourceInfoRefreshError(); - for (MediaSource[] mediaSources : adGroupMediaSources) { - for (MediaSource mediaSource : mediaSources) { - if (mediaSource != null) { - mediaSource.maybeThrowSourceInfoRefreshError(); - } - } - } + adsMediaSource.maybeThrowSourceInfoRefreshError(); } @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - if (adPlaybackState.adGroupCount > 0 && id.isAd()) { - final int adGroupIndex = id.adGroupIndex; - final int adIndexInAdGroup = id.adIndexInAdGroup; - if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { - MediaSource adMediaSource = new ExtractorMediaSource( - adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup], dataSourceFactory, - new DefaultExtractorsFactory(), mainHandler, adsLoaderListener); - int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; - if (adIndexInAdGroup >= oldAdCount) { - int adCount = adIndexInAdGroup + 1; - adGroupMediaSources[adGroupIndex] = - Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount); - adDurationsUs[adGroupIndex] = Arrays.copyOf(adDurationsUs[adGroupIndex], adCount); - Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET); - } - adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource; - adMediaSource.prepareSource(player, false, new Listener() { - @Override - public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - onAdSourceInfoRefreshed(adGroupIndex, adIndexInAdGroup, timeline); - } - }); - } - MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; - MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), allocator); - adMediaSourceByMediaPeriod.put(mediaPeriod, mediaSource); - return mediaPeriod; - } else { - return contentMediaSource.createPeriod(id, allocator); - } + return adsMediaSource.createPeriod(id, allocator); } @Override public void releasePeriod(MediaPeriod mediaPeriod) { - if (adMediaSourceByMediaPeriod.containsKey(mediaPeriod)) { - adMediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod); - } else { - contentMediaSource.releasePeriod(mediaPeriod); - } + adsMediaSource.releasePeriod(mediaPeriod); } @Override public void releaseSource() { - released = true; - contentMediaSource.releaseSource(); - for (MediaSource[] mediaSources : adGroupMediaSources) { - for (MediaSource mediaSource : mediaSources) { - if (mediaSource != null) { - mediaSource.releaseSource(); - } - } - } - mainHandler.post(new Runnable() { - @Override - public void run() { - imaAdsLoader.detachPlayer(); - } - }); - } - - // Internal methods. - - private void onAdPlaybackState(AdPlaybackState adPlaybackState) { - if (this.adPlaybackState == null) { - adGroupMediaSources = new MediaSource[adPlaybackState.adGroupCount][]; - Arrays.fill(adGroupMediaSources, new MediaSource[0]); - adDurationsUs = new long[adPlaybackState.adGroupCount][]; - Arrays.fill(adDurationsUs, new long[0]); - } - this.adPlaybackState = adPlaybackState; - maybeUpdateSourceInfo(); - } - - private void onLoadError(final IOException error) { - Log.w(TAG, "Ad load error", error); - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - if (!released) { - eventListener.onAdLoadError(error); - } - } - }); - } - } - - private void onContentSourceInfoRefreshed(Timeline timeline, Object manifest) { - contentTimeline = timeline; - contentManifest = manifest; - maybeUpdateSourceInfo(); - } - - private void onAdSourceInfoRefreshed(int adGroupIndex, int adIndexInAdGroup, Timeline timeline) { - Assertions.checkArgument(timeline.getPeriodCount() == 1); - adDurationsUs[adGroupIndex][adIndexInAdGroup] = timeline.getPeriod(0, period).getDurationUs(); - maybeUpdateSourceInfo(); - } - - private void maybeUpdateSourceInfo() { - if (adPlaybackState != null && contentTimeline != null) { - Timeline timeline = adPlaybackState.adGroupCount == 0 ? contentTimeline - : new SinglePeriodAdTimeline(contentTimeline, adPlaybackState.adGroupTimesUs, - adPlaybackState.adCounts, adPlaybackState.adsLoadedCounts, - adPlaybackState.adsPlayedCounts, adDurationsUs, adPlaybackState.adResumePositionUs); - listener.onSourceInfoRefreshed(timeline, contentManifest); - } - } - - /** - * Listener for ad loading events. All methods are called on the main thread. - */ - private final class AdsLoaderListener implements ImaAdsLoader.EventListener, - ExtractorMediaSource.EventListener { - - @Override - public void onAdPlaybackState(final AdPlaybackState adPlaybackState) { - if (released) { - return; - } - playerHandler.post(new Runnable() { - @Override - public void run() { - if (released) { - return; - } - ImaAdsMediaSource.this.onAdPlaybackState(adPlaybackState); - } - }); - } - - @Override - public void onLoadError(final IOException error) { - if (released) { - return; - } - playerHandler.post(new Runnable() { - @Override - public void run() { - if (released) { - return; - } - ImaAdsMediaSource.this.onLoadError(error); - } - }); - } - - @Override - public void onAdClicked() { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - if (!released) { - eventListener.onAdClicked(); - } - } - }); - } - } - - @Override - public void onAdTapped() { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - if (!released) { - eventListener.onAdTapped(); - } - } - }); - } - } - + adsMediaSource.releaseSource(); } } diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackState.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java similarity index 98% rename from extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackState.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java index 0edd7d6558..97c97dec8f 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackState.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.ext.ima; +package com.google.android.exoplayer2.source.ads; import android.net.Uri; import com.google.android.exoplayer2.C; @@ -22,7 +22,7 @@ import java.util.Arrays; /** * Represents the structure of ads to play and the state of loaded/played ads. */ -/* package */ final class AdPlaybackState { +public final class AdPlaybackState { /** * The number of ad groups. 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 new file mode 100644 index 0000000000..241750a21f --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.ads; + +import android.view.ViewGroup; +import com.google.android.exoplayer2.ExoPlayer; +import java.io.IOException; + +/** + * Interface for loaders of ads, which can be used with {@link AdsMediaSource}. + *

+ * Ad loaders notify the {@link AdsMediaSource} about events via {@link EventListener}. In + * particular, implementations must call {@link EventListener#onAdPlaybackState(AdPlaybackState)} + * 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 #attachPlayer(ExoPlayer, EventListener, ViewGroup)} 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 #detachPlayer()} will be called. Loaders should maintain any ad playback state + * in preparation for a later call to {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)}. If + * an ad is playing when the player is detached, store the current playback position via + * {@link AdPlaybackState#setAdResumePositionUs(long)}. + *

+ * If {@link EventListener#onAdPlaybackState(AdPlaybackState)} has been called, the implementation + * of {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)} should invoke the same listener to + * provide the existing playback state to the new player. + */ +public interface AdsLoader { + + /** + * Listener for ad loader events. All methods are called on the main thread. + */ + interface EventListener { + + /** + * Called when the ad playback state has been updated. + * + * @param adPlaybackState The new ad playback state. + */ + void onAdPlaybackState(AdPlaybackState adPlaybackState); + + /** + * Called when there was an error loading ads. + * + * @param error The error. + */ + void onLoadError(IOException error); + + /** + * Called when the user clicks through an ad (for example, following a 'learn more' link). + */ + void onAdClicked(); + + /** + * Called when the user taps a non-clickthrough part of an ad. + */ + void onAdTapped(); + + } + + /** + * Attaches a player that will play ads loaded using this instance. Called on the main thread by + * {@link AdsMediaSource}. + * + * @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. + */ + void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup); + + /** + * Detaches the attached player and event listener. Called on the main thread by + * {@link AdsMediaSource}. + */ + void detachPlayer(); + + /** + * Releases the loader. Called by the application on the main thread when the instance is no + * longer needed. + */ + void release(); + +} 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 new file mode 100644 index 0000000000..41a856f83f --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -0,0 +1,349 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.ads; + +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.Nullable; +import android.util.Log; +import android.view.ViewGroup; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * A {@link MediaSource} that inserts ads linearly with a provided content media source. + */ +public final class AdsMediaSource implements MediaSource { + + /** + * Listener for events relating to ad loading. + */ + public interface AdsListener { + + /** + * Called if there was an error loading ads. The media source will load the content without ads + * if ads can't be loaded, so listen for this event if you need to implement additional handling + * (for example, stopping the player). + * + * @param error The error. + */ + void onAdLoadError(IOException error); + + /** + * Called when the user clicks through an ad (for example, following a 'learn more' link). + */ + void onAdClicked(); + + /** + * Called when the user taps a non-clickthrough part of an ad. + */ + void onAdTapped(); + + } + + private static final String TAG = "AdsMediaSource"; + + private final MediaSource contentMediaSource; + private final DataSource.Factory dataSourceFactory; + private final AdsLoader adsLoader; + private final ViewGroup adUiViewGroup; + private final Handler mainHandler; + private final ComponentListener componentListener; + private final Map adMediaSourceByMediaPeriod; + private final Timeline.Period period; + @Nullable + private final Handler eventHandler; + @Nullable + private final AdsListener eventListener; + + private Handler playerHandler; + private ExoPlayer player; + private volatile boolean released; + + // Accessed on the player thread. + private Timeline contentTimeline; + private Object contentManifest; + private AdPlaybackState adPlaybackState; + private MediaSource[][] adGroupMediaSources; + private long[][] adDurationsUs; + private MediaSource.Listener listener; + + /** + * 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 adsLoader The loader for ads. + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + */ + public AdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, + AdsLoader adsLoader, ViewGroup adUiViewGroup) { + this(contentMediaSource, dataSourceFactory, adsLoader, adUiViewGroup, null, 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 adsLoader The loader for ads. + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public AdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, + AdsLoader adsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler, + @Nullable AdsListener eventListener) { + this.contentMediaSource = contentMediaSource; + this.dataSourceFactory = dataSourceFactory; + this.adsLoader = adsLoader; + this.adUiViewGroup = adUiViewGroup; + this.eventHandler = eventHandler; + this.eventListener = eventListener; + mainHandler = new Handler(Looper.getMainLooper()); + componentListener = new ComponentListener(); + adMediaSourceByMediaPeriod = new HashMap<>(); + period = new Timeline.Period(); + adGroupMediaSources = new MediaSource[0][]; + adDurationsUs = new long[0][]; + } + + @Override + public void prepareSource(final ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Assertions.checkArgument(isTopLevelSource); + this.listener = listener; + this.player = player; + playerHandler = new Handler(); + contentMediaSource.prepareSource(player, false, new Listener() { + @Override + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { + AdsMediaSource.this.onContentSourceInfoRefreshed(timeline, manifest); + } + }); + mainHandler.post(new Runnable() { + @Override + public void run() { + adsLoader.attachPlayer(player, componentListener, adUiViewGroup); + } + }); + } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + contentMediaSource.maybeThrowSourceInfoRefreshError(); + for (MediaSource[] mediaSources : adGroupMediaSources) { + for (MediaSource mediaSource : mediaSources) { + if (mediaSource != null) { + mediaSource.maybeThrowSourceInfoRefreshError(); + } + } + } + } + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + if (adPlaybackState.adGroupCount > 0 && id.isAd()) { + final int adGroupIndex = id.adGroupIndex; + final int adIndexInAdGroup = id.adIndexInAdGroup; + if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { + MediaSource adMediaSource = new ExtractorMediaSource( + adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup], dataSourceFactory, + new DefaultExtractorsFactory(), mainHandler, componentListener); + int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; + if (adIndexInAdGroup >= oldAdCount) { + int adCount = adIndexInAdGroup + 1; + adGroupMediaSources[adGroupIndex] = + Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount); + adDurationsUs[adGroupIndex] = Arrays.copyOf(adDurationsUs[adGroupIndex], adCount); + Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET); + } + adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource; + adMediaSource.prepareSource(player, false, new Listener() { + @Override + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { + onAdSourceInfoRefreshed(adGroupIndex, adIndexInAdGroup, timeline); + } + }); + } + MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; + MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), allocator); + adMediaSourceByMediaPeriod.put(mediaPeriod, mediaSource); + return mediaPeriod; + } else { + return contentMediaSource.createPeriod(id, allocator); + } + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + if (adMediaSourceByMediaPeriod.containsKey(mediaPeriod)) { + adMediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod); + } else { + contentMediaSource.releasePeriod(mediaPeriod); + } + } + + @Override + public void releaseSource() { + released = true; + contentMediaSource.releaseSource(); + for (MediaSource[] mediaSources : adGroupMediaSources) { + for (MediaSource mediaSource : mediaSources) { + if (mediaSource != null) { + mediaSource.releaseSource(); + } + } + } + mainHandler.post(new Runnable() { + @Override + public void run() { + adsLoader.detachPlayer(); + } + }); + } + + // Internal methods. + + private void onAdPlaybackState(AdPlaybackState adPlaybackState) { + if (this.adPlaybackState == null) { + adGroupMediaSources = new MediaSource[adPlaybackState.adGroupCount][]; + Arrays.fill(adGroupMediaSources, new MediaSource[0]); + adDurationsUs = new long[adPlaybackState.adGroupCount][]; + Arrays.fill(adDurationsUs, new long[0]); + } + this.adPlaybackState = adPlaybackState; + maybeUpdateSourceInfo(); + } + + private void onLoadError(final IOException error) { + Log.w(TAG, "Ad load error", error); + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + if (!released) { + eventListener.onAdLoadError(error); + } + } + }); + } + } + + private void onContentSourceInfoRefreshed(Timeline timeline, Object manifest) { + contentTimeline = timeline; + contentManifest = manifest; + maybeUpdateSourceInfo(); + } + + private void onAdSourceInfoRefreshed(int adGroupIndex, int adIndexInAdGroup, Timeline timeline) { + Assertions.checkArgument(timeline.getPeriodCount() == 1); + adDurationsUs[adGroupIndex][adIndexInAdGroup] = timeline.getPeriod(0, period).getDurationUs(); + maybeUpdateSourceInfo(); + } + + private void maybeUpdateSourceInfo() { + if (adPlaybackState != null && contentTimeline != null) { + Timeline timeline = adPlaybackState.adGroupCount == 0 ? contentTimeline + : new SinglePeriodAdTimeline(contentTimeline, adPlaybackState.adGroupTimesUs, + adPlaybackState.adCounts, adPlaybackState.adsLoadedCounts, + adPlaybackState.adsPlayedCounts, adDurationsUs, adPlaybackState.adResumePositionUs); + listener.onSourceInfoRefreshed(timeline, contentManifest); + } + } + + /** + * Listener for component events. All methods are called on the main thread. + */ + private final class ComponentListener implements AdsLoader.EventListener, + ExtractorMediaSource.EventListener { + + @Override + public void onAdPlaybackState(final AdPlaybackState adPlaybackState) { + if (released) { + return; + } + playerHandler.post(new Runnable() { + @Override + public void run() { + if (released) { + return; + } + AdsMediaSource.this.onAdPlaybackState(adPlaybackState); + } + }); + } + + @Override + public void onAdClicked() { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + if (!released) { + eventListener.onAdClicked(); + } + } + }); + } + } + + @Override + public void onAdTapped() { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + if (!released) { + eventListener.onAdTapped(); + } + } + }); + } + } + + @Override + public void onLoadError(final IOException error) { + if (released) { + return; + } + playerHandler.post(new Runnable() { + @Override + public void run() { + if (released) { + return; + } + AdsMediaSource.this.onLoadError(error); + } + }); + } + + } + +} diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java similarity index 98% rename from extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java index 0162d22c34..c2974681db 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.ext.ima; +package com.google.android.exoplayer2.source.ads; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline;