mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Propagate download byte range to ProgressiveDownloader
PiperOrigin-RevId: 729484044
This commit is contained in:
parent
378e70e15f
commit
d35fccef59
@ -35,6 +35,13 @@
|
|||||||
locked in case the data source throws an `Exception` other than
|
locked in case the data source throws an `Exception` other than
|
||||||
`IOException`
|
`IOException`
|
||||||
([#9760](https://github.com/google/ExoPlayer/issues/9760)).
|
([#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:
|
* OkHttp Extension:
|
||||||
* Cronet Extension:
|
* Cronet Extension:
|
||||||
* RTMP Extension:
|
* RTMP Extension:
|
||||||
|
@ -78,13 +78,16 @@ public class DefaultDownloaderFactory implements DownloaderFactory {
|
|||||||
case C.CONTENT_TYPE_SS:
|
case C.CONTENT_TYPE_SS:
|
||||||
return createDownloader(request, contentType);
|
return createDownloader(request, contentType);
|
||||||
case C.CONTENT_TYPE_OTHER:
|
case C.CONTENT_TYPE_OTHER:
|
||||||
|
@Nullable DownloadRequest.ByteRange byteRange = request.byteRange;
|
||||||
return new ProgressiveDownloader(
|
return new ProgressiveDownloader(
|
||||||
new MediaItem.Builder()
|
new MediaItem.Builder()
|
||||||
.setUri(request.uri)
|
.setUri(request.uri)
|
||||||
.setCustomCacheKey(request.customCacheKey)
|
.setCustomCacheKey(request.customCacheKey)
|
||||||
.build(),
|
.build(),
|
||||||
cacheDataSourceFactory,
|
cacheDataSourceFactory,
|
||||||
executor);
|
executor,
|
||||||
|
(byteRange != null) ? byteRange.offset : 0,
|
||||||
|
(byteRange != null) ? byteRange.length : C.LENGTH_UNSET);
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Unsupported type: " + contentType);
|
throw new IllegalArgumentException("Unsupported type: " + contentType);
|
||||||
}
|
}
|
||||||
|
@ -15,9 +15,11 @@
|
|||||||
*/
|
*/
|
||||||
package androidx.media3.exoplayer.offline;
|
package androidx.media3.exoplayer.offline;
|
||||||
|
|
||||||
|
import static androidx.annotation.VisibleForTesting.PRIVATE;
|
||||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.PriorityTaskManager;
|
import androidx.media3.common.PriorityTaskManager;
|
||||||
@ -39,7 +41,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
public final class ProgressiveDownloader implements Downloader {
|
public final class ProgressiveDownloader implements Downloader {
|
||||||
|
|
||||||
private final Executor executor;
|
private final Executor executor;
|
||||||
private final DataSpec dataSpec;
|
|
||||||
|
@VisibleForTesting(otherwise = PRIVATE)
|
||||||
|
/* package */ final DataSpec dataSpec;
|
||||||
|
|
||||||
private final CacheDataSource dataSource;
|
private final CacheDataSource dataSource;
|
||||||
private final CacheWriter cacheWriter;
|
private final CacheWriter cacheWriter;
|
||||||
@Nullable private final PriorityTaskManager priorityTaskManager;
|
@Nullable private final PriorityTaskManager priorityTaskManager;
|
||||||
@ -57,7 +62,26 @@ public final class ProgressiveDownloader implements Downloader {
|
|||||||
*/
|
*/
|
||||||
public ProgressiveDownloader(
|
public ProgressiveDownloader(
|
||||||
MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory) {
|
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(
|
public ProgressiveDownloader(
|
||||||
MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory, Executor executor) {
|
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);
|
this.executor = Assertions.checkNotNull(executor);
|
||||||
Assertions.checkNotNull(mediaItem.localConfiguration);
|
Assertions.checkNotNull(mediaItem.localConfiguration);
|
||||||
dataSpec =
|
dataSpec =
|
||||||
@ -79,6 +131,8 @@ public final class ProgressiveDownloader implements Downloader {
|
|||||||
.setUri(mediaItem.localConfiguration.uri)
|
.setUri(mediaItem.localConfiguration.uri)
|
||||||
.setKey(mediaItem.localConfiguration.customCacheKey)
|
.setKey(mediaItem.localConfiguration.customCacheKey)
|
||||||
.setFlags(DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION)
|
.setFlags(DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION)
|
||||||
|
.setPosition(position)
|
||||||
|
.setLength(length)
|
||||||
.build();
|
.build();
|
||||||
dataSource = cacheDataSourceFactory.createDataSourceForDownloading();
|
dataSource = cacheDataSourceFactory.createDataSourceForDownloading();
|
||||||
@SuppressWarnings("nullness:methodref.receiver.bound")
|
@SuppressWarnings("nullness:methodref.receiver.bound")
|
||||||
|
@ -18,6 +18,7 @@ package androidx.media3.exoplayer.offline;
|
|||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.datasource.PlaceholderDataSource;
|
import androidx.media3.datasource.PlaceholderDataSource;
|
||||||
import androidx.media3.datasource.cache.Cache;
|
import androidx.media3.datasource.cache.Cache;
|
||||||
import androidx.media3.datasource.cache.CacheDataSource;
|
import androidx.media3.datasource.cache.CacheDataSource;
|
||||||
@ -31,7 +32,28 @@ import org.mockito.Mockito;
|
|||||||
public final class DefaultDownloaderFactoryTest {
|
public final class DefaultDownloaderFactoryTest {
|
||||||
|
|
||||||
@Test
|
@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 =
|
CacheDataSource.Factory cacheDataSourceFactory =
|
||||||
new CacheDataSource.Factory()
|
new CacheDataSource.Factory()
|
||||||
.setCache(Mockito.mock(Cache.class))
|
.setCache(Mockito.mock(Cache.class))
|
||||||
@ -43,6 +65,10 @@ public final class DefaultDownloaderFactoryTest {
|
|||||||
factory.createDownloader(
|
factory.createDownloader(
|
||||||
new DownloadRequest.Builder(/* id= */ "id", Uri.parse("https://www.test.com/download"))
|
new DownloadRequest.Builder(/* id= */ "id", Uri.parse("https://www.test.com/download"))
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
assertThat(downloader).isInstanceOf(ProgressiveDownloader.class);
|
assertThat(downloader).isInstanceOf(ProgressiveDownloader.class);
|
||||||
|
ProgressiveDownloader progressiveDownloader = (ProgressiveDownloader) downloader;
|
||||||
|
assertThat(progressiveDownloader.dataSpec.position).isEqualTo(0);
|
||||||
|
assertThat(progressiveDownloader.dataSpec.length).isEqualTo(C.LENGTH_UNSET);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,6 +68,27 @@ public class ProgressiveDownloaderTest {
|
|||||||
Util.recursiveDelete(testDir);
|
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
|
@Test
|
||||||
public void download_afterReadFailure_succeeds() throws Exception {
|
public void download_afterReadFailure_succeeds() throws Exception {
|
||||||
Uri uri = Uri.parse("test:///test.mp4");
|
Uri uri = Uri.parse("test:///test.mp4");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user