Open source IMA extension

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=156312761
This commit is contained in:
andrewlewis 2017-05-17 08:54:09 -07:00 committed by Oliver Woodman
parent 4359d44331
commit cdac347f8f
12 changed files with 1628 additions and 2 deletions

View File

@ -52,6 +52,7 @@ dependencies {
compile project(':library-ui')
withExtensionsCompile project(path: ':extension-ffmpeg')
withExtensionsCompile project(path: ':extension-flac')
withExtensionsCompile project(path: ':extension-ima')
withExtensionsCompile project(path: ':extension-opus')
withExtensionsCompile project(path: ':extension-vp9')
}

View File

@ -452,5 +452,85 @@
]
}
]
},
{
"name": "IMA sample ad tags",
"samples": [
{
"name": "Single inline linear",
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator="
},
{
"name": "Single skippable inline",
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dskippablelinear&correlator="
},
{
"name": "Single redirect linear",
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirectlinear&correlator="
},
{
"name": "Single redirect error",
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&nofb=1&correlator="
},
{
"name": "Single redirect broken (fallback)",
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&correlator="
},
{
"name": "VMAP pre-roll",
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonly&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-roll + bumper",
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonlybumper&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP post-roll",
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonly&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP post-roll + bumper",
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonlybumper&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-, mid- and post-rolls, single ads",
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator="
},
{
"name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad",
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpod&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad",
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpod&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad (bumpers around all ad breaks)",
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpodbumper&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad (bumpers around all ad breaks)",
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpodbumper&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad",
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator="
}
]
}
]

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.demo;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
@ -26,7 +27,9 @@ import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
@ -69,6 +72,7 @@ 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.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
@ -92,6 +96,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
"com.google.android.exoplayer.demo.action.VIEW_LIST";
public static final String URI_LIST_EXTRA = "uri_list";
public static final String EXTENSION_LIST_EXTRA = "extension_list";
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
private static final CookieManager DEFAULT_COOKIE_MANAGER;
@ -312,6 +317,26 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
}
MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
: new ConcatenatingMediaSource(mediaSources);
String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA);
if (adTagUriString != null) {
Uri adTagUri = Uri.parse(adTagUriString);
ViewGroup adOverlayViewGroup = new FrameLayout(this);
// Load the extension source using reflection so that demo app doesn't have to depend on it.
try {
Class<?> clazz = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsMediaSource");
Constructor<?> constructor = clazz.getConstructor(MediaSource.class,
DataSource.Factory.class, Context.class, Uri.class, ViewGroup.class);
mediaSource = (MediaSource) constructor.newInstance(mediaSource,
mediaDataSourceFactory, this, adTagUri, adOverlayViewGroup);
// The demo app has a non-null overlay frame layout.
simpleExoPlayerView.getOverlayFrameLayout().addView(adOverlayViewGroup);
// Show a multi-window time bar, which will include ad break position markers.
simpleExoPlayerView.setShowMultiWindowTimeBar(true);
} catch (Exception e) {
// Throw if the media source class was not found, or there was an error instantiating it.
showToast(R.string.ima_not_loaded);
}
}
boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
if (haveResumePosition) {
player.seekTo(resumeWindow, resumePosition);

View File

@ -184,6 +184,7 @@ public class SampleChooserActivity extends Activity {
String[] drmKeyRequestProperties = null;
boolean preferExtensionDecoders = false;
ArrayList<UriSample> playlistSamples = null;
String adTagUri = null;
reader.beginObject();
while (reader.hasNext()) {
@ -233,6 +234,9 @@ public class SampleChooserActivity extends Activity {
}
reader.endArray();
break;
case "ad_tag_uri":
adTagUri = reader.nextString();
break;
default:
throw new ParserException("Unsupported attribute name: " + name);
}
@ -246,7 +250,7 @@ public class SampleChooserActivity extends Activity {
preferExtensionDecoders, playlistSamplesArray);
} else {
return new UriSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties,
preferExtensionDecoders, uri, extension);
preferExtensionDecoders, uri, extension, adTagUri);
}
}
@ -402,13 +406,15 @@ public class SampleChooserActivity extends Activity {
public final String uri;
public final String extension;
public final String adTagUri;
public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl,
String[] drmKeyRequestProperties, boolean preferExtensionDecoders, String uri,
String extension) {
String extension, String adTagUri) {
super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders);
this.uri = uri;
this.extension = extension;
this.adTagUri = adTagUri;
}
@Override
@ -416,6 +422,7 @@ public class SampleChooserActivity extends Activity {
return super.buildIntent(context)
.setData(Uri.parse(uri))
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
.putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri)
.setAction(PlayerActivity.ACTION_VIEW);
}

View File

@ -58,4 +58,6 @@
<string name="sample_list_load_error">One or more sample lists failed to load</string>
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
</resources>

31
extensions/ima/README.md Normal file
View File

@ -0,0 +1,31 @@
# ExoPlayer IMA extension #
## Description ##
The IMA extension is a [MediaSource][] implementation wrapping the
[Interactive Media Ads SDK for Android][IMA]. You can use it to insert ads
alongside content.
[IMA]: https://developers.google.com/interactive-media-ads/docs/sdks/android/
[MediaSource]: https://github.com/google/ExoPlayer/blob/release-v2/library/src/main/java/com/google/android/exoplayer2/source/MediaSource.java
## Using the extension ##
Pass a single-window content `MediaSource` to `ImaAdsMediaSource`'s constructor,
along with a `ViewGroup` that is on top of the player and the ad tag URI to
show. The IMA documentation includes some [sample ad tags][] for testing. Then
pass the `ImaAdsMediaSource` to `ExoPlayer.prepare`.
[sample ad tags]: https://developers.google.com/interactive-media-ads/docs/sdks/android/tags
## Known issues ##
This is a preview version with some known issues:
* Midroll ads are not yet fully supported. In particular, seeking with midroll
ads is not yet supported. Played ad periods are not removed. Also, `playAd` and
`AD_STARTED` events are sometimes delayed, meaning that midroll ads take a long
time to start and the ad overlay does not show immediately.
* Tapping the 'More info' button on an ad in the demo app will pause the
activity, which destroys the ImaAdsMediaSource. Played ad breaks will be
shown to the user again if the demo app returns to the foreground.

View File

