diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 7f1200ccd76..23201b9b91b 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -53,6 +53,11 @@
([#7956](https://github.com/google/ExoPlayer/issues/7956)).
* Allow apps to specify a `VideoAdPlayerCallback`
([#7944](https://github.com/google/ExoPlayer/issues/7944)).
+ * Accept ad tags via the `AdsMediaSource` constructor and deprecate
+ passing them via the `ImaAdsLoader` constructor/builders. Passing the
+ ad tag via media item playback properties continues to be supported.
+ This is in preparation for supporting ads in playlists
+ ([#3750](https://github.com/google/ExoPlayer/issues/3750)).
### 2.12.0 (2020-09-11) ###
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
index 8fb92ed2701..c35080c47fa 100644
--- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
@@ -375,7 +375,7 @@ private AdsLoader getAdsLoader(Uri adTagUri) {
}
// The ads loader is reused for multiple playbacks, so that ad playback can resume.
if (adsLoader == null) {
- adsLoader = new ImaAdsLoader(/* context= */ PlayerActivity.this, adTagUri);
+ adsLoader = new ImaAdsLoader.Builder(/* context= */ this).build();
}
adsLoader.setPlayer(player);
return adsLoader;
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
index ffece0f110e..64ad8970630 100644
--- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
@@ -18,7 +18,6 @@
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
-import static com.google.android.exoplayer2.util.Util.castNonNull;
import static java.lang.Math.max;
import android.content.Context;
@@ -92,6 +91,15 @@
* #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling
* {@link #release()}.
*
+ *
See https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
+ * information on compatible ad tag formats. Pass the ad tag URI when setting media item playback
+ * properties (if using the media item API) or as a {@link DataSpec} when constructing the {@link
+ * com.google.android.exoplayer2.source.ads.AdsMediaSource} (if using media sources directly). For
+ * the latter case, please note that this implementation delegates loading of the data spec to the
+ * IMA SDK, so range and headers specifications will be ignored in ad tag URIs. Literal ads
+ * responses can be encoded as data scheme data specs, for example, by constructing the data spec
+ * using a URI generated via {@link Util#getDataUriForString(String, String)}.
+ *
*
The IMA SDK can report obstructions to the ad view for accurate viewability measurement. This
* means that any overlay views that obstruct the ad overlay but are essential for playback need to
* be registered via the {@link AdViewProvider} passed to the {@link
@@ -331,7 +339,12 @@ public Builder setPlayAdBeforeStartPosition(boolean playAdBeforeStartPosition) {
* https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
* information on compatible ad tags.
* @return The new {@link ImaAdsLoader}.
+ * @deprecated Pass the ad tag URI when setting media item playback properties (if using the
+ * media item API) or as a {@link DataSpec} when constructing the {@link
+ * com.google.android.exoplayer2.source.ads.AdsMediaSource} (if using media sources
+ * directly).
*/
+ @Deprecated
public ImaAdsLoader buildForAdTag(Uri adTagUri) {
return new ImaAdsLoader(
/* builder= */ this, /* adTagUri= */ adTagUri, /* adsResponse= */ null);
@@ -343,10 +356,21 @@ public ImaAdsLoader buildForAdTag(Uri adTagUri) {
* @param adsResponse The sideloaded VAST, VMAP, or ad rules response to be used instead of
* making a request via an ad tag URL.
* @return The new {@link ImaAdsLoader}.
+ * @deprecated Pass the ads response as a data URI when setting media item playback properties
+ * (if using the media item API) or as a {@link DataSpec} when constructing the {@link
+ * com.google.android.exoplayer2.source.ads.AdsMediaSource} (if using media sources
+ * directly). {@link Util#getDataUriForString(String, String)} can be used to construct a
+ * data URI from literal string ads response (with MIME type text/xml).
*/
+ @Deprecated
public ImaAdsLoader buildForAdsResponse(String adsResponse) {
return new ImaAdsLoader(/* builder= */ this, /* adTagUri= */ null, adsResponse);
}
+
+ /** Returns a new {@link ImaAdsLoader}. */
+ public ImaAdsLoader build() {
+ return new ImaAdsLoader(/* builder= */ this, /* adTagUri= */ null, /* adsResponse= */ null);
+ }
}
private static final boolean DEBUG = false;
@@ -400,6 +424,8 @@ public ImaAdsLoader buildForAdsResponse(String adsResponse) {
*/
private static final int IMA_AD_STATE_PAUSED = 2;
+ private static final DataSpec EMPTY_AD_TAG_DATA_SPEC = new DataSpec(Uri.EMPTY);
+
private final Context context;
@Nullable private final Uri adTagUri;
@Nullable private final String adsResponse;
@@ -430,6 +456,7 @@ public ImaAdsLoader buildForAdsResponse(String adsResponse) {
private List supportedMimeTypes;
@Nullable private EventListener eventListener;
@Nullable private Player player;
+ private DataSpec adTagDataSpec;
private VideoProgressUpdate lastContentProgress;
private VideoProgressUpdate lastAdProgress;
private int lastVolumePercent;
@@ -505,14 +532,18 @@ public ImaAdsLoader buildForAdsResponse(String adsResponse) {
* @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.
+ * @deprecated Use {@link Builder} to create an instance. Pass the ad tag URI when setting media
+ * item playback properties (if using the media item API) or as a {@link DataSpec} when
+ * constructing the {@link com.google.android.exoplayer2.source.ads.AdsMediaSource} (if using
+ * media sources directly).
*/
+ @Deprecated
public ImaAdsLoader(Context context, Uri adTagUri) {
this(new Builder(context), adTagUri, /* adsResponse= */ null);
}
@SuppressWarnings({"nullness:argument.type.incompatible", "methodref.receiver.bound.invalid"})
private ImaAdsLoader(Builder builder, @Nullable Uri adTagUri, @Nullable String adsResponse) {
- checkArgument(adTagUri != null || adsResponse != null);
this.context = builder.context.getApplicationContext();
this.adTagUri = adTagUri;
this.adsResponse = adsResponse;
@@ -547,6 +578,7 @@ private ImaAdsLoader(Builder builder, @Nullable Uri adTagUri, @Nullable String a
updateAdProgressRunnable = this::updateAdProgress;
adInfoByAdMediaInfo = HashBiMap.create();
supportedMimeTypes = Collections.emptyList();
+ adTagDataSpec = EMPTY_AD_TAG_DATA_SPEC;
lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
@@ -592,12 +624,62 @@ public AdDisplayContainer getAdDisplayContainer() {
*
* @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI, or {@code
* null} if playing audio-only ads.
+ * @deprecated Use {@link #requestAds(DataSpec, ViewGroup)}, specifying the ad tag data spec to
+ * request, and migrate off deprecated builder methods/constructor that require an ad tag or
+ * ads response.
*/
+ @Deprecated
public void requestAds(@Nullable ViewGroup adViewGroup) {
+ requestAds(adTagDataSpec, adViewGroup);
+ }
+
+ /**
+ * Requests ads, if they have not already been requested. Must be called on the main thread.
+ *
+ * Ads will be requested automatically when the player is prepared if this method has not been
+ * called, so it is only necessary to call this method if you want to request ads before preparing
+ * the player.
+ *
+ * @param adTagDataSpec The data specification of the ad tag to load. See class javadoc for
+ * information about compatible ad tag formats.
+ * @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI, or {@code
+ * null} if playing audio-only ads.
+ */
+ public void requestAds(DataSpec adTagDataSpec, @Nullable ViewGroup adViewGroup) {
if (hasAdPlaybackState || adsManager != null || pendingAdRequestContext != null) {
// Ads have already been requested.
return;
}
+
+ if (EMPTY_AD_TAG_DATA_SPEC.equals(adTagDataSpec)) {
+ // Handle deprecated ways of specifying the ad tag.
+ if (adTagUri != null) {
+ adTagDataSpec = new DataSpec(adTagUri);
+ } else if (adsResponse != null) {
+ adTagDataSpec = new DataSpec(Util.getDataUriForString(adsResponse, "text/xml"));
+ } else {
+ throw new IllegalStateException();
+ }
+ }
+
+ AdsRequest request;
+ try {
+ request = ImaUtil.getAdsRequestForAdTagDataSpec(imaFactory, adTagDataSpec);
+ } catch (IOException e) {
+ hasAdPlaybackState = true;
+ updateAdPlaybackState();
+ pendingAdLoadError = AdLoadException.createForAllAds(e);
+ maybeNotifyPendingAdLoadError();
+ return;
+ }
+ this.adTagDataSpec = adTagDataSpec;
+ pendingAdRequestContext = new Object();
+ request.setUserRequestContext(pendingAdRequestContext);
+ if (vastLoadTimeoutMs != TIMEOUT_UNSET) {
+ request.setVastLoadTimeout(vastLoadTimeoutMs);
+ }
+ request.setContentProgressProvider(componentListener);
+
if (adViewGroup != null) {
adDisplayContainer =
imaFactory.createAdDisplayContainer(adViewGroup, /* player= */ componentListener);
@@ -608,24 +690,13 @@ public void requestAds(@Nullable ViewGroup adViewGroup) {
if (companionAdSlots != null) {
adDisplayContainer.setCompanionSlots(companionAdSlots);
}
+
adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer);
adsLoader.addAdErrorListener(componentListener);
if (adErrorListener != null) {
adsLoader.addAdErrorListener(adErrorListener);
}
adsLoader.addAdsLoadedListener(componentListener);
- AdsRequest request = imaFactory.createAdsRequest();
- if (adTagUri != null) {
- request.setAdTagUrl(adTagUri.toString());
- } else {
- request.setAdsResponse(castNonNull(adsResponse));
- }
- if (vastLoadTimeoutMs != TIMEOUT_UNSET) {
- request.setVastLoadTimeout(vastLoadTimeoutMs);
- }
- request.setContentProgressProvider(componentListener);
- pendingAdRequestContext = new Object();
- request.setUserRequestContext(pendingAdRequestContext);
adsLoader.requestAds(request);
}
@@ -674,6 +745,11 @@ public void setSupportedContentTypes(@C.ContentType int... contentTypes) {
this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes);
}
+ @Override
+ public void setAdTagDataSpec(DataSpec adTagDataSpec) {
+ this.adTagDataSpec = adTagDataSpec;
+ }
+
@Override
public void start(EventListener eventListener, AdViewProvider adViewProvider) {
checkState(
@@ -700,7 +776,7 @@ public void start(EventListener eventListener, AdViewProvider adViewProvider) {
updateAdPlaybackState();
} else {
// Ads haven't loaded yet, so request them.
- requestAds(adViewProvider.getAdViewGroup());
+ requestAds(adTagDataSpec, adViewProvider.getAdViewGroup());
}
if (adDisplayContainer != null) {
for (OverlayInfo overlayInfo : adViewProvider.getAdOverlayInfos()) {
@@ -1431,7 +1507,7 @@ private void updateAdPlaybackState() {
private void maybeNotifyPendingAdLoadError() {
if (pendingAdLoadError != null && eventListener != null) {
- eventListener.onAdLoadError(pendingAdLoadError, getAdsDataSpec(adTagUri));
+ eventListener.onAdLoadError(pendingAdLoadError, adTagDataSpec);
pendingAdLoadError = null;
}
}
@@ -1446,8 +1522,7 @@ private void maybeNotifyInternalError(String name, Exception cause) {
updateAdPlaybackState();
if (eventListener != null) {
eventListener.onAdLoadError(
- AdLoadException.createForUnexpected(new RuntimeException(message, cause)),
- getAdsDataSpec(adTagUri));
+ AdLoadException.createForUnexpected(new RuntimeException(message, cause)), adTagDataSpec);
}
}
@@ -1500,10 +1575,6 @@ private String getAdMediaInfoString(AdMediaInfo adMediaInfo) {
return "AdMediaInfo[" + adMediaInfo.getUrl() + (adInfo != null ? ", " + adInfo : "") + "]";
}
- private static DataSpec getAdsDataSpec(@Nullable Uri adTagUri) {
- return new DataSpec(adTagUri != null ? adTagUri : Uri.EMPTY);
- }
-
private static long getContentPeriodPositionMs(
Player player, Timeline timeline, Timeline.Period period) {
long contentWindowPositionMs = player.getContentPosition();
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java
index c4b2c3dca37..e896e1c1151 100644
--- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java
@@ -32,6 +32,10 @@
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.source.ads.AdsLoader.OverlayInfo;
+import com.google.android.exoplayer2.upstream.DataSchemeDataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
import java.util.Arrays;
import java.util.List;
@@ -116,6 +120,24 @@ public static AdPlaybackState getInitialAdPlaybackStateForCuePoints(List
return new AdPlaybackState(adGroupTimesUs);
}
+ /** Returns an {@link AdsRequest} based on the specified ad tag {@link DataSpec}. */
+ public static AdsRequest getAdsRequestForAdTagDataSpec(
+ ImaFactory imaFactory, DataSpec adTagDataSpec) throws IOException {
+ AdsRequest request = imaFactory.createAdsRequest();
+ if (DataSchemeDataSource.SCHEME_DATA.equals(adTagDataSpec.uri.getScheme())) {
+ DataSchemeDataSource dataSchemeDataSource = new DataSchemeDataSource();
+ try {
+ dataSchemeDataSource.open(adTagDataSpec);
+ request.setAdsResponse(Util.fromUtf8Bytes(Util.readToEnd(dataSchemeDataSource)));
+ } finally {
+ dataSchemeDataSource.close();
+ }
+ } else {
+ request.setAdTagUrl(adTagDataSpec.uri.toString());
+ }
+ return request;
+ }
+
/** Returns whether the ad error indicates that an entire ad group failed to load. */
public static boolean isAdGroupLoadError(AdError adError) {
// TODO: Find out what other errors need to be handled (if any), and whether each one relates to
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
index 98610654540..dc6f5b517c6 100644
--- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
@@ -64,6 +64,7 @@
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
@@ -97,8 +98,9 @@ public final class ImaAdsLoaderTest {
/* isSeekable= */ true, /* isDynamic= */ false, CONTENT_DURATION_US));
private static final long CONTENT_PERIOD_DURATION_US =
CONTENT_TIMELINE.getPeriod(/* periodIndex= */ 0, new Period()).durationUs;
- private static final Uri TEST_URI = Uri.EMPTY;
- private static final AdMediaInfo TEST_AD_MEDIA_INFO = new AdMediaInfo(TEST_URI.toString());
+ private static final Uri TEST_URI = Uri.parse("https://www.google.com");
+ private static final DataSpec TEST_DATA_SPEC = new DataSpec(TEST_URI);
+ private static final AdMediaInfo TEST_AD_MEDIA_INFO = new AdMediaInfo("https://www.google.com");
private static final long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND;
private static final ImmutableList PREROLL_CUE_POINTS_SECONDS = ImmutableList.of(0f);
@@ -285,7 +287,7 @@ public void playback_withPrerollAd_marksAdAsPlayed() {
new AdPlaybackState(/* adGroupTimesUs...= */ 0)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
- .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, /* uri= */ TEST_URI)
+ .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI)
.withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}})
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
.withAdResumePositionUs(/* adResumePositionUs= */ 0));
@@ -550,7 +552,8 @@ public void resumePlaybackBeforeMidroll_withoutPlayAdBeforeStartPosition_skipsPr
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
- .buildForAdTag(TEST_URI));
+ .build(),
+ TEST_DATA_SPEC);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) - 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
@@ -582,7 +585,8 @@ public void resumePlaybackAtMidroll_withoutPlayAdBeforeStartPosition_skipsPrerol
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
- .buildForAdTag(TEST_URI));
+ .build(),
+ TEST_DATA_SPEC);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs));
imaAdsLoader.start(adsLoaderListener, adViewProvider);
@@ -614,7 +618,8 @@ public void resumePlaybackAfterMidroll_withoutPlayAdBeforeStartPosition_skipsMid
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
- .buildForAdTag(TEST_URI));
+ .build(),
+ TEST_DATA_SPEC);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) + 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
@@ -650,7 +655,8 @@ public void resumePlaybackAfterMidroll_withoutPlayAdBeforeStartPosition_skipsMid
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
- .buildForAdTag(TEST_URI));
+ .build(),
+ TEST_DATA_SPEC);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs) - 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
@@ -689,7 +695,8 @@ public void resumePlaybackAtSecondMidroll_withoutPlayAdBeforeStartPosition_skips
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
- .buildForAdTag(TEST_URI));
+ .build(),
+ TEST_DATA_SPEC);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs));
imaAdsLoader.start(adsLoaderListener, adViewProvider);
@@ -707,11 +714,51 @@ public void resumePlaybackAtSecondMidroll_withoutPlayAdBeforeStartPosition_skips
.withSkippedAdGroup(/* adGroupIndex= */ 0));
}
+ @Test
+ public void requestAdTagWithDataScheme_requestsWithAdsResponse() throws Exception {
+ String adsResponse =
+ "\n"
+ + "\n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + "";
+ DataSpec adDataSpec = new DataSpec(Util.getDataUriForString("text/xml", adsResponse));
+
+ setupPlayback(
+ CONTENT_TIMELINE,
+ ImmutableList.of(0f),
+ new ImaAdsLoader.Builder(getApplicationContext())
+ .setImaFactory(mockImaFactory)
+ .setImaSdkSettings(mockImaSdkSettings)
+ .build(),
+ adDataSpec);
+ imaAdsLoader.start(adsLoaderListener, adViewProvider);
+
+ verify(mockAdsRequest).setAdsResponse(adsResponse);
+ }
+
+ @Test
+ public void requestAdTagWithUri_requestsWithAdTagUrl() throws Exception {
+ setupPlayback(
+ CONTENT_TIMELINE,
+ ImmutableList.of(0f),
+ new ImaAdsLoader.Builder(getApplicationContext())
+ .setImaFactory(mockImaFactory)
+ .setImaSdkSettings(mockImaSdkSettings)
+ .build(),
+ TEST_DATA_SPEC);
+ imaAdsLoader.start(adsLoaderListener, adViewProvider);
+
+ verify(mockAdsRequest).setAdTagUrl(TEST_DATA_SPEC.uri.toString());
+ }
+
@Test
public void stop_unregistersAllVideoControlOverlays() {
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
- imaAdsLoader.requestAds(adViewGroup);
+ imaAdsLoader.requestAds(TEST_DATA_SPEC, adViewGroup);
imaAdsLoader.stop();
InOrder inOrder = inOrder(mockAdDisplayContainer);
@@ -775,16 +822,21 @@ private void setupPlayback(Timeline contentTimeline, List cuePoints) {
new ImaAdsLoader.Builder(getApplicationContext())
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
- .buildForAdTag(TEST_URI));
+ .build(),
+ TEST_DATA_SPEC);
}
private void setupPlayback(
- Timeline contentTimeline, List cuePoints, ImaAdsLoader imaAdsLoader) {
+ Timeline contentTimeline,
+ List cuePoints,
+ ImaAdsLoader imaAdsLoader,
+ DataSpec adTagDataSpec) {
fakeExoPlayer = new FakePlayer();
adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline);
when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints);
this.imaAdsLoader = imaAdsLoader;
imaAdsLoader.setPlayer(fakeExoPlayer);
+ imaAdsLoader.setAdTagDataSpec(adTagDataSpec);
}
private void setupMocks() {
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java
index 5505649cf0e..88d197b88a8 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java
@@ -48,6 +48,7 @@
import android.security.NetworkSecurityPolicy;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
+import android.util.Base64;
import android.view.Display;
import android.view.SurfaceView;
import android.view.WindowManager;
@@ -1956,6 +1957,14 @@ private static boolean shouldEscapeCharacter(char c) {
return builder.toString();
}
+ /** Returns a data URI with the specified MIME type and data. */
+ public static Uri getDataUriForString(String mimeType, String data) {
+ // TODO(internal: b/169937045): For now we don't pass the URL_SAFE flag as DataSchemeDataSource
+ // doesn't decode using it.
+ return Uri.parse(
+ "data:" + mimeType + ";base64," + Base64.encodeToString(data.getBytes(), Base64.NO_WRAP));
+ }
+
/**
* A hacky method that always throws {@code t} even if {@code t} is a checked exception,
* and is not declared to be thrown.
diff --git a/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java
index cda9e054f16..dd2ee7af890 100644
--- a/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java
+++ b/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java
@@ -876,6 +876,14 @@ public void escapeUnescapeFileName_returnsEscapedString() {
}
}
+ @Test
+ public void getDataUriForString_returnsCorrectDataUri() {
+ assertThat(
+ Util.getDataUriForString(/* mimeType= */ "text/plain", "Some Data!<>:\"/\\|?*%")
+ .toString())
+ .isEqualTo("data:text/plain;base64,U29tZSBEYXRhITw+OiIvXHw/KiU=");
+ }
+
@Test
public void crc32_returnsUpdatedCrc32() {
byte[] bytes = {0x5F, 0x78, 0x04, 0x7B, 0x5F};
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java
index 3f1c03d3b18..8f85a0ac171 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java
@@ -29,6 +29,7 @@
import com.google.android.exoplayer2.source.ads.AdsLoader.AdViewProvider;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
@@ -280,7 +281,8 @@ private static MediaSource maybeClipMediaSource(MediaItem mediaItem, MediaSource
private MediaSource maybeWrapWithAdsMediaSource(MediaItem mediaItem, MediaSource mediaSource) {
Assertions.checkNotNull(mediaItem.playbackProperties);
- if (mediaItem.playbackProperties.adTagUri == null) {
+ @Nullable Uri adTagUri = mediaItem.playbackProperties.adTagUri;
+ if (adTagUri == null) {
return mediaSource;
}
AdsLoaderProvider adsLoaderProvider = this.adsLoaderProvider;
@@ -292,14 +294,17 @@ private MediaSource maybeWrapWithAdsMediaSource(MediaItem mediaItem, MediaSource
+ " setAdViewProvider.");
return mediaSource;
}
- @Nullable
- AdsLoader adsLoader = adsLoaderProvider.getAdsLoader(mediaItem.playbackProperties.adTagUri);
+ @Nullable AdsLoader adsLoader = adsLoaderProvider.getAdsLoader(adTagUri);
if (adsLoader == null) {
Log.w(TAG, "Playing media without ads. No AdsLoader for provided adTagUri");
return mediaSource;
}
return new AdsMediaSource(
- mediaSource, /* adMediaSourceFactory= */ this, adsLoader, adViewProvider);
+ mediaSource,
+ new DataSpec(adTagUri),
+ /* adMediaSourceFactory= */ this,
+ adsLoader,
+ adViewProvider);
}
private static SparseArray loadDelegates(
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java
index f1c17c10935..fda5e15215d 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java
@@ -198,6 +198,14 @@ public OverlayInfo(View view, @Purpose int purpose, @Nullable String detailedRea
*/
void setSupportedContentTypes(@C.ContentType int... contentTypes);
+ /**
+ * Sets the data spec of the ad tag to load.
+ *
+ * @param adTagDataSpec The data spec of the ad tag to load. See the implementation's
+ * documentation for information about compatible ad tag formats.
+ */
+ void setAdTagDataSpec(DataSpec adTagDataSpec);
+
/**
* Starts using the ads loader for playback. Called on the main thread by {@link AdsMediaSource}.
*
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java
index 62c3e2ed173..7320f6f6c57 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java
@@ -128,6 +128,7 @@ public RuntimeException getRuntimeExceptionForUnexpected() {
private final MediaSourceFactory adMediaSourceFactory;
private final AdsLoader adsLoader;
private final AdsLoader.AdViewProvider adViewProvider;
+ @Nullable private final DataSpec adTagDataSpec;
private final Handler mainHandler;
private final Timeline.Period period;
@@ -145,7 +146,10 @@ public RuntimeException getRuntimeExceptionForUnexpected() {
* @param dataSourceFactory Factory for data sources used to load ad media.
* @param adsLoader The loader for ads.
* @param adViewProvider Provider of views for the ad UI.
+ * @deprecated Use {@link AdsMediaSource#AdsMediaSource(MediaSource, DataSpec, MediaSourceFactory,
+ * AdsLoader, AdsLoader.AdViewProvider)} instead.
*/
+ @Deprecated
public AdsMediaSource(
MediaSource contentMediaSource,
DataSource.Factory dataSourceFactory,
@@ -155,7 +159,33 @@ public AdsMediaSource(
contentMediaSource,
new ProgressiveMediaSource.Factory(dataSourceFactory),
adsLoader,
- adViewProvider);
+ adViewProvider,
+ /* adTagDataSpec= */ 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 adMediaSourceFactory Factory for media sources used to load ad media.
+ * @param adsLoader The loader for ads.
+ * @param adViewProvider Provider of views for the ad UI.
+ * @deprecated Use {@link AdsMediaSource#AdsMediaSource(MediaSource, DataSpec, MediaSourceFactory,
+ * AdsLoader, AdsLoader.AdViewProvider)} instead.
+ */
+ @Deprecated
+ public AdsMediaSource(
+ MediaSource contentMediaSource,
+ MediaSourceFactory adMediaSourceFactory,
+ AdsLoader adsLoader,
+ AdsLoader.AdViewProvider adViewProvider) {
+ this(
+ contentMediaSource,
+ adMediaSourceFactory,
+ adsLoader,
+ adViewProvider,
+ /* adTagDataSpec= */ null);
}
/**
@@ -163,19 +193,31 @@ public AdsMediaSource(
* contentMediaSource}.
*
* @param contentMediaSource The {@link MediaSource} providing the content to play.
+ * @param adTagDataSpec The data specification of the ad tag to load.
* @param adMediaSourceFactory Factory for media sources used to load ad media.
* @param adsLoader The loader for ads.
* @param adViewProvider Provider of views for the ad UI.
*/
public AdsMediaSource(
MediaSource contentMediaSource,
+ DataSpec adTagDataSpec,
MediaSourceFactory adMediaSourceFactory,
AdsLoader adsLoader,
AdsLoader.AdViewProvider adViewProvider) {
+ this(contentMediaSource, adMediaSourceFactory, adsLoader, adViewProvider, adTagDataSpec);
+ }
+
+ private AdsMediaSource(
+ MediaSource contentMediaSource,
+ MediaSourceFactory adMediaSourceFactory,
+ AdsLoader adsLoader,
+ AdsLoader.AdViewProvider adViewProvider,
+ @Nullable DataSpec adTagDataSpec) {
this.contentMediaSource = contentMediaSource;
this.adMediaSourceFactory = adMediaSourceFactory;
this.adsLoader = adsLoader;
this.adViewProvider = adViewProvider;
+ this.adTagDataSpec = adTagDataSpec;
mainHandler = new Handler(Looper.getMainLooper());
period = new Timeline.Period();
adMediaSourceHolders = new AdMediaSourceHolder[0][];
@@ -204,7 +246,13 @@ protected void prepareSourceInternal(@Nullable TransferListener mediaTransferLis
ComponentListener componentListener = new ComponentListener();
this.componentListener = componentListener;
prepareChildSource(CHILD_SOURCE_MEDIA_PERIOD_ID, contentMediaSource);
- mainHandler.post(() -> adsLoader.start(componentListener, adViewProvider));
+ mainHandler.post(
+ () -> {
+ if (adTagDataSpec != null) {
+ adsLoader.setAdTagDataSpec(adTagDataSpec);
+ }
+ adsLoader.start(componentListener, adViewProvider);
+ });
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java
index 680ebbb2b1a..2c3670f52a6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java
@@ -59,7 +59,8 @@ public long open(DataSpec dataSpec) throws IOException {
String dataString = uriParts[1];
if (uriParts[0].contains(";base64")) {
try {
- data = Base64.decode(dataString, 0);
+ // TODO(internal: b/169937045): Consider passing Base64.URL_SAFE flag.
+ data = Base64.decode(dataString, /* flags= */ Base64.DEFAULT);
} catch (IllegalArgumentException e) {
throw new ParserException("Error while parsing Base64 encoded string: " + dataString, e);
}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java
index ffd46f90891..45ba914d34e 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java
@@ -58,6 +58,7 @@
import com.google.android.exoplayer2.source.ClippingMediaSource;
import com.google.android.exoplayer2.source.CompositeMediaSource;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
+import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
import com.google.android.exoplayer2.source.LoopingMediaSource;
import com.google.android.exoplayer2.source.MaskingMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
@@ -98,7 +99,7 @@
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.Allocation;
import com.google.android.exoplayer2.upstream.Allocator;
-import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
+import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
@@ -5554,7 +5555,8 @@ public void setMediaSources_secondAdMediaSource_throws() throws Exception {
AdsMediaSource adsMediaSource =
new AdsMediaSource(
new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)),
- new DefaultDataSourceFactory(context),
+ /* adTagDataSpec= */ new DataSpec(Uri.EMPTY),
+ new DefaultMediaSourceFactory(context),
new FakeAdsLoader(),
new FakeAdViewProvider());
Exception[] exception = {null};
@@ -5591,7 +5593,8 @@ public void setMediaSources_multipleMediaSourcesWithAd_throws() throws Exception
AdsMediaSource adsMediaSource =
new AdsMediaSource(
mediaSource,
- new DefaultDataSourceFactory(context),
+ /* adTagDataSpec= */ new DataSpec(Uri.EMPTY),
+ new DefaultMediaSourceFactory(context),
new FakeAdsLoader(),
new FakeAdViewProvider());
final Exception[] exception = {null};
@@ -5630,7 +5633,8 @@ public void setMediaSources_addingMediaSourcesWithAdToNonEmptyPlaylist_throws()
AdsMediaSource adsMediaSource =
new AdsMediaSource(
mediaSource,
- new DefaultDataSourceFactory(context),
+ /* adTagDataSpec= */ new DataSpec(Uri.EMPTY),
+ new DefaultMediaSourceFactory(context),
new FakeAdsLoader(),
new FakeAdViewProvider());
final Exception[] exception = {null};
@@ -8548,6 +8552,9 @@ public void release() {}
@Override
public void setSupportedContentTypes(int... contentTypes) {}
+ @Override
+ public void setAdTagDataSpec(DataSpec adTagDataSpec) {}
+
@Override
public void start(AdsLoader.EventListener eventListener, AdViewProvider adViewProvider) {}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java
index 0acbd74891e..dee68fe9683 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java
@@ -146,6 +146,14 @@ public void malformedData() {
}
}
+ @Test
+ public void readSourceToEnd_readsEncodedString() throws Exception {
+ String data = "Some Data!<>:\"/\\|?*%";
+ schemeDataDataSource.open(new DataSpec(Util.getDataUriForString("text/plain", data)));
+
+ assertThat(Util.fromUtf8Bytes(Util.readToEnd(schemeDataDataSource))).isEqualTo(data);
+ }
+
private static DataSpec buildDataSpec(String uriString) {
return buildDataSpec(uriString, /* position= */ 0, /* length= */ C.LENGTH_UNSET);
}