diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java index d2d962c568e..d1cb0357187 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java @@ -173,7 +173,9 @@ private static void addPlaybackPropertiesToIntent( .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); } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index c35080c47fa..776ab68a799 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -19,7 +19,6 @@ 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 @@ private List createMediaItems(Intent intent) { return Collections.emptyList(); } } - hasAds |= mediaItem.playbackProperties.adTagUri != null; + hasAds |= mediaItem.playbackProperties.adsConfiguration != null; } if (!hasAds) { releaseAdsLoader(); @@ -363,16 +361,12 @@ private List createMediaItems(Intent intent) { 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 @@ private void releaseAdsLoader() { if (adsLoader != null) { adsLoader.release(); adsLoader = null; - loadedAdTagUri = null; playerView.getOverlayFrameLayout().removeAllViews(); } } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index ea5b38ce8e8..a66a1e03014 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -252,7 +252,7 @@ private int getDownloadUnsupportedStringId(PlaylistHolder playlistHolder) { } 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(); diff --git a/extensions/ima/README.md b/extensions/ima/README.md index c67dfdbb5d5..016f848c7af 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -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 diff --git a/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java b/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java index 9527d35cef9..839c8329516 100644 --- a/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java +++ b/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java @@ -248,6 +248,7 @@ protected MediaSource buildSource( return new AdsMediaSource( contentMediaSource, adTagDataSpec, + /* adsId= */ adTagDataSpec.uri, new DefaultMediaSourceFactory(dataSourceFactory), Assertions.checkNotNull(imaAdsLoader), new AdViewProvider() { diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 2bd8e0c03d4..c5c17c02d6f 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -23,7 +23,6 @@ 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 Builder setDebugModeEnabled(boolean debugModeEnabled) { 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 supportedMimeTypes; - private DataSpec adTagDataSpec; @Nullable private Player player; - /** - * Creates a new IMA ads loader. - * - *

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 AdDisplayContainer getAdDisplayContainer() { return adTagLoader != null ? adTagLoader.getAdDisplayContainer() : null; } - /** - * Requests ads, if they have not already been requested. Must be called on the main thread. - * - *

Ads will be requested automatically when the player is prepared if this method has not been - * called, so it is only necessary to call this method if you want to request ads before preparing - * the player. - * - * @param 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 void requestAds(@Nullable ViewGroup adViewGroup) { * null} if playing audio-only ads. */ public void requestAds(DataSpec adTagDataSpec, @Nullable ViewGroup adViewGroup) { - if (adTagLoader != null) { - return; - } - - if (EMPTY_AD_TAG_DATA_SPEC.equals(adTagDataSpec)) { - adTagDataSpec = checkNotNull(deprecatedAdTagDataSpec); + if (adTagLoader == null) { + adTagLoader = + new AdTagLoader( + context, configuration, imaFactory, supportedMimeTypes, adTagDataSpec, adViewGroup); } - adTagLoader = - new AdTagLoader( - context, configuration, imaFactory, supportedMimeTypes, adTagDataSpec, adViewGroup); } /** @@ -579,12 +474,12 @@ public void setSupportedContentTypes(@C.ContentType int... contentTypes) { } @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; diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java index 5532b1c7ed6..ab7e9f34853 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java @@ -28,6 +28,7 @@ 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.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 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 void teardown() { 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 void builder_overridesPlayerType() { @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 void start_setsAdUiViewGroup() { @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 void startForAudioOnlyAds_createsAudioOnlyAdDisplayContainer() { 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 void start_withPlaceholderContent_initializedAdsLoader() { @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 void start_updatesAdPlaybackState() { 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 void startAndCallbacksAfterRelease() { // 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 void playback_withPrerollAd_marksAdAsPlayed() { 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 void playback_withMidrollFetchError_marksAdAsInErrorState() { 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 void playback_withMidrollFetchError_updatesContentProgress() { 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 void playback_withPostrollFetchError_marksAdAsInErrorState() { 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 void playback_withAdNotPreloadingBeforeTimeout_hasNoError() { 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 void playback_withAdNotPreloadingAfterTimeout_hasErrorAdGroup() { 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 void resumePlaybackBeforeMidroll_playsPreroll() { 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 void resumePlaybackAtMidroll_skipsPreroll() { 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 playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class); verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture()); @@ -467,7 +488,8 @@ public void resumePlaybackAfterMidroll_skipsPreroll() { 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 playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class); verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture()); @@ -499,7 +521,8 @@ public void resumePlaybackBeforeSecondMidroll_playsFirstMidroll() { 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 void resumePlaybackAtSecondMidroll_skipsFirstMidroll() { 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 playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class); verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture()); @@ -555,10 +579,12 @@ public void resumePlaybackBeforeMidroll_withoutPlayAdBeforeStartPosition_skipsPr .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 playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class); verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture()); @@ -588,10 +614,12 @@ public void resumePlaybackAtMidroll_withoutPlayAdBeforeStartPosition_skipsPrerol .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 playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class); verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture()); @@ -621,10 +649,12 @@ public void resumePlaybackAfterMidroll_withoutPlayAdBeforeStartPosition_skipsMid .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 void resumePlaybackAfterMidroll_withoutPlayAdBeforeStartPosition_skipsMid .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 playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class); verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture()); @@ -698,10 +730,12 @@ public void resumePlaybackAtSecondMidroll_withoutPlayAdBeforeStartPosition_skips .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 playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class); verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture()); @@ -735,8 +769,9 @@ public void requestAdTagWithDataScheme_requestsWithAdsResponse() throws Exceptio .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 void requestAdTagWithUri_requestsWithAdTagUrl() throws Exception { .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 void requestAdTagWithUri_requestsWithAdTagUrl() throws Exception { 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 void buildWithAdMediaMimeTypes_setsMimeTypes() throws Exception { .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 void buildWithAdMediaMimeTypes_setsMimeTypes() throws Exception { @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 void loadAd_withLargeAdCuePoint_updatesAdPlaybackStateWithLoadedAd() { float midrollTimeSecs = Float.MAX_VALUE; ImmutableList 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 @@ private void setupPlayback(Timeline contentTimeline, List cuePoints) { .setImaFactory(mockImaFactory) .setImaSdkSettings(mockImaSdkSettings) .build(), - TEST_DATA_SPEC); + TEST_DATA_SPEC, + TEST_ADS_ID); } private void setupPlayback( Timeline contentTimeline, List 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() { diff --git a/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java b/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java index 14c1d6d1e75..33e62c1bcf2 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java @@ -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 static final class Builder { @Nullable private String customCacheKey; private List subtitles; @Nullable private Uri adTagUri; + @Nullable private Object adsId; @Nullable private Object tag; @Nullable private MediaMetadata mediaMetadata; private long liveTargetOffsetMs; @@ -112,7 +116,6 @@ private Builder(MediaItem 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 @@ private Builder(MediaItem 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 Builder setSubtitles(@Nullable List subtitles) { } /** - * Sets the optional ad tag URI. + * Sets the optional ad tag {@link Uri}. + * + *

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. * *

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}. * + *

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. + * *

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. + * + *

All ads media items in the playlist with the same ads identifier and loader will share the + * same ad playback state. + * + *

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 Builder setMediaMetadata(MediaMetadata mediaMetadata) { * 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 MediaItem build() { 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 int hashCode() { } } + /** 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 static final class PlaybackProperties { /** 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 streamKeys; @@ -682,9 +767,6 @@ public static final class PlaybackProperties { /** Optional subtitles to be sideloaded. */ public final List 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 @@ private PlaybackProperties( Uri uri, @Nullable String mimeType, @Nullable DrmConfiguration drmConfiguration, + @Nullable AdsConfiguration adsConfiguration, List streamKeys, @Nullable String customCacheKey, List 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 boolean equals(@Nullable Object obj) { 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 int hashCode() { 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 int hashCode() { /** 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. */ diff --git a/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java b/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java index 683e3cbf7f9..5cbc5f78cbd 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java @@ -281,7 +281,20 @@ public void builderSetAdTagUri_setsAdTagUri() { 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 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java index a4b97219d25..a04dbd215ff 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java @@ -18,7 +18,6 @@ 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 @@ * *

Ad support for media items with ad tag URIs

* - *

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. + *

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. * *

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 DefaultMediaSourceFactory( /** * 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 @@ private static MediaSource maybeClipMediaSource(MediaItem mediaItem, MediaSource 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 @@ private MediaSource maybeWrapWithAdsMediaSource(MediaItem mediaItem, MediaSource + " 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); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java index fda5e15215d..f0bff82b1f2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java @@ -38,16 +38,17 @@ * 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). * - *

{@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 + *

{@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)}. * *

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 OverlayInfo(View view, @Purpose int purpose, @Nullable String detailedRea /** * 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 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 7320f6f6c57..99805122f0c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -33,9 +33,7 @@ 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 RuntimeException getRuntimeExceptionForUnexpected() { 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 RuntimeException getRuntimeExceptionForUnexpected() { @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 AdsMediaSource( 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 @@ protected void prepareSourceInternal(@Nullable TransferListener mediaTransferLis 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 diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 27aa7f3b00c..3dab9d9a65d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -5570,6 +5570,7 @@ public void setMediaSources_secondAdMediaSource_throws() throws Exception { 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 void setMediaSources_multipleMediaSourcesWithAd_throws() throws Exception new AdsMediaSource( mediaSource, /* adTagDataSpec= */ new DataSpec(Uri.EMPTY), + /* adsId= */ new Object(), new DefaultMediaSourceFactory(context), new FakeAdsLoader(), new FakeAdViewProvider()); @@ -5648,6 +5650,7 @@ public void setMediaSources_addingMediaSourcesWithAdToNonEmptyPlaylist_throws() new AdsMediaSource( mediaSource, /* adTagDataSpec= */ new DataSpec(Uri.EMPTY), + /* adsId= */ new Object(), new DefaultMediaSourceFactory(context), new FakeAdsLoader(), new FakeAdViewProvider()); @@ -9018,10 +9021,12 @@ public void release() {} 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 ImmutableList getAdOverlayInfos() { * Returns an argument matcher for {@link Timeline} instances that ignores period and window uids. */ private static ArgumentMatcher noUid(Timeline timeline) { - return new ArgumentMatcher() { - @Override - public boolean matches(Timeline argument) { - return new NoUidTimeline(timeline).equals(new NoUidTimeline(argument)); - } - }; + return argument -> new NoUidTimeline(timeline).equals(new NoUidTimeline(argument)); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java index 08200f93f33..2af226b23ad 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java @@ -204,7 +204,7 @@ public void createMediaSource_withAdTagUri_callsAdsLoader() { 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); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java index 8395fcb1f4e..7fcd740d5f8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java @@ -38,6 +38,7 @@ 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 void setUp() { 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();