From eae886fe28964a2171210bfd44dc295e2a7ca0c7 Mon Sep 17 00:00:00 2001 From: tianyifeng Date: Wed, 9 Apr 2025 04:30:18 -0700 Subject: [PATCH] Add Factory for SegmentDownloader implementations PiperOrigin-RevId: 745530254 --- RELEASENOTES.md | 1 + libraries/exoplayer/proguard-rules.txt | 18 +-- .../offline/DefaultDownloaderFactory.java | 104 ++++++++++-------- .../exoplayer/offline/SegmentDownloader.java | 48 +++++--- .../offline/SegmentDownloaderFactory.java | 45 ++++++++ .../dash/offline/DashDownloader.java | 92 ++++++++++++---- .../dash/offline/DashDownloaderTest.java | 5 +- .../exoplayer/hls/offline/HlsDownloader.java | 94 ++++++++++++---- .../hls/offline/HlsDownloaderTest.java | 5 +- .../smoothstreaming/offline/SsDownloader.java | 97 ++++++++++++---- .../playback/gts/DashDownloadTest.java | 5 +- 11 files changed, 377 insertions(+), 137 deletions(-) create mode 100644 libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/SegmentDownloaderFactory.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 88b8b15bf1..e0d9017b5d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -76,6 +76,7 @@ correspondingly. * Add `DownloadHelper.Factory` with which the static `DownloadHelper.forMediaItem()` methods are replaced. + * Add `Factory` for `SegmentDownloader` implementations. * OkHttp extension: * Cronet extension: * RTMP extension: diff --git a/libraries/exoplayer/proguard-rules.txt b/libraries/exoplayer/proguard-rules.txt index a9fd1b6ee2..048d9b7545 100644 --- a/libraries/exoplayer/proguard-rules.txt +++ b/libraries/exoplayer/proguard-rules.txt @@ -39,17 +39,17 @@ } # Constructors accessed via reflection in DefaultDownloaderFactory --dontnote androidx.media3.exoplayer.dash.offline.DashDownloader --keepclassmembers class androidx.media3.exoplayer.dash.offline.DashDownloader { - (androidx.media3.common.MediaItem, androidx.media3.datasource.cache.CacheDataSource$Factory, java.util.concurrent.Executor); +-dontnote androidx.media3.exoplayer.dash.offline.DashDownloader$Factory +-keepclassmembers class androidx.media3.exoplayer.dash.offline.DashDownloader$Factory { + (androidx.media3.datasource.cache.CacheDataSource$Factory); } --dontnote androidx.media3.exoplayer.hls.offline.HlsDownloader --keepclassmembers class androidx.media3.exoplayer.hls.offline.HlsDownloader { - (androidx.media3.common.MediaItem, androidx.media3.datasource.cache.CacheDataSource$Factory, java.util.concurrent.Executor); +-dontnote androidx.media3.exoplayer.hls.offline.HlsDownloader$Factory +-keepclassmembers class androidx.media3.exoplayer.hls.offline.HlsDownloader$Factory { + (androidx.media3.datasource.cache.CacheDataSource$Factory); } --dontnote androidx.media3.exoplayer.smoothstreaming.offline.SsDownloader --keepclassmembers class androidx.media3.exoplayer.smoothstreaming.offline.SsDownloader { - (androidx.media3.common.MediaItem, androidx.media3.datasource.cache.CacheDataSource$Factory, java.util.concurrent.Executor); +-dontnote androidx.media3.exoplayer.smoothstreaming.offline.SsDownloader$Factory +-keepclassmembers class androidx.media3.exoplayer.smoothstreaming.offline.SsDownloader$Factory { + (androidx.media3.datasource.cache.CacheDataSource$Factory); } # Constructors accessed via reflection in DefaultMediaSourceFactory diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DefaultDownloaderFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DefaultDownloaderFactory.java index 3da759a007..81c16c506f 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DefaultDownloaderFactory.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DefaultDownloaderFactory.java @@ -15,6 +15,8 @@ */ package androidx.media3.exoplayer.offline; +import static androidx.media3.common.util.Util.contains; + import android.util.SparseArray; import androidx.annotation.Nullable; import androidx.media3.common.C; @@ -23,7 +25,6 @@ import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.cache.CacheDataSource; -import java.lang.reflect.Constructor; import java.util.concurrent.Executor; /** @@ -34,11 +35,9 @@ import java.util.concurrent.Executor; @UnstableApi public class DefaultDownloaderFactory implements DownloaderFactory { - private static final SparseArray> CONSTRUCTORS = - createDownloaderConstructors(); - private final CacheDataSource.Factory cacheDataSourceFactory; private final Executor executor; + private final SparseArray segmentDownloaderFactories; /** * Creates an instance. @@ -66,6 +65,7 @@ public class DefaultDownloaderFactory implements DownloaderFactory { CacheDataSource.Factory cacheDataSourceFactory, Executor executor) { this.cacheDataSourceFactory = Assertions.checkNotNull(cacheDataSourceFactory); this.executor = Assertions.checkNotNull(executor); + this.segmentDownloaderFactories = new SparseArray<>(); } @Override @@ -76,7 +76,7 @@ public class DefaultDownloaderFactory implements DownloaderFactory { case C.CONTENT_TYPE_DASH: case C.CONTENT_TYPE_HLS: case C.CONTENT_TYPE_SS: - return createDownloader(request, contentType); + return createSegmentDownloader(request, contentType); case C.CONTENT_TYPE_OTHER: @Nullable DownloadRequest.ByteRange byteRange = request.byteRange; return new ProgressiveDownloader( @@ -93,64 +93,78 @@ public class DefaultDownloaderFactory implements DownloaderFactory { } } - private Downloader createDownloader(DownloadRequest request, @C.ContentType int contentType) { - @Nullable Constructor constructor = CONSTRUCTORS.get(contentType); - if (constructor == null) { - throw new IllegalStateException("Module missing for content type " + contentType); - } + private Downloader createSegmentDownloader( + DownloadRequest request, @C.ContentType int contentType) { + SegmentDownloaderFactory downloaderFactory = + getSegmentDownloaderFactory(contentType, cacheDataSourceFactory); MediaItem mediaItem = new MediaItem.Builder() .setUri(request.uri) .setStreamKeys(request.streamKeys) .setCustomCacheKey(request.customCacheKey) .build(); - try { - return constructor.newInstance(mediaItem, cacheDataSourceFactory, executor); - } catch (Exception e) { - throw new IllegalStateException( - "Failed to instantiate downloader for content type " + contentType, e); - } + return downloaderFactory.setExecutor(executor).create(mediaItem); } // LINT.IfChange - private static SparseArray> createDownloaderConstructors() { - SparseArray> array = new SparseArray<>(); - try { - array.put( - C.CONTENT_TYPE_DASH, - getDownloaderConstructor( - Class.forName("androidx.media3.exoplayer.dash.offline.DashDownloader"))); - } catch (ClassNotFoundException e) { - // Expected if the app was built without the DASH module. + private SegmentDownloaderFactory getSegmentDownloaderFactory( + @C.ContentType int contentType, CacheDataSource.Factory cacheDataSourceFactory) { + if (contains(segmentDownloaderFactories, contentType)) { + return segmentDownloaderFactories.get(contentType); } + SegmentDownloaderFactory downloaderFactory; try { - array.put( - C.CONTENT_TYPE_HLS, - getDownloaderConstructor( - Class.forName("androidx.media3.exoplayer.hls.offline.HlsDownloader"))); + downloaderFactory = loadSegmentDownloaderFactory(contentType, cacheDataSourceFactory); } catch (ClassNotFoundException e) { - // Expected if the app was built without the HLS module. + throw new IllegalStateException("Module missing for content type " + contentType, e); } - try { - array.put( - C.CONTENT_TYPE_SS, - getDownloaderConstructor( - Class.forName("androidx.media3.exoplayer.smoothstreaming.offline.SsDownloader"))); - } catch (ClassNotFoundException e) { - // Expected if the app was built without the SmoothStreaming module. - } - return array; + return downloaderFactory; } - private static Constructor getDownloaderConstructor(Class clazz) { + private SegmentDownloaderFactory loadSegmentDownloaderFactory( + @C.ContentType int contentType, CacheDataSource.Factory cacheDataSourceFactory) + throws ClassNotFoundException { + SegmentDownloaderFactory factory; + switch (contentType) { + case C.CONTENT_TYPE_DASH: + factory = + createSegmentDownloaderFactory( + Class.forName("androidx.media3.exoplayer.dash.offline.DashDownloader$Factory") + .asSubclass(SegmentDownloaderFactory.class), + cacheDataSourceFactory); + break; + case C.CONTENT_TYPE_HLS: + factory = + createSegmentDownloaderFactory( + Class.forName("androidx.media3.exoplayer.hls.offline.HlsDownloader$Factory") + .asSubclass(SegmentDownloaderFactory.class), + cacheDataSourceFactory); + break; + case C.CONTENT_TYPE_SS: + factory = + createSegmentDownloaderFactory( + Class.forName( + "androidx.media3.exoplayer.smoothstreaming.offline.SsDownloader$Factory") + .asSubclass(SegmentDownloaderFactory.class), + cacheDataSourceFactory); + break; + default: + throw new IllegalArgumentException("Unsupported type: " + contentType); + } + segmentDownloaderFactories.put(contentType, factory); + return factory; + } + + private static SegmentDownloaderFactory createSegmentDownloaderFactory( + Class clazz, + CacheDataSource.Factory cacheDataSourceFactory) { try { return clazz - .asSubclass(Downloader.class) - .getConstructor(MediaItem.class, CacheDataSource.Factory.class, Executor.class); - } catch (NoSuchMethodException e) { - // The downloader is present, but the expected constructor is missing. - throw new IllegalStateException("Downloader constructor missing", e); + .getConstructor(CacheDataSource.Factory.class) + .newInstance(cacheDataSourceFactory); + } catch (Exception e) { + throw new IllegalStateException("Downloader factory missing", e); } } // LINT.ThenChange(../../../../../../../proguard-rules.txt) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/SegmentDownloader.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/SegmentDownloader.java index 7bda49ce8c..05ca39435b 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/SegmentDownloader.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/SegmentDownloader.java @@ -38,6 +38,7 @@ import androidx.media3.datasource.cache.CacheWriter; import androidx.media3.datasource.cache.ContentMetadata; import androidx.media3.exoplayer.upstream.ParsingLoadable; import androidx.media3.exoplayer.upstream.ParsingLoadable.Parser; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.IOException; import java.util.ArrayDeque; import java.util.ArrayList; @@ -56,6 +57,37 @@ import java.util.concurrent.Executor; @UnstableApi public abstract class SegmentDownloader> implements Downloader { + /** A base class of the factory of the concrete extension of {@link SegmentDownloader}. */ + protected abstract static class BaseFactory> + implements SegmentDownloaderFactory { + + protected final CacheDataSource.Factory cacheDataSourceFactory; + protected Parser manifestParser; + protected Executor executor; + protected long maxMergedSegmentStartTimeDiffMs; + + public BaseFactory(CacheDataSource.Factory cacheDataSourceFactory, Parser manifestParser) { + this.cacheDataSourceFactory = cacheDataSourceFactory; + this.manifestParser = manifestParser; + this.executor = Runnable::run; + this.maxMergedSegmentStartTimeDiffMs = DEFAULT_MAX_MERGED_SEGMENT_START_TIME_DIFF_MS; + } + + @Override + @CanIgnoreReturnValue + public BaseFactory setExecutor(Executor executor) { + this.executor = executor; + return this; + } + + @Override + @CanIgnoreReturnValue + public BaseFactory setMaxMergedSegmentStartTimeDiffMs(long maxMergedSegmentStartTimeDiffMs) { + this.maxMergedSegmentStartTimeDiffMs = maxMergedSegmentStartTimeDiffMs; + return this; + } + } + /** Smallest unit of content to be downloaded. */ protected static class Segment implements Comparable { @@ -103,19 +135,7 @@ public abstract class SegmentDownloader> impleme private volatile boolean isCanceled; - /** - * @param mediaItem The {@link MediaItem} to be downloaded. - * @param manifestParser A parser for manifests belonging to the media to be downloaded. - * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the - * download will be written. - * @param executor An {@link Executor} used to make requests for the media being downloaded. - * Providing an {@link Executor} that uses multiple threads will speed up the download by - * allowing parts of it to be executed in parallel. - * @param maxMergedSegmentStartTimeDiffMs The maximum difference of the start time of two - * segments, up to which the segments (of the same URI) should be merged into a single - * download segment, in milliseconds. - */ - public SegmentDownloader( + protected SegmentDownloader( MediaItem mediaItem, Parser manifestParser, CacheDataSource.Factory cacheDataSourceFactory, @@ -414,12 +434,14 @@ public abstract class SegmentDownloader> impleme } } + @SuppressWarnings("FutureReturnValueIgnored") private void removeActiveRunnable(RunnableFutureTask runnable) { synchronized (activeRunnables) { activeRunnables.remove(runnable); } } + @SuppressWarnings("FutureReturnValueIgnored") private void removeActiveRunnable(int index) { synchronized (activeRunnables) { activeRunnables.remove(index); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/SegmentDownloaderFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/SegmentDownloaderFactory.java new file mode 100644 index 0000000000..8c43fa179c --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/SegmentDownloaderFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.exoplayer.offline; + +import androidx.media3.common.MediaItem; +import androidx.media3.common.util.UnstableApi; +import java.util.concurrent.Executor; + +/** A factory to create {@linkplain SegmentDownloader segment downloaders}. */ +@UnstableApi +/* package */ interface SegmentDownloaderFactory { + + /** + * Sets the {@link Executor} used to make requests for the media being downloaded. Providing an + * {@link Executor} that uses multiple threads will speed up the download by allowing parts of it + * to be executed in parallel. + */ + SegmentDownloaderFactory setExecutor(Executor executor); + + /** + * Sets the maximum difference of the start time of two segments, up to which the segments (of the + * same URI) should be merged into a single download segment, in milliseconds. + */ + SegmentDownloaderFactory setMaxMergedSegmentStartTimeDiffMs(long maxMergedSegmentStartTimeDiffMs); + + /** + * Creates the segment downloader. + * + * @param mediaItem The {@link MediaItem} to be downloaded. + */ + SegmentDownloader create(MediaItem mediaItem); +} diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/offline/DashDownloader.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/offline/DashDownloader.java index a473c1033c..ac1d6e6dc5 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/offline/DashDownloader.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/offline/DashDownloader.java @@ -41,6 +41,7 @@ import androidx.media3.exoplayer.offline.SegmentDownloader; import androidx.media3.exoplayer.upstream.ParsingLoadable.Parser; import androidx.media3.extractor.ChunkIndex; import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -60,12 +61,11 @@ import java.util.concurrent.Executor; * // Create a downloader for the first representation of the first adaptation set of the first * // period. * DashDownloader dashDownloader = - * new DashDownloader( - * new MediaItem.Builder() - * .setUri(manifestUrl) - * .setStreamKeys(Collections.singletonList(new StreamKey(0, 0, 0))) - * .build(), - * cacheDataSourceFactory); + * new DashDownloader.Factory(cacheDataSourceFactory) + * .create(new MediaItem.Builder() + * .setUri(manifestUrl) + * .setStreamKeys(ImmutableList.of(new StreamKey(0, 0, 0))) + * .build()); * // Perform the download. * dashDownloader.download(progressListener); * // Use the downloaded data for playback. @@ -76,29 +76,81 @@ import java.util.concurrent.Executor; @UnstableApi public final class DashDownloader extends SegmentDownloader { + /** A factory for {@linkplain DashDownloader DASH downloaders}. */ + public static final class Factory extends BaseFactory { + + /** + * Creates a factory for {@link DashDownloader}. + * + * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the + * download will be written. + */ + public Factory(CacheDataSource.Factory cacheDataSourceFactory) { + super(cacheDataSourceFactory, new DashManifestParser()); + } + + /** + * Sets a parser for DASH manifests. + * + * @return This factory, for convenience. + */ + @CanIgnoreReturnValue + public Factory setManifestParser(DashManifestParser manifestParser) { + this.manifestParser = manifestParser; + return this; + } + + /** + * Sets the {@link Executor} used to make requests for the media being downloaded. Providing an + * {@link Executor} that uses multiple threads will speed up the download by allowing parts of + * it to be executed in parallel. + * + * @return This factory, for convenience. + */ + @Override + @CanIgnoreReturnValue + public Factory setExecutor(Executor executor) { + return (Factory) super.setExecutor(executor); + } + + /** + * Sets the maximum difference of the start time of two segments, up to which the segments (of + * the same URI) should be merged into a single download segment, in milliseconds. + * + * @return This factory, for convenience. + */ + @Override + @CanIgnoreReturnValue + public Factory setMaxMergedSegmentStartTimeDiffMs(long maxMergedSegmentStartTimeDiffMs) { + return (Factory) super.setMaxMergedSegmentStartTimeDiffMs(maxMergedSegmentStartTimeDiffMs); + } + + /** Creates {@linkplain DashDownloader DASH downloaders}. */ + @Override + public DashDownloader create(MediaItem mediaItem) { + return new DashDownloader( + mediaItem, + manifestParser, + cacheDataSourceFactory, + executor, + maxMergedSegmentStartTimeDiffMs); + } + } + private final BaseUrlExclusionList baseUrlExclusionList; /** - * Creates a new instance. - * - * @param mediaItem The {@link MediaItem} to be downloaded. - * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the - * download will be written. + * @deprecated Use {@link DashDownloader.Factory#create(MediaItem)} instead. */ + @Deprecated public DashDownloader(MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory) { this(mediaItem, cacheDataSourceFactory, Runnable::run); } /** - * Creates a new instance. - * - * @param mediaItem The {@link MediaItem} to be downloaded. - * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the - * download will be written. - * @param executor An {@link Executor} used to make requests for the media being downloaded. - * Providing an {@link Executor} that uses multiple threads will speed up the download by - * allowing parts of it to be executed in parallel. + * @deprecated Use {@link DashDownloader.Factory#create(MediaItem)} instead. */ + @Deprecated public DashDownloader( MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory, Executor executor) { this( @@ -123,7 +175,7 @@ public final class DashDownloader extends SegmentDownloader { * segments, up to which the segments (of the same URI) should be merged into a single * download segment, in milliseconds. */ - public DashDownloader( + private DashDownloader( MediaItem mediaItem, Parser manifestParser, CacheDataSource.Factory cacheDataSourceFactory, diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/offline/DashDownloaderTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/offline/DashDownloaderTest.java index b42a77ee42..b6590507f6 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/offline/DashDownloaderTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/offline/DashDownloaderTest.java @@ -340,9 +340,8 @@ public class DashDownloaderTest { new CacheDataSource.Factory() .setCache(cache) .setUpstreamDataSourceFactory(upstreamDataSourceFactory); - return new DashDownloader( - new MediaItem.Builder().setUri(TEST_MPD_URI).setStreamKeys(keysList(keys)).build(), - cacheDataSourceFactory); + return new DashDownloader.Factory(cacheDataSourceFactory) + .create(new MediaItem.Builder().setUri(TEST_MPD_URI).setStreamKeys(keysList(keys)).build()); } private static ArrayList keysList(StreamKey... keys) { diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/offline/HlsDownloader.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/offline/HlsDownloader.java index 432090a519..4b1f39da71 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/offline/HlsDownloader.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/offline/HlsDownloader.java @@ -29,6 +29,7 @@ import androidx.media3.exoplayer.hls.playlist.HlsPlaylist; import androidx.media3.exoplayer.hls.playlist.HlsPlaylistParser; import androidx.media3.exoplayer.offline.SegmentDownloader; import androidx.media3.exoplayer.upstream.ParsingLoadable.Parser; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; @@ -48,14 +49,13 @@ import java.util.concurrent.Executor; * .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory()); * // Create a downloader for the first variant in a multivariant playlist. * HlsDownloader hlsDownloader = - * new HlsDownloader( - * new MediaItem.Builder() + * new HlsDownloader.Factory(cacheDataSourceFactory) + * .create(new MediaItem.Builder() * .setUri(playlistUri) - * .setStreamKeys( - * Collections.singletonList( - * new StreamKey(HlsMultivariantPlaylist.GROUP_INDEX_VARIANT, 0))) - * .build(), - * Collections.singletonList(); + * .setStreamKeys( + * ImmutableList.of( + * new StreamKey(HlsMultivariantPlaylist.GROUP_INDEX_VARIANT, 0))) + * .build()); * // Perform the download. * hlsDownloader.download(progressListener); * // Use the downloaded data for playback. @@ -66,27 +66,79 @@ import java.util.concurrent.Executor; @UnstableApi public final class HlsDownloader extends SegmentDownloader { + /** A factory for {@linkplain HlsDownloader HLS downloaders}. */ + public static final class Factory extends BaseFactory { + + /** + * Creates a factory for {@link HlsDownloader}. + * + * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the + * download will be written. + */ + public Factory(CacheDataSource.Factory cacheDataSourceFactory) { + super(cacheDataSourceFactory, new HlsPlaylistParser()); + } + + /** + * Sets a parser for HLS playlists. + * + * @return This factory, for convenience. + */ + @CanIgnoreReturnValue + public Factory setManifestParser(HlsPlaylistParser manifestParser) { + this.manifestParser = manifestParser; + return this; + } + + /** + * Sets the {@link Executor} used to make requests for the media being downloaded. Providing an + * {@link Executor} that uses multiple threads will speed up the download by allowing parts of + * it to be executed in parallel. + * + * @return This factory, for convenience. + */ + @Override + @CanIgnoreReturnValue + public Factory setExecutor(Executor executor) { + return (Factory) super.setExecutor(executor); + } + + /** + * Sets the maximum difference of the start time of two segments, up to which the segments (of + * the same URI) should be merged into a single download segment, in milliseconds. + * + * @return This factory, for convenience. + */ + @Override + @CanIgnoreReturnValue + public Factory setMaxMergedSegmentStartTimeDiffMs(long maxMergedSegmentStartTimeDiffMs) { + return (Factory) super.setMaxMergedSegmentStartTimeDiffMs(maxMergedSegmentStartTimeDiffMs); + } + + /** Creates {@linkplain HlsDownloader HLS downloaders}. */ + @Override + public HlsDownloader create(MediaItem mediaItem) { + return new HlsDownloader( + mediaItem, + manifestParser, + cacheDataSourceFactory, + executor, + maxMergedSegmentStartTimeDiffMs); + } + } + /** - * Creates a new instance. - * - * @param mediaItem The {@link MediaItem} to be downloaded. - * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the - * download will be written. + * @deprecated Use {@link HlsDownloader.Factory#create(MediaItem)} instead. */ + @Deprecated public HlsDownloader(MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory) { this(mediaItem, cacheDataSourceFactory, Runnable::run); } /** - * Creates a new instance. - * - * @param mediaItem The {@link MediaItem} to be downloaded. - * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the - * download will be written. - * @param executor An {@link Executor} used to make requests for the media being downloaded. - * Providing an {@link Executor} that uses multiple threads will speed up the download by - * allowing parts of it to be executed in parallel. + * @deprecated Use {@link HlsDownloader.Factory#create(MediaItem)} instead. */ + @Deprecated public HlsDownloader( MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory, Executor executor) { this( @@ -111,7 +163,7 @@ public final class HlsDownloader extends SegmentDownloader { * segments, up to which the segments (of the same URI) should be merged into a single * download segment, in milliseconds. */ - public HlsDownloader( + private HlsDownloader( MediaItem mediaItem, Parser manifestParser, CacheDataSource.Factory cacheDataSourceFactory, diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/offline/HlsDownloaderTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/offline/HlsDownloaderTest.java index 4e239e3502..a97fcfe679 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/offline/HlsDownloaderTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/offline/HlsDownloaderTest.java @@ -223,9 +223,8 @@ public class HlsDownloaderTest { new CacheDataSource.Factory() .setCache(cache) .setUpstreamDataSourceFactory(new FakeDataSource.Factory().setFakeDataSet(fakeDataSet)); - return new HlsDownloader( - new MediaItem.Builder().setUri(mediaPlaylistUri).setStreamKeys(keys).build(), - cacheDataSourceFactory); + return new HlsDownloader.Factory(cacheDataSourceFactory) + .create(new MediaItem.Builder().setUri(mediaPlaylistUri).setStreamKeys(keys).build()); } private static ArrayList getKeys(int... variantIndices) { diff --git a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/offline/SsDownloader.java b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/offline/SsDownloader.java index 5e6c963a94..bccacc9a2a 100644 --- a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/offline/SsDownloader.java +++ b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/offline/SsDownloader.java @@ -28,6 +28,7 @@ import androidx.media3.exoplayer.smoothstreaming.manifest.SsManifest; import androidx.media3.exoplayer.smoothstreaming.manifest.SsManifest.StreamElement; import androidx.media3.exoplayer.smoothstreaming.manifest.SsManifestParser; import androidx.media3.exoplayer.upstream.ParsingLoadable.Parser; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; @@ -45,12 +46,11 @@ import java.util.concurrent.Executor; * .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory()); * // Create a downloader for the first track of the first stream element. * SsDownloader ssDownloader = - * new SsDownloader( - * new MediaItem.Builder() - * .setUri(manifestUri) - * .setStreamKeys(Collections.singletonList(new StreamKey(0, 0))) - * .build(), - * cacheDataSourceFactory); + * new SsDownloader.Factory(cacheDataSourceFactory) + * .create(new MediaItem.Builder() + * .setUri(manifestUri) + * .setStreamKeys(ImmutableList.of(new StreamKey(0, 0))) + * .build()); * // Perform the download. * ssDownloader.download(progressListener); * // Use the downloaded data for playback. @@ -61,27 +61,84 @@ import java.util.concurrent.Executor; @UnstableApi public final class SsDownloader extends SegmentDownloader { + /** A factory for {@linkplain SsDownloader SmoothStreaming downloaders}. */ + public static final class Factory extends BaseFactory { + + /** + * Creates a factory for {@link SsDownloader}. + * + * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the + * download will be written. + */ + public Factory(CacheDataSource.Factory cacheDataSourceFactory) { + super(cacheDataSourceFactory, new SsManifestParser()); + } + + /** + * Sets a parser for SmoothStreaming manifests. + * + * @return This factory, for convenience. + */ + @CanIgnoreReturnValue + public Factory setManifestParser(SsManifestParser manifestParser) { + this.manifestParser = manifestParser; + return this; + } + + /** + * Sets the {@link Executor} used to make requests for the media being downloaded. Providing an + * {@link Executor} that uses multiple threads will speed up the download by allowing parts of + * it to be executed in parallel. + * + * @return This factory, for convenience. + */ + @Override + @CanIgnoreReturnValue + public Factory setExecutor(Executor executor) { + return (Factory) super.setExecutor(executor); + } + + /** + * Sets the maximum difference of the start time of two segments, up to which the segments (of + * the same URI) should be merged into a single download segment, in milliseconds. + * + * @return This factory, for convenience. + */ + @Override + @CanIgnoreReturnValue + public Factory setMaxMergedSegmentStartTimeDiffMs(long maxMergedSegmentStartTimeDiffMs) { + return (Factory) super.setMaxMergedSegmentStartTimeDiffMs(maxMergedSegmentStartTimeDiffMs); + } + + /** Creates {@linkplain SsDownloader SmoothStreaming downloaders}. */ + @Override + public SsDownloader create(MediaItem mediaItem) { + return new SsDownloader( + mediaItem + .buildUpon() + .setUri( + Util.fixSmoothStreamingIsmManifestUri( + checkNotNull(mediaItem.localConfiguration).uri)) + .build(), + manifestParser, + cacheDataSourceFactory, + executor, + maxMergedSegmentStartTimeDiffMs); + } + } + /** - * Creates an instance. - * - * @param mediaItem The {@link MediaItem} to be downloaded. - * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the - * download will be written. + * @deprecated Use {@link SsDownloader.Factory#create(MediaItem)} instead. */ + @Deprecated public SsDownloader(MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory) { this(mediaItem, cacheDataSourceFactory, Runnable::run); } /** - * Creates an instance. - * - * @param mediaItem The {@link MediaItem} to be downloaded. - * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the - * download will be written. - * @param executor An {@link Executor} used to make requests for the media being downloaded. - * Providing an {@link Executor} that uses multiple threads will speed up the download by - * allowing parts of it to be executed in parallel. + * @deprecated Use {@link SsDownloader.Factory#create(MediaItem)} instead. */ + @Deprecated public SsDownloader( MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory, Executor executor) { this( @@ -111,7 +168,7 @@ public final class SsDownloader extends SegmentDownloader { * segments, up to which the segments (of the same URI) should be merged into a single * download segment, in milliseconds. */ - public SsDownloader( + private SsDownloader( MediaItem mediaItem, Parser manifestParser, CacheDataSource.Factory cacheDataSourceFactory, diff --git a/libraries/test_exoplayer_playback/src/androidTest/java/androidx/media3/test/exoplayer/playback/gts/DashDownloadTest.java b/libraries/test_exoplayer_playback/src/androidTest/java/androidx/media3/test/exoplayer/playback/gts/DashDownloadTest.java index 313f187a4e..89944ec9bf 100644 --- a/libraries/test_exoplayer_playback/src/androidTest/java/androidx/media3/test/exoplayer/playback/gts/DashDownloadTest.java +++ b/libraries/test_exoplayer_playback/src/androidTest/java/androidx/media3/test/exoplayer/playback/gts/DashDownloadTest.java @@ -126,8 +126,7 @@ public final class DashDownloadTest { new CacheDataSource.Factory() .setCache(cache) .setUpstreamDataSourceFactory(httpDataSourceFactory); - return new DashDownloader( - new MediaItem.Builder().setUri(MANIFEST_URI).setStreamKeys(keys).build(), - cacheDataSourceFactory); + return new DashDownloader.Factory(cacheDataSourceFactory) + .create(new MediaItem.Builder().setUri(MANIFEST_URI).setStreamKeys(keys).build()); } }