Pass ad tags via AdsMediaSource

This is in preparation for supporting playlists of ads media sources using
ImaAdsLoader.

Existing ways of passing ad tags should still function but are deprecated (and
won't be supported with playlists).

Issue: #3750
PiperOrigin-RevId: 335618364
This commit is contained in:
andrewlewis 2020-10-06 13:17:38 +01:00 committed by kim-vde
parent 6ed371aaf3
commit 39277ebe95
13 changed files with 289 additions and 45 deletions

View file

@ -49,6 +49,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) ###

View file

@ -375,7 +375,7 @@ public class PlayerActivity extends AppCompatActivity
}
// 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;

View file

@ -18,7 +18,6 @@ package com.google.android.exoplayer2.ext.ima;
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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling
* {@link #release()}.
*
* <p>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)}.
*
* <p>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 final class ImaAdsLoader
* 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 final class ImaAdsLoader
* @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 final class ImaAdsLoader
*/
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 final class ImaAdsLoader
private List<String> 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 final class ImaAdsLoader
* @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 @@ public final class ImaAdsLoader
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 final class ImaAdsLoader
*
* @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.
*
* <p>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 final class ImaAdsLoader
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 final class ImaAdsLoader
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 final class ImaAdsLoader
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 @@ public final class ImaAdsLoader
private void maybeNotifyPendingAdLoadError() {
if (pendingAdLoadError != null && eventListener != null) {
eventListener.onAdLoadError(pendingAdLoadError, getAdsDataSpec(adTagUri));
eventListener.onAdLoadError(pendingAdLoadError, adTagDataSpec);
pendingAdLoadError = null;
}
}
@ -1446,8 +1522,7 @@ public final class ImaAdsLoader
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 @@ public final class ImaAdsLoader
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();

View file

@ -32,6 +32,10 @@ import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
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 @@ import java.util.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

View file

@ -64,6 +64,7 @@ import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline;
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<Float> PREROLL_CUE_POINTS_SECONDS = ImmutableList.of(0f);
@ -285,7 +287,7 @@ public final class ImaAdsLoaderTest {
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 final class ImaAdsLoaderTest {
.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 final class ImaAdsLoaderTest {
.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 final class ImaAdsLoaderTest {
.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 final class ImaAdsLoaderTest {
.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 final class ImaAdsLoaderTest {
.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 final class ImaAdsLoaderTest {
.withSkippedAdGroup(/* adGroupIndex= */ 0));
}
@Test
public void requestAdTagWithDataScheme_requestsWithAdsResponse() throws Exception {
String adsResponse =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<VAST xmlns:xsi=\"https://www.w3.org/2001/XMLSchema-instance\""
+ " xsi:noNamespaceSchemaLocation=\"vast.xsd\" version=\"2.0\">\n"
+ " <Ad id=\"17180293\">\n"
+ " <InLine></InLine>\n"
+ " </Ad>\n"
+ "</VAST>";
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 @@ public final class ImaAdsLoaderTest {
new ImaAdsLoader.Builder(getApplicationContext())
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.buildForAdTag(TEST_URI));
.build(),
TEST_DATA_SPEC);
}
private void setupPlayback(
Timeline contentTimeline, List<Float> cuePoints, ImaAdsLoader imaAdsLoader) {
Timeline contentTimeline,
List<Float> 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() {

View file

@ -48,6 +48,7 @@ import android.os.SystemClock;
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;
@ -2007,6 +2008,14 @@ public final class Util {
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.

View file

@ -876,6 +876,14 @@ public class UtilTest {
}
}
@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};

View file

@ -29,6 +29,7 @@ import com.google.android.exoplayer2.source.ads.AdsLoader;
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 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
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 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
+ " 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<MediaSourceFactory> loadDelegates(

View file

@ -198,6 +198,14 @@ public interface AdsLoader {
*/
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}.
*

View file

@ -128,6 +128,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
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 final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
* @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,8 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
contentMediaSource,
new ProgressiveMediaSource.Factory(dataSourceFactory),
adsLoader,
adViewProvider);
adViewProvider,
/* adTagDataSpec= */ null);
}
/**
@ -166,16 +171,53 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
* @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);
}
/**
* 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 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 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
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

View file

@ -59,7 +59,8 @@ public final class DataSchemeDataSource extends BaseDataSource {
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);
}

View file

@ -61,6 +61,7 @@ import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
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;
@ -101,7 +102,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection;
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;
@ -5556,7 +5557,8 @@ public final class ExoPlayerTest {
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};
@ -5593,7 +5595,8 @@ public final class ExoPlayerTest {
AdsMediaSource adsMediaSource =
new AdsMediaSource(
mediaSource,
new DefaultDataSourceFactory(context),
/* adTagDataSpec= */ new DataSpec(Uri.EMPTY),
new DefaultMediaSourceFactory(context),
new FakeAdsLoader(),
new FakeAdViewProvider());
final Exception[] exception = {null};
@ -5632,7 +5635,8 @@ public final class ExoPlayerTest {
AdsMediaSource adsMediaSource =
new AdsMediaSource(
mediaSource,
new DefaultDataSourceFactory(context),
/* adTagDataSpec= */ new DataSpec(Uri.EMPTY),
new DefaultMediaSourceFactory(context),
new FakeAdsLoader(),
new FakeAdViewProvider());
final Exception[] exception = {null};
@ -8539,6 +8543,9 @@ public final class ExoPlayerTest {
@Override
public void setSupportedContentTypes(int... contentTypes) {}
@Override
public void setAdTagDataSpec(DataSpec adTagDataSpec) {}
@Override
public void start(AdsLoader.EventListener eventListener, AdViewProvider adViewProvider) {}

View file

@ -145,6 +145,14 @@ public final class DataSchemeDataSourceTest {
}
}
@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);
}