@ -0,0 +1,35 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion 14
targetSdkVersion project.ext.targetSdkVersion
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
}
dependencies {
compile project(':library-core')
compile 'com.android.support:support-annotations:' + supportLibraryVersion
compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.6.0'
compile 'com.google.android.gms:play-services-ads:10.2.4'
// There exists a dependency chain:
// com.google.android.gms:play-services-ads:10.2.4
// |-> com.google.android.gms:play-services-ads-lite:10.2.4
// |-> com.google.android.gms:play-services-basement:10.2.4
// |-> com.android.support:support-v4:24.0.0
// The support-v4:24.0.0 module directly includes older versions of the same
// classes as com.android.support:support-annotations. We need to manually
// force it to the version we're using to avoid a compilation failure. This
// will become unnecessary when the support-v4 dependency in the chain above
// has been updated to 24.2.0 or later.
compile 'com.android.support:support-v4:' + supportLibraryVersion
androidTestCompile project(':library')
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
androidTestCompile 'com.android.support.test:runner:' + testSupportLibraryVersion
}

View File

@ -0,0 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.ext.ima">
<meta-data android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>
</manifest>

View File

@ -0,0 +1,275 @@
/*
* 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.ext.ima;
import android.util.Pair;
import com.google.ads.interactivemedia.v3.api.Ad;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
/**
* A {@link Timeline} for {@link ImaAdsMediaSource}.
*/
/* package */ final class AdTimeline extends Timeline {
private static final Object AD_ID = new Object();
/**
* Builder for ad timelines.
*/
public static final class Builder {
private final Timeline contentTimeline;
private final long contentDurationUs;
private final ArrayList<Boolean> isAd;
private final ArrayList<Ad> ads;
private final ArrayList<Long> startTimesUs;
private final ArrayList<Long> endTimesUs;
private final ArrayList<Object> uids;
/**
* Creates a new ad timeline builder using the specified {@code contentTimeline} as the timeline
* of the content within which to insert ad breaks.
*
* @param contentTimeline The timeline of the content within which to insert ad breaks.
*/
public Builder(Timeline contentTimeline) {
this.contentTimeline = contentTimeline;
contentDurationUs = contentTimeline.getPeriod(0, new Period()).durationUs;
isAd = new ArrayList<>();
ads = new ArrayList<>();
startTimesUs = new ArrayList<>();
endTimesUs = new ArrayList<>();
uids = new ArrayList<>();
}
/**
* Adds an ad period. Each individual ad in an ad pod is represented by a separate ad period.
*
* @param ad The {@link Ad} instance representing the ad break, or {@code null} if not known.
* @param adBreakIndex The index of the ad break that contains the ad in the timeline.
* @param adIndexInAdBreak The index of the ad in its ad break.
* @param durationUs The duration of the ad, in microseconds. May be {@link C#TIME_UNSET}.
* @return The builder.
*/
public Builder addAdPeriod(Ad ad, int adBreakIndex, int adIndexInAdBreak, long durationUs) {
isAd.add(true);
ads.add(ad);
startTimesUs.add(0L);
endTimesUs.add(durationUs);
uids.add(Pair.create(adBreakIndex, adIndexInAdBreak));
return this;
}
/**
* Adds a content period.
*
* @param startTimeUs The start time of the period relative to the start of the content
* timeline, in microseconds.
* @param endTimeUs The end time of the period relative to the start of the content timeline, in
* microseconds. May be {@link C#TIME_UNSET} to include the rest of the content.
* @return The builder.
*/
public Builder addContent(long startTimeUs, long endTimeUs) {
ads.add(null);
isAd.add(false);
startTimesUs.add(startTimeUs);
endTimesUs.add(endTimeUs == C.TIME_UNSET ? contentDurationUs : endTimeUs);
uids.add(Pair.create(startTimeUs, endTimeUs));
return this;
}
/**
* Builds and returns the ad timeline.
*/
public AdTimeline build() {
int periodCount = uids.size();
Assertions.checkState(periodCount > 0);
Ad[] ads = new Ad[periodCount];
boolean[] isAd = new boolean[periodCount];
long[] startTimesUs = new long[periodCount];
long[] endTimesUs = new long[periodCount];
for (int i = 0; i < periodCount; i++) {
ads[i] = this.ads.get(i);
isAd[i] = this.isAd.get(i);
startTimesUs[i] = this.startTimesUs.get(i);
endTimesUs[i] = this.endTimesUs.get(i);
}
Object[] uids = this.uids.toArray(new Object[periodCount]);
return new AdTimeline(contentTimeline, isAd, ads, startTimesUs, endTimesUs, uids);
}
}
private final Period contentPeriod;
private final Window contentWindow;
private final boolean[] isAd;
private final Ad[] ads;
private final long[] startTimesUs;
private final long[] endTimesUs;
private final Object[] uids;
private AdTimeline(Timeline contentTimeline, boolean[] isAd, Ad[] ads, long[] startTimesUs,
long[] endTimesUs, Object[] uids) {
contentWindow = contentTimeline.getWindow(0, new Window(), true);
contentPeriod = contentTimeline.getPeriod(0, new Period(), true);
this.isAd = isAd;
this.ads = ads;
this.startTimesUs = startTimesUs;
this.endTimesUs = endTimesUs;
this.uids = uids;
}
/**
* Returns whether the period at {@code index} contains ad media.
*/
public boolean isPeriodAd(int index) {
return isAd[index];
}
/**
* Returns the duration of the content within which ads have been inserted, in microseconds.
*/
public long getContentDurationUs() {
return contentPeriod.durationUs;
}
/**
* Returns the start time of the period at {@code periodIndex} relative to the start of the
* content, in microseconds.
*
* @throws IllegalArgumentException Thrown if the period at {@code periodIndex} is not a content
* period.
*/
public long getContentStartTimeUs(int periodIndex) {
Assertions.checkArgument(!isAd[periodIndex]);
return startTimesUs[periodIndex];
}
/**
* Returns the end time of the period at {@code periodIndex} relative to the start of the content,
* in microseconds.
*
* @throws IllegalArgumentException Thrown if the period at {@code periodIndex} is not a content
* period.
*/
public long getContentEndTimeUs(int periodIndex) {
Assertions.checkArgument(!isAd[periodIndex]);
return endTimesUs[periodIndex];
}
/**
* Returns the index of the ad break to which the period at {@code periodIndex} belongs.
*
* @param periodIndex The period index.
* @return The index of the ad break to which the period belongs.
* @throws IllegalArgumentException Thrown if the period at {@code periodIndex} is not an ad.
*/
public int getAdBreakIndex(int periodIndex) {
Assertions.checkArgument(isAd[periodIndex]);
int adBreakIndex = 0;
for (int i = 1; i < periodIndex; i++) {
if (!isAd[i] && isAd[i - 1]) {
adBreakIndex++;
}
}
return adBreakIndex;
}
/**
* Returns the index of the ad at {@code periodIndex} in its ad break.
*
* @param periodIndex The period index.
* @return The index of the ad at {@code periodIndex} in its ad break.
* @throws IllegalArgumentException Thrown if the period at {@code periodIndex} is not an ad.
*/
public int getAdIndexInAdBreak(int periodIndex) {
Assertions.checkArgument(isAd[periodIndex]);
int adIndex = 0;
for (int i = 0; i < periodIndex; i++) {
if (isAd[i]) {
adIndex++;
} else {
adIndex = 0;
}
}
return adIndex;
}
@Override
public int getWindowCount() {
return uids.length;
}
@Override
public int getNextWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) {
if (repeatMode == ExoPlayer.REPEAT_MODE_ONE) {
repeatMode = ExoPlayer.REPEAT_MODE_ALL;
}
return super.getNextWindowIndex(windowIndex, repeatMode);
}
@Override
public int getPreviousWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) {
if (repeatMode == ExoPlayer.REPEAT_MODE_ONE) {
repeatMode = ExoPlayer.REPEAT_MODE_ALL;
}
return super.getPreviousWindowIndex(windowIndex, repeatMode);
}
@Override
public Window getWindow(int index, Window window, boolean setIds,
long defaultPositionProjectionUs) {
long startTimeUs = startTimesUs[index];
long durationUs = endTimesUs[index] - startTimeUs;
if (isAd[index]) {
window.set(ads[index], C.TIME_UNSET, C.TIME_UNSET, false, false, 0L, durationUs, index, index,
0L);
} else {
window.set(contentWindow.id, contentWindow.presentationStartTimeMs + C.usToMs(startTimeUs),
contentWindow.windowStartTimeMs + C.usToMs(startTimeUs), contentWindow.isSeekable, false,
0L, durationUs, index, index, 0L);
}
return window;
}
@Override
public int getPeriodCount() {
return uids.length;
}
@Override
public Period getPeriod(int index, Period period, boolean setIds) {
Object id = setIds ? (isAd[index] ? AD_ID : contentPeriod.id) : null;
return period.set(id, uids[index], index, endTimesUs[index] - startTimesUs[index], 0,
isAd[index]);
}
@Override
public int getIndexOfPeriod(Object uid) {
for (int i = 0; i < uids.length; i++) {
if (Util.areEqual(uid, uids[i])) {
return i;
}
}
return C.INDEX_UNSET;
}
}

