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.util.Pair;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.MediaItem.LocalConfiguration;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.BundleCollectionUtil;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
@ -175,14 +176,22 @@ public abstract class Timeline {
|
||||
public Object uid;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #mediaItem} instead.
|
||||
* @deprecated Use {@link LocalConfiguration#tag} of {@link #mediaItem} instead.
|
||||
*/
|
||||
@UnstableApi @Deprecated @Nullable public Object tag;
|
||||
|
||||
/** The {@link MediaItem} associated to the window. Not necessarily unique. */
|
||||
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;
|
||||
|
||||
/**
|
||||
@ -263,7 +272,7 @@ public abstract class Timeline {
|
||||
/** Sets the data held by this window. */
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings("deprecation") // Using Window.tag for backwards compatibility
|
||||
public Window set(
|
||||
Object uid,
|
||||
@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}.
|
||||
*
|
||||
* <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 adTagDataSpec A data spec for the ad tag to load.
|
||||
* @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
|
||||
* thread by {@link AdsMediaSource}.
|
||||
*
|
||||
* <p>If you override this callback for the purpose of reading ad data from the timeline to
|
||||
* populate the {@link AdPlaybackState} with, you need to pass true to the constructor of {@link
|
||||
* <p>The default implementation returns false which makes the content timeline immediately being
|
||||
* 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,
|
||||
* AdViewProvider, boolean) AdsMediaSource} to indicate the content source needs to be prepared
|
||||
* upfront.
|
||||
* AdViewProvider, boolean) AdsMediaSource} to indicate that the content source needs to be
|
||||
* 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 timeline The timeline of the content source.
|
||||
* @return true If {@link EventListener#onAdPlaybackState(AdPlaybackState)} is or will be called,
|
||||
* false otherwise.
|
||||
*/
|
||||
@UnstableApi
|
||||
default void handleContentTimelineChanged(AdsMediaSource adsMediaSource, Timeline timeline) {
|
||||
// Do nothing.
|
||||
default boolean handleContentTimelineChanged(AdsMediaSource adsMediaSource, Timeline timeline) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -147,12 +147,14 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
||||
private final Object adsId;
|
||||
private final Handler mainHandler;
|
||||
private final Timeline.Period period;
|
||||
private final boolean useLazyContentSourcePreparation;
|
||||
|
||||
// Accessed on the player thread.
|
||||
@Nullable private ComponentListener componentListener;
|
||||
@Nullable private Timeline contentTimeline;
|
||||
@Nullable private AdPlaybackState adPlaybackState;
|
||||
private @NullableType AdMediaSourceHolder[][] adMediaSourceHolders;
|
||||
@Nullable private Handler playerHandler;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* wait for an {@link AdPlaybackState} to be set before preparing. False if the timeline is
|
||||
* required {@linkplain AdsLoader#handleContentTimelineChanged(AdsMediaSource, Timeline) to
|
||||
* read ad data from it} to populate the {@link AdPlaybackState} (for instance from HLS
|
||||
* interstitials).
|
||||
* read ad data from it} to populate the {@link AdPlaybackState} (See {@link
|
||||
* Timeline.Window#manifest} also).
|
||||
*/
|
||||
public AdsMediaSource(
|
||||
MediaSource contentMediaSource,
|
||||
@ -216,6 +218,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
||||
AdsLoader adsLoader,
|
||||
AdViewProvider adViewProvider,
|
||||
boolean useLazyContentSourcePreparation) {
|
||||
this.useLazyContentSourcePreparation = useLazyContentSourcePreparation;
|
||||
this.contentMediaSource =
|
||||
new MaskingMediaSource(
|
||||
contentMediaSource, /* useLazyPreparation= */ useLazyContentSourcePreparation);
|
||||
@ -256,7 +259,8 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
||||
@Override
|
||||
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
||||
super.prepareSourceInternal(mediaTransferListener);
|
||||
ComponentListener componentListener = new ComponentListener();
|
||||
this.playerHandler = Util.createHandlerForCurrentLooper();
|
||||
ComponentListener componentListener = new ComponentListener(playerHandler);
|
||||
this.componentListener = componentListener;
|
||||
contentTimeline = contentMediaSource.getTimeline();
|
||||
prepareChildSource(CHILD_SOURCE_MEDIA_PERIOD_ID, contentMediaSource);
|
||||
@ -320,6 +324,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
||||
super.releaseSourceInternal();
|
||||
ComponentListener componentListener = checkNotNull(this.componentListener);
|
||||
this.componentListener = null;
|
||||
this.playerHandler = null;
|
||||
componentListener.stop();
|
||||
contentTimeline = null;
|
||||
adPlaybackState = null;
|
||||
@ -335,12 +340,26 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
||||
int adIndexInAdGroup = childSourceId.adIndexInAdGroup;
|
||||
checkNotNull(adMediaSourceHolders[adGroupIndex][adIndexInAdGroup])
|
||||
.handleSourceInfoRefresh(newTimeline);
|
||||
maybeUpdateSourceInfo();
|
||||
} else {
|
||||
Assertions.checkArgument(newTimeline.getPeriodCount() == 1);
|
||||
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
|
||||
@ -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
|
||||
* events on the external event listener thread.
|
||||
*/
|
||||
public ComponentListener() {
|
||||
playerHandler = Util.createHandlerForCurrentLooper();
|
||||
public ComponentListener(Handler playerHandler) {
|
||||
this.playerHandler = playerHandler;
|
||||
}
|
||||
|
||||
/** Stops event delivery from this instance. */
|
||||
|
@ -242,6 +242,7 @@ public final class AdsMediaSourceTest {
|
||||
mock(Allocator.class),
|
||||
/* startPositionUs= */ 0);
|
||||
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
contentMediaSource.assertMediaPeriodCreated(
|
||||
new MediaPeriodId(CONTENT_PERIOD_UID, /* windowSequenceNumber= */ 0));
|
||||
ArgumentCaptor<Timeline> adsTimelineCaptor = ArgumentCaptor.forClass(Timeline.class);
|
||||
@ -396,9 +397,10 @@ public final class AdsMediaSourceTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleContentTimelineChanged(
|
||||
public boolean handleContentTimelineChanged(
|
||||
AdsMediaSource adsMediaSource, Timeline timeline) {
|
||||
contentTimelineChangedCalledLatch.countDown();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
MediaSource.Factory adMediaSourceFactory = mock(MediaSource.Factory.class);
|
||||
@ -573,9 +575,10 @@ public final class AdsMediaSourceTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleContentTimelineChanged(
|
||||
public boolean handleContentTimelineChanged(
|
||||
AdsMediaSource adsMediaSource, Timeline timeline) {
|
||||
contentTimelineChangedCallCount.incrementAndGet();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
MediaSource.Factory adMediaSourceFactory = mock(MediaSource.Factory.class);
|
||||
@ -966,9 +969,6 @@ public final class AdsMediaSourceTest {
|
||||
int adGroupIndex,
|
||||
int adIndexInAdGroup,
|
||||
IOException exception) {}
|
||||
|
||||
@Override
|
||||
public void handleContentTimelineChanged(AdsMediaSource adsMediaSource, Timeline timeline) {}
|
||||
}
|
||||
|
||||
private static MediaSource buildMediaSource(MediaItem mediaItem) {
|
||||
|
@ -591,7 +591,7 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleContentTimelineChanged(AdsMediaSource adsMediaSource, Timeline timeline) {
|
||||
public boolean handleContentTimelineChanged(AdsMediaSource adsMediaSource, Timeline timeline) {
|
||||
Object adsId = adsMediaSource.getAdsId();
|
||||
if (isReleased) {
|
||||
EventListener eventListener = activeEventListeners.remove(adsId);
|
||||
@ -604,14 +604,14 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
eventListener.onAdPlaybackState(new AdPlaybackState(adsId));
|
||||
}
|
||||
}
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
AdPlaybackState adPlaybackState = checkNotNull(activeAdPlaybackStates.get(adsId));
|
||||
if (!adPlaybackState.equals(AdPlaybackState.NONE)
|
||||
&& !adPlaybackState.endsWithLivePostrollPlaceHolder()) {
|
||||
// Multiple timeline updates for VOD not supported.
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (adPlaybackState.equals(AdPlaybackState.NONE)) {
|
||||
@ -662,12 +662,13 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
adsId, timeline, /* windowIndex= */ 0, contentPositionUs);
|
||||
}
|
||||
}
|
||||
putAndNotifyAdPlaybackStateUpdate(adsId, adPlaybackState);
|
||||
boolean adPlaybackStateUpdated = putAndNotifyAdPlaybackStateUpdate(adsId, adPlaybackState);
|
||||
if (!unsupportedAdsIds.contains(adsId)) {
|
||||
notifyListeners(
|
||||
listener ->
|
||||
listener.onContentTimelineChanged(adsMediaSource.getMediaItem(), adsId, timeline));
|
||||
}
|
||||
return adPlaybackStateUpdated;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -864,18 +865,20 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
return loader;
|
||||
}
|
||||
|
||||
private void putAndNotifyAdPlaybackStateUpdate(Object adsId, AdPlaybackState adPlaybackState) {
|
||||
private boolean putAndNotifyAdPlaybackStateUpdate(Object adsId, AdPlaybackState adPlaybackState) {
|
||||
@Nullable
|
||||
AdPlaybackState oldAdPlaybackState = activeAdPlaybackStates.put(adsId, adPlaybackState);
|
||||
if (!adPlaybackState.equals(oldAdPlaybackState)) {
|
||||
@Nullable EventListener eventListener = activeEventListeners.get(adsId);
|
||||
if (eventListener != null) {
|
||||
eventListener.onAdPlaybackState(adPlaybackState);
|
||||
return true;
|
||||
} else {
|
||||
activeAdPlaybackStates.remove(adsId);
|
||||
insertedInterstitialIds.remove(adsId);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void notifyAssetResolutionFailed(Object adsId, int adGroupIndex, int adIndexInAdGroup) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user