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); }