Commit 39277ebe by andrewlewis Committed by kim-vde

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
parent 6ed371aa
......@@ -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) ###
......
......@@ -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;
......
......@@ -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
......
......@@ -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);
......@@ -708,10 +715,50 @@ public final class ImaAdsLoaderTest {
}
@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() {
......
......@@ -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.
......
......@@ -877,6 +877,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};
int start = 1;
......
......@@ -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(
......
......@@ -199,6 +199,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}.
*
* @param eventListener Listener for ads loader events.
......
......@@ -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,33 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
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 final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
* 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
......
......@@ -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);
}
......
......@@ -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};
......@@ -8540,6 +8544,9 @@ public final class ExoPlayerTest {
public void setSupportedContentTypes(int... contentTypes) {}
@Override
public void setAdTagDataSpec(DataSpec adTagDataSpec) {}
@Override
public void start(AdsLoader.EventListener eventListener, AdViewProvider adViewProvider) {}
@Override
......
......@@ -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);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment