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
This commit is contained in:
andrewlewis 2020-11-24 10:36:09 +00:00 committed by kim-vde
parent 2ddb5b8d94
commit 866c7f85f8
6 changed files with 20 additions and 216 deletions

View File

@ -69,6 +69,8 @@
existing decoder instance for the new format, and if not then the existing decoder instance for the new format, and if not then the
reasons why. reasons why.
* IMA extension: * 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` * Upgrade IMA SDK dependency to 3.21.0, and release the `AdsLoader`
([#7344](https://github.com/google/ExoPlayer/issues/7344)). ([#7344](https://github.com/google/ExoPlayer/issues/7344)).
* Improve handling of ad tags with unsupported VPAID ads * 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 * Fix a bug that caused multiple ads in an ad pod to be skipped when one
ad in the ad pod was skipped. ad in the ad pod was skipped.
* Fix passing an ads response to the `ImaAdsLoader` builder. * Fix passing an ads response to the `ImaAdsLoader` builder.
* Set the overlay language based on the device locale by default.
* Cronet extension: * Cronet extension:
* Fix handling of HTTP status code 200 when making unbounded length range * Fix handling of HTTP status code 200 when making unbounded length range
requests ([#8090](https://github.com/google/ExoPlayer/issues/8090)). requests ([#8090](https://github.com/google/ExoPlayer/issues/8090)).
@ -88,8 +91,6 @@
* Notify onBufferingEnded when the state of origin player becomes * Notify onBufferingEnded when the state of origin player becomes
STATE_IDLE or STATE_ENDED. STATE_IDLE or STATE_ENDED.
* Allow to remove all playlist items that makes the player reset. * 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) ### ### 2.12.1 (2020-10-23) ###

View File

@ -502,6 +502,23 @@
"name": "VMAP midroll ad pod at 5 s with 10 skippable ads", "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", "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" "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="
}
]
} }
] ]
}, },

View File

@ -355,11 +355,6 @@ public class PlayerActivity extends AppCompatActivity
} }
private AdsLoader getAdsLoader(MediaItem.AdsConfiguration adsConfiguration) { 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. // The ads loader is reused for multiple playbacks, so that ad playback can resume.
if (adsLoader == null) { if (adsLoader == null) {
adsLoader = new ImaAdsLoader.Builder(/* context= */ this).build(); adsLoader = new ImaAdsLoader.Builder(/* context= */ this).build();

View File

@ -45,8 +45,6 @@
<string name="sample_list_load_error">One or more sample lists failed to load</string> <string name="sample_list_load_error">One or more sample lists failed to load</string>
<string name="unsupported_ads_in_playlist">Playing without ads, as ads are not supported in playlists</string>
<string name="download_start_error">Failed to start download</string> <string name="download_start_error">Failed to start download</string>
<string name="download_start_error_offline_license">Failed to obtain offline license</string> <string name="download_start_error_offline_license">Failed to obtain offline license</string>

View File

@ -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.MediaSourceFactory;
import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.ShuffleOrder;
import com.google.android.exoplayer2.source.TrackGroupArray; 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.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector;
@ -92,7 +91,6 @@ import java.util.concurrent.TimeoutException;
private SeekParameters seekParameters; private SeekParameters seekParameters;
private ShuffleOrder shuffleOrder; private ShuffleOrder shuffleOrder;
private boolean pauseAtEndOfMediaItems; private boolean pauseAtEndOfMediaItems;
private boolean hasAdsMediaSource;
// Playback information when there is no pending seek/set source operation. // Playback information when there is no pending seek/set source operation.
private PlaybackInfo playbackInfo; private PlaybackInfo playbackInfo;
@ -435,7 +433,6 @@ import java.util.concurrent.TimeoutException;
@Override @Override
public void addMediaSources(int index, List<MediaSource> mediaSources) { public void addMediaSources(int index, List<MediaSource> mediaSources) {
Assertions.checkArgument(index >= 0); Assertions.checkArgument(index >= 0);
validateMediaSources(mediaSources, /* mediaSourceReplacement= */ false);
Timeline oldTimeline = getCurrentTimeline(); Timeline oldTimeline = getCurrentTimeline();
pendingOperationAcks++; pendingOperationAcks++;
List<MediaSourceList.MediaSourceHolder> holders = addMediaSourceHolders(index, mediaSources); List<MediaSourceList.MediaSourceHolder> holders = addMediaSourceHolders(index, mediaSources);
@ -1140,7 +1137,6 @@ import java.util.concurrent.TimeoutException;
int startWindowIndex, int startWindowIndex,
long startPositionMs, long startPositionMs,
boolean resetToDefaultPosition) { boolean resetToDefaultPosition) {
validateMediaSources(mediaSources, /* mediaSourceReplacement= */ true);
int currentWindowIndex = getCurrentWindowIndexInternal(); int currentWindowIndex = getCurrentWindowIndexInternal();
long currentPositionMs = getCurrentPosition(); long currentPositionMs = getCurrentPosition();
pendingOperationAcks++; pendingOperationAcks++;
@ -1239,39 +1235,6 @@ import java.util.concurrent.TimeoutException;
mediaSourceHolderSnapshots.remove(i); mediaSourceHolderSnapshots.remove(i);
} }
shuffleOrder = shuffleOrder.cloneAndRemove(fromIndex, toIndexExclusive); 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<MediaSource> 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() { private Timeline createMaskingTimeline() {

View File

@ -47,7 +47,6 @@ import android.media.AudioManager;
import android.net.Uri; import android.net.Uri;
import android.os.Looper; import android.os.Looper;
import android.view.Surface; import android.view.Surface;
import android.view.ViewGroup;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; 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.ClippingMediaSource;
import com.google.android.exoplayer2.source.CompositeMediaSource; import com.google.android.exoplayer2.source.CompositeMediaSource;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource; 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.LoopingMediaSource;
import com.google.android.exoplayer2.source.MaskingMediaSource; import com.google.android.exoplayer2.source.MaskingMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod; 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.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.ads.AdPlaybackState; 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.Action;
import com.google.android.exoplayer2.testutil.ActionSchedule; import com.google.android.exoplayer2.testutil.ActionSchedule;
import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; 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.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.Allocation; import com.google.android.exoplayer2.upstream.Allocation;
import com.google.android.exoplayer2.upstream.Allocator; 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.Loader;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
@ -5571,124 +5566,6 @@ public final class ExoPlayerTest {
assertThat(positionAfterSetShuffleOrder.get()).isAtLeast(5000); 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<MediaSource> 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 @Test
public void setMediaSources_empty_whenEmpty_correctMaskingWindowIndex() throws Exception { public void setMediaSources_empty_whenEmpty_correctMaskingWindowIndex() throws Exception {
Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); 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<AdsLoader.OverlayInfo> getAdOverlayInfos() {
return ImmutableList.of();
}
}
/** /**
* Returns an argument matcher for {@link Timeline} instances that ignores period and window uids. * Returns an argument matcher for {@link Timeline} instances that ignores period and window uids.
*/ */