Fix handling of repeated ads identifiers

Previously the `AdTagLoader` only had one listener which meant that updates
that should affect all periods with matching identifiers in the timeline only
affected the last-attached one. Fix this by having `AdTagLoader` track all its
listeners.

Issue: #3750
PiperOrigin-RevId: 347571323
This commit is contained in:
andrewlewis 2020-12-15 10:29:44 +00:00 committed by Christos Tsilopoulos
parent 9982e1f154
commit 0633778c70
5 changed files with 103 additions and 40 deletions

View file

@ -132,6 +132,7 @@ import java.util.Map;
private final Timeline.Period period;
private final Handler handler;
private final ComponentListener componentListener;
private final List<EventListener> eventListeners;
private final List<VideoAdPlayer.VideoAdPlayerCallback> adCallbacks;
private final Runnable updateAdProgressRunnable;
private final BiMap<AdMediaInfo, AdInfo> adInfoByAdMediaInfo;
@ -139,7 +140,6 @@ import java.util.Map;
private final AdsLoader adsLoader;
@Nullable private Object pendingAdRequestContext;
@Nullable private EventListener eventListener;
@Nullable private Player player;
private VideoProgressUpdate lastContentProgress;
private VideoProgressUpdate lastAdProgress;
@ -235,6 +235,7 @@ import java.util.Map;
period = new Timeline.Period();
handler = Util.createHandler(getImaLooper(), /* callback= */ null);
componentListener = new ComponentListener();
eventListeners = new ArrayList<>();
adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
if (configuration.applicationVideoAdPlayerCallback != null) {
adCallbacks.add(configuration.applicationVideoAdPlayerCallback);
@ -284,8 +285,16 @@ import java.util.Map;
* 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;
public void addListenerWithAdView(EventListener eventListener, AdViewProvider adViewProvider) {
boolean isStarted = !eventListeners.isEmpty();
eventListeners.add(eventListener);
if (isStarted) {
if (!AdPlaybackState.NONE.equals(adPlaybackState)) {
// Pass the existing ad playback state to the new listener.
eventListener.onAdPlaybackState(adPlaybackState);
}
return;
}
lastVolumePercent = 0;
lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
@ -350,9 +359,11 @@ import java.util.Map;
}
/** Stops passing of events from this instance and unregisters obstructions. */
public void stop() {
eventListener = null;
adDisplayContainer.unregisterAllFriendlyObstructions();
public void removeListener(EventListener eventListener) {
eventListeners.remove(eventListener);
if (eventListeners.isEmpty()) {
adDisplayContainer.unregisterAllFriendlyObstructions();
}
}
/** Releases all resources used by the ad tag loader. */
@ -713,13 +724,13 @@ import java.util.Map;
pauseContentInternal();
break;
case TAPPED:
if (eventListener != null) {
eventListener.onAdTapped();
for (int i = 0; i < eventListeners.size(); i++) {
eventListeners.get(i).onAdTapped();
}
break;
case CLICKED:
if (eventListener != null) {
eventListener.onAdClicked();
for (int i = 0; i < eventListeners.size(); i++) {
eventListeners.get(i).onAdClicked();
}
break;
case CONTENT_RESUME_REQUESTED:
@ -1115,15 +1126,16 @@ import java.util.Map;
}
private void updateAdPlaybackState() {
// Ignore updates while detached. When a player is attached it will receive the latest state.
if (eventListener != null) {
eventListener.onAdPlaybackState(adPlaybackState);
for (int i = 0; i < eventListeners.size(); i++) {
eventListeners.get(i).onAdPlaybackState(adPlaybackState);
}
}
private void maybeNotifyPendingAdLoadError() {
if (pendingAdLoadError != null && eventListener != null) {
eventListener.onAdLoadError(pendingAdLoadError, adTagDataSpec);
if (pendingAdLoadError != null) {
for (int i = 0; i < eventListeners.size(); i++) {
eventListeners.get(i).onAdLoadError(pendingAdLoadError, adTagDataSpec);
}
pendingAdLoadError = null;
}
}
@ -1136,9 +1148,12 @@ import java.util.Map;
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
}
updateAdPlaybackState();
if (eventListener != null) {
eventListener.onAdLoadError(
AdLoadException.createForUnexpected(new RuntimeException(message, cause)), adTagDataSpec);
for (int i = 0; i < eventListeners.size(); i++) {
eventListeners
.get(i)
.onAdLoadError(
AdLoadException.createForUnexpected(new RuntimeException(message, cause)),
adTagDataSpec);
}
}

View file

@ -530,16 +530,16 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
adTagLoader = adTagLoaderByAdsId.get(adsId);
}
adTagLoaderByAdsMediaSource.put(adsMediaSource, checkNotNull(adTagLoader));
checkNotNull(adTagLoader).start(adViewProvider, eventListener);
adTagLoader.addListenerWithAdView(eventListener, adViewProvider);
maybeUpdateCurrentAdTagLoader();
}
@Override
public void stop(AdsMediaSource adsMediaSource) {
public void stop(AdsMediaSource adsMediaSource, EventListener eventListener) {
@Nullable AdTagLoader removedAdTagLoader = adTagLoaderByAdsMediaSource.remove(adsMediaSource);
maybeUpdateCurrentAdTagLoader();
if (removedAdTagLoader != null) {
removedAdTagLoader.stop();
removedAdTagLoader.removeListener(eventListener);
}
if (player != null && adTagLoaderByAdsMediaSource.isEmpty()) {

View file

@ -952,7 +952,7 @@ public final class ImaAdsLoaderTest {
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
imaAdsLoader.requestAds(TEST_DATA_SPEC, TEST_ADS_ID, adViewGroup);
imaAdsLoader.stop(adsMediaSource);
imaAdsLoader.stop(adsMediaSource, adsLoaderListener);
InOrder inOrder = inOrder(mockAdDisplayContainer);
inOrder.verify(mockAdDisplayContainer).registerFriendlyObstruction(mockFriendlyObstruction);
@ -1115,8 +1115,8 @@ public final class ImaAdsLoaderTest {
secondAdsMediaSource, TEST_DATA_SPEC, secondAdsId, adViewProvider, secondAdsLoaderListener);
// Simulate backgrounding/resuming.
imaAdsLoader.stop(adsMediaSource);
imaAdsLoader.stop(secondAdsMediaSource);
imaAdsLoader.stop(adsMediaSource, adsLoaderListener);
imaAdsLoader.stop(secondAdsMediaSource, secondAdsLoaderListener);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
imaAdsLoader.start(
@ -1137,6 +1137,52 @@ public final class ImaAdsLoaderTest {
.isEqualTo(new AdPlaybackState(secondAdsId, /* adGroupTimesUs...= */ 0));
}
@Test
public void playbackWithTwoAdsMediaSourcesAndMatchingAdsIds_hasMatchingAdPlaybackState() {
AdsMediaSource secondAdsMediaSource =
new AdsMediaSource(
new FakeMediaSource(CONTENT_TIMELINE),
TEST_DATA_SPEC,
TEST_ADS_ID,
new DefaultMediaSourceFactory((Context) getApplicationContext()),
imaAdsLoader,
adViewProvider);
timelineWindowDefinitions =
new TimelineWindowDefinition[] {
getInitialTimelineWindowDefinition(TEST_ADS_ID),
getInitialTimelineWindowDefinition(TEST_ADS_ID)
};
TestAdsLoaderListener secondAdsLoaderListener = new TestAdsLoaderListener(/* periodIndex= */ 1);
// Load and play the preroll ad then content.
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
adEventListener.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd));
videoAdPlayer.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo);
imaAdsLoader.start(
secondAdsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, secondAdsLoaderListener);
adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd));
videoAdPlayer.playAd(TEST_AD_MEDIA_INFO);
fakePlayer.setPlayingAdPosition(
/* periodIndex= */ 0,
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
/* positionMs= */ 0,
/* contentPositionMs= */ 0);
fakePlayer.setState(Player.STATE_READY, /* playWhenReady= */ true);
adEventListener.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd));
adEventListener.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, mockPrerollSingleAd));
adEventListener.onAdEvent(getAdEvent(AdEventType.MIDPOINT, mockPrerollSingleAd));
adEventListener.onAdEvent(getAdEvent(AdEventType.THIRD_QUARTILE, mockPrerollSingleAd));
fakePlayer.setPlayingContentPosition(/* periodIndex= */ 0, /* positionMs= */ 0);
videoAdPlayer.stopAd(TEST_AD_MEDIA_INFO);
adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
// Verify that the ad playback states for the two periods match.
assertThat(getAdPlaybackState(/* periodIndex= */ 0))
.isEqualTo(getAdPlaybackState(/* periodIndex= */ 1));
}
@Test
public void buildWithDefaultEnableContinuousPlayback_doesNotSetAdsRequestProperty() {
imaAdsLoader =

View file

@ -40,11 +40,11 @@ import java.util.List;
*
* <p>{@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(AdsMediaSource)} 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)}.
* player enters the background, {@link #stop(AdsMediaSource, EventListener)} 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)}.
*
* <p>If {@link EventListener#onAdPlaybackState(AdPlaybackState)} has been called, the
* implementation of {@link #start(AdsMediaSource, DataSpec, Object, AdViewProvider, EventListener)}
@ -220,8 +220,9 @@ public interface AdsLoader {
* thread by {@link AdsMediaSource}.
*
* @param adsMediaSource The ads media source requesting to stop loading/playing ads.
* @param eventListener The ads media source's listener for ads loader events.
*/
void stop(AdsMediaSource adsMediaSource);
void stop(AdsMediaSource adsMediaSource, EventListener eventListener);
/**
* Notifies the ads loader that preparation of an ad media period is complete. Called on the main

View file

@ -252,12 +252,13 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
@Override
protected void releaseSourceInternal() {
super.releaseSourceInternal();
checkNotNull(componentListener).release();
componentListener = null;
ComponentListener componentListener = checkNotNull(this.componentListener);
this.componentListener = null;
componentListener.stop();
contentTimeline = null;
adPlaybackState = null;
adMediaSourceHolders = new AdMediaSourceHolder[0][];
mainHandler.post(() -> adsLoader.stop(/* adsMediaSource= */ this));
mainHandler.post(() -> adsLoader.stop(/* adsMediaSource= */ this, componentListener));
}
@Override
@ -355,7 +356,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
private final Handler playerHandler;
private volatile boolean released;
private volatile boolean stopped;
/**
* Creates new listener which forwards ad playback states on the creating thread and all other
@ -365,20 +366,20 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
playerHandler = Util.createHandlerForCurrentLooper();
}
/** Releases the component listener. */
public void release() {
released = true;
/** Stops event delivery from this instance. */
public void stop() {
stopped = true;
playerHandler.removeCallbacksAndMessages(null);
}
@Override
public void onAdPlaybackState(final AdPlaybackState adPlaybackState) {
if (released) {
if (stopped) {
return;
}
playerHandler.post(
() -> {
if (released) {
if (stopped) {
return;
}
AdsMediaSource.this.onAdPlaybackState(adPlaybackState);
@ -387,7 +388,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
@Override
public void onAdLoadError(final AdLoadException error, DataSpec dataSpec) {
if (released) {
if (stopped) {
return;
}
createEventDispatcher(/* mediaPeriodId= */ null)