Open source IMA extension
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156312761
This commit is contained in:
parent
4359d44331
commit
cdac347f8f
@ -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')
|
||||
}
|
||||
|
@ -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="
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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
31
extensions/ima/README.md
Normal 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.
|
35
extensions/ima/build.gradle
Normal file
35
extensions/ima/build.gradle
Normal 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
|
||||
}
|
5
extensions/ima/src/main/AndroidManifest.xml
Normal file
5
extensions/ima/src/main/AndroidManifest.xml
Normal 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>
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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')
|
||||
|
Loading…
x
Reference in New Issue
Block a user