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
This commit is contained in:
andrewlewis 2017-09-14 10:34:22 -07:00 committed by Oliver Woodman
parent 0ad39c642d
commit 58293abc11
7 changed files with 486 additions and 349 deletions

View File

@ -56,6 +56,8 @@ import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource; 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.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.CookieHandler; import java.net.CookieHandler;
import java.net.CookieManager; import java.net.CookieManager;
import java.net.CookiePolicy; 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. // 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 Uri loadedAdTagUri;
private ViewGroup adOverlayViewGroup; private ViewGroup adUiViewGroup;
// Activity lifecycle // 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. // 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. // 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"); Class<?> loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader");
if (imaAdsLoader == null) { if (adsLoader == null) {
imaAdsLoader = loaderClass.getConstructor(Context.class, Uri.class) adsLoader = (AdsLoader) loaderClass.getConstructor(Context.class, Uri.class)
.newInstance(this, adTagUri); .newInstance(this, adTagUri);
adOverlayViewGroup = new FrameLayout(this); adUiViewGroup = new FrameLayout(this);
// The demo app has a non-null overlay frame layout. // The demo app has a non-null overlay frame layout.
simpleExoPlayerView.getOverlayFrameLayout().addView(adOverlayViewGroup); simpleExoPlayerView.getOverlayFrameLayout().addView(adUiViewGroup);
} }
Class<?> sourceClass = return new AdsMediaSource(mediaSource, mediaDataSourceFactory, adsLoader, adUiViewGroup);
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() { private void releaseAdsLoader() {
if (imaAdsLoader != null) { if (adsLoader != null) {
try { adsLoader.release();
Class<?> loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader"); adsLoader = null;
Method releaseMethod = loaderClass.getMethod("release");
releaseMethod.invoke(imaAdsLoader);
} catch (Exception e) {
// Should never happen.
throw new IllegalStateException(e);
}
imaAdsLoader = null;
loadedAdTagUri = null; loadedAdTagUri = null;
simpleExoPlayerView.getOverlayFrameLayout().removeAllViews(); simpleExoPlayerView.getOverlayFrameLayout().removeAllViews();
} }

View File

@ -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.AdEventListener;
import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType; import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType;
import com.google.ads.interactivemedia.v3.api.AdPodInfo; 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.AdsLoader.AdsLoadedListener;
import com.google.ads.interactivemedia.v3.api.AdsManager; import com.google.ads.interactivemedia.v3.api.AdsManager;
import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; 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.Player;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.TrackGroupArray; 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.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException; 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. * 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 { 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 { static {
ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); ExoPlayerLibraryInfo.registerModule("goog.exo.ima");
} }
@ -126,7 +96,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
private final List<VideoAdPlayerCallback> adCallbacks; private final List<VideoAdPlayerCallback> adCallbacks;
private final ImaSdkFactory imaSdkFactory; private final ImaSdkFactory imaSdkFactory;
private final AdDisplayContainer adDisplayContainer; private final AdDisplayContainer adDisplayContainer;
private final AdsLoader adsLoader; private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
private EventListener eventListener; private EventListener eventListener;
private Player player; private Player player;
@ -160,7 +130,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
*/ */
private boolean imaPausedInAd; 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; private boolean sentContentComplete;
@ -248,15 +219,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
contentDurationMs = C.TIME_UNSET; contentDurationMs = C.TIME_UNSET;
} }
/** @Override
* Attaches a player that will play ads loaded using this instance. public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) {
*
* @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) {
this.player = player; this.player = player;
this.eventListener = eventListener; this.eventListener = eventListener;
this.adUiViewGroup = adUiViewGroup; this.adUiViewGroup = adUiViewGroup;
@ -265,7 +229,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
adDisplayContainer.setAdContainer(adUiViewGroup); adDisplayContainer.setAdContainer(adUiViewGroup);
player.addListener(this); player.addListener(this);
if (adPlaybackState != null) { if (adPlaybackState != null) {
eventListener.onAdPlaybackState(adPlaybackState); eventListener.onAdPlaybackState(adPlaybackState.copy());
if (imaPausedContent) { if (imaPausedContent) {
adsManager.resume(); adsManager.resume();
} }
@ -274,12 +238,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
} }
} }
/** @Override
* Detaches the attached player and event listener. To attach a new player, call public void detachPlayer() {
* {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)}. Call {@link #release()} to release
* all resources associated with this instance.
*/
/* package */ void detachPlayer() {
if (adsManager != null && imaPausedContent) { if (adsManager != null && imaPausedContent) {
adPlaybackState.setAdResumePositionUs(C.msToUs(player.getCurrentPosition())); adPlaybackState.setAdResumePositionUs(C.msToUs(player.getCurrentPosition()));
adsManager.pause(); adsManager.pause();
@ -292,9 +252,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
adUiViewGroup = null; adUiViewGroup = null;
} }
/** @Override
* Releases the loader. Must be called when the instance is no longer needed.
*/
public void release() { public void release() {
released = true; released = true;
if (adsManager != null) { if (adsManager != null) {

View File

@ -16,83 +16,25 @@
package com.google.android.exoplayer2.ext.ima; package com.google.android.exoplayer2.ext.ima;
import android.os.Handler; import android.os.Handler;
import android.os.Looper;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer; 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.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource; 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.Allocator;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException; 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 * A {@link MediaSource} that inserts ads linearly with a provided content media source.
* Interactive Media Ads SDK for ad loading and tracking. *
* @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader.
*/ */
@Deprecated
public final class ImaAdsMediaSource implements MediaSource { public final class ImaAdsMediaSource implements MediaSource {
/** private final AdsMediaSource adsMediaSource;
* 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<MediaPeriod, MediaSource> 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 * 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, public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory,
ImaAdsLoader imaAdsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler, ImaAdsLoader imaAdsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler,
@Nullable AdsListener eventListener) { @Nullable AdsMediaSource.AdsListener eventListener) {
this.contentMediaSource = contentMediaSource; adsMediaSource = new AdsMediaSource(contentMediaSource, dataSourceFactory, imaAdsLoader,
this.dataSourceFactory = dataSourceFactory; adUiViewGroup, eventHandler, eventListener);
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][];
} }
@Override @Override
public void prepareSource(final ExoPlayer player, boolean isTopLevelSource, Listener listener) { public void prepareSource(final ExoPlayer player, boolean isTopLevelSource, Listener listener) {
Assertions.checkArgument(isTopLevelSource); adsMediaSource.prepareSource(player, isTopLevelSource, listener);
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);
}
});
} }
@Override @Override
public void maybeThrowSourceInfoRefreshError() throws IOException { public void maybeThrowSourceInfoRefreshError() throws IOException {
contentMediaSource.maybeThrowSourceInfoRefreshError(); adsMediaSource.maybeThrowSourceInfoRefreshError();
for (MediaSource[] mediaSources : adGroupMediaSources) {
for (MediaSource mediaSource : mediaSources) {
if (mediaSource != null) {
mediaSource.maybeThrowSourceInfoRefreshError();
}
}
}
} }
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
if (adPlaybackState.adGroupCount > 0 && id.isAd()) { return adsMediaSource.createPeriod(id, allocator);
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);
}
} }
@Override @Override
public void releasePeriod(MediaPeriod mediaPeriod) { public void releasePeriod(MediaPeriod mediaPeriod) {
if (adMediaSourceByMediaPeriod.containsKey(mediaPeriod)) { adsMediaSource.releasePeriod(mediaPeriod);
adMediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod);
} else {
contentMediaSource.releasePeriod(mediaPeriod);
}
} }
@Override @Override
public void releaseSource() { public void releaseSource() {
released = true; adsMediaSource.releaseSource();
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();
}
}
});
}
}
} }
} }

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer2.ext.ima; package com.google.android.exoplayer2.source.ads;
import android.net.Uri; import android.net.Uri;
import com.google.android.exoplayer2.C; 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. * 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. * The number of ad groups.

View File

@ -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}.
* <p>
* 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).
* <p>
* {@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)}.
* <p>
* 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();
}

View File

@ -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<MediaPeriod, MediaSource> 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);
}
});
}
}
}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * 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.C;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;