From 3dede2415d5dd530c896e95ce6b4481061f6ded6 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 17 Dec 2024 08:22:33 -0800 Subject: [PATCH] Add AdsMediaSourceFactory for HLS interstitials PiperOrigin-RevId: 707109323 --- RELEASENOTES.md | 7 ++ .../hls/HlsInterstitialsAdsLoader.java | 111 ++++++++++++++++++ .../hls/HlsInterstitialsAdsLoaderTest.java | 17 ++- 3 files changed, 125 insertions(+), 10 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 137ef765c6..b2d9bc05c4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -101,6 +101,13 @@ * Cronet Extension: * RTMP Extension: * HLS Extension: + * Add a first version of `HlsInterstitialsAdsLoader`. The ads loader reads + the HLS interstitials of an HLS media playlist and maps them to the + `AdPlaybackState` that is passed to the `AdsMediaSource`. This initial + version only supports HLS VOD streams with `X-ASSET-URI` attributes. + * Add `HlsInterstitialsAdsLoader.AdsMediaSourceFactory`. Apps can use it + to create `AdsMediaSource` instances that use an + `HlsInterstitialsAdsLoader` in a convenient and safe way. * DASH Extension: * Add AC-4 Level-4 format support for DASH ([#1898](https://github.com/androidx/media/pull/1898)). 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 78c05c1186..d62dafe4ba 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 @@ -16,11 +16,13 @@ package androidx.media3.exoplayer.hls; import static androidx.media3.common.Player.DISCONTINUITY_REASON_AUTO_TRANSITION; +import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkStateNotNull; import static java.lang.Math.max; +import android.content.Context; import androidx.annotation.Nullable; import androidx.media3.common.AdPlaybackState; import androidx.media3.common.AdViewProvider; @@ -38,10 +40,15 @@ import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSpec; +import androidx.media3.datasource.DefaultDataSource; +import androidx.media3.exoplayer.drm.DrmSessionManagerProvider; import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist; import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial; +import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.ads.AdsLoader; import androidx.media3.exoplayer.source.ads.AdsMediaSource; +import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; @@ -64,6 +71,110 @@ import java.util.Set; @UnstableApi public final class HlsInterstitialsAdsLoader implements AdsLoader { + /** + * A {@link MediaSource.Factory} to create a media source to play HLS streams with interstitials. + */ + public static final class AdsMediaSourceFactory implements MediaSource.Factory { + + private final MediaSource.Factory mediaSourceFactory; + private final AdViewProvider adViewProvider; + private final HlsInterstitialsAdsLoader adsLoader; + + /** + * Creates an instance with a {@link + * androidx.media3.exoplayer.source.DefaultMediaSourceFactory}. + * + * @param adsLoader The {@link HlsInterstitialsAdsLoader}. + * @param adViewProvider Provider of views for the ad UI. + * @param context The {@link Context}. + */ + public AdsMediaSourceFactory( + HlsInterstitialsAdsLoader adsLoader, AdViewProvider adViewProvider, Context context) { + this(adsLoader, context, /* mediaSourceFactory= */ null, adViewProvider); + } + + /** + * Creates an instance with a custom {@link MediaSource.Factory}. + * + * @param adsLoader The {@link HlsInterstitialsAdsLoader}. + * @param adViewProvider Provider of views for the ad UI. + * @param mediaSourceFactory The {@link MediaSource.Factory} used to create content and ad media + * sources. + * @throws IllegalStateException If the provided {@linkplain MediaSource.Factory media source + * factory} doesn't support content type {@link C#CONTENT_TYPE_HLS}. + */ + public AdsMediaSourceFactory( + HlsInterstitialsAdsLoader adsLoader, + AdViewProvider adViewProvider, + MediaSource.Factory mediaSourceFactory) { + this(adsLoader, /* context= */ null, mediaSourceFactory, adViewProvider); + } + + private AdsMediaSourceFactory( + HlsInterstitialsAdsLoader adsLoader, + @Nullable Context context, + @Nullable MediaSource.Factory mediaSourceFactory, + AdViewProvider adViewProvider) { + checkArgument(context != null || mediaSourceFactory != null); + this.adsLoader = adsLoader; + this.mediaSourceFactory = + mediaSourceFactory != null + ? mediaSourceFactory + : new HlsMediaSource.Factory(new DefaultDataSource.Factory(checkNotNull(context))); + this.adViewProvider = adViewProvider; + boolean supportsHls = false; + for (int supportedType : this.mediaSourceFactory.getSupportedTypes()) { + if (supportedType == C.CONTENT_TYPE_HLS) { + supportsHls = true; + break; + } + } + checkState(supportsHls); + } + + @Override + public @C.ContentType int[] getSupportedTypes() { + return new int[] {C.CONTENT_TYPE_HLS}; + } + + @Override + @CanIgnoreReturnValue + public AdsMediaSourceFactory setDrmSessionManagerProvider( + DrmSessionManagerProvider drmSessionManagerProvider) { + mediaSourceFactory.setDrmSessionManagerProvider(drmSessionManagerProvider); + return this; + } + + @Override + @CanIgnoreReturnValue + public AdsMediaSourceFactory setLoadErrorHandlingPolicy( + LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + mediaSourceFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy); + return this; + } + + @Override + public MediaSource createMediaSource(MediaItem mediaItem) { + checkNotNull(mediaItem.localConfiguration); + MediaSource contentMediaSource = mediaSourceFactory.createMediaSource(mediaItem); + if (mediaItem.localConfiguration.adsConfiguration == null) { + return contentMediaSource; + } else if (!(mediaItem.localConfiguration.adsConfiguration.adsId instanceof String)) { + throw new IllegalArgumentException( + "Please use an AdsConfiguration with an adsId of type String when using" + + " HlsInterstitialsAdsLoader"); + } + return new AdsMediaSource( + contentMediaSource, + new DataSpec(mediaItem.localConfiguration.adsConfiguration.adTagUri), // unused + checkNotNull(mediaItem.localConfiguration.adsConfiguration.adsId), + mediaSourceFactory, + adsLoader, + adViewProvider, + /* useLazyContentSourcePreparation= */ false); + } + } + /** A listener to be notified of events emitted by the ads loader. */ public interface Listener { diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoaderTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoaderTest.java index 8d2b1c269b..411a1fbc20 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoaderTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoaderTest.java @@ -90,18 +90,15 @@ public class HlsInterstitialsAdsLoaderTest { .setAdsConfiguration( new MediaItem.AdsConfiguration.Builder(Uri.EMPTY).setAdsId("adsId").build()) .build(); - adTagDataSpec = new DataSpec(contentMediaItem.localConfiguration.adsConfiguration.adTagUri); - DefaultMediaSourceFactory defaultMediaSourceFactory = - new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); + adTagDataSpec = new DataSpec(Uri.EMPTY); // The ads media source using the ads loader. adsMediaSource = - new AdsMediaSource( - defaultMediaSourceFactory.createMediaSource(contentMediaItem), - new DataSpec(Uri.EMPTY), - "adsId", - defaultMediaSourceFactory, - adsLoader, - mockAdViewProvider); + (AdsMediaSource) + new HlsInterstitialsAdsLoader.AdsMediaSourceFactory( + adsLoader, + mockAdViewProvider, + (Context) ApplicationProvider.getApplicationContext()) + .createMediaSource(contentMediaItem); // The content timeline with empty ad playback state. contentWindowDefinition = new FakeTimeline.TimelineWindowDefinition(