From 866c7f85f845a084fda2677544e480e351f87a29 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 24 Nov 2020 10:36:09 +0000 Subject: [PATCH] Allow playing ads in playlists - Remove restriction on `AdsMediaSource`s in playlists in `ExoPlayerImpl`. - Allow playing playlists of `AdsMediaSource`s in the demo app. - Add a sample with ads in a playlist in the demo app. Issue: #3750 PiperOrigin-RevId: 344018774 --- RELEASENOTES.md | 5 +- demos/main/src/main/assets/media.exolist.json | 17 ++ .../exoplayer2/demo/PlayerActivity.java | 5 - demos/main/src/main/res/values/strings.xml | 2 - .../android/exoplayer2/ExoPlayerImpl.java | 37 ---- .../android/exoplayer2/ExoPlayerTest.java | 170 ------------------ 6 files changed, 20 insertions(+), 216 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ae01c34f4c..5dc4b0f6f7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -69,6 +69,8 @@ existing decoder instance for the new format, and if not then the reasons why. * IMA extension: + * Add support for playback of ads in playlists + ([#3750](https://github.com/google/ExoPlayer/issues/3750)). * Upgrade IMA SDK dependency to 3.21.0, and release the `AdsLoader` ([#7344](https://github.com/google/ExoPlayer/issues/7344)). * Improve handling of ad tags with unsupported VPAID ads @@ -76,6 +78,7 @@ * Fix a bug that caused multiple ads in an ad pod to be skipped when one ad in the ad pod was skipped. * Fix passing an ads response to the `ImaAdsLoader` builder. + * Set the overlay language based on the device locale by default. * Cronet extension: * Fix handling of HTTP status code 200 when making unbounded length range requests ([#8090](https://github.com/google/ExoPlayer/issues/8090)). @@ -88,8 +91,6 @@ * Notify onBufferingEnded when the state of origin player becomes STATE_IDLE or STATE_ENDED. * Allow to remove all playlist items that makes the player reset. -* IMA extension: - * Set the overlay language based on the device locale by default. ### 2.12.1 (2020-10-23) ### diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 4fdfaddea6..8b07d7f625 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -502,6 +502,23 @@ "name": "VMAP midroll ad pod at 5 s with 10 skippable ads", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4", "ad_tag_uri": "https://vastsynthesizer.appspot.com/midroll-10-skippable-ads" + }, + { + "name": "Playlist with three ad tags", + "playlist": [ + { + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=" + }, + { + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-25s.mp4", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator=" + }, + { + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator=" + } + ] } ] }, 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 465471a405..bb2d50ec5b 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 @@ -355,11 +355,6 @@ public class PlayerActivity extends AppCompatActivity } private AdsLoader getAdsLoader(MediaItem.AdsConfiguration adsConfiguration) { - if (mediaItems.size() > 1) { - showToast(R.string.unsupported_ads_in_playlist); - releaseAdsLoader(); - return null; - } // The ads loader is reused for multiple playbacks, so that ad playback can resume. if (adsLoader == null) { adsLoader = new ImaAdsLoader.Builder(/* context= */ this).build(); diff --git a/demos/main/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml index bd5cd63467..9085c43bd3 100644 --- a/demos/main/src/main/res/values/strings.xml +++ b/demos/main/src/main/res/values/strings.xml @@ -45,8 +45,6 @@ One or more sample lists failed to load - Playing without ads, as ads are not supported in playlists - Failed to start download Failed to obtain offline license diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index ace5b411ee..73ad867961 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -34,7 +34,6 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; @@ -92,7 +91,6 @@ import java.util.concurrent.TimeoutException; private SeekParameters seekParameters; private ShuffleOrder shuffleOrder; private boolean pauseAtEndOfMediaItems; - private boolean hasAdsMediaSource; // Playback information when there is no pending seek/set source operation. private PlaybackInfo playbackInfo; @@ -435,7 +433,6 @@ import java.util.concurrent.TimeoutException; @Override public void addMediaSources(int index, List mediaSources) { Assertions.checkArgument(index >= 0); - validateMediaSources(mediaSources, /* mediaSourceReplacement= */ false); Timeline oldTimeline = getCurrentTimeline(); pendingOperationAcks++; List holders = addMediaSourceHolders(index, mediaSources); @@ -1140,7 +1137,6 @@ import java.util.concurrent.TimeoutException; int startWindowIndex, long startPositionMs, boolean resetToDefaultPosition) { - validateMediaSources(mediaSources, /* mediaSourceReplacement= */ true); int currentWindowIndex = getCurrentWindowIndexInternal(); long currentPositionMs = getCurrentPosition(); pendingOperationAcks++; @@ -1239,39 +1235,6 @@ import java.util.concurrent.TimeoutException; mediaSourceHolderSnapshots.remove(i); } shuffleOrder = shuffleOrder.cloneAndRemove(fromIndex, toIndexExclusive); - if (mediaSourceHolderSnapshots.isEmpty()) { - hasAdsMediaSource = false; - } - } - - /** - * Validates media sources before any modification of the existing list of media sources is made. - * This way we can throw an exception before changing the state of the player in case of a - * validation failure. - * - * @param mediaSources The media sources to set or add. - * @param mediaSourceReplacement Whether the given media sources will replace existing ones. - */ - private void validateMediaSources( - List mediaSources, boolean mediaSourceReplacement) { - if (hasAdsMediaSource && !mediaSourceReplacement && !mediaSources.isEmpty()) { - // Adding media sources to an ads media source is not allowed - // (see https://github.com/google/ExoPlayer/issues/3750). - throw new IllegalStateException(); - } - int sizeAfterModification = - mediaSources.size() + (mediaSourceReplacement ? 0 : mediaSourceHolderSnapshots.size()); - for (int i = 0; i < mediaSources.size(); i++) { - MediaSource mediaSource = checkNotNull(mediaSources.get(i)); - if (mediaSource instanceof AdsMediaSource) { - if (sizeAfterModification > 1) { - // Ads media sources only allowed with a single source - // (see https://github.com/google/ExoPlayer/issues/3750). - throw new IllegalArgumentException(); - } - hasAdsMediaSource = true; - } - } } private Timeline createMaskingTimeline() { 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 a853c81042..06c542691f 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 @@ -47,7 +47,6 @@ import android.media.AudioManager; import android.net.Uri; import android.os.Looper; import android.view.Surface; -import android.view.ViewGroup; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -65,7 +64,6 @@ import com.google.android.exoplayer2.robolectric.TestPlayerRunHelper; import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.android.exoplayer2.source.CompositeMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; import com.google.android.exoplayer2.source.LoopingMediaSource; import com.google.android.exoplayer2.source.MaskingMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; @@ -77,8 +75,6 @@ import com.google.android.exoplayer2.source.SilenceMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; 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.testutil.Action; import com.google.android.exoplayer2.testutil.ActionSchedule; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; @@ -106,7 +102,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.Allocation; import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; @@ -5571,124 +5566,6 @@ public final class ExoPlayerTest { assertThat(positionAfterSetShuffleOrder.get()).isAtLeast(5000); } - @Test - public void setMediaSources_secondAdMediaSource_throws() throws Exception { - AdsMediaSource adsMediaSource = - new AdsMediaSource( - new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), - /* adTagDataSpec= */ new DataSpec(Uri.EMPTY), - /* adsId= */ new Object(), - new DefaultMediaSourceFactory(context), - new FakeAdsLoader(), - new FakeAdViewProvider()); - Exception[] exception = {null}; - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - try { - player.setMediaSource(adsMediaSource); - player.addMediaSource(adsMediaSource); - } catch (Exception e) { - exception[0] = e; - } - player.prepare(); - } - }) - .build(); - - new ExoPlayerTestRunner.Builder(context) - .setActionSchedule(actionSchedule) - .build() - .start(/* doPrepare= */ false) - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - assertThat(exception[0]).isInstanceOf(IllegalStateException.class); - } - - @Test - public void setMediaSources_multipleMediaSourcesWithAd_throws() throws Exception { - MediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)); - AdsMediaSource adsMediaSource = - new AdsMediaSource( - mediaSource, - /* adTagDataSpec= */ new DataSpec(Uri.EMPTY), - /* adsId= */ new Object(), - new DefaultMediaSourceFactory(context), - new FakeAdsLoader(), - new FakeAdViewProvider()); - final Exception[] exception = {null}; - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - try { - List sources = new ArrayList<>(); - sources.add(mediaSource); - sources.add(adsMediaSource); - player.setMediaSources(sources); - } catch (Exception e) { - exception[0] = e; - } - player.prepare(); - } - }) - .build(); - - new ExoPlayerTestRunner.Builder(context) - .setActionSchedule(actionSchedule) - .build() - .start(/* doPrepare= */ false) - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - assertThat(exception[0]).isInstanceOf(IllegalArgumentException.class); - } - - @Test - public void setMediaSources_addingMediaSourcesWithAdToNonEmptyPlaylist_throws() throws Exception { - MediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)); - AdsMediaSource adsMediaSource = - new AdsMediaSource( - mediaSource, - /* adTagDataSpec= */ new DataSpec(Uri.EMPTY), - /* adsId= */ new Object(), - new DefaultMediaSourceFactory(context), - new FakeAdsLoader(), - new FakeAdViewProvider()); - final Exception[] exception = {null}; - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - .waitForPlaybackState(Player.STATE_READY) - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - try { - player.addMediaSource(adsMediaSource); - } catch (Exception e) { - exception[0] = e; - } - } - }) - .build(); - - new ExoPlayerTestRunner.Builder(context) - .setMediaSources(mediaSource) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - assertThat(exception[0]).isInstanceOf(IllegalArgumentException.class); - } - @Test public void setMediaSources_empty_whenEmpty_correctMaskingWindowIndex() throws Exception { Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); @@ -9117,53 +8994,6 @@ public final class ExoPlayerTest { } } - private static class FakeAdsLoader implements AdsLoader { - - @Override - public void setPlayer(@Nullable Player player) {} - - @Override - public void release() {} - - @Override - public void setSupportedContentTypes(int... contentTypes) {} - - @Override - public void start( - AdsMediaSource adsMediaSource, - DataSpec adTagDataSpec, - Object adsId, - AdViewProvider adViewProvider, - AdsLoader.EventListener eventListener) {} - - @Override - public void stop(AdsMediaSource adsMediaSource) {} - - @Override - public void handlePrepareComplete( - AdsMediaSource adsMediaSource, int adGroupIndex, int adIndexInAdGroup) {} - - @Override - public void handlePrepareError( - AdsMediaSource adsMediaSource, - int adGroupIndex, - int adIndexInAdGroup, - IOException exception) {} - } - - private static class FakeAdViewProvider implements AdsLoader.AdViewProvider { - - @Override - public ViewGroup getAdViewGroup() { - return null; - } - - @Override - public ImmutableList getAdOverlayInfos() { - return ImmutableList.of(); - } - } - /** * Returns an argument matcher for {@link Timeline} instances that ignores period and window uids. */