diff --git a/libraries/common/src/main/java/androidx/media3/common/Timeline.java b/libraries/common/src/main/java/androidx/media3/common/Timeline.java index 463ff0aca3..fb93681f05 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Timeline.java +++ b/libraries/common/src/main/java/androidx/media3/common/Timeline.java @@ -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}. + * + *

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, diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsLoader.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsLoader.java index 73281d2218..46277d1896 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsLoader.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsLoader.java @@ -134,6 +134,12 @@ public interface AdsLoader { /** * Starts using the ads loader for playback. Called on the main thread by {@link AdsMediaSource}. * + *

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}. * - *

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 + *

The default implementation returns false which makes the content timeline immediately being + * reported to the player. + * + *

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. + * + *

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. + * + *

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; } /** diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsMediaSource.java index a616278181..64e77da0f7 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsMediaSource.java @@ -147,12 +147,14 @@ public final class AdsMediaSource extends CompositeMediaSource { 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 { * @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 { 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 { @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 { 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 { 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 { * 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. */ diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/AdsMediaSourceTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/AdsMediaSourceTest.java index ebaffda29c..4d1adf768c 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/AdsMediaSourceTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/AdsMediaSourceTest.java @@ -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 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) { diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoader.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoader.java index 6baf5559e4..9885141c48 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoader.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoader.java @@ -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) {