Enable DownloadHelper to create DownloadRequest with byteRange

This change only enable the partial support for progressive stream. For now, creating `DownloadRequest` for partial adaptive media will result in an `IllegalStateException`.

PiperOrigin-RevId: 729100584
This commit is contained in:
tianyifeng 2025-02-20 07:14:24 -08:00 committed by Copybara-Service
parent daf8f9ff58
commit a5ffae17c3
2 changed files with 406 additions and 66 deletions

View File

@ -16,13 +16,18 @@
package androidx.media3.exoplayer.offline;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.castNonNull;
import static java.lang.Math.min;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.util.SparseIntArray;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
@ -33,11 +38,14 @@ import androidx.media3.common.TrackSelectionOverride;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.NullableType;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.TransferListener;
import androidx.media3.datasource.cache.Cache;
import androidx.media3.datasource.cache.CacheDataSource;
import androidx.media3.exoplayer.DefaultRendererCapabilitiesList;
import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.exoplayer.LoadingInfo;
@ -51,6 +59,7 @@ import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
import androidx.media3.exoplayer.source.MediaSource.MediaSourceCaller;
import androidx.media3.exoplayer.source.ProgressiveMediaSource;
import androidx.media3.exoplayer.source.TrackGroupArray;
import androidx.media3.exoplayer.source.chunk.MediaChunk;
import androidx.media3.exoplayer.source.chunk.MediaChunkIterator;
@ -65,7 +74,11 @@ import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.exoplayer.upstream.BandwidthMeter;
import androidx.media3.exoplayer.upstream.DefaultAllocator;
import androidx.media3.extractor.ExtractorsFactory;
import androidx.media3.extractor.SeekMap;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -118,6 +131,20 @@ public final class DownloadHelper {
return DEFAULT_TRACK_SELECTOR_PARAMETERS;
}
@Documented
@Retention(SOURCE)
@Target(TYPE_USE)
@IntDef({
MODE_NOT_PREPARE,
MODE_PREPARE_PROGRESSIVE_SOURCE,
MODE_PREPARE_NON_PROGRESSIVE_SOURCE_AND_SELECT_TRACKS
})
private @interface Mode {}
private static final int MODE_NOT_PREPARE = 0;
private static final int MODE_PREPARE_PROGRESSIVE_SOURCE = 1;
private static final int MODE_PREPARE_NON_PROGRESSIVE_SOURCE_AND_SELECT_TRACKS = 2;
/** A callback to be notified when the {@link DownloadHelper} is prepared. */
public interface Callback {
@ -158,6 +185,28 @@ public final class DownloadHelper {
/* drmSessionManager= */ null);
}
/**
* Creates a {@link DownloadHelper} for the given media item.
*
* @param context The context.
* @param mediaItem A {@link MediaItem}.
* @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest for adaptive
* streams or the {@link SeekMap} for progressive streams. In the latter case, this has to be
* a {@link CacheDataSource.Factory} for the {@link Cache} into which downloads will be
* written.
* @throws IllegalStateException If the corresponding module is missing for DASH, HLS or
* SmoothStreaming media items.
*/
public static DownloadHelper forMediaItem(
Context context, MediaItem mediaItem, DataSource.Factory dataSourceFactory) {
return forMediaItem(
mediaItem,
getDefaultTrackSelectorParameters(context),
/* renderersFactory= */ null,
dataSourceFactory,
/* drmSessionManager= */ null);
}
/**
* Creates a {@link DownloadHelper} for the given media item.
*
@ -166,9 +215,10 @@ public final class DownloadHelper {
* @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are
* selected.
* @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest for adaptive
* streams. This argument is required for adaptive streams and ignored for progressive
* streams.
* @return A {@link DownloadHelper}.
* streams or the {@link SeekMap} for progressive streams. This argument is required for
* adaptive streams or when requesting partial downloads for progressive streams. In the
* latter case, this has to be a {@link CacheDataSource.Factory} for the {@link Cache} into
* which downloads will be written.
* @throws IllegalStateException If the corresponding module is missing for DASH, HLS or
* SmoothStreaming media items.
* @throws IllegalArgumentException If the {@code dataSourceFactory} is null for adaptive streams.
@ -195,9 +245,10 @@ public final class DownloadHelper {
* @param trackSelectionParameters {@link TrackSelectionParameters} for selecting tracks for
* downloading.
* @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest for adaptive
* streams. This argument is required for adaptive streams and ignored for progressive
* streams.
* @return A {@link DownloadHelper}.
* streams or the {@link SeekMap} for progressive streams. This argument is required for
* adaptive streams or when requesting partial downloads for progressive streams. In the
* latter case, this has to be a {@link CacheDataSource.Factory} for the {@link Cache} into
* which downloads will be written.
* @throws IllegalStateException If the corresponding module is missing for DASH, HLS or
* SmoothStreaming media items.
* @throws IllegalArgumentException If the {@code dataSourceFactory} is null for adaptive streams.
@ -224,11 +275,12 @@ public final class DownloadHelper {
* @param trackSelectionParameters {@link TrackSelectionParameters} for selecting tracks for
* downloading.
* @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest for adaptive
* streams. This argument is required for adaptive streams and ignored for progressive
* streams.
* streams or the {@link SeekMap} for progressive streams. This argument is required for
* adaptive streams or when requesting partial downloads for progressive streams. In the
* latter case, this has to be a {@link CacheDataSource.Factory} for the {@link Cache} into
* which downloads will be written.
* @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which
* tracks can be selected.
* @return A {@link DownloadHelper}.
* @throws IllegalStateException If the corresponding module is missing for DASH, HLS or
* SmoothStreaming media items.
* @throws IllegalArgumentException If the {@code dataSourceFactory} is null for adaptive streams.
@ -243,7 +295,7 @@ public final class DownloadHelper {
Assertions.checkArgument(isProgressive || dataSourceFactory != null);
return new DownloadHelper(
mediaItem,
isProgressive
isProgressive && dataSourceFactory == null
? null
: createMediaSourceInternal(
mediaItem, castNonNull(dataSourceFactory), drmSessionManager),
@ -281,8 +333,11 @@ public final class DownloadHelper {
downloadRequest.toMediaItem(), dataSourceFactory, drmSessionManager);
}
private static final String TAG = "DownloadHelper";
private final MediaItem.LocalConfiguration localConfiguration;
@Nullable private final MediaSource mediaSource;
private final @Mode int mode;
private final DefaultTrackSelector trackSelector;
private final RendererCapabilitiesList rendererCapabilities;
private final SparseIntArray scratchSet;
@ -290,6 +345,7 @@ public final class DownloadHelper {
private final Timeline.Window window;
private boolean isPreparedWithMedia;
private boolean areTracksSelected;
private @MonotonicNonNull Callback callback;
private @MonotonicNonNull MediaPreparer mediaPreparer;
private TrackGroupArray @MonotonicNonNull [] trackGroupArrays;
@ -316,6 +372,12 @@ public final class DownloadHelper {
RendererCapabilitiesList rendererCapabilities) {
this.localConfiguration = checkNotNull(mediaItem.localConfiguration);
this.mediaSource = mediaSource;
this.mode =
(mediaSource == null)
? MODE_NOT_PREPARE
: (mediaSource instanceof ProgressiveMediaSource)
? MODE_PREPARE_PROGRESSIVE_SOURCE
: MODE_PREPARE_NON_PROGRESSIVE_SOURCE_AND_SELECT_TRACKS;
this.trackSelector =
new DefaultTrackSelector(trackSelectionParameters, new DownloadTrackSelection.Factory());
this.rendererCapabilities = rendererCapabilities;
@ -334,8 +396,8 @@ public final class DownloadHelper {
public void prepare(Callback callback) {
Assertions.checkState(this.callback == null);
this.callback = callback;
if (mediaSource != null) {
mediaPreparer = new MediaPreparer(mediaSource, /* downloadHelper= */ this);
if (mode != MODE_NOT_PREPARE) {
mediaPreparer = new MediaPreparer(checkNotNull(mediaSource), /* downloadHelper= */ this);
} else {
callbackHandler.post(() -> callback.onPrepared(this));
}
@ -374,7 +436,7 @@ public final class DownloadHelper {
return 0;
}
assertPreparedWithMedia();
return trackGroupArrays.length;
return mediaPreparer.mediaPeriods.length;
}
/**
@ -386,7 +448,7 @@ public final class DownloadHelper {
* content.
*/
public Tracks getTracks(int periodIndex) {
assertPreparedWithMedia();
assertPreparedWithNonProgressiveSourceAndTracksSelected();
return TrackSelectionUtil.buildTracks(
mappedTrackInfos[periodIndex], immutableTrackSelectionsByPeriodAndRenderer[periodIndex]);
}
@ -402,7 +464,7 @@ public final class DownloadHelper {
* content.
*/
public TrackGroupArray getTrackGroups(int periodIndex) {
assertPreparedWithMedia();
assertPreparedWithNonProgressiveSourceAndTracksSelected();
return trackGroupArrays[periodIndex];
}
@ -414,7 +476,7 @@ public final class DownloadHelper {
* @return The {@link MappedTrackInfo} for the period.
*/
public MappedTrackInfo getMappedTrackInfo(int periodIndex) {
assertPreparedWithMedia();
assertPreparedWithNonProgressiveSourceAndTracksSelected();
return mappedTrackInfos[periodIndex];
}
@ -427,7 +489,7 @@ public final class DownloadHelper {
* @return A list of selected {@link ExoTrackSelection track selections}.
*/
public List<ExoTrackSelection> getTrackSelections(int periodIndex, int rendererIndex) {
assertPreparedWithMedia();
assertPreparedWithNonProgressiveSourceAndTracksSelected();
return immutableTrackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex];
}
@ -438,7 +500,7 @@ public final class DownloadHelper {
* @param periodIndex The period index for which track selections are cleared.
*/
public void clearTrackSelections(int periodIndex) {
assertPreparedWithMedia();
assertPreparedWithNonProgressiveSourceAndTracksSelected();
for (int i = 0; i < rendererCapabilities.size(); i++) {
trackSelectionsByPeriodAndRenderer[periodIndex][i].clear();
}
@ -455,7 +517,7 @@ public final class DownloadHelper {
public void replaceTrackSelections(
int periodIndex, TrackSelectionParameters trackSelectionParameters) {
try {
assertPreparedWithMedia();
assertPreparedWithNonProgressiveSourceAndTracksSelected();
clearTrackSelections(periodIndex);
addTrackSelectionInternal(periodIndex, trackSelectionParameters);
} catch (ExoPlaybackException e) {
@ -474,7 +536,7 @@ public final class DownloadHelper {
public void addTrackSelection(
int periodIndex, TrackSelectionParameters trackSelectionParameters) {
try {
assertPreparedWithMedia();
assertPreparedWithNonProgressiveSourceAndTracksSelected();
addTrackSelectionInternal(periodIndex, trackSelectionParameters);
} catch (ExoPlaybackException e) {
throw new IllegalStateException(e);
@ -491,7 +553,7 @@ public final class DownloadHelper {
*/
public void addAudioLanguagesToSelection(String... languages) {
try {
assertPreparedWithMedia();
assertPreparedWithNonProgressiveSourceAndTracksSelected();
TrackSelectionParameters.Builder parametersBuilder =
DEFAULT_TRACK_SELECTOR_PARAMETERS.buildUpon();
@ -531,7 +593,7 @@ public final class DownloadHelper {
public void addTextLanguagesToSelection(
boolean selectUndeterminedTextLanguage, String... languages) {
try {
assertPreparedWithMedia();
assertPreparedWithNonProgressiveSourceAndTracksSelected();
TrackSelectionParameters.Builder parametersBuilder =
DEFAULT_TRACK_SELECTOR_PARAMETERS.buildUpon();
@ -576,7 +638,7 @@ public final class DownloadHelper {
DefaultTrackSelector.Parameters trackSelectorParameters,
List<SelectionOverride> overrides) {
try {
assertPreparedWithMedia();
assertPreparedWithNonProgressiveSourceAndTracksSelected();
DefaultTrackSelector.Parameters.Builder builder = trackSelectorParameters.buildUpon();
for (int i = 0; i < mappedTrackInfos[periodIndex].getRendererCount(); i++) {
builder.setRendererDisabled(/* rendererIndex= */ i, /* disabled= */ i != rendererIndex);
@ -601,21 +663,72 @@ public final class DownloadHelper {
* after preparation completes. The uri of the {@link DownloadRequest} will be used as content id.
*
* @param data Application provided data to store in {@link DownloadRequest#data}.
* @return The built {@link DownloadRequest}.
*/
public DownloadRequest getDownloadRequest(@Nullable byte[] data) {
return getDownloadRequest(localConfiguration.uri.toString(), data);
}
/**
* Builds a {@link DownloadRequest} for downloading the selected tracks and time range. Must not
* be called until preparation completes.
*
* <p>This method is only supported for progressive streams.
*
* @param data Application provided data to store in {@link DownloadRequest#data}.
* @param startPositionMs The start position (in milliseconds) of the media that download should
* cover from, or {@link C#TIME_UNSET} if the download should cover from the default start
* position.
* @param durationMs The end position (in milliseconds) of the media that download should cover
* 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) {
return getDownloadRequest(localConfiguration.uri.toString(), data, startPositionMs, durationMs);
}
/**
* Builds a {@link DownloadRequest} for downloading the selected tracks. Must not be called until
* after preparation completes.
*
* @param id The unique content id.
* @param data Application provided data to store in {@link DownloadRequest#data}.
* @return The built {@link DownloadRequest}.
*/
public DownloadRequest getDownloadRequest(String id, @Nullable byte[] data) {
return getDownloadRequestBuilder(id, data).build();
}
/**
* Builds a {@link DownloadRequest} for downloading the selected tracks and time range. Must not
* be called until preparation completes.
*
* <p>This method is only supported for progressive streams.
*
* @param id The unique content id.
* @param data Application provided data to store in {@link DownloadRequest#data}.
* @param startPositionMs The start position (in milliseconds) of the media that download should
* cover from, or {@link C#TIME_UNSET} if the download should cover from the default start
* position.
* @param durationMs The duration (in milliseconds) of the media that download should cover, or
* {@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();
populateDownloadRequestBuilderWithDownloadRange(builder, startPositionMs, durationMs);
return builder.build();
}
private DownloadRequest.Builder getDownloadRequestBuilder(String id, @Nullable byte[] data) {
DownloadRequest.Builder requestBuilder =
new DownloadRequest.Builder(id, localConfiguration.uri)
.setMimeType(localConfiguration.mimeType)
@ -625,10 +738,8 @@ public final class DownloadHelper {
: null)
.setCustomCacheKey(localConfiguration.customCacheKey)
.setData(data);
if (mediaSource == null) {
return requestBuilder.build();
}
assertPreparedWithMedia();
if (mode == MODE_PREPARE_NON_PROGRESSIVE_SOURCE_AND_SELECT_TRACKS) {
assertPreparedWithNonProgressiveSourceAndTracksSelected();
List<StreamKey> streamKeys = new ArrayList<>();
List<ExoTrackSelection> allSelections = new ArrayList<>();
int periodCount = trackSelectionsByPeriodAndRenderer.length;
@ -640,7 +751,62 @@ public final class DownloadHelper {
}
streamKeys.addAll(mediaPreparer.mediaPeriods[periodIndex].getStreamKeys(allSelections));
}
return requestBuilder.setStreamKeys(streamKeys).build();
requestBuilder.setStreamKeys(streamKeys);
}
return requestBuilder;
}
private void populateDownloadRequestBuilderWithDownloadRange(
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 =
timeline.getPeriodPositionUs(
window,
period,
/* windowIndex= */ 0,
/* windowPositionUs= */ Util.msToUs(startPositionMs))
.second;
long periodEndPositionUs = C.TIME_UNSET;
if (durationMs != C.TIME_UNSET) {
periodEndPositionUs = periodStartPositionUs + Util.msToUs(durationMs);
if (period.durationUs != C.TIME_UNSET) {
periodEndPositionUs = min(periodEndPositionUs, period.durationUs - 1);
}
}
// SeekMap should be available for prepared progressive media.
SeekMap seekMap = mediaPreparer.seekMap;
if (seekMap.isSeekable()) {
long byteRangeStartPositionOffset =
seekMap.getSeekPoints(periodStartPositionUs).first.position;
long byteRangeLength = C.LENGTH_UNSET;
if (periodEndPositionUs != C.TIME_UNSET) {
long byteRangeEndPositionOffset =
seekMap.getSeekPoints(periodEndPositionUs).second.position;
// When the start and end positions are after the last seek point, they will both have only
// that one mapped seek point. Then we should download from that seek point to the end of
// the media, otherwise nothing will be downloaded as the resolved length is 0.
boolean areStartAndEndPositionsAfterTheLastSeekPoint =
periodStartPositionUs != periodEndPositionUs
&& byteRangeStartPositionOffset == byteRangeEndPositionOffset;
byteRangeLength =
!areStartAndEndPositionsAfterTheLastSeekPoint
? byteRangeEndPositionOffset - byteRangeStartPositionOffset
: C.LENGTH_UNSET;
}
requestBuilder.setByteRange(byteRangeStartPositionOffset, byteRangeLength);
} else {
Log.w(TAG, "Cannot set download byte range for progressive stream that is unseekable");
}
}
@RequiresNonNull({
@ -670,6 +836,7 @@ public final class DownloadHelper {
checkNotNull(mediaPreparer);
checkNotNull(mediaPreparer.mediaPeriods);
checkNotNull(mediaPreparer.timeline);
if (mode == MODE_PREPARE_NON_PROGRESSIVE_SOURCE_AND_SELECT_TRACKS) {
int periodCount = mediaPreparer.mediaPeriods.length;
int rendererCount = rendererCapabilities.size();
trackSelectionsByPeriodAndRenderer =
@ -691,7 +858,12 @@ public final class DownloadHelper {
trackSelector.onSelectionActivated(trackSelectorResult.info);
mappedTrackInfos[i] = checkNotNull(trackSelector.getCurrentMappedTrackInfo());
}
setPreparedWithMedia();
setPreparedWithNonProgressiveSourceAndTracksSelected();
} else {
checkState(mode == MODE_PREPARE_PROGRESSIVE_SOURCE);
checkNotNull(mediaPreparer.seekMap);
setPreparedWithProgressiveSource();
}
checkNotNull(callbackHandler).post(() -> checkNotNull(callback).onPrepared(this));
}
@ -708,8 +880,26 @@ public final class DownloadHelper {
"mediaPreparer.timeline",
"mediaPreparer.mediaPeriods"
})
private void setPreparedWithMedia() {
private void setPreparedWithNonProgressiveSourceAndTracksSelected() {
isPreparedWithMedia = true;
areTracksSelected = true;
}
@RequiresNonNull({
"mediaPreparer",
"mediaPreparer.timeline",
"mediaPreparer.seekMap",
"mediaPreparer.mediaPeriods"
})
private void setPreparedWithProgressiveSource() {
isPreparedWithMedia = true;
}
@EnsuresNonNull({"mediaPreparer", "mediaPreparer.timeline", "mediaPreparer.mediaPeriods"})
@SuppressWarnings("nullness:contracts.postcondition")
private void assertPreparedWithMedia() {
Assertions.checkState(mode != MODE_NOT_PREPARE);
Assertions.checkState(isPreparedWithMedia);
}
@EnsuresNonNull({
@ -722,7 +912,21 @@ public final class DownloadHelper {
"mediaPreparer.mediaPeriods"
})
@SuppressWarnings("nullness:contracts.postcondition")
private void assertPreparedWithMedia() {
private void assertPreparedWithNonProgressiveSourceAndTracksSelected() {
Assertions.checkState(mode == MODE_PREPARE_NON_PROGRESSIVE_SOURCE_AND_SELECT_TRACKS);
Assertions.checkState(isPreparedWithMedia);
Assertions.checkState(areTracksSelected);
}
@EnsuresNonNull({
"mediaPreparer",
"mediaPreparer.timeline",
"mediaPreparer.seekMap",
"mediaPreparer.mediaPeriods"
})
@SuppressWarnings("nullness:contracts.postcondition")
private void assertPreparedWithProgressiveSource() {
Assertions.checkState(mode == MODE_PREPARE_PROGRESSIVE_SOURCE);
Assertions.checkState(isPreparedWithMedia);
}
@ -783,8 +987,10 @@ public final class DownloadHelper {
MediaItem mediaItem,
DataSource.Factory dataSourceFactory,
@Nullable DrmSessionManager drmSessionManager) {
DefaultMediaSourceFactory mediaSourceFactory =
new DefaultMediaSourceFactory(dataSourceFactory, ExtractorsFactory.EMPTY);
MediaSource.Factory mediaSourceFactory =
isProgressive(checkNotNull(mediaItem.localConfiguration))
? new ProgressiveMediaSource.Factory(dataSourceFactory)
: new DefaultMediaSourceFactory(dataSourceFactory, ExtractorsFactory.EMPTY);
if (drmSessionManager != null) {
mediaSourceFactory.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager);
}
@ -798,7 +1004,10 @@ public final class DownloadHelper {
}
private static final class MediaPreparer
implements MediaSourceCaller, MediaPeriod.Callback, Handler.Callback {
implements MediaSourceCaller,
ProgressiveMediaSource.Listener,
MediaPeriod.Callback,
Handler.Callback {
private static final int MESSAGE_PREPARE_SOURCE = 1;
private static final int MESSAGE_CHECK_FOR_FAILURE = 2;
@ -817,6 +1026,7 @@ public final class DownloadHelper {
private final Handler mediaSourceHandler;
public @MonotonicNonNull Timeline timeline;
public @MonotonicNonNull SeekMap seekMap;
public MediaPeriod @MonotonicNonNull [] mediaPeriods;
private boolean released;
@ -850,6 +1060,9 @@ public final class DownloadHelper {
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_PREPARE_SOURCE:
if (mediaSource instanceof ProgressiveMediaSource) {
((ProgressiveMediaSource) mediaSource).setListener(this);
}
mediaSource.prepareSource(
/* caller= */ this, /* mediaTransferListener= */ null, PlayerId.UNSET);
mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE);
@ -883,6 +1096,9 @@ public final class DownloadHelper {
mediaSource.releasePeriod(period);
}
}
if (mediaSource instanceof ProgressiveMediaSource) {
((ProgressiveMediaSource) mediaSource).clearListener();
}
mediaSource.releaseSource(this);
mediaSourceHandler.removeCallbacksAndMessages(null);
mediaSourceThread.quit();
@ -924,6 +1140,13 @@ public final class DownloadHelper {
}
}
// ProgressiveMediaSource.Listener implementation.
@Override
public void onSeekMap(MediaSource source, SeekMap seekMap) {
this.seekMap = seekMap;
}
// MediaPeriod.Callback implementation.
@Override

View File

@ -18,8 +18,10 @@ 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;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
@ -447,6 +449,121 @@ public class DownloadHelperTest {
new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 2, /* streamIndex= */ 0));
}
@Test
public void
getDownloadRequest_createsDownloadRequestWithConcreteTimeRange_requestContainsConcreteByteRange()
throws Exception {
Context context = getApplicationContext();
DownloadHelper downloadHelper =
DownloadHelper.forMediaItem(
context,
MediaItem.fromUri("asset:///media/mp4/long_1080p_lowbitrate.mp4"),
new DefaultDataSource.Factory(context));
prepareDownloadHelper(downloadHelper);
DownloadRequest downloadRequest =
downloadHelper.getDownloadRequest(
/* data= */ null, /* startPositionMs= */ 0, /* durationMs= */ 30000);
assertThat(downloadRequest.byteRange).isNotNull();
assertThat(downloadRequest.byteRange.offset).isAtLeast(0);
assertThat(downloadRequest.byteRange.length).isGreaterThan(0);
}
@Test
public void
getDownloadRequest_createsDownloadRequestWithUnsetStartPosition_requestContainsConcreteByteRange()
throws Exception {
Context context = getApplicationContext();
DownloadHelper downloadHelper =
DownloadHelper.forMediaItem(
context,
MediaItem.fromUri("asset:///media/mp4/long_1080p_lowbitrate.mp4"),
new DefaultDataSource.Factory(context));
prepareDownloadHelper(downloadHelper);
DownloadRequest downloadRequest =
downloadHelper.getDownloadRequest(
/* data= */ null, /* startPositionMs= */ C.TIME_UNSET, /* durationMs= */ 30000);
assertThat(downloadRequest.byteRange).isNotNull();
assertThat(downloadRequest.byteRange.offset).isAtLeast(0);
assertThat(downloadRequest.byteRange.length).isGreaterThan(0);
}
@Test
public void getDownloadRequest_createsDownloadRequestWithUnsetLength_requestContainsUnsetLength()
throws Exception {
Context context = getApplicationContext();
DownloadHelper downloadHelper =
DownloadHelper.forMediaItem(
context,
MediaItem.fromUri("asset:///media/mp4/long_1080p_lowbitrate.mp4"),
new DefaultDataSource.Factory(context));
prepareDownloadHelper(downloadHelper);
DownloadRequest downloadRequest =
downloadHelper.getDownloadRequest(
/* data= */ null, /* startPositionMs= */ 30000, /* durationMs= */ C.TIME_UNSET);
assertThat(downloadRequest.byteRange).isNotNull();
assertThat(downloadRequest.byteRange.offset).isAtLeast(0);
assertThat(downloadRequest.byteRange.length).isEqualTo(C.LENGTH_UNSET);
}
@Test
public void
getDownloadRequest_createsDownloadRequestForTooShortStreamWithTimeRange_requestContainsUnsetLength()
throws Exception {
Context context = getApplicationContext();
DownloadHelper downloadHelper =
DownloadHelper.forMediaItem(
context,
MediaItem.fromUri("asset:///media/mp4/sample.mp4"),
new DefaultDataSource.Factory(context));
prepareDownloadHelper(downloadHelper);
DownloadRequest downloadRequest =
downloadHelper.getDownloadRequest(
/* data= */ null, /* startPositionMs= */ 0, /* durationMs= */ 30000);
assertThat(downloadRequest.byteRange).isNotNull();
assertThat(downloadRequest.byteRange.offset).isAtLeast(0);
assertThat(downloadRequest.byteRange.length).isEqualTo(C.LENGTH_UNSET);
}
@Test
public void
getDownloadRequest_createsDownloadRequestWithoutTimeRange_requestContainsNullByteRange()
throws Exception {
Context context = getApplicationContext();
DownloadHelper downloadHelper =
DownloadHelper.forMediaItem(
getApplicationContext(),
MediaItem.fromUri("asset:///media/mp4/sample.mp4"),
new DefaultDataSource.Factory(context));
prepareDownloadHelper(downloadHelper);
DownloadRequest downloadRequest = downloadHelper.getDownloadRequest(/* data= */ null);
assertThat(downloadRequest.byteRange).isNull();
}
@Test
public void
getDownloadRequest_createDownloadRequestWithTimeRangeForNonProgressiveStream_throwsIllegalStateException()
throws Exception {
// We use this.downloadHelper as it was created with a TestMediaSource, thus the DownloadHelper
// will treat it as non-progressive.
prepareDownloadHelper(downloadHelper);
assertThrows(
IllegalStateException.class,
() ->
downloadHelper.getDownloadRequest(
/* data= */ null, /* startPositionMs= */ 0, /* durationMs= */ 10000));
}
// https://github.com/androidx/media/issues/1224
@Test
public void prepareThenRelease_renderersReleased() throws Exception {
@ -460,10 +577,10 @@ public class DownloadHelperTest {
new Renderer[] {textRenderer, audioRenderer, videoRenderer};
DownloadHelper downloadHelper =
DownloadHelper.forMediaItem(
testMediaItem,
MediaItem.fromUri("asset:///media/mp4/sample.mp4"),
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
renderersFactory,
new FakeDataSource.Factory());
new DefaultDataSource.Factory(getApplicationContext()));
prepareDownloadHelper(downloadHelper);
downloadHelper.release();