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;