Signal an ads identifier to the AdsLoader

In a later change, the AdPlaybackState will include the playing adsId (set by
the AdsLoader) and the ads loader will use this to determine what ad
information is associated with the playing/next periods, to allow loading ads
in playlists.

Apps can continue to pass just a URI for an ad tag with their MediaItem, in
which case the associated playlist will request that ad tag just and the same
state will be used for all occurrences of the ad tag.

This change has breaking changes to the AdsLoader interface and removes
deprecated ways of passing the ad tag, as it's very likely to go into a major
release anyway and not needing to handle the deprecated cases simplifies
ImaAdsLoader.

Issue: #3750
PiperOrigin-RevId: 340438580
This commit is contained in:
andrewlewis 2020-11-03 15:21:05 +00:00 committed by Andrew Lewis
parent f937e40eab
commit 5fd1601f91
15 changed files with 334 additions and 330 deletions

View file

@ -173,7 +173,9 @@ public class IntentUtil {
.putExtra(MIME_TYPE_EXTRA + extrasKeySuffix, playbackProperties.mimeType)
.putExtra(
AD_TAG_URI_EXTRA + extrasKeySuffix,
playbackProperties.adTagUri != null ? playbackProperties.adTagUri.toString() : null);
playbackProperties.adsConfiguration != null
? playbackProperties.adsConfiguration.adTagUri.toString()
: null);
if (playbackProperties.drmConfiguration != null) {
addDrmConfigurationToIntent(playbackProperties.drmConfiguration, intent, extrasKeySuffix);
}

View file

