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; package androidx.media3.exoplayer.offline;
import static androidx.media3.common.util.Assertions.checkNotNull; 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 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.content.Context;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.Message; import android.os.Message;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
@ -33,11 +38,14 @@ import androidx.media3.common.TrackSelectionOverride;
import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks; import androidx.media3.common.Tracks;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.NullableType; import androidx.media3.common.util.NullableType;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.TransferListener; 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.DefaultRendererCapabilitiesList;
import androidx.media3.exoplayer.ExoPlaybackException; import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.exoplayer.LoadingInfo; 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;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
import androidx.media3.exoplayer.source.MediaSource.MediaSourceCaller; import androidx.media3.exoplayer.source.MediaSource.MediaSourceCaller;
import androidx.media3.exoplayer.source.ProgressiveMediaSource;
import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.source.TrackGroupArray;
import androidx.media3.exoplayer.source.chunk.MediaChunk; import androidx.media3.exoplayer.source.chunk.MediaChunk;
import androidx.media3.exoplayer.source.chunk.MediaChunkIterator; 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.BandwidthMeter;
import androidx.media3.exoplayer.upstream.DefaultAllocator; import androidx.media3.exoplayer.upstream.DefaultAllocator;
import androidx.media3.extractor.ExtractorsFactory; import androidx.media3.extractor.ExtractorsFactory;
import androidx.media3.extractor.SeekMap;
import java.io.IOException; 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.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -118,6 +131,20 @@ public final class DownloadHelper {
return DEFAULT_TRACK_SELECTOR_PARAMETERS; 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. */ /** A callback to be notified when the {@link DownloadHelper} is prepared. */
public interface Callback { public interface Callback {
@ -158,6 +185,28 @@ public final class DownloadHelper {
/* drmSessionManager= */ null); /* 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. * 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 * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are
* selected. * selected.
* @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest for adaptive * @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 or the {@link SeekMap} for progressive streams. This argument is required for
* streams. * adaptive streams or when requesting partial downloads for progressive streams. In the
* @return A {@link DownloadHelper}. * 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 * @throws IllegalStateException If the corresponding module is missing for DASH, HLS or
* SmoothStreaming media items. * SmoothStreaming media items.
* @throws IllegalArgumentException If the {@code dataSourceFactory} is null for adaptive streams. * @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 * @param trackSelectionParameters {@link TrackSelectionParameters} for selecting tracks for
* downloading. * downloading.
* @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest for adaptive * @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 or the {@link SeekMap} for progressive streams. This argument is required for
* streams. * adaptive streams or when requesting partial downloads for progressive streams. In the
* @return A {@link DownloadHelper}. * 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 * @throws IllegalStateException If the corresponding module is missing for DASH, HLS or
* SmoothStreaming media items. * SmoothStreaming media items.
* @throws IllegalArgumentException If the {@code dataSourceFactory} is null for adaptive streams. * @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 * @param trackSelectionParameters {@link TrackSelectionParameters} for selecting tracks for
* downloading. * downloading.
* @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest for adaptive * @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 or the {@link SeekMap} for progressive streams. This argument is required for
* streams. * 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 * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which
* tracks can be selected. * tracks can be selected.
* @return A {@link DownloadHelper}.
* @throws IllegalStateException If the corresponding module is missing for DASH, HLS or * @throws IllegalStateException If the corresponding module is missing for DASH, HLS or
* SmoothStreaming media items. * SmoothStreaming media items.
* @throws IllegalArgumentException If the {@code dataSourceFactory} is null for adaptive streams. * @throws IllegalArgumentException If the {@code dataSourceFactory} is null for adaptive streams.
@ -243,7 +295,7 @@ public final class DownloadHelper {
Assertions.checkArgument(isProgressive || dataSourceFactory != null); Assertions.checkArgument(isProgressive || dataSourceFactory != null);
return new DownloadHelper( return new DownloadHelper(
mediaItem, mediaItem,
isProgressive isProgressive && dataSourceFactory == null
? null ? null
: createMediaSourceInternal( : createMediaSourceInternal(
mediaItem, castNonNull(dataSourceFactory), drmSessionManager), mediaItem, castNonNull(dataSourceFactory), drmSessionManager),
@ -281,8 +333,11 @@ public final class DownloadHelper {
downloadRequest.toMediaItem(), dataSourceFactory, drmSessionManager); downloadRequest.toMediaItem(), dataSourceFactory, drmSessionManager);
} }
private static final String TAG = "DownloadHelper";
private final MediaItem.LocalConfiguration localConfiguration; private final MediaItem.LocalConfiguration localConfiguration;
@Nullable private final MediaSource mediaSource; @Nullable private final MediaSource mediaSource;
private final @Mode int mode;
private final DefaultTrackSelector trackSelector; private final DefaultTrackSelector trackSelector;
private final RendererCapabilitiesList rendererCapabilities; private final RendererCapabilitiesList rendererCapabilities;
private final SparseIntArray scratchSet; private final SparseIntArray scratchSet;
@ -290,6 +345,7 @@ public final class DownloadHelper {
private final Timeline.Window window; private final Timeline.Window window;
private boolean isPreparedWithMedia; private boolean isPreparedWithMedia;
private boolean areTracksSelected;
private @MonotonicNonNull Callback callback; private @MonotonicNonNull Callback callback;
private @MonotonicNonNull MediaPreparer mediaPreparer; private @MonotonicNonNull MediaPreparer mediaPreparer;
private TrackGroupArray @MonotonicNonNull [] trackGroupArrays; private TrackGroupArray @MonotonicNonNull [] trackGroupArrays;
@ -316,6 +372,12 @@ public final class DownloadHelper {
RendererCapabilitiesList rendererCapabilities) { RendererCapabilitiesList rendererCapabilities) {
this.localConfiguration = checkNotNull(mediaItem.localConfiguration); this.localConfiguration = checkNotNull(mediaItem.localConfiguration);
this.mediaSource = mediaSource; 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 = this.trackSelector =
new DefaultTrackSelector(trackSelectionParameters, new DownloadTrackSelection.Factory()); new DefaultTrackSelector(trackSelectionParameters, new DownloadTrackSelection.Factory());
this.rendererCapabilities = rendererCapabilities; this.rendererCapabilities = rendererCapabilities;
@ -334,8 +396,8 @@ public final class DownloadHelper {
public void prepare(Callback callback) { public void prepare(Callback callback) {
Assertions.checkState(this.callback == null); Assertions.checkState(this.callback == null);
this.callback = callback; this.callback = callback;
if (mediaSource != null) { if (mode != MODE_NOT_PREPARE) {
mediaPreparer = new MediaPreparer(mediaSource, /* downloadHelper= */ this); mediaPreparer = new MediaPreparer(checkNotNull(mediaSource), /* downloadHelper= */ this);
} else { } else {
callbackHandler.post(() -> callback.onPrepared(this)); callbackHandler.post(() -> callback.onPrepared(this));
} }
@ -374,7 +436,7 @@ public final class DownloadHelper {
return 0; return 0;
} }
assertPreparedWithMedia(); assertPreparedWithMedia();
return trackGroupArrays.length; return mediaPreparer.mediaPeriods.length;
} }
/** /**
@ -386,7 +448,7 @@ public final class DownloadHelper {
* content. * content.
*/ */
public Tracks getTracks(int periodIndex) { public Tracks getTracks(int periodIndex) {
assertPreparedWithMedia(); assertPreparedWithNonProgressiveSourceAndTracksSelected();
return TrackSelectionUtil.buildTracks( return TrackSelectionUtil.buildTracks(
mappedTrackInfos[periodIndex], immutableTrackSelectionsByPeriodAndRenderer[periodIndex]); mappedTrackInfos[periodIndex], immutableTrackSelectionsByPeriodAndRenderer[periodIndex]);
} }
@ -402,7 +464,7 @@ public final class DownloadHelper {
* content. * content.
*/ */
public TrackGroupArray getTrackGroups(int periodIndex) { public TrackGroupArray getTrackGroups(int periodIndex) {
assertPreparedWithMedia(); assertPreparedWithNonProgressiveSourceAndTracksSelected();
return trackGroupArrays[periodIndex]; return trackGroupArrays[periodIndex];
} }
@ -414,7 +476,7 @@ public final class DownloadHelper {
* @return The {@link MappedTrackInfo} for the period. * @return The {@link MappedTrackInfo} for the period.
*/ */
public MappedTrackInfo getMappedTrackInfo(int periodIndex) { public MappedTrackInfo getMappedTrackInfo(int periodIndex) {
assertPreparedWithMedia(); assertPreparedWithNonProgressiveSourceAndTracksSelected();
return mappedTrackInfos[periodIndex]; return mappedTrackInfos[periodIndex];
} }
@ -427,7 +489,7 @@ public final class DownloadHelper {
* @return A list of selected {@link ExoTrackSelection track selections}. * @return A list of selected {@link ExoTrackSelection track selections}.
*/ */
public List<ExoTrackSelection> getTrackSelections(int periodIndex, int rendererIndex) { public List<ExoTrackSelection> getTrackSelections(int periodIndex, int rendererIndex) {
assertPreparedWithMedia(); assertPreparedWithNonProgressiveSourceAndTracksSelected();
return immutableTrackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex]; return immutableTrackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex];
} }
@ -438,7 +500,7 @@ public final class DownloadHelper {
* @param periodIndex The period index for which track selections are cleared. * @param periodIndex The period index for which track selections are cleared.
*/ */
public void clearTrackSelections(int periodIndex) { public void clearTrackSelections(int periodIndex) {
assertPreparedWithMedia(); assertPreparedWithNonProgressiveSourceAndTracksSelected();
for (int i = 0; i < rendererCapabilities.size(); i++) { for (int i = 0; i < rendererCapabilities.size(); i++) {
trackSelectionsByPeriodAndRenderer[periodIndex][i].clear(); trackSelectionsByPeriodAndRenderer[periodIndex][i].clear();
} }
@ -455,7 +517,7 @@ public final class DownloadHelper {
public void replaceTrackSelections( public void replaceTrackSelections(
int periodIndex, TrackSelectionParameters trackSelectionParameters) { int periodIndex, TrackSelectionParameters trackSelectionParameters) {
try { try {
assertPreparedWithMedia(); assertPreparedWithNonProgressiveSourceAndTracksSelected();
clearTrackSelections(periodIndex); clearTrackSelections(periodIndex);
addTrackSelectionInternal(periodIndex, trackSelectionParameters); addTrackSelectionInternal(periodIndex, trackSelectionParameters);
} catch (ExoPlaybackException e) { } catch (ExoPlaybackException e) {
@ -474,7 +536,7 @@ public final class DownloadHelper {
public void addTrackSelection( public void addTrackSelection(
int periodIndex, TrackSelectionParameters trackSelectionParameters) { int periodIndex, TrackSelectionParameters trackSelectionParameters) {
try { try {
assertPreparedWithMedia(); assertPreparedWithNonProgressiveSourceAndTracksSelected();
addTrackSelectionInternal(periodIndex, trackSelectionParameters); addTrackSelectionInternal(periodIndex, trackSelectionParameters);
} catch (ExoPlaybackException e) { } catch (ExoPlaybackException e) {
throw new IllegalStateException(e); throw new IllegalStateException(e);
@ -491,7 +553,7 @@ public final class DownloadHelper {
*/ */
public void addAudioLanguagesToSelection(String... languages) { public void addAudioLanguagesToSelection(String... languages) {
try { try {
assertPreparedWithMedia(); assertPreparedWithNonProgressiveSourceAndTracksSelected();
TrackSelectionParameters.Builder parametersBuilder = TrackSelectionParameters.Builder parametersBuilder =
DEFAULT_TRACK_SELECTOR_PARAMETERS.buildUpon(); DEFAULT_TRACK_SELECTOR_PARAMETERS.buildUpon();
@ -531,7 +593,7 @@ public final class DownloadHelper {
public void addTextLanguagesToSelection( public void addTextLanguagesToSelection(
boolean selectUndeterminedTextLanguage, String... languages) { boolean selectUndeterminedTextLanguage, String... languages) {
try { try {
assertPreparedWithMedia(); assertPreparedWithNonProgressiveSourceAndTracksSelected();
TrackSelectionParameters.Builder parametersBuilder = TrackSelectionParameters.Builder parametersBuilder =
DEFAULT_TRACK_SELECTOR_PARAMETERS.buildUpon(); DEFAULT_TRACK_SELECTOR_PARAMETERS.buildUpon();
@ -576,7 +638,7 @@ public final class DownloadHelper {
DefaultTrackSelector.Parameters trackSelectorParameters, DefaultTrackSelector.Parameters trackSelectorParameters,
List<SelectionOverride> overrides) { List<SelectionOverride> overrides) {
try { try {
assertPreparedWithMedia(); assertPreparedWithNonProgressiveSourceAndTracksSelected();
DefaultTrackSelector.Parameters.Builder builder = trackSelectorParameters.buildUpon(); DefaultTrackSelector.Parameters.Builder builder = trackSelectorParameters.buildUpon();
for (int i = 0; i < mappedTrackInfos[periodIndex].getRendererCount(); i++) { for (int i = 0; i < mappedTrackInfos[periodIndex].getRendererCount(); i++) {
builder.setRendererDisabled(/* rendererIndex= */ i, /* disabled= */ i != rendererIndex); 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. * 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}. * @param data Application provided data to store in {@link DownloadRequest#data}.
* @return The built {@link DownloadRequest}.
*/ */
public DownloadRequest getDownloadRequest(@Nullable byte[] data) { public DownloadRequest getDownloadRequest(@Nullable byte[] data) {
return getDownloadRequest(localConfiguration.uri.toString(), 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 * Builds a {@link DownloadRequest} for downloading the selected tracks. Must not be called until
* after preparation completes. * after preparation completes.
* *
* @param id The unique content id. * @param id The unique content id.
* @param data Application provided data to store in {@link DownloadRequest#data}. * @param data Application provided data to store in {@link DownloadRequest#data}.
* @return The built {@link DownloadRequest}.
*/ */
public DownloadRequest getDownloadRequest(String id, @Nullable byte[] data) { 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 = DownloadRequest.Builder requestBuilder =
new DownloadRequest.Builder(id, localConfiguration.uri) new DownloadRequest.Builder(id, localConfiguration.uri)
.setMimeType(localConfiguration.mimeType) .setMimeType(localConfiguration.mimeType)
@ -625,10 +738,8 @@ public final class DownloadHelper {
: null) : null)
.setCustomCacheKey(localConfiguration.customCacheKey) .setCustomCacheKey(localConfiguration.customCacheKey)
.setData(data); .setData(data);
if (mediaSource == null) { if (mode == MODE_PREPARE_NON_PROGRESSIVE_SOURCE_AND_SELECT_TRACKS) {
return requestBuilder.build(); assertPreparedWithNonProgressiveSourceAndTracksSelected();
}
assertPreparedWithMedia();
List<StreamKey> streamKeys = new ArrayList<>(); List<StreamKey> streamKeys = new ArrayList<>();
List<ExoTrackSelection> allSelections = new ArrayList<>(); List<ExoTrackSelection> allSelections = new ArrayList<>();
int periodCount = trackSelectionsByPeriodAndRenderer.length; int periodCount = trackSelectionsByPeriodAndRenderer.length;
@ -640,7 +751,62 @@ public final class DownloadHelper {
} }
streamKeys.addAll(mediaPreparer.mediaPeriods[periodIndex].getStreamKeys(allSelections)); 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({ @RequiresNonNull({
@ -670,6 +836,7 @@ public final class DownloadHelper {
checkNotNull(mediaPreparer); checkNotNull(mediaPreparer);
checkNotNull(mediaPreparer.mediaPeriods); checkNotNull(mediaPreparer.mediaPeriods);
checkNotNull(mediaPreparer.timeline); checkNotNull(mediaPreparer.timeline);
if (mode == MODE_PREPARE_NON_PROGRESSIVE_SOURCE_AND_SELECT_TRACKS) {
int periodCount = mediaPreparer.mediaPeriods.length; int periodCount = mediaPreparer.mediaPeriods.length;
int rendererCount = rendererCapabilities.size(); int rendererCount = rendererCapabilities.size();
trackSelectionsByPeriodAndRenderer = trackSelectionsByPeriodAndRenderer =
@ -691,7 +858,12 @@ public final class DownloadHelper {
trackSelector.onSelectionActivated(trackSelectorResult.info); trackSelector.onSelectionActivated(trackSelectorResult.info);
mappedTrackInfos[i] = checkNotNull(trackSelector.getCurrentMappedTrackInfo()); 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)); checkNotNull(callbackHandler).post(() -> checkNotNull(callback).onPrepared(this));
} }
@ -708,8 +880,26 @@ public final class DownloadHelper {
"mediaPreparer.timeline", "mediaPreparer.timeline",
"mediaPreparer.mediaPeriods" "mediaPreparer.mediaPeriods"
}) })
private void setPreparedWithMedia() { private void setPreparedWithNonProgressiveSourceAndTracksSelected() {
isPreparedWithMedia = true; 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({ @EnsuresNonNull({
@ -722,7 +912,21 @@ public final class DownloadHelper {
"mediaPreparer.mediaPeriods" "mediaPreparer.mediaPeriods"
}) })
@SuppressWarnings("nullness:contracts.postcondition") @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); Assertions.checkState(isPreparedWithMedia);
} }
@ -783,8 +987,10 @@ public final class DownloadHelper {
MediaItem mediaItem, MediaItem mediaItem,
DataSource.Factory dataSourceFactory, DataSource.Factory dataSourceFactory,
@Nullable DrmSessionManager drmSessionManager) { @Nullable DrmSessionManager drmSessionManager) {
DefaultMediaSourceFactory mediaSourceFactory = MediaSource.Factory mediaSourceFactory =
new DefaultMediaSourceFactory(dataSourceFactory, ExtractorsFactory.EMPTY); isProgressive(checkNotNull(mediaItem.localConfiguration))
? new ProgressiveMediaSource.Factory(dataSourceFactory)
: new DefaultMediaSourceFactory(dataSourceFactory, ExtractorsFactory.EMPTY);
if (drmSessionManager != null) { if (drmSessionManager != null) {
mediaSourceFactory.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager); mediaSourceFactory.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager);
} }
@ -798,7 +1004,10 @@ public final class DownloadHelper {
} }
private static final class MediaPreparer 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_PREPARE_SOURCE = 1;
private static final int MESSAGE_CHECK_FOR_FAILURE = 2; private static final int MESSAGE_CHECK_FOR_FAILURE = 2;
@ -817,6 +1026,7 @@ public final class DownloadHelper {
private final Handler mediaSourceHandler; private final Handler mediaSourceHandler;
public @MonotonicNonNull Timeline timeline; public @MonotonicNonNull Timeline timeline;
public @MonotonicNonNull SeekMap seekMap;
public MediaPeriod @MonotonicNonNull [] mediaPeriods; public MediaPeriod @MonotonicNonNull [] mediaPeriods;
private boolean released; private boolean released;
@ -850,6 +1060,9 @@ public final class DownloadHelper {
public boolean handleMessage(Message msg) { public boolean handleMessage(Message msg) {
switch (msg.what) { switch (msg.what) {
case MESSAGE_PREPARE_SOURCE: case MESSAGE_PREPARE_SOURCE:
if (mediaSource instanceof ProgressiveMediaSource) {
((ProgressiveMediaSource) mediaSource).setListener(this);
}
mediaSource.prepareSource( mediaSource.prepareSource(
/* caller= */ this, /* mediaTransferListener= */ null, PlayerId.UNSET); /* caller= */ this, /* mediaTransferListener= */ null, PlayerId.UNSET);
mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE); mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE);
@ -883,6 +1096,9 @@ public final class DownloadHelper {
mediaSource.releasePeriod(period); mediaSource.releasePeriod(period);
} }
} }
if (mediaSource instanceof ProgressiveMediaSource) {
((ProgressiveMediaSource) mediaSource).clearListener();
}
mediaSource.releaseSource(this); mediaSource.releaseSource(this);
mediaSourceHandler.removeCallbacksAndMessages(null); mediaSourceHandler.removeCallbacksAndMessages(null);
mediaSourceThread.quit(); 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. // MediaPeriod.Callback implementation.
@Override @Override

View File

@ -18,8 +18,10 @@ package androidx.media3.exoplayer.offline;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.junit.Assert.assertThrows;
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
import android.content.Context;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
@ -447,6 +449,121 @@ public class DownloadHelperTest {
new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 2, /* streamIndex= */ 0)); 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 // https://github.com/androidx/media/issues/1224
@Test @Test
public void prepareThenRelease_renderersReleased() throws Exception { public void prepareThenRelease_renderersReleased() throws Exception {
@ -460,10 +577,10 @@ public class DownloadHelperTest {
new Renderer[] {textRenderer, audioRenderer, videoRenderer}; new Renderer[] {textRenderer, audioRenderer, videoRenderer};
DownloadHelper downloadHelper = DownloadHelper downloadHelper =
DownloadHelper.forMediaItem( DownloadHelper.forMediaItem(
testMediaItem, MediaItem.fromUri("asset:///media/mp4/sample.mp4"),
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
renderersFactory, renderersFactory,
new FakeDataSource.Factory()); new DefaultDataSource.Factory(getApplicationContext()));
prepareDownloadHelper(downloadHelper); prepareDownloadHelper(downloadHelper);
downloadHelper.release(); downloadHelper.release();