Propagate download byte range to ProgressiveDownloader

PiperOrigin-RevId: 729484044
This commit is contained in:
tianyifeng 2025-02-21 04:52:15 -08:00 committed by Copybara-Service
parent 378e70e15f
commit d35fccef59
5 changed files with 115 additions and 4 deletions

View File

@ -35,6 +35,13 @@
locked in case the data source throws an `Exception` other than
`IOException`
([#9760](https://github.com/google/ExoPlayer/issues/9760)).
* Add partial download support for progressive streams. Apps can prepare a
progressive stream with `DownloadHelper`, and request a
`DownloadRequest` from the helper with specifying the time-based media
start and end positions that the download should cover. The returned
`DownloadRequest` carries the resolved byte range, with which a
`ProgressiveDownloader` can be created and download the content
correspondingly.
* OkHttp Extension:
* Cronet Extension:
* RTMP Extension:

View File

@ -78,13 +78,16 @@ public class DefaultDownloaderFactory implements DownloaderFactory {
case C.CONTENT_TYPE_SS:
return createDownloader(request, contentType);
case C.CONTENT_TYPE_OTHER:
@Nullable DownloadRequest.ByteRange byteRange = request.byteRange;
return new ProgressiveDownloader(
new MediaItem.Builder()
.setUri(request.uri)
.setCustomCacheKey(request.customCacheKey)
.build(),
cacheDataSourceFactory,
executor);
executor,
(byteRange != null) ? byteRange.offset : 0,
(byteRange != null) ? byteRange.length : C.LENGTH_UNSET);
default:
throw new IllegalArgumentException("Unsupported type: " + contentType);
}

View File

@ -15,9 +15,11 @@
*/
package androidx.media3.exoplayer.offline;
import static androidx.annotation.VisibleForTesting.PRIVATE;
import static androidx.media3.common.util.Assertions.checkNotNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.PriorityTaskManager;
@ -39,7 +41,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public final class ProgressiveDownloader implements Downloader {
private final Executor executor;
private final DataSpec dataSpec;
@VisibleForTesting(otherwise = PRIVATE)
/* package */ final DataSpec dataSpec;
private final CacheDataSource dataSource;
private final CacheWriter cacheWriter;
@Nullable private final PriorityTaskManager priorityTaskManager;
@ -57,7 +62,26 @@ public final class ProgressiveDownloader implements Downloader {
*/
public ProgressiveDownloader(
MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory) {
this(mediaItem, cacheDataSourceFactory, Runnable::run);
this(mediaItem, cacheDataSourceFactory, /* executor= */ Runnable::run);
}
/**
* Creates a new instance.
*
* @param mediaItem The media item with a uri to the stream to be downloaded.
* @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
* download will be written.
* @param position The position of the {@link DataSpec} from which the {@link
* ProgressiveDownloader} downloads.
* @param length The length of the {@link DataSpec} for which the {@link ProgressiveDownloader}
* downloads.
*/
public ProgressiveDownloader(
MediaItem mediaItem,
CacheDataSource.Factory cacheDataSourceFactory,
long position,
long length) {
this(mediaItem, cacheDataSourceFactory, /* executor= */ Runnable::run, position, length);
}
/**
@ -72,6 +96,34 @@ public final class ProgressiveDownloader implements Downloader {
*/
public ProgressiveDownloader(
MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory, Executor executor) {
this(
mediaItem,
cacheDataSourceFactory,
executor,
/* position= */ 0,
/* length= */ C.LENGTH_UNSET);
}
/**
* Creates a new instance.
*
* @param mediaItem The media item with a uri to the stream 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. In
* the future, providing an {@link Executor} that uses multiple threads may speed up the
* download by allowing parts of it to be executed in parallel.
* @param position The position of the {@link DataSpec} from which the {@link
* ProgressiveDownloader} downloads.
* @param length The length of the {@link DataSpec} for which the {@link ProgressiveDownloader}
* downloads.
*/
public ProgressiveDownloader(
MediaItem mediaItem,
CacheDataSource.Factory cacheDataSourceFactory,
Executor executor,
long position,
long length) {
this.executor = Assertions.checkNotNull(executor);
Assertions.checkNotNull(mediaItem.localConfiguration);
dataSpec =
@ -79,6 +131,8 @@ public final class ProgressiveDownloader implements Downloader {
.setUri(mediaItem.localConfiguration.uri)
.setKey(mediaItem.localConfiguration.customCacheKey)
.setFlags(DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION)
.setPosition(position)
.setLength(length)
.build();
dataSource = cacheDataSourceFactory.createDataSourceForDownloading();
@SuppressWarnings("nullness:methodref.receiver.bound")

View File

@ -18,6 +18,7 @@ package androidx.media3.exoplayer.offline;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.media3.common.C;
import androidx.media3.datasource.PlaceholderDataSource;
import androidx.media3.datasource.cache.Cache;
import androidx.media3.datasource.cache.CacheDataSource;
@ -31,7 +32,28 @@ import org.mockito.Mockito;
public final class DefaultDownloaderFactoryTest {
@Test
public void createProgressiveDownloader() throws Exception {
public void createProgressiveDownloader_downloadRequestWithByteRange() throws Exception {
CacheDataSource.Factory cacheDataSourceFactory =
new CacheDataSource.Factory()
.setCache(Mockito.mock(Cache.class))
.setUpstreamDataSourceFactory(PlaceholderDataSource.FACTORY);
DownloaderFactory factory =
new DefaultDownloaderFactory(cacheDataSourceFactory, /* executor= */ Runnable::run);
Downloader downloader =
factory.createDownloader(
new DownloadRequest.Builder(/* id= */ "id", Uri.parse("https://www.test.com/download"))
.setByteRange(/* offset= */ 10, /* length= */ 20)
.build());
assertThat(downloader).isInstanceOf(ProgressiveDownloader.class);
ProgressiveDownloader progressiveDownloader = (ProgressiveDownloader) downloader;
assertThat(progressiveDownloader.dataSpec.position).isEqualTo(10);
assertThat(progressiveDownloader.dataSpec.length).isEqualTo(20);
}
@Test
public void createProgressiveDownloader_downloadRequestWithoutByteRange() throws Exception {
CacheDataSource.Factory cacheDataSourceFactory =
new CacheDataSource.Factory()
.setCache(Mockito.mock(Cache.class))
@ -43,6 +65,10 @@ public final class DefaultDownloaderFactoryTest {
factory.createDownloader(
new DownloadRequest.Builder(/* id= */ "id", Uri.parse("https://www.test.com/download"))
.build());
assertThat(downloader).isInstanceOf(ProgressiveDownloader.class);
ProgressiveDownloader progressiveDownloader = (ProgressiveDownloader) downloader;
assertThat(progressiveDownloader.dataSpec.position).isEqualTo(0);
assertThat(progressiveDownloader.dataSpec.length).isEqualTo(C.LENGTH_UNSET);
}
}

View File

@ -68,6 +68,27 @@ public class ProgressiveDownloaderTest {
Util.recursiveDelete(testDir);
}
@Test
public void download_withNonDefaultByteRange_succeeds() throws Exception {
Uri uri = Uri.parse("test:///test.mp4");
FakeDataSet data = new FakeDataSet();
data.newData(uri).appendReadData(1024);
DataSource.Factory upstreamDataSource = new FakeDataSource.Factory().setFakeDataSet(data);
MediaItem mediaItem = MediaItem.fromUri(uri);
CacheDataSource.Factory cacheDataSourceFactory =
new CacheDataSource.Factory()
.setCache(downloadCache)
.setUpstreamDataSourceFactory(upstreamDataSource);
ProgressiveDownloader downloader =
new ProgressiveDownloader(
mediaItem, cacheDataSourceFactory, /* position= */ 0, /* length= */ 100);
TestProgressListener progressListener = new TestProgressListener();
downloader.download(progressListener);
assertThat(progressListener.bytesDownloaded).isEqualTo(100);
}
@Test
public void download_afterReadFailure_succeeds() throws Exception {
Uri uri = Uri.parse("test:///test.mp4");