@ -19,7 +19,6 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.util.Pair;
import android.view.KeyEvent;
@ -102,12 +101,11 @@ public class PlayerActivity extends AppCompatActivity
private int startWindow;
private long startPosition;
// Fields used only for ad playback.
// For ad playback only.
private AdsLoader adsLoader;
private Uri loadedAdTagUri;
// Activity lifecycle
// Activity lifecycle.
@Override
public void onCreate(Bundle savedInstanceState) {
@ -355,7 +353,7 @@ public class PlayerActivity extends AppCompatActivity
return Collections.emptyList();
}
}
hasAds |= mediaItem.playbackProperties.adTagUri != null;
hasAds |= mediaItem.playbackProperties.adsConfiguration != null;
}
if (!hasAds) {
releaseAdsLoader();
@ -363,16 +361,12 @@ public class PlayerActivity extends AppCompatActivity
return mediaItems;
}
private AdsLoader getAdsLoader(Uri adTagUri) {
private AdsLoader getAdsLoader(MediaItem.AdsConfiguration adsConfiguration) {
if (mediaItems.size() > 1) {
showToast(R.string.unsupported_ads_in_playlist);
releaseAdsLoader();
return null;
}
if (!adTagUri.equals(loadedAdTagUri)) {
releaseAdsLoader();
loadedAdTagUri = adTagUri;
}
// The ads loader is reused for multiple playbacks, so that ad playback can resume.
if (adsLoader == null) {
adsLoader = new ImaAdsLoader.Builder(/* context= */ this).build();
@ -401,7 +395,6 @@ public class PlayerActivity extends AppCompatActivity
if (adsLoader != null) {
adsLoader.release();
adsLoader = null;
loadedAdTagUri = null;
playerView.getOverlayFrameLayout().removeAllViews();
}
}

View file

@ -252,7 +252,7 @@ public class SampleChooserActivity extends AppCompatActivity
}
MediaItem.PlaybackProperties playbackProperties =
checkNotNull(playlistHolder.mediaItems.get(0).playbackProperties);
if (playbackProperties.adTagUri != null) {
if (playbackProperties.adsConfiguration != null) {
return R.string.download_ads_unsupported;
}
String scheme = playbackProperties.uri.getScheme();

View file

@ -33,17 +33,19 @@ of the developer guide. The `AdsLoaderProvider` passed to the player's
extension only supports players which are accessed on the application's main
thread.
Resuming the player after entering the background requires some special handling
when playing ads. The player and its media source are released on entering the
background, and are recreated when returning to the foreground. When playing ads
it is necessary to persist ad playback state while in the background by keeping
a reference to the `ImaAdsLoader`. When re-entering the foreground, pass the
same instance back when `AdsLoaderProvider.getAdsLoader(Uri adTagUri)` is called
to restore the state. It is also important to persist the player position when
entering the background by storing the value of `player.getContentPosition()`.
On returning to the foreground, seek to that position before preparing the new
player instance. Finally, it is important to call `ImaAdsLoader.release()` when
playback has finished and will not be resumed.
Resuming the player after entering the background requires some special
handling when playing ads. The player and its media source are released on
entering the background, and are recreated when returning to the foreground.
When playing ads it is necessary to persist ad playback state while in the
background by keeping a reference to the `ImaAdsLoader`. When re-entering the
foreground, pass the same instance back when
`AdsLoaderProvider.getAdsLoader(MediaItem.AdsConfiguration adsConfiguration)`
is called to restore the state. It is also important to persist the player
position when entering the background by storing the value of
`player.getContentPosition()`. On returning to the foreground, seek to that
position before preparing the new player instance. Finally, it is important to
call `ImaAdsLoader.release()` when playback has finished and will not be
resumed.
You can try the IMA extension in the ExoPlayer demo app, which has test content
in the "IMA sample ad tags" section of the sample chooser. The demo app's

View file

@ -248,6 +248,7 @@ public final class ImaPlaybackTest {
return new AdsMediaSource(
contentMediaSource,
adTagDataSpec,
/* adsId= */ adTagDataSpec.uri,
new DefaultMediaSourceFactory(dataSourceFactory),
Assertions.checkNotNull(imaAdsLoader),
new AdViewProvider() {

View file

@ -23,7 +23,6 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import android.content.Context;
import android.net.Uri;
import android.os.Looper;
import android.view.View;
import android.view.ViewGroup;
@ -343,125 +342,44 @@ public final class ImaAdsLoader
return this;
}
/**
* Returns a new {@link ImaAdsLoader} for the specified ad tag.
*
* @param adTagUri The URI of a compatible ad tag to load. See
* 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 AdsMediaSource} (if
* using media sources directly).
*/
@Deprecated
public ImaAdsLoader buildForAdTag(Uri adTagUri) {
return new ImaAdsLoader(
context,
getConfiguration(),
imaFactory,
/* adTagUri= */ adTagUri,
/* adsResponse= */ null);
}
/**
* Returns a new {@link ImaAdsLoader} with the specified sideloaded ads response.
*
* @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
* 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(
context, getConfiguration(), imaFactory, /* adTagUri= */ null, adsResponse);
}
/** Returns a new {@link ImaAdsLoader}. */
public ImaAdsLoader build() {
return new ImaAdsLoader(
context, getConfiguration(), imaFactory, /* adTagUri= */ null, /* adsResponse= */ null);
}
// TODO(internal: b/169646419): Remove/hide once the deprecated constructor has been removed.
/* package */ ImaUtil.Configuration getConfiguration() {
return new ImaUtil.Configuration(
adPreloadTimeoutMs,
vastLoadTimeoutMs,
mediaLoadTimeoutMs,
focusSkipButtonWhenAvailable,
playAdBeforeStartPosition,
mediaBitrate,
adMediaMimeTypes,
adUiElements,
companionAdSlots,
adErrorListener,
adEventListener,
videoAdPlayerCallback,
imaSdkSettings,
debugModeEnabled);
context,
new ImaUtil.Configuration(
adPreloadTimeoutMs,
vastLoadTimeoutMs,
mediaLoadTimeoutMs,
focusSkipButtonWhenAvailable,
playAdBeforeStartPosition,
mediaBitrate,
adMediaMimeTypes,
adUiElements,
companionAdSlots,
adErrorListener,
adEventListener,
videoAdPlayerCallback,
imaSdkSettings,
debugModeEnabled),
imaFactory);
}
}
private static final DataSpec EMPTY_AD_TAG_DATA_SPEC = new DataSpec(Uri.EMPTY);
private final ImaUtil.Configuration configuration;
private final Context context;
private final ImaUtil.ImaFactory imaFactory;
@Nullable private final DataSpec deprecatedAdTagDataSpec;
private boolean wasSetPlayerCalled;
@Nullable private Player nextPlayer;
@Nullable private AdTagLoader adTagLoader;
private List<String> supportedMimeTypes;
private DataSpec adTagDataSpec;
@Nullable private Player player;
/**
* Creates a new IMA ads loader.
*
* <p>If you need to customize the ad request, use {@link ImaAdsLoader.Builder} instead.
*
* @param context The context.
* @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See
* https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
* more information.
* @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 AdsMediaSource} (if using media sources directly).
*/
@Deprecated
public ImaAdsLoader(Context context, Uri adTagUri) {
this(
context,
new Builder(context).getConfiguration(),
new DefaultImaFactory(),
adTagUri,
/* adsResponse= */ null);
}
private ImaAdsLoader(
Context context,
ImaUtil.Configuration configuration,
ImaUtil.ImaFactory imaFactory,
@Nullable Uri adTagUri,
@Nullable String adsResponse) {
Context context, ImaUtil.Configuration configuration, ImaUtil.ImaFactory imaFactory) {
this.context = context.getApplicationContext();
this.configuration = configuration;
this.imaFactory = imaFactory;
deprecatedAdTagDataSpec =
adTagUri != null
? new DataSpec(adTagUri)
: adsResponse != null
? new DataSpec(
Util.getDataUriForString(/* mimeType= */ "text/xml", /* data= */ adsResponse))
: null;
adTagDataSpec = EMPTY_AD_TAG_DATA_SPEC;
supportedMimeTypes = ImmutableList.of();
}
@ -490,24 +408,6 @@ public final class ImaAdsLoader
return adTagLoader != null ? adTagLoader.getAdDisplayContainer() : null;
}
/**
* 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 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.
*
@ -521,16 +421,11 @@ public final class ImaAdsLoader
* null} if playing audio-only ads.
*/
public void requestAds(DataSpec adTagDataSpec, @Nullable ViewGroup adViewGroup) {
if (adTagLoader != null) {
return;
if (adTagLoader == null) {
adTagLoader =
new AdTagLoader(
context, configuration, imaFactory, supportedMimeTypes, adTagDataSpec, adViewGroup);
}
if (EMPTY_AD_TAG_DATA_SPEC.equals(adTagDataSpec)) {
adTagDataSpec = checkNotNull(deprecatedAdTagDataSpec);
}
adTagLoader =
new AdTagLoader(
context, configuration, imaFactory, supportedMimeTypes, adTagDataSpec, adViewGroup);
}
/**
@ -579,12 +474,12 @@ public final class ImaAdsLoader
}
@Override
public void setAdTagDataSpec(DataSpec adTagDataSpec) {
this.adTagDataSpec = adTagDataSpec;
}
@Override
public void start(EventListener eventListener, AdViewProvider adViewProvider) {
public void start(
AdsMediaSource adsMediaSource,
DataSpec adTagDataSpec,
Object adsId,
AdViewProvider adViewProvider,
EventListener eventListener) {
checkState(
wasSetPlayerCalled, "Set player using adsLoader.setPlayer before preparing the player.");
player = nextPlayer;

View file

@ -28,6 +28,7 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.net.Uri;
import android.view.View;
import android.view.ViewGroup;
@ -56,11 +57,14 @@ import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.ext.ima.ImaUtil.ImaFactory;
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
import com.google.android.exoplayer2.source.MaskingMediaSource.PlaceholderTimeline;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline;
import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.upstream.DataSpec;
@ -101,6 +105,7 @@ public final class ImaAdsLoaderTest {
CONTENT_TIMELINE.getPeriod(/* periodIndex= */ 0, new Period()).durationUs;
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 Object TEST_ADS_ID = new Object();
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);
@ -119,6 +124,7 @@ public final class ImaAdsLoaderTest {
@Mock private AdPodInfo mockAdPodInfo;
@Mock private Ad mockPrerollSingleAd;
private AdsMediaSource adsMediaSource;
private ViewGroup adViewGroup;
private AdsLoader.AdViewProvider adViewProvider;
private AdsLoader.AdViewProvider audioAdsAdViewProvider;
@ -172,7 +178,8 @@ public final class ImaAdsLoaderTest {
public void builder_overridesPlayerType() {
when(mockImaSdkSettings.getPlayerType()).thenReturn("test player type");
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
verify(mockImaSdkSettings).setPlayerType("google/exo.ext.ima");
}
@ -180,7 +187,8 @@ public final class ImaAdsLoaderTest {
@Test
public void start_setsAdUiViewGroup() {
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
verify(mockImaFactory, atLeastOnce()).createAdDisplayContainer(adViewGroup, videoAdPlayer);
verify(mockImaFactory, never()).createAudioAdDisplayContainer(any(), any());
@ -190,7 +198,8 @@ public final class ImaAdsLoaderTest {
@Test
public void startForAudioOnlyAds_createsAudioOnlyAdDisplayContainer() {
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.start(adsLoaderListener, audioAdsAdViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, audioAdsAdViewProvider, adsLoaderListener);
verify(mockImaFactory, atLeastOnce())
.createAudioAdDisplayContainer(getApplicationContext(), videoAdPlayer);
@ -202,7 +211,8 @@ public final class ImaAdsLoaderTest {
public void start_withPlaceholderContent_initializedAdsLoader() {
Timeline placeholderTimeline = new PlaceholderTimeline(MediaItem.fromUri(Uri.EMPTY));
setupPlayback(placeholderTimeline, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
// We'll only create the rendering settings when initializing the ads loader.
verify(mockImaFactory).createAdsRenderingSettings();
@ -211,7 +221,8 @@ public final class ImaAdsLoaderTest {
@Test
public void start_updatesAdPlaybackState() {
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
@ -223,16 +234,18 @@ public final class ImaAdsLoaderTest {
public void startAfterRelease() {
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.release();
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
}
@Test
public void startAndCallbacksAfterRelease() {
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
// Request ads in order to get a reference to the ad event listener.
imaAdsLoader.requestAds(adViewGroup);
imaAdsLoader.requestAds(TEST_DATA_SPEC, adViewGroup);
imaAdsLoader.release();
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
fakeExoPlayer.setPlayingContentPosition(/* position= */ 0);
fakeExoPlayer.setState(Player.STATE_READY, true);
@ -240,7 +253,7 @@ public final class ImaAdsLoaderTest {
// Note: we can't currently call getContentProgress/getAdProgress as a VerifyError is thrown
// when using Robolectric and accessing VideoProgressUpdate.VIDEO_TIME_NOT_READY, due to the IMA
// SDK being proguarded.
imaAdsLoader.requestAds(adViewGroup);
imaAdsLoader.requestAds(TEST_DATA_SPEC, adViewGroup);
adEventListener.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd));
videoAdPlayer.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo);
adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd));
@ -260,7 +273,8 @@ public final class ImaAdsLoaderTest {
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
// Load the preroll ad.
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
adEventListener.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd));
videoAdPlayer.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo);
adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd));
@ -304,7 +318,8 @@ public final class ImaAdsLoaderTest {
setupPlayback(CONTENT_TIMELINE, ImmutableList.of(20.5f));
// Simulate loading an empty midroll ad.
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
adEventListener.onAdEvent(mockMidrollFetchErrorAdEvent);
assertThat(adsLoaderListener.adPlaybackState)
@ -325,7 +340,8 @@ public final class ImaAdsLoaderTest {
setupPlayback(CONTENT_TIMELINE, ImmutableList.of(5.5f));
// Simulate loading an empty midroll ad and advancing the player position.
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
adEventListener.onAdEvent(mockMidrollFetchErrorAdEvent);
long playerPositionUs = CONTENT_DURATION_US - C.MICROS_PER_SECOND;
long playerPositionInPeriodUs =
@ -350,7 +366,8 @@ public final class ImaAdsLoaderTest {
setupPlayback(CONTENT_TIMELINE, ImmutableList.of(-1f));
// Simulate loading an empty postroll ad.
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
adEventListener.onAdEvent(mockPostrollFetchErrorAdEvent);
assertThat(adsLoaderListener.adPlaybackState)
@ -373,7 +390,8 @@ public final class ImaAdsLoaderTest {
setupPlayback(CONTENT_TIMELINE, cuePoints);
// Advance playback to just before the midroll and simulate buffering.
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(adGroupPositionInWindowUs));
fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true);
// Advance before the timeout and simulating polling content progress.
@ -397,7 +415,8 @@ public final class ImaAdsLoaderTest {
setupPlayback(CONTENT_TIMELINE, cuePoints);
// Advance playback to just before the midroll and simulate buffering.
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(adGroupPositionInWindowUs));
fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true);
// Advance past the timeout and simulate polling content progress.
@ -423,7 +442,8 @@ public final class ImaAdsLoaderTest {
setupPlayback(CONTENT_TIMELINE, cuePoints);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) - 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble());
assertThat(adsLoaderListener.adPlaybackState)
@ -442,7 +462,8 @@ public final class ImaAdsLoaderTest {
setupPlayback(CONTENT_TIMELINE, cuePoints);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs));
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
@ -467,7 +488,8 @@ public final class ImaAdsLoaderTest {
setupPlayback(CONTENT_TIMELINE, cuePoints);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) + 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
@ -499,7 +521,8 @@ public final class ImaAdsLoaderTest {
setupPlayback(CONTENT_TIMELINE, cuePoints);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs) - 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble());
assertThat(adsLoaderListener.adPlaybackState)
@ -525,7 +548,8 @@ public final class ImaAdsLoaderTest {
setupPlayback(CONTENT_TIMELINE, cuePoints);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs));
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
@ -555,10 +579,12 @@ public final class ImaAdsLoaderTest {
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.build(),
TEST_DATA_SPEC);
TEST_DATA_SPEC,
TEST_ADS_ID);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) - 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
@ -588,10 +614,12 @@ public final class ImaAdsLoaderTest {
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.build(),
TEST_DATA_SPEC);
TEST_DATA_SPEC,
TEST_ADS_ID);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs));
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
@ -621,10 +649,12 @@ public final class ImaAdsLoaderTest {
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.build(),
TEST_DATA_SPEC);
TEST_DATA_SPEC,
TEST_ADS_ID);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) + 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
verify(mockAdsManager).destroy();
assertThat(adsLoaderListener.adPlaybackState)
@ -658,10 +688,12 @@ public final class ImaAdsLoaderTest {
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.build(),
TEST_DATA_SPEC);
TEST_DATA_SPEC,
TEST_ADS_ID);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs) - 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
@ -698,10 +730,12 @@ public final class ImaAdsLoaderTest {
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.build(),
TEST_DATA_SPEC);
TEST_DATA_SPEC,
TEST_ADS_ID);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs));
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
@ -735,8 +769,9 @@ public final class ImaAdsLoaderTest {
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.build(),
adDataSpec);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
adDataSpec,
TEST_ADS_ID);
imaAdsLoader.start(adsMediaSource, adDataSpec, TEST_ADS_ID, adViewProvider, adsLoaderListener);
verify(mockAdsRequest).setAdsResponse(adsResponse);
}
@ -750,8 +785,10 @@ public final class ImaAdsLoaderTest {
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.build(),
TEST_DATA_SPEC);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
TEST_DATA_SPEC,
TEST_ADS_ID);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
verify(mockAdsRequest).setAdTagUrl(TEST_DATA_SPEC.uri.toString());
}
@ -760,7 +797,8 @@ public final class ImaAdsLoaderTest {
public void setsDefaultMimeTypes() throws Exception {
setupPlayback(CONTENT_TIMELINE, ImmutableList.of(0f));
imaAdsLoader.setSupportedContentTypes(C.TYPE_DASH, C.TYPE_OTHER);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
verify(mockAdsRenderingSettings)
.setMimeTypes(
@ -783,9 +821,11 @@ public final class ImaAdsLoaderTest {
.setImaSdkSettings(mockImaSdkSettings)
.setAdMediaMimeTypes(ImmutableList.of(MimeTypes.AUDIO_MPEG))
.build(),
TEST_DATA_SPEC);
TEST_DATA_SPEC,
TEST_ADS_ID);
imaAdsLoader.setSupportedContentTypes(C.TYPE_OTHER);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
verify(mockAdsRenderingSettings).setMimeTypes(ImmutableList.of(MimeTypes.AUDIO_MPEG));
}
@ -793,7 +833,8 @@ public final class ImaAdsLoaderTest {
@Test
public void stop_unregistersAllVideoControlOverlays() {
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
imaAdsLoader.requestAds(TEST_DATA_SPEC, adViewGroup);
imaAdsLoader.stop();
@ -808,7 +849,8 @@ public final class ImaAdsLoaderTest {
float midrollTimeSecs = Float.MAX_VALUE;
ImmutableList<Float> cuePoints = ImmutableList.of(midrollTimeSecs);
setupPlayback(CONTENT_TIMELINE, cuePoints);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
videoAdPlayer.loadAd(
TEST_AD_MEDIA_INFO,
new AdPodInfo() {
@ -860,20 +902,29 @@ public final class ImaAdsLoaderTest {
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.build(),
TEST_DATA_SPEC);
TEST_DATA_SPEC,
TEST_ADS_ID);
}
private void setupPlayback(
Timeline contentTimeline,
List<Float> cuePoints,
ImaAdsLoader imaAdsLoader,
DataSpec adTagDataSpec) {
DataSpec adTagDataSpec,
Object adsId) {
fakeExoPlayer = new FakePlayer();
adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline);
adsMediaSource =
new AdsMediaSource(
new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)),
adTagDataSpec,
adsId,
new DefaultMediaSourceFactory((Context) getApplicationContext()),
imaAdsLoader,
adViewProvider);
when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints);
this.imaAdsLoader = imaAdsLoader;
imaAdsLoader.setPlayer(fakeExoPlayer);
imaAdsLoader.setAdTagDataSpec(adTagDataSpec);
}
private void setupMocks() {

View file

@ -15,6 +15,9 @@
*/
package com.google.android.exoplayer2;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import android.net.Uri;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.offline.StreamKey;
@ -74,6 +77,7 @@ public final class MediaItem {
@Nullable private String customCacheKey;
private List<Subtitle> subtitles;
@Nullable private Uri adTagUri;
@Nullable private Object adsId;
@Nullable private Object tag;
@Nullable private MediaMetadata mediaMetadata;
private long liveTargetOffsetMs;
@ -112,7 +116,6 @@ public final class MediaItem {
liveMaxPlaybackSpeed = mediaItem.liveConfiguration.maxPlaybackSpeed;
@Nullable PlaybackProperties playbackProperties = mediaItem.playbackProperties;
if (playbackProperties != null) {
adTagUri = playbackProperties.adTagUri;
customCacheKey = playbackProperties.customCacheKey;
mimeType = playbackProperties.mimeType;
uri = playbackProperties.uri;
@ -130,6 +133,11 @@ public final class MediaItem {
drmUuid = drmConfiguration.uuid;
drmKeySetId = drmConfiguration.getKeySetId();
}
@Nullable AdsConfiguration adsConfiguration = playbackProperties.adsConfiguration;
if (adsConfiguration != null) {
adTagUri = adsConfiguration.adTagUri;
adsId = adsConfiguration.adsId;
}
}
}
@ -408,24 +416,56 @@ public final class MediaItem {
}
/**
* Sets the optional ad tag URI.
* Sets the optional ad tag {@link Uri}.
*
* <p>All ads media items in the playlist with the same ad tag URI and loader will share the
* same ad playback state. To resume ad playback when recreating the playlist on returning from
* the background, pass the same ad tag URI.
*
* <p>If {@link #setUri} is passed a non-null {@code uri}, the ad tag URI is used to create a
* {@link PlaybackProperties} object. Otherwise it will be ignored.
*
* @param adTagUri The ad tag URI to load.
*/
public Builder setAdTagUri(@Nullable String adTagUri) {
this.adTagUri = adTagUri != null ? Uri.parse(adTagUri) : null;
return this;
return setAdTagUri(adTagUri != null ? Uri.parse(adTagUri) : null);
}
/**
* Sets the optional ad tag {@link Uri}.
*
* <p>All ads media items in the playlist with the same ad tag URI and loader will share the
* same ad playback state. To resume ad playback when recreating the playlist on returning from
* the background, pass the same ad tag URI.
*
* <p>If {@link #setUri} is passed a non-null {@code uri}, the ad tag URI is used to create a
* {@link PlaybackProperties} object. Otherwise it will be ignored.
*
* @param adTagUri The ad tag URI to load.
*/
public Builder setAdTagUri(@Nullable Uri adTagUri) {
return setAdTagUri(adTagUri, /* adsId= */ adTagUri);
}
/**
* Sets the optional ad tag {@link Uri} and ads identifier.
*
* <p>All ads media items in the playlist with the same ads identifier and loader will share the
* same ad playback state.
*
* <p>If {@link #setUri} is passed a non-null {@code uri}, the ad tag URI is used to create a
* {@link PlaybackProperties} object. Otherwise it will be ignored.
*
* @param adTagUri The ad tag URI to load.
* @param adsId An opaque identifier for ad playback state associated with this item. Must be
* non-null if {@code adTagUri} is non-null. Ad loading and playback state is shared among
* all media items that have the same ads id (by {@link Object#equals(Object) equality}) and
* ads loader, so it is important to pass the same identifiers when constructing playlist
* items each time the player returns to the foreground.
*/
public Builder setAdTagUri(@Nullable Uri adTagUri, @Nullable Object adsId) {
this.adTagUri = adTagUri;
this.adsId = adsId;
return this;
}
@ -517,8 +557,9 @@ public final class MediaItem {
* Returns a new {@link MediaItem} instance with the current builder values.
*/
public MediaItem build() {
Assertions.checkState(drmLicenseUri == null || drmUuid != null);
checkState(drmLicenseUri == null || drmUuid != null);
@Nullable PlaybackProperties playbackProperties = null;
@Nullable Uri uri = this.uri;
if (uri != null) {
playbackProperties =
new PlaybackProperties(
@ -535,15 +576,15 @@ public final class MediaItem {
drmSessionForClearTypes,
drmKeySetId)
: null,
adTagUri != null ? new AdsConfiguration(adTagUri, checkNotNull(adsId)) : null,
streamKeys,
customCacheKey,
subtitles,
adTagUri,
tag);
mediaId = mediaId != null ? mediaId : uri.toString();
}
return new MediaItem(
Assertions.checkNotNull(mediaId),
checkNotNull(mediaId),
new ClippingProperties(
clipStartPositionMs,
clipEndPositionMs,
@ -656,6 +697,47 @@ public final class MediaItem {
}
}
/** Configuration for playing back linear ads with a media item. */
public static final class AdsConfiguration {
public final Uri adTagUri;
public final Object adsId;
/**
* Creates an ads configuration with the given ad tag URI and ads identifier.
*
* @param adTagUri The ad tag URI to load.
* @param adsId An opaque identifier for ad playback state associated with this item. Ad loading
* and playback state is shared among all media items that have the same ads id (by {@link
* Object#equals(Object) equality}), so it is important to pass the same identifiers when
* constructing playlist items each time the player returns to the foreground.
*/
private AdsConfiguration(Uri adTagUri, Object adsId) {
this.adTagUri = adTagUri;
this.adsId = adsId;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof AdsConfiguration)) {
return false;
}
AdsConfiguration other = (AdsConfiguration) obj;
return adTagUri.equals(other.adTagUri) && adsId.equals(other.adsId);
}
@Override
public int hashCode() {
int result = adTagUri.hashCode();
result = 31 * result + adsId.hashCode();
return result;
}
}
/** Properties for local playback. */
public static final class PlaybackProperties {
@ -673,6 +755,9 @@ public final class MediaItem {
/** Optional {@link DrmConfiguration} for the media. */
@Nullable public final DrmConfiguration drmConfiguration;
/** Optional ads configuration. */
@Nullable public final AdsConfiguration adsConfiguration;
/** Optional stream keys by which the manifest is filtered. */
public final List<StreamKey> streamKeys;
@ -682,9 +767,6 @@ public final class MediaItem {
/** Optional subtitles to be sideloaded. */
public final List<Subtitle> subtitles;
/** Optional ad tag {@link Uri}. */
@Nullable public final Uri adTagUri;
/**
* Optional tag for custom attributes. The tag for the media source which will be published in
* the {@code com.google.android.exoplayer2.Timeline} of the source as {@code
@ -696,18 +778,18 @@ public final class MediaItem {
Uri uri,
@Nullable String mimeType,
@Nullable DrmConfiguration drmConfiguration,
@Nullable AdsConfiguration adsConfiguration,
List<StreamKey> streamKeys,
@Nullable String customCacheKey,
List<Subtitle> subtitles,
@Nullable Uri adTagUri,
@Nullable Object tag) {
this.uri = uri;
this.mimeType = mimeType;
this.drmConfiguration = drmConfiguration;
this.adsConfiguration = adsConfiguration;
this.streamKeys = streamKeys;
this.customCacheKey = customCacheKey;
this.subtitles = subtitles;
this.adTagUri = adTagUri;
this.tag = tag;
}
@ -724,10 +806,10 @@ public final class MediaItem {
return uri.equals(other.uri)
&& Util.areEqual(mimeType, other.mimeType)
&& Util.areEqual(drmConfiguration, other.drmConfiguration)
&& Util.areEqual(adsConfiguration, other.adsConfiguration)
&& streamKeys.equals(other.streamKeys)
&& Util.areEqual(customCacheKey, other.customCacheKey)
&& subtitles.equals(other.subtitles)
&& Util.areEqual(adTagUri, other.adTagUri)
&& Util.areEqual(tag, other.tag);
}
@ -736,10 +818,10 @@ public final class MediaItem {
int result = uri.hashCode();
result = 31 * result + (mimeType == null ? 0 : mimeType.hashCode());
result = 31 * result + (drmConfiguration == null ? 0 : drmConfiguration.hashCode());
result = 31 * result + (adsConfiguration == null ? 0 : adsConfiguration.hashCode());
result = 31 * result + streamKeys.hashCode();
result = 31 * result + (customCacheKey == null ? 0 : customCacheKey.hashCode());
result = 31 * result + subtitles.hashCode();
result = 31 * result + (adTagUri == null ? 0 : adTagUri.hashCode());
result = 31 * result + (tag == null ? 0 : tag.hashCode());
return result;
}
@ -1004,7 +1086,7 @@ public final class MediaItem {
/** Identifies the media item. */
public final String mediaId;
/** Optional playback properties. Maybe be {@code null} if shared over process boundaries. */
/** Optional playback properties. May be {@code null} if shared over process boundaries. */
@Nullable public final PlaybackProperties playbackProperties;
/** The live playback configuration. */

View file

@ -281,7 +281,20 @@ public class MediaItemTest {
MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).setAdTagUri(adTagUri).build();
assertThat(mediaItem.playbackProperties.adTagUri).isEqualTo(adTagUri);
assertThat(mediaItem.playbackProperties.adsConfiguration.adTagUri).isEqualTo(adTagUri);
assertThat(mediaItem.playbackProperties.adsConfiguration.adsId).isEqualTo(adTagUri);
}
@Test
public void builderSetAdTagUriAndAdsId_setsAdsConfiguration() {
Uri adTagUri = Uri.parse(URI_STRING + "/ad");
Object adsId = new Object();
MediaItem mediaItem =
new MediaItem.Builder().setUri(URI_STRING).setAdTagUri(adTagUri, adsId).build();
assertThat(mediaItem.playbackProperties.adsConfiguration.adTagUri).isEqualTo(adTagUri);
assertThat(mediaItem.playbackProperties.adsConfiguration.adsId).isEqualTo(adsId);
}
@Test

View file

@ -18,7 +18,6 @@ package com.google.android.exoplayer2.source;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.content.Context;
import android.net.Uri;
import android.util.SparseArray;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
@ -74,27 +73,28 @@ import java.util.List;
*
* <h3>Ad support for media items with ad tag URIs</h3>
*
* <p>To support media items with {@link MediaItem.PlaybackProperties#adTagUri ad tag URIs}, {@link
* #setAdsLoaderProvider} and {@link #setAdViewProvider} need to be called to configure the factory
* with the required providers.
* <p>To support media items with {@link MediaItem.PlaybackProperties#adsConfiguration ads
* configuration}, {@link #setAdsLoaderProvider} and {@link #setAdViewProvider} need to be called to
* configure the factory with the required providers.
*/
public final class DefaultMediaSourceFactory implements MediaSourceFactory {
/**
* Provides {@link AdsLoader} instances for media items that have {@link
* MediaItem.PlaybackProperties#adTagUri ad tag URIs}.
* MediaItem.PlaybackProperties#adsConfiguration ad tag URIs}.
*/
public interface AdsLoaderProvider {
/**
* Returns an {@link AdsLoader} for the given {@link MediaItem.PlaybackProperties#adTagUri ad
* tag URI}, or null if no ads loader is available for the given ad tag URI.
* Returns an {@link AdsLoader} for the given {@link
* MediaItem.PlaybackProperties#adsConfiguration ads configuration}, or {@code null} if no ads
* loader is available for the given ads configuration.
*
* <p>This method is called each time a {@link MediaSource} is created from a {@link MediaItem}
* that defines an {@link MediaItem.PlaybackProperties#adTagUri ad tag URI}.
* that defines an {@link MediaItem.PlaybackProperties#adsConfiguration ads configuration}.
*/
@Nullable
AdsLoader getAdsLoader(Uri adTagUri);
AdsLoader getAdsLoader(MediaItem.AdsConfiguration adsConfiguration);
}
private static final String TAG = "DefaultMediaSourceFactory";
@ -171,7 +171,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
/**
* Sets the {@link AdsLoaderProvider} that provides {@link AdsLoader} instances for media items
* that have {@link MediaItem.PlaybackProperties#adTagUri ad tag URIs}.
* that have {@link MediaItem.PlaybackProperties#adsConfiguration ads configurations}.
*
* @param adsLoaderProvider A provider for {@link AdsLoader} instances.
* @return This factory, for convenience.
@ -389,8 +389,9 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
private MediaSource maybeWrapWithAdsMediaSource(MediaItem mediaItem, MediaSource mediaSource) {
Assertions.checkNotNull(mediaItem.playbackProperties);
@Nullable Uri adTagUri = mediaItem.playbackProperties.adTagUri;
if (adTagUri == null) {
@Nullable
MediaItem.AdsConfiguration adsConfiguration = mediaItem.playbackProperties.adsConfiguration;
if (adsConfiguration == null) {
return mediaSource;
}
AdsLoaderProvider adsLoaderProvider = this.adsLoaderProvider;
@ -402,14 +403,15 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
+ " setAdViewProvider.");
return mediaSource;
}
@Nullable AdsLoader adsLoader = adsLoaderProvider.getAdsLoader(adTagUri);
@Nullable AdsLoader adsLoader = adsLoaderProvider.getAdsLoader(adsConfiguration);
if (adsLoader == null) {
Log.w(TAG, "Playing media without ads. No AdsLoader for provided adTagUri");
Log.w(TAG, "Playing media without ads, as no AdsLoader was provided.");
return mediaSource;
}
return new AdsMediaSource(
mediaSource,
new DataSpec(adTagUri),
new DataSpec(adsConfiguration.adTagUri),
adsConfiguration.adsId,
/* adMediaSourceFactory= */ this,
adsLoader,
adViewProvider);

View file

@ -38,16 +38,17 @@ import java.util.List;
* with a new copy of the current {@link AdPlaybackState} whenever further information about ads
* becomes known (for example, when an ad media URI is available, or an ad has played to the end).
*
* <p>{@link #start(EventListener, AdViewProvider)} will be called when the ads media source first
* initializes, at which point the loader can request ads. If the player enters the background,
* {@link #stop()} will be called. Loaders should maintain any ad playback state in preparation for
* a later call to {@link #start(EventListener, AdViewProvider)}. If an ad is playing when the
* player is detached, update the ad playback state with the current playback position using {@link
* <p>{@link #start(AdsMediaSource, DataSpec, Object, AdViewProvider, EventListener)} will be called
* when an ads media source first initializes, at which point the loader can request ads. If the
* player enters the background, {@link #stop()} will be called. Loaders should maintain any ad
* playback state in preparation for a later call to {@link #start(AdsMediaSource, DataSpec, Object,
* AdViewProvider, EventListener)}. If an ad is playing when the player is detached, update the ad
* playback state with the current playback position using {@link
* AdPlaybackState#withAdResumePositionUs(long)}.
*
* <p>If {@link EventListener#onAdPlaybackState(AdPlaybackState)} has been called, the
* implementation of {@link #start(EventListener, AdViewProvider)} should invoke the same listener
* to provide the existing playback state to the new player.
* implementation of {@link #start(AdsMediaSource, DataSpec, Object, AdViewProvider, EventListener)}
* should invoke the same listener to provide the existing playback state to the new player.
*/
public interface AdsLoader {
@ -190,29 +191,29 @@ public interface AdsLoader {
/**
* Sets the supported content types for ad media. Must be called before the first call to {@link
* #start(EventListener, AdViewProvider)}. Subsequent calls may be ignored. Called on the main
* thread by {@link AdsMediaSource}.
* #start(AdsMediaSource, DataSpec, Object, AdViewProvider, EventListener)}. Subsequent calls may
* be ignored. Called on the main thread by {@link AdsMediaSource}.
*
* @param contentTypes The supported content types for ad media. Each element must be one of
* {@link C#TYPE_DASH}, {@link C#TYPE_HLS}, {@link C#TYPE_SS} and {@link C#TYPE_OTHER}.
*/
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.
* @param adsMediaSource The ads media source requesting to start loading ads.
* @param adTagDataSpec A data spec for the ad tag to load.
* @param adsId An opaque identifier for the ad playback state across start/stop calls.
* @param adViewProvider Provider of views for the ad UI.
* @param eventListener Listener for ads loader events.
*/
void start(EventListener eventListener, AdViewProvider adViewProvider);
void start(
AdsMediaSource adsMediaSource,
DataSpec adTagDataSpec,
Object adsId,
AdViewProvider adViewProvider,
EventListener eventListener);
/**
* Stops using the ads loader for playback and deregisters the event listener. Called on the main

View file

@ -33,9 +33,7 @@ import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
@ -128,7 +126,8 @@ 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 DataSpec adTagDataSpec;
private final Object adsId;
private final Handler mainHandler;
private final Timeline.Period period;
@ -138,62 +137,16 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
@Nullable private AdPlaybackState adPlaybackState;
private @NullableType AdMediaSourceHolder[][] adMediaSourceHolders;
/**
* Constructs a new source that inserts ads linearly with the content specified by {@code
* contentMediaSource}. Ad media is loaded using {@link ProgressiveMediaSource}.
*
* @param contentMediaSource The {@link MediaSource} providing the content to play.
* @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,
AdsLoader adsLoader,
AdsLoader.AdViewProvider adViewProvider) {
this(
contentMediaSource,
new ProgressiveMediaSource.Factory(dataSourceFactory),
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 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 adsId An opaque identifier for ad playback state associated with this instance. Ad
* loading and playback state is shared among all playlist items that have the same ads id (by
* {@link Object#equals(Object) equality}), so it is important to pass the same identifiers
* when constructing playlist items each time the player returns to the foreground.
* @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.
@ -201,23 +154,16 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
public AdsMediaSource(
MediaSource contentMediaSource,
DataSpec adTagDataSpec,
Object adsId,
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;
this.adsId = adsId;
mainHandler = new Handler(Looper.getMainLooper());
period = new Timeline.Period();
adMediaSourceHolders = new AdMediaSourceHolder[0][];
@ -247,12 +193,13 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
this.componentListener = componentListener;
prepareChildSource(CHILD_SOURCE_MEDIA_PERIOD_ID, contentMediaSource);
mainHandler.post(
() -> {
if (adTagDataSpec != null) {
adsLoader.setAdTagDataSpec(adTagDataSpec);
}
adsLoader.start(componentListener, adViewProvider);
});
() ->
adsLoader.start(
/* adsMediaSource= */ this,
adTagDataSpec,
adsId,
adViewProvider,
componentListener));
}
@Override

View file

@ -5570,6 +5570,7 @@ public final class ExoPlayerTest {
new AdsMediaSource(
new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)),
/* adTagDataSpec= */ new DataSpec(Uri.EMPTY),
/* adsId= */ new Object(),
new DefaultMediaSourceFactory(context),
new FakeAdsLoader(),
new FakeAdViewProvider());
@ -5608,6 +5609,7 @@ public final class ExoPlayerTest {
new AdsMediaSource(
mediaSource,
/* adTagDataSpec= */ new DataSpec(Uri.EMPTY),
/* adsId= */ new Object(),
new DefaultMediaSourceFactory(context),
new FakeAdsLoader(),
new FakeAdViewProvider());
@ -5648,6 +5650,7 @@ public final class ExoPlayerTest {
new AdsMediaSource(
mediaSource,
/* adTagDataSpec= */ new DataSpec(Uri.EMPTY),
/* adsId= */ new Object(),
new DefaultMediaSourceFactory(context),
new FakeAdsLoader(),
new FakeAdViewProvider());
@ -9018,10 +9021,12 @@ public final class ExoPlayerTest {
public void setSupportedContentTypes(int... contentTypes) {}
@Override
public void setAdTagDataSpec(DataSpec adTagDataSpec) {}
@Override
public void start(AdsLoader.EventListener eventListener, AdViewProvider adViewProvider) {}
public void start(
AdsMediaSource adsMediaSource,
DataSpec adTagDataSpec,
Object adsId,
AdViewProvider adViewProvider,
AdsLoader.EventListener eventListener) {}
@Override
public void stop() {}
@ -9050,11 +9055,6 @@ public final class ExoPlayerTest {
* Returns an argument matcher for {@link Timeline} instances that ignores period and window uids.
*/
private static ArgumentMatcher<Timeline> noUid(Timeline timeline) {
return new ArgumentMatcher<Timeline>() {
@Override
public boolean matches(Timeline argument) {
return new NoUidTimeline(timeline).equals(new NoUidTimeline(argument));
}
};
return argument -> new NoUidTimeline(timeline).equals(new NoUidTimeline(argument));
}
}

View file

@ -204,7 +204,7 @@ public final class DefaultMediaSourceFactoryTest {
MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA).setAdTagUri(adTagUri).build();
DefaultMediaSourceFactory defaultMediaSourceFactory =
new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext())
.setAdsLoaderProvider(ignoredAdTagUri -> mock(AdsLoader.class))
.setAdsLoaderProvider(ignoredAdsConfiguration -> mock(AdsLoader.class))
.setAdViewProvider(mock(AdsLoader.AdViewProvider.class));
MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem);

View file

@ -38,6 +38,7 @@ import com.google.android.exoplayer2.source.ads.AdsLoader.AdViewProvider;
import com.google.android.exoplayer2.source.ads.AdsLoader.EventListener;
import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSpec;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@ -83,6 +84,9 @@ public final class AdsMediaSourceTest {
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
.withAdResumePositionUs(/* adResumePositionUs= */ 0);
private static final DataSpec TEST_ADS_DATA_SPEC = new DataSpec(Uri.EMPTY);
private static final Object TEST_ADS_ID = new Object();
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
private FakeMediaSource contentMediaSource;
@ -107,10 +111,21 @@ public final class AdsMediaSourceTest {
ArgumentCaptor.forClass(AdsLoader.EventListener.class);
adsMediaSource =
new AdsMediaSource(
contentMediaSource, adMediaSourceFactory, mockAdsLoader, mockAdViewProvider);
contentMediaSource,
TEST_ADS_DATA_SPEC,
TEST_ADS_ID,
adMediaSourceFactory,
mockAdsLoader,
mockAdViewProvider);
adsMediaSource.prepareSource(mockMediaSourceCaller, /* mediaTransferListener= */ null);
shadowOf(Looper.getMainLooper()).idle();
verify(mockAdsLoader).start(eventListenerArgumentCaptor.capture(), eq(mockAdViewProvider));
verify(mockAdsLoader)
.start(
eq(adsMediaSource),
eq(TEST_ADS_DATA_SPEC),
eq(TEST_ADS_ID),
eq(mockAdViewProvider),
eventListenerArgumentCaptor.capture());
// Simulate loading a preroll ad.
AdsLoader.EventListener adsLoaderEventListener = eventListenerArgumentCaptor.getValue();