Commit 05f6d248 by andrewlewis Committed by kim-vde

Add support for ad playlists with `ImaAdsLoader`

Issue: #3750
PiperOrigin-RevId: 343878310
parent 689e89e5
......@@ -17,8 +17,8 @@ package com.google.android.exoplayer2.ext.ima;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.BITRATE_UNSET;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.TIMEOUT_UNSET;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupTimesUsForCuePoints;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.getImaLooper;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static java.lang.Math.max;
......@@ -280,11 +280,11 @@ import java.util.Map;
}
}
/** Starts using the ads loader for playback. */
public void start(Player player, AdViewProvider adViewProvider, EventListener eventListener) {
this.player = player;
player.addListener(this);
boolean playWhenReady = player.getPlayWhenReady();
/**
* Starts passing events from this instance (including any pending ad playback state) and
* registers obstructions.
*/
public void start(AdViewProvider adViewProvider, EventListener eventListener) {
this.eventListener = eventListener;
lastVolumePercent = 0;
lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
......@@ -293,13 +293,9 @@ import java.util.Map;
if (!AdPlaybackState.NONE.equals(adPlaybackState)) {
// Pass the ad playback state to the player, and resume ads if necessary.
eventListener.onAdPlaybackState(adPlaybackState);
if (adsManager != null && imaPausedContent && playWhenReady) {
adsManager.resume();
}
} else if (adsManager != null) {
adPlaybackState =
new AdPlaybackState(
adsId, ImaUtil.getAdGroupTimesUsForCuePoints(adsManager.getAdCuePoints()));
new AdPlaybackState(adsId, getAdGroupTimesUsForCuePoints(adsManager.getAdCuePoints()));
updateAdPlaybackState();
}
for (OverlayInfo overlayInfo : adViewProvider.getAdOverlayInfos()) {
......@@ -311,14 +307,36 @@ import java.util.Map;
}
}
/** Stops using the ads loader for playback. */
public void stop() {
@Nullable Player player = this.player;
if (player == null) {
return;
/**
* Populates the ad playback state with loaded cue points, if available. Any preroll will be
* paused immediately while waiting for this instance to be {@link #activate(Player) activated}.
*/
public void maybePreloadAds(long contentPositionMs, long contentDurationMs) {
maybeInitializeAdsManager(contentPositionMs, contentDurationMs);
}
/** Activates playback. */
public void activate(Player player) {
this.player = player;
player.addListener(this);
boolean playWhenReady = player.getPlayWhenReady();
onTimelineChanged(player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
if (!AdPlaybackState.NONE.equals(adPlaybackState)
&& adsManager != null
&& imaPausedContent
&& playWhenReady) {
adsManager.resume();
}
}
if (adsManager != null && imaPausedContent) {
/** Deactivates playback. */
public void deactivate() {
Player player = checkNotNull(this.player);
if (!AdPlaybackState.NONE.equals(adPlaybackState) && imaPausedContent) {
if (adsManager != null) {
adsManager.pause();
}
adPlaybackState =
adPlaybackState.withAdResumePositionUs(
playingAd ? C.msToUs(player.getCurrentPosition()) : 0);
......@@ -326,10 +344,15 @@ import java.util.Map;
lastVolumePercent = getPlayerVolumePercent();
lastAdProgress = getAdVideoProgressUpdate();
lastContentProgress = getContentVideoProgressUpdate();
adDisplayContainer.unregisterAllFriendlyObstructions();
player.removeListener(this);
this.player = null;
}
/** Stops passing of events from this instance and unregisters obstructions. */
public void stop() {
eventListener = null;
adDisplayContainer.unregisterAllFriendlyObstructions();
}
/** Releases all resources used by the ad tag loader. */
......@@ -392,7 +415,6 @@ import java.util.Map;
// The player is being reset or contains no media.
return;
}
checkArgument(timeline.getPeriodCount() == 1);
this.timeline = timeline;
Player player = checkNotNull(this.player);
long contentDurationUs = timeline.getPeriod(player.getCurrentPeriodIndex(), period).durationUs;
......@@ -592,14 +614,13 @@ import java.util.Map;
}
private VideoProgressUpdate getContentVideoProgressUpdate() {
if (player == null) {
return lastContentProgress;
}
boolean hasContentDuration = contentDurationMs != C.TIME_UNSET;
long contentPositionMs;
if (pendingContentPositionMs != C.TIME_UNSET) {
sentPendingContentPositionMs = true;
contentPositionMs = pendingContentPositionMs;
} else if (player == null) {
return lastContentProgress;
} else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {
long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;
......@@ -923,7 +944,8 @@ import java.util.Map;
adCallbacks.get(i).onResume(adMediaInfo);
}
}
if (!checkNotNull(player).getPlayWhenReady()) {
if (player == null || !player.getPlayWhenReady()) {
// Either this loader hasn't been activated yet, or the player is paused now.
checkNotNull(adsManager).pause();
}
}
......@@ -941,7 +963,14 @@ import java.util.Map;
// to a different position, so drop the event. See also [Internal: b/159111848].
return;
}
checkState(adMediaInfo.equals(imaAdMediaInfo));
if (configuration.debugModeEnabled && !adMediaInfo.equals(imaAdMediaInfo)) {
Log.w(
TAG,
"Unexpected pauseAd for "
+ getAdMediaInfoString(adMediaInfo)
+ ", expected "
+ getAdMediaInfoString(imaAdMediaInfo));
}
imaAdState = IMA_AD_STATE_PAUSED;
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onPause(adMediaInfo);
......@@ -1157,9 +1186,13 @@ import java.util.Map;
throw new IllegalStateException("Failed to find cue point");
}
private String getAdMediaInfoString(AdMediaInfo adMediaInfo) {
private String getAdMediaInfoString(@Nullable AdMediaInfo adMediaInfo) {
@Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo);
return "AdMediaInfo[" + adMediaInfo.getUrl() + (adInfo != null ? ", " + adInfo : "") + "]";
return "AdMediaInfo["
+ (adMediaInfo == null ? "null" : adMediaInfo.getUrl())
+ ", "
+ adInfo
+ "]";
}
private static long getContentPeriodPositionMs(
......@@ -1226,18 +1259,14 @@ import java.util.Map;
if (configuration.applicationAdEventListener != null) {
adsManager.addAdEventListener(configuration.applicationAdEventListener);
}
if (player != null) {
// If a player is attached already, start playback immediately.
try {
adPlaybackState =
new AdPlaybackState(
adsId, ImaUtil.getAdGroupTimesUsForCuePoints(adsManager.getAdCuePoints()));
new AdPlaybackState(adsId, getAdGroupTimesUsForCuePoints(adsManager.getAdCuePoints()));
updateAdPlaybackState();
} catch (RuntimeException e) {
maybeNotifyInternalError("onAdsManagerLoaded", e);
}
}
}
// ContentProgressProvider implementation.
......
......@@ -44,6 +44,7 @@ import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
......@@ -57,6 +58,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Set;
......@@ -371,12 +373,16 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
private final ImaUtil.Configuration configuration;
private final Context context;
private final ImaUtil.ImaFactory imaFactory;
private final HashMap<Object, AdTagLoader> adTagLoaderByAdsId;
private final HashMap<AdsMediaSource, AdTagLoader> adTagLoaderByAdsMediaSource;
private final Timeline.Period period;
private final Timeline.Window window;
private boolean wasSetPlayerCalled;
@Nullable private Player nextPlayer;
@Nullable private AdTagLoader adTagLoader;
private List<String> supportedMimeTypes;
@Nullable private Player player;
@Nullable private AdTagLoader currentAdTagLoader;
private ImaAdsLoader(
Context context, ImaUtil.Configuration configuration, ImaUtil.ImaFactory imaFactory) {
......@@ -384,6 +390,10 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
this.configuration = configuration;
this.imaFactory = imaFactory;
supportedMimeTypes = ImmutableList.of();
adTagLoaderByAdsId = new HashMap<>();
adTagLoaderByAdsMediaSource = new HashMap<>();
period = new Timeline.Period();
window = new Timeline.Window();
}
/**
......@@ -394,7 +404,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
@SuppressWarnings("nullness:nullness.on.outer")
@Nullable
public com.google.ads.interactivemedia.v3.api.AdsLoader getAdsLoader() {
return adTagLoader != null ? adTagLoader.getAdsLoader() : null;
return currentAdTagLoader != null ? currentAdTagLoader.getAdsLoader() : null;
}
/**
......@@ -410,7 +420,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
*/
@Nullable
public AdDisplayContainer getAdDisplayContainer() {
return adTagLoader != null ? adTagLoader.getAdDisplayContainer() : null;
return currentAdTagLoader != null ? currentAdTagLoader.getAdDisplayContainer() : null;
}
/**
......@@ -427,8 +437,8 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
* null} if playing audio-only ads.
*/
public void requestAds(DataSpec adTagDataSpec, Object adsId, @Nullable ViewGroup adViewGroup) {
if (adTagLoader == null) {
adTagLoader =
if (!adTagLoaderByAdsId.containsKey(adsId)) {
AdTagLoader adTagLoader =
new AdTagLoader(
context,
configuration,
......@@ -437,6 +447,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
adTagDataSpec,
adsId,
adViewGroup);
adTagLoaderByAdsId.put(adsId, adTagLoader);
}
}
......@@ -448,8 +459,8 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
* IMA SDK provides the UI to skip ads in the ad view group passed via {@link AdViewProvider}.
*/
public void skipAd() {
if (adTagLoader != null) {
adTagLoader.skipAd();
if (currentAdTagLoader != null) {
currentAdTagLoader.skipAd();
}
}
......@@ -494,37 +505,67 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
EventListener eventListener) {
checkState(
wasSetPlayerCalled, "Set player using adsLoader.setPlayer before preparing the player.");
if (adTagLoaderByAdsMediaSource.isEmpty()) {
player = nextPlayer;
@Nullable Player player = this.player;
if (player == null) {
return;
}
player.addListener(this);
}
@Nullable AdTagLoader adTagLoader = adTagLoaderByAdsId.get(adsId);
if (adTagLoader == null) {
requestAds(adTagDataSpec, adsId, adViewProvider.getAdViewGroup());
adTagLoader = adTagLoaderByAdsId.get(adsId);
}
checkNotNull(adTagLoader).start(player, adViewProvider, eventListener);
adTagLoaderByAdsMediaSource.put(adsMediaSource, checkNotNull(adTagLoader));
checkNotNull(adTagLoader).start(adViewProvider, eventListener);
maybeUpdateCurrentAdTagLoader();
}
@Override
public void stop(AdsMediaSource adsMediaSource) {
if (player != null && adTagLoader != null) {
adTagLoader.stop();
@Nullable AdTagLoader removedAdTagLoader = adTagLoaderByAdsMediaSource.remove(adsMediaSource);
maybeUpdateCurrentAdTagLoader();
if (removedAdTagLoader != null) {
removedAdTagLoader.stop();
}
if (player != null && adTagLoaderByAdsMediaSource.isEmpty()) {
player.removeListener(this);
player = null;
}
}
@Override
public void release() {
if (adTagLoader != null) {
if (player != null) {
player.removeListener(this);
player = null;
maybeUpdateCurrentAdTagLoader();
}
nextPlayer = null;
for (AdTagLoader adTagLoader : adTagLoaderByAdsMediaSource.values()) {
adTagLoader.release();
}
adTagLoaderByAdsMediaSource.clear();
for (AdTagLoader adTagLoader : adTagLoaderByAdsId.values()) {
adTagLoader.release();
}
adTagLoaderByAdsId.clear();
}
@Override
public void handlePrepareComplete(
AdsMediaSource adsMediaSource, int adGroupIndex, int adIndexInAdGroup) {
if (adTagLoader != null) {
adTagLoader.handlePrepareComplete(adGroupIndex, adIndexInAdGroup);
if (player == null) {
return;
}
checkNotNull(adTagLoaderByAdsMediaSource.get(adsMediaSource))
.handlePrepareComplete(adGroupIndex, adIndexInAdGroup);
}
@Override
......@@ -533,9 +574,112 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
int adGroupIndex,
int adIndexInAdGroup,
IOException exception) {
if (adTagLoader != null) {
adTagLoader.handlePrepareError(adGroupIndex, adIndexInAdGroup, exception);
if (player == null) {
return;
}
checkNotNull(adTagLoaderByAdsMediaSource.get(adsMediaSource))
.handlePrepareError(adGroupIndex, adIndexInAdGroup, exception);
}
// Player.EventListener implementation.
@Override
public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
if (timeline.isEmpty()) {
// The player is being reset or contains no media.
return;
}
maybeUpdateCurrentAdTagLoader();
maybePreloadNextPeriodAds();
}
@Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
maybeUpdateCurrentAdTagLoader();
maybePreloadNextPeriodAds();
}
@Override
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
maybePreloadNextPeriodAds();
}
@Override
public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
maybePreloadNextPeriodAds();
}
// Internal methods.
private void maybeUpdateCurrentAdTagLoader() {
@Nullable AdTagLoader oldAdTagLoader = currentAdTagLoader;
@Nullable AdTagLoader newAdTagLoader = getCurrentAdTagLoader();
if (!Util.areEqual(oldAdTagLoader, newAdTagLoader)) {
if (oldAdTagLoader != null) {
oldAdTagLoader.deactivate();
}
currentAdTagLoader = newAdTagLoader;
if (newAdTagLoader != null) {
newAdTagLoader.activate(checkNotNull(player));
}
}
}
@Nullable
private AdTagLoader getCurrentAdTagLoader() {
@Nullable Player player = this.player;
if (player == null) {
return null;
}
Timeline timeline = player.getCurrentTimeline();
if (timeline.isEmpty()) {
return null;
}
int periodIndex = player.getCurrentPeriodIndex();
@Nullable Object adsId = timeline.getPeriod(periodIndex, period).getAdsId();
if (adsId == null) {
return null;
}
@Nullable AdTagLoader adTagLoader = adTagLoaderByAdsId.get(adsId);
if (adTagLoader == null || !adTagLoaderByAdsMediaSource.containsValue(adTagLoader)) {
return null;
}
return adTagLoader;
}
private void maybePreloadNextPeriodAds() {
@Nullable Player player = this.player;
if (player == null) {
return;
}
Timeline timeline = player.getCurrentTimeline();
if (timeline.isEmpty()) {
return;
}
int nextPeriodIndex =
timeline.getNextPeriodIndex(
player.getCurrentPeriodIndex(),
period,
window,
player.getRepeatMode(),
player.getShuffleModeEnabled());
if (nextPeriodIndex == C.INDEX_UNSET) {
return;
}
timeline.getPeriod(nextPeriodIndex, period);
@Nullable Object nextAdsId = period.getAdsId();
if (nextAdsId == null) {
return;
}
@Nullable AdTagLoader nextAdTagLoader = adTagLoaderByAdsId.get(nextAdsId);
if (nextAdTagLoader == null || nextAdTagLoader == currentAdTagLoader) {
return;
}
long periodPositionUs =
timeline.getPeriodPosition(
window, period, period.windowIndex, /* windowPositionUs= */ C.TIME_UNSET)
.second;
nextAdTagLoader.maybePreloadAds(C.usToMs(periodPositionUs), C.usToMs(period.durationUs));
}
/**
......
......@@ -21,25 +21,30 @@ import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.testutil.StubExoPlayer;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import java.util.ArrayList;
import com.google.android.exoplayer2.util.ListenerSet;
/** A fake player for testing content/ad playback. */
/* package */ final class FakePlayer extends StubExoPlayer {
private final ArrayList<Player.EventListener> listeners;
private final ListenerSet<EventListener, Events> listeners;
private final Timeline.Period period;
private final Timeline timeline;
private Timeline timeline;
@Player.State private int state;
private boolean playWhenReady;
private long position;
private long contentPosition;
private int periodIndex;
private long positionMs;
private long contentPositionMs;
private boolean isPlayingAd;
private int adGroupIndex;
private int adIndexInAdGroup;
public FakePlayer() {
listeners = new ArrayList<>();
listeners =
new ListenerSet<>(
Looper.getMainLooper(),
Player.Events::new,
(listener, eventFlags) -> listener.onEvents(/* player= */ this, eventFlags));
period = new Timeline.Period();
state = Player.STATE_IDLE;
playWhenReady = true;
......@@ -48,26 +53,27 @@ import java.util.ArrayList;
/** Sets the timeline on this fake player, which notifies listeners with the changed timeline. */
public void updateTimeline(Timeline timeline, @TimelineChangeReason int reason) {
for (Player.EventListener listener : listeners) {
listener.onTimelineChanged(timeline, reason);
}
this.timeline = timeline;
listeners.sendEvent(
Player.EVENT_TIMELINE_CHANGED, listener -> listener.onTimelineChanged(timeline, reason));
}
/**
* Sets the state of this player as if it were playing content at the given {@code position}. If
* an ad is currently playing, this will trigger a position discontinuity.
*/
public void setPlayingContentPosition(long position) {
public void setPlayingContentPosition(int periodIndex, long positionMs) {
boolean notify = isPlayingAd;
isPlayingAd = false;
adGroupIndex = C.INDEX_UNSET;
adIndexInAdGroup = C.INDEX_UNSET;
this.position = position;
contentPosition = position;
this.periodIndex = periodIndex;
this.positionMs = positionMs;
contentPositionMs = positionMs;
if (notify) {
for (Player.EventListener listener : listeners) {
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION);
}
listeners.sendEvent(
Player.EVENT_POSITION_DISCONTINUITY,
listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION));
}
}
......@@ -77,17 +83,22 @@ import java.util.ArrayList;
* position discontinuity.
*/
public void setPlayingAdPosition(
int adGroupIndex, int adIndexInAdGroup, long position, long contentPosition) {
int periodIndex,
int adGroupIndex,
int adIndexInAdGroup,
long positionMs,
long contentPositionMs) {
boolean notify = !isPlayingAd || this.adIndexInAdGroup != adIndexInAdGroup;
isPlayingAd = true;
this.periodIndex = periodIndex;
this.adGroupIndex = adGroupIndex;
this.adIndexInAdGroup = adIndexInAdGroup;
this.position = position;
this.contentPosition = contentPosition;
this.positionMs = positionMs;
this.contentPositionMs = contentPositionMs;
if (notify) {
for (Player.EventListener listener : listeners) {
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION);
}
listeners.sendEvent(
EVENT_POSITION_DISCONTINUITY,
listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION));
}
}
......@@ -99,7 +110,9 @@ import java.util.ArrayList;
this.state = state;
this.playWhenReady = playWhenReady;
if (playbackStateChanged || playWhenReadyChanged) {
for (Player.EventListener listener : listeners) {
listeners.sendEvent(
Player.EVENT_PLAYBACK_STATE_CHANGED,
listener -> {
listener.onPlayerStateChanged(playWhenReady, state);
if (playbackStateChanged) {
listener.onPlaybackStateChanged(state);
......@@ -108,7 +121,7 @@ import java.util.ArrayList;
listener.onPlayWhenReadyChanged(
playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
}
}
});
}
}
......@@ -146,6 +159,17 @@ import java.util.ArrayList;
}
@Override
@RepeatMode
public int getRepeatMode() {
return REPEAT_MODE_OFF;
}
@Override
public boolean getShuffleModeEnabled() {
return false;
}
@Override
public int getRendererCount() {
return 0;
}
......@@ -162,7 +186,7 @@ import java.util.ArrayList;
@Override
public int getCurrentPeriodIndex() {
return 0;
return periodIndex;
}
@Override
......@@ -186,7 +210,7 @@ import java.util.ArrayList;
@Override
public long getCurrentPosition() {
return position;
return positionMs;
}
@Override
......@@ -206,6 +230,6 @@ import java.util.ArrayList;
@Override
public long getContentPosition() {
return contentPosition;
return contentPositionMs;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment