diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java index 559bbcef0f..b0e56b9f92 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -210,7 +210,7 @@ public class DownloadTracker implements DownloadManager.Listener { DownloadService.startWithAction(context, DemoDownloadService.class, action, false); } - private DownloadHelper getDownloadHelper( + private DownloadHelper getDownloadHelper( Uri uri, String extension, RenderersFactory renderersFactory) { int type = Util.inferContentType(uri, extension); switch (type) { @@ -231,10 +231,11 @@ public class DownloadTracker implements DownloadManager.Listener { private final class StartDownloadDialogHelper implements DownloadHelper.Callback, DialogInterface.OnClickListener, + DialogInterface.OnDismissListener, View.OnClickListener, TrackSelectionView.DialogCallback { - private final DownloadHelper downloadHelper; + private final DownloadHelper downloadHelper; private final String name; private final LayoutInflater dialogInflater; private final AlertDialog dialog; @@ -244,20 +245,21 @@ public class DownloadTracker implements DownloadManager.Listener { private DefaultTrackSelector.Parameters parameters; private StartDownloadDialogHelper( - Activity activity, DownloadHelper downloadHelper, String name) { + Activity activity, DownloadHelper downloadHelper, String name) { this.downloadHelper = downloadHelper; this.name = name; AlertDialog.Builder builder = new AlertDialog.Builder(activity) .setTitle(R.string.download_preparing) - .setPositiveButton(android.R.string.ok, this) - .setNegativeButton(android.R.string.cancel, null); + .setPositiveButton(android.R.string.ok, /* listener= */ this) + .setNegativeButton(android.R.string.cancel, /* listener= */ null); // Inflate with the builder's context to ensure the correct style is used. dialogInflater = LayoutInflater.from(builder.getContext()); selectionList = (LinearLayout) dialogInflater.inflate(R.layout.start_download_dialog, null); builder.setView(selectionList); dialog = builder.create(); + dialog.setOnDismissListener(/* listener= */ this); dialog.show(); dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); @@ -268,19 +270,17 @@ public class DownloadTracker implements DownloadManager.Listener { // DownloadHelper.Callback implementation. @Override - public void onPrepared(DownloadHelper helper) { - if (helper.getPeriodCount() < 1) { - onPrepareError(downloadHelper, new IOException("Content is empty.")); - return; + public void onPrepared(DownloadHelper helper) { + if (helper.getPeriodCount() > 0) { + mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); + updateSelectionList(); } - mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); - updateSelectionList(); dialog.setTitle(R.string.exo_download_description); dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true); } @Override - public void onPrepareError(DownloadHelper helper, IOException e) { + public void onPrepareError(DownloadHelper helper, IOException e) { Toast.makeText( context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG) .show(); @@ -326,6 +326,13 @@ public class DownloadTracker implements DownloadManager.Listener { startDownload(downloadAction); } + // DialogInterface.OnDismissListener implementation. + + @Override + public void onDismiss(DialogInterface dialog) { + downloadHelper.release(); + } + // Internal methods. private void updateSelectionList() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index e799aff4b2..0cd8081708 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -17,7 +17,9 @@ package com.google.android.exoplayer2.offline; import android.net.Uri; import android.os.Handler; +import android.os.HandlerThread; import android.os.Looper; +import android.os.Message; import android.support.annotation.Nullable; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; @@ -27,6 +29,8 @@ import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -36,7 +40,9 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Paramet import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; +import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -58,19 +64,19 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; *

A typical usage of DownloadHelper follows these steps: * *

    - *
  1. Construct the download helper with information about the {@link RenderersFactory renderers} - * and {@link DefaultTrackSelector.Parameters parameters} for track selection. + *
  2. Construct the download helper with the {@link MediaSource}, information about the {@link + * RenderersFactory renderers} and {@link DefaultTrackSelector.Parameters parameters} for + * track selection. *
  3. Prepare the helper using {@link #prepare(Callback)} and wait for the callback. *
  4. Optional: Inspect the selected tracks using {@link #getMappedTrackInfo(int)} and {@link * #getTrackSelections(int, int)}, and make adjustments using {@link * #clearTrackSelections(int)}, {@link #replaceTrackSelections(int, Parameters)} and {@link * #addTrackSelection(int, Parameters)}. - *
  5. Create download actions for the selected track using {@link #getDownloadAction(byte[])}. + *
  6. Create a download action for the selected track using {@link #getDownloadAction(byte[])}. + *
  7. Release the helper using {@link #release()}. *
- * - * @param The manifest type. */ -public abstract class DownloadHelper { +public abstract class DownloadHelper { /** * The default parameters used for track selection for downloading. This default selects the @@ -87,7 +93,7 @@ public abstract class DownloadHelper { * * @param helper The reporting {@link DownloadHelper}. */ - void onPrepared(DownloadHelper helper); + void onPrepared(DownloadHelper helper); /** * Called when preparation fails. @@ -95,18 +101,21 @@ public abstract class DownloadHelper { * @param helper The reporting {@link DownloadHelper}. * @param e The error. */ - void onPrepareError(DownloadHelper helper, IOException e); + void onPrepareError(DownloadHelper helper, IOException e); } private final String downloadType; private final Uri uri; @Nullable private final String cacheKey; + @Nullable private final MediaSource mediaSource; private final DefaultTrackSelector trackSelector; private final RendererCapabilities[] rendererCapabilities; private final SparseIntArray scratchSet; - private int currentTrackSelectionPeriodIndex; - @Nullable private T manifest; + private boolean isPreparedWithMedia; + private @MonotonicNonNull Callback callback; + private @MonotonicNonNull Handler callbackHandler; + private @MonotonicNonNull MediaPreparer mediaPreparer; private TrackGroupArray @MonotonicNonNull [] trackGroupArrays; private MappedTrackInfo @MonotonicNonNull [] mappedTrackInfos; private List @MonotonicNonNull [][] trackSelectionsByPeriodAndRenderer; @@ -118,10 +127,12 @@ public abstract class DownloadHelper { * @param downloadType A download type. This value will be used as {@link DownloadAction#type}. * @param uri A {@link Uri}. * @param cacheKey An optional cache key. + * @param mediaSource A {@link MediaSource} for which tracks are selected, or null if no track + * selection needs to be made. * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for * downloading. * @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks - * are selected. + * are selected, or null if no track selection needs to be made. * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by * {@code renderersFactory}. */ @@ -129,14 +140,19 @@ public abstract class DownloadHelper { String downloadType, Uri uri, @Nullable String cacheKey, + @Nullable MediaSource mediaSource, DefaultTrackSelector.Parameters trackSelectorParameters, - RenderersFactory renderersFactory, + @Nullable RenderersFactory renderersFactory, @Nullable DrmSessionManager drmSessionManager) { this.downloadType = downloadType; this.uri = uri; this.cacheKey = cacheKey; + this.mediaSource = mediaSource; this.trackSelector = new DefaultTrackSelector(new DownloadTrackSelection.Factory()); - this.rendererCapabilities = Util.getRendererCapabilities(renderersFactory, drmSessionManager); + this.rendererCapabilities = + renderersFactory == null + ? new RendererCapabilities[0] + : Util.getRendererCapabilities(renderersFactory, drmSessionManager); this.scratchSet = new SparseIntArray(); trackSelector.setParameters(trackSelectorParameters); trackSelector.init(/* listener= */ () -> {}, new DummyBandwidthMeter()); @@ -148,35 +164,38 @@ public abstract class DownloadHelper { * @param callback A callback to be notified when preparation completes or fails. The callback * will be invoked on the calling thread unless that thread does not have an associated {@link * Looper}, in which case it will be called on the application's main thread. + * @throws IllegalStateException If the download helper has already been prepared. */ public final void prepare(Callback callback) { - Handler handler = + Assertions.checkState(this.callback == null); + this.callback = callback; + callbackHandler = new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper()); - new Thread( - () -> { - try { - manifest = loadManifest(uri); - trackGroupArrays = getTrackGroupArrays(manifest); - initializeTrackSelectionLists(trackGroupArrays.length, rendererCapabilities.length); - mappedTrackInfos = new MappedTrackInfo[trackGroupArrays.length]; - for (int i = 0; i < trackGroupArrays.length; i++) { - TrackSelectorResult trackSelectorResult = runTrackSelection(/* periodIndex= */ i); - trackSelector.onSelectionActivated(trackSelectorResult.info); - mappedTrackInfos[i] = - Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo()); - } - handler.post(() -> callback.onPrepared(DownloadHelper.this)); - } catch (final IOException e) { - handler.post(() -> callback.onPrepareError(DownloadHelper.this, e)); - } - }) - .start(); + if (mediaSource != null) { + mediaPreparer = new MediaPreparer(mediaSource, /* downloadHelper= */ this); + } else { + callbackHandler.post(() -> callback.onPrepared(this)); + } } - /** Returns the manifest. Must not be called until after preparation completes. */ - public final T getManifest() { - Assertions.checkNotNull(manifest); - return manifest; + /** Releases the helper and all resources it is holding. */ + public final void release() { + if (mediaPreparer != null) { + mediaPreparer.release(); + } + } + + /** + * Returns the manifest, or null if no manifest is loaded. Must not be called until after + * preparation completes. + */ + @Nullable + public final Object getManifest() { + if (mediaSource == null) { + return null; + } + assertPreparedWithMedia(); + return mediaPreparer.manifest; } /** @@ -184,7 +203,10 @@ public abstract class DownloadHelper { * preparation completes. */ public final int getPeriodCount() { - Assertions.checkNotNull(trackGroupArrays); + if (mediaSource == null) { + return 0; + } + assertPreparedWithMedia(); return trackGroupArrays.length; } @@ -199,7 +221,7 @@ public abstract class DownloadHelper { * content. */ public final TrackGroupArray getTrackGroups(int periodIndex) { - Assertions.checkNotNull(trackGroupArrays); + assertPreparedWithMedia(); return trackGroupArrays[periodIndex]; } @@ -211,7 +233,7 @@ public abstract class DownloadHelper { * @return The {@link MappedTrackInfo} for the period. */ public final MappedTrackInfo getMappedTrackInfo(int periodIndex) { - Assertions.checkNotNull(mappedTrackInfos); + assertPreparedWithMedia(); return mappedTrackInfos[periodIndex]; } @@ -224,7 +246,7 @@ public abstract class DownloadHelper { * @return A list of selected {@link TrackSelection track selections}. */ public final List getTrackSelections(int periodIndex, int rendererIndex) { - Assertions.checkNotNull(immutableTrackSelectionsByPeriodAndRenderer); + assertPreparedWithMedia(); return immutableTrackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex]; } @@ -235,7 +257,7 @@ public abstract class DownloadHelper { * @param periodIndex The period index for which track selections are cleared. */ public final void clearTrackSelections(int periodIndex) { - Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer); + assertPreparedWithMedia(); for (int i = 0; i < rendererCapabilities.length; i++) { trackSelectionsByPeriodAndRenderer[periodIndex][i].clear(); } @@ -265,8 +287,7 @@ public abstract class DownloadHelper { */ public final void addTrackSelection( int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) { - Assertions.checkNotNull(trackGroupArrays); - Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer); + assertPreparedWithMedia(); trackSelector.setParameters(trackSelectorParameters); runTrackSelection(periodIndex); } @@ -279,26 +300,21 @@ public abstract class DownloadHelper { * @return The built {@link DownloadAction}. */ public final DownloadAction getDownloadAction(@Nullable byte[] data) { - Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer); - Assertions.checkNotNull(trackGroupArrays); + if (mediaSource == null) { + return DownloadAction.createDownloadAction( + downloadType, uri, /* keys= */ Collections.emptyList(), cacheKey, data); + } + assertPreparedWithMedia(); List streamKeys = new ArrayList<>(); + List allSelections = new ArrayList<>(); int periodCount = trackSelectionsByPeriodAndRenderer.length; for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) { + allSelections.clear(); int rendererCount = trackSelectionsByPeriodAndRenderer[periodIndex].length; for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { - List trackSelectionList = - trackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex]; - for (int selectionIndex = 0; selectionIndex < trackSelectionList.size(); selectionIndex++) { - TrackSelection trackSelection = trackSelectionList.get(selectionIndex); - int trackGroupIndex = - trackGroupArrays[periodIndex].indexOf(trackSelection.getTrackGroup()); - int trackCount = trackSelection.length(); - for (int trackListIndex = 0; trackListIndex < trackCount; trackListIndex++) { - int trackIndex = trackSelection.getIndexInTrackGroup(trackListIndex); - streamKeys.add(toStreamKey(periodIndex, trackGroupIndex, trackIndex)); - } - } + allSelections.addAll(trackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex]); } + streamKeys.addAll(mediaPreparer.mediaPeriods[periodIndex].getStreamKeys(allSelections)); } return DownloadAction.createDownloadAction(downloadType, uri, streamKeys, cacheKey, data); } @@ -312,36 +328,14 @@ public abstract class DownloadHelper { return DownloadAction.createRemoveAction(downloadType, uri, cacheKey); } - /** - * Loads the manifest. This method is called on a background thread. - * - * @param uri The manifest uri. - * @throws IOException If loading fails. - */ - protected abstract T loadManifest(Uri uri) throws IOException; - - /** - * Returns the track group arrays for each period in the manifest. - * - * @param manifest The manifest. - * @return An array of {@link TrackGroupArray}s. One for each period in the manifest. - */ - protected abstract TrackGroupArray[] getTrackGroupArrays(T manifest); - - /** - * Converts a track of a track group of a period to the corresponding {@link StreamKey}. - * - * @param periodIndex The index of the containing period. - * @param trackGroupIndex The index of the containing track group within the period. - * @param trackIndexInTrackGroup The index of the track within the track group. - * @return The corresponding {@link StreamKey}. - */ - protected abstract StreamKey toStreamKey( - int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup); - + // Initialization of array of Lists. @SuppressWarnings("unchecked") - @EnsuresNonNull("trackSelectionsByPeriodAndRenderer") - private void initializeTrackSelectionLists(int periodCount, int rendererCount) { + private void onMediaPrepared() { + Assertions.checkNotNull(mediaPreparer); + Assertions.checkNotNull(mediaPreparer.mediaPeriods); + Assertions.checkNotNull(mediaPreparer.timeline); + int periodCount = mediaPreparer.mediaPeriods.length; + int rendererCount = rendererCapabilities.length; trackSelectionsByPeriodAndRenderer = (List[][]) new List[periodCount][rendererCount]; immutableTrackSelectionsByPeriodAndRenderer = @@ -353,6 +347,49 @@ public abstract class DownloadHelper { Collections.unmodifiableList(trackSelectionsByPeriodAndRenderer[i][j]); } } + trackGroupArrays = new TrackGroupArray[periodCount]; + mappedTrackInfos = new MappedTrackInfo[periodCount]; + for (int i = 0; i < periodCount; i++) { + trackGroupArrays[i] = mediaPreparer.mediaPeriods[i].getTrackGroups(); + TrackSelectorResult trackSelectorResult = runTrackSelection(/* periodIndex= */ i); + trackSelector.onSelectionActivated(trackSelectorResult.info); + mappedTrackInfos[i] = Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo()); + } + setPreparedWithMedia(); + Assertions.checkNotNull(callbackHandler) + .post(() -> Assertions.checkNotNull(callback).onPrepared(this)); + } + + private void onMediaPreparationFailed(IOException error) { + Assertions.checkNotNull(callbackHandler) + .post(() -> Assertions.checkNotNull(callback).onPrepareError(this, error)); + } + + @RequiresNonNull({ + "trackGroupArrays", + "mappedTrackInfos", + "trackSelectionsByPeriodAndRenderer", + "immutableTrackSelectionsByPeriodAndRenderer", + "mediaPreparer", + "mediaPreparer.timeline", + "mediaPreparer.mediaPeriods" + }) + private void setPreparedWithMedia() { + isPreparedWithMedia = true; + } + + @EnsuresNonNull({ + "trackGroupArrays", + "mappedTrackInfos", + "trackSelectionsByPeriodAndRenderer", + "immutableTrackSelectionsByPeriodAndRenderer", + "mediaPreparer", + "mediaPreparer.timeline", + "mediaPreparer.mediaPeriods" + }) + @SuppressWarnings("nullness:contracts.postcondition.not.satisfied") + private void assertPreparedWithMedia() { + Assertions.checkState(isPreparedWithMedia); } /** @@ -361,26 +398,27 @@ public abstract class DownloadHelper { */ // Intentional reference comparison of track group instances. @SuppressWarnings("ReferenceEquality") - @RequiresNonNull({"trackGroupArrays", "trackSelectionsByPeriodAndRenderer"}) + @RequiresNonNull({ + "trackGroupArrays", + "trackSelectionsByPeriodAndRenderer", + "mediaPreparer", + "mediaPreparer.timeline" + }) private TrackSelectorResult runTrackSelection(int periodIndex) { - // TODO: Use actual timeline and media period id. - MediaPeriodId dummyMediaPeriodId = new MediaPeriodId(new Object()); - Timeline dummyTimeline = Timeline.EMPTY; - currentTrackSelectionPeriodIndex = periodIndex; try { TrackSelectorResult trackSelectorResult = trackSelector.selectTracks( rendererCapabilities, trackGroupArrays[periodIndex], - dummyMediaPeriodId, - dummyTimeline); + new MediaPeriodId(mediaPreparer.timeline.getUidOfPeriod(periodIndex)), + mediaPreparer.timeline); for (int i = 0; i < trackSelectorResult.length; i++) { TrackSelection newSelection = trackSelectorResult.selections.get(i); if (newSelection == null) { continue; } List existingSelectionList = - trackSelectionsByPeriodAndRenderer[currentTrackSelectionPeriodIndex][i]; + trackSelectionsByPeriodAndRenderer[periodIndex][i]; boolean mergedWithExistingSelection = false; for (int j = 0; j < existingSelectionList.size(); j++) { TrackSelection existingSelection = existingSelectionList.get(j); @@ -414,6 +452,113 @@ public abstract class DownloadHelper { } } + private static final class MediaPreparer + implements MediaSource.SourceInfoRefreshListener, MediaPeriod.Callback, Handler.Callback { + + private static final int MESSAGE_PREPARE_SOURCE = 0; + private static final int MESSAGE_CHECK_FOR_FAILURE = 1; + + private final MediaSource mediaSource; + private final DownloadHelper downloadHelper; + private final Allocator allocator; + private final HandlerThread mediaSourceThread; + private final Handler mediaSourceHandler; + + @Nullable public Object manifest; + public @MonotonicNonNull Timeline timeline; + public MediaPeriod @MonotonicNonNull [] mediaPeriods; + + private int pendingPreparations; + + public MediaPreparer(MediaSource mediaSource, DownloadHelper downloadHelper) { + this.mediaSource = mediaSource; + this.downloadHelper = downloadHelper; + allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); + mediaSourceThread = new HandlerThread("DownloadHelper"); + mediaSourceThread.start(); + mediaSourceHandler = Util.createHandler(mediaSourceThread.getLooper(), /* callback= */ this); + mediaSourceHandler.sendEmptyMessage(MESSAGE_PREPARE_SOURCE); + } + + public void release() { + if (mediaPeriods != null) { + for (MediaPeriod mediaPeriod : mediaPeriods) { + mediaSource.releasePeriod(mediaPeriod); + } + } + mediaSource.releaseSource(this); + mediaSourceThread.quit(); + } + + // Handler.Callback + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_PREPARE_SOURCE: + mediaSource.prepareSource(/* listener= */ this, /* mediaTransferListener= */ null); + mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE); + return true; + case MESSAGE_CHECK_FOR_FAILURE: + try { + if (mediaPeriods == null) { + mediaSource.maybeThrowSourceInfoRefreshError(); + } else { + for (MediaPeriod mediaPeriod : mediaPeriods) { + mediaPeriod.maybeThrowPrepareError(); + } + } + mediaSourceHandler.sendEmptyMessageDelayed( + MESSAGE_CHECK_FOR_FAILURE, /* delayMillis= */ 100); + } catch (IOException e) { + downloadHelper.onMediaPreparationFailed(e); + } + return true; + default: + return false; + } + } + + // MediaSource.SourceInfoRefreshListener implementation. + + @Override + public void onSourceInfoRefreshed( + MediaSource source, Timeline timeline, @Nullable Object manifest) { + if (this.timeline != null) { + // Ignore dynamic updates. + return; + } + this.timeline = timeline; + this.manifest = manifest; + mediaPeriods = new MediaPeriod[timeline.getPeriodCount()]; + pendingPreparations = mediaPeriods.length; + for (int i = 0; i < mediaPeriods.length; i++) { + mediaPeriods[i] = + mediaSource.createPeriod( + new MediaPeriodId(timeline.getUidOfPeriod(/* periodIndex= */ i)), + allocator, + /* startPositionUs= */ 0); + mediaPeriods[i].prepare(/* callback= */ this, /* positionUs= */ 0); + } + } + + // MediaPeriod.Callback implementation. + + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + pendingPreparations--; + if (pendingPreparations == 0) { + mediaSourceHandler.removeMessages(MESSAGE_CHECK_FOR_FAILURE); + downloadHelper.onMediaPrepared(); + } + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + // Ignore. + } + } + private static final class DownloadTrackSelection extends BaseTrackSelection { private static final class Factory implements TrackSelection.Factory { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java index 2ec14368ca..1850eaebf2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java @@ -17,11 +17,9 @@ package com.google.android.exoplayer2.offline; import android.net.Uri; import android.support.annotation.Nullable; -import com.google.android.exoplayer2.Renderer; -import com.google.android.exoplayer2.source.TrackGroupArray; /** A {@link DownloadHelper} for progressive streams. */ -public final class ProgressiveDownloadHelper extends DownloadHelper { +public final class ProgressiveDownloadHelper extends DownloadHelper { /** * Creates download helper for progressive streams. @@ -43,24 +41,9 @@ public final class ProgressiveDownloadHelper extends DownloadHelper { DownloadAction.TYPE_PROGRESSIVE, uri, cacheKey, - DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, - (handler, videoListener, audioListener, metadata, text, drm) -> new Renderer[0], + /* mediaSource= */ null, + /* trackSelectorParameters= */ DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, + /* renderersFactory= */ null, /* drmSessionManager= */ null); } - - @Override - protected Void loadManifest(Uri uri) { - return null; - } - - @Override - protected TrackGroupArray[] getTrackGroupArrays(Void manifest) { - return new TrackGroupArray[] {TrackGroupArray.EMPTY}; - } - - @Override - protected StreamKey toStreamKey( - int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) { - return new StreamKey(periodIndex, trackGroupIndex, trackIndexInTrackGroup); - } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java index 5f287d8685..e6cca02140 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java @@ -22,17 +22,28 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.offline.DownloadHelper.Callback; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.testutil.FakeMediaPeriod; +import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeRenderer; +import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; +import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.ParametersBuilder; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicReference; @@ -40,15 +51,19 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowLooper; /** Unit tests for {@link DownloadHelper}. */ @RunWith(RobolectricTestRunner.class) +@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) public class DownloadHelperTest { private static final String TEST_DOWNLOAD_TYPE = "downloadType"; private static final String TEST_CACHE_KEY = "cacheKey"; - private static final ManifestType TEST_MANIFEST = new ManifestType(); + private static final Timeline TEST_TIMELINE = + new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ new Object())); + private static final Object TEST_MANIFEST = new Object(); private static final Format VIDEO_FORMAT_LOW = createVideoFormat(/* bitrate= */ 200_000); private static final Format VIDEO_FORMAT_HIGH = createVideoFormat(/* bitrate= */ 800_000); @@ -98,7 +113,7 @@ public class DownloadHelperTest { public void getManifest_returnsManifest() throws Exception { prepareDownloadHelper(downloadHelper); - ManifestType manifest = downloadHelper.getManifest(); + Object manifest = downloadHelper.getManifest(); assertThat(manifest).isEqualTo(TEST_MANIFEST); } @@ -337,12 +352,12 @@ public class DownloadHelperTest { downloadHelper.prepare( new Callback() { @Override - public void onPrepared(DownloadHelper helper) { + public void onPrepared(DownloadHelper helper) { preparedCondition.open(); } @Override - public void onPrepareError(DownloadHelper helper, IOException e) { + public void onPrepareError(DownloadHelper helper, IOException e) { prepareException.set(e); preparedCondition.open(); } @@ -411,35 +426,52 @@ public class DownloadHelperTest { assertThat(selectedTracksInGroup).isEqualTo(tracks); } - private static final class ManifestType {} - - private static final class FakeDownloadHelper extends DownloadHelper { + private static final class FakeDownloadHelper extends DownloadHelper { public FakeDownloadHelper(Uri testUri, RenderersFactory renderersFactory) { super( TEST_DOWNLOAD_TYPE, testUri, TEST_CACHE_KEY, + new TestMediaSource(), DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, renderersFactory, /* drmSessionManager= */ null); } + } - @Override - protected ManifestType loadManifest(Uri uri) throws IOException { - return TEST_MANIFEST; + private static final class TestMediaSource extends FakeMediaSource { + + public TestMediaSource() { + super(TEST_TIMELINE, TEST_MANIFEST); } @Override - protected TrackGroupArray[] getTrackGroupArrays(ManifestType manifest) { - assertThat(manifest).isEqualTo(TEST_MANIFEST); - return TRACK_GROUP_ARRAYS; + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + int periodIndex = TEST_TIMELINE.getIndexOfPeriod(id.periodUid); + return new FakeMediaPeriod( + TRACK_GROUP_ARRAYS[periodIndex], + new EventDispatcher() + .withParameters(/* windowIndex= */ 0, id, /* mediaTimeOffsetMs= */ 0)) { + @Override + public List getStreamKeys(List trackSelections) { + List result = new ArrayList<>(); + for (TrackSelection trackSelection : trackSelections) { + int groupIndex = + TRACK_GROUP_ARRAYS[periodIndex].indexOf(trackSelection.getTrackGroup()); + for (int i = 0; i < trackSelection.length(); i++) { + result.add( + new StreamKey(periodIndex, groupIndex, trackSelection.getIndexInTrackGroup(i))); + } + } + return result; + } + }; } @Override - protected StreamKey toStreamKey( - int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) { - return new StreamKey(periodIndex, trackGroupIndex, trackIndexInTrackGroup); + public void releasePeriod(MediaPeriod mediaPeriod) { + // Do nothing. } } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java index f86e47ed3d..b611cf0d5f 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java @@ -17,30 +17,17 @@ package com.google.android.exoplayer2.source.dash.offline; import android.net.Uri; import android.support.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadHelper; -import com.google.android.exoplayer2.offline.StreamKey; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; -import com.google.android.exoplayer2.source.dash.manifest.DashManifest; -import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; -import com.google.android.exoplayer2.source.dash.manifest.Representation; +import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.ParsingLoadable; -import java.io.IOException; -import java.util.List; /** A {@link DownloadHelper} for DASH streams. */ -public final class DashDownloadHelper extends DownloadHelper { - - private final DataSource.Factory manifestDataSourceFactory; +public final class DashDownloadHelper extends DownloadHelper { /** * Creates a DASH download helper. @@ -85,42 +72,9 @@ public final class DashDownloadHelper extends DownloadHelper { DownloadAction.TYPE_DASH, uri, /* cacheKey= */ null, + new DashMediaSource.Factory(manifestDataSourceFactory).createMediaSource(uri), trackSelectorParameters, renderersFactory, drmSessionManager); - this.manifestDataSourceFactory = manifestDataSourceFactory; - } - - @Override - protected DashManifest loadManifest(Uri uri) throws IOException { - DataSource dataSource = manifestDataSourceFactory.createDataSource(); - return ParsingLoadable.load(dataSource, new DashManifestParser(), uri, C.DATA_TYPE_MANIFEST); - } - - @Override - public TrackGroupArray[] getTrackGroupArrays(DashManifest manifest) { - int periodCount = manifest.getPeriodCount(); - TrackGroupArray[] trackGroupArrays = new TrackGroupArray[periodCount]; - for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) { - List adaptationSets = manifest.getPeriod(periodIndex).adaptationSets; - TrackGroup[] trackGroups = new TrackGroup[adaptationSets.size()]; - for (int i = 0; i < trackGroups.length; i++) { - List representations = adaptationSets.get(i).representations; - Format[] formats = new Format[representations.size()]; - int representationsCount = representations.size(); - for (int j = 0; j < representationsCount; j++) { - formats[j] = representations.get(j).format; - } - trackGroups[i] = new TrackGroup(formats); - } - trackGroupArrays[periodIndex] = new TrackGroupArray(trackGroups); - } - return trackGroupArrays; - } - - @Override - protected StreamKey toStreamKey( - int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) { - return new StreamKey(periodIndex, trackGroupIndex, trackIndexInTrackGroup); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java index e0f55aa738..ee6bbe333a 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java @@ -17,34 +17,17 @@ package com.google.android.exoplayer2.source.hls.offline; import android.net.Uri; import android.support.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadHelper; -import com.google.android.exoplayer2.offline.StreamKey; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.ParsingLoadable; -import com.google.android.exoplayer2.util.Assertions; -import java.io.IOException; -import java.util.Arrays; -import java.util.List; /** A {@link DownloadHelper} for HLS streams. */ -public final class HlsDownloadHelper extends DownloadHelper { - - private final DataSource.Factory manifestDataSourceFactory; - - private int[] renditionGroups; +public final class HlsDownloadHelper extends DownloadHelper { /** * Creates a HLS download helper. @@ -89,56 +72,11 @@ public final class HlsDownloadHelper extends DownloadHelper { DownloadAction.TYPE_HLS, uri, /* cacheKey= */ null, + new HlsMediaSource.Factory(manifestDataSourceFactory) + .setAllowChunklessPreparation(true) + .createMediaSource(uri), trackSelectorParameters, renderersFactory, drmSessionManager); - this.manifestDataSourceFactory = manifestDataSourceFactory; - } - - @Override - protected HlsPlaylist loadManifest(Uri uri) throws IOException { - DataSource dataSource = manifestDataSourceFactory.createDataSource(); - return ParsingLoadable.load(dataSource, new HlsPlaylistParser(), uri, C.DATA_TYPE_MANIFEST); - } - - @Override - protected TrackGroupArray[] getTrackGroupArrays(HlsPlaylist playlist) { - Assertions.checkNotNull(playlist); - if (playlist instanceof HlsMediaPlaylist) { - renditionGroups = new int[0]; - return new TrackGroupArray[] {TrackGroupArray.EMPTY}; - } - // TODO: Generate track groups as in playback. Reverse the mapping in toStreamKey. - HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; - TrackGroup[] trackGroups = new TrackGroup[3]; - renditionGroups = new int[3]; - int trackGroupIndex = 0; - if (!masterPlaylist.variants.isEmpty()) { - renditionGroups[trackGroupIndex] = HlsMasterPlaylist.GROUP_INDEX_VARIANT; - trackGroups[trackGroupIndex++] = new TrackGroup(toFormats(masterPlaylist.variants)); - } - if (!masterPlaylist.audios.isEmpty()) { - renditionGroups[trackGroupIndex] = HlsMasterPlaylist.GROUP_INDEX_AUDIO; - trackGroups[trackGroupIndex++] = new TrackGroup(toFormats(masterPlaylist.audios)); - } - if (!masterPlaylist.subtitles.isEmpty()) { - renditionGroups[trackGroupIndex] = HlsMasterPlaylist.GROUP_INDEX_SUBTITLE; - trackGroups[trackGroupIndex++] = new TrackGroup(toFormats(masterPlaylist.subtitles)); - } - return new TrackGroupArray[] {new TrackGroupArray(Arrays.copyOf(trackGroups, trackGroupIndex))}; - } - - @Override - protected StreamKey toStreamKey( - int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) { - return new StreamKey(renditionGroups[trackGroupIndex], trackIndexInTrackGroup); - } - - private static Format[] toFormats(List hlsUrls) { - Format[] formats = new Format[hlsUrls.size()]; - for (int i = 0; i < hlsUrls.size(); i++) { - formats[i] = hlsUrls.get(i).format; - } - return formats; } } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java index b17768f202..f76fb4ee90 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java @@ -17,27 +17,17 @@ package com.google.android.exoplayer2.source.smoothstreaming.offline; import android.net.Uri; import android.support.annotation.Nullable; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadHelper; -import com.google.android.exoplayer2.offline.StreamKey; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsUtil; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.ParsingLoadable; -import java.io.IOException; /** A {@link DownloadHelper} for SmoothStreaming streams. */ -public final class SsDownloadHelper extends DownloadHelper { - - private final DataSource.Factory manifestDataSourceFactory; +public final class SsDownloadHelper extends DownloadHelper { /** * Creates a SmoothStreaming download helper. @@ -82,32 +72,9 @@ public final class SsDownloadHelper extends DownloadHelper { DownloadAction.TYPE_SS, uri, /* cacheKey= */ null, + new SsMediaSource.Factory(manifestDataSourceFactory).createMediaSource(uri), trackSelectorParameters, renderersFactory, drmSessionManager); - this.manifestDataSourceFactory = manifestDataSourceFactory; - } - - @Override - protected SsManifest loadManifest(Uri uri) throws IOException { - DataSource dataSource = manifestDataSourceFactory.createDataSource(); - Uri fixedUri = SsUtil.fixManifestUri(uri); - return ParsingLoadable.load(dataSource, new SsManifestParser(), fixedUri, C.DATA_TYPE_MANIFEST); - } - - @Override - protected TrackGroupArray[] getTrackGroupArrays(SsManifest manifest) { - SsManifest.StreamElement[] streamElements = manifest.streamElements; - TrackGroup[] trackGroups = new TrackGroup[streamElements.length]; - for (int i = 0; i < streamElements.length; i++) { - trackGroups[i] = new TrackGroup(streamElements[i].formats); - } - return new TrackGroupArray[] {new TrackGroupArray(trackGroups)}; - } - - @Override - protected StreamKey toStreamKey( - int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) { - return new StreamKey(trackGroupIndex, trackIndexInTrackGroup); } }