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