View File

@ -0,0 +1,633 @@
/*
* 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.ext.ima;
import android.content.Context;
import android.net.Uri;
import android.os.SystemClock;
import android.util.Log;
import android.view.ViewGroup;
import com.google.ads.interactivemedia.v3.api.Ad;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
import com.google.ads.interactivemedia.v3.api.AdErrorEvent;
import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener;
import com.google.ads.interactivemedia.v3.api.AdEvent;
import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener;
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;
import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
import com.google.ads.interactivemedia.v3.api.AdsRequest;
import com.google.ads.interactivemedia.v3.api.ImaSdkFactory;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider;
import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Loads ads using the IMA SDK. All methods are called on the main thread.
*/
/* package */ final class ImaAdsLoader implements ExoPlayer.EventListener, VideoAdPlayer,
ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener {
private static final boolean DEBUG = false;
private static final String TAG = "ImaAdsLoader";
/**
* Listener for ad loader events. All methods are called on the main thread.
*/
public interface EventListener {
/**
* Called when the timestamps of ad breaks are known.
*
* @param adBreakTimesUs The times of ad breaks, in microseconds.
*/
void onAdBreakTimesUsLoaded(long[] adBreakTimesUs);
/**
* Called when the URI for the media of an ad has been loaded.
*
* @param adBreakIndex The index of the ad break containing the ad with the media URI.
* @param adIndexInAdBreak The index of the ad in its ad break.
* @param uri The URI for the ad's media.
*/
void onUriLoaded(int adBreakIndex, int adIndexInAdBreak, Uri uri);
/**
* Called when the {@link Ad} instance for a specified ad has been loaded.
*
* @param adBreakIndex The index of the ad break containing the ad.
* @param adIndexInAdBreak The index of the ad in its ad break.
* @param ad The {@link Ad} instance for the ad.
*/
void onAdLoaded(int adBreakIndex, int adIndexInAdBreak, Ad ad);
/**
* Called when the specified ad break has been played to the end.
*
* @param adBreakIndex The index of the ad break.
*/
void onAdBreakPlayedToEnd(int adBreakIndex);
/**
* Called when there was an error loading ads.
*
* @param error The error.
*/
void onLoadError(IOException error);
}
/**
* Whether to enable preloading of ads in {@link AdsRenderingSettings}.
*/
private static final boolean ENABLE_PRELOADING = true;
private static final String IMA_SDK_SETTINGS_PLAYER_TYPE =
"google/com.google.android.exoplayer2.ext.ima";
private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION;
/**
* Threshold before the end of content at which IMA is notified that content is complete if the
* player buffers, in milliseconds.
*/
private static final long END_OF_CONTENT_POSITION_THRESHOLD_MS = 5000;
private final EventListener eventListener;
private final ExoPlayer player;
private final Timeline.Period period;
private final List<VideoAdPlayerCallback> adCallbacks;
private final AdsLoader adsLoader;
private AdsManager adsManager;
private AdTimeline adTimeline;
private long contentDurationMs;
private int lastContentPeriodIndex;
private int playerPeriodIndex;
private boolean released;
// Fields tracking IMA's state.
/**
* The index of the current ad break that IMA is loading.
*/
private int adBreakIndex;
/**
* The index of the ad within its ad break, in {@link #loadAd(String)}.
*/
private int adIndexInAdBreak;
/**
* The total number of ads in the current ad break, or {@link C#INDEX_UNSET} if unknown.
*/
private int adCountInAdBreak;
/**
* Tracks the period currently being played in IMA's model of playback.
*/
private int imaPeriodIndex;
/**
* Whether the period at {@link #imaPeriodIndex} is an ad.
*/
private boolean isAdDisplayed;
/**
* Whether {@link AdsLoader#contentComplete()} has been called since starting ad playback.
*/
private boolean sentContentComplete;
/**
* If {@link #isAdDisplayed} is set, stores whether IMA has called {@link #playAd()} and not
* {@link #stopAd()}.
*/
private boolean playingAd;
/**
* If {@link #isAdDisplayed} is set, stores whether IMA has called {@link #pauseAd()} since a
* preceding call to {@link #playAd()} for the current ad.
*/
private boolean pausedInAd;
/**
* If a content period has finished but IMA has not yet sent an ad event with
* {@link AdEvent.AdEventType#CONTENT_PAUSE_REQUESTED}, stores the value of
* {@link SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to
* determine a fake, increasing content position. {@link C#TIME_UNSET} otherwise.
*/
private long fakeContentProgressElapsedRealtimeMs;
/**
* Creates a new IMA ads loader.
*
* @param context The context.
* @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See
* https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
* more information.
* @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
* @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to
* use the default settings. If set, the player type and version fields may be overwritten.
* @param player The player instance that will play the loaded ad schedule. The player's timeline
* must be an {@link AdTimeline} matching the loaded ad schedule.
* @param eventListener Listener for ad loader events.
*/
public ImaAdsLoader(Context context, Uri adTagUri, ViewGroup adUiViewGroup,
ImaSdkSettings imaSdkSettings, ExoPlayer player, EventListener eventListener) {
this.eventListener = eventListener;
this.player = player;
period = new Timeline.Period();
adCallbacks = new ArrayList<>(1);
lastContentPeriodIndex = C.INDEX_UNSET;
adCountInAdBreak = C.INDEX_UNSET;
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
player.addListener(this);
ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance();
AdDisplayContainer adDisplayContainer = imaSdkFactory.createAdDisplayContainer();
adDisplayContainer.setPlayer(this);
adDisplayContainer.setAdContainer(adUiViewGroup);
if (imaSdkSettings == null) {
imaSdkSettings = imaSdkFactory.createImaSdkSettings();
}
imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE);
imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION);
AdsRequest request = imaSdkFactory.createAdsRequest();
request.setAdTagUrl(adTagUri.toString());
request.setAdDisplayContainer(adDisplayContainer);
request.setContentProgressProvider(this);
adsLoader = imaSdkFactory.createAdsLoader(context, imaSdkSettings);
adsLoader.addAdErrorListener(this);
adsLoader.addAdsLoadedListener(this);
adsLoader.requestAds(request);
}
/**
* Releases the loader. Must be called when the instance is no longer needed.
*/
public void release() {
if (adsManager != null) {
adsManager.destroy();
adsManager = null;
}
player.removeListener(this);
released = true;
}
// AdsLoader.AdsLoadedListener implementation.
@Override
public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) {
adsManager = adsManagerLoadedEvent.getAdsManager();
adsManager.addAdErrorListener(this);
adsManager.addAdEventListener(this);
if (ENABLE_PRELOADING) {
ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance();
AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings();
adsRenderingSettings.setEnablePreloading(true);
adsManager.init(adsRenderingSettings);
if (DEBUG) {
Log.d(TAG, "Initialized with preloading");
}
} else {
adsManager.init();
if (DEBUG) {
Log.d(TAG, "Initialized without preloading");
}
}
eventListener.onAdBreakTimesUsLoaded(getAdBreakTimesUs(adsManager.getAdCuePoints()));
}
// AdEvent.AdEventListener implementation.
@Override
public void onAdEvent(AdEvent adEvent) {
if (DEBUG) {
Log.d(TAG, "onAdEvent " + adEvent.getType());
}
if (released) {
// The ads manager may pass CONTENT_RESUME_REQUESTED after it is destroyed.
return;
}
switch (adEvent.getType()) {
case LOADED:
adsManager.start();
break;
case STARTED:
// Note: This event is sometimes delivered several seconds after playAd is called.
// See [Internal: b/37775441].
Ad ad = adEvent.getAd();
AdPodInfo adPodInfo = ad.getAdPodInfo();
adCountInAdBreak = adPodInfo.getTotalAds();
int adPosition = adPodInfo.getAdPosition();
eventListener.onAdLoaded(adBreakIndex, adPosition - 1, ad);
if (DEBUG) {
Log.d(TAG, "Started ad " + adPosition + " of " + adCountInAdBreak + " in ad break "
+ adBreakIndex);
}
break;
case CONTENT_PAUSE_REQUESTED:
// After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads
// before sending CONTENT_RESUME_REQUESTED.
pauseContentInternal();
break;
case SKIPPED: // Fall through.
case CONTENT_RESUME_REQUESTED:
resumeContentInternal();
break;
case ALL_ADS_COMPLETED:
// Do nothing. The ads manager will be released when the source is released.
default:
break;
}
}
// AdErrorEvent.AdErrorListener implementation.
@Override
public void onAdError(AdErrorEvent adErrorEvent) {
if (DEBUG) {
Log.d(TAG, "onAdError " + adErrorEvent);
}
IOException exception = new IOException("Ad error: " + adErrorEvent, adErrorEvent.getError());
eventListener.onLoadError(exception);
// TODO: Provide a timeline to the player if it doesn't have one yet, so the content can play.
}
// ContentProgressProvider implementation.
@Override
public VideoProgressUpdate getContentProgress() {
if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {
long contentEndTimeMs = C.usToMs(adTimeline.getContentEndTimeUs(imaPeriodIndex));
long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
return new VideoProgressUpdate(contentEndTimeMs + elapsedSinceEndMs, contentDurationMs);
}
if (adTimeline == null || isAdDisplayed || imaPeriodIndex != playerPeriodIndex
|| contentDurationMs == C.TIME_UNSET) {
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
}
checkForContentComplete();
long positionMs = C.usToMs(adTimeline.getContentStartTimeUs(imaPeriodIndex))
+ player.getCurrentPosition();
return new VideoProgressUpdate(positionMs, contentDurationMs);
}
// VideoAdPlayer implementation.
@Override
public VideoProgressUpdate getAdProgress() {
if (adTimeline == null || !isAdDisplayed || imaPeriodIndex != playerPeriodIndex
|| adTimeline.getPeriod(imaPeriodIndex, period).getDurationUs() == C.TIME_UNSET) {
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
}
return new VideoProgressUpdate(player.getCurrentPosition(), period.getDurationMs());
}
@Override
public void loadAd(String adUriString) {
if (DEBUG) {
Log.d(TAG, "loadAd at index " + adIndexInAdBreak + " in ad break " + adBreakIndex);
}
eventListener.onUriLoaded(adBreakIndex, adIndexInAdBreak, Uri.parse(adUriString));
adIndexInAdBreak++;
}
@Override
public void addCallback(VideoAdPlayerCallback videoAdPlayerCallback) {
adCallbacks.add(videoAdPlayerCallback);
}
@Override
public void removeCallback(VideoAdPlayerCallback videoAdPlayerCallback) {
adCallbacks.remove(videoAdPlayerCallback);
}
@Override
public void playAd() {
if (DEBUG) {
Log.d(TAG, "playAd");
}
Assertions.checkState(isAdDisplayed);
if (playingAd && !pausedInAd) {
// Work around an issue where IMA does not always call stopAd before resuming content.
// See [Internal: b/38354028].
if (DEBUG) {
Log.d(TAG, "Unexpected playAd without stopAd");
}
stopAdInternal();
}
player.setPlayWhenReady(true);
if (!playingAd) {
playingAd = true;
for (VideoAdPlayerCallback callback : adCallbacks) {
callback.onPlay();
}
} else if (pausedInAd) {
pausedInAd = false;
for (VideoAdPlayerCallback callback : adCallbacks) {
callback.onResume();
}
}
}
@Override
public void stopAd() {
if (!playingAd) {
if (DEBUG) {
Log.d(TAG, "Ignoring unexpected stopAd");
}
return;
}
if (DEBUG) {
Log.d(TAG, "stopAd");
}
stopAdInternal();
}
@Override
public void pauseAd() {
if (DEBUG) {
Log.d(TAG, "pauseAd");
}
if (released || !playingAd) {
// This method is called after content is resumed, and may also be called after release.
return;
}
pausedInAd = true;
player.setPlayWhenReady(false);
for (VideoAdPlayerCallback callback : adCallbacks) {
callback.onPause();
}
}
@Override
public void resumeAd() {
// This method is never called. See [Internal: b/18931719].
throw new IllegalStateException();
}
// ExoPlayer.EventListener implementation.
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
if (timeline.isEmpty()) {
// The player is being re-prepared and this source will be released.
return;
}
if (adTimeline == null) {
// TODO: Handle initial seeks after the first period.
isAdDisplayed = timeline.getPeriod(0, period).isAd;
imaPeriodIndex = 0;
player.seekTo(0, 0);
}
adTimeline = (AdTimeline) timeline;
contentDurationMs = C.usToMs(adTimeline.getContentDurationUs());
lastContentPeriodIndex = adTimeline.getPeriodCount() - 1;
while (adTimeline.isPeriodAd(lastContentPeriodIndex)) {
// All timelines have at least one content period.
lastContentPeriodIndex--;
}
}
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
// Do nothing.
}
@Override
public void onLoadingChanged(boolean isLoading) {
// Do nothing.
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (playbackState == ExoPlayer.STATE_BUFFERING && playWhenReady) {
checkForContentComplete();
} else if (playbackState == ExoPlayer.STATE_ENDED && isAdDisplayed) {
// IMA is waiting for the ad playback to finish so invoke the callback now.
// Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again.
for (VideoAdPlayerCallback callback : adCallbacks) {
callback.onEnded();
}
}
}
@Override
public void onRepeatModeChanged(int repeatMode) {
// Do nothing.
}
@Override
public void onPlayerError(ExoPlaybackException error) {
if (isAdDisplayed && adTimeline.isPeriodAd(playerPeriodIndex)) {
for (VideoAdPlayerCallback callback : adCallbacks) {
callback.onError();
}
}
}
@Override
public void onPositionDiscontinuity() {
if (player.getCurrentPeriodIndex() == playerPeriodIndex + 1) {
if (isAdDisplayed) {
// IMA is waiting for the ad playback to finish so invoke the callback now.
// Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again.
for (VideoAdPlayerCallback callback : adCallbacks) {
callback.onEnded();
}
} else {
player.setPlayWhenReady(false);
if (imaPeriodIndex == playerPeriodIndex) {
// IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position.
Assertions.checkState(fakeContentProgressElapsedRealtimeMs == C.TIME_UNSET);
fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime();
}
}
}
playerPeriodIndex = player.getCurrentPeriodIndex();
}
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
// Do nothing.
}
// Internal methods.
/**
* Resumes the player, ensuring the current period is a content period by seeking if necessary.
*/
private void resumeContentInternal() {
if (adTimeline != null) {
if (imaPeriodIndex < lastContentPeriodIndex) {
if (playingAd) {
// Work around an issue where IMA does not always call stopAd before resuming content.
// See [Internal: b/38354028].
if (DEBUG) {
Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd");
}
stopAdInternal();
}
while (adTimeline.isPeriodAd(imaPeriodIndex)) {
imaPeriodIndex++;
}
synchronizePlayerToIma();
}
}
player.setPlayWhenReady(true);
}
/**
* Pauses the player, and ensures that the current period is an ad period by seeking if necessary.
*/
private void pauseContentInternal() {
// IMA is requesting to pause content, so stop faking the content position.
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
if (adTimeline != null && !isAdDisplayed) {
// Seek to the next ad.
while (!adTimeline.isPeriodAd(imaPeriodIndex)) {
imaPeriodIndex++;
}
synchronizePlayerToIma();
} else {
// IMA is sending an initial CONTENT_PAUSE_REQUESTED before a pre-roll ad.
Assertions.checkState(playerPeriodIndex == 0 && imaPeriodIndex == 0);
}
player.setPlayWhenReady(false);
}
/**
* Stops the currently playing ad, seeking to the next content period if there is one. May only be
* called when {@link #playingAd} is {@code true}.
*/
private void stopAdInternal() {
Assertions.checkState(playingAd);
if (imaPeriodIndex != adTimeline.getPeriodCount() - 1) {
player.setPlayWhenReady(false);
imaPeriodIndex++;
if (!adTimeline.isPeriodAd(imaPeriodIndex)) {
eventListener.onAdBreakPlayedToEnd(adBreakIndex);
adBreakIndex++;
adIndexInAdBreak = 0;
}
synchronizePlayerToIma();
} else {
eventListener.onAdBreakPlayedToEnd(adTimeline.getAdBreakIndex(imaPeriodIndex));
}
}
private void synchronizePlayerToIma() {
if (playerPeriodIndex != imaPeriodIndex) {
player.seekTo(imaPeriodIndex, 0);
}
isAdDisplayed = adTimeline.isPeriodAd(imaPeriodIndex);
// If an ad is displayed, these flags will be updated in response to playAd/pauseAd/stopAd until
// the content is resumed.
playingAd = false;
pausedInAd = false;
}
private void checkForContentComplete() {
if (adTimeline == null || isAdDisplayed || sentContentComplete) {
return;
}
long positionMs = C.usToMs(adTimeline.getContentStartTimeUs(imaPeriodIndex))
+ player.getCurrentPosition();
if (playerPeriodIndex == lastContentPeriodIndex
&& positionMs + END_OF_CONTENT_POSITION_THRESHOLD_MS
>= C.usToMs(adTimeline.getContentEndTimeUs(playerPeriodIndex))) {
adsLoader.contentComplete();
if (DEBUG) {
Log.d(TAG, "adsLoader.contentComplete");
}
sentContentComplete = true;
}
}
private static long[] getAdBreakTimesUs(List<Float> cuePoints) {
if (cuePoints.isEmpty()) {
// If no cue points are specified, there is a preroll ad break.
return new long[] {0};
}
int count = cuePoints.size();
long[] adBreakTimesUs = new long[count];
for (int i = 0; i < count; i++) {
double cuePoint = cuePoints.get(i);
adBreakTimesUs[i] = cuePoint == -1.0 ? C.TIME_UNSET : (long) (C.MICROS_PER_SECOND * cuePoint);
}
return adBreakTimesUs;
}
}

