From 7f6ddef5021ba532cef89593e712c73af7c423a2 Mon Sep 17 00:00:00 2001 From: tianyifeng Date: Thu, 17 Apr 2025 10:11:55 -0700 Subject: [PATCH] Enable DownloadHelper to create DownloadRequest with timeRange PiperOrigin-RevId: 748722156 --- .../exoplayer/offline/DownloadHelper.java | 45 +++- .../exoplayer/offline/DownloadHelperTest.java | 193 +++++++++++++++--- 2 files changed, 202 insertions(+), 36 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java index 2e2562710f..685dd215bc 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java @@ -830,7 +830,6 @@ public final class DownloadHelper { * to, or {@link C#TIME_UNSET} if the download should cover to the end of the media. If the * {@code endPositionMs} is larger than the duration of the media, then the download will * cover to the end of the media. - * @throws IllegalStateException If the media item is of type DASH, HLS or SmoothStreaming. */ public DownloadRequest getDownloadRequest( @Nullable byte[] data, long startPositionMs, long durationMs) { @@ -863,15 +862,11 @@ public final class DownloadHelper { * {@link C#TIME_UNSET} if the download should cover to the end of the media. If the end * position resolved from {@code startPositionMs} and {@code durationMs} is beyond the * duration of the media, then the download will just cover to the end of the media. - * @throws IllegalStateException If the media item is of type DASH, HLS or SmoothStreaming. */ public DownloadRequest getDownloadRequest( String id, @Nullable byte[] data, long startPositionMs, long durationMs) { - checkState( - mode == MODE_PREPARE_PROGRESSIVE_SOURCE, - "Partial download is only supported for progressive streams"); DownloadRequest.Builder builder = getDownloadRequestBuilder(id, data); - assertPreparedWithProgressiveSource(); + assertPreparedWithMedia(); populateDownloadRequestBuilderWithDownloadRange(builder, startPositionMs, durationMs); return builder.build(); } @@ -906,13 +901,22 @@ public final class DownloadHelper { private void populateDownloadRequestBuilderWithDownloadRange( DownloadRequest.Builder requestBuilder, long startPositionMs, long durationMs) { + switch (mode) { + case MODE_PREPARE_PROGRESSIVE_SOURCE: + populateDownloadRequestBuilderWithByteRange(requestBuilder, startPositionMs, durationMs); + break; + case MODE_PREPARE_NON_PROGRESSIVE_SOURCE_AND_SELECT_TRACKS: + populateDownloadRequestBuilderWithTimeRange(requestBuilder, startPositionMs, durationMs); + break; + default: + break; + } + } + + private void populateDownloadRequestBuilderWithByteRange( + DownloadRequest.Builder requestBuilder, long startPositionMs, long durationMs) { assertPreparedWithProgressiveSource(); Timeline timeline = mediaPreparer.timeline; - if (mediaPreparer.mediaPeriods.length > 1) { - Log.w(TAG, "Partial download is only supported for single period."); - return; - } - Timeline.Window window = new Timeline.Window(); Timeline.Period period = new Timeline.Period(); long periodStartPositionUs = @@ -957,6 +961,25 @@ public final class DownloadHelper { } } + private void populateDownloadRequestBuilderWithTimeRange( + DownloadRequest.Builder requestBuilder, long startPositionMs, long durationMs) { + assertPreparedWithNonProgressiveSourceAndTracksSelected(); + Timeline timeline = mediaPreparer.timeline; + Timeline.Window window = timeline.getWindow(0, new Timeline.Window()); + + long startPositionUs = + startPositionMs == C.TIME_UNSET + ? window.getDefaultPositionUs() + : Util.msToUs(startPositionMs); + long windowDurationUs = window.getDurationUs(); + long durationUs = durationMs == C.TIME_UNSET ? windowDurationUs : Util.msToUs(durationMs); + if (windowDurationUs != C.TIME_UNSET) { + startPositionUs = min(startPositionUs, windowDurationUs); + durationUs = min(durationUs, windowDurationUs - startPositionUs); + } + requestBuilder.setTimeRange(startPositionUs, durationUs); + } + @RequiresNonNull({ "trackGroupArrays", "trackSelectionsByPeriodAndRenderer", diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DownloadHelperTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DownloadHelperTest.java index 72eed79b14..3cfc53e6aa 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DownloadHelperTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DownloadHelperTest.java @@ -18,7 +18,6 @@ package androidx.media3.exoplayer.offline; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static org.junit.Assert.assertThrows; import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; import android.content.Context; @@ -72,10 +71,15 @@ import org.junit.runner.RunWith; public class DownloadHelperTest { private static final Object TEST_MANIFEST = new Object(); + + private static final long TEST_WINDOW_DEFAULT_POSITION_US = C.MICROS_PER_SECOND; private static final Timeline TEST_TIMELINE = new FakeTimeline( new Object[] {TEST_MANIFEST}, - new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ new Object())); + new TimelineWindowDefinition.Builder() + .setPeriodCount(2) + .setDefaultPositionUs(TEST_WINDOW_DEFAULT_POSITION_US) + .build()); private static TrackGroup trackGroupVideoLow; private static TrackGroup trackGroupVideoLowAndHigh; @@ -86,6 +90,7 @@ public class DownloadHelperTest { private static TrackGroupArray[] trackGroupArrays; private static MediaItem testMediaItem; + private RenderersFactory renderersFactory; private DownloadHelper downloadHelper; @BeforeClass @@ -124,7 +129,7 @@ public class DownloadHelperTest { FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); FakeRenderer audioRenderer = new FakeRenderer(C.TRACK_TYPE_AUDIO); FakeRenderer textRenderer = new FakeRenderer(C.TRACK_TYPE_TEXT); - RenderersFactory renderersFactory = + renderersFactory = (handler, videoListener, audioListener, metadata, text) -> new Renderer[] {textRenderer, audioRenderer, videoRenderer}; @@ -329,8 +334,8 @@ public class DownloadHelperTest { prepareDownloadHelper(downloadHelper); // Select parameters to require some merging of track groups because the new parameters add // all video tracks to initial video single track selection. - TrackSelectionParameters parameters = - new TrackSelectionParameters.Builder(getApplicationContext()) + DefaultTrackSelector.Parameters parameters = + new DefaultTrackSelector.Parameters.Builder() .setPreferredAudioLanguage("de") .setPreferredTextLanguage("en") .build(); @@ -433,8 +438,8 @@ public class DownloadHelperTest { prepareDownloadHelper(downloadHelper); // Ensure we have track groups with multiple indices, renderers with multiple track groups and // also renderers without any track groups. - TrackSelectionParameters parameters = - new TrackSelectionParameters.Builder(getApplicationContext()) + DefaultTrackSelector.Parameters parameters = + new DefaultTrackSelector.Parameters.Builder() .setPreferredAudioLanguage("de") .setPreferredTextLanguage("en") .build(); @@ -464,8 +469,8 @@ public class DownloadHelperTest { throws Exception { prepareDownloadHelper(downloadHelper); - TrackSelectionParameters parameters = - new TrackSelectionParameters.Builder(getApplicationContext()) + DefaultTrackSelector.Parameters parameters = + new DefaultTrackSelector.Parameters.Builder() .addOverride(new TrackSelectionOverride(trackGroupAudioUs, /* trackIndex= */ 0)) .addOverride(new TrackSelectionOverride(trackGroupAudioZh, /* trackIndex= */ 0)) .setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, /* disabled= */ true) @@ -485,7 +490,7 @@ public class DownloadHelperTest { @Test public void - getDownloadRequest_createsDownloadRequestWithConcreteTimeRange_requestContainsConcreteByteRange() + getDownloadRequestForProgressive_withConcreteTimeRange_requestContainsConcreteByteRange() throws Exception { DownloadHelper downloadHelper = new DownloadHelper.Factory() @@ -504,7 +509,7 @@ public class DownloadHelperTest { @Test public void - getDownloadRequest_createsDownloadRequestWithUnsetStartPosition_requestContainsConcreteByteRange() + getDownloadRequestForProgressive_withUnsetStartPosition_requestContainsConcreteByteRange() throws Exception { DownloadHelper downloadHelper = new DownloadHelper.Factory() @@ -522,8 +527,9 @@ public class DownloadHelperTest { } @Test - public void getDownloadRequest_createsDownloadRequestWithUnsetLength_requestContainsUnsetLength() - throws Exception { + public void + getDownloadRequestForProgressive_withUnsetDuration_requestContainsUnsetByteRangeLength() + throws Exception { DownloadHelper downloadHelper = new DownloadHelper.Factory() .setDataSourceFactory(new DefaultDataSource.Factory(getApplicationContext())) @@ -541,7 +547,7 @@ public class DownloadHelperTest { @Test public void - getDownloadRequest_createsDownloadRequestForTooShortStreamWithTimeRange_requestContainsUnsetLength() + getDownloadRequestForShortProgressive_withConcreteTimeRange_requestContainsUnsetByteRangeLength() throws Exception { DownloadHelper downloadHelper = new DownloadHelper.Factory() @@ -559,9 +565,8 @@ public class DownloadHelperTest { } @Test - public void - getDownloadRequest_createsDownloadRequestWithoutTimeRange_requestContainsNullByteRange() - throws Exception { + public void getDownloadRequestForProgressive_withoutRange_requestContainsNullByteRange() + throws Exception { DownloadHelper downloadHelper = new DownloadHelper.Factory() .setDataSourceFactory(new DefaultDataSource.Factory(getApplicationContext())) @@ -575,17 +580,155 @@ public class DownloadHelperTest { @Test public void - getDownloadRequest_createDownloadRequestWithTimeRangeForNonProgressiveStream_throwsIllegalStateException() + getDownloadRequestForNonProgressive_withConcreteTimeRange_requestContainsCorrectTimeRange() throws Exception { - // We use this.downloadHelper as it was created with a TestMediaSource, thus the DownloadHelper - // will treat it as non-progressive. + DownloadHelper downloadHelper = + new DownloadHelper( + new MediaItem.Builder() + .setUri("http://test.uri") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build(), + new TestMediaSource(), + DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, + new DefaultRendererCapabilitiesList.Factory(renderersFactory) + .createRendererCapabilitiesList()); prepareDownloadHelper(downloadHelper); - assertThrows( - IllegalStateException.class, - () -> - downloadHelper.getDownloadRequest( - /* data= */ null, /* startPositionMs= */ 0, /* durationMs= */ 10000)); + DownloadRequest downloadRequest = + downloadHelper.getDownloadRequest( + /* data= */ null, /* startPositionMs= */ 0, /* durationMs= */ 10000); + + assertThat(downloadRequest.timeRange).isNotNull(); + assertThat(downloadRequest.timeRange.startPositionUs).isEqualTo(0); + assertThat(downloadRequest.timeRange.durationUs).isEqualTo(10000000); + } + + @Test + public void + getDownloadRequestForNonProgressive_withUnsetStartPosition_requestContainsCorrectTimeRange() + throws Exception { + DownloadHelper downloadHelper = + new DownloadHelper( + new MediaItem.Builder() + .setUri("http://test.uri") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build(), + new TestMediaSource(), + DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, + new DefaultRendererCapabilitiesList.Factory(renderersFactory) + .createRendererCapabilitiesList()); + prepareDownloadHelper(downloadHelper); + + DownloadRequest downloadRequest = + downloadHelper.getDownloadRequest( + /* data= */ null, /* startPositionMs= */ C.TIME_UNSET, /* durationMs= */ 5000); + + assertThat(downloadRequest.timeRange).isNotNull(); + // The startPositionUs is set to window.defaultPositionUs. + Timeline.Window window = TEST_TIMELINE.getWindow(0, new Timeline.Window()); + assertThat(downloadRequest.timeRange.startPositionUs).isEqualTo(window.defaultPositionUs); + assertThat(downloadRequest.timeRange.durationUs).isEqualTo(5000000); + } + + @Test + public void + getDownloadRequestForNonProgressive_withStartPositionExceedingWindowDuration_requestContainsCorrectTimeRange() + throws Exception { + DownloadHelper downloadHelper = + new DownloadHelper( + new MediaItem.Builder() + .setUri("http://test.uri") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build(), + new TestMediaSource(), + DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, + new DefaultRendererCapabilitiesList.Factory(renderersFactory) + .createRendererCapabilitiesList()); + prepareDownloadHelper(downloadHelper); + Timeline.Window window = TEST_TIMELINE.getWindow(0, new Timeline.Window()); + + DownloadRequest downloadRequest = + downloadHelper.getDownloadRequest( + /* data= */ null, + /* startPositionMs= */ window.durationUs + 100, + /* durationMs= */ C.TIME_UNSET); + + assertThat(downloadRequest.timeRange).isNotNull(); + // The startPositionUs is set to window.durationUs. + assertThat(downloadRequest.timeRange.startPositionUs).isEqualTo(window.durationUs); + assertThat(downloadRequest.timeRange.durationUs).isEqualTo(0); + } + + @Test + public void + getDownloadRequestForNonProgressive_withUnsetDuration_requestContainsCorrectTimeRange() + throws Exception { + DownloadHelper downloadHelper = + new DownloadHelper( + new MediaItem.Builder() + .setUri("http://test.uri") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build(), + new TestMediaSource(), + DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, + new DefaultRendererCapabilitiesList.Factory(renderersFactory) + .createRendererCapabilitiesList()); + prepareDownloadHelper(downloadHelper); + + DownloadRequest downloadRequest = + downloadHelper.getDownloadRequest( + /* data= */ null, /* startPositionMs= */ 10, /* durationMs= */ C.TIME_UNSET); + + assertThat(downloadRequest.timeRange).isNotNull(); + assertThat(downloadRequest.timeRange.startPositionUs).isEqualTo(10_000); + Timeline.Window window = TEST_TIMELINE.getWindow(0, new Timeline.Window()); + assertThat(downloadRequest.timeRange.durationUs).isEqualTo(window.durationUs - 10_000); + } + + @Test + public void + getDownloadRequestForNonProgressive_withDurationExceedingWindowDuration_requestContainsCorrectTimeRange() + throws Exception { + DownloadHelper downloadHelper = + new DownloadHelper( + new MediaItem.Builder() + .setUri("http://test.uri") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build(), + new TestMediaSource(), + DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, + new DefaultRendererCapabilitiesList.Factory(renderersFactory) + .createRendererCapabilitiesList()); + prepareDownloadHelper(downloadHelper); + Timeline.Window window = TEST_TIMELINE.getWindow(0, new Timeline.Window()); + + DownloadRequest downloadRequest = + downloadHelper.getDownloadRequest( + /* data= */ null, /* startPositionMs= */ 0, /* durationMs= */ window.durationUs + 100); + + assertThat(downloadRequest.timeRange).isNotNull(); + assertThat(downloadRequest.timeRange.startPositionUs).isEqualTo(0); + assertThat(downloadRequest.timeRange.durationUs).isEqualTo(window.durationUs); + } + + @Test + public void getDownloadRequestForNonProgressive_withoutRange_requestContainsNullTimeRange() + throws Exception { + DownloadHelper downloadHelper = + new DownloadHelper( + new MediaItem.Builder() + .setUri("http://test.uri") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build(), + new TestMediaSource(), + DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, + new DefaultRendererCapabilitiesList.Factory(renderersFactory) + .createRendererCapabilitiesList()); + prepareDownloadHelper(downloadHelper); + + DownloadRequest downloadRequest = downloadHelper.getDownloadRequest(/* data= */ null); + + assertThat(downloadRequest.timeRange).isNull(); } // https://github.com/androidx/media/issues/1224