mirror of
https://github.com/androidx/media.git
synced 2025-04-29 22:36:54 +08:00
Ensure ad playback state and timeline are in sync
Before this change a timeline update of a live content source has produced a timeline refresh before passing the timeline to the ads loader. When in such a case the ads loader updates the ad playback state, a second timeline refresh is trigger that then includes the updated ad data also. This can result in a timeline being pulished with stale ad information. This change prevents this by introducing a boolean return value that requires the ads loader to signal whether the ad playback state has been passed back to the source. This ensures that an update of timeline and ad playback state produces a single timeline update and is published in sync. PiperOrigin-RevId: 748288650
This commit is contained in:
parent
f860fb156e
commit
fd8547fc3a
@ -26,6 +26,7 @@ import android.os.IBinder;
|
|||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media3.common.MediaItem.LocalConfiguration;
|
||||||
import androidx.media3.common.util.Assertions;
|
import androidx.media3.common.util.Assertions;
|
||||||
import androidx.media3.common.util.BundleCollectionUtil;
|
import androidx.media3.common.util.BundleCollectionUtil;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
@ -175,14 +176,22 @@ public abstract class Timeline {
|
|||||||
public Object uid;
|
public Object uid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Use {@link #mediaItem} instead.
|
* @deprecated Use {@link LocalConfiguration#tag} of {@link #mediaItem} instead.
|
||||||
*/
|
*/
|
||||||
@UnstableApi @Deprecated @Nullable public Object tag;
|
@UnstableApi @Deprecated @Nullable public Object tag;
|
||||||
|
|
||||||
/** The {@link MediaItem} associated to the window. Not necessarily unique. */
|
/** The {@link MediaItem} associated to the window. Not necessarily unique. */
|
||||||
public MediaItem mediaItem;
|
public MediaItem mediaItem;
|
||||||
|
|
||||||
/** The manifest of the window. May be {@code null}. */
|
/**
|
||||||
|
* The manifest of the window. May be {@code null}.
|
||||||
|
*
|
||||||
|
* <p>The concrete type depends on the media sources producing the timeline window. Examples
|
||||||
|
* provided by Media3 media source modules are {@code
|
||||||
|
* androidx.media3.exoplayer.dash.manifest.DashManifest}, {@code
|
||||||
|
* androidx.media3.exoplayer.hls.HlsManifest} and {@code
|
||||||
|
* androidx.media3.exoplayer.smoothstreaming.SSManifest}.
|
||||||
|
*/
|
||||||
@Nullable public Object manifest;
|
@Nullable public Object manifest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -263,7 +272,7 @@ public abstract class Timeline {
|
|||||||
/** Sets the data held by this window. */
|
/** Sets the data held by this window. */
|
||||||
@CanIgnoreReturnValue
|
@CanIgnoreReturnValue
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation") // Using Window.tag for backwards compatibility
|
||||||
public Window set(
|
public Window set(
|
||||||
Object uid,
|
Object uid,
|
||||||
@Nullable MediaItem mediaItem,
|
@Nullable MediaItem mediaItem,
|
||||||
|
@ -134,6 +134,12 @@ public interface AdsLoader {
|
|||||||
/**
|
/**
|
||||||
* Starts using the ads loader for playback. Called on the main thread by {@link AdsMediaSource}.
|
* Starts using the ads loader for playback. Called on the main thread by {@link AdsMediaSource}.
|
||||||
*
|
*
|
||||||
|
* <p>Requests the ads loader to start loading ad data from the provided {@link DataSpec
|
||||||
|
* adTagDataSpec}. Publishing an initial {@link AdPlaybackState} to provided {@link
|
||||||
|
* EventListener#onAdPlaybackState(AdPlaybackState) eventListener} is required to start playback.
|
||||||
|
* In the case of a pre roll, this ensures that the player doesn't briefly start playing content
|
||||||
|
* before ad data is available.
|
||||||
|
*
|
||||||
* @param adsMediaSource The ads media source requesting to start loading ads.
|
* @param adsMediaSource The ads media source requesting to start loading ads.
|
||||||
* @param adTagDataSpec A data spec for the ad tag to load.
|
* @param adTagDataSpec A data spec for the ad tag to load.
|
||||||
* @param adsId An opaque identifier for the ad playback state across start/stop calls.
|
* @param adsId An opaque identifier for the ad playback state across start/stop calls.
|
||||||
@ -162,18 +168,39 @@ public interface AdsLoader {
|
|||||||
* Notifies the ads loader when the content source has changed its timeline. Called on the main
|
* Notifies the ads loader when the content source has changed its timeline. Called on the main
|
||||||
* thread by {@link AdsMediaSource}.
|
* thread by {@link AdsMediaSource}.
|
||||||
*
|
*
|
||||||
* <p>If you override this callback for the purpose of reading ad data from the timeline to
|
* <p>The default implementation returns false which makes the content timeline immediately being
|
||||||
* populate the {@link AdPlaybackState} with, you need to pass true to the constructor of {@link
|
* reported to the player.
|
||||||
|
*
|
||||||
|
* <p>When overriding this method for the purpose of reading ad data from the timeline to populate
|
||||||
|
* the {@link AdPlaybackState} with, false needs to be passed to the constructor of {@link
|
||||||
* AdsMediaSource#AdsMediaSource(MediaSource, DataSpec, Object, MediaSource.Factory, AdsLoader,
|
* AdsMediaSource#AdsMediaSource(MediaSource, DataSpec, Object, MediaSource.Factory, AdsLoader,
|
||||||
* AdViewProvider, boolean) AdsMediaSource} to indicate the content source needs to be prepared
|
* AdViewProvider, boolean) AdsMediaSource} to indicate that the content source needs to be
|
||||||
* upfront.
|
* prepared upfront. This way an ads loader can defer calling {@link
|
||||||
|
* EventListener#onAdPlaybackState(AdPlaybackState)} until the ad data from the timeline is
|
||||||
|
* available and populate the initial ad playback state with that data before publishing.
|
||||||
|
*
|
||||||
|
* <p>For live streams, this method is called additional times when the content source reports an
|
||||||
|
* advancing {@linkplain Timeline live window} with new available media and/or new ad data in the
|
||||||
|
* manifest. If in such a case, the ads loader as a result calls {@link
|
||||||
|
* EventListener#onAdPlaybackState(AdPlaybackState)}, true must be returned. This prevents the
|
||||||
|
* timeline being reported with stale ad data. Conversely, when the ad playback state is not
|
||||||
|
* passed into {@link EventListener#onAdPlaybackState(AdPlaybackState)}, false must be returned to
|
||||||
|
* not drop a timeline update that needs to be published to the player.
|
||||||
|
*
|
||||||
|
* <p>Generally, if the timeline is not required to populate the ad playback state, {@link
|
||||||
|
* #start(AdsMediaSource, DataSpec, Object, AdViewProvider, EventListener)} should be used to
|
||||||
|
* initiate loading ad data and publish the first ad playback state as early as possible. This
|
||||||
|
* method can still be overridden for informational or other purpose. In this case, false is
|
||||||
|
* returned here and the {@link AdsMediaSource} is used with lazy preparation enabled.
|
||||||
*
|
*
|
||||||
* @param adsMediaSource The ads media source for which the content timeline changed.
|
* @param adsMediaSource The ads media source for which the content timeline changed.
|
||||||
* @param timeline The timeline of the content source.
|
* @param timeline The timeline of the content source.
|
||||||
|
* @return true If {@link EventListener#onAdPlaybackState(AdPlaybackState)} is or will be called,
|
||||||
|
* false otherwise.
|
||||||
*/
|
*/
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
default void handleContentTimelineChanged(AdsMediaSource adsMediaSource, Timeline timeline) {
|
default boolean handleContentTimelineChanged(AdsMediaSource adsMediaSource, Timeline timeline) {
|
||||||
// Do nothing.
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -147,12 +147,14 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||||||
private final Object adsId;
|
private final Object adsId;
|
||||||
private final Handler mainHandler;
|
private final Handler mainHandler;
|
||||||
private final Timeline.Period period;
|
private final Timeline.Period period;
|
||||||
|
private final boolean useLazyContentSourcePreparation;
|
||||||
|
|
||||||
// Accessed on the player thread.
|
// Accessed on the player thread.
|
||||||
@Nullable private ComponentListener componentListener;
|
@Nullable private ComponentListener componentListener;
|
||||||
@Nullable private Timeline contentTimeline;
|
@Nullable private Timeline contentTimeline;
|
||||||
@Nullable private AdPlaybackState adPlaybackState;
|
@Nullable private AdPlaybackState adPlaybackState;
|
||||||
private @NullableType AdMediaSourceHolder[][] adMediaSourceHolders;
|
private @NullableType AdMediaSourceHolder[][] adMediaSourceHolders;
|
||||||
|
@Nullable private Handler playerHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new source that inserts ads linearly with the content specified by {@code
|
* Constructs a new source that inserts ads linearly with the content specified by {@code
|
||||||
@ -205,8 +207,8 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||||||
* @param useLazyContentSourcePreparation True if the content source should be prepared lazily and
|
* @param useLazyContentSourcePreparation True if the content source should be prepared lazily and
|
||||||
* wait for an {@link AdPlaybackState} to be set before preparing. False if the timeline is
|
* wait for an {@link AdPlaybackState} to be set before preparing. False if the timeline is
|
||||||
* required {@linkplain AdsLoader#handleContentTimelineChanged(AdsMediaSource, Timeline) to
|
* required {@linkplain AdsLoader#handleContentTimelineChanged(AdsMediaSource, Timeline) to
|
||||||
* read ad data from it} to populate the {@link AdPlaybackState} (for instance from HLS
|
* read ad data from it} to populate the {@link AdPlaybackState} (See {@link
|
||||||
* interstitials).
|
* Timeline.Window#manifest} also).
|
||||||
*/
|
*/
|
||||||
public AdsMediaSource(
|
public AdsMediaSource(
|
||||||
MediaSource contentMediaSource,
|
MediaSource contentMediaSource,
|
||||||
@ -216,6 +218,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||||||
AdsLoader adsLoader,
|
AdsLoader adsLoader,
|
||||||
AdViewProvider adViewProvider,
|
AdViewProvider adViewProvider,
|
||||||
boolean useLazyContentSourcePreparation) {
|
boolean useLazyContentSourcePreparation) {
|
||||||
|
this.useLazyContentSourcePreparation = useLazyContentSourcePreparation;
|
||||||
this.contentMediaSource =
|
this.contentMediaSource =
|
||||||
new MaskingMediaSource(
|
new MaskingMediaSource(
|
||||||
contentMediaSource, /* useLazyPreparation= */ useLazyContentSourcePreparation);
|
contentMediaSource, /* useLazyPreparation= */ useLazyContentSourcePreparation);
|
||||||
@ -256,7 +259,8 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||||||
@Override
|
@Override
|
||||||
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
||||||
super.prepareSourceInternal(mediaTransferListener);
|
super.prepareSourceInternal(mediaTransferListener);
|
||||||
ComponentListener componentListener = new ComponentListener();
|
this.playerHandler = Util.createHandlerForCurrentLooper();
|
||||||
|
ComponentListener componentListener = new ComponentListener(playerHandler);
|
||||||
this.componentListener = componentListener;
|
this.componentListener = componentListener;
|
||||||
contentTimeline = contentMediaSource.getTimeline();
|
contentTimeline = contentMediaSource.getTimeline();
|
||||||
prepareChildSource(CHILD_SOURCE_MEDIA_PERIOD_ID, contentMediaSource);
|
prepareChildSource(CHILD_SOURCE_MEDIA_PERIOD_ID, contentMediaSource);
|
||||||
@ -320,6 +324,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||||||
super.releaseSourceInternal();
|
super.releaseSourceInternal();
|
||||||
ComponentListener componentListener = checkNotNull(this.componentListener);
|
ComponentListener componentListener = checkNotNull(this.componentListener);
|
||||||
this.componentListener = null;
|
this.componentListener = null;
|
||||||
|
this.playerHandler = null;
|
||||||
componentListener.stop();
|
componentListener.stop();
|
||||||
contentTimeline = null;
|
contentTimeline = null;
|
||||||
adPlaybackState = null;
|
adPlaybackState = null;
|
||||||
@ -335,12 +340,26 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||||||
int adIndexInAdGroup = childSourceId.adIndexInAdGroup;
|
int adIndexInAdGroup = childSourceId.adIndexInAdGroup;
|
||||||
checkNotNull(adMediaSourceHolders[adGroupIndex][adIndexInAdGroup])
|
checkNotNull(adMediaSourceHolders[adGroupIndex][adIndexInAdGroup])
|
||||||
.handleSourceInfoRefresh(newTimeline);
|
.handleSourceInfoRefresh(newTimeline);
|
||||||
|
maybeUpdateSourceInfo();
|
||||||
} else {
|
} else {
|
||||||
Assertions.checkArgument(newTimeline.getPeriodCount() == 1);
|
Assertions.checkArgument(newTimeline.getPeriodCount() == 1);
|
||||||
contentTimeline = newTimeline;
|
contentTimeline = newTimeline;
|
||||||
mainHandler.post(() -> adsLoader.handleContentTimelineChanged(this, newTimeline));
|
mainHandler.post(
|
||||||
|
() -> {
|
||||||
|
boolean sourceInfoUpdated = adsLoader.handleContentTimelineChanged(this, newTimeline);
|
||||||
|
// The ad playback state must not be updated when lazy preparation is used.
|
||||||
|
checkState(!sourceInfoUpdated || !useLazyContentSourcePreparation);
|
||||||
|
// If the source isn't updated by the ads loader we do, if not already published.
|
||||||
|
if (!sourceInfoUpdated && !useLazyContentSourcePreparation) {
|
||||||
|
checkNotNull(playerHandler).post(this::maybeUpdateSourceInfo);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (useLazyContentSourcePreparation) {
|
||||||
|
// If lazy preparation is used, the ads loader is not allowed to update the ad playback
|
||||||
|
// state on timeline change. We can synchronously publish the timeline as early as possible.
|
||||||
|
maybeUpdateSourceInfo();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
maybeUpdateSourceInfo();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -495,8 +514,8 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||||||
* Creates new listener which forwards ad playback states on the creating thread and all other
|
* Creates new listener which forwards ad playback states on the creating thread and all other
|
||||||
* events on the external event listener thread.
|
* events on the external event listener thread.
|
||||||
*/
|
*/
|
||||||
public ComponentListener() {
|
public ComponentListener(Handler playerHandler) {
|
||||||
playerHandler = Util.createHandlerForCurrentLooper();
|
this.playerHandler = playerHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Stops event delivery from this instance. */
|
/** Stops event delivery from this instance. */
|
||||||
|
@ -242,6 +242,7 @@ public final class AdsMediaSourceTest {
|
|||||||
mock(Allocator.class),
|
mock(Allocator.class),
|
||||||
/* startPositionUs= */ 0);
|
/* startPositionUs= */ 0);
|
||||||
|
|
||||||
|
shadowOf(Looper.getMainLooper()).idle();
|
||||||
contentMediaSource.assertMediaPeriodCreated(
|
contentMediaSource.assertMediaPeriodCreated(
|
||||||
new MediaPeriodId(CONTENT_PERIOD_UID, /* windowSequenceNumber= */ 0));
|
new MediaPeriodId(CONTENT_PERIOD_UID, /* windowSequenceNumber= */ 0));
|
||||||
ArgumentCaptor<Timeline> adsTimelineCaptor = ArgumentCaptor.forClass(Timeline.class);
|
ArgumentCaptor<Timeline> adsTimelineCaptor = ArgumentCaptor.forClass(Timeline.class);
|
||||||
@ -396,9 +397,10 @@ public final class AdsMediaSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleContentTimelineChanged(
|
public boolean handleContentTimelineChanged(
|
||||||
AdsMediaSource adsMediaSource, Timeline timeline) {
|
AdsMediaSource adsMediaSource, Timeline timeline) {
|
||||||
contentTimelineChangedCalledLatch.countDown();
|
contentTimelineChangedCalledLatch.countDown();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
MediaSource.Factory adMediaSourceFactory = mock(MediaSource.Factory.class);
|
MediaSource.Factory adMediaSourceFactory = mock(MediaSource.Factory.class);
|
||||||
@ -573,9 +575,10 @@ public final class AdsMediaSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleContentTimelineChanged(
|
public boolean handleContentTimelineChanged(
|
||||||
AdsMediaSource adsMediaSource, Timeline timeline) {
|
AdsMediaSource adsMediaSource, Timeline timeline) {
|
||||||
contentTimelineChangedCallCount.incrementAndGet();
|
contentTimelineChangedCallCount.incrementAndGet();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
MediaSource.Factory adMediaSourceFactory = mock(MediaSource.Factory.class);
|
MediaSource.Factory adMediaSourceFactory = mock(MediaSource.Factory.class);
|
||||||
@ -966,9 +969,6 @@ public final class AdsMediaSourceTest {
|
|||||||
int adGroupIndex,
|
int adGroupIndex,
|
||||||
int adIndexInAdGroup,
|
int adIndexInAdGroup,
|
||||||
IOException exception) {}
|
IOException exception) {}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleContentTimelineChanged(AdsMediaSource adsMediaSource, Timeline timeline) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaSource buildMediaSource(MediaItem mediaItem) {
|
private static MediaSource buildMediaSource(MediaItem mediaItem) {
|
||||||
|
@ -591,7 +591,7 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleContentTimelineChanged(AdsMediaSource adsMediaSource, Timeline timeline) {
|
public boolean handleContentTimelineChanged(AdsMediaSource adsMediaSource, Timeline timeline) {
|
||||||
Object adsId = adsMediaSource.getAdsId();
|
Object adsId = adsMediaSource.getAdsId();
|
||||||
if (isReleased) {
|
if (isReleased) {
|
||||||
EventListener eventListener = activeEventListeners.remove(adsId);
|
EventListener eventListener = activeEventListeners.remove(adsId);
|
||||||
@ -604,14 +604,14 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
|||||||
eventListener.onAdPlaybackState(new AdPlaybackState(adsId));
|
eventListener.onAdPlaybackState(new AdPlaybackState(adsId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
AdPlaybackState adPlaybackState = checkNotNull(activeAdPlaybackStates.get(adsId));
|
AdPlaybackState adPlaybackState = checkNotNull(activeAdPlaybackStates.get(adsId));
|
||||||
if (!adPlaybackState.equals(AdPlaybackState.NONE)
|
if (!adPlaybackState.equals(AdPlaybackState.NONE)
|
||||||
&& !adPlaybackState.endsWithLivePostrollPlaceHolder()) {
|
&& !adPlaybackState.endsWithLivePostrollPlaceHolder()) {
|
||||||
// Multiple timeline updates for VOD not supported.
|
// Multiple timeline updates for VOD not supported.
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (adPlaybackState.equals(AdPlaybackState.NONE)) {
|
if (adPlaybackState.equals(AdPlaybackState.NONE)) {
|
||||||
@ -662,12 +662,13 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
|||||||
adsId, timeline, /* windowIndex= */ 0, contentPositionUs);
|
adsId, timeline, /* windowIndex= */ 0, contentPositionUs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
putAndNotifyAdPlaybackStateUpdate(adsId, adPlaybackState);
|
boolean adPlaybackStateUpdated = putAndNotifyAdPlaybackStateUpdate(adsId, adPlaybackState);
|
||||||
if (!unsupportedAdsIds.contains(adsId)) {
|
if (!unsupportedAdsIds.contains(adsId)) {
|
||||||
notifyListeners(
|
notifyListeners(
|
||||||
listener ->
|
listener ->
|
||||||
listener.onContentTimelineChanged(adsMediaSource.getMediaItem(), adsId, timeline));
|
listener.onContentTimelineChanged(adsMediaSource.getMediaItem(), adsId, timeline));
|
||||||
}
|
}
|
||||||
|
return adPlaybackStateUpdated;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -864,18 +865,20 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
|||||||
return loader;
|
return loader;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void putAndNotifyAdPlaybackStateUpdate(Object adsId, AdPlaybackState adPlaybackState) {
|
private boolean putAndNotifyAdPlaybackStateUpdate(Object adsId, AdPlaybackState adPlaybackState) {
|
||||||
@Nullable
|
@Nullable
|
||||||
AdPlaybackState oldAdPlaybackState = activeAdPlaybackStates.put(adsId, adPlaybackState);
|
AdPlaybackState oldAdPlaybackState = activeAdPlaybackStates.put(adsId, adPlaybackState);
|
||||||
if (!adPlaybackState.equals(oldAdPlaybackState)) {
|
if (!adPlaybackState.equals(oldAdPlaybackState)) {
|
||||||
@Nullable EventListener eventListener = activeEventListeners.get(adsId);
|
@Nullable EventListener eventListener = activeEventListeners.get(adsId);
|
||||||
if (eventListener != null) {
|
if (eventListener != null) {
|
||||||
eventListener.onAdPlaybackState(adPlaybackState);
|
eventListener.onAdPlaybackState(adPlaybackState);
|
||||||
|
return true;
|
||||||
} else {
|
} else {
|
||||||
activeAdPlaybackStates.remove(adsId);
|
activeAdPlaybackStates.remove(adsId);
|
||||||
insertedInterstitialIds.remove(adsId);
|
insertedInterstitialIds.remove(adsId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyAssetResolutionFailed(Object adsId, int adGroupIndex, int adIndexInAdGroup) {
|
private void notifyAssetResolutionFailed(Object adsId, int adGroupIndex, int adIndexInAdGroup) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user