View File

@ -0,0 +1,530 @@
/*
* 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.ext.ima;
import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.view.ViewGroup;
import com.google.ads.interactivemedia.v3.api.Ad;
import com.google.ads.interactivemedia.v3.api.AdPodInfo;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.source.ClippingMediaPeriod;
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.SampleStream;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection;
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.
*/
public final class ImaAdsMediaSource implements MediaSource {
private final MediaSource contentMediaSource;
private final DataSource.Factory dataSourceFactory;
private final Context context;
private final Uri adTagUri;
private final ViewGroup adUiViewGroup;
private final ImaSdkSettings imaSdkSettings;
private final Handler mainHandler;
private final AdListener adLoaderListener;
private final Map<MediaPeriod, MediaSource> mediaSourceByMediaPeriod;
private Handler playerHandler;
private ExoPlayer player;
private volatile boolean released;
// Accessed on the player thread.
private Timeline contentTimeline;
private Object contentManifest;
private long[] adBreakTimesUs;
private boolean[] playedAdBreak;
private Ad[][] adBreakAds;
private Timeline[][] adBreakTimelines;
private MediaSource[][] adBreakMediaSources;
private DeferredMediaPeriod[][] adBreakDeferredMediaPeriods;
private AdTimeline timeline;
private MediaSource.Listener listener;
private IOException adLoadError;
// Accessed on the main thread.
private ImaAdsLoader imaAdsLoader;
/**
* Constructs a new source that inserts ads linearly with the content specified by
* {@code contentMediaSource}.
*
* @param contentMediaSource The {@link MediaSource} providing the content to play.
* @param dataSourceFactory Factory for data sources used to load ad media.
* @param context The context.
* @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See
* https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
* more information.
* @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad user
* interface.
*/
public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory,
Context context, Uri adTagUri, ViewGroup adUiViewGroup) {
this(contentMediaSource, dataSourceFactory, context, adTagUri, adUiViewGroup, null);
}
/**
* Constructs a new source that inserts ads linearly with the content specified by
* {@code contentMediaSource}.
*
* @param contentMediaSource The {@link MediaSource} providing the content to play.
* @param dataSourceFactory Factory for data sources used to load ad media.
* @param context The context.
* @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See
* https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
* more information.
* @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
* @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to
* use the default settings. If set, the player type and version fields may be overwritten.
*/
public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory,
Context context, Uri adTagUri, ViewGroup adUiViewGroup, ImaSdkSettings imaSdkSettings) {
this.contentMediaSource = contentMediaSource;
this.dataSourceFactory = dataSourceFactory;
this.context = context;
this.adTagUri = adTagUri;
this.adUiViewGroup = adUiViewGroup;
this.imaSdkSettings = imaSdkSettings;
mainHandler = new Handler(Looper.getMainLooper());
adLoaderListener = new AdListener();
mediaSourceByMediaPeriod = new HashMap<>();
adBreakMediaSources = new MediaSource[0][];
}
@Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
Assertions.checkArgument(isTopLevelSource);
this.listener = listener;
this.player = player;
playerHandler = new Handler();
mainHandler.post(new Runnable() {
@Override
public void run() {
imaAdsLoader = new ImaAdsLoader(context, adTagUri, adUiViewGroup, imaSdkSettings,
ImaAdsMediaSource.this.player, adLoaderListener);
}
});
contentMediaSource.prepareSource(player, false, new Listener() {
@Override
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
ImaAdsMediaSource.this.onContentSourceInfoRefreshed(timeline, manifest);
}
});
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
if (adLoadError != null) {
throw adLoadError;
}
contentMediaSource.maybeThrowSourceInfoRefreshError();
for (MediaSource[] mediaSources : adBreakMediaSources) {
for (MediaSource mediaSource : mediaSources) {
mediaSource.maybeThrowSourceInfoRefreshError();
}
}
}
@Override
public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
if (timeline.isPeriodAd(index)) {
int adBreakIndex = timeline.getAdBreakIndex(index);
int adIndexInAdBreak = timeline.getAdIndexInAdBreak(index);
if (adIndexInAdBreak >= adBreakMediaSources[adBreakIndex].length) {
DeferredMediaPeriod deferredPeriod = new DeferredMediaPeriod(0, allocator, positionUs);
if (adIndexInAdBreak >= adBreakDeferredMediaPeriods[adBreakIndex].length) {
adBreakDeferredMediaPeriods[adBreakIndex] = Arrays.copyOf(
adBreakDeferredMediaPeriods[adBreakIndex], adIndexInAdBreak + 1);
}
adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak] = deferredPeriod;
return deferredPeriod;
}
MediaSource adBreakMediaSource = adBreakMediaSources[adBreakIndex][adIndexInAdBreak];
MediaPeriod adBreakMediaPeriod = adBreakMediaSource.createPeriod(0, allocator, positionUs);
mediaSourceByMediaPeriod.put(adBreakMediaPeriod, adBreakMediaSource);
return adBreakMediaPeriod;
} else {
long startUs = timeline.getContentStartTimeUs(index);
long endUs = timeline.getContentEndTimeUs(index);
long contentStartUs = startUs + positionUs;
MediaPeriod contentMediaPeriod = contentMediaSource.createPeriod(0, allocator,
contentStartUs);
ClippingMediaPeriod clippingPeriod = new ClippingMediaPeriod(contentMediaPeriod);
clippingPeriod.setClipping(startUs, endUs == C.TIME_UNSET ? C.TIME_END_OF_SOURCE : endUs);
mediaSourceByMediaPeriod.put(contentMediaPeriod, contentMediaSource);
return clippingPeriod;
}
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
if (mediaPeriod instanceof DeferredMediaPeriod) {
mediaPeriod = ((DeferredMediaPeriod) mediaPeriod).mediaPeriod;
if (mediaPeriod == null) {
// Nothing to do.
return;
}
} else if (mediaPeriod instanceof ClippingMediaPeriod) {
mediaPeriod = ((ClippingMediaPeriod) mediaPeriod).mediaPeriod;
}
mediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod);
}
@Override
public void releaseSource() {
released = true;
adLoadError = null;
contentMediaSource.releaseSource();
for (MediaSource[] mediaSources : adBreakMediaSources) {
for (MediaSource mediaSource : mediaSources) {
mediaSource.releaseSource();
}
}
mainHandler.post(new Runnable() {
@Override
public void run() {
// TODO: The source will be released when the application is paused/stopped, which can occur
// if the user taps on the ad. In this case, we should keep the ads manager alive but pause
// it, instead of destroying it.
imaAdsLoader.release();
imaAdsLoader = null;
}
});
}
// Internal methods.
private void onAdBreakTimesUsLoaded(long[] adBreakTimesUs) {
Assertions.checkState(this.adBreakTimesUs == null);
this.adBreakTimesUs = adBreakTimesUs;
int adBreakCount = adBreakTimesUs.length;
adBreakAds = new Ad[adBreakCount][];
Arrays.fill(adBreakAds, new Ad[0]);
adBreakTimelines = new Timeline[adBreakCount][];
Arrays.fill(adBreakTimelines, new Timeline[0]);
adBreakMediaSources = new MediaSource[adBreakCount][];
Arrays.fill(adBreakMediaSources, new MediaSource[0]);
adBreakDeferredMediaPeriods = new DeferredMediaPeriod[adBreakCount][];
Arrays.fill(adBreakDeferredMediaPeriods, new DeferredMediaPeriod[0]);
playedAdBreak = new boolean[adBreakCount];
maybeUpdateSourceInfo();
}
private void onContentSourceInfoRefreshed(Timeline timeline, Object manifest) {
contentTimeline = timeline;
contentManifest = manifest;
maybeUpdateSourceInfo();
}
private void onAdUriLoaded(final int adBreakIndex, final int adIndexInAdBreak, Uri uri) {
MediaSource adMediaSource = new ExtractorMediaSource(uri, dataSourceFactory,
new DefaultExtractorsFactory(), mainHandler, adLoaderListener);
if (adBreakMediaSources[adBreakIndex].length <= adIndexInAdBreak) {
int adCount = adIndexInAdBreak + 1;
adBreakMediaSources[adBreakIndex] = Arrays.copyOf(adBreakMediaSources[adBreakIndex], adCount);
adBreakTimelines[adBreakIndex] = Arrays.copyOf(adBreakTimelines[adBreakIndex], adCount);
}
adBreakMediaSources[adBreakIndex][adIndexInAdBreak] = adMediaSource;
if (adIndexInAdBreak < adBreakDeferredMediaPeriods[adBreakIndex].length
&& adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak] != null) {
adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak].setMediaSource(
adBreakMediaSources[adBreakIndex][adIndexInAdBreak]);
mediaSourceByMediaPeriod.put(
adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak].mediaPeriod, adMediaSource);
}
adMediaSource.prepareSource(player, false, new Listener() {
@Override
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
onAdSourceInfoRefreshed(adBreakIndex, adIndexInAdBreak, timeline);
}
});
}
private void onAdSourceInfoRefreshed(int adBreakIndex, int adIndexInAdBreak, Timeline timeline) {
adBreakTimelines[adBreakIndex][adIndexInAdBreak] = timeline;
maybeUpdateSourceInfo();
}
private void onAdLoaded(int adBreakIndex, int adIndexInAdBreak, Ad ad) {
if (adBreakAds[adBreakIndex].length <= adIndexInAdBreak) {
int adCount = adIndexInAdBreak + 1;
adBreakAds[adBreakIndex] = Arrays.copyOf(adBreakAds[adBreakIndex], adCount);
}
adBreakAds[adBreakIndex][adIndexInAdBreak] = ad;
maybeUpdateSourceInfo();
}
private void maybeUpdateSourceInfo() {
if (adBreakTimesUs == null || contentTimeline == null) {
// We don't have enough information to start building the timeline yet.
return;
}
AdTimeline.Builder builder = new AdTimeline.Builder(contentTimeline);
int count = adBreakTimesUs.length;
boolean preroll = adBreakTimesUs[0] == 0;
boolean postroll = adBreakTimesUs[count - 1] == C.TIME_UNSET;
int midrollCount = count - (preroll ? 1 : 0) - (postroll ? 1 : 0);
int adBreakIndex = 0;
long contentTimeUs = 0;
if (preroll) {
addAdBreak(builder, adBreakIndex++);
}
for (int i = 0; i < midrollCount; i++) {
long startTimeUs = contentTimeUs;
contentTimeUs = adBreakTimesUs[adBreakIndex];
builder.addContent(startTimeUs, contentTimeUs);
addAdBreak(builder, adBreakIndex++);
}
builder.addContent(contentTimeUs, C.TIME_UNSET);
if (postroll) {
addAdBreak(builder, adBreakIndex);
}
timeline = builder.build();
listener.onSourceInfoRefreshed(timeline, contentManifest);
}
private void addAdBreak(AdTimeline.Builder builder, int adBreakIndex) {
int adCount = adBreakMediaSources[adBreakIndex].length;
AdPodInfo adPodInfo = null;
for (int adIndex = 0; adIndex < adCount; adIndex++) {
Timeline adTimeline = adBreakTimelines[adBreakIndex][adIndex];
long adDurationUs = adTimeline != null
? adTimeline.getPeriod(0, new Timeline.Period()).getDurationUs() : C.TIME_UNSET;
Ad ad = adIndex < adBreakAds[adBreakIndex].length
? adBreakAds[adBreakIndex][adIndex] : null;
builder.addAdPeriod(ad, adBreakIndex, adIndex, adDurationUs);
if (ad != null) {
adPodInfo = ad.getAdPodInfo();
}
}
if (adPodInfo == null || adPodInfo.getTotalAds() > adCount) {
// We don't know how many ads are in the ad break, or they have not loaded yet.
builder.addAdPeriod(null, adBreakIndex, adCount, C.TIME_UNSET);
}
}
private void onAdBreakPlayedToEnd(int adBreakIndex) {
playedAdBreak[adBreakIndex] = true;
}
/**
* Listener for ad loading events. All methods are called on the main thread.
*/
private final class AdListener implements ImaAdsLoader.EventListener,
ExtractorMediaSource.EventListener {
@Override
public void onAdBreakTimesUsLoaded(final long[] adBreakTimesUs) {
if (released) {
return;
}
playerHandler.post(new Runnable() {
@Override
public void run() {
if (released) {
return;
}
ImaAdsMediaSource.this.onAdBreakTimesUsLoaded(adBreakTimesUs);
}
});
}
@Override
public void onUriLoaded(final int adBreakIndex, final int adIndexInAdBreak, final Uri uri) {
if (released) {
return;
}
playerHandler.post(new Runnable() {
@Override
public void run() {
if (released) {
return;
}
ImaAdsMediaSource.this.onAdUriLoaded(adBreakIndex, adIndexInAdBreak, uri);
}
});
}
@Override
public void onAdLoaded(final int adBreakIndex, final int adIndexInAdBreak, final Ad ad) {
if (released) {
return;
}
playerHandler.post(new Runnable() {
@Override
public void run() {
if (released) {
return;
}
ImaAdsMediaSource.this.onAdLoaded(adBreakIndex, adIndexInAdBreak, ad);
}
});
}
@Override
public void onAdBreakPlayedToEnd(final int adBreakIndex) {
if (released) {
return;
}
playerHandler.post(new Runnable() {
@Override
public void run() {
if (released) {
return;
}
ImaAdsMediaSource.this.onAdBreakPlayedToEnd(adBreakIndex);
}
});
}
@Override
public void onLoadError(final IOException error) {
if (released) {
return;
}
playerHandler.post(new Runnable() {
@Override
public void run() {
if (released) {
return;
}
adLoadError = error;
}
});
}
}
private static final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
private final int index;
private final Allocator allocator;
private final long positionUs;
public MediaPeriod mediaPeriod;
private MediaPeriod.Callback callback;
public DeferredMediaPeriod(int index, Allocator allocator, long positionUs) {
this.index = index;
this.allocator = allocator;
this.positionUs = positionUs;
}
public void setMediaSource(MediaSource mediaSource) {
mediaPeriod = mediaSource.createPeriod(index, allocator, positionUs);
if (callback != null) {
mediaPeriod.prepare(this);
}
}
@Override
public void prepare(Callback callback) {
this.callback = callback;
if (mediaPeriod != null) {
mediaPeriod.prepare(this);
}
}
@Override
public void maybeThrowPrepareError() throws IOException {
if (mediaPeriod != null) {
mediaPeriod.maybeThrowPrepareError();
}
}
@Override
public TrackGroupArray getTrackGroups() {
return mediaPeriod.getTrackGroups();
}
@Override
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags,
positionUs);
}
@Override
public void discardBuffer(long positionUs) {
// Do nothing.
}
@Override
public long readDiscontinuity() {
return mediaPeriod.readDiscontinuity();
}
@Override
public long getBufferedPositionUs() {
return mediaPeriod.getBufferedPositionUs();
}
@Override
public long seekToUs(long positionUs) {
return mediaPeriod.seekToUs(positionUs);
}
@Override
public long getNextLoadPositionUs() {
return mediaPeriod.getNextLoadPositionUs();
}
@Override
public boolean continueLoading(long positionUs) {
return mediaPeriod.continueLoading(positionUs);
}
// MediaPeriod.Callback implementation.
@Override
public void onPrepared(MediaPeriod mediaPeriod) {
Assertions.checkArgument(this.mediaPeriod == mediaPeriod);
callback.onPrepared(this);
}
@Override
public void onContinueLoadingRequested(MediaPeriod mediaPeriod) {
Assertions.checkArgument(this.mediaPeriod == mediaPeriod);
callback.onContinueLoadingRequested(this);
}
}
}

View File

@ -23,6 +23,7 @@ include ':playbacktests'
include ':extension-ffmpeg'
include ':extension-flac'
include ':extension-gvr'
include ':extension-ima'
include ':extension-okhttp'
include ':extension-opus'
include ':extension-vp9'
@ -38,6 +39,7 @@ project(':library-ui').projectDir = new File(settingsDir, 'library/ui')
project(':extension-ffmpeg').projectDir = new File(settingsDir, 'extensions/ffmpeg')
project(':extension-flac').projectDir = new File(settingsDir, 'extensions/flac')
project(':extension-gvr').projectDir = new File(settingsDir, 'extensions/gvr')
project(':extension-ima').projectDir = new File(settingsDir, 'extensions/ima')
project(':extension-okhttp').projectDir = new File(settingsDir, 'extensions/okhttp')
project(':extension-opus').projectDir = new File(settingsDir, 'extensions/opus')
project(':extension-vp9').projectDir = new File(settingsDir, 'extensions/vp9')