getKeys() {
return keys;
}
@@ -187,7 +188,8 @@ public final class DownloadAction {
return false;
}
DownloadAction that = (DownloadAction) o;
- return type.equals(that.type)
+ return id.equals(that.id)
+ && type.equals(that.type)
&& uri.equals(that.uri)
&& isRemoveAction == that.isRemoveAction
&& keys.equals(that.keys)
@@ -198,6 +200,7 @@ public final class DownloadAction {
@Override
public final int hashCode() {
int result = type.hashCode();
+ result = 31 * result + id.hashCode();
result = 31 * result + uri.hashCode();
result = 31 * result + (isRemoveAction ? 1 : 0);
result = 31 * result + keys.hashCode();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadActionUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadActionUtil.java
new file mode 100644
index 0000000000..f722f9b59b
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadActionUtil.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.offline;
+
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.HashSet;
+
+/** {@link DownloadAction} related utility methods. */
+public class DownloadActionUtil {
+
+ private DownloadActionUtil() {}
+
+ /**
+ * Merge {@link DownloadAction}s in {@code actionQueue} to minimum number of actions.
+ *
+ * All actions must have the same type and must be for the same media.
+ *
+ * @param actionQueue Queue of actions. Must not be empty.
+ * @return The first action in the queue.
+ */
+ public static DownloadAction mergeActions(ArrayDeque actionQueue) {
+ DownloadAction removeAction = null;
+ DownloadAction downloadAction = null;
+ HashSet keys = new HashSet<>();
+ boolean downloadAllTracks = false;
+ DownloadAction firstAction = Assertions.checkNotNull(actionQueue.peek());
+
+ while (!actionQueue.isEmpty()) {
+ DownloadAction action = actionQueue.remove();
+ Assertions.checkState(action.type.equals(firstAction.type));
+ Assertions.checkState(action.isSameMedia(firstAction));
+ if (action.isRemoveAction) {
+ removeAction = action;
+ downloadAction = null;
+ keys.clear();
+ downloadAllTracks = false;
+ } else {
+ if (!downloadAllTracks) {
+ if (action.keys.isEmpty()) {
+ downloadAllTracks = true;
+ keys.clear();
+ } else {
+ keys.addAll(action.keys);
+ }
+ }
+ downloadAction = action;
+ }
+ }
+
+ if (removeAction != null) {
+ actionQueue.add(removeAction);
+ }
+ if (downloadAction != null) {
+ actionQueue.add(
+ DownloadAction.createDownloadAction(
+ downloadAction.type,
+ downloadAction.uri,
+ new ArrayList<>(keys),
+ downloadAction.customCacheKey,
+ downloadAction.data));
+ }
+ return Assertions.checkNotNull(actionQueue.peek());
+ }
+}
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 044bd8cc8a..e799aff4b2 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
@@ -19,18 +19,66 @@ import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.Nullable;
+import android.util.SparseIntArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.RendererCapabilities;
+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.MediaSource.MediaPeriodId;
+import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.BaseTrackSelection;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Parameters;
+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.BandwidthMeter;
+import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
* A helper for initializing and removing downloads.
*
+ * The helper extracts track information from the media, selects tracks for downloading, and
+ * creates {@link DownloadAction download actions} based on the selected tracks.
+ *
+ *
A typical usage of DownloadHelper follows these steps:
+ *
+ *
+ * - Construct the download helper with information about the {@link RenderersFactory renderers}
+ * and {@link DefaultTrackSelector.Parameters parameters} for track selection.
+ *
- Prepare the helper using {@link #prepare(Callback)} and wait for the callback.
+ *
- 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)}.
+ *
- Create download actions for the selected track using {@link #getDownloadAction(byte[])}.
+ *
+ *
* @param The manifest type.
*/
public abstract class DownloadHelper {
+ /**
+ * The default parameters used for track selection for downloading. This default selects the
+ * highest bitrate audio and video tracks which are supported by the renderers.
+ */
+ public static final DefaultTrackSelector.Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS =
+ new DefaultTrackSelector.ParametersBuilder().setForceHighestSupportedBitrate(true).build();
+
/** A callback to be notified when the {@link DownloadHelper} is prepared. */
public interface Callback {
@@ -39,7 +87,7 @@ public abstract class DownloadHelper {
*
* @param helper The reporting {@link DownloadHelper}.
*/
- void onPrepared(DownloadHelper helper);
+ void onPrepared(DownloadHelper> helper);
/**
* Called when preparation fails.
@@ -47,27 +95,51 @@ 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;
+ private final DefaultTrackSelector trackSelector;
+ private final RendererCapabilities[] rendererCapabilities;
+ private final SparseIntArray scratchSet;
+ private int currentTrackSelectionPeriodIndex;
@Nullable private T manifest;
- @Nullable private TrackGroupArray[] trackGroupArrays;
+ private TrackGroupArray @MonotonicNonNull [] trackGroupArrays;
+ private MappedTrackInfo @MonotonicNonNull [] mappedTrackInfos;
+ private List @MonotonicNonNull [][] trackSelectionsByPeriodAndRenderer;
+ private List @MonotonicNonNull [][] immutableTrackSelectionsByPeriodAndRenderer;
/**
- * Create download helper.
+ * Creates download helper.
*
* @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 trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
+ * downloading.
+ * @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks
+ * are selected.
+ * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by
+ * {@code renderersFactory}.
*/
- public DownloadHelper(String downloadType, Uri uri, @Nullable String cacheKey) {
+ public DownloadHelper(
+ String downloadType,
+ Uri uri,
+ @Nullable String cacheKey,
+ DefaultTrackSelector.Parameters trackSelectorParameters,
+ RenderersFactory renderersFactory,
+ @Nullable DrmSessionManager drmSessionManager) {
this.downloadType = downloadType;
this.uri = uri;
this.cacheKey = cacheKey;
+ this.trackSelector = new DefaultTrackSelector(new DownloadTrackSelection.Factory());
+ this.rendererCapabilities = Util.getRendererCapabilities(renderersFactory, drmSessionManager);
+ this.scratchSet = new SparseIntArray();
+ trackSelector.setParameters(trackSelectorParameters);
+ trackSelector.init(/* listener= */ () -> {}, new DummyBandwidthMeter());
}
/**
@@ -77,21 +149,28 @@ public abstract class DownloadHelper {
* 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.
*/
- public final void prepare(final Callback callback) {
- final Handler handler =
+ public final void prepare(Callback callback) {
+ Handler handler =
new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper());
- new Thread() {
- @Override
- public void run() {
- try {
- manifest = loadManifest(uri);
- trackGroupArrays = getTrackGroupArrays(manifest);
- handler.post(() -> callback.onPrepared(DownloadHelper.this));
- } catch (final IOException e) {
- handler.post(() -> callback.onPrepareError(DownloadHelper.this, e));
- }
- }
- }.start();
+ 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();
}
/** Returns the manifest. Must not be called until after preparation completes. */
@@ -113,6 +192,8 @@ public abstract class DownloadHelper {
* Returns the track groups for the given period. Must not be called until after preparation
* completes.
*
+ * Use {@link #getMappedTrackInfo(int)} to get the track groups mapped to renderers.
+ *
* @param periodIndex The period index.
* @return The track groups for the period. May be {@link TrackGroupArray#EMPTY} for single stream
* content.
@@ -123,16 +204,103 @@ public abstract class DownloadHelper {
}
/**
- * Builds a {@link DownloadAction} for downloading the specified tracks. Must not be called until
+ * Returns the mapped track info for the given period. Must not be called until after preparation
+ * completes.
+ *
+ * @param periodIndex The period index.
+ * @return The {@link MappedTrackInfo} for the period.
+ */
+ public final MappedTrackInfo getMappedTrackInfo(int periodIndex) {
+ Assertions.checkNotNull(mappedTrackInfos);
+ return mappedTrackInfos[periodIndex];
+ }
+
+ /**
+ * Returns all {@link TrackSelection track selections} for a period and renderer. Must not be
+ * called until after preparation completes.
+ *
+ * @param periodIndex The period index.
+ * @param rendererIndex The renderer index.
+ * @return A list of selected {@link TrackSelection track selections}.
+ */
+ public final List getTrackSelections(int periodIndex, int rendererIndex) {
+ Assertions.checkNotNull(immutableTrackSelectionsByPeriodAndRenderer);
+ return immutableTrackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex];
+ }
+
+ /**
+ * Clears the selection of tracks for a period. Must not be called until after preparation
+ * completes.
+ *
+ * @param periodIndex The period index for which track selections are cleared.
+ */
+ public final void clearTrackSelections(int periodIndex) {
+ Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer);
+ for (int i = 0; i < rendererCapabilities.length; i++) {
+ trackSelectionsByPeriodAndRenderer[periodIndex][i].clear();
+ }
+ }
+
+ /**
+ * Replaces a selection of tracks to be downloaded. Must not be called until after preparation
+ * completes.
+ *
+ * @param periodIndex The period index for which the track selection is replaced.
+ * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new
+ * selection of tracks.
+ */
+ public final void replaceTrackSelections(
+ int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) {
+ clearTrackSelections(periodIndex);
+ addTrackSelection(periodIndex, trackSelectorParameters);
+ }
+
+ /**
+ * Adds a selection of tracks to be downloaded. Must not be called until after preparation
+ * completes.
+ *
+ * @param periodIndex The period index this track selection is added for.
+ * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new
+ * selection of tracks.
+ */
+ public final void addTrackSelection(
+ int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) {
+ Assertions.checkNotNull(trackGroupArrays);
+ Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer);
+ trackSelector.setParameters(trackSelectorParameters);
+ runTrackSelection(periodIndex);
+ }
+
+ /**
+ * Builds a {@link DownloadAction} for downloading the selected tracks. Must not be called until
* after preparation completes.
*
* @param data Application provided data to store in {@link DownloadAction#data}.
- * @param trackKeys The selected tracks. If empty, all streams will be downloaded.
* @return The built {@link DownloadAction}.
*/
- public final DownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) {
- return DownloadAction.createDownloadAction(
- downloadType, uri, toStreamKeys(trackKeys), cacheKey, data);
+ public final DownloadAction getDownloadAction(@Nullable byte[] data) {
+ Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer);
+ Assertions.checkNotNull(trackGroupArrays);
+ List streamKeys = new ArrayList<>();
+ int periodCount = trackSelectionsByPeriodAndRenderer.length;
+ for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) {
+ 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));
+ }
+ }
+ }
+ }
+ return DownloadAction.createDownloadAction(downloadType, uri, streamKeys, cacheKey, data);
}
/**
@@ -161,10 +329,151 @@ public abstract class DownloadHelper {
protected abstract TrackGroupArray[] getTrackGroupArrays(T manifest);
/**
- * Converts a list of {@link TrackKey track keys} to {@link StreamKey stream keys}.
+ * Converts a track of a track group of a period to the corresponding {@link StreamKey}.
*
- * @param trackKeys A list of track keys.
- * @return A corresponding list of stream keys.
+ * @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 List toStreamKeys(List trackKeys);
+ protected abstract StreamKey toStreamKey(
+ int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup);
+
+ @SuppressWarnings("unchecked")
+ @EnsuresNonNull("trackSelectionsByPeriodAndRenderer")
+ private void initializeTrackSelectionLists(int periodCount, int rendererCount) {
+ trackSelectionsByPeriodAndRenderer =
+ (List[][]) new List>[periodCount][rendererCount];
+ immutableTrackSelectionsByPeriodAndRenderer =
+ (List[][]) new List>[periodCount][rendererCount];
+ for (int i = 0; i < periodCount; i++) {
+ for (int j = 0; j < rendererCount; j++) {
+ trackSelectionsByPeriodAndRenderer[i][j] = new ArrayList<>();
+ immutableTrackSelectionsByPeriodAndRenderer[i][j] =
+ Collections.unmodifiableList(trackSelectionsByPeriodAndRenderer[i][j]);
+ }
+ }
+ }
+
+ /**
+ * Runs the track selection for a given period index with the current parameters. The selected
+ * tracks will be added to {@link #trackSelectionsByPeriodAndRenderer}.
+ */
+ // Intentional reference comparison of track group instances.
+ @SuppressWarnings("ReferenceEquality")
+ @RequiresNonNull({"trackGroupArrays", "trackSelectionsByPeriodAndRenderer"})
+ 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);
+ for (int i = 0; i < trackSelectorResult.length; i++) {
+ TrackSelection newSelection = trackSelectorResult.selections.get(i);
+ if (newSelection == null) {
+ continue;
+ }
+ List existingSelectionList =
+ trackSelectionsByPeriodAndRenderer[currentTrackSelectionPeriodIndex][i];
+ boolean mergedWithExistingSelection = false;
+ for (int j = 0; j < existingSelectionList.size(); j++) {
+ TrackSelection existingSelection = existingSelectionList.get(j);
+ if (existingSelection.getTrackGroup() == newSelection.getTrackGroup()) {
+ // Merge with existing selection.
+ scratchSet.clear();
+ for (int k = 0; k < existingSelection.length(); k++) {
+ scratchSet.put(existingSelection.getIndexInTrackGroup(k), 0);
+ }
+ for (int k = 0; k < newSelection.length(); k++) {
+ scratchSet.put(newSelection.getIndexInTrackGroup(k), 0);
+ }
+ int[] mergedTracks = new int[scratchSet.size()];
+ for (int k = 0; k < scratchSet.size(); k++) {
+ mergedTracks[k] = scratchSet.keyAt(k);
+ }
+ existingSelectionList.set(
+ j, new DownloadTrackSelection(existingSelection.getTrackGroup(), mergedTracks));
+ mergedWithExistingSelection = true;
+ break;
+ }
+ }
+ if (!mergedWithExistingSelection) {
+ existingSelectionList.add(newSelection);
+ }
+ }
+ return trackSelectorResult;
+ } catch (ExoPlaybackException e) {
+ // DefaultTrackSelector does not throw exceptions during track selection.
+ throw new UnsupportedOperationException(e);
+ }
+ }
+
+ private static final class DownloadTrackSelection extends BaseTrackSelection {
+
+ private static final class Factory implements TrackSelection.Factory {
+
+ @Override
+ public @NullableType TrackSelection[] createTrackSelections(
+ @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
+ @NullableType TrackSelection[] selections = new TrackSelection[definitions.length];
+ for (int i = 0; i < definitions.length; i++) {
+ selections[i] =
+ definitions[i] == null
+ ? null
+ : new DownloadTrackSelection(definitions[i].group, definitions[i].tracks);
+ }
+ return selections;
+ }
+ }
+
+ public DownloadTrackSelection(TrackGroup trackGroup, int[] tracks) {
+ super(trackGroup, tracks);
+ }
+
+ @Override
+ public int getSelectedIndex() {
+ return 0;
+ }
+
+ @Override
+ public int getSelectionReason() {
+ return C.SELECTION_REASON_UNKNOWN;
+ }
+
+ @Nullable
+ @Override
+ public Object getSelectionData() {
+ return null;
+ }
+ }
+
+ private static final class DummyBandwidthMeter implements BandwidthMeter {
+
+ @Override
+ public long getBitrateEstimate() {
+ return 0;
+ }
+
+ @Nullable
+ @Override
+ public TransferListener getTransferListener() {
+ return null;
+ }
+
+ @Override
+ public void addEventListener(Handler eventHandler, EventListener eventListener) {
+ // Do nothing.
+ }
+
+ @Override
+ public void removeEventListener(EventListener eventListener) {
+ // Do nothing.
+ }
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java
index 4a76c80d64..997f4e09a2 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java
@@ -15,11 +15,12 @@
*/
package com.google.android.exoplayer2.offline;
-import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_CANCELED;
-import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_COMPLETED;
-import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_FAILED;
-import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_QUEUED;
-import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_STARTED;
+import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.FAILURE_REASON_NONE;
+import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.FAILURE_REASON_UNKNOWN;
+import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_COMPLETED;
+import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_FAILED;
+import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_QUEUED;
+import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_STARTED;
import android.os.ConditionVariable;
import android.os.Handler;
@@ -35,6 +36,7 @@ import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;
@@ -58,41 +60,40 @@ public final class DownloadManager {
*/
void onInitialized(DownloadManager downloadManager);
/**
- * Called when the state of a task changes.
+ * Called when the state of a download changes.
*
* @param downloadManager The reporting instance.
- * @param taskState The state of the task.
+ * @param downloadState The state of the download.
*/
- void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState);
+ void onDownloadStateChanged(DownloadManager downloadManager, DownloadState downloadState);
/**
- * Called when there is no active task left.
+ * Called when there is no active download left.
*
* @param downloadManager The reporting instance.
*/
void onIdle(DownloadManager downloadManager);
}
- /** The default maximum number of simultaneous download tasks. */
+ /** The default maximum number of simultaneous downloads. */
public static final int DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS = 1;
- /** The default minimum number of times a task must be retried before failing. */
+ /** The default minimum number of times a download must be retried before failing. */
public static final int DEFAULT_MIN_RETRY_COUNT = 5;
private static final String TAG = "DownloadManager";
private static final boolean DEBUG = false;
- private final int maxActiveDownloadTasks;
+ private final int maxActiveDownloads;
private final int minRetryCount;
private final ActionFile actionFile;
private final DownloaderFactory downloaderFactory;
- private final ArrayList tasks;
- private final ArrayList activeDownloadTasks;
+ private final ArrayList downloads;
+ private final ArrayList activeDownloads;
private final Handler handler;
private final HandlerThread fileIOThread;
private final Handler fileIOHandler;
private final CopyOnWriteArraySet listeners;
- private int nextTaskId;
private boolean initialized;
private boolean released;
private boolean downloadsStopped;
@@ -113,8 +114,8 @@ public final class DownloadManager {
*
* @param actionFile The file in which active actions are saved.
* @param downloaderFactory A factory for creating {@link Downloader}s.
- * @param maxSimultaneousDownloads The maximum number of simultaneous download tasks.
- * @param minRetryCount The minimum number of times a task must be retried before failing.
+ * @param maxSimultaneousDownloads The maximum number of simultaneous downloads.
+ * @param minRetryCount The minimum number of times a download must be retried before failing.
*/
public DownloadManager(
File actionFile,
@@ -123,12 +124,12 @@ public final class DownloadManager {
int minRetryCount) {
this.actionFile = new ActionFile(actionFile);
this.downloaderFactory = downloaderFactory;
- this.maxActiveDownloadTasks = maxSimultaneousDownloads;
+ this.maxActiveDownloads = maxSimultaneousDownloads;
this.minRetryCount = minRetryCount;
this.downloadsStopped = true;
- tasks = new ArrayList<>();
- activeDownloadTasks = new ArrayList<>();
+ downloads = new ArrayList<>();
+ activeDownloads = new ArrayList<>();
Looper looper = Looper.myLooper();
if (looper == null) {
@@ -164,85 +165,78 @@ public final class DownloadManager {
listeners.remove(listener);
}
- /** Starts the download tasks. */
+ /** Starts the downloads. */
public void startDownloads() {
Assertions.checkState(!released);
if (downloadsStopped) {
downloadsStopped = false;
- maybeStartTasks();
+ maybeStartDownloads();
logd("Downloads are started");
}
}
- /** Stops all of the download tasks. Call {@link #startDownloads()} to restart tasks. */
+ /** Stops all of the downloads. Call {@link #startDownloads()} to restart downloads. */
public void stopDownloads() {
Assertions.checkState(!released);
if (!downloadsStopped) {
downloadsStopped = true;
- for (int i = 0; i < activeDownloadTasks.size(); i++) {
- activeDownloadTasks.get(i).stop();
+ for (int i = 0; i < activeDownloads.size(); i++) {
+ activeDownloads.get(i).stop();
}
logd("Downloads are stopping");
}
}
/**
- * Handles the given action. A task is created and added to the task queue. If it's a remove
- * action then any download tasks for the same media are immediately canceled.
+ * Handles the given action.
*
* @param action The action to be executed.
- * @return The id of the newly created task.
*/
- public int handleAction(DownloadAction action) {
+ public void handleAction(DownloadAction action) {
Assertions.checkState(!released);
- Task task = addTaskForAction(action);
+ Download download = getDownloadForAction(action);
if (initialized) {
saveActions();
- maybeStartTasks();
- if (task.state == STATE_QUEUED) {
- // Task did not change out of its initial state, and so its initial state won't have been
+ maybeStartDownloads();
+ if (download.state == STATE_QUEUED) {
+ // Download did not change out of its initial state, and so its initial state won't have
+ // been
// reported to listeners. Do so now.
- notifyListenersTaskStateChange(task);
+ notifyListenersDownloadStateChange(download);
}
}
- return task.id;
}
- /** Returns the number of tasks. */
- public int getTaskCount() {
- Assertions.checkState(!released);
- return tasks.size();
- }
-
- /** Returns the number of download tasks. */
+ /** Returns the number of downloads. */
public int getDownloadCount() {
- int count = 0;
- for (int i = 0; i < tasks.size(); i++) {
- if (!tasks.get(i).action.isRemoveAction) {
- count++;
- }
- }
- return count;
+ Assertions.checkState(!released);
+ return downloads.size();
}
- /** Returns the state of a task, or null if no such task exists */
- public @Nullable TaskState getTaskState(int taskId) {
+ /**
+ * Returns {@link DownloadState} for the given content id, or null if no such download exists.
+ *
+ * @param id The unique content id.
+ * @return DownloadState for the given content id, or null if no such download exists.
+ */
+ @Nullable
+ public DownloadState getDownloadState(String id) {
Assertions.checkState(!released);
- for (int i = 0; i < tasks.size(); i++) {
- Task task = tasks.get(i);
- if (task.id == taskId) {
- return task.getTaskState();
+ for (int i = 0; i < downloads.size(); i++) {
+ Download download = downloads.get(i);
+ if (download.id.equals(id)) {
+ return download.getDownloadState();
}
}
return null;
}
- /** Returns the states of all current tasks. */
- public TaskState[] getAllTaskStates() {
+ /** Returns the states of all current downloads. */
+ public DownloadState[] getAllDownloadStates() {
Assertions.checkState(!released);
- TaskState[] states = new TaskState[tasks.size()];
+ DownloadState[] states = new DownloadState[downloads.size()];
for (int i = 0; i < states.length; i++) {
- states[i] = tasks.get(i).getTaskState();
+ states[i] = downloads.get(i).getDownloadState();
}
return states;
}
@@ -253,14 +247,14 @@ public final class DownloadManager {
return initialized;
}
- /** Returns whether there are no active tasks. */
+ /** Returns whether there are no active downloads. */
public boolean isIdle() {
Assertions.checkState(!released);
if (!initialized) {
return false;
}
- for (int i = 0; i < tasks.size(); i++) {
- if (tasks.get(i).isStarted()) {
+ for (int i = 0; i < downloads.size(); i++) {
+ if (downloads.get(i).isStarted()) {
return false;
}
}
@@ -268,16 +262,17 @@ public final class DownloadManager {
}
/**
- * Stops all of the tasks and releases resources. If the action file isn't up to date, waits for
- * the changes to be written. The manager must not be accessed after this method has been called.
+ * Stops all of the downloads and releases resources. If the action file isn't up to date, waits
+ * for the changes to be written. The manager must not be accessed after this method has been
+ * called.
*/
public void release() {
if (released) {
return;
}
released = true;
- for (int i = 0; i < tasks.size(); i++) {
- tasks.get(i).stop();
+ for (int i = 0; i < downloads.size(); i++) {
+ downloads.get(i).stop();
}
final ConditionVariable fileIOFinishedCondition = new ConditionVariable();
fileIOHandler.post(fileIOFinishedCondition::open);
@@ -286,66 +281,46 @@ public final class DownloadManager {
logd("Released");
}
- private Task addTaskForAction(DownloadAction action) {
- Task task = new Task(nextTaskId++, this, downloaderFactory, action, minRetryCount);
- tasks.add(task);
- logd("Task is added", task);
- return task;
+ private Download getDownloadForAction(DownloadAction action) {
+ for (int i = 0; i < downloads.size(); i++) {
+ Download download = downloads.get(i);
+ if (download.action.isSameMedia(action)) {
+ download.addAction(action);
+ logd("Action is added to existing download", download);
+ return download;
+ }
+ }
+ Download download = new Download(this, downloaderFactory, action, minRetryCount);
+ downloads.add(download);
+ logd("Download is added", download);
+ return download;
}
/**
- * Iterates through the task queue and starts any task if all of the following are true:
+ * Iterates through the download queue and starts any download if all of the following are true:
*
*
* - It hasn't started yet.
- *
- There are no preceding conflicting tasks.
- *
- If it's a download task then there are no preceding download tasks on hold and the
- * maximum number of active downloads hasn't been reached.
+ *
- The maximum number of active downloads hasn't been reached.
*
- *
- * If the task is a remove action then preceding conflicting tasks are canceled.
*/
- private void maybeStartTasks() {
+ private void maybeStartDownloads() {
if (!initialized || released) {
return;
}
- boolean skipDownloadActions = downloadsStopped
- || activeDownloadTasks.size() == maxActiveDownloadTasks;
- for (int i = 0; i < tasks.size(); i++) {
- Task task = tasks.get(i);
- if (!task.canStart()) {
+ boolean skipDownloads = downloadsStopped || activeDownloads.size() == maxActiveDownloads;
+ for (int i = 0; i < downloads.size(); i++) {
+ Download download = downloads.get(i);
+ if (!download.canStart()) {
continue;
}
-
- DownloadAction action = task.action;
- boolean isRemoveAction = action.isRemoveAction;
- if (!isRemoveAction && skipDownloadActions) {
- continue;
- }
-
- boolean canStartTask = true;
- for (int j = 0; j < i; j++) {
- Task otherTask = tasks.get(j);
- if (otherTask.action.isSameMedia(action)) {
- if (isRemoveAction) {
- canStartTask = false;
- logd(task + " clashes with " + otherTask);
- otherTask.cancel();
- // Continue loop to cancel any other preceding clashing tasks.
- } else if (otherTask.action.isRemoveAction) {
- canStartTask = false;
- skipDownloadActions = true;
- break;
- }
- }
- }
-
- if (canStartTask) {
- task.start();
+ boolean isRemoveAction = download.action.isRemoveAction;
+ if (isRemoveAction || !skipDownloads) {
+ download.start();
if (!isRemoveAction) {
- activeDownloadTasks.add(task);
- skipDownloadActions = activeDownloadTasks.size() == maxActiveDownloadTasks;
+ activeDownloads.add(download);
+ skipDownloads = activeDownloads.size() == maxActiveDownloads;
}
}
}
@@ -361,30 +336,30 @@ public final class DownloadManager {
}
}
- private void onTaskStateChange(Task task) {
+ private void onDownloadStateChange(Download download) {
if (released) {
return;
}
- boolean stopped = !task.isStarted();
+ boolean stopped = !download.isStarted();
if (stopped) {
- activeDownloadTasks.remove(task);
+ activeDownloads.remove(download);
}
- notifyListenersTaskStateChange(task);
- if (task.isFinished()) {
- tasks.remove(task);
+ notifyListenersDownloadStateChange(download);
+ if (download.isFinished()) {
+ downloads.remove(download);
saveActions();
}
if (stopped) {
- maybeStartTasks();
+ maybeStartDownloads();
maybeNotifyListenersIdle();
}
}
- private void notifyListenersTaskStateChange(Task task) {
- logd("Task state is changed", task);
- TaskState taskState = task.getTaskState();
+ private void notifyListenersDownloadStateChange(Download download) {
+ logd("Download state is changed", download);
+ DownloadState downloadState = download.getDownloadState();
for (Listener listener : listeners) {
- listener.onTaskStateChanged(this, taskState);
+ listener.onDownloadStateChanged(this, downloadState);
}
}
@@ -405,27 +380,27 @@ public final class DownloadManager {
if (released) {
return;
}
- List pendingTasks = new ArrayList<>(tasks);
- tasks.clear();
+ List pendingDownloads = new ArrayList<>(downloads);
+ downloads.clear();
for (DownloadAction action : actions) {
- addTaskForAction(action);
+ getDownloadForAction(action);
}
- logd("Tasks are created.");
+ logd("Downloads are created.");
initialized = true;
for (Listener listener : listeners) {
listener.onInitialized(DownloadManager.this);
}
- if (!pendingTasks.isEmpty()) {
- tasks.addAll(pendingTasks);
+ if (!pendingDownloads.isEmpty()) {
+ downloads.addAll(pendingDownloads);
saveActions();
}
- maybeStartTasks();
- for (int i = 0; i < tasks.size(); i++) {
- Task task = tasks.get(i);
- if (task.state == STATE_QUEUED) {
- // Task did not change out of its initial state, and so its initial state
+ maybeStartDownloads();
+ for (int i = 0; i < downloads.size(); i++) {
+ Download download = downloads.get(i);
+ if (download.state == STATE_QUEUED) {
+ // Download did not change out of its initial state, and so its initial state
// won't have been reported to listeners. Do so now.
- notifyListenersTaskStateChange(task);
+ notifyListenersDownloadStateChange(download);
}
}
});
@@ -436,14 +411,15 @@ public final class DownloadManager {
if (released) {
return;
}
- final DownloadAction[] actions = new DownloadAction[tasks.size()];
- for (int i = 0; i < tasks.size(); i++) {
- actions[i] = tasks.get(i).action;
+ ArrayList actions = new ArrayList<>(downloads.size());
+ for (int i = 0; i < downloads.size(); i++) {
+ actions.addAll(downloads.get(i).actionQueue);
}
+ final DownloadAction[] actionsArray = actions.toArray(new DownloadAction[0]);
fileIOHandler.post(
() -> {
try {
- actionFile.store(actions);
+ actionFile.store(actionsArray);
logd("Actions persisted.");
} catch (IOException e) {
Log.e(TAG, "Persisting actions failed.", e);
@@ -457,39 +433,46 @@ public final class DownloadManager {
}
}
- private static void logd(String message, Task task) {
- logd(message + ": " + task);
+ private static void logd(String message, Download download) {
+ logd(message + ": " + download);
}
- /** Represents state of a task. */
- public static final class TaskState {
+ /** Represents state of a download. */
+ public static final class DownloadState {
/**
- * Task states. One of {@link #STATE_QUEUED}, {@link #STATE_STARTED}, {@link #STATE_COMPLETED},
- * {@link #STATE_CANCELED} or {@link #STATE_FAILED}.
+ * Download states. One of {@link #STATE_QUEUED}, {@link #STATE_STARTED}, {@link
+ * #STATE_COMPLETED} or {@link #STATE_FAILED}.
*
* Transition diagram:
*
*
- * ┌────────┬─────→ canceled
* queued ↔ started ┬→ completed
* └→ failed
*
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
- @IntDef({STATE_QUEUED, STATE_STARTED, STATE_COMPLETED, STATE_CANCELED, STATE_FAILED})
+ @IntDef({STATE_QUEUED, STATE_STARTED, STATE_COMPLETED, STATE_FAILED})
public @interface State {}
- /** The task is waiting to be started. */
+ /** The download is waiting to be started. */
public static final int STATE_QUEUED = 0;
- /** The task is currently started. */
+ /** The download is currently started. */
public static final int STATE_STARTED = 1;
- /** The task completed. */
+ /** The download completed. */
public static final int STATE_COMPLETED = 2;
- /** The task was canceled. */
- public static final int STATE_CANCELED = 3;
- /** The task failed. */
- public static final int STATE_FAILED = 4;
+ /** The download failed. */
+ public static final int STATE_FAILED = 3;
+
+ /** Failure reasons. Either {@link #FAILURE_REASON_NONE} or {@link #FAILURE_REASON_UNKNOWN}. */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({FAILURE_REASON_NONE, FAILURE_REASON_UNKNOWN})
+ public @interface FailureReason {}
+ /** The download isn't failed. */
+ public static final int FAILURE_REASON_NONE = 0;
+ /** The download is failed because of unknown reason. */
+ public static final int FAILURE_REASON_UNKNOWN = 1;
/** Returns the state string for the given state value. */
public static String getStateString(@State int state) {
@@ -500,8 +483,6 @@ public final class DownloadManager {
return "STARTED";
case STATE_COMPLETED:
return "COMPLETED";
- case STATE_CANCELED:
- return "CANCELED";
case STATE_FAILED:
return "FAILED";
default:
@@ -509,97 +490,151 @@ public final class DownloadManager {
}
}
- /** The unique task id. */
- public final int taskId;
+ /** Returns the failure string for the given failure reason value. */
+ public static String getFailureString(@FailureReason int failureReason) {
+ switch (failureReason) {
+ case FAILURE_REASON_NONE:
+ return "NO_REASON";
+ case FAILURE_REASON_UNKNOWN:
+ return "UNKNOWN_REASON";
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ /** The unique content id. */
+ public final String id;
/** The action being executed. */
public final DownloadAction action;
- /** The state of the task. */
+ /** The state of the download. */
public final @State int state;
-
- /**
- * The estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is available
- * or if this is a removal task.
- */
+ /** The estimated download percentage, or {@link C#PERCENTAGE_UNSET} if unavailable. */
public final float downloadPercentage;
/** The total number of downloaded bytes. */
public final long downloadedBytes;
+ /** The total size of the media, or {@link C#LENGTH_UNSET} if unknown. */
+ public final long totalBytes;
+ /** The first time when download entry is created. */
+ public final long startTimeMs;
+ /** The last update time. */
+ public final long updateTimeMs;
- /** If {@link #state} is {@link #STATE_FAILED} then this is the cause, otherwise null. */
- @Nullable public final Throwable error;
+ /**
+ * If {@link #state} is {@link #STATE_FAILED} then this is the cause, otherwise {@link
+ * #FAILURE_REASON_NONE}.
+ */
+ @FailureReason public final int failureReason;
- private TaskState(
- int taskId,
+ private DownloadState(
DownloadAction action,
@State int state,
float downloadPercentage,
long downloadedBytes,
- @Nullable Throwable error) {
- this.taskId = taskId;
+ long totalBytes,
+ @FailureReason int failureReason,
+ long startTimeMs) {
+ Assertions.checkState(
+ failureReason == FAILURE_REASON_NONE ? state != STATE_FAILED : state == STATE_FAILED);
+ this.id = action.id;
this.action = action;
this.state = state;
this.downloadPercentage = downloadPercentage;
this.downloadedBytes = downloadedBytes;
- this.error = error;
+ this.totalBytes = totalBytes;
+ this.failureReason = failureReason;
+ this.startTimeMs = startTimeMs;
+ updateTimeMs = System.currentTimeMillis();
}
}
- private static final class Task implements Runnable {
+ private static final class Download {
/** Target states for the download thread. */
@Documented
@Retention(RetentionPolicy.SOURCE)
- @IntDef({STATE_COMPLETED, STATE_QUEUED, STATE_CANCELED})
+ @IntDef({STATE_QUEUED, STATE_COMPLETED})
public @interface TargetState {}
- private final int id;
+ private final String id;
private final DownloadManager downloadManager;
private final DownloaderFactory downloaderFactory;
- private final DownloadAction action;
private final int minRetryCount;
- /** The current state of the task. */
- @TaskState.State private int state;
+ private final long startTimeMs;
+ private final ArrayDeque actionQueue;
+ private DownloadAction action;
+ /** The current state of the download. */
+ @DownloadState.State private int state;
/**
- * When started, this is the target state that the task will transition to when the download
+ * When started, this is the target state that the download will transition to when the download
* thread stops.
*/
@TargetState private volatile int targetState;
@MonotonicNonNull private Downloader downloader;
- @MonotonicNonNull private Thread thread;
- @MonotonicNonNull private Throwable error;
+ @MonotonicNonNull private DownloadThread downloadThread;
+ @MonotonicNonNull @DownloadState.FailureReason private int failureReason;
- private Task(
- int id,
+ private Download(
DownloadManager downloadManager,
DownloaderFactory downloaderFactory,
DownloadAction action,
int minRetryCount) {
- this.id = id;
+ this.id = action.id;
this.downloadManager = downloadManager;
this.downloaderFactory = downloaderFactory;
this.action = action;
this.minRetryCount = minRetryCount;
+ this.startTimeMs = System.currentTimeMillis();
state = STATE_QUEUED;
targetState = STATE_COMPLETED;
+ actionQueue = new ArrayDeque<>();
+ actionQueue.add(action);
}
- public TaskState getTaskState() {
+ public void addAction(DownloadAction newAction) {
+ Assertions.checkState(action.type.equals(newAction.type));
+ actionQueue.add(newAction);
+ DownloadAction updatedAction = DownloadActionUtil.mergeActions(actionQueue);
+ if (action.equals(updatedAction)) {
+ return;
+ }
+ if (state == STATE_STARTED) {
+ if (targetState == STATE_COMPLETED) {
+ stopDownloadThread();
+ }
+ } else {
+ Assertions.checkState(state == STATE_QUEUED);
+ action = updatedAction;
+ downloadManager.onDownloadStateChange(this);
+ }
+ }
+
+ public DownloadState getDownloadState() {
float downloadPercentage = C.PERCENTAGE_UNSET;
long downloadedBytes = 0;
+ long totalBytes = C.LENGTH_UNSET;
if (downloader != null) {
downloadPercentage = downloader.getDownloadPercentage();
downloadedBytes = downloader.getDownloadedBytes();
+ totalBytes = downloader.getTotalBytes();
}
- return new TaskState(id, action, state, downloadPercentage, downloadedBytes, error);
+ return new DownloadState(
+ action,
+ state,
+ downloadPercentage,
+ downloadedBytes,
+ totalBytes,
+ failureReason,
+ startTimeMs);
}
- /** Returns whether the task is finished. */
+ /** Returns whether the download is finished. */
public boolean isFinished() {
- return state == STATE_FAILED || state == STATE_COMPLETED || state == STATE_CANCELED;
+ return state == STATE_FAILED || state == STATE_COMPLETED;
}
- /** Returns whether the task is started. */
+ /** Returns whether the download is started. */
public boolean isStarted() {
return state == STATE_STARTED;
}
@@ -610,9 +645,9 @@ public final class DownloadManager {
+ ' '
+ (action.isRemoveAction ? "remove" : "download")
+ ' '
- + TaskState.getStateString(state)
+ + DownloadState.getStateString(state)
+ ' '
- + TaskState.getStateString(targetState);
+ + DownloadState.getStateString(targetState);
}
public boolean canStart() {
@@ -622,77 +657,108 @@ public final class DownloadManager {
public void start() {
if (state == STATE_QUEUED) {
state = STATE_STARTED;
+ action = actionQueue.peek();
targetState = STATE_COMPLETED;
- downloadManager.onTaskStateChange(this);
downloader = downloaderFactory.createDownloader(action);
- thread = new Thread(this);
- thread.start();
- }
- }
-
- public void cancel() {
- if (state == STATE_STARTED) {
- stopDownloadThread(STATE_CANCELED);
- } else if (state == STATE_QUEUED) {
- state = STATE_CANCELED;
- downloadManager.handler.post(() -> downloadManager.onTaskStateChange(this));
+ downloadThread =
+ new DownloadThread(
+ this, downloader, action.isRemoveAction, minRetryCount, downloadManager.handler);
+ downloadManager.onDownloadStateChange(this);
}
}
public void stop() {
- if (state == STATE_STARTED && targetState == STATE_COMPLETED) {
- stopDownloadThread(STATE_QUEUED);
+ if (state == STATE_STARTED) {
+ stopDownloadThread();
}
}
// Internal methods running on the main thread.
- private void stopDownloadThread(@TargetState int targetState) {
- this.targetState = targetState;
- Assertions.checkNotNull(downloader).cancel();
- Assertions.checkNotNull(thread).interrupt();
+ private void stopDownloadThread() {
+ this.targetState = DownloadState.STATE_QUEUED;
+ Assertions.checkNotNull(downloadThread).cancel();
}
private void onDownloadThreadStopped(@Nullable Throwable finalError) {
- @TaskState.State int finalState = targetState;
- if (targetState == STATE_COMPLETED && finalError != null) {
- finalState = STATE_FAILED;
- } else {
- finalError = null;
+ state = targetState;
+ failureReason = FAILURE_REASON_NONE;
+ if (targetState == STATE_COMPLETED) {
+ if (finalError != null) {
+ state = STATE_FAILED;
+ failureReason = FAILURE_REASON_UNKNOWN;
+ } else {
+ actionQueue.remove();
+ if (!actionQueue.isEmpty()) {
+ // Don't continue running. Wait to be restarted by maybeStartDownloads().
+ state = STATE_QUEUED;
+ action = actionQueue.peek();
+ }
+ }
}
- state = finalState;
- error = finalError;
- downloadManager.onTaskStateChange(this);
+ downloadManager.onDownloadStateChange(this);
+ }
+ }
+
+ private static class DownloadThread implements Runnable {
+
+ private final Download download;
+ private final Downloader downloader;
+ private final boolean remove;
+ private final int minRetryCount;
+ private final Handler callbackHandler;
+ private final Thread thread;
+ private volatile boolean isCanceled;
+
+ private DownloadThread(
+ Download download,
+ Downloader downloader,
+ boolean remove,
+ int minRetryCount,
+ Handler callbackHandler) {
+ this.download = download;
+ this.downloader = downloader;
+ this.remove = remove;
+ this.minRetryCount = minRetryCount;
+ this.callbackHandler = callbackHandler;
+ thread = new Thread(this);
+ thread.start();
+ }
+
+ public void cancel() {
+ isCanceled = true;
+ downloader.cancel();
+ thread.interrupt();
}
// Methods running on download thread.
@Override
public void run() {
- logd("Task is started", this);
+ logd("Download is started", download);
Throwable error = null;
try {
- if (action.isRemoveAction) {
+ if (remove) {
downloader.remove();
} else {
int errorCount = 0;
long errorPosition = C.LENGTH_UNSET;
- while (targetState == STATE_COMPLETED) {
+ while (!isCanceled) {
try {
downloader.download();
break;
} catch (IOException e) {
- if (targetState == STATE_COMPLETED) {
+ if (!isCanceled) {
long downloadedBytes = downloader.getDownloadedBytes();
if (downloadedBytes != errorPosition) {
- logd("Reset error count. downloadedBytes = " + downloadedBytes, this);
+ logd("Reset error count. downloadedBytes = " + downloadedBytes, download);
errorPosition = downloadedBytes;
errorCount = 0;
}
if (++errorCount > minRetryCount) {
throw e;
}
- logd("Download error. Retry " + errorCount, this);
+ logd("Download error. Retry " + errorCount, download);
Thread.sleep(getRetryDelayMillis(errorCount));
}
}
@@ -702,7 +768,7 @@ public final class DownloadManager {
error = e;
}
final Throwable finalError = error;
- downloadManager.handler.post(() -> onDownloadThreadStopped(finalError));
+ callbackHandler.post(() -> download.onDownloadThreadStopped(isCanceled ? null : finalError));
}
private int getRetryDelayMillis(int errorCount) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java
index cfca8ede79..d49b33d2ae 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java
@@ -24,7 +24,7 @@ import android.os.IBinder;
import android.os.Looper;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
-import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
+import com.google.android.exoplayer2.offline.DownloadManager.DownloadState;
import com.google.android.exoplayer2.scheduler.Requirements;
import com.google.android.exoplayer2.scheduler.RequirementsWatcher;
import com.google.android.exoplayer2.scheduler.Scheduler;
@@ -71,9 +71,9 @@ public abstract class DownloadService extends Service {
private static final String TAG = "DownloadService";
private static final boolean DEBUG = false;
- // Keep the requirements helper for each DownloadService as long as there are tasks (and the
- // process is running). This allows tasks to resume when there's no scheduler. It may also allow
- // tasks the resume more quickly than when relying on the scheduler alone.
+ // Keep the requirements helper for each DownloadService as long as there are downloads (and the
+ // process is running). This allows downloads to resume when there's no scheduler. It may also
+ // allow downloads the resume more quickly than when relying on the scheduler alone.
private static final HashMap, RequirementsHelper>
requirementsHelpers = new HashMap<>();
private static final Requirements DEFAULT_REQUIREMENTS =
@@ -99,7 +99,7 @@ public abstract class DownloadService extends Service {
* If {@code foregroundNotificationId} isn't {@link #FOREGROUND_NOTIFICATION_ID_NONE} (value
* {@value #FOREGROUND_NOTIFICATION_ID_NONE}) the service runs in the foreground with {@link
* #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}. In that case {@link
- * #getForegroundNotification(TaskState[])} should be overridden in the subclass.
+ * #getForegroundNotification(DownloadState[])} should be overridden in the subclass.
*
* @param foregroundNotificationId The notification id for the foreground notification, or {@link
* #FOREGROUND_NOTIFICATION_ID_NONE} (value {@value #FOREGROUND_NOTIFICATION_ID_NONE})
@@ -110,7 +110,7 @@ public abstract class DownloadService extends Service {
/**
* Creates a DownloadService which will run in the foreground. {@link
- * #getForegroundNotification(TaskState[])} should be overridden in the subclass.
+ * #getForegroundNotification(DownloadState[])} should be overridden in the subclass.
*
* @param foregroundNotificationId The notification id for the foreground notification, must not
* be 0.
@@ -128,7 +128,7 @@ public abstract class DownloadService extends Service {
/**
* Creates a DownloadService which will run in the foreground. {@link
- * #getForegroundNotification(TaskState[])} should be overridden in the subclass.
+ * #getForegroundNotification(DownloadState[])} should be overridden in the subclass.
*
* @param foregroundNotificationId The notification id for the foreground notification. Must not
* be 0.
@@ -338,29 +338,29 @@ public abstract class DownloadService extends Service {
*
*
Returns a notification to be displayed when this service running in the foreground.
*
- *
This method is called when there is a task state change and periodically while there are
- * active tasks. The periodic update interval can be set using {@link #DownloadService(int,
+ *
This method is called when there is a download state change and periodically while there are
+ * active downloads. The periodic update interval can be set using {@link #DownloadService(int,
* long)}.
*
*
On API level 26 and above, this method may also be called just before the service stops,
- * with an empty {@code taskStates} array. The returned notification is used to satisfy system
+ * with an empty {@code downloadStates} array. The returned notification is used to satisfy system
* requirements for foreground services.
*
- * @param taskStates The states of all current tasks.
+ * @param downloadStates The states of all current downloads.
* @return The foreground notification to display.
*/
- protected Notification getForegroundNotification(TaskState[] taskStates) {
+ protected Notification getForegroundNotification(DownloadState[] downloadStates) {
throw new IllegalStateException(
getClass().getName()
+ " is started in the foreground but getForegroundNotification() is not implemented.");
}
/**
- * Called when the state of a task changes.
+ * Called when the state of a download changes.
*
- * @param taskState The state of the task.
+ * @param downloadState The state of the download.
*/
- protected void onTaskStateChanged(TaskState taskState) {
+ protected void onDownloadStateChanged(DownloadState downloadState) {
// Do nothing.
}
@@ -428,10 +428,11 @@ public abstract class DownloadService extends Service {
}
@Override
- public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) {
- DownloadService.this.onTaskStateChanged(taskState);
+ public void onDownloadStateChanged(
+ DownloadManager downloadManager, DownloadState downloadState) {
+ DownloadService.this.onDownloadStateChanged(downloadState);
if (foregroundNotificationUpdater != null) {
- if (taskState.state == TaskState.STATE_STARTED) {
+ if (downloadState.state == DownloadState.STATE_STARTED) {
foregroundNotificationUpdater.startPeriodicUpdates();
} else {
foregroundNotificationUpdater.update();
@@ -471,8 +472,8 @@ public abstract class DownloadService extends Service {
}
public void update() {
- TaskState[] taskStates = downloadManager.getAllTaskStates();
- startForeground(notificationId, getForegroundNotification(taskStates));
+ DownloadState[] downloadStates = downloadManager.getAllDownloadStates();
+ startForeground(notificationId, getForegroundNotification(downloadStates));
notificationDisplayed = true;
if (periodicUpdatesStarted) {
handler.removeCallbacks(this);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/FilteringManifestParser.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/FilteringManifestParser.java
index c32cdf7126..c25e5099cf 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/FilteringManifestParser.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/FilteringManifestParser.java
@@ -16,22 +16,27 @@
package com.google.android.exoplayer2.offline;
import android.net.Uri;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.upstream.ParsingLoadable.Parser;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
-/** A manifest parser that includes only the streams identified by the given stream keys. */
+/**
+ * A manifest parser that includes only the streams identified by the given stream keys.
+ *
+ * @param The {@link FilterableManifest} type.
+ */
public final class FilteringManifestParser> implements Parser {
- private final Parser parser;
- private final List streamKeys;
+ private final Parser extends T> parser;
+ @Nullable private final List streamKeys;
/**
* @param parser A parser for the manifest that will be filtered.
* @param streamKeys The stream keys. If null or empty then filtering will not occur.
*/
- public FilteringManifestParser(Parser parser, List streamKeys) {
+ public FilteringManifestParser(Parser extends T> parser, @Nullable List streamKeys) {
this.parser = parser;
this.streamKeys = streamKeys;
}
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 70587694c4..2ec14368ca 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,19 +17,35 @@ 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;
-import java.util.Collections;
-import java.util.List;
/** A {@link DownloadHelper} for progressive streams. */
public final class ProgressiveDownloadHelper extends DownloadHelper {
+ /**
+ * Creates download helper for progressive streams.
+ *
+ * @param uri The stream {@link Uri}.
+ */
public ProgressiveDownloadHelper(Uri uri) {
- this(uri, null);
+ this(uri, /* cacheKey= */ null);
}
- public ProgressiveDownloadHelper(Uri uri, @Nullable String customCacheKey) {
- super(DownloadAction.TYPE_PROGRESSIVE, uri, customCacheKey);
+ /**
+ * Creates download helper for progressive streams.
+ *
+ * @param uri The stream {@link Uri}.
+ * @param cacheKey An optional cache key.
+ */
+ public ProgressiveDownloadHelper(Uri uri, @Nullable String cacheKey) {
+ super(
+ DownloadAction.TYPE_PROGRESSIVE,
+ uri,
+ cacheKey,
+ DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
+ (handler, videoListener, audioListener, metadata, text, drm) -> new Renderer[0],
+ /* drmSessionManager= */ null);
}
@Override
@@ -43,7 +59,8 @@ public final class ProgressiveDownloadHelper extends DownloadHelper {
}
@Override
- protected List toStreamKeys(List trackKeys) {
- return Collections.emptyList();
+ protected StreamKey toStreamKey(
+ int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) {
+ return new StreamKey(periodIndex, trackGroupIndex, trackIndexInTrackGroup);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java
index 838073cd99..1caeaca61e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java
@@ -19,8 +19,11 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
/**
- * Identifies a given track by the index of the containing period, the index of the containing group
- * within the period, and the index of the track within the group.
+ * A key for a subset of media which can be separately loaded (a "stream").
+ *
+ * The stream key consists of a period index, a group index within the period and a track index
+ * within the group. The interpretation of these indices depends on the type of media for which the
+ * stream key is used.
*/
public final class StreamKey implements Comparable {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/TrackKey.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/TrackKey.java
deleted file mode 100644
index f6a411c3a1..0000000000
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/TrackKey.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.android.exoplayer2.offline;
-
-/**
- * Identifies a given track by the index of the containing period, the index of the containing group
- * within the period, and the index of the track within the group.
- */
-public final class TrackKey {
-
- /** The period index. */
- public final int periodIndex;
- /** The group index. */
- public final int groupIndex;
- /** The track index. */
- public final int trackIndex;
-
- /**
- * @param periodIndex The period index.
- * @param groupIndex The group index.
- * @param trackIndex The track index.
- */
- public TrackKey(int periodIndex, int groupIndex, int trackIndex) {
- this.periodIndex = periodIndex;
- this.groupIndex = groupIndex;
- this.trackIndex = trackIndex;
- }
-}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
index 26667e641f..e3114298f3 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
@@ -826,7 +826,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource();
this.uid = new Object();
}
@@ -951,10 +951,18 @@ public class ConcatenatingMediaSource extends CompositeMediaSourceThis method is only called after the period has been prepared.
+ *
+ * @param trackSelection The {@link TrackSelection} describing the tracks for which stream keys
+ * are requested.
+ * @return The corresponding {@link StreamKey stream keys} for the selected tracks, or an empty
+ * list if filtering is not possible and the entire media needs to be loaded to play the
+ * selected tracks.
+ */
+ default List getStreamKeys(TrackSelection trackSelection) {
+ return Collections.emptyList();
+ }
+
/**
* Performs a track selection.
*
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
index 2e868077a5..b39f467968 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
@@ -68,6 +68,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
private static final String ATTR_END = "end";
private static final String ATTR_STYLE = "style";
private static final String ATTR_REGION = "region";
+ private static final String ATTR_IMAGE = "backgroundImage";
private static final Pattern CLOCK_TIME =
Pattern.compile("^([0-9][0-9]+):([0-9][0-9]):([0-9][0-9])"
@@ -77,6 +78,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
private static final Pattern FONT_SIZE = Pattern.compile("^(([0-9]*.)?[0-9]+)(px|em|%)$");
private static final Pattern PERCENTAGE_COORDINATES =
Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$");
+ private static final Pattern PIXEL_COORDINATES =
+ Pattern.compile("^(\\d+\\.?\\d*?)px (\\d+\\.?\\d*?)px$");
private static final Pattern CELL_RESOLUTION = Pattern.compile("^(\\d+) (\\d+)$");
private static final int DEFAULT_FRAME_RATE = 30;
@@ -105,6 +108,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
XmlPullParser xmlParser = xmlParserFactory.newPullParser();
Map globalStyles = new HashMap<>();
Map regionMap = new HashMap<>();
+ Map imageMap = new HashMap<>();
regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion(null));
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length);
xmlParser.setInput(inputStream, null);
@@ -114,6 +118,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
int eventType = xmlParser.getEventType();
FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE;
CellResolution cellResolution = DEFAULT_CELL_RESOLUTION;
+ TtsExtent ttsExtent = null;
while (eventType != XmlPullParser.END_DOCUMENT) {
TtmlNode parent = nodeStack.peek();
if (unsupportedNodeDepth == 0) {
@@ -122,12 +127,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
if (TtmlNode.TAG_TT.equals(name)) {
frameAndTickRate = parseFrameAndTickRates(xmlParser);
cellResolution = parseCellResolution(xmlParser, DEFAULT_CELL_RESOLUTION);
+ ttsExtent = parseTtsExtent(xmlParser);
}
if (!isSupportedTag(name)) {
Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName());
unsupportedNodeDepth++;
} else if (TtmlNode.TAG_HEAD.equals(name)) {
- parseHeader(xmlParser, globalStyles, regionMap, cellResolution);
+ parseHeader(xmlParser, globalStyles, cellResolution, ttsExtent, regionMap, imageMap);
} else {
try {
TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate);
@@ -145,7 +151,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
parent.addChild(TtmlNode.buildTextNode(xmlParser.getText()));
} else if (eventType == XmlPullParser.END_TAG) {
if (xmlParser.getName().equals(TtmlNode.TAG_TT)) {
- ttmlSubtitle = new TtmlSubtitle(nodeStack.peek(), globalStyles, regionMap);
+ ttmlSubtitle = new TtmlSubtitle(nodeStack.peek(), globalStyles, regionMap, imageMap);
}
nodeStack.pop();
}
@@ -226,11 +232,34 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
}
}
+ private TtsExtent parseTtsExtent(XmlPullParser xmlParser) {
+ String ttsExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT);
+ if (ttsExtent == null) {
+ return null;
+ }
+
+ Matcher extentMatcher = PIXEL_COORDINATES.matcher(ttsExtent);
+ if (!extentMatcher.matches()) {
+ Log.w(TAG, "Ignoring non-pixel tts extent: " + ttsExtent);
+ return null;
+ }
+ try {
+ int width = Integer.parseInt(extentMatcher.group(1));
+ int height = Integer.parseInt(extentMatcher.group(2));
+ return new TtsExtent(width, height);
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Ignoring malformed tts extent: " + ttsExtent);
+ return null;
+ }
+ }
+
private Map parseHeader(
XmlPullParser xmlParser,
Map globalStyles,
+ CellResolution cellResolution,
+ TtsExtent ttsExtent,
Map globalRegions,
- CellResolution cellResolution)
+ Map imageMap)
throws IOException, XmlPullParserException {
do {
xmlParser.next();
@@ -246,23 +275,41 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
globalStyles.put(style.getId(), style);
}
} else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) {
- TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser, cellResolution);
+ TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser, cellResolution, ttsExtent);
if (ttmlRegion != null) {
globalRegions.put(ttmlRegion.id, ttmlRegion);
}
+ } else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_METADATA)) {
+ parseMetadata(xmlParser, imageMap);
}
} while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_HEAD));
return globalStyles;
}
+ private void parseMetadata(XmlPullParser xmlParser, Map imageMap)
+ throws IOException, XmlPullParserException {
+ do {
+ xmlParser.next();
+ if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_IMAGE)) {
+ String id = XmlPullParserUtil.getAttributeValue(xmlParser, "id");
+ if (id != null) {
+ String encodedBitmapData = xmlParser.nextText();
+ imageMap.put(id, encodedBitmapData);
+ }
+ }
+ } while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_METADATA));
+ }
+
/**
* Parses a region declaration.
*
- * If the region defines an origin and extent, it is required that they're defined as
- * percentages of the viewport. Region declarations that define origin and extent in other formats
- * are unsupported, and null is returned.
+ *
Supports both percentage and pixel defined regions. In case of pixel defined regions the
+ * passed {@code ttsExtent} is used as a reference window to convert the pixel values to
+ * fractions. In case of missing tts:extent the pixel defined regions can't be parsed, and null is
+ * returned.
*/
- private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser, CellResolution cellResolution) {
+ private TtmlRegion parseRegionAttributes(
+ XmlPullParser xmlParser, CellResolution cellResolution, TtsExtent ttsExtent) {
String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID);
if (regionId == null) {
return null;
@@ -270,13 +317,30 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
float position;
float line;
+
String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN);
if (regionOrigin != null) {
- Matcher originMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin);
- if (originMatcher.matches()) {
+ Matcher originPercentageMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin);
+ Matcher originPixelMatcher = PIXEL_COORDINATES.matcher(regionOrigin);
+ if (originPercentageMatcher.matches()) {
try {
- position = Float.parseFloat(originMatcher.group(1)) / 100f;
- line = Float.parseFloat(originMatcher.group(2)) / 100f;
+ position = Float.parseFloat(originPercentageMatcher.group(1)) / 100f;
+ line = Float.parseFloat(originPercentageMatcher.group(2)) / 100f;
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Ignoring region with malformed origin: " + regionOrigin);
+ return null;
+ }
+ } else if (originPixelMatcher.matches()) {
+ if (ttsExtent == null) {
+ Log.w(TAG, "Ignoring region with missing tts:extent: " + regionOrigin);
+ return null;
+ }
+ try {
+ int width = Integer.parseInt(originPixelMatcher.group(1));
+ int height = Integer.parseInt(originPixelMatcher.group(2));
+ // Convert pixel values to fractions.
+ position = width / (float) ttsExtent.width;
+ line = height / (float) ttsExtent.height;
} catch (NumberFormatException e) {
Log.w(TAG, "Ignoring region with malformed origin: " + regionOrigin);
return null;
@@ -299,11 +363,27 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
float height;
String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT);
if (regionExtent != null) {
- Matcher extentMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent);
- if (extentMatcher.matches()) {
+ Matcher extentPercentageMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent);
+ Matcher extentPixelMatcher = PIXEL_COORDINATES.matcher(regionExtent);
+ if (extentPercentageMatcher.matches()) {
try {
- width = Float.parseFloat(extentMatcher.group(1)) / 100f;
- height = Float.parseFloat(extentMatcher.group(2)) / 100f;
+ width = Float.parseFloat(extentPercentageMatcher.group(1)) / 100f;
+ height = Float.parseFloat(extentPercentageMatcher.group(2)) / 100f;
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Ignoring region with malformed extent: " + regionOrigin);
+ return null;
+ }
+ } else if (extentPixelMatcher.matches()) {
+ if (ttsExtent == null) {
+ Log.w(TAG, "Ignoring region with missing tts:extent: " + regionOrigin);
+ return null;
+ }
+ try {
+ int extentWidth = Integer.parseInt(extentPixelMatcher.group(1));
+ int extentHeight = Integer.parseInt(extentPixelMatcher.group(2));
+ // Convert pixel values to fractions.
+ width = extentWidth / (float) ttsExtent.width;
+ height = extentHeight / (float) ttsExtent.height;
} catch (NumberFormatException e) {
Log.w(TAG, "Ignoring region with malformed extent: " + regionOrigin);
return null;
@@ -457,6 +537,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
long startTime = C.TIME_UNSET;
long endTime = C.TIME_UNSET;
String regionId = TtmlNode.ANONYMOUS_REGION_ID;
+ String imageId = null;
String[] styleIds = null;
int attributeCount = parser.getAttributeCount();
TtmlStyle style = parseStyleAttributes(parser, null);
@@ -487,6 +568,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
regionId = value;
}
break;
+ case ATTR_IMAGE:
+ // Parse URI reference only if refers to an element in the same document (it must start
+ // with '#'). Resolving URIs from external sources is not supported.
+ if (value.startsWith("#")) {
+ imageId = value.substring(1);
+ }
+ break;
default:
// Do nothing.
break;
@@ -509,7 +597,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
endTime = parent.endTimeUs;
}
}
- return TtmlNode.buildNode(parser.getName(), startTime, endTime, style, styleIds, regionId);
+ return TtmlNode.buildNode(
+ parser.getName(), startTime, endTime, style, styleIds, regionId, imageId);
}
private static boolean isSupportedTag(String tag) {
@@ -525,9 +614,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
|| tag.equals(TtmlNode.TAG_LAYOUT)
|| tag.equals(TtmlNode.TAG_REGION)
|| tag.equals(TtmlNode.TAG_METADATA)
- || tag.equals(TtmlNode.TAG_SMPTE_IMAGE)
- || tag.equals(TtmlNode.TAG_SMPTE_DATA)
- || tag.equals(TtmlNode.TAG_SMPTE_INFORMATION);
+ || tag.equals(TtmlNode.TAG_IMAGE)
+ || tag.equals(TtmlNode.TAG_DATA)
+ || tag.equals(TtmlNode.TAG_INFORMATION);
}
private static void parseFontSize(String expression, TtmlStyle out) throws
@@ -651,4 +740,15 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
this.rows = rows;
}
}
+
+ /** Represents the tts:extent for a TTML file. */
+ private static final class TtsExtent {
+ final int width;
+ final int height;
+
+ TtsExtent(int width, int height) {
+ this.width = width;
+ this.height = height;
+ }
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java
index c8b9a59de4..020bbe201b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java
@@ -15,7 +15,12 @@
*/
package com.google.android.exoplayer2.text.ttml;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.support.annotation.Nullable;
import android.text.SpannableStringBuilder;
+import android.util.Base64;
+import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.util.Assertions;
@@ -44,9 +49,9 @@ import java.util.TreeSet;
public static final String TAG_LAYOUT = "layout";
public static final String TAG_REGION = "region";
public static final String TAG_METADATA = "metadata";
- public static final String TAG_SMPTE_IMAGE = "smpte:image";
- public static final String TAG_SMPTE_DATA = "smpte:data";
- public static final String TAG_SMPTE_INFORMATION = "smpte:information";
+ public static final String TAG_IMAGE = "image";
+ public static final String TAG_DATA = "data";
+ public static final String TAG_INFORMATION = "information";
public static final String ANONYMOUS_REGION_ID = "";
public static final String ATTR_ID = "id";
@@ -75,34 +80,57 @@ import java.util.TreeSet;
public static final String START = "start";
public static final String END = "end";
- public final String tag;
- public final String text;
+ @Nullable public final String tag;
+ @Nullable public final String text;
public final boolean isTextNode;
public final long startTimeUs;
public final long endTimeUs;
- public final TtmlStyle style;
+ @Nullable public final TtmlStyle style;
+ @Nullable private final String[] styleIds;
public final String regionId;
+ @Nullable public final String imageId;
- private final String[] styleIds;
private final HashMap nodeStartsByRegion;
private final HashMap nodeEndsByRegion;
private List children;
public static TtmlNode buildTextNode(String text) {
- return new TtmlNode(null, TtmlRenderUtil.applyTextElementSpacePolicy(text), C.TIME_UNSET,
- C.TIME_UNSET, null, null, ANONYMOUS_REGION_ID);
+ return new TtmlNode(
+ /* tag= */ null,
+ TtmlRenderUtil.applyTextElementSpacePolicy(text),
+ /* startTimeUs= */ C.TIME_UNSET,
+ /* endTimeUs= */ C.TIME_UNSET,
+ /* style= */ null,
+ /* styleIds= */ null,
+ ANONYMOUS_REGION_ID,
+ /* imageId= */ null);
}
- public static TtmlNode buildNode(String tag, long startTimeUs, long endTimeUs,
- TtmlStyle style, String[] styleIds, String regionId) {
- return new TtmlNode(tag, null, startTimeUs, endTimeUs, style, styleIds, regionId);
+ public static TtmlNode buildNode(
+ @Nullable String tag,
+ long startTimeUs,
+ long endTimeUs,
+ @Nullable TtmlStyle style,
+ @Nullable String[] styleIds,
+ String regionId,
+ @Nullable String imageId) {
+ return new TtmlNode(
+ tag, /* text= */ null, startTimeUs, endTimeUs, style, styleIds, regionId, imageId);
}
- private TtmlNode(String tag, String text, long startTimeUs, long endTimeUs,
- TtmlStyle style, String[] styleIds, String regionId) {
+ private TtmlNode(
+ @Nullable String tag,
+ @Nullable String text,
+ long startTimeUs,
+ long endTimeUs,
+ @Nullable TtmlStyle style,
+ @Nullable String[] styleIds,
+ String regionId,
+ @Nullable String imageId) {
this.tag = tag;
this.text = text;
+ this.imageId = imageId;
this.style = style;
this.styleIds = styleIds;
this.isTextNode = text != null;
@@ -151,7 +179,8 @@ import java.util.TreeSet;
private void getEventTimes(TreeSet out, boolean descendsPNode) {
boolean isPNode = TAG_P.equals(tag);
- if (descendsPNode || isPNode) {
+ boolean isDivNode = TAG_DIV.equals(tag);
+ if (descendsPNode || isPNode || (isDivNode && imageId != null)) {
if (startTimeUs != C.TIME_UNSET) {
out.add(startTimeUs);
}
@@ -171,13 +200,46 @@ import java.util.TreeSet;
return styleIds;
}
- public List getCues(long timeUs, Map globalStyles,
- Map regionMap) {
- TreeMap regionOutputs = new TreeMap<>();
- traverseForText(timeUs, false, regionId, regionOutputs);
- traverseForStyle(timeUs, globalStyles, regionOutputs);
+ public List getCues(
+ long timeUs,
+ Map globalStyles,
+ Map regionMap,
+ Map imageMap) {
+
+ List> regionImageOutputs = new ArrayList<>();
+ traverseForImage(timeUs, regionId, regionImageOutputs);
+
+ TreeMap regionTextOutputs = new TreeMap<>();
+ traverseForText(timeUs, false, regionId, regionTextOutputs);
+ traverseForStyle(timeUs, globalStyles, regionTextOutputs);
+
List cues = new ArrayList<>();
- for (Entry entry : regionOutputs.entrySet()) {
+
+ // Create image based cues.
+ for (Pair regionImagePair : regionImageOutputs) {
+ String encodedBitmapData = imageMap.get(regionImagePair.second);
+ if (encodedBitmapData == null) {
+ // Image reference points to an invalid image. Do nothing.
+ continue;
+ }
+
+ byte[] bitmapData = Base64.decode(encodedBitmapData, Base64.DEFAULT);
+ Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, /* offset= */ 0, bitmapData.length);
+ TtmlRegion region = regionMap.get(regionImagePair.first);
+
+ cues.add(
+ new Cue(
+ bitmap,
+ region.position,
+ Cue.ANCHOR_TYPE_MIDDLE,
+ region.line,
+ region.lineAnchor,
+ region.width,
+ /* height= */ Cue.DIMEN_UNSET));
+ }
+
+ // Create text based cues.
+ for (Entry entry : regionTextOutputs.entrySet()) {
TtmlRegion region = regionMap.get(entry.getKey());
cues.add(
new Cue(
@@ -192,9 +254,22 @@ import java.util.TreeSet;
region.textSizeType,
region.textSize));
}
+
return cues;
}
+ private void traverseForImage(
+ long timeUs, String inheritedRegion, List> regionImageList) {
+ String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId;
+ if (isActive(timeUs) && TAG_DIV.equals(tag) && imageId != null) {
+ regionImageList.add(new Pair<>(resolvedRegionId, imageId));
+ return;
+ }
+ for (int i = 0; i < getChildCount(); ++i) {
+ getChild(i).traverseForImage(timeUs, resolvedRegionId, regionImageList);
+ }
+ }
+
private void traverseForText(
long timeUs,
boolean descendsPNode,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java
index 2ac1427e91..1779d9890a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java
@@ -33,11 +33,16 @@ import java.util.Map;
private final long[] eventTimesUs;
private final Map globalStyles;
private final Map regionMap;
+ private final Map imageMap;
- public TtmlSubtitle(TtmlNode root, Map globalStyles,
- Map regionMap) {
+ public TtmlSubtitle(
+ TtmlNode root,
+ Map globalStyles,
+ Map regionMap,
+ Map imageMap) {
this.root = root;
this.regionMap = regionMap;
+ this.imageMap = imageMap;
this.globalStyles =
globalStyles != null ? Collections.unmodifiableMap(globalStyles) : Collections.emptyMap();
this.eventTimesUs = root.getEventTimesUs();
@@ -66,7 +71,7 @@ import java.util.Map;
@Override
public List getCues(long timeUs) {
- return root.getCues(timeUs, globalStyles, regionMap);
+ return root.getCues(timeUs, globalStyles, regionMap, imageMap);
}
@VisibleForTesting
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
index 66b49555ef..b39a5d19f0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
@@ -227,8 +227,36 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
}
@Override
- public AdaptiveTrackSelection createTrackSelection(
- TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks) {
+ public @NullableType TrackSelection[] createTrackSelections(
+ @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
+ TrackSelection[] selections = new TrackSelection[definitions.length];
+ AdaptiveTrackSelection adaptiveSelection = null;
+ int totalFixedBandwidth = 0;
+ for (int i = 0; i < definitions.length; i++) {
+ Definition definition = definitions[i];
+ if (definition == null) {
+ continue;
+ }
+ if (definition.tracks.length > 1) {
+ adaptiveSelection =
+ createAdaptiveTrackSelection(definition.group, bandwidthMeter, definition.tracks);
+ selections[i] = adaptiveSelection;
+ } else {
+ selections[i] = new FixedTrackSelection(definition.group, definition.tracks[0]);
+ int trackBitrate = definition.group.getFormat(definition.tracks[0]).bitrate;
+ if (trackBitrate != Format.NO_VALUE) {
+ totalFixedBandwidth += trackBitrate;
+ }
+ }
+ }
+ if (blockFixedTrackSelectionBandwidth && adaptiveSelection != null) {
+ adaptiveSelection.experimental_setNonAllocatableBandwidth(totalFixedBandwidth);
+ }
+ return selections;
+ }
+
+ private AdaptiveTrackSelection createAdaptiveTrackSelection(
+ TrackGroup group, BandwidthMeter bandwidthMeter, int[] tracks) {
if (this.bandwidthMeter != null) {
bandwidthMeter = this.bandwidthMeter;
}
@@ -246,34 +274,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
adaptiveTrackSelection.experimental_setTrackBitrateEstimator(trackBitrateEstimator);
return adaptiveTrackSelection;
}
-
- @Override
- public @NullableType TrackSelection[] createTrackSelections(
- @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
- TrackSelection[] selections = new TrackSelection[definitions.length];
- AdaptiveTrackSelection adaptiveSelection = null;
- int totalFixedBandwidth = 0;
- for (int i = 0; i < definitions.length; i++) {
- Definition definition = definitions[i];
- if (definition == null) {
- continue;
- }
- if (definition.tracks.length > 1) {
- selections[i] = createTrackSelection(definition.group, bandwidthMeter, definition.tracks);
- adaptiveSelection = (AdaptiveTrackSelection) selections[i];
- } else {
- selections[i] = new FixedTrackSelection(definition.group, definition.tracks[0]);
- int trackBitrate = definition.group.getFormat(definition.tracks[0]).bitrate;
- if (trackBitrate != Format.NO_VALUE) {
- totalFixedBandwidth += trackBitrate;
- }
- }
- }
- if (blockFixedTrackSelectionBandwidth && adaptiveSelection != null) {
- adaptiveSelection.experimental_setNonAllocatableBandwidth(totalFixedBandwidth);
- }
- return selections;
- }
}
public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java
index 6239dd04ad..5c8350cb1d 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java
@@ -24,12 +24,14 @@ import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
+import com.google.android.exoplayer2.trackselection.TrackSelection.Definition;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.PriorityTaskManager;
import java.util.List;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
/**
* Builder for a {@link TrackSelection.Factory} and {@link LoadControl} that implement buffer size
@@ -273,19 +275,22 @@ public final class BufferSizeAdaptationBuilder {
TrackSelection.Factory trackSelectionFactory =
new TrackSelection.Factory() {
@Override
- public TrackSelection createTrackSelection(
- TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks) {
- return new BufferSizeAdaptiveTrackSelection(
- group,
- tracks,
- bandwidthMeter,
- minBufferMs,
- maxBufferMs,
- hysteresisBufferMs,
- startUpBandwidthFraction,
- startUpMinBufferForQualityIncreaseMs,
- dynamicFormatFilter,
- clock);
+ public @NullableType TrackSelection[] createTrackSelections(
+ @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
+ return TrackSelectionUtil.createTrackSelectionsForDefinitions(
+ definitions,
+ definition ->
+ new BufferSizeAdaptiveTrackSelection(
+ definition.group,
+ definition.tracks,
+ bandwidthMeter,
+ minBufferMs,
+ maxBufferMs,
+ hysteresisBufferMs,
+ startUpBandwidthFraction,
+ startUpMinBufferForQualityIncreaseMs,
+ dynamicFormatFilter,
+ clock));
}
};
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java
index 7755e437ce..79b5d93dc7 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java
@@ -21,8 +21,8 @@ import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
-import com.google.android.exoplayer2.util.Assertions;
import java.util.List;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
/**
* A {@link TrackSelection} consisting of a single track.
@@ -56,10 +56,12 @@ public final class FixedTrackSelection extends BaseTrackSelection {
}
@Override
- public FixedTrackSelection createTrackSelection(
- TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks) {
- Assertions.checkArgument(tracks.length == 1);
- return new FixedTrackSelection(group, tracks[0], reason, data);
+ public @NullableType TrackSelection[] createTrackSelections(
+ @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
+ return TrackSelectionUtil.createTrackSelectionsForDefinitions(
+ definitions,
+ definition ->
+ new FixedTrackSelection(definition.group, definition.tracks[0], reason, data));
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java
index e3c643670b..217a16e4a6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java
@@ -24,6 +24,7 @@ import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import java.util.List;
import java.util.Random;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
/**
* A {@link TrackSelection} whose selected track is updated randomly.
@@ -49,9 +50,11 @@ public final class RandomTrackSelection extends BaseTrackSelection {
}
@Override
- public RandomTrackSelection createTrackSelection(
- TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks) {
- return new RandomTrackSelection(group, tracks, random);
+ public @NullableType TrackSelection[] createTrackSelections(
+ @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
+ return TrackSelectionUtil.createTrackSelectionsForDefinitions(
+ definitions,
+ definition -> new RandomTrackSelection(definition.group, definition.tracks, random));
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java
index 13e823da29..251c0ac76b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java
@@ -21,8 +21,8 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
+import com.google.android.exoplayer2.trackselection.TrackSelectionUtil.AdaptiveTrackSelectionFactory;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
-import com.google.android.exoplayer2.util.Assertions;
import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
@@ -61,42 +61,31 @@ public interface TrackSelection {
interface Factory {
/**
- * Creates a new selection.
- *
- * @param group The {@link TrackGroup}. Must not be null.
- * @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks.
- * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
- * null or empty. May be in any order.
- * @return The created selection.
+ * @deprecated Implement {@link #createTrackSelections(Definition[], BandwidthMeter)} instead.
+ * Calling {@link TrackSelectionUtil#createTrackSelectionsForDefinitions(Definition[],
+ * AdaptiveTrackSelectionFactory)} helps to create a single adaptive track selection in the
+ * same way as using this deprecated method.
*/
- TrackSelection createTrackSelection(
- TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks);
+ @Deprecated
+ default TrackSelection createTrackSelection(
+ TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks) {
+ throw new UnsupportedOperationException();
+ }
/**
* Creates a new selection for each {@link Definition}.
*
* @param definitions A {@link Definition} array. May include null values.
* @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks.
- * @return The created selections. For null entries in {@code definitions} returns null values.
+ * @return The created selections. Must have the same length as {@code definitions} and may
+ * include null values.
*/
+ @SuppressWarnings("deprecation")
default @NullableType TrackSelection[] createTrackSelections(
@NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
- TrackSelection[] selections = new TrackSelection[definitions.length];
- boolean createdAdaptiveTrackSelection = false;
- for (int i = 0; i < definitions.length; i++) {
- Definition definition = definitions[i];
- if (definition == null) {
- continue;
- }
- if (definition.tracks.length > 1) {
- Assertions.checkState(!createdAdaptiveTrackSelection);
- createdAdaptiveTrackSelection = true;
- selections[i] = createTrackSelection(definition.group, bandwidthMeter, definition.tracks);
- } else {
- selections[i] = new FixedTrackSelection(definition.group, definition.tracks[0]);
- }
- }
- return selections;
+ return TrackSelectionUtil.createTrackSelectionsForDefinitions(
+ definitions,
+ definition -> createTrackSelection(definition.group, bandwidthMeter, definition.tracks));
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java
index 947f64be2c..7800495a62 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java
@@ -22,15 +22,59 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.source.chunk.MediaChunkListIterator;
+import com.google.android.exoplayer2.trackselection.TrackSelection.Definition;
import com.google.android.exoplayer2.util.Assertions;
import java.util.Arrays;
import java.util.List;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Track selection related utility methods. */
public final class TrackSelectionUtil {
private TrackSelectionUtil() {}
+ /** Functional interface to create a single adaptive track selection. */
+ public interface AdaptiveTrackSelectionFactory {
+
+ /**
+ * Creates an adaptive track selection for the provided track selection definition.
+ *
+ * @param trackSelectionDefinition A {@link Definition} for the track selection.
+ * @return The created track selection.
+ */
+ TrackSelection createAdaptiveTrackSelection(Definition trackSelectionDefinition);
+ }
+
+ /**
+ * Creates track selections for an array of track selection definitions, with at most one
+ * multi-track adaptive selection.
+ *
+ * @param definitions The list of track selection {@link Definition definitions}. May include null
+ * values.
+ * @param adaptiveTrackSelectionFactory A factory for the multi-track adaptive track selection.
+ * @return The array of created track selection. For null entries in {@code definitions} returns
+ * null values.
+ */
+ public static @NullableType TrackSelection[] createTrackSelectionsForDefinitions(
+ @NullableType Definition[] definitions,
+ AdaptiveTrackSelectionFactory adaptiveTrackSelectionFactory) {
+ TrackSelection[] selections = new TrackSelection[definitions.length];
+ boolean createdAdaptiveTrackSelection = false;
+ for (int i = 0; i < definitions.length; i++) {
+ Definition definition = definitions[i];
+ if (definition == null) {
+ continue;
+ }
+ if (definition.tracks.length > 1 && !createdAdaptiveTrackSelection) {
+ createdAdaptiveTrackSelection = true;
+ selections[i] = adaptiveTrackSelectionFactory.createAdaptiveTrackSelection(definition);
+ } else {
+ selections[i] = new FixedTrackSelection(definition.group, definition.tracks[0]);
+ }
+ }
+ return selections;
+ }
+
/**
* Returns average bitrate for chunks in bits per second. Chunks are included in average until
* {@code maxDurationMs} or the first unknown length chunk.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java
index c33c7c823f..ef0efe6140 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java
@@ -108,10 +108,7 @@ public final class DataSpec {
* {@link DataSpec} is not intended to be used in conjunction with a cache.
*/
public final @Nullable String key;
- /**
- * Request flags. Currently {@link #FLAG_ALLOW_GZIP} and
- * {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} are the only supported flags.
- */
+ /** Request {@link Flags flags}. */
public final @Flags int flags;
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java
index a769e9acac..8641746c74 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java
@@ -62,7 +62,7 @@ public interface Cache {
void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan);
}
-
+
/**
* Thrown when an error is encountered when writing data.
*/
@@ -82,7 +82,7 @@ public interface Cache {
* Releases the cache. This method must be called when the cache is no longer required. The cache
* must not be used after calling this method.
*/
- void release() throws CacheException;
+ void release();
/**
* Registers a listener to listen for changes to a given key.
@@ -223,25 +223,6 @@ public interface Cache {
*/
long getCachedLength(String key, long position, long length);
- /**
- * Sets the content length for the given key.
- *
- * @param key The cache key for the data.
- * @param length The length of the data.
- * @throws CacheException If an error is encountered.
- */
- void setContentLength(String key, long length) throws CacheException;
-
- /**
- * Returns the content length for the given key if one set, or {@link
- * com.google.android.exoplayer2.C#LENGTH_UNSET} otherwise.
- *
- * @param key The cache key for the data.
- * @return The content length for the given key if one set, or {@link
- * com.google.android.exoplayer2.C#LENGTH_UNSET} otherwise.
- */
- long getContentLength(String key);
-
/**
* Applies {@code mutations} to the {@link ContentMetadata} for the given key. A new {@link
* CachedContent} is added if there isn't one already with the given key.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java
index eaf72cf7fb..1b4b28d67e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java
@@ -42,10 +42,6 @@ import java.util.Map;
* A {@link DataSource} that reads and writes a {@link Cache}. Requests are fulfilled from the cache
* when possible. When data is not cached it is requested from an upstream {@link DataSource} and
* written into the cache.
- *
- * By default requests whose length can not be resolved are not cached. This is to prevent
- * caching of progressive live streams, which should usually not be cached. Caching of this kind of
- * requests can be enabled per request with {@link DataSpec#FLAG_ALLOW_CACHING_UNKNOWN_LENGTH}.
*/
public final class CacheDataSource implements DataSource {
@@ -303,7 +299,7 @@ public final class CacheDataSource implements DataSource {
if (dataSpec.length != C.LENGTH_UNSET || currentRequestIgnoresCache) {
bytesRemaining = dataSpec.length;
} else {
- bytesRemaining = cache.getContentLength(key);
+ bytesRemaining = ContentMetadata.getContentLength(cache.getContentMetadata(key));
if (bytesRemaining != C.LENGTH_UNSET) {
bytesRemaining -= dataSpec.position;
if (bytesRemaining <= 0) {
@@ -488,16 +484,12 @@ public final class CacheDataSource implements DataSource {
ContentMetadataMutations mutations = new ContentMetadataMutations();
if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) {
bytesRemaining = resolvedLength;
- ContentMetadataInternal.setContentLength(mutations, readPosition + bytesRemaining);
+ ContentMetadataMutations.setContentLength(mutations, readPosition + bytesRemaining);
}
if (isReadingFromUpstream()) {
actualUri = currentDataSource.getUri();
boolean isRedirected = !uri.equals(actualUri);
- if (isRedirected) {
- ContentMetadataInternal.setRedirectedUri(mutations, actualUri);
- } else {
- ContentMetadataInternal.removeRedirectedUri(mutations);
- }
+ ContentMetadataMutations.setRedirectedUri(mutations, isRedirected ? actualUri : null);
}
if (isWritingToCache()) {
cache.applyContentMetadataMutations(key, mutations);
@@ -507,14 +499,15 @@ public final class CacheDataSource implements DataSource {
private void setNoBytesRemainingAndMaybeStoreLength() throws IOException {
bytesRemaining = 0;
if (isWritingToCache()) {
- cache.setContentLength(key, readPosition);
+ ContentMetadataMutations mutations = new ContentMetadataMutations();
+ ContentMetadataMutations.setContentLength(mutations, readPosition);
+ cache.applyContentMetadataMutations(key, mutations);
}
}
private static Uri getRedirectedUriOrDefault(Cache cache, String key, Uri defaultUri) {
- ContentMetadata contentMetadata = cache.getContentMetadata(key);
- Uri redirectedUri = ContentMetadataInternal.getRedirectedUri(contentMetadata);
- return redirectedUri == null ? defaultUri : redirectedUri;
+ Uri redirectedUri = ContentMetadata.getRedirectedUri(cache.getContentMetadata(key));
+ return redirectedUri != null ? redirectedUri : defaultUri;
}
private static boolean isCausedByPositionOutOfRange(IOException e) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java
index fd4937ef86..9714df6ad0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java
@@ -84,7 +84,10 @@ public final class CacheUtil {
CachingCounters counters) {
String key = buildCacheKey(dataSpec, cacheKeyFactory);
long start = dataSpec.absoluteStreamPosition;
- long left = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : cache.getContentLength(key);
+ long left =
+ dataSpec.length != C.LENGTH_UNSET
+ ? dataSpec.length
+ : ContentMetadata.getContentLength(cache.getContentMetadata(key));
counters.contentLength = left;
counters.alreadyCachedBytes = 0;
counters.newlyCachedBytes = 0;
@@ -188,7 +191,10 @@ public final class CacheUtil {
String key = buildCacheKey(dataSpec, cacheKeyFactory);
long start = dataSpec.absoluteStreamPosition;
- long left = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : cache.getContentLength(key);
+ long left =
+ dataSpec.length != C.LENGTH_UNSET
+ ? dataSpec.length
+ : ContentMetadata.getContentLength(cache.getContentMetadata(key));
while (left != 0) {
throwExceptionIfInterruptedOrCancelled(isCanceled);
long blockLength =
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java
index 4d15de5932..5494454d54 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java
@@ -55,7 +55,7 @@ import java.util.TreeSet;
if (version < VERSION_METADATA_INTRODUCED) {
long length = input.readLong();
ContentMetadataMutations mutations = new ContentMetadataMutations();
- ContentMetadataInternal.setContentLength(mutations, length);
+ ContentMetadataMutations.setContentLength(mutations, length);
cachedContent.applyMetadataMutations(mutations);
} else {
cachedContent.metadata = DefaultContentMetadata.readFromStream(input);
@@ -216,7 +216,7 @@ import java.util.TreeSet;
int result = id;
result = 31 * result + key.hashCode();
if (version < VERSION_METADATA_INTRODUCED) {
- long length = ContentMetadataInternal.getContentLength(metadata);
+ long length = ContentMetadata.getContentLength(metadata);
result = 31 * result + (int) (length ^ (length >>> 32));
} else {
result = 31 * result + metadata.hashCode();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
index 19160c73d4..a744917230 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.upstream.cache;
import android.support.annotation.VisibleForTesting;
import android.util.SparseArray;
+import android.util.SparseBooleanArray;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.AtomicFile;
@@ -42,6 +43,7 @@ import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Maintains the index of cached content. */
/* package */ class CachedContentIndex {
@@ -53,7 +55,30 @@ import javax.crypto.spec.SecretKeySpec;
private static final int FLAG_ENCRYPTED_INDEX = 1;
private final HashMap keyToContent;
- private final SparseArray idToKey;
+ /**
+ * Maps assigned ids to their corresponding keys. Also contains (id -> null) entries for ids that
+ * have been removed from the index since it was last stored. This prevents reuse of these ids,
+ * which is necessary to avoid clashes that could otherwise occur as a result of the sequence:
+ *
+ * [1] (key1, id1) is removed from the in-memory index ... the index is not stored to disk ...
+ * [2] id1 is reused for a different key2 ... the index is not stored to disk ... [3] A file for
+ * key2 is partially written using a path corresponding to id1 ... the process is killed before
+ * the index is stored to disk ... [4] The index is read from disk, causing the partially written
+ * file to be incorrectly associated to key1
+ *
+ *
By avoiding id reuse in step [2], a new id2 will be used instead. Step [4] will then delete
+ * the partially written file because the index does not contain an entry for id2.
+ *
+ *
When the index is next stored (id -> null) entries are removed, making the ids eligible for
+ * reuse.
+ */
+ private final SparseArray<@NullableType String> idToKey;
+ /**
+ * Tracks ids for which (id -> null) entries are present in idToKey, so that they can be removed
+ * efficiently when the index is next stored.
+ */
+ private final SparseBooleanArray removedIds;
+
private final AtomicFile atomicFile;
private final Cipher cipher;
private final SecretKeySpec secretKeySpec;
@@ -105,6 +130,7 @@ import javax.crypto.spec.SecretKeySpec;
}
keyToContent = new HashMap<>();
idToKey = new SparseArray<>();
+ removedIds = new SparseBooleanArray();
atomicFile = new AtomicFile(new File(cacheDir, FILE_NAME));
}
@@ -125,6 +151,12 @@ import javax.crypto.spec.SecretKeySpec;
}
writeFile();
changed = false;
+ // Make ids that were removed since the index was last stored eligible for re-use.
+ int removedIdCount = removedIds.size();
+ for (int i = 0; i < removedIdCount; i++) {
+ idToKey.remove(removedIds.keyAt(i));
+ }
+ removedIds.clear();
}
/**
@@ -169,8 +201,11 @@ import javax.crypto.spec.SecretKeySpec;
CachedContent cachedContent = keyToContent.get(key);
if (cachedContent != null && cachedContent.isEmpty() && !cachedContent.isLocked()) {
keyToContent.remove(key);
- idToKey.remove(cachedContent.id);
changed = true;
+ // Keep an entry in idToKey to stop the id from being reused until the index is next stored.
+ idToKey.put(cachedContent.id, /* value= */ null);
+ // Track that the entry should be removed from idToKey when the index is next stored.
+ removedIds.put(cachedContent.id, /* value= */ true);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java
index aacd11f915..f0075343ad 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java
@@ -15,44 +15,73 @@
*/
package com.google.android.exoplayer2.upstream.cache;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+
/**
* Interface for an immutable snapshot of keyed metadata.
- *
- *
Internal metadata names are prefixed with {@value #INTERNAL_METADATA_NAME_PREFIX}. Custom
- * metadata names should avoid this prefix to prevent clashes.
*/
public interface ContentMetadata {
- /** Prefix of internal metadata names. */
- String INTERNAL_METADATA_NAME_PREFIX = "exo_";
+ /**
+ * Prefix for custom metadata keys. Applications can use keys starting with this prefix without
+ * any risk of their keys colliding with ones defined by the ExoPlayer library.
+ */
+ @SuppressWarnings("unused")
+ String KEY_CUSTOM_PREFIX = "custom_";
+ /** Key for redirected uri (type: String). */
+ String KEY_REDIRECTED_URI = "exo_redir";
+ /** Key for content length in bytes (type: long). */
+ String KEY_CONTENT_LENGTH = "exo_len";
/**
* Returns a metadata value.
*
- * @param name Name of the metadata to be returned.
+ * @param key Key of the metadata to be returned.
* @param defaultValue Value to return if the metadata doesn't exist.
* @return The metadata value.
*/
- byte[] get(String name, byte[] defaultValue);
+ @Nullable
+ byte[] get(String key, @Nullable byte[] defaultValue);
/**
* Returns a metadata value.
*
- * @param name Name of the metadata to be returned.
+ * @param key Key of the metadata to be returned.
* @param defaultValue Value to return if the metadata doesn't exist.
* @return The metadata value.
*/
- String get(String name, String defaultValue);
+ @Nullable
+ String get(String key, @Nullable String defaultValue);
/**
* Returns a metadata value.
*
- * @param name Name of the metadata to be returned.
+ * @param key Key of the metadata to be returned.
* @param defaultValue Value to return if the metadata doesn't exist.
* @return The metadata value.
*/
- long get(String name, long defaultValue);
+ long get(String key, long defaultValue);
/** Returns whether the metadata is available. */
- boolean contains(String name);
+ boolean contains(String key);
+
+ /**
+ * Returns the value stored under {@link #KEY_CONTENT_LENGTH}, or {@link C#LENGTH_UNSET} if not
+ * set.
+ */
+ static long getContentLength(ContentMetadata contentMetadata) {
+ return contentMetadata.get(KEY_CONTENT_LENGTH, C.LENGTH_UNSET);
+ }
+
+ /**
+ * Returns the value stored under {@link #KEY_REDIRECTED_URI} as a {@link Uri}, or {code null} if
+ * not set.
+ */
+ @Nullable
+ static Uri getRedirectedUri(ContentMetadata contentMetadata) {
+ String redirectedUri = contentMetadata.get(KEY_REDIRECTED_URI, (String) null);
+ return redirectedUri == null ? null : Uri.parse(redirectedUri);
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataInternal.java
deleted file mode 100644
index 0065018260..0000000000
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataInternal.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.android.exoplayer2.upstream.cache;
-
-import android.net.Uri;
-import android.support.annotation.Nullable;
-import com.google.android.exoplayer2.C;
-
-/** Helper classes to easily access and modify internal metadata values. */
-/* package */ final class ContentMetadataInternal {
-
- private static final String PREFIX = ContentMetadata.INTERNAL_METADATA_NAME_PREFIX;
- private static final String METADATA_NAME_REDIRECTED_URI = PREFIX + "redir";
- private static final String METADATA_NAME_CONTENT_LENGTH = PREFIX + "len";
-
- /** Returns the content length metadata, or {@link C#LENGTH_UNSET} if not set. */
- public static long getContentLength(ContentMetadata contentMetadata) {
- return contentMetadata.get(METADATA_NAME_CONTENT_LENGTH, C.LENGTH_UNSET);
- }
-
- /** Adds a mutation to set content length metadata value. */
- public static void setContentLength(ContentMetadataMutations mutations, long length) {
- mutations.set(METADATA_NAME_CONTENT_LENGTH, length);
- }
-
- /** Adds a mutation to remove content length metadata value. */
- public static void removeContentLength(ContentMetadataMutations mutations) {
- mutations.remove(METADATA_NAME_CONTENT_LENGTH);
- }
-
- /** Returns the redirected uri metadata, or {@code null} if not set. */
- public @Nullable static Uri getRedirectedUri(ContentMetadata contentMetadata) {
- String redirectedUri = contentMetadata.get(METADATA_NAME_REDIRECTED_URI, (String) null);
- return redirectedUri == null ? null : Uri.parse(redirectedUri);
- }
-
- /**
- * Adds a mutation to set redirected uri metadata value. Passing {@code null} as {@code uri} isn't
- * allowed.
- */
- public static void setRedirectedUri(ContentMetadataMutations mutations, Uri uri) {
- mutations.set(METADATA_NAME_REDIRECTED_URI, uri.toString());
- }
-
- /** Adds a mutation to remove redirected uri metadata value. */
- public static void removeRedirectedUri(ContentMetadataMutations mutations) {
- mutations.remove(METADATA_NAME_REDIRECTED_URI);
- }
-
- private ContentMetadataInternal() {
- // Prevent instantiation.
- }
-}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java
index 70154b0308..fb3f6e362d 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java
@@ -15,6 +15,9 @@
*/
package com.google.android.exoplayer2.upstream.cache;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import java.util.ArrayList;
import java.util.Arrays;
@@ -30,6 +33,36 @@ import java.util.Map.Entry;
*/
public class ContentMetadataMutations {
+ /**
+ * Adds a mutation to set the {@link ContentMetadata#KEY_CONTENT_LENGTH} value, or to remove any
+ * existing value if {@link C#LENGTH_UNSET} is passed.
+ *
+ * @param mutations The mutations to modify.
+ * @param length The length value, or {@link C#LENGTH_UNSET} to remove any existing entry.
+ * @return The mutations instance, for convenience.
+ */
+ public static ContentMetadataMutations setContentLength(
+ ContentMetadataMutations mutations, long length) {
+ return mutations.set(ContentMetadata.KEY_CONTENT_LENGTH, length);
+ }
+
+ /**
+ * Adds a mutation to set the {@link ContentMetadata#KEY_REDIRECTED_URI} value, or to remove any
+ * existing entry if {@code null} is passed.
+ *
+ * @param mutations The mutations to modify.
+ * @param uri The {@link Uri} value, or {@code null} to remove any existing entry.
+ * @return The mutations instance, for convenience.
+ */
+ public static ContentMetadataMutations setRedirectedUri(
+ ContentMetadataMutations mutations, @Nullable Uri uri) {
+ if (uri == null) {
+ return mutations.remove(ContentMetadata.KEY_REDIRECTED_URI);
+ } else {
+ return mutations.set(ContentMetadata.KEY_REDIRECTED_URI, uri.toString());
+ }
+ }
+
private final Map editedValues;
private final List removedValues;
@@ -45,7 +78,7 @@ public class ContentMetadataMutations {
*
* @param name The name of the metadata value.
* @param value The value to be set.
- * @return This Editor instance, for convenience.
+ * @return This instance, for convenience.
*/
public ContentMetadataMutations set(String name, String value) {
return checkAndSet(name, value);
@@ -56,7 +89,7 @@ public class ContentMetadataMutations {
*
* @param name The name of the metadata value.
* @param value The value to be set.
- * @return This Editor instance, for convenience.
+ * @return This instance, for convenience.
*/
public ContentMetadataMutations set(String name, long value) {
return checkAndSet(name, value);
@@ -68,7 +101,7 @@ public class ContentMetadataMutations {
*
* @param name The name of the metadata value.
* @param value The value to be set.
- * @return This Editor instance, for convenience.
+ * @return This instance, for convenience.
*/
public ContentMetadataMutations set(String name, byte[] value) {
return checkAndSet(name, Arrays.copyOf(value, value.length));
@@ -78,7 +111,7 @@ public class ContentMetadataMutations {
* Adds a mutation to remove a metadata value.
*
* @param name The name of the metadata value.
- * @return This Editor instance, for convenience.
+ * @return This instance, for convenience.
*/
public ContentMetadataMutations remove(String name) {
removedValues.add(name);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java
index e16ff5483a..843dd19444 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java
@@ -64,6 +64,10 @@ public final class DefaultContentMetadata implements ContentMetadata {
private final Map metadata;
+ public DefaultContentMetadata() {
+ this(Collections.emptyMap());
+ }
+
private DefaultContentMetadata(Map metadata) {
this.metadata = Collections.unmodifiableMap(metadata);
}
@@ -74,7 +78,7 @@ public final class DefaultContentMetadata implements ContentMetadata {
*/
public DefaultContentMetadata copyWithMutationsApplied(ContentMetadataMutations mutations) {
Map mutatedMetadata = applyMutations(metadata, mutations);
- if (isMetadataEqual(mutatedMetadata)) {
+ if (isMetadataEqual(metadata, mutatedMetadata)) {
return this;
}
return new DefaultContentMetadata(mutatedMetadata);
@@ -97,7 +101,8 @@ public final class DefaultContentMetadata implements ContentMetadata {
}
@Override
- public final byte[] get(String name, byte[] defaultValue) {
+ @Nullable
+ public final byte[] get(String name, @Nullable byte[] defaultValue) {
if (metadata.containsKey(name)) {
byte[] bytes = metadata.get(name);
return Arrays.copyOf(bytes, bytes.length);
@@ -107,7 +112,8 @@ public final class DefaultContentMetadata implements ContentMetadata {
}
@Override
- public final String get(String name, String defaultValue) {
+ @Nullable
+ public final String get(String name, @Nullable String defaultValue) {
if (metadata.containsKey(name)) {
byte[] bytes = metadata.get(name);
return new String(bytes, Charset.forName(C.UTF8_NAME));
@@ -139,21 +145,7 @@ public final class DefaultContentMetadata implements ContentMetadata {
if (o == null || getClass() != o.getClass()) {
return false;
}
- return isMetadataEqual(((DefaultContentMetadata) o).metadata);
- }
-
- private boolean isMetadataEqual(Map otherMetadata) {
- if (metadata.size() != otherMetadata.size()) {
- return false;
- }
- for (Entry entry : metadata.entrySet()) {
- byte[] value = entry.getValue();
- byte[] otherValue = otherMetadata.get(entry.getKey());
- if (!Arrays.equals(value, otherValue)) {
- return false;
- }
- }
- return true;
+ return isMetadataEqual(metadata, ((DefaultContentMetadata) o).metadata);
}
@Override
@@ -168,6 +160,20 @@ public final class DefaultContentMetadata implements ContentMetadata {
return hashCode;
}
+ private static boolean isMetadataEqual(Map first, Map second) {
+ if (first.size() != second.size()) {
+ return false;
+ }
+ for (Entry entry : first.entrySet()) {
+ byte[] value = entry.getValue();
+ byte[] otherValue = second.get(entry.getKey());
+ if (!Arrays.equals(value, otherValue)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
private static Map applyMutations(
Map otherMetadata, ContentMetadataMutations mutations) {
HashMap metadata = new HashMap<>(otherMetadata);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java
index 1fd6dc63bc..8bcf1758fa 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java
@@ -146,13 +146,16 @@ public final class SimpleCache implements Cache {
}
@Override
- public synchronized void release() throws CacheException {
+ public synchronized void release() {
if (released) {
return;
}
listeners.clear();
+ removeStaleSpans();
try {
- removeStaleSpansAndCachedContents();
+ index.store();
+ } catch (CacheException e) {
+ Log.e(TAG, "Storing index file failed", e);
} finally {
unlockFolder(cacheDir);
released = true;
@@ -265,7 +268,7 @@ public final class SimpleCache implements Cache {
if (!cacheDir.exists()) {
// For some reason the cache directory doesn't exist. Make a best effort to create it.
cacheDir.mkdirs();
- removeStaleSpansAndCachedContents();
+ removeStaleSpans();
}
evictor.onStartFile(this, key, position, maxLength);
return SimpleCacheSpan.getCacheFile(
@@ -290,7 +293,7 @@ public final class SimpleCache implements Cache {
return;
}
// Check if the span conflicts with the set content length
- long length = ContentMetadataInternal.getContentLength(cachedContent.getMetadata());
+ long length = ContentMetadata.getContentLength(cachedContent.getMetadata());
if (length != C.LENGTH_UNSET) {
Assertions.checkState((span.position + span.length) <= length);
}
@@ -311,9 +314,9 @@ public final class SimpleCache implements Cache {
}
@Override
- public synchronized void removeSpan(CacheSpan span) throws CacheException {
+ public synchronized void removeSpan(CacheSpan span) {
Assertions.checkState(!released);
- removeSpan(span, true);
+ removeSpanInternal(span);
}
@Override
@@ -330,18 +333,6 @@ public final class SimpleCache implements Cache {
return cachedContent != null ? cachedContent.getCachedBytesLength(position, length) : -length;
}
- @Override
- public synchronized void setContentLength(String key, long length) throws CacheException {
- ContentMetadataMutations mutations = new ContentMetadataMutations();
- ContentMetadataInternal.setContentLength(mutations, length);
- applyContentMetadataMutations(key, mutations);
- }
-
- @Override
- public synchronized long getContentLength(String key) {
- return ContentMetadataInternal.getContentLength(getContentMetadata(key));
- }
-
@Override
public synchronized void applyContentMetadataMutations(
String key, ContentMetadataMutations mutations) throws CacheException {
@@ -379,7 +370,7 @@ public final class SimpleCache implements Cache {
if (span.isCached && !span.file.exists()) {
// The file has been deleted from under us. It's likely that other files will have been
// deleted too, so scan the whole in-memory representation.
- removeStaleSpansAndCachedContents();
+ removeStaleSpans();
continue;
}
return span;
@@ -431,27 +422,21 @@ public final class SimpleCache implements Cache {
notifySpanAdded(span);
}
- private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) throws CacheException {
+ private void removeSpanInternal(CacheSpan span) {
CachedContent cachedContent = index.get(span.key);
if (cachedContent == null || !cachedContent.removeSpan(span)) {
return;
}
totalSpace -= span.length;
- try {
- if (removeEmptyCachedContent) {
- index.maybeRemove(cachedContent.key);
- index.store();
- }
- } finally {
- notifySpanRemoved(span);
- }
+ index.maybeRemove(cachedContent.key);
+ notifySpanRemoved(span);
}
/**
* Scans all of the cached spans in the in-memory representation, removing any for which files no
* longer exist.
*/
- private void removeStaleSpansAndCachedContents() throws CacheException {
+ private void removeStaleSpans() {
ArrayList spansToBeRemoved = new ArrayList<>();
for (CachedContent cachedContent : index.getAll()) {
for (CacheSpan span : cachedContent.getSpans()) {
@@ -461,11 +446,8 @@ public final class SimpleCache implements Cache {
}
}
for (int i = 0; i < spansToBeRemoved.size(); i++) {
- // Remove span but not CachedContent to prevent multiple index.store() calls.
- removeSpan(spansToBeRemoved.get(i), false);
+ removeSpanInternal(spansToBeRemoved.get(i));
}
- index.removeEmpty();
- index.store();
}
private void notifySpanRemoved(CacheSpan span) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
index 67586fe672..49f6be9763 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
@@ -98,7 +98,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
private final EventDispatcher eventDispatcher;
private final long allowedJoiningTimeMs;
private final int maxDroppedFramesToNotify;
- private final boolean deviceNeedsAutoFrcWorkaround;
+ private final boolean deviceNeedsNoPostProcessWorkaround;
private final long[] pendingOutputStreamOffsetsUs;
private final long[] pendingOutputStreamSwitchTimesUs;
@@ -226,7 +226,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
this.context = context.getApplicationContext();
frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(this.context);
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
- deviceNeedsAutoFrcWorkaround = deviceNeedsAutoFrcWorkaround();
+ deviceNeedsNoPostProcessWorkaround = deviceNeedsNoPostProcessWorkaround();
pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];
pendingOutputStreamSwitchTimesUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];
outputStreamOffsetUs = C.TIME_UNSET;
@@ -484,7 +484,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
format,
codecMaxValues,
codecOperatingRate,
- deviceNeedsAutoFrcWorkaround,
+ deviceNeedsNoPostProcessWorkaround,
tunnelingAudioSessionId);
if (surface == null) {
Assertions.checkState(shouldUseDummySurface(codecInfo));
@@ -1036,8 +1036,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
* @param codecMaxValues Codec max values that should be used when configuring the decoder.
* @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if
* no codec operating rate should be set.
- * @param deviceNeedsAutoFrcWorkaround Whether the device is known to enable frame-rate conversion
- * logic that negatively impacts ExoPlayer.
+ * @param deviceNeedsNoPostProcessWorkaround Whether the device is known to do post processing by
+ * default that isn't compatible with ExoPlayer.
* @param tunnelingAudioSessionId The audio session id to use for tunneling, or {@link
* C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled.
* @return The framework {@link MediaFormat} that should be used to configure the decoder.
@@ -1047,7 +1047,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
Format format,
CodecMaxValues codecMaxValues,
float codecOperatingRate,
- boolean deviceNeedsAutoFrcWorkaround,
+ boolean deviceNeedsNoPostProcessWorkaround,
int tunnelingAudioSessionId) {
MediaFormat mediaFormat = new MediaFormat();
// Set format parameters that should always be set.
@@ -1071,7 +1071,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
mediaFormat.setFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate);
}
}
- if (deviceNeedsAutoFrcWorkaround) {
+ if (deviceNeedsNoPostProcessWorkaround) {
+ mediaFormat.setInteger("no-post-process", 1);
mediaFormat.setInteger("auto-frc", 0);
}
if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {
@@ -1265,21 +1266,21 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
/**
- * Returns whether the device is known to enable frame-rate conversion logic that negatively
- * impacts ExoPlayer.
- *
- * If true is returned then we explicitly disable the feature.
+ * Returns whether the device is known to do post processing by default that isn't compatible with
+ * ExoPlayer.
*
- * @return True if the device is known to enable frame-rate conversion logic that negatively
- * impacts ExoPlayer. False otherwise.
+ * @return Whether the device is known to do post processing by default that isn't compatible with
+ * ExoPlayer.
*/
- private static boolean deviceNeedsAutoFrcWorkaround() {
- // nVidia Shield prior to M tries to adjust the playback rate to better map the frame-rate of
+ private static boolean deviceNeedsNoPostProcessWorkaround() {
+ // Nvidia devices prior to M try to adjust the playback rate to better map the frame-rate of
// content to the refresh rate of the display. For example playback of 23.976fps content is
// adjusted to play at 1.001x speed when the output display is 60Hz. Unfortunately the
// implementation causes ExoPlayer's reported playback position to drift out of sync. Captions
- // also lose sync [Internal: b/26453592].
- return Util.SDK_INT <= 22 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER);
+ // also lose sync [Internal: b/26453592]. Even after M, the devices may apply post processing
+ // operations that can modify frame output timestamps, which is incompatible with ExoPlayer's
+ // logic for skipping decode-only frames.
+ return "NVIDIA".equals(Util.MANUFACTURER);
}
/*
@@ -1305,163 +1306,171 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
* incorrectly.
*/
protected boolean codecNeedsSetOutputSurfaceWorkaround(String name) {
- if (Util.SDK_INT >= 27 || name.startsWith("OMX.google")) {
- // Devices running API level 27 or later should also be unaffected. Google OMX decoders are
- // not known to have this issue on any API level.
+ if (name.startsWith("OMX.google")) {
+ // Google OMX decoders are not known to have this issue on any API level.
return false;
}
- // Work around:
- // https://github.com/google/ExoPlayer/issues/3236,
- // https://github.com/google/ExoPlayer/issues/3355,
- // https://github.com/google/ExoPlayer/issues/3439,
- // https://github.com/google/ExoPlayer/issues/3724,
- // https://github.com/google/ExoPlayer/issues/3835,
- // https://github.com/google/ExoPlayer/issues/4006,
- // https://github.com/google/ExoPlayer/issues/4084,
- // https://github.com/google/ExoPlayer/issues/4104,
- // https://github.com/google/ExoPlayer/issues/4134,
- // https://github.com/google/ExoPlayer/issues/4315,
- // https://github.com/google/ExoPlayer/issues/4419,
- // https://github.com/google/ExoPlayer/issues/4460,
- // https://github.com/google/ExoPlayer/issues/4468.
synchronized (MediaCodecVideoRenderer.class) {
if (!evaluatedDeviceNeedsSetOutputSurfaceWorkaround) {
- switch (Util.DEVICE) {
- case "1601":
- case "1713":
- case "1714":
- case "A10-70F":
- case "A1601":
- case "A2016a40":
- case "A7000-a":
- case "A7000plus":
- case "A7010a48":
- case "A7020a48":
- case "AquaPowerM":
- case "ASUS_X00AD_2":
- case "Aura_Note_2":
- case "BLACK-1X":
- case "BRAVIA_ATV2":
- case "C1":
- case "ComioS1":
- case "CP8676_I02":
- case "CPH1609":
- case "CPY83_I00":
- case "cv1":
- case "cv3":
- case "deb":
- case "E5643":
- case "ELUGA_A3_Pro":
- case "ELUGA_Note":
- case "ELUGA_Prim":
- case "ELUGA_Ray_X":
- case "EverStar_S":
- case "F3111":
- case "F3113":
- case "F3116":
- case "F3211":
- case "F3213":
- case "F3215":
- case "F3311":
- case "flo":
- case "GiONEE_CBL7513":
- case "GiONEE_GBL7319":
- case "GIONEE_GBL7360":
- case "GIONEE_SWW1609":
- case "GIONEE_SWW1627":
- case "GIONEE_SWW1631":
- case "GIONEE_WBL5708":
- case "GIONEE_WBL7365":
- case "GIONEE_WBL7519":
- case "griffin":
- case "htc_e56ml_dtul":
- case "hwALE-H":
- case "HWBLN-H":
- case "HWCAM-H":
- case "HWVNS-H":
- case "i9031":
- case "iball8735_9806":
- case "Infinix-X572":
- case "iris60":
- case "itel_S41":
- case "j2xlteins":
- case "JGZ":
- case "K50a40":
- case "kate":
- case "le_x6":
- case "LS-5017":
- case "M5c":
- case "manning":
- case "marino_f":
- case "MEIZU_M5":
- case "mh":
- case "mido":
- case "MX6":
- case "namath":
- case "nicklaus_f":
- case "NX541J":
- case "NX573J":
- case "OnePlus5T":
- case "p212":
- case "P681":
- case "P85":
- case "panell_d":
- case "panell_dl":
- case "panell_ds":
- case "panell_dt":
- case "PB2-670M":
- case "PGN528":
- case "PGN610":
- case "PGN611":
- case "Phantom6":
- case "Pixi4-7_3G":
- case "Pixi5-10_4G":
- case "PLE":
- case "PRO7S":
- case "Q350":
- case "Q4260":
- case "Q427":
- case "Q4310":
- case "Q5":
- case "QM16XE_U":
- case "QX1":
- case "santoni":
- case "Slate_Pro":
- case "SVP-DTV15":
- case "s905x018":
- case "taido_row":
- case "TB3-730F":
- case "TB3-730X":
- case "TB3-850F":
- case "TB3-850M":
- case "tcl_eu":
- case "V1":
- case "V23GB":
- case "V5":
- case "vernee_M5":
- case "watson":
- case "whyred":
- case "woods_f":
- case "woods_fn":
- case "X3_HK":
- case "XE2X":
- case "XT1663":
- case "Z12_PRO":
- case "Z80":
- deviceNeedsSetOutputSurfaceWorkaround = true;
- break;
- default:
- // Do nothing.
- break;
- }
- switch (Util.MODEL) {
- case "AFTA":
- case "AFTN":
- deviceNeedsSetOutputSurfaceWorkaround = true;
- break;
- default:
- // Do nothing.
- break;
+ if (Util.SDK_INT <= 27 && "dangal".equals(Util.DEVICE)) {
+ // Dangal is affected on API level 27: https://github.com/google/ExoPlayer/issues/5169.
+ deviceNeedsSetOutputSurfaceWorkaround = true;
+ } else if (Util.SDK_INT >= 27) {
+ // In general, devices running API level 27 or later should be unaffected. Do nothing.
+ } else {
+ // Enable the workaround on a per-device basis. Works around:
+ // https://github.com/google/ExoPlayer/issues/3236,
+ // https://github.com/google/ExoPlayer/issues/3355,
+ // https://github.com/google/ExoPlayer/issues/3439,
+ // https://github.com/google/ExoPlayer/issues/3724,
+ // https://github.com/google/ExoPlayer/issues/3835,
+ // https://github.com/google/ExoPlayer/issues/4006,
+ // https://github.com/google/ExoPlayer/issues/4084,
+ // https://github.com/google/ExoPlayer/issues/4104,
+ // https://github.com/google/ExoPlayer/issues/4134,
+ // https://github.com/google/ExoPlayer/issues/4315,
+ // https://github.com/google/ExoPlayer/issues/4419,
+ // https://github.com/google/ExoPlayer/issues/4460,
+ // https://github.com/google/ExoPlayer/issues/4468.
+ switch (Util.DEVICE) {
+ case "1601":
+ case "1713":
+ case "1714":
+ case "A10-70F":
+ case "A1601":
+ case "A2016a40":
+ case "A7000-a":
+ case "A7000plus":
+ case "A7010a48":
+ case "A7020a48":
+ case "AquaPowerM":
+ case "ASUS_X00AD_2":
+ case "Aura_Note_2":
+ case "BLACK-1X":
+ case "BRAVIA_ATV2":
+ case "BRAVIA_ATV3_4K":
+ case "C1":
+ case "ComioS1":
+ case "CP8676_I02":
+ case "CPH1609":
+ case "CPY83_I00":
+ case "cv1":
+ case "cv3":
+ case "deb":
+ case "E5643":
+ case "ELUGA_A3_Pro":
+ case "ELUGA_Note":
+ case "ELUGA_Prim":
+ case "ELUGA_Ray_X":
+ case "EverStar_S":
+ case "F3111":
+ case "F3113":
+ case "F3116":
+ case "F3211":
+ case "F3213":
+ case "F3215":
+ case "F3311":
+ case "flo":
+ case "fugu":
+ case "GiONEE_CBL7513":
+ case "GiONEE_GBL7319":
+ case "GIONEE_GBL7360":
+ case "GIONEE_SWW1609":
+ case "GIONEE_SWW1627":
+ case "GIONEE_SWW1631":
+ case "GIONEE_WBL5708":
+ case "GIONEE_WBL7365":
+ case "GIONEE_WBL7519":
+ case "griffin":
+ case "htc_e56ml_dtul":
+ case "hwALE-H":
+ case "HWBLN-H":
+ case "HWCAM-H":
+ case "HWVNS-H":
+ case "i9031":
+ case "iball8735_9806":
+ case "Infinix-X572":
+ case "iris60":
+ case "itel_S41":
+ case "j2xlteins":
+ case "JGZ":
+ case "K50a40":
+ case "kate":
+ case "le_x6":
+ case "LS-5017":
+ case "M5c":
+ case "manning":
+ case "marino_f":
+ case "MEIZU_M5":
+ case "mh":
+ case "mido":
+ case "MX6":
+ case "namath":
+ case "nicklaus_f":
+ case "NX541J":
+ case "NX573J":
+ case "OnePlus5T":
+ case "p212":
+ case "P681":
+ case "P85":
+ case "panell_d":
+ case "panell_dl":
+ case "panell_ds":
+ case "panell_dt":
+ case "PB2-670M":
+ case "PGN528":
+ case "PGN610":
+ case "PGN611":
+ case "Phantom6":
+ case "Pixi4-7_3G":
+ case "Pixi5-10_4G":
+ case "PLE":
+ case "PRO7S":
+ case "Q350":
+ case "Q4260":
+ case "Q427":
+ case "Q4310":
+ case "Q5":
+ case "QM16XE_U":
+ case "QX1":
+ case "santoni":
+ case "Slate_Pro":
+ case "SVP-DTV15":
+ case "s905x018":
+ case "taido_row":
+ case "TB3-730F":
+ case "TB3-730X":
+ case "TB3-850F":
+ case "TB3-850M":
+ case "tcl_eu":
+ case "V1":
+ case "V23GB":
+ case "V5":
+ case "vernee_M5":
+ case "watson":
+ case "whyred":
+ case "woods_f":
+ case "woods_fn":
+ case "X3_HK":
+ case "XE2X":
+ case "XT1663":
+ case "Z12_PRO":
+ case "Z80":
+ deviceNeedsSetOutputSurfaceWorkaround = true;
+ break;
+ default:
+ // Do nothing.
+ break;
+ }
+ switch (Util.MODEL) {
+ case "AFTA":
+ case "AFTN":
+ deviceNeedsSetOutputSurfaceWorkaround = true;
+ break;
+ default:
+ // Do nothing.
+ break;
+ }
}
evaluatedDeviceNeedsSetOutputSurfaceWorkaround = true;
}
diff --git a/library/core/src/test/assets/ttml/bitmap_percentage_region.xml b/library/core/src/test/assets/ttml/bitmap_percentage_region.xml
new file mode 100644
index 0000000000..9631650178
--- /dev/null
+++ b/library/core/src/test/assets/ttml/bitmap_percentage_region.xml
@@ -0,0 +1,26 @@
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/library/core/src/test/assets/ttml/bitmap_pixel_region.xml b/library/core/src/test/assets/ttml/bitmap_pixel_region.xml
new file mode 100644
index 0000000000..c724c46626
--- /dev/null
+++ b/library/core/src/test/assets/ttml/bitmap_pixel_region.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/library/core/src/test/assets/ttml/bitmap_unsupported_region.xml b/library/core/src/test/assets/ttml/bitmap_unsupported_region.xml
new file mode 100644
index 0000000000..5e72d706fc
--- /dev/null
+++ b/library/core/src/test/assets/ttml/bitmap_unsupported_region.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadActionUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadActionUtilTest.java
new file mode 100644
index 0000000000..a494057a05
--- /dev/null
+++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadActionUtilTest.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.offline;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import android.net.Uri;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests for {@link DownloadActionUtil} class. */
+@RunWith(RobolectricTestRunner.class)
+public class DownloadActionUtilTest {
+ private Uri uri1;
+ private Uri uri2;
+
+ @Before
+ public void setUp() throws Exception {
+ uri1 = Uri.parse("http://abc.com/media1");
+ uri2 = Uri.parse("http://abc.com/media2");
+ }
+
+ @Test
+ public void mergeActions_ifQueueEmpty_throwsException() {
+ try {
+ DownloadActionUtil.mergeActions(toActionQueue());
+ fail();
+ } catch (Exception e) {
+ // Expected.
+ }
+ }
+
+ @Test
+ public void mergeActions_ifOneActionInQueue_returnsTheSameAction() {
+ DownloadAction action = createDownloadAction(uri1);
+
+ assertThat(DownloadActionUtil.mergeActions(toActionQueue(action))).isEqualTo(action);
+ }
+
+ @Test
+ public void mergeActions_ifActionsHaveDifferentType_throwsException() {
+ DownloadAction downloadAction1 =
+ DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_PROGRESSIVE,
+ uri1,
+ Collections.emptyList(),
+ /* customCacheKey= */ null,
+ /* data= */ null);
+ DownloadAction downloadAction2 =
+ DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_DASH,
+ uri1,
+ Collections.emptyList(),
+ /* customCacheKey= */ null,
+ /* data= */ null);
+ ArrayDeque actionQueue = toActionQueue(downloadAction1, downloadAction2);
+
+ try {
+ DownloadActionUtil.mergeActions(actionQueue);
+ fail();
+ } catch (Exception e) {
+ // Expected.
+ }
+ }
+
+ @Test
+ public void mergeActions_ifActionsHaveDifferentCacheKeys_throwsException() {
+ DownloadAction downloadAction1 =
+ DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_PROGRESSIVE,
+ uri1,
+ Collections.emptyList(),
+ /* customCacheKey= */ "cacheKey1",
+ /* data= */ null);
+ DownloadAction downloadAction2 =
+ DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_PROGRESSIVE,
+ uri1,
+ Collections.emptyList(),
+ /* customCacheKey= */ "cacheKey2",
+ /* data= */ null);
+ ArrayDeque actionQueue = toActionQueue(downloadAction1, downloadAction2);
+
+ try {
+ DownloadActionUtil.mergeActions(actionQueue);
+ fail();
+ } catch (Exception e) {
+ // Expected.
+ }
+ }
+
+ @Test
+ public void mergeActions_nullCacheKeyAndDifferentUrl_throwsException() {
+ DownloadAction downloadAction1 =
+ DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_PROGRESSIVE,
+ uri1,
+ Collections.emptyList(),
+ /* customCacheKey= */ null,
+ /* data= */ null);
+ DownloadAction downloadAction2 =
+ DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_PROGRESSIVE,
+ uri2,
+ Collections.emptyList(),
+ /* customCacheKey= */ null,
+ /* data= */ null);
+ ArrayDeque actionQueue = toActionQueue(downloadAction1, downloadAction2);
+
+ try {
+ DownloadActionUtil.mergeActions(actionQueue);
+ fail();
+ } catch (Exception e) {
+ // Expected.
+ }
+ }
+
+ @Test
+ public void mergeActions_sameCacheKeyAndDifferentUrl_latterUrlUsed() {
+ DownloadAction downloadAction1 =
+ DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_PROGRESSIVE,
+ uri1,
+ Collections.emptyList(),
+ /* customCacheKey= */ "cacheKey1",
+ /* data= */ null);
+ DownloadAction downloadAction2 =
+ DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_PROGRESSIVE,
+ uri2,
+ Collections.emptyList(),
+ /* customCacheKey= */ "cacheKey1",
+ /* data= */ null);
+ ArrayDeque actionQueue = toActionQueue(downloadAction1, downloadAction2);
+
+ DownloadActionUtil.mergeActions(actionQueue);
+
+ DownloadAction mergedAction = DownloadActionUtil.mergeActions(actionQueue);
+
+ assertThat(mergedAction.uri).isEqualTo(uri2);
+ }
+
+ @Test
+ public void mergeActions_differentData_latterDataUsed() {
+ byte[] data1 = "data1".getBytes();
+ DownloadAction downloadAction1 =
+ DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_PROGRESSIVE,
+ uri1,
+ Collections.emptyList(),
+ /* customCacheKey= */ null,
+ /* data= */ data1);
+ byte[] data2 = "data2".getBytes();
+ DownloadAction downloadAction2 =
+ DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_PROGRESSIVE,
+ uri1,
+ Collections.emptyList(),
+ /* customCacheKey= */ null,
+ /* data= */ data2);
+ ArrayDeque actionQueue = toActionQueue(downloadAction1, downloadAction2);
+
+ DownloadActionUtil.mergeActions(actionQueue);
+
+ DownloadAction mergedAction = DownloadActionUtil.mergeActions(actionQueue);
+
+ assertThat(mergedAction.data).isEqualTo(data2);
+ }
+
+ @Test
+ public void mergeActions_ifRemoveActionLast_returnsRemoveAction() {
+ DownloadAction downloadAction = createDownloadAction(uri1);
+ DownloadAction removeAction = createRemoveAction(uri1);
+ ArrayDeque actionQueue = toActionQueue(downloadAction, removeAction);
+
+ DownloadAction action = DownloadActionUtil.mergeActions(actionQueue);
+
+ assertThat(action).isEqualTo(removeAction);
+ assertThat(actionQueue).containsExactly(removeAction);
+ }
+
+ @Test
+ public void mergeActions_downloadActionAfterRemove_returnsRemoveKeepsDownload() {
+ DownloadAction removeAction = createRemoveAction(uri1);
+ DownloadAction downloadAction = createDownloadAction(uri1);
+ ArrayDeque actionQueue = toActionQueue(removeAction, downloadAction);
+
+ DownloadAction action = DownloadActionUtil.mergeActions(actionQueue);
+
+ assertThat(action).isEqualTo(removeAction);
+ assertThat(actionQueue).containsExactly(removeAction, downloadAction);
+ }
+
+ @Test
+ public void mergeActions_downloadActionsAfterRemove_returnsRemoveMergesDownloads() {
+ DownloadAction removeAction = createRemoveAction(uri1);
+ StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
+ DownloadAction downloadAction1 =
+ createDownloadAction(uri1, Collections.singletonList(streamKey1));
+ StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1);
+ DownloadAction downloadAction2 =
+ createDownloadAction(uri1, Collections.singletonList(streamKey2));
+ ArrayDeque actionQueue =
+ toActionQueue(removeAction, downloadAction1, downloadAction2);
+ DownloadAction mergedDownloadAction =
+ createDownloadAction(uri1, Arrays.asList(streamKey1, streamKey2));
+
+ DownloadAction action = DownloadActionUtil.mergeActions(actionQueue);
+
+ assertThat(action).isEqualTo(removeAction);
+ assertThat(actionQueue).containsExactly(removeAction, mergedDownloadAction);
+ }
+
+ @Test
+ public void mergeActions_actionBeforeRemove_ignoresActionBeforeRemove() {
+ DownloadAction removeAction = createRemoveAction(uri1);
+ StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
+ DownloadAction downloadAction1 =
+ createDownloadAction(uri1, Collections.singletonList(streamKey1));
+ StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1);
+ DownloadAction downloadAction2 =
+ createDownloadAction(uri1, Collections.singletonList(streamKey2));
+ StreamKey streamKey3 = new StreamKey(/* groupIndex= */ 2, /* trackIndex= */ 2);
+ DownloadAction downloadAction3 =
+ createDownloadAction(uri1, Collections.singletonList(streamKey3));
+ ArrayDeque actionQueue =
+ toActionQueue(downloadAction1, removeAction, downloadAction2, downloadAction3);
+ DownloadAction mergedDownloadAction =
+ createDownloadAction(uri1, Arrays.asList(streamKey2, streamKey3));
+
+ DownloadAction action = DownloadActionUtil.mergeActions(actionQueue);
+
+ assertThat(action).isEqualTo(removeAction);
+ assertThat(actionQueue).containsExactly(removeAction, mergedDownloadAction);
+ }
+
+ @Test
+ public void mergeActions_returnsMergedAction() {
+ StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
+ StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1);
+ StreamKey[] keys1 = new StreamKey[] {streamKey1};
+ StreamKey[] keys2 = new StreamKey[] {streamKey2};
+ StreamKey[] expectedKeys = new StreamKey[] {streamKey1, streamKey2};
+
+ doTestMergeActionsReturnsMergedKeys(keys1, keys2, expectedKeys);
+ }
+
+ @Test
+ public void mergeActions_returnsUniqueKeys() {
+ StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
+ StreamKey streamKey1Copy = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
+ StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1);
+ StreamKey[] keys1 = new StreamKey[] {streamKey1};
+ StreamKey[] keys2 = new StreamKey[] {streamKey2, streamKey1Copy};
+ StreamKey[] expectedKeys = new StreamKey[] {streamKey1, streamKey2};
+
+ doTestMergeActionsReturnsMergedKeys(keys1, keys2, expectedKeys);
+ }
+
+ @Test
+ public void mergeActions_ifFirstActionKeysEmpty_returnsEmptyKeys() {
+ StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
+ StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1);
+ StreamKey[] keys1 = new StreamKey[] {};
+ StreamKey[] keys2 = new StreamKey[] {streamKey2, streamKey1};
+ StreamKey[] expectedKeys = new StreamKey[] {};
+
+ doTestMergeActionsReturnsMergedKeys(keys1, keys2, expectedKeys);
+ }
+
+ @Test
+ public void mergeActions_ifNotFirstActionKeysEmpty_returnsEmptyKeys() {
+ StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
+ StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1);
+ StreamKey[] keys1 = new StreamKey[] {streamKey2, streamKey1};
+ StreamKey[] keys2 = new StreamKey[] {};
+ StreamKey[] expectedKeys = new StreamKey[] {};
+
+ doTestMergeActionsReturnsMergedKeys(keys1, keys2, expectedKeys);
+ }
+
+ private void doTestMergeActionsReturnsMergedKeys(
+ StreamKey[] keys1, StreamKey[] keys2, StreamKey[] expectedKeys) {
+ DownloadAction action1 = createDownloadAction(uri1, Arrays.asList(keys1));
+ DownloadAction action2 = createDownloadAction(uri1, Arrays.asList(keys2));
+ ArrayDeque actionQueue = toActionQueue(action1, action2);
+
+ DownloadAction mergedAction = DownloadActionUtil.mergeActions(actionQueue);
+
+ assertThat(mergedAction.type).isEqualTo(action1.type);
+ assertThat(mergedAction.uri).isEqualTo(action1.uri);
+ assertThat(mergedAction.customCacheKey).isEqualTo(action1.customCacheKey);
+ assertThat(mergedAction.isRemoveAction).isEqualTo(action1.isRemoveAction);
+ assertThat(mergedAction.keys).containsExactly((Object[]) expectedKeys);
+ assertThat(actionQueue).containsExactly(mergedAction);
+ }
+
+ private ArrayDeque toActionQueue(DownloadAction... actions) {
+ return new ArrayDeque<>(Arrays.asList(actions));
+ }
+
+ private static DownloadAction createDownloadAction(Uri uri) {
+ return createDownloadAction(uri, Collections.emptyList());
+ }
+
+ private static DownloadAction createDownloadAction(Uri uri, List keys) {
+ return DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_PROGRESSIVE, uri, keys, /* customCacheKey= */ null, /* data= */ null);
+ }
+
+ private static DownloadAction createRemoveAction(Uri uri) {
+ return DownloadAction.createRemoveAction(
+ DownloadAction.TYPE_PROGRESSIVE, uri, /* customCacheKey= */ null);
+ }
+}
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
new file mode 100644
index 0000000000..5f287d8685
--- /dev/null
+++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java
@@ -0,0 +1,445 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.offline;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.Uri;
+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.offline.DownloadHelper.Callback;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.testutil.FakeRenderer;
+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.util.ConditionVariable;
+import com.google.android.exoplayer2.util.MimeTypes;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.shadows.ShadowLooper;
+
+/** Unit tests for {@link DownloadHelper}. */
+@RunWith(RobolectricTestRunner.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 Format VIDEO_FORMAT_LOW = createVideoFormat(/* bitrate= */ 200_000);
+ private static final Format VIDEO_FORMAT_HIGH = createVideoFormat(/* bitrate= */ 800_000);
+ private static final Format AUDIO_FORMAT_US = createAudioFormat(/* language= */ "US");
+ private static final Format AUDIO_FORMAT_ZH = createAudioFormat(/* language= */ "ZH");
+ private static final Format TEXT_FORMAT_US = createTextFormat(/* language= */ "US");
+ private static final Format TEXT_FORMAT_ZH = createTextFormat(/* language= */ "ZH");
+
+ private static final TrackGroup TRACK_GROUP_VIDEO_BOTH =
+ new TrackGroup(VIDEO_FORMAT_LOW, VIDEO_FORMAT_HIGH);
+ private static final TrackGroup TRACK_GROUP_VIDEO_SINGLE = new TrackGroup(VIDEO_FORMAT_LOW);
+ private static final TrackGroup TRACK_GROUP_AUDIO_US = new TrackGroup(AUDIO_FORMAT_US);
+ private static final TrackGroup TRACK_GROUP_AUDIO_ZH = new TrackGroup(AUDIO_FORMAT_ZH);
+ private static final TrackGroup TRACK_GROUP_TEXT_US = new TrackGroup(TEXT_FORMAT_US);
+ private static final TrackGroup TRACK_GROUP_TEXT_ZH = new TrackGroup(TEXT_FORMAT_ZH);
+ private static final TrackGroupArray TRACK_GROUP_ARRAY_ALL =
+ new TrackGroupArray(
+ TRACK_GROUP_VIDEO_BOTH,
+ TRACK_GROUP_AUDIO_US,
+ TRACK_GROUP_AUDIO_ZH,
+ TRACK_GROUP_TEXT_US,
+ TRACK_GROUP_TEXT_ZH);
+ private static final TrackGroupArray TRACK_GROUP_ARRAY_SINGLE =
+ new TrackGroupArray(TRACK_GROUP_VIDEO_SINGLE, TRACK_GROUP_AUDIO_US);
+ private static final TrackGroupArray[] TRACK_GROUP_ARRAYS =
+ new TrackGroupArray[] {TRACK_GROUP_ARRAY_ALL, TRACK_GROUP_ARRAY_SINGLE};
+
+ private Uri testUri;
+
+ private FakeDownloadHelper downloadHelper;
+
+ @Before
+ public void setUp() {
+ testUri = Uri.parse("http://test.uri");
+
+ FakeRenderer videoRenderer = new FakeRenderer(VIDEO_FORMAT_LOW, VIDEO_FORMAT_HIGH);
+ FakeRenderer audioRenderer = new FakeRenderer(AUDIO_FORMAT_US, AUDIO_FORMAT_ZH);
+ FakeRenderer textRenderer = new FakeRenderer(TEXT_FORMAT_US, TEXT_FORMAT_ZH);
+ RenderersFactory renderersFactory =
+ (handler, videoListener, audioListener, metadata, text, drm) ->
+ new Renderer[] {textRenderer, audioRenderer, videoRenderer};
+
+ downloadHelper = new FakeDownloadHelper(testUri, renderersFactory);
+ }
+
+ @Test
+ public void getManifest_returnsManifest() throws Exception {
+ prepareDownloadHelper(downloadHelper);
+
+ ManifestType manifest = downloadHelper.getManifest();
+
+ assertThat(manifest).isEqualTo(TEST_MANIFEST);
+ }
+
+ @Test
+ public void getPeriodCount_returnsPeriodCount() throws Exception {
+ prepareDownloadHelper(downloadHelper);
+
+ int periodCount = downloadHelper.getPeriodCount();
+
+ assertThat(periodCount).isEqualTo(2);
+ }
+
+ @Test
+ public void getTrackGroups_returnsTrackGroups() throws Exception {
+ prepareDownloadHelper(downloadHelper);
+
+ TrackGroupArray trackGroupArrayPeriod0 = downloadHelper.getTrackGroups(/* periodIndex= */ 0);
+ TrackGroupArray trackGroupArrayPeriod1 = downloadHelper.getTrackGroups(/* periodIndex= */ 1);
+
+ assertThat(trackGroupArrayPeriod0).isEqualTo(TRACK_GROUP_ARRAYS[0]);
+ assertThat(trackGroupArrayPeriod1).isEqualTo(TRACK_GROUP_ARRAYS[1]);
+ }
+
+ @Test
+ public void getMappedTrackInfo_returnsMappedTrackInfo() throws Exception {
+ prepareDownloadHelper(downloadHelper);
+
+ MappedTrackInfo mappedTracks0 = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
+ MappedTrackInfo mappedTracks1 = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 1);
+
+ assertThat(mappedTracks0.getRendererCount()).isEqualTo(3);
+ assertThat(mappedTracks0.getRendererType(/* rendererIndex= */ 0)).isEqualTo(C.TRACK_TYPE_TEXT);
+ assertThat(mappedTracks0.getRendererType(/* rendererIndex= */ 1)).isEqualTo(C.TRACK_TYPE_AUDIO);
+ assertThat(mappedTracks0.getRendererType(/* rendererIndex= */ 2)).isEqualTo(C.TRACK_TYPE_VIDEO);
+ assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 0).length).isEqualTo(2);
+ assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 1).length).isEqualTo(2);
+ assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 2).length).isEqualTo(1);
+ assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 0).get(/* index= */ 0))
+ .isEqualTo(TRACK_GROUP_TEXT_US);
+ assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 0).get(/* index= */ 1))
+ .isEqualTo(TRACK_GROUP_TEXT_ZH);
+ assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 1).get(/* index= */ 0))
+ .isEqualTo(TRACK_GROUP_AUDIO_US);
+ assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 1).get(/* index= */ 1))
+ .isEqualTo(TRACK_GROUP_AUDIO_ZH);
+ assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 2).get(/* index= */ 0))
+ .isEqualTo(TRACK_GROUP_VIDEO_BOTH);
+
+ assertThat(mappedTracks1.getRendererCount()).isEqualTo(3);
+ assertThat(mappedTracks1.getRendererType(/* rendererIndex= */ 0)).isEqualTo(C.TRACK_TYPE_TEXT);
+ assertThat(mappedTracks1.getRendererType(/* rendererIndex= */ 1)).isEqualTo(C.TRACK_TYPE_AUDIO);
+ assertThat(mappedTracks1.getRendererType(/* rendererIndex= */ 2)).isEqualTo(C.TRACK_TYPE_VIDEO);
+ assertThat(mappedTracks1.getTrackGroups(/* rendererIndex= */ 0).length).isEqualTo(0);
+ assertThat(mappedTracks1.getTrackGroups(/* rendererIndex= */ 1).length).isEqualTo(1);
+ assertThat(mappedTracks1.getTrackGroups(/* rendererIndex= */ 2).length).isEqualTo(1);
+ assertThat(mappedTracks1.getTrackGroups(/* rendererIndex= */ 1).get(/* index= */ 0))
+ .isEqualTo(TRACK_GROUP_AUDIO_US);
+ assertThat(mappedTracks1.getTrackGroups(/* rendererIndex= */ 2).get(/* index= */ 0))
+ .isEqualTo(TRACK_GROUP_VIDEO_SINGLE);
+ }
+
+ @Test
+ public void getTrackSelections_returnsInitialSelection() throws Exception {
+ prepareDownloadHelper(downloadHelper);
+
+ List selectedText0 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);
+ List selectedAudio0 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);
+ List selectedVideo0 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);
+ List selectedText1 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);
+ List selectedAudio1 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);
+ List selectedVideo1 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2);
+
+ assertSingleTrackSelectionEquals(selectedText0, TRACK_GROUP_TEXT_US, 0);
+ assertSingleTrackSelectionEquals(selectedAudio0, TRACK_GROUP_AUDIO_US, 0);
+ assertSingleTrackSelectionEquals(selectedVideo0, TRACK_GROUP_VIDEO_BOTH, 1);
+
+ assertThat(selectedText1).isEmpty();
+ assertSingleTrackSelectionEquals(selectedAudio1, TRACK_GROUP_AUDIO_US, 0);
+ assertSingleTrackSelectionEquals(selectedVideo1, TRACK_GROUP_VIDEO_SINGLE, 0);
+ }
+
+ @Test
+ public void getTrackSelections_afterClearTrackSelections_isEmpty() throws Exception {
+ prepareDownloadHelper(downloadHelper);
+
+ // Clear only one period selection to verify second period selection is untouched.
+ downloadHelper.clearTrackSelections(/* periodIndex= */ 0);
+ List selectedText0 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);
+ List selectedAudio0 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);
+ List selectedVideo0 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);
+ List selectedText1 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);
+ List selectedAudio1 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);
+ List selectedVideo1 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2);
+
+ assertThat(selectedText0).isEmpty();
+ assertThat(selectedAudio0).isEmpty();
+ assertThat(selectedVideo0).isEmpty();
+
+ // Verify
+ assertThat(selectedText1).isEmpty();
+ assertSingleTrackSelectionEquals(selectedAudio1, TRACK_GROUP_AUDIO_US, 0);
+ assertSingleTrackSelectionEquals(selectedVideo1, TRACK_GROUP_VIDEO_SINGLE, 0);
+ }
+
+ @Test
+ public void getTrackSelections_afterReplaceTrackSelections_returnsNewSelections()
+ throws Exception {
+ prepareDownloadHelper(downloadHelper);
+ DefaultTrackSelector.Parameters parameters =
+ new ParametersBuilder()
+ .setPreferredAudioLanguage("ZH")
+ .setPreferredTextLanguage("ZH")
+ .setRendererDisabled(/* rendererIndex= */ 2, true)
+ .build();
+
+ // Replace only one period selection to verify second period selection is untouched.
+ downloadHelper.replaceTrackSelections(/* periodIndex= */ 0, parameters);
+ List selectedText0 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);
+ List selectedAudio0 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);
+ List selectedVideo0 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);
+ List selectedText1 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);
+ List selectedAudio1 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);
+ List selectedVideo1 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2);
+
+ assertSingleTrackSelectionEquals(selectedText0, TRACK_GROUP_TEXT_ZH, 0);
+ assertSingleTrackSelectionEquals(selectedAudio0, TRACK_GROUP_AUDIO_ZH, 0);
+ assertThat(selectedVideo0).isEmpty();
+
+ assertThat(selectedText1).isEmpty();
+ assertSingleTrackSelectionEquals(selectedAudio1, TRACK_GROUP_AUDIO_US, 0);
+ assertSingleTrackSelectionEquals(selectedVideo1, TRACK_GROUP_VIDEO_SINGLE, 0);
+ }
+
+ @Test
+ public void getTrackSelections_afterAddTrackSelections_returnsCombinedSelections()
+ throws Exception {
+ prepareDownloadHelper(downloadHelper);
+ // Select parameters to require some merging of track groups because the new parameters add
+ // all video tracks to initial video single track selection.
+ DefaultTrackSelector.Parameters parameters =
+ new ParametersBuilder()
+ .setPreferredAudioLanguage("ZH")
+ .setPreferredTextLanguage("US")
+ .build();
+
+ // Add only to one period selection to verify second period selection is untouched.
+ downloadHelper.addTrackSelection(/* periodIndex= */ 0, parameters);
+ List selectedText0 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);
+ List selectedAudio0 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);
+ List selectedVideo0 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);
+ List selectedText1 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);
+ List selectedAudio1 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);
+ List selectedVideo1 =
+ downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2);
+
+ assertSingleTrackSelectionEquals(selectedText0, TRACK_GROUP_TEXT_US, 0);
+ assertThat(selectedAudio0).hasSize(2);
+ assertTrackSelectionEquals(selectedAudio0.get(0), TRACK_GROUP_AUDIO_US, 0);
+ assertTrackSelectionEquals(selectedAudio0.get(1), TRACK_GROUP_AUDIO_ZH, 0);
+ assertSingleTrackSelectionEquals(selectedVideo0, TRACK_GROUP_VIDEO_BOTH, 0, 1);
+
+ assertThat(selectedText1).isEmpty();
+ assertSingleTrackSelectionEquals(selectedAudio1, TRACK_GROUP_AUDIO_US, 0);
+ assertSingleTrackSelectionEquals(selectedVideo1, TRACK_GROUP_VIDEO_SINGLE, 0);
+ }
+
+ @Test
+ public void getDownloadAction_createsDownloadAction_withAllSelectedTracks() throws Exception {
+ prepareDownloadHelper(downloadHelper);
+ // Ensure we have track groups with multiple indices, renderers with multiple track groups and
+ // also renderers without any track groups.
+ DefaultTrackSelector.Parameters parameters =
+ new ParametersBuilder()
+ .setPreferredAudioLanguage("ZH")
+ .setPreferredTextLanguage("US")
+ .build();
+ downloadHelper.addTrackSelection(/* periodIndex= */ 0, parameters);
+ byte[] data = new byte[10];
+ Arrays.fill(data, (byte) 123);
+
+ DownloadAction downloadAction = downloadHelper.getDownloadAction(data);
+
+ assertThat(downloadAction.type).isEqualTo(TEST_DOWNLOAD_TYPE);
+ assertThat(downloadAction.uri).isEqualTo(testUri);
+ assertThat(downloadAction.customCacheKey).isEqualTo(TEST_CACHE_KEY);
+ assertThat(downloadAction.isRemoveAction).isFalse();
+ assertThat(downloadAction.data).isEqualTo(data);
+ assertThat(downloadAction.keys)
+ .containsExactly(
+ new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 0, /* trackIndex= */ 0),
+ new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 0, /* trackIndex= */ 1),
+ new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 0),
+ new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 2, /* trackIndex= */ 0),
+ new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 3, /* trackIndex= */ 0),
+ new StreamKey(/* periodIndex= */ 1, /* groupIndex= */ 0, /* trackIndex= */ 0),
+ new StreamKey(/* periodIndex= */ 1, /* groupIndex= */ 1, /* trackIndex= */ 0));
+ }
+
+ @Test
+ public void getRemoveAction_returnsRemoveAction() {
+ DownloadAction removeAction = downloadHelper.getRemoveAction();
+
+ assertThat(removeAction.type).isEqualTo(TEST_DOWNLOAD_TYPE);
+ assertThat(removeAction.uri).isEqualTo(testUri);
+ assertThat(removeAction.customCacheKey).isEqualTo(TEST_CACHE_KEY);
+ assertThat(removeAction.isRemoveAction).isTrue();
+ }
+
+ private static void prepareDownloadHelper(FakeDownloadHelper downloadHelper) throws Exception {
+ AtomicReference prepareException = new AtomicReference<>(null);
+ ConditionVariable preparedCondition = new ConditionVariable();
+ downloadHelper.prepare(
+ new Callback() {
+ @Override
+ public void onPrepared(DownloadHelper> helper) {
+ preparedCondition.open();
+ }
+
+ @Override
+ public void onPrepareError(DownloadHelper> helper, IOException e) {
+ prepareException.set(e);
+ preparedCondition.open();
+ }
+ });
+ while (!preparedCondition.block(0)) {
+ ShadowLooper.runMainLooperToNextTask();
+ }
+ if (prepareException.get() != null) {
+ throw prepareException.get();
+ }
+ }
+
+ private static Format createVideoFormat(int bitrate) {
+ return Format.createVideoSampleFormat(
+ /* id= */ null,
+ /* sampleMimeType= */ MimeTypes.VIDEO_H264,
+ /* codecs= */ null,
+ /* bitrate= */ bitrate,
+ /* maxInputSize= */ Format.NO_VALUE,
+ /* width= */ 480,
+ /* height= */ 360,
+ /* frameRate= */ Format.NO_VALUE,
+ /* initializationData= */ null,
+ /* drmInitData= */ null);
+ }
+
+ private static Format createAudioFormat(String language) {
+ return Format.createAudioSampleFormat(
+ /* id= */ null,
+ /* sampleMimeType= */ MimeTypes.AUDIO_AAC,
+ /* codecs= */ null,
+ /* bitrate= */ 48000,
+ /* maxInputSize= */ Format.NO_VALUE,
+ /* channelCount= */ 2,
+ /* sampleRate */ 44100,
+ /* initializationData= */ null,
+ /* drmInitData= */ null,
+ /* selectionFlags= */ C.SELECTION_FLAG_DEFAULT,
+ /* language= */ language);
+ }
+
+ private static Format createTextFormat(String language) {
+ return Format.createTextSampleFormat(
+ /* id= */ null,
+ /* sampleMimeType= */ MimeTypes.TEXT_VTT,
+ /* selectionFlags= */ C.SELECTION_FLAG_DEFAULT,
+ /* language= */ language);
+ }
+
+ private static void assertSingleTrackSelectionEquals(
+ List trackSelectionList, TrackGroup trackGroup, int... tracks) {
+ assertThat(trackSelectionList).hasSize(1);
+ assertTrackSelectionEquals(trackSelectionList.get(0), trackGroup, tracks);
+ }
+
+ private static void assertTrackSelectionEquals(
+ TrackSelection trackSelection, TrackGroup trackGroup, int... tracks) {
+ assertThat(trackSelection.getTrackGroup()).isEqualTo(trackGroup);
+ assertThat(trackSelection.length()).isEqualTo(tracks.length);
+ int[] selectedTracksInGroup = new int[trackSelection.length()];
+ for (int i = 0; i < trackSelection.length(); i++) {
+ selectedTracksInGroup[i] = trackSelection.getIndexInTrackGroup(i);
+ }
+ Arrays.sort(selectedTracksInGroup);
+ Arrays.sort(tracks);
+ assertThat(selectedTracksInGroup).isEqualTo(tracks);
+ }
+
+ private static final class ManifestType {}
+
+ private static final class FakeDownloadHelper extends DownloadHelper {
+
+ public FakeDownloadHelper(Uri testUri, RenderersFactory renderersFactory) {
+ super(
+ TEST_DOWNLOAD_TYPE,
+ testUri,
+ TEST_CACHE_KEY,
+ DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
+ renderersFactory,
+ /* drmSessionManager= */ null);
+ }
+
+ @Override
+ protected ManifestType loadManifest(Uri uri) throws IOException {
+ return TEST_MANIFEST;
+ }
+
+ @Override
+ protected TrackGroupArray[] getTrackGroupArrays(ManifestType manifest) {
+ assertThat(manifest).isEqualTo(TEST_MANIFEST);
+ return TRACK_GROUP_ARRAYS;
+ }
+
+ @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/DownloadManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java
index 294e8a14a2..e7d54b4141 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java
@@ -20,16 +20,17 @@ import static org.junit.Assert.fail;
import android.net.Uri;
import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
-import com.google.android.exoplayer2.offline.DownloadManager.TaskState.State;
+import com.google.android.exoplayer2.offline.DownloadManager.DownloadState;
+import com.google.android.exoplayer2.offline.DownloadManager.DownloadState.State;
import com.google.android.exoplayer2.testutil.DummyMainThread;
import com.google.android.exoplayer2.testutil.RobolectricUtil;
import com.google.android.exoplayer2.testutil.TestDownloadManagerListener;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException;
-import java.util.Collections;
-import java.util.IdentityHashMap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.After;
@@ -52,7 +53,9 @@ public class DownloadManagerTest {
private static final int ASSERT_FALSE_TIME = 1000;
/* Maximum retry delay in DownloadManager. */
private static final int MAX_RETRY_DELAY = 5000;
-
+ /* Maximum number of times a downloader can be restarted before doing a released check. */
+ private static final int MAX_STARTS_BEFORE_RELEASED = 1;
+ /** The minimum number of times a task must be retried before failing. */
private static final int MIN_RETRY_COUNT = 3;
private Uri uri1;
@@ -84,309 +87,329 @@ public class DownloadManagerTest {
}
@Test
- public void testDownloadActionRuns() throws Throwable {
- doTestDownloaderRuns(createDownloadRunner(uri1));
+ public void downloadRunner_multipleInstancePerContent_throwsException() {
+ boolean exceptionThrown = false;
+ try {
+ new DownloadRunner(uri1);
+ new DownloadRunner(uri1);
+ // can't put fail() here as it would be caught in the catch below.
+ } catch (Throwable e) {
+ exceptionThrown = true;
+ }
+ assertThat(exceptionThrown).isTrue();
}
@Test
- public void testRemoveActionRuns() throws Throwable {
- doTestDownloaderRuns(createRemoveRunner(uri1));
+ public void downloadRunner_handleActionReturnsDifferentTaskId_throwsException() {
+ DownloadRunner runner = new DownloadRunner(uri1).postDownloadAction();
+ TaskWrapper task = runner.getTask();
+ runner.setTask(new TaskWrapper(task.taskId + 10000));
+ boolean exceptionThrown = false;
+ try {
+ runner.postDownloadAction();
+ // can't put fail() here as it would be caught in the catch below.
+ } catch (Throwable e) {
+ exceptionThrown = true;
+ }
+ assertThat(exceptionThrown).isTrue();
}
@Test
- public void testDownloadRetriesThenFails() throws Throwable {
- DownloadRunner downloadRunner = createDownloadRunner(uri1);
- downloadRunner.postAction();
- FakeDownloader fakeDownloader = downloadRunner.downloader;
- fakeDownloader.enableDownloadIOException = true;
+ public void multipleActionsForTheSameContent_executedOnTheSameTask() {
+ // Two download actions on first task
+ new DownloadRunner(uri1).postDownloadAction().postDownloadAction();
+ // One download, one remove actions on second task
+ new DownloadRunner(uri2).postDownloadAction().postRemoveAction();
+ // Two remove actions on third task
+ new DownloadRunner(uri3).postRemoveAction().postRemoveAction();
+ }
+
+ @Test
+ public void actionsForDifferentContent_executedOnDifferentTasks() {
+ TaskWrapper task1 = new DownloadRunner(uri1).postDownloadAction().getTask();
+ TaskWrapper task2 = new DownloadRunner(uri2).postDownloadAction().getTask();
+ TaskWrapper task3 = new DownloadRunner(uri3).postRemoveAction().getTask();
+
+ assertThat(task1).isNoneOf(task2, task3);
+ assertThat(task2).isNotEqualTo(task3);
+ }
+
+ @Test
+ public void postDownloadAction_downloads() throws Throwable {
+ DownloadRunner runner = new DownloadRunner(uri1);
+ TaskWrapper task = runner.postDownloadAction().getTask();
+ task.assertStarted();
+ runner.getDownloader(0).unblock().assertReleased().assertStartCount(1);
+ task.assertCompleted();
+ runner.assertCreatedDownloaderCount(1);
+ downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
+ }
+
+ @Test
+ public void postRemoveAction_removes() throws Throwable {
+ DownloadRunner runner = new DownloadRunner(uri1);
+ TaskWrapper task = runner.postRemoveAction().getTask();
+ task.assertStarted();
+ runner.getDownloader(0).unblock().assertReleased().assertStartCount(1);
+ task.assertCompleted();
+ runner.assertCreatedDownloaderCount(1);
+ downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
+ }
+
+ @Test
+ public void downloadFails_retriesThenTaskFails() throws Throwable {
+ DownloadRunner runner = new DownloadRunner(uri1);
+ runner.postDownloadAction();
+ FakeDownloader downloader = runner.getDownloader(0);
+
for (int i = 0; i <= MIN_RETRY_COUNT; i++) {
- fakeDownloader.assertStarted(MAX_RETRY_DELAY).unblock();
+ downloader.assertStarted(MAX_RETRY_DELAY).fail();
}
- downloadRunner.assertFailed();
- downloadManagerListener.clearDownloadError();
- downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
+ downloader.assertReleased().assertStartCount(MIN_RETRY_COUNT + 1);
+ runner.getTask().assertFailed();
+ downloadManagerListener.blockUntilTasksComplete();
}
@Test
- public void testDownloadNoRetryWhenCanceled() throws Throwable {
- DownloadRunner downloadRunner = createDownloadRunner(uri1).ignoreInterrupts();
- downloadRunner.downloader.enableDownloadIOException = true;
- downloadRunner.postAction().assertStarted();
+ public void downloadFails_retries() throws Throwable {
+ DownloadRunner runner = new DownloadRunner(uri1);
+ runner.postDownloadAction();
+ FakeDownloader downloader = runner.getDownloader(0);
- DownloadRunner removeRunner = createRemoveRunner(uri1).postAction();
-
- downloadRunner.unblock().assertCanceled();
- removeRunner.unblock();
-
- downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
- }
-
- @Test
- public void testDownloadRetriesThenContinues() throws Throwable {
- DownloadRunner downloadRunner = createDownloadRunner(uri1);
- downloadRunner.postAction();
- FakeDownloader fakeDownloader = downloadRunner.downloader;
- fakeDownloader.enableDownloadIOException = true;
- for (int i = 0; i <= MIN_RETRY_COUNT; i++) {
- fakeDownloader.assertStarted(MAX_RETRY_DELAY);
- if (i == MIN_RETRY_COUNT) {
- fakeDownloader.enableDownloadIOException = false;
- }
- fakeDownloader.unblock();
+ for (int i = 0; i < MIN_RETRY_COUNT; i++) {
+ downloader.assertStarted(MAX_RETRY_DELAY).fail();
}
- downloadRunner.assertCompleted();
+ downloader.assertStarted(MAX_RETRY_DELAY).unblock();
- downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
+ downloader.assertReleased().assertStartCount(MIN_RETRY_COUNT + 1);
+ runner.getTask().assertCompleted();
+ downloadManagerListener.blockUntilTasksComplete();
}
@Test
- @SuppressWarnings({"NonAtomicVolatileUpdate", "NonAtomicOperationOnVolatileField"})
- public void testDownloadRetryCountResetsOnProgress() throws Throwable {
- DownloadRunner downloadRunner = createDownloadRunner(uri1);
- downloadRunner.postAction();
- FakeDownloader fakeDownloader = downloadRunner.downloader;
- fakeDownloader.enableDownloadIOException = true;
- fakeDownloader.downloadedBytes = 0;
- for (int i = 0; i <= MIN_RETRY_COUNT + 10; i++) {
- fakeDownloader.assertStarted(MAX_RETRY_DELAY);
- fakeDownloader.downloadedBytes++;
- if (i == MIN_RETRY_COUNT + 10) {
- fakeDownloader.enableDownloadIOException = false;
- }
- fakeDownloader.unblock();
+ public void downloadProgressOnRetry_retryCountResets() throws Throwable {
+ DownloadRunner runner = new DownloadRunner(uri1);
+ runner.postDownloadAction();
+ FakeDownloader downloader = runner.getDownloader(0);
+
+ int tooManyRetries = MIN_RETRY_COUNT + 10;
+ for (int i = 0; i < tooManyRetries; i++) {
+ downloader.increaseDownloadedByteCount();
+ downloader.assertStarted(MAX_RETRY_DELAY).fail();
}
- downloadRunner.assertCompleted();
+ downloader.assertStarted(MAX_RETRY_DELAY).unblock();
+ downloader.assertReleased().assertStartCount(tooManyRetries + 1);
+ runner.getTask().assertCompleted();
+ downloadManagerListener.blockUntilTasksComplete();
+ }
+
+ @Test
+ public void removeCancelsDownload() throws Throwable {
+ DownloadRunner runner = new DownloadRunner(uri1);
+ FakeDownloader downloader1 = runner.getDownloader(0);
+
+ runner.postDownloadAction();
+ downloader1.assertStarted();
+ runner.postRemoveAction();
+
+ downloader1.assertCanceled().assertStartCount(1);
+ runner.getDownloader(1).unblock().assertNotCanceled();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
@Test
- public void testDifferentMediaDownloadActionsStartInParallel() throws Throwable {
- doTestDownloadersRunInParallel(createDownloadRunner(uri1), createDownloadRunner(uri2));
- }
+ public void downloadNotCancelRemove() throws Throwable {
+ DownloadRunner runner = new DownloadRunner(uri1);
+ FakeDownloader downloader1 = runner.getDownloader(0);
- @Test
- public void testDifferentMediaDifferentActionsStartInParallel() throws Throwable {
- doTestDownloadersRunInParallel(createDownloadRunner(uri1), createRemoveRunner(uri2));
- }
+ runner.postRemoveAction();
+ downloader1.assertStarted();
+ runner.postDownloadAction();
- @Test
- public void testSameMediaDownloadActionsStartInParallel() throws Throwable {
- doTestDownloadersRunInParallel(createDownloadRunner(uri1), createDownloadRunner(uri1));
- }
-
- @Test
- public void testSameMediaRemoveActionWaitsDownloadAction() throws Throwable {
- doTestDownloadersRunSequentially(createDownloadRunner(uri1), createRemoveRunner(uri1));
- }
-
- @Test
- public void testSameMediaDownloadActionWaitsRemoveAction() throws Throwable {
- doTestDownloadersRunSequentially(createRemoveRunner(uri1), createDownloadRunner(uri1));
- }
-
- @Test
- public void testSameMediaRemoveActionWaitsRemoveAction() throws Throwable {
- doTestDownloadersRunSequentially(createRemoveRunner(uri1), createRemoveRunner(uri1));
- }
-
- @Test
- public void testSameMediaMultipleActions() throws Throwable {
- DownloadRunner downloadAction1 = createDownloadRunner(uri1).ignoreInterrupts();
- DownloadRunner downloadAction2 = createDownloadRunner(uri1).ignoreInterrupts();
- DownloadRunner removeAction1 = createRemoveRunner(uri1);
- DownloadRunner downloadAction3 = createDownloadRunner(uri1);
- DownloadRunner removeAction2 = createRemoveRunner(uri1);
-
- // Two download actions run in parallel.
- downloadAction1.postAction().assertStarted();
- downloadAction2.postAction().assertStarted();
- // removeAction1 is added. It interrupts the two download actions' threads but they are
- // configured to ignore it so removeAction1 doesn't start.
- removeAction1.postAction().assertDoesNotStart();
-
- // downloadAction2 finishes but it isn't enough to start removeAction1.
- downloadAction2.unblock().assertCanceled();
- removeAction1.assertDoesNotStart();
- // downloadAction3 is postAction to DownloadManager but it waits for removeAction1 to finish.
- downloadAction3.postAction().assertDoesNotStart();
-
- // When downloadAction1 finishes, removeAction1 starts.
- downloadAction1.unblock().assertCanceled();
- removeAction1.assertStarted();
- // downloadAction3 still waits removeAction1
- downloadAction3.assertDoesNotStart();
-
- // removeAction2 is posted. removeAction1 and downloadAction3 is canceled so removeAction2
- // starts immediately.
- removeAction2.postAction();
- removeAction1.assertCanceled();
- downloadAction3.assertCanceled();
- removeAction2.assertStarted().unblock().assertCompleted();
+ downloader1.unblock().assertNotCanceled();
+ runner.getDownloader(1).unblock().assertNotCanceled();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
@Test
- public void testMultipleRemoveActionWaitsLastCancelsAllOther() throws Throwable {
- DownloadRunner removeAction1 = createRemoveRunner(uri1).ignoreInterrupts();
- DownloadRunner removeAction2 = createRemoveRunner(uri1);
- DownloadRunner removeAction3 = createRemoveRunner(uri1);
+ public void secondSameRemoveActionIgnored() throws Throwable {
+ DownloadRunner runner = new DownloadRunner(uri1);
+ FakeDownloader downloader1 = runner.getDownloader(0);
- removeAction1.postAction().assertStarted();
- removeAction2.postAction().assertDoesNotStart();
- removeAction3.postAction().assertDoesNotStart();
-
- removeAction2.assertCanceled();
-
- removeAction1.unblock().assertCanceled();
- removeAction3.assertStarted().unblock().assertCompleted();
+ runner.postRemoveAction();
+ downloader1.assertStarted();
+ runner.postRemoveAction();
+ downloader1.unblock().assertNotCanceled();
+ runner.getTask().assertCompleted();
+ runner.assertCreatedDownloaderCount(1);
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
@Test
- public void testGetTasks() throws Throwable {
- DownloadRunner removeAction = createRemoveRunner(uri1);
- DownloadRunner downloadAction1 = createDownloadRunner(uri1);
- DownloadRunner downloadAction2 = createDownloadRunner(uri1);
+ public void secondSameDownloadActionIgnored() throws Throwable {
+ DownloadRunner runner = new DownloadRunner(uri1);
+ FakeDownloader downloader1 = runner.getDownloader(0);
- removeAction.postAction().assertStarted();
- downloadAction1.postAction().assertDoesNotStart();
- downloadAction2.postAction().assertDoesNotStart();
+ runner.postDownloadAction();
+ downloader1.assertStarted();
+ runner.postDownloadAction();
+
+ downloader1.unblock().assertNotCanceled();
+ runner.getTask().assertCompleted();
+ runner.assertCreatedDownloaderCount(1);
+ downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
+ }
+
+ @Test
+ public void differentDownloadActionsMerged() throws Throwable {
+ DownloadRunner runner = new DownloadRunner(uri1);
+ FakeDownloader downloader1 = runner.getDownloader(0);
+
+ StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
+ StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1);
+
+ runner.postDownloadAction(streamKey1);
+ downloader1.assertStarted();
+ runner.postDownloadAction(streamKey2);
+
+ downloader1.unblock().assertCanceled();
+
+ FakeDownloader downloader2 = runner.getDownloader(1);
+ downloader2.assertStarted();
+ assertThat(downloader2.action.keys).containsExactly(streamKey1, streamKey2);
+ downloader2.unblock();
+
+ runner.getTask().assertCompleted();
+ runner.assertCreatedDownloaderCount(2);
+ downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
+ }
+
+ @Test
+ public void actionsForDifferentContent_executedInParallel() throws Throwable {
+ DownloadRunner runner1 = new DownloadRunner(uri1).postDownloadAction();
+ DownloadRunner runner2 = new DownloadRunner(uri2).postDownloadAction();
+ FakeDownloader downloader1 = runner1.getDownloader(0);
+ FakeDownloader downloader2 = runner2.getDownloader(0);
+
+ downloader1.assertStarted();
+ downloader2.assertStarted();
+ downloader1.unblock();
+ downloader2.unblock();
+
+ runner1.getTask().assertCompleted();
+ runner2.getTask().assertCompleted();
+ downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
+ }
+
+ @Test
+ public void actionsForDifferentContent_ifMaxDownloadIs1_executedSequentially() throws Throwable {
+ setUpDownloadManager(1);
+ DownloadRunner runner1 = new DownloadRunner(uri1).postDownloadAction();
+ DownloadRunner runner2 = new DownloadRunner(uri2).postDownloadAction();
+ FakeDownloader downloader1 = runner1.getDownloader(0);
+ FakeDownloader downloader2 = runner2.getDownloader(0);
+
+ downloader1.assertStarted();
+ downloader2.assertDoesNotStart();
+ downloader1.unblock();
+ downloader2.assertStarted();
+ downloader2.unblock();
+
+ runner1.getTask().assertCompleted();
+ runner2.getTask().assertCompleted();
+ downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
+ }
+
+ @Test
+ public void removeActionForDifferentContent_ifMaxDownloadIs1_executedInParallel()
+ throws Throwable {
+ setUpDownloadManager(1);
+ DownloadRunner runner1 = new DownloadRunner(uri1).postDownloadAction();
+ DownloadRunner runner2 = new DownloadRunner(uri2).postRemoveAction();
+ FakeDownloader downloader1 = runner1.getDownloader(0);
+ FakeDownloader downloader2 = runner2.getDownloader(0);
+
+ downloader1.assertStarted();
+ downloader2.assertStarted();
+ downloader1.unblock();
+ downloader2.unblock();
+
+ runner1.getTask().assertCompleted();
+ runner2.getTask().assertCompleted();
+ downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
+ }
+
+ @Test
+ public void downloadActionFollowingRemove_ifMaxDownloadIs1_isNotStarted() throws Throwable {
+ setUpDownloadManager(1);
+ DownloadRunner runner1 = new DownloadRunner(uri1).postDownloadAction();
+ DownloadRunner runner2 = new DownloadRunner(uri2).postRemoveAction().postDownloadAction();
+ FakeDownloader downloader1 = runner1.getDownloader(0);
+ FakeDownloader downloader2 = runner2.getDownloader(0);
+ FakeDownloader downloader3 = runner2.getDownloader(1);
+
+ downloader1.assertStarted();
+ downloader2.assertStarted();
+ downloader2.unblock();
+ downloader3.assertDoesNotStart();
+ downloader1.unblock();
+ downloader3.assertStarted();
+ downloader3.unblock();
+
+ runner1.getTask().assertCompleted();
+ runner2.getTask().assertCompleted();
+ downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
+ }
+
+ @Test
+ public void getTasks_returnTasks() {
+ TaskWrapper task1 = new DownloadRunner(uri1).postDownloadAction().getTask();
+ TaskWrapper task2 = new DownloadRunner(uri2).postDownloadAction().getTask();
+ TaskWrapper task3 = new DownloadRunner(uri3).postRemoveAction().getTask();
+
+ DownloadState[] states = downloadManager.getAllDownloadStates();
- TaskState[] states = downloadManager.getAllTaskStates();
assertThat(states).hasLength(3);
- assertThat(states[0].action).isEqualTo(removeAction.action);
- assertThat(states[1].action).isEqualTo(downloadAction1.action);
- assertThat(states[2].action).isEqualTo(downloadAction2.action);
+ String[] taskIds = {task1.taskId, task2.taskId, task3.taskId};
+ String[] stateTaskIds = {states[0].id, states[1].id, states[2].id};
+ assertThat(stateTaskIds).isEqualTo(taskIds);
}
@Test
- public void testMultipleWaitingDownloadActionStartsInParallel() throws Throwable {
- DownloadRunner removeAction = createRemoveRunner(uri1);
- DownloadRunner downloadAction1 = createDownloadRunner(uri1);
- DownloadRunner downloadAction2 = createDownloadRunner(uri1);
+ public void stopAndResume() throws Throwable {
+ DownloadRunner runner1 = new DownloadRunner(uri1);
+ DownloadRunner runner2 = new DownloadRunner(uri2);
+ DownloadRunner runner3 = new DownloadRunner(uri3);
- removeAction.postAction().assertStarted();
- downloadAction1.postAction().assertDoesNotStart();
- downloadAction2.postAction().assertDoesNotStart();
-
- removeAction.unblock().assertCompleted();
- downloadAction1.assertStarted();
- downloadAction2.assertStarted();
- downloadAction1.unblock().assertCompleted();
- downloadAction2.unblock().assertCompleted();
-
- downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
- }
-
- @Test
- public void testDifferentMediaDownloadActionsPreserveOrder() throws Throwable {
- DownloadRunner removeRunner = createRemoveRunner(uri1).ignoreInterrupts();
- DownloadRunner downloadRunner1 = createDownloadRunner(uri1);
- DownloadRunner downloadRunner2 = createDownloadRunner(uri2);
-
- removeRunner.postAction().assertStarted();
- downloadRunner1.postAction().assertDoesNotStart();
- downloadRunner2.postAction().assertDoesNotStart();
-
- removeRunner.unblock().assertCompleted();
- downloadRunner1.assertStarted();
- downloadRunner2.assertStarted();
- downloadRunner1.unblock().assertCompleted();
- downloadRunner2.unblock().assertCompleted();
-
- downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
- }
-
- @Test
- public void testDifferentMediaRemoveActionsDoNotPreserveOrder() throws Throwable {
- DownloadRunner downloadRunner = createDownloadRunner(uri1).ignoreInterrupts();
- DownloadRunner removeRunner1 = createRemoveRunner(uri1);
- DownloadRunner removeRunner2 = createRemoveRunner(uri2);
-
- downloadRunner.postAction().assertStarted();
- removeRunner1.postAction().assertDoesNotStart();
- removeRunner2.postAction().assertStarted();
-
- downloadRunner.unblock().assertCanceled();
- removeRunner2.unblock().assertCompleted();
-
- removeRunner1.assertStarted();
- removeRunner1.unblock().assertCompleted();
-
- downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
- }
-
- @Test
- public void testStopAndResume() throws Throwable {
- DownloadRunner download1Runner = createDownloadRunner(uri1);
- DownloadRunner remove2Runner = createRemoveRunner(uri2);
- DownloadRunner download2Runner = createDownloadRunner(uri2);
- DownloadRunner remove1Runner = createRemoveRunner(uri1);
- DownloadRunner download3Runner = createDownloadRunner(uri3);
-
- download1Runner.postAction().assertStarted();
- remove2Runner.postAction().assertStarted();
- download2Runner.postAction().assertDoesNotStart();
+ runner1.postDownloadAction().getTask().assertStarted();
+ runner2.postRemoveAction().getTask().assertStarted();
+ runner2.postDownloadAction();
runOnMainThread(() -> downloadManager.stopDownloads());
- download1Runner.assertStopped();
+ runner1.getTask().assertQueued();
// remove actions aren't stopped.
- remove2Runner.unblock().assertCompleted();
+ runner2.getDownloader(0).unblock().assertReleased();
+ runner2.getTask().assertQueued();
// Although remove2 is finished, download2 doesn't start.
- download2Runner.assertDoesNotStart();
+ runner2.getDownloader(1).assertDoesNotStart();
// When a new remove action is added, it cancels stopped download actions with the same media.
- remove1Runner.postAction();
- download1Runner.assertCanceled();
- remove1Runner.assertStarted().unblock().assertCompleted();
+ runner1.postRemoveAction();
+ runner1.getDownloader(1).assertStarted().unblock();
+ runner1.getTask().assertCompleted();
// New download actions can be added but they don't start.
- download3Runner.postAction().assertDoesNotStart();
+ runner3.postDownloadAction().getDownloader(0).assertDoesNotStart();
runOnMainThread(() -> downloadManager.startDownloads());
- download2Runner.assertStarted().unblock().assertCompleted();
- download3Runner.assertStarted().unblock().assertCompleted();
-
- downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
- }
-
- @Test
- public void testResumeBeforeTotallyStopped() throws Throwable {
- setUpDownloadManager(2);
- DownloadRunner download1Runner = createDownloadRunner(uri1).ignoreInterrupts();
- DownloadRunner download2Runner = createDownloadRunner(uri2);
- DownloadRunner download3Runner = createDownloadRunner(uri3);
-
- download1Runner.postAction().assertStarted();
- download2Runner.postAction().assertStarted();
- // download3 doesn't start as DM was configured to run two downloads in parallel.
- download3Runner.postAction().assertDoesNotStart();
-
- runOnMainThread(() -> downloadManager.stopDownloads());
-
- // download1 doesn't stop yet as it ignores interrupts.
- download2Runner.assertStopped();
-
- runOnMainThread(() -> downloadManager.startDownloads());
-
- // download2 starts immediately.
- download2Runner.assertStarted();
-
- // download3 doesn't start as download1 still holds its slot.
- download3Runner.assertDoesNotStart();
-
- // when unblocked download1 stops and starts immediately.
- download1Runner.unblock().assertStopped().assertStarted();
-
- download1Runner.unblock();
- download2Runner.unblock();
- download3Runner.unblock();
+ runner2.getDownloader(1).assertStarted().unblock();
+ runner3.getDownloader(0).assertStarted().unblock();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
@@ -419,101 +442,103 @@ public class DownloadManagerTest {
}
}
- private void doTestDownloaderRuns(DownloadRunner runner) throws Throwable {
- runner.postAction().assertStarted().unblock().assertCompleted();
- downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
- }
-
- private void doTestDownloadersRunSequentially(DownloadRunner runner1, DownloadRunner runner2)
- throws Throwable {
- runner1.ignoreInterrupts().postAction().assertStarted();
- runner2.postAction().assertDoesNotStart();
-
- runner1.unblock();
- runner2.assertStarted();
-
- runner2.unblock().assertCompleted();
- downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
- }
-
- private void doTestDownloadersRunInParallel(DownloadRunner runner1, DownloadRunner runner2)
- throws Throwable {
- runner1.postAction().assertStarted();
- runner2.postAction().assertStarted();
- runner1.unblock().assertCompleted();
- runner2.unblock().assertCompleted();
- downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
- }
-
- private DownloadRunner createDownloadRunner(Uri uri) {
- return new DownloadRunner(uri, /* isRemoveAction= */ false);
- }
-
- private DownloadRunner createRemoveRunner(Uri uri) {
- return new DownloadRunner(uri, /* isRemoveAction= */ true);
- }
-
private void runOnMainThread(final Runnable r) {
dummyMainThread.runOnMainThread(r);
}
- private class DownloadRunner {
+ private final class DownloadRunner {
- public final DownloadAction action;
- public final FakeDownloader downloader;
+ private final Uri uri;
+ private final ArrayList downloaders;
+ private int createdDownloaderCount = 0;
+ private FakeDownloader downloader;
+ private TaskWrapper taskWrapper;
- private DownloadRunner(Uri uri, boolean isRemoveAction) {
- action =
- isRemoveAction
- ? DownloadAction.createRemoveAction(
- DownloadAction.TYPE_PROGRESSIVE, uri, /* customCacheKey= */ null)
- : DownloadAction.createDownloadAction(
- DownloadAction.TYPE_PROGRESSIVE,
- uri,
- /* keys= */ Collections.emptyList(),
- /* customCacheKey= */ null,
- /* data= */ null);
- downloader = new FakeDownloader(isRemoveAction);
- downloaderFactory.putFakeDownloader(action, downloader);
+ private DownloadRunner(Uri uri) {
+ this.uri = uri;
+ downloaders = new ArrayList<>();
+ downloader = addDownloader();
+ downloaderFactory.registerDownloadRunner(this);
}
- private DownloadRunner postAction() {
+ private DownloadRunner postRemoveAction() {
+ return postAction(createRemoveAction(uri));
+ }
+
+ private DownloadRunner postDownloadAction(StreamKey... keys) {
+ return postAction(createDownloadAction(uri, keys));
+ }
+
+ private DownloadRunner postAction(DownloadAction action) {
runOnMainThread(() -> downloadManager.handleAction(action));
+ if (taskWrapper == null) {
+ taskWrapper = new TaskWrapper(action.id);
+ } else {
+ assertThat(action.id).isEqualTo(taskWrapper.taskId);
+ }
return this;
}
- private DownloadRunner assertDoesNotStart() throws InterruptedException {
- Thread.sleep(ASSERT_FALSE_TIME);
- assertThat(downloader.started.getCount()).isEqualTo(1);
- return this;
+ private synchronized FakeDownloader addDownloader() {
+ FakeDownloader fakeDownloader = new FakeDownloader();
+ downloaders.add(fakeDownloader);
+ return fakeDownloader;
}
- private DownloadRunner assertStarted() throws InterruptedException {
- downloader.assertStarted(ASSERT_TRUE_TIMEOUT);
- return assertState(TaskState.STATE_STARTED);
+ private synchronized FakeDownloader getDownloader(int index) {
+ while (downloaders.size() <= index) {
+ addDownloader();
+ }
+ return downloaders.get(index);
}
- private DownloadRunner assertCompleted() {
- return assertState(TaskState.STATE_COMPLETED);
+ private synchronized Downloader createDownloader(DownloadAction action) {
+ downloader = getDownloader(createdDownloaderCount++);
+ downloader.action = action;
+ return downloader;
}
- private DownloadRunner assertFailed() {
- return assertState(TaskState.STATE_FAILED);
+ private TaskWrapper getTask() {
+ return taskWrapper;
}
- private DownloadRunner assertCanceled() {
- return assertState(TaskState.STATE_CANCELED);
+ public void setTask(TaskWrapper taskWrapper) {
+ this.taskWrapper = taskWrapper;
}
- private DownloadRunner assertStopped() {
- return assertState(TaskState.STATE_QUEUED);
+ private void assertCreatedDownloaderCount(int count) {
+ assertThat(createdDownloaderCount).isEqualTo(count);
+ }
+ }
+
+ private final class TaskWrapper {
+ private final String taskId;
+
+ private TaskWrapper(String taskId) {
+ this.taskId = taskId;
}
- private DownloadRunner assertState(@State int expectedState) {
+ private TaskWrapper assertStarted() throws InterruptedException {
+ return assertState(DownloadState.STATE_STARTED);
+ }
+
+ private TaskWrapper assertCompleted() {
+ return assertState(DownloadState.STATE_COMPLETED);
+ }
+
+ private TaskWrapper assertFailed() {
+ return assertState(DownloadState.STATE_FAILED);
+ }
+
+ private TaskWrapper assertQueued() {
+ return assertState(DownloadState.STATE_QUEUED);
+ }
+
+ private TaskWrapper assertState(@State int expectedState) {
while (true) {
Integer state = null;
try {
- state = downloadManagerListener.pollStateChange(action, ASSERT_TRUE_TIMEOUT);
+ state = downloadManagerListener.pollStateChange(taskId, ASSERT_TRUE_TIMEOUT);
} catch (InterruptedException e) {
fail(e.getMessage());
}
@@ -523,69 +548,98 @@ public class DownloadManagerTest {
}
}
- private DownloadRunner unblock() {
- downloader.unblock();
- return this;
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ return taskId.equals(((TaskWrapper) o).taskId);
}
- private DownloadRunner ignoreInterrupts() {
- downloader.ignoreInterrupts = true;
- return this;
+ @Override
+ public int hashCode() {
+ return taskId.hashCode();
}
}
- private static class FakeDownloaderFactory implements DownloaderFactory {
+ private static DownloadAction createDownloadAction(Uri uri, StreamKey... keys) {
+ return DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_PROGRESSIVE,
+ uri,
+ Arrays.asList(keys),
+ /* customCacheKey= */ null,
+ /* data= */ null);
+ }
- public IdentityHashMap downloaders;
+ private static DownloadAction createRemoveAction(Uri uri) {
+ return DownloadAction.createRemoveAction(
+ DownloadAction.TYPE_PROGRESSIVE, uri, /* customCacheKey= */ null);
+ }
+
+ private static final class FakeDownloaderFactory implements DownloaderFactory {
+
+ private final HashMap downloaders;
public FakeDownloaderFactory() {
- downloaders = new IdentityHashMap<>();
+ downloaders = new HashMap<>();
}
- public void putFakeDownloader(DownloadAction action, FakeDownloader downloader) {
- downloaders.put(action, downloader);
+ public void registerDownloadRunner(DownloadRunner downloadRunner) {
+ assertThat(downloaders.put(downloadRunner.uri, downloadRunner)).isNull();
}
@Override
public Downloader createDownloader(DownloadAction action) {
- return downloaders.get(action);
+ return downloaders.get(action.uri).createDownloader(action);
}
}
- private static class FakeDownloader implements Downloader {
+ private static final class FakeDownloader implements Downloader {
private final com.google.android.exoplayer2.util.ConditionVariable blocker;
- private final boolean isRemove;
+ private DownloadAction action;
private CountDownLatch started;
- private boolean ignoreInterrupts;
+ private volatile boolean interrupted;
+ private volatile boolean cancelled;
private volatile boolean enableDownloadIOException;
- private volatile int downloadedBytes = C.LENGTH_UNSET;
+ private volatile int downloadedBytes;
+ private volatile int startCount;
- private FakeDownloader(boolean isRemove) {
- this.isRemove = isRemove;
+ private FakeDownloader() {
this.started = new CountDownLatch(1);
this.blocker = new com.google.android.exoplayer2.util.ConditionVariable();
+ downloadedBytes = C.LENGTH_UNSET;
}
+ @SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"})
@Override
public void download() throws InterruptedException, IOException {
- assertThat(isRemove).isFalse();
+ // It's ok to update this directly as no other thread will update it.
+ startCount++;
+ assertThat(action.isRemoveAction).isFalse();
started.countDown();
block();
if (enableDownloadIOException) {
+ enableDownloadIOException = false;
throw new IOException();
}
}
@Override
public void cancel() {
- // Do nothing.
+ cancelled = true;
}
+ @SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"})
@Override
public void remove() throws InterruptedException {
- assertThat(isRemove).isTrue();
+ // It's ok to update this directly as no other thread will update it.
+ startCount++;
+ assertThat(action.isRemoveAction).isTrue();
started.countDown();
block();
}
@@ -597,9 +651,8 @@ public class DownloadManagerTest {
blocker.block();
break;
} catch (InterruptedException e) {
- if (!ignoreInterrupts) {
- throw e;
- }
+ interrupted = true;
+ throw e;
}
}
} finally {
@@ -607,17 +660,56 @@ public class DownloadManagerTest {
}
}
+ private FakeDownloader assertStarted() throws InterruptedException {
+ return assertStarted(ASSERT_TRUE_TIMEOUT);
+ }
+
private FakeDownloader assertStarted(int timeout) throws InterruptedException {
assertThat(started.await(timeout, TimeUnit.MILLISECONDS)).isTrue();
started = new CountDownLatch(1);
return this;
}
+ private FakeDownloader assertStartCount(int count) throws InterruptedException {
+ assertThat(startCount).isEqualTo(count);
+ return this;
+ }
+
+ private FakeDownloader assertReleased() throws InterruptedException {
+ int count = 0;
+ while (started.await(ASSERT_TRUE_TIMEOUT, TimeUnit.MILLISECONDS)) {
+ if (count++ >= MAX_STARTS_BEFORE_RELEASED) {
+ fail();
+ }
+ started = new CountDownLatch(1);
+ }
+ return this;
+ }
+
+ private FakeDownloader assertCanceled() throws InterruptedException {
+ assertReleased();
+ assertThat(interrupted).isTrue();
+ assertThat(cancelled).isTrue();
+ return this;
+ }
+
+ private FakeDownloader assertNotCanceled() throws InterruptedException {
+ assertReleased();
+ assertThat(interrupted).isFalse();
+ assertThat(cancelled).isFalse();
+ return this;
+ }
+
private FakeDownloader unblock() {
blocker.open();
return this;
}
+ private FakeDownloader fail() {
+ enableDownloadIOException = true;
+ return unblock();
+ }
+
@Override
public long getDownloadedBytes() {
return downloadedBytes;
@@ -632,5 +724,15 @@ public class DownloadManagerTest {
public float getDownloadPercentage() {
return C.PERCENTAGE_UNSET;
}
+
+ private void assertDoesNotStart() throws InterruptedException {
+ Thread.sleep(ASSERT_FALSE_TIME);
+ assertThat(started.getCount()).isEqualTo(1);
+ }
+
+ @SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"})
+ private void increaseDownloadedByteCount() {
+ downloadedBytes++;
+ }
}
}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java
index fdf454e5df..72fff9606c 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java
@@ -63,6 +63,9 @@ public final class TtmlDecoderTest {
private static final String FONT_SIZE_INVALID_TTML_FILE = "ttml/font_size_invalid.xml";
private static final String FONT_SIZE_EMPTY_TTML_FILE = "ttml/font_size_empty.xml";
private static final String FRAME_RATE_TTML_FILE = "ttml/frame_rate.xml";
+ private static final String BITMAP_REGION_FILE = "ttml/bitmap_percentage_region.xml";
+ private static final String BITMAP_PIXEL_REGION_FILE = "ttml/bitmap_pixel_region.xml";
+ private static final String BITMAP_UNSUPPORTED_REGION_FILE = "ttml/bitmap_unsupported_region.xml";
@Test
public void testInlineAttributes() throws IOException, SubtitleDecoderException {
@@ -259,56 +262,56 @@ public final class TtmlDecoderTest {
@Test
public void testMultipleRegions() throws IOException, SubtitleDecoderException {
TtmlSubtitle subtitle = getSubtitle(MULTIPLE_REGIONS_TTML_FILE);
- List output = subtitle.getCues(1000000);
- assertThat(output).hasSize(2);
- Cue ttmlCue = output.get(0);
- assertThat(ttmlCue.text.toString()).isEqualTo("lorem");
- assertThat(ttmlCue.position).isEqualTo(10f / 100f);
- assertThat(ttmlCue.line).isEqualTo(10f / 100f);
- assertThat(ttmlCue.size).isEqualTo(20f / 100f);
+ List cues = subtitle.getCues(1000000);
+ assertThat(cues).hasSize(2);
+ Cue cue = cues.get(0);
+ assertThat(cue.text.toString()).isEqualTo("lorem");
+ assertThat(cue.position).isEqualTo(10f / 100f);
+ assertThat(cue.line).isEqualTo(10f / 100f);
+ assertThat(cue.size).isEqualTo(20f / 100f);
- ttmlCue = output.get(1);
- assertThat(ttmlCue.text.toString()).isEqualTo("amet");
- assertThat(ttmlCue.position).isEqualTo(60f / 100f);
- assertThat(ttmlCue.line).isEqualTo(10f / 100f);
- assertThat(ttmlCue.size).isEqualTo(20f / 100f);
+ cue = cues.get(1);
+ assertThat(cue.text.toString()).isEqualTo("amet");
+ assertThat(cue.position).isEqualTo(60f / 100f);
+ assertThat(cue.line).isEqualTo(10f / 100f);
+ assertThat(cue.size).isEqualTo(20f / 100f);
- output = subtitle.getCues(5000000);
- assertThat(output).hasSize(1);
- ttmlCue = output.get(0);
- assertThat(ttmlCue.text.toString()).isEqualTo("ipsum");
- assertThat(ttmlCue.position).isEqualTo(40f / 100f);
- assertThat(ttmlCue.line).isEqualTo(40f / 100f);
- assertThat(ttmlCue.size).isEqualTo(20f / 100f);
+ cues = subtitle.getCues(5000000);
+ assertThat(cues).hasSize(1);
+ cue = cues.get(0);
+ assertThat(cue.text.toString()).isEqualTo("ipsum");
+ assertThat(cue.position).isEqualTo(40f / 100f);
+ assertThat(cue.line).isEqualTo(40f / 100f);
+ assertThat(cue.size).isEqualTo(20f / 100f);
- output = subtitle.getCues(9000000);
- assertThat(output).hasSize(1);
- ttmlCue = output.get(0);
- assertThat(ttmlCue.text.toString()).isEqualTo("dolor");
- assertThat(ttmlCue.position).isEqualTo(Cue.DIMEN_UNSET);
- assertThat(ttmlCue.line).isEqualTo(Cue.DIMEN_UNSET);
- assertThat(ttmlCue.size).isEqualTo(Cue.DIMEN_UNSET);
+ cues = subtitle.getCues(9000000);
+ assertThat(cues).hasSize(1);
+ cue = cues.get(0);
+ assertThat(cue.text.toString()).isEqualTo("dolor");
+ assertThat(cue.position).isEqualTo(Cue.DIMEN_UNSET);
+ assertThat(cue.line).isEqualTo(Cue.DIMEN_UNSET);
+ assertThat(cue.size).isEqualTo(Cue.DIMEN_UNSET);
// TODO: Should be as below, once https://github.com/google/ExoPlayer/issues/2953 is fixed.
- // assertEquals(10f / 100f, ttmlCue.position);
- // assertEquals(80f / 100f, ttmlCue.line);
- // assertEquals(1f, ttmlCue.size);
+ // assertEquals(10f / 100f, cue.position);
+ // assertEquals(80f / 100f, cue.line);
+ // assertEquals(1f, cue.size);
- output = subtitle.getCues(21000000);
- assertThat(output).hasSize(1);
- ttmlCue = output.get(0);
- assertThat(ttmlCue.text.toString()).isEqualTo("She first said this");
- assertThat(ttmlCue.position).isEqualTo(45f / 100f);
- assertThat(ttmlCue.line).isEqualTo(45f / 100f);
- assertThat(ttmlCue.size).isEqualTo(35f / 100f);
- output = subtitle.getCues(25000000);
- ttmlCue = output.get(0);
- assertThat(ttmlCue.text.toString()).isEqualTo("She first said this\nThen this");
- output = subtitle.getCues(29000000);
- assertThat(output).hasSize(1);
- ttmlCue = output.get(0);
- assertThat(ttmlCue.text.toString()).isEqualTo("She first said this\nThen this\nFinally this");
- assertThat(ttmlCue.position).isEqualTo(45f / 100f);
- assertThat(ttmlCue.line).isEqualTo(45f / 100f);
+ cues = subtitle.getCues(21000000);
+ assertThat(cues).hasSize(1);
+ cue = cues.get(0);
+ assertThat(cue.text.toString()).isEqualTo("She first said this");
+ assertThat(cue.position).isEqualTo(45f / 100f);
+ assertThat(cue.line).isEqualTo(45f / 100f);
+ assertThat(cue.size).isEqualTo(35f / 100f);
+ cues = subtitle.getCues(25000000);
+ cue = cues.get(0);
+ assertThat(cue.text.toString()).isEqualTo("She first said this\nThen this");
+ cues = subtitle.getCues(29000000);
+ assertThat(cues).hasSize(1);
+ cue = cues.get(0);
+ assertThat(cue.text.toString()).isEqualTo("She first said this\nThen this\nFinally this");
+ assertThat(cue.position).isEqualTo(45f / 100f);
+ assertThat(cue.line).isEqualTo(45f / 100f);
}
@Test
@@ -499,6 +502,91 @@ public final class TtmlDecoderTest {
assertThat((double) subtitle.getEventTime(3)).isWithin(2000).of(2_002_000_000);
}
+ @Test
+ public void testBitmapPercentageRegion() throws IOException, SubtitleDecoderException {
+ TtmlSubtitle subtitle = getSubtitle(BITMAP_REGION_FILE);
+
+ List cues = subtitle.getCues(1000000);
+ assertThat(cues).hasSize(1);
+ Cue cue = cues.get(0);
+ assertThat(cue.text).isNull();
+ assertThat(cue.bitmap).isNotNull();
+ assertThat(cue.position).isEqualTo(24f / 100f);
+ assertThat(cue.line).isEqualTo(28f / 100f);
+ assertThat(cue.size).isEqualTo(51f / 100f);
+ assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
+
+ cues = subtitle.getCues(4000000);
+ assertThat(cues).hasSize(1);
+ cue = cues.get(0);
+ assertThat(cue.text).isNull();
+ assertThat(cue.bitmap).isNotNull();
+ assertThat(cue.position).isEqualTo(21f / 100f);
+ assertThat(cue.line).isEqualTo(35f / 100f);
+ assertThat(cue.size).isEqualTo(57f / 100f);
+ assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
+
+ cues = subtitle.getCues(7500000);
+ assertThat(cues).hasSize(1);
+ cue = cues.get(0);
+ assertThat(cue.text).isNull();
+ assertThat(cue.bitmap).isNotNull();
+ assertThat(cue.position).isEqualTo(24f / 100f);
+ assertThat(cue.line).isEqualTo(28f / 100f);
+ assertThat(cue.size).isEqualTo(51f / 100f);
+ assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
+ }
+
+ @Test
+ public void testBitmapPixelRegion() throws IOException, SubtitleDecoderException {
+ TtmlSubtitle subtitle = getSubtitle(BITMAP_PIXEL_REGION_FILE);
+
+ List cues = subtitle.getCues(1000000);
+ assertThat(cues).hasSize(1);
+ Cue cue = cues.get(0);
+ assertThat(cue.text).isNull();
+ assertThat(cue.bitmap).isNotNull();
+ assertThat(cue.position).isEqualTo(307f / 1280f);
+ assertThat(cue.line).isEqualTo(562f / 720f);
+ assertThat(cue.size).isEqualTo(653f / 1280f);
+ assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
+
+ cues = subtitle.getCues(4000000);
+ assertThat(cues).hasSize(1);
+ cue = cues.get(0);
+ assertThat(cue.text).isNull();
+ assertThat(cue.bitmap).isNotNull();
+ assertThat(cue.position).isEqualTo(269f / 1280f);
+ assertThat(cue.line).isEqualTo(612f / 720f);
+ assertThat(cue.size).isEqualTo(730f / 1280f);
+ assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
+ }
+
+ @Test
+ public void testBitmapUnsupportedRegion() throws IOException, SubtitleDecoderException {
+ TtmlSubtitle subtitle = getSubtitle(BITMAP_UNSUPPORTED_REGION_FILE);
+
+ List cues = subtitle.getCues(1000000);
+ assertThat(cues).hasSize(1);
+ Cue cue = cues.get(0);
+ assertThat(cue.text).isNull();
+ assertThat(cue.bitmap).isNotNull();
+ assertThat(cue.position).isEqualTo(Cue.DIMEN_UNSET);
+ assertThat(cue.line).isEqualTo(Cue.DIMEN_UNSET);
+ assertThat(cue.size).isEqualTo(Cue.DIMEN_UNSET);
+ assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
+
+ cues = subtitle.getCues(4000000);
+ assertThat(cues).hasSize(1);
+ cue = cues.get(0);
+ assertThat(cue.text).isNull();
+ assertThat(cue.bitmap).isNotNull();
+ assertThat(cue.position).isEqualTo(Cue.DIMEN_UNSET);
+ assertThat(cue.line).isEqualTo(Cue.DIMEN_UNSET);
+ assertThat(cue.size).isEqualTo(Cue.DIMEN_UNSET);
+ assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
+ }
+
private void assertSpans(
TtmlSubtitle subtitle,
int second,
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java
index 29018a520c..c3836e63f6 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java
@@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.testutil.FakeClock;
import com.google.android.exoplayer2.testutil.FakeMediaChunk;
+import com.google.android.exoplayer2.trackselection.TrackSelection.Definition;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.MimeTypes;
import java.util.ArrayList;
@@ -66,15 +67,20 @@ public final class AdaptiveTrackSelectionTest {
}
@Test
+ @SuppressWarnings("deprecation")
public void testFactoryUsesInitiallyProvidedBandwidthMeter() {
BandwidthMeter initialBandwidthMeter = mock(BandwidthMeter.class);
BandwidthMeter injectedBandwidthMeter = mock(BandwidthMeter.class);
- Format format = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);
- @SuppressWarnings("deprecation")
- AdaptiveTrackSelection adaptiveTrackSelection =
+ Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);
+ Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);
+ TrackSelection[] trackSelections =
new AdaptiveTrackSelection.Factory(initialBandwidthMeter)
- .createTrackSelection(new TrackGroup(format), injectedBandwidthMeter, /* tracks= */ 0);
- adaptiveTrackSelection.updateSelectedTrack(
+ .createTrackSelections(
+ new Definition[] {
+ new Definition(new TrackGroup(format1, format2), /* tracks= */ 0, 1)
+ },
+ injectedBandwidthMeter);
+ trackSelections[0].updateSelectedTrack(
/* playbackPositionUs= */ 0,
/* bufferedDurationUs= */ 0,
/* availableDurationUs= */ C.TIME_UNSET,
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java
index 9182074eb9..666fa87e9e 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java
@@ -247,7 +247,8 @@ public final class CacheDataSourceTest {
// Read partial at EOS but don't cross it so length is unknown.
CacheDataSource cacheDataSource = createCacheDataSource(false, true);
assertReadData(cacheDataSource, dataSpec, true);
- assertThat(cache.getContentLength(defaultCacheKey)).isEqualTo(C.LENGTH_UNSET);
+ assertThat(ContentMetadata.getContentLength(cache.getContentMetadata(defaultCacheKey)))
+ .isEqualTo(C.LENGTH_UNSET);
// Now do an unbounded request for whole data. This will cause a bounded request from upstream.
// End of data from upstream shouldn't be mixed up with EOS and cause length set wrong.
@@ -285,7 +286,8 @@ public final class CacheDataSourceTest {
cacheDataSource.close();
assertThat(upstream.getAndClearOpenedDataSpecs()).hasLength(1);
- assertThat(cache.getContentLength(defaultCacheKey)).isEqualTo(TEST_DATA.length);
+ assertThat(ContentMetadata.getContentLength(cache.getContentMetadata(defaultCacheKey)))
+ .isEqualTo(TEST_DATA.length);
}
@Test
@@ -467,11 +469,7 @@ public final class CacheDataSourceTest {
NavigableSet cachedSpans = cache.getCachedSpans(defaultCacheKey);
for (CacheSpan cachedSpan : cachedSpans) {
if (cachedSpan.position >= halfDataLength) {
- try {
- cache.removeSpan(cachedSpan);
- } catch (Cache.CacheException e) {
- // do nothing
- }
+ cache.removeSpan(cachedSpan);
}
}
@@ -516,7 +514,9 @@ public final class CacheDataSourceTest {
// If the request was unbounded then the content length should be cached, either because the
// content length was known or because EOS was read. If the request was bounded then the content
// length will not have been determined.
- assertThat(cache.getContentLength(customCacheKey ? this.customCacheKey : defaultCacheKey))
+ ContentMetadata metadata =
+ cache.getContentMetadata(customCacheKey ? this.customCacheKey : defaultCacheKey);
+ assertThat(ContentMetadata.getContentLength(metadata))
.isEqualTo(dataSpec.length == C.LENGTH_UNSET ? TEST_DATA.length : C.LENGTH_UNSET);
}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java
index d0bdfa2b83..4fbe93888e 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java
@@ -79,8 +79,11 @@ public final class CacheUtilTest {
}
@Override
- public long getContentLength(String key) {
- return contentLength;
+ public ContentMetadata getContentMetadata(String key) {
+ DefaultContentMetadata metadata = new DefaultContentMetadata();
+ ContentMetadataMutations mutations = new ContentMetadataMutations();
+ ContentMetadataMutations.setContentLength(mutations, contentLength);
+ return metadata.copyWithMutationsApplied(mutations);
}
}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java
index 6080f6eccd..57913f3652 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java
@@ -154,11 +154,11 @@ public class CachedContentIndexTest {
assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5);
ContentMetadata metadata = index.get("ABCDE").getMetadata();
- assertThat(ContentMetadataInternal.getContentLength(metadata)).isEqualTo(10);
+ assertThat(ContentMetadata.getContentLength(metadata)).isEqualTo(10);
assertThat(index.assignIdForKey("KLMNO")).isEqualTo(2);
ContentMetadata metadata2 = index.get("KLMNO").getMetadata();
- assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560);
+ assertThat(ContentMetadata.getContentLength(metadata2)).isEqualTo(2560);
}
@Test
@@ -172,12 +172,12 @@ public class CachedContentIndexTest {
assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5);
ContentMetadata metadata = index.get("ABCDE").getMetadata();
- assertThat(ContentMetadataInternal.getContentLength(metadata)).isEqualTo(10);
- assertThat(ContentMetadataInternal.getRedirectedUri(metadata)).isEqualTo(Uri.parse("abcde"));
+ assertThat(ContentMetadata.getContentLength(metadata)).isEqualTo(10);
+ assertThat(ContentMetadata.getRedirectedUri(metadata)).isEqualTo(Uri.parse("abcde"));
assertThat(index.assignIdForKey("KLMNO")).isEqualTo(2);
ContentMetadata metadata2 = index.get("KLMNO").getMetadata();
- assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560);
+ assertThat(ContentMetadata.getContentLength(metadata2)).isEqualTo(2560);
}
@Test
@@ -297,11 +297,11 @@ public class CachedContentIndexTest {
private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2)
throws IOException {
ContentMetadataMutations mutations1 = new ContentMetadataMutations();
- ContentMetadataInternal.setContentLength(mutations1, 2560);
+ ContentMetadataMutations.setContentLength(mutations1, 2560);
index.getOrAdd("KLMNO").applyMetadataMutations(mutations1);
ContentMetadataMutations mutations2 = new ContentMetadataMutations();
- ContentMetadataInternal.setContentLength(mutations2, 10);
- ContentMetadataInternal.setRedirectedUri(mutations2, Uri.parse("abcde"));
+ ContentMetadataMutations.setContentLength(mutations2, 10);
+ ContentMetadataMutations.setRedirectedUri(mutations2, Uri.parse("abcde"));
index.getOrAdd("ABCDE").applyMetadataMutations(mutations2);
index.store();
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java
index a4e444386a..c2b7f481fa 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java
@@ -47,6 +47,7 @@ import org.robolectric.RuntimeEnvironment;
public class SimpleCacheTest {
private static final String KEY_1 = "key1";
+ private static final String KEY_2 = "key2";
private File cacheDir;
@@ -105,18 +106,26 @@ public class SimpleCacheTest {
}
@Test
- public void testSetGetLength() throws Exception {
+ public void testSetGetContentMetadata() throws Exception {
SimpleCache simpleCache = getSimpleCache();
- assertThat(simpleCache.getContentLength(KEY_1)).isEqualTo(LENGTH_UNSET);
+ assertThat(ContentMetadata.getContentLength(simpleCache.getContentMetadata(KEY_1)))
+ .isEqualTo(LENGTH_UNSET);
- simpleCache.setContentLength(KEY_1, 15);
- assertThat(simpleCache.getContentLength(KEY_1)).isEqualTo(15);
+ ContentMetadataMutations mutations = new ContentMetadataMutations();
+ ContentMetadataMutations.setContentLength(mutations, 15);
+ simpleCache.applyContentMetadataMutations(KEY_1, mutations);
+ assertThat(ContentMetadata.getContentLength(simpleCache.getContentMetadata(KEY_1)))
+ .isEqualTo(15);
simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15);
- simpleCache.setContentLength(KEY_1, 150);
- assertThat(simpleCache.getContentLength(KEY_1)).isEqualTo(150);
+
+ mutations = new ContentMetadataMutations();
+ ContentMetadataMutations.setContentLength(mutations, 150);
+ simpleCache.applyContentMetadataMutations(KEY_1, mutations);
+ assertThat(ContentMetadata.getContentLength(simpleCache.getContentMetadata(KEY_1)))
+ .isEqualTo(150);
addCache(simpleCache, KEY_1, 140, 10);
@@ -124,14 +133,16 @@ public class SimpleCacheTest {
// Check if values are kept after cache is reloaded.
SimpleCache simpleCache2 = getSimpleCache();
- assertThat(simpleCache2.getContentLength(KEY_1)).isEqualTo(150);
+ assertThat(ContentMetadata.getContentLength(simpleCache2.getContentMetadata(KEY_1)))
+ .isEqualTo(150);
// Removing the last span shouldn't cause the length be change next time cache loaded
SimpleCacheSpan lastSpan = simpleCache2.startReadWrite(KEY_1, 145);
simpleCache2.removeSpan(lastSpan);
simpleCache2.release();
simpleCache2 = getSimpleCache();
- assertThat(simpleCache2.getContentLength(KEY_1)).isEqualTo(150);
+ assertThat(ContentMetadata.getContentLength(simpleCache2.getContentMetadata(KEY_1)))
+ .isEqualTo(150);
}
@Test
@@ -152,6 +163,40 @@ public class SimpleCacheTest {
assertCachedDataReadCorrect(cacheSpan2);
}
+ @Test
+ public void testReloadCacheWithoutRelease() throws Exception {
+ SimpleCache simpleCache = getSimpleCache();
+
+ // Write data for KEY_1.
+ CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
+ addCache(simpleCache, KEY_1, 0, 15);
+ simpleCache.releaseHoleSpan(cacheSpan1);
+ // Write and remove data for KEY_2.
+ CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_2, 0);
+ addCache(simpleCache, KEY_2, 0, 15);
+ simpleCache.releaseHoleSpan(cacheSpan2);
+ simpleCache.removeSpan(simpleCache.getCachedSpans(KEY_2).first());
+
+ // Don't release the cache. This means the index file wont have been written to disk after the
+ // data for KEY_2 was removed. Move the cache instead, so we can reload it without failing the
+ // folder locking check.
+ File cacheDir2 = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest");
+ cacheDir2.delete();
+ cacheDir.renameTo(cacheDir2);
+
+ // Reload the cache from its new location.
+ simpleCache = new SimpleCache(cacheDir2, new NoOpCacheEvictor());
+
+ // Read data back for KEY_1.
+ CacheSpan cacheSpan3 = simpleCache.startReadWrite(KEY_1, 0);
+ assertCachedDataReadCorrect(cacheSpan3);
+
+ // Check the entry for KEY_2 was removed when the cache was reloaded.
+ assertThat(simpleCache.getCachedSpans(KEY_2)).isEmpty();
+
+ Util.recursiveDelete(cacheDir2);
+ }
+
@Test
public void testEncryptedIndex() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
index 1f08a43731..c5ce70cfd3 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
@@ -26,6 +26,8 @@ import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.offline.FilteringManifestParser;
+import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory;
@@ -59,6 +61,7 @@ import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.text.ParseException;
import java.text.SimpleDateFormat;
+import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Matcher;
@@ -75,15 +78,16 @@ public final class DashMediaSource extends BaseMediaSource {
public static final class Factory implements AdsMediaSource.MediaSourceFactory {
private final DashChunkSource.Factory chunkSourceFactory;
- private final @Nullable DataSource.Factory manifestDataSourceFactory;
+ @Nullable private final DataSource.Factory manifestDataSourceFactory;
- private @Nullable ParsingLoadable.Parser extends DashManifest> manifestParser;
+ @Nullable private ParsingLoadable.Parser extends DashManifest> manifestParser;
+ @Nullable private List streamKeys;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private long livePresentationDelayMs;
private boolean livePresentationDelayOverridesManifest;
private boolean isCreateCalled;
- private @Nullable Object tag;
+ @Nullable private Object tag;
/**
* Creates a new factory for {@link DashMediaSource}s.
@@ -210,6 +214,19 @@ public final class DashMediaSource extends BaseMediaSource {
return this;
}
+ /**
+ * Sets a list of {@link StreamKey stream keys} by which the manifest is filtered.
+ *
+ * @param streamKeys A list of {@link StreamKey stream keys}.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ */
+ public Factory setStreamKeys(List streamKeys) {
+ Assertions.checkState(!isCreateCalled);
+ this.streamKeys = streamKeys;
+ return this;
+ }
+
/**
* Sets the factory to create composite {@link SequenceableLoader}s for when this media source
* loads data from multiple streams (video, audio etc...). The default is an instance of {@link
@@ -240,6 +257,9 @@ public final class DashMediaSource extends BaseMediaSource {
public DashMediaSource createMediaSource(DashManifest manifest) {
Assertions.checkArgument(!manifest.dynamic);
isCreateCalled = true;
+ if (streamKeys != null && !streamKeys.isEmpty()) {
+ manifest = manifest.copy(streamKeys);
+ }
return new DashMediaSource(
manifest,
/* manifestUri= */ null,
@@ -281,6 +301,9 @@ public final class DashMediaSource extends BaseMediaSource {
if (manifestParser == null) {
manifestParser = new DashManifestParser();
}
+ if (streamKeys != null) {
+ manifestParser = new FilteringManifestParser<>(manifestParser, streamKeys);
+ }
return new DashMediaSource(
/* manifest= */ null,
Assertions.checkNotNull(manifestUri),
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 f4e43f4641..f86e47ed3d 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
@@ -16,22 +16,25 @@
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.offline.TrackKey;
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.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
import java.io.IOException;
-import java.util.ArrayList;
import java.util.List;
/** A {@link DownloadHelper} for DASH streams. */
@@ -39,8 +42,52 @@ public final class DashDownloadHelper extends DownloadHelper {
private final DataSource.Factory manifestDataSourceFactory;
- public DashDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) {
- super(DownloadAction.TYPE_DASH, uri, /* cacheKey= */ null);
+ /**
+ * Creates a DASH download helper.
+ *
+ * The helper uses {@link DownloadHelper#DEFAULT_TRACK_SELECTOR_PARAMETERS} for track selection
+ * and does not support drm protected content.
+ *
+ * @param uri A manifest {@link Uri}.
+ * @param manifestDataSourceFactory A {@link DataSource.Factory} used to load the manifest.
+ * @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks
+ * are selected.
+ */
+ public DashDownloadHelper(
+ Uri uri, DataSource.Factory manifestDataSourceFactory, RenderersFactory renderersFactory) {
+ this(
+ uri,
+ manifestDataSourceFactory,
+ DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
+ renderersFactory,
+ /* drmSessionManager= */ null);
+ }
+
+ /**
+ * Creates a DASH download helper.
+ *
+ * @param uri A manifest {@link Uri}.
+ * @param manifestDataSourceFactory A {@link DataSource.Factory} used to load the manifest.
+ * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
+ * downloading.
+ * @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks
+ * are selected.
+ * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by
+ * {@code renderersFactory}.
+ */
+ public DashDownloadHelper(
+ Uri uri,
+ DataSource.Factory manifestDataSourceFactory,
+ DefaultTrackSelector.Parameters trackSelectorParameters,
+ RenderersFactory renderersFactory,
+ @Nullable DrmSessionManager drmSessionManager) {
+ super(
+ DownloadAction.TYPE_DASH,
+ uri,
+ /* cacheKey= */ null,
+ trackSelectorParameters,
+ renderersFactory,
+ drmSessionManager);
this.manifestDataSourceFactory = manifestDataSourceFactory;
}
@@ -72,12 +119,8 @@ public final class DashDownloadHelper extends DownloadHelper {
}
@Override
- protected List toStreamKeys(List trackKeys) {
- List streamKeys = new ArrayList<>(trackKeys.size());
- for (int i = 0; i < trackKeys.size(); i++) {
- TrackKey trackKey = trackKeys.get(i);
- streamKeys.add(new StreamKey(trackKey.periodIndex, trackKey.groupIndex, trackKey.trackIndex));
- }
- return streamKeys;
+ 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/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java
index a9b0c579ac..20374fad69 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java
@@ -22,6 +22,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory;
@@ -34,6 +35,7 @@ import com.google.android.exoplayer2.source.SinglePeriodTimeline;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistParserFactory;
import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker;
+import com.google.android.exoplayer2.source.hls.playlist.FilteringHlsPlaylistParserFactory;
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;
@@ -64,12 +66,13 @@ public final class HlsMediaSource extends BaseMediaSource
private HlsExtractorFactory extractorFactory;
private HlsPlaylistParserFactory playlistParserFactory;
+ @Nullable private List streamKeys;
private HlsPlaylistTracker.Factory playlistTrackerFactory;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private boolean allowChunklessPreparation;
private boolean isCreateCalled;
- private @Nullable Object tag;
+ @Nullable private Object tag;
/**
* Creates a new factory for {@link HlsMediaSource}s.
@@ -164,8 +167,8 @@ public final class HlsMediaSource extends BaseMediaSource
}
/**
- * Sets the factory from which playlist parsers will be obtained. The default value is created
- * by calling {@link DefaultHlsPlaylistParserFactory#DefaultHlsPlaylistParserFactory()}.
+ * Sets the factory from which playlist parsers will be obtained. The default value is a {@link
+ * DefaultHlsPlaylistParserFactory}.
*
* @param playlistParserFactory An {@link HlsPlaylistParserFactory}.
* @return This factory, for convenience.
@@ -177,6 +180,19 @@ public final class HlsMediaSource extends BaseMediaSource
return this;
}
+ /**
+ * Sets a list of {@link StreamKey stream keys} by which the playlists are filtered.
+ *
+ * @param streamKeys A list of {@link StreamKey stream keys}.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ */
+ public Factory setStreamKeys(List streamKeys) {
+ Assertions.checkState(!isCreateCalled);
+ this.streamKeys = streamKeys;
+ return this;
+ }
+
/**
* Sets the {@link HlsPlaylistTracker} factory. The default value is {@link
* DefaultHlsPlaylistTracker#FACTORY}.
@@ -232,6 +248,10 @@ public final class HlsMediaSource extends BaseMediaSource
@Override
public HlsMediaSource createMediaSource(Uri playlistUri) {
isCreateCalled = true;
+ if (streamKeys != null) {
+ playlistParserFactory =
+ new FilteringHlsPlaylistParserFactory(playlistParserFactory, streamKeys);
+ }
return new HlsMediaSource(
playlistUri,
hlsDataSourceFactory,
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 c6ebe8e294..e0f55aa738 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
@@ -16,23 +16,26 @@
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.offline.TrackKey;
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.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.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -43,8 +46,52 @@ public final class HlsDownloadHelper extends DownloadHelper {
private int[] renditionGroups;
- public HlsDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) {
- super(DownloadAction.TYPE_HLS, uri, /* cacheKey= */ null);
+ /**
+ * Creates a HLS download helper.
+ *
+ * The helper uses {@link DownloadHelper#DEFAULT_TRACK_SELECTOR_PARAMETERS} for track selection
+ * and does not support drm protected content.
+ *
+ * @param uri A manifest {@link Uri}.
+ * @param manifestDataSourceFactory A {@link DataSource.Factory} used to load the manifest.
+ * @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks
+ * are selected.
+ */
+ public HlsDownloadHelper(
+ Uri uri, DataSource.Factory manifestDataSourceFactory, RenderersFactory renderersFactory) {
+ this(
+ uri,
+ manifestDataSourceFactory,
+ DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
+ renderersFactory,
+ /* drmSessionManager= */ null);
+ }
+
+ /**
+ * Creates a HLS download helper.
+ *
+ * @param uri A manifest {@link Uri}.
+ * @param manifestDataSourceFactory A {@link DataSource.Factory} used to load the manifest.
+ * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
+ * downloading.
+ * @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks
+ * are selected.
+ * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by
+ * {@code renderersFactory}.
+ */
+ public HlsDownloadHelper(
+ Uri uri,
+ DataSource.Factory manifestDataSourceFactory,
+ DefaultTrackSelector.Parameters trackSelectorParameters,
+ RenderersFactory renderersFactory,
+ @Nullable DrmSessionManager drmSessionManager) {
+ super(
+ DownloadAction.TYPE_HLS,
+ uri,
+ /* cacheKey= */ null,
+ trackSelectorParameters,
+ renderersFactory,
+ drmSessionManager);
this.manifestDataSourceFactory = manifestDataSourceFactory;
}
@@ -61,7 +108,7 @@ public final class HlsDownloadHelper extends DownloadHelper {
renditionGroups = new int[0];
return new TrackGroupArray[] {TrackGroupArray.EMPTY};
}
- // TODO: Generate track groups as in playback. Reverse the mapping in getDownloadAction.
+ // 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];
@@ -82,14 +129,9 @@ public final class HlsDownloadHelper extends DownloadHelper {
}
@Override
- protected List toStreamKeys(List trackKeys) {
- List representationKeys = new ArrayList<>(trackKeys.size());
- for (int i = 0; i < trackKeys.size(); i++) {
- TrackKey trackKey = trackKeys.get(i);
- representationKeys.add(
- new StreamKey(renditionGroups[trackKey.groupIndex], trackKey.trackIndex));
- }
- return representationKeys;
+ protected StreamKey toStreamKey(
+ int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) {
+ return new StreamKey(renditionGroups[trackGroupIndex], trackIndexInTrackGroup);
}
private static Format[] toFormats(List hlsUrls) {
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java
index 9058980c73..d185e2a3e8 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java
@@ -15,40 +15,19 @@
*/
package com.google.android.exoplayer2.source.hls.playlist;
-import com.google.android.exoplayer2.offline.FilteringManifestParser;
-import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
-import java.util.Collections;
-import java.util.List;
/** Default implementation for {@link HlsPlaylistParserFactory}. */
public final class DefaultHlsPlaylistParserFactory implements HlsPlaylistParserFactory {
- private final List streamKeys;
-
- /** Creates an instance that does not filter any parsing results. */
- public DefaultHlsPlaylistParserFactory() {
- this(Collections.emptyList());
- }
-
- /**
- * Creates an instance that filters the parsing results using the given {@code streamKeys}.
- *
- * @param streamKeys See {@link
- * FilteringManifestParser#FilteringManifestParser(ParsingLoadable.Parser, List)}.
- */
- public DefaultHlsPlaylistParserFactory(List streamKeys) {
- this.streamKeys = streamKeys;
- }
-
@Override
public ParsingLoadable.Parser createPlaylistParser() {
- return new FilteringManifestParser<>(new HlsPlaylistParser(), streamKeys);
+ return new HlsPlaylistParser();
}
@Override
public ParsingLoadable.Parser createPlaylistParser(
HlsMasterPlaylist masterPlaylist) {
- return new FilteringManifestParser<>(new HlsPlaylistParser(masterPlaylist), streamKeys);
+ return new HlsPlaylistParser(masterPlaylist);
}
}
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParserFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParserFactory.java
new file mode 100644
index 0000000000..2d7ad5a78a
--- /dev/null
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParserFactory.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.hls.playlist;
+
+import com.google.android.exoplayer2.offline.FilteringManifestParser;
+import com.google.android.exoplayer2.offline.StreamKey;
+import com.google.android.exoplayer2.upstream.ParsingLoadable;
+import java.util.List;
+
+/**
+ * A {@link HlsPlaylistParserFactory} that includes only the streams identified by the given stream
+ * keys.
+ */
+public final class FilteringHlsPlaylistParserFactory implements HlsPlaylistParserFactory {
+
+ private final HlsPlaylistParserFactory hlsPlaylistParserFactory;
+ private final List streamKeys;
+
+ /**
+ * @param hlsPlaylistParserFactory A factory for the parsers of the playlists which will be
+ * filtered.
+ * @param streamKeys The stream keys. If null or empty then filtering will not occur.
+ */
+ public FilteringHlsPlaylistParserFactory(
+ HlsPlaylistParserFactory hlsPlaylistParserFactory, List streamKeys) {
+ this.hlsPlaylistParserFactory = hlsPlaylistParserFactory;
+ this.streamKeys = streamKeys;
+ }
+
+ @Override
+ public ParsingLoadable.Parser createPlaylistParser() {
+ return new FilteringManifestParser<>(
+ hlsPlaylistParserFactory.createPlaylistParser(), streamKeys);
+ }
+
+ @Override
+ public ParsingLoadable.Parser createPlaylistParser(
+ HlsMasterPlaylist masterPlaylist) {
+ return new FilteringManifestParser<>(
+ hlsPlaylistParserFactory.createPlaylistParser(masterPlaylist), streamKeys);
+ }
+}
diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java
index 14b54bc471..fc22c45c5a 100644
--- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java
+++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java
@@ -20,6 +20,7 @@ import android.util.Base64;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox;
+import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
@@ -37,12 +38,11 @@ import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
import com.google.android.exoplayer2.upstream.TransferListener;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.List;
-/**
- * A SmoothStreaming {@link MediaPeriod}.
- */
-/* package */ final class SsMediaPeriod implements MediaPeriod,
- SequenceableLoader.Callback> {
+/** A SmoothStreaming {@link MediaPeriod}. */
+/* package */ final class SsMediaPeriod
+ implements MediaPeriod, SequenceableLoader.Callback> {
private static final int INITIALIZATION_VECTOR_SIZE = 8;
@@ -112,6 +112,8 @@ import java.util.ArrayList;
eventDispatcher.mediaPeriodReleased();
}
+ // MediaPeriod implementation.
+
@Override
public void prepare(Callback callback, long positionUs) {
this.callback = callback;
@@ -157,6 +159,16 @@ import java.util.ArrayList;
return positionUs;
}
+ @Override
+ public List getStreamKeys(TrackSelection trackSelection) {
+ List streamKeys = new ArrayList<>(trackSelection.length());
+ int streamElementIndex = trackGroups.indexOf(trackSelection.getTrackGroup());
+ for (int i = 0; i < trackSelection.length(); i++) {
+ streamKeys.add(new StreamKey(streamElementIndex, trackSelection.getIndexInTrackGroup(i)));
+ }
+ return streamKeys;
+ }
+
@Override
public void discardBuffer(long positionUs, boolean toKeyframe) {
for (ChunkSampleStream sampleStream : sampleStreams) {
@@ -211,7 +223,7 @@ import java.util.ArrayList;
return positionUs;
}
- // SequenceableLoader.Callback implementation
+ // SequenceableLoader.Callback implementation.
@Override
public void onContinueLoadingRequested(ChunkSampleStream sampleStream) {
@@ -277,5 +289,4 @@ import java.util.ArrayList;
data[firstPosition] = data[secondPosition];
data[secondPosition] = temp;
}
-
}
diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java
index 103a52a55a..1923c4b08a 100644
--- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java
+++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java
@@ -24,6 +24,8 @@ import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.offline.FilteringManifestParser;
+import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory;
@@ -50,6 +52,7 @@ import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.List;
/** A SmoothStreaming {@link MediaSource}. */
public final class SsMediaSource extends BaseMediaSource
@@ -63,14 +66,15 @@ public final class SsMediaSource extends BaseMediaSource
public static final class Factory implements AdsMediaSource.MediaSourceFactory {
private final SsChunkSource.Factory chunkSourceFactory;
- private final @Nullable DataSource.Factory manifestDataSourceFactory;
+ @Nullable private final DataSource.Factory manifestDataSourceFactory;
- private @Nullable ParsingLoadable.Parser extends SsManifest> manifestParser;
+ @Nullable private ParsingLoadable.Parser extends SsManifest> manifestParser;
+ @Nullable private List streamKeys;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private long livePresentationDelayMs;
private boolean isCreateCalled;
- private @Nullable Object tag;
+ @Nullable private Object tag;
/**
* Creates a new factory for {@link SsMediaSource}s.
@@ -178,6 +182,19 @@ public final class SsMediaSource extends BaseMediaSource
return this;
}
+ /**
+ * Sets a list of {@link StreamKey stream keys} by which the manifest is filtered.
+ *
+ * @param streamKeys A list of {@link StreamKey stream keys}.
+ * @return This factory, for convenience.
+ * @throws IllegalStateException If one of the {@code create} methods has already been called.
+ */
+ public Factory setStreamKeys(List streamKeys) {
+ Assertions.checkState(!isCreateCalled);
+ this.streamKeys = streamKeys;
+ return this;
+ }
+
/**
* Sets the factory to create composite {@link SequenceableLoader}s for when this media source
* loads data from multiple streams (video, audio etc.). The default is an instance of {@link
@@ -208,6 +225,9 @@ public final class SsMediaSource extends BaseMediaSource
public SsMediaSource createMediaSource(SsManifest manifest) {
Assertions.checkArgument(!manifest.isLive);
isCreateCalled = true;
+ if (streamKeys != null && !streamKeys.isEmpty()) {
+ manifest = manifest.copy(streamKeys);
+ }
return new SsMediaSource(
manifest,
/* manifestUri= */ null,
@@ -248,6 +268,9 @@ public final class SsMediaSource extends BaseMediaSource
if (manifestParser == null) {
manifestParser = new SsManifestParser();
}
+ if (streamKeys != null) {
+ manifestParser = new FilteringManifestParser<>(manifestParser, streamKeys);
+ }
return new SsMediaSource(
/* manifest= */ null,
Assertions.checkNotNull(manifestUri),
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 154ac30ac6..b17768f202 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
@@ -16,35 +16,83 @@
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.offline.TrackKey;
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.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
/** A {@link DownloadHelper} for SmoothStreaming streams. */
public final class SsDownloadHelper extends DownloadHelper {
private final DataSource.Factory manifestDataSourceFactory;
- public SsDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) {
- super(DownloadAction.TYPE_SS, uri, /* cacheKey= */ null);
+ /**
+ * Creates a SmoothStreaming download helper.
+ *
+ * The helper uses {@link DownloadHelper#DEFAULT_TRACK_SELECTOR_PARAMETERS} for track selection
+ * and does not support drm protected content.
+ *
+ * @param uri A manifest {@link Uri}.
+ * @param manifestDataSourceFactory A {@link DataSource.Factory} used to load the manifest.
+ * @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks
+ * are selected.
+ */
+ public SsDownloadHelper(
+ Uri uri, DataSource.Factory manifestDataSourceFactory, RenderersFactory renderersFactory) {
+ this(
+ uri,
+ manifestDataSourceFactory,
+ DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
+ renderersFactory,
+ /* drmSessionManager= */ null);
+ }
+
+ /**
+ * Creates a SmoothStreaming download helper.
+ *
+ * @param uri A manifest {@link Uri}.
+ * @param manifestDataSourceFactory A {@link DataSource.Factory} used to load the manifest.
+ * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
+ * downloading.
+ * @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks
+ * are selected.
+ * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by
+ * {@code renderersFactory}.
+ */
+ public SsDownloadHelper(
+ Uri uri,
+ DataSource.Factory manifestDataSourceFactory,
+ DefaultTrackSelector.Parameters trackSelectorParameters,
+ RenderersFactory renderersFactory,
+ @Nullable DrmSessionManager drmSessionManager) {
+ super(
+ DownloadAction.TYPE_SS,
+ uri,
+ /* cacheKey= */ null,
+ trackSelectorParameters,
+ renderersFactory,
+ drmSessionManager);
this.manifestDataSourceFactory = manifestDataSourceFactory;
}
@Override
protected SsManifest loadManifest(Uri uri) throws IOException {
DataSource dataSource = manifestDataSourceFactory.createDataSource();
- return ParsingLoadable.load(dataSource, new SsManifestParser(), uri, C.DATA_TYPE_MANIFEST);
+ Uri fixedUri = SsUtil.fixManifestUri(uri);
+ return ParsingLoadable.load(dataSource, new SsManifestParser(), fixedUri, C.DATA_TYPE_MANIFEST);
}
@Override
@@ -58,12 +106,8 @@ public final class SsDownloadHelper extends DownloadHelper {
}
@Override
- protected List toStreamKeys(List trackKeys) {
- List representationKeys = new ArrayList<>(trackKeys.size());
- for (int i = 0; i < trackKeys.size(); i++) {
- TrackKey trackKey = trackKeys.get(i);
- representationKeys.add(new StreamKey(trackKey.groupIndex, trackKey.trackIndex));
- }
- return representationKeys;
+ protected StreamKey toStreamKey(
+ int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) {
+ return new StreamKey(trackGroupIndex, trackIndexInTrackGroup);
}
}
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java
index 97832abfc7..3f98da06f0 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java
@@ -23,7 +23,7 @@ import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v4.app.NotificationCompat;
import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
+import com.google.android.exoplayer2.offline.DownloadManager.DownloadState;
/** Helper for creating download notifications. */
public final class DownloadNotificationUtil {
@@ -33,7 +33,7 @@ public final class DownloadNotificationUtil {
private DownloadNotificationUtil() {}
/**
- * Returns a progress notification for the given task states.
+ * Returns a progress notification for the given download states.
*
* @param context A context for accessing resources.
* @param smallIcon A small icon for the notification.
@@ -41,7 +41,7 @@ public final class DownloadNotificationUtil {
* above.
* @param contentIntent An optional content intent to send when the notification is clicked.
* @param message An optional message to display on the notification.
- * @param taskStates The task states.
+ * @param downloadStates The download states.
* @return The notification.
*/
public static Notification buildProgressNotification(
@@ -50,28 +50,28 @@ public final class DownloadNotificationUtil {
String channelId,
@Nullable PendingIntent contentIntent,
@Nullable String message,
- TaskState[] taskStates) {
+ DownloadState[] downloadStates) {
float totalPercentage = 0;
int downloadTaskCount = 0;
boolean allDownloadPercentagesUnknown = true;
boolean haveDownloadedBytes = false;
boolean haveDownloadTasks = false;
boolean haveRemoveTasks = false;
- for (TaskState taskState : taskStates) {
- if (taskState.state != TaskState.STATE_STARTED
- && taskState.state != TaskState.STATE_COMPLETED) {
+ for (DownloadState downloadState : downloadStates) {
+ if (downloadState.state != DownloadState.STATE_STARTED
+ && downloadState.state != DownloadState.STATE_COMPLETED) {
continue;
}
- if (taskState.action.isRemoveAction) {
+ if (downloadState.action.isRemoveAction) {
haveRemoveTasks = true;
continue;
}
haveDownloadTasks = true;
- if (taskState.downloadPercentage != C.PERCENTAGE_UNSET) {
+ if (downloadState.downloadPercentage != C.PERCENTAGE_UNSET) {
allDownloadPercentagesUnknown = false;
- totalPercentage += taskState.downloadPercentage;
+ totalPercentage += downloadState.downloadPercentage;
}
- haveDownloadedBytes |= taskState.downloadedBytes > 0;
+ haveDownloadedBytes |= downloadState.downloadedBytes > 0;
downloadTaskCount++;
}
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java
index 41ddd3c7eb..1adc557142 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java
@@ -29,7 +29,6 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Looper;
import android.support.annotation.IntDef;
-import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
@@ -187,8 +186,9 @@ import java.util.List;
* Type: {@link AspectRatioFrameLayout}
*
* {@code exo_shutter} - A view that's made visible when video should be hidden. This
- * view is typically an opaque view that covers the video surface view, thereby obscuring it
- * when visible.
+ * view is typically an opaque view that covers the video surface, thereby obscuring it when
+ * visible. Obscuring the surface in this way also helps to prevent flicker at the start of
+ * playback when {@code surface_type="surface_view"}.
*
@@ -271,13 +271,13 @@ public class PlayerView extends FrameLayout {
private static final int SURFACE_TYPE_MONO360_VIEW = 3;
// LINT.ThenChange(../../../../../../res/values/attrs.xml)
- private final AspectRatioFrameLayout contentFrame;
+ @Nullable private final AspectRatioFrameLayout contentFrame;
private final View shutterView;
- private final View surfaceView;
+ @Nullable private final View surfaceView;
private final ImageView artworkView;
private final SubtitleView subtitleView;
- private final @Nullable View bufferingView;
- private final @Nullable TextView errorMessageView;
+ @Nullable private final View bufferingView;
+ @Nullable private final TextView errorMessageView;
private final PlayerControlView controller;
private final ComponentListener componentListener;
private final FrameLayout overlayFrameLayout;
@@ -285,11 +285,11 @@ public class PlayerView extends FrameLayout {
private Player player;
private boolean useController;
private boolean useArtwork;
- private @Nullable Drawable defaultArtwork;
+ @Nullable private Drawable defaultArtwork;
private @ShowBuffering int showBuffering;
private boolean keepContentOnPlayerReset;
- private @Nullable ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
- private @Nullable CharSequence customErrorMessage;
+ @Nullable private ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
+ @Nullable private CharSequence customErrorMessage;
private int controllerShowTimeoutMs;
private boolean controllerAutoShow;
private boolean controllerHideDuringAds;
@@ -474,9 +474,7 @@ public class PlayerView extends FrameLayout {
* @param newPlayerView The new view to attach to the player.
*/
public static void switchTargetView(
- @NonNull Player player,
- @Nullable PlayerView oldPlayerView,
- @Nullable PlayerView newPlayerView) {
+ Player player, @Nullable PlayerView oldPlayerView, @Nullable PlayerView newPlayerView) {
if (oldPlayerView == newPlayerView) {
return;
}
@@ -1080,6 +1078,26 @@ public class PlayerView extends FrameLayout {
}
}
+ /**
+ * Called when there's a change in the aspect ratio of the content being displayed. The default
+ * implementation sets the aspect ratio of the content frame to that of the content, unless the
+ * content view is a {@link SphericalSurfaceView} in which case the frame's aspect ratio is
+ * cleared.
+ *
+ * @param contentAspectRatio The aspect ratio of the content.
+ * @param contentFrame The content frame, or {@code null}.
+ * @param contentView The view that holds the content being displayed, or {@code null}.
+ */
+ protected void onContentAspectRatioChanged(
+ float contentAspectRatio,
+ @Nullable AspectRatioFrameLayout contentFrame,
+ @Nullable View contentView) {
+ if (contentFrame != null) {
+ contentFrame.setAspectRatio(
+ contentView instanceof SphericalSurfaceView ? 0 : contentAspectRatio);
+ }
+ }
+
private boolean toggleControllerVisibility() {
if (!useController || player == null) {
return false;
@@ -1193,9 +1211,8 @@ public class PlayerView extends FrameLayout {
int drawableWidth = drawable.getIntrinsicWidth();
int drawableHeight = drawable.getIntrinsicHeight();
if (drawableWidth > 0 && drawableHeight > 0) {
- if (contentFrame != null) {
- contentFrame.setAspectRatio((float) drawableWidth / drawableHeight);
- }
+ float artworkAspectRatio = (float) drawableWidth / drawableHeight;
+ onContentAspectRatioChanged(artworkAspectRatio, contentFrame, artworkView);
artworkView.setImageDrawable(drawable);
artworkView.setVisibility(VISIBLE);
return true;
@@ -1328,9 +1345,6 @@ public class PlayerView extends FrameLayout {
@Override
public void onVideoSizeChanged(
int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
- if (contentFrame == null) {
- return;
- }
float videoAspectRatio =
(height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height;
@@ -1351,11 +1365,9 @@ public class PlayerView extends FrameLayout {
surfaceView.addOnLayoutChangeListener(this);
}
applyTextureViewRotation((TextureView) surfaceView, textureViewRotation);
- } else if (surfaceView instanceof SphericalSurfaceView) {
- videoAspectRatio = 0;
}
- contentFrame.setAspectRatio(videoAspectRatio);
+ onContentAspectRatioChanged(videoAspectRatio, contentFrame, surfaceView);
}
@Override
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java
index 3f09ac2427..fd78631337 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java
@@ -15,7 +15,6 @@
*/
package com.google.android.exoplayer2.ui;
-import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
@@ -33,13 +32,25 @@ import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;
-import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
+import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.util.Assertions;
import java.util.Arrays;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** A view for making track selections. */
public class TrackSelectionView extends LinearLayout {
+ /** Callback which is invoked when a track selection has been made. */
+ public interface DialogCallback {
+
+ /**
+ * Called when track are selected.
+ *
+ * @param parameters The {@link DefaultTrackSelector.Parameters} for the selected tracks.
+ */
+ void onTracksSelected(DefaultTrackSelector.Parameters parameters);
+ }
+
private final int selectableItemBackgroundResourceId;
private final LayoutInflater inflater;
private final CheckedTextView disableView;
@@ -51,35 +62,64 @@ public class TrackSelectionView extends LinearLayout {
private TrackNameProvider trackNameProvider;
private CheckedTextView[][] trackViews;
- private DefaultTrackSelector trackSelector;
+ private @MonotonicNonNull MappedTrackInfo mappedTrackInfo;
private int rendererIndex;
+ private DefaultTrackSelector.Parameters parameters;
private TrackGroupArray trackGroups;
private boolean isDisabled;
- private @Nullable SelectionOverride override;
+ @Nullable private SelectionOverride override;
/**
* Gets a pair consisting of a dialog and the {@link TrackSelectionView} that will be shown by it.
*
- * @param activity The parent activity.
+ * The dialog shows the current configuration of the provided {@code TrackSelector} and updates
+ * the parameters when closing the dialog.
+ *
+ * @param context The parent context.
* @param title The dialog's title.
* @param trackSelector The track selector.
* @param rendererIndex The index of the renderer.
* @return The dialog and the {@link TrackSelectionView} that will be shown by it.
*/
public static Pair getDialog(
- Activity activity,
+ Context context, CharSequence title, DefaultTrackSelector trackSelector, int rendererIndex) {
+ return getDialog(
+ context,
+ title,
+ Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo()),
+ rendererIndex,
+ trackSelector.getParameters(),
+ trackSelector::setParameters);
+ }
+
+ /**
+ * Gets a pair consisting of a dialog and the {@link TrackSelectionView} that will be shown by it.
+ *
+ * @param context The parent context.
+ * @param title The dialog's title.
+ * @param mappedTrackInfo The {@link MappedTrackInfo}.
+ * @param rendererIndex The index of the renderer.
+ * @param parameters The {@link DefaultTrackSelector.Parameters}.
+ * @param callback The {@link DialogCallback} invoked when the dialog is closed successfully.
+ * @return The dialog and the {@link TrackSelectionView} that will be shown by it.
+ */
+ public static Pair getDialog(
+ Context context,
CharSequence title,
- DefaultTrackSelector trackSelector,
- int rendererIndex) {
- AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ MappedTrackInfo mappedTrackInfo,
+ int rendererIndex,
+ DefaultTrackSelector.Parameters parameters,
+ DialogCallback callback) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
// Inflate with the builder's context to ensure the correct style is used.
LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
View dialogView = dialogInflater.inflate(R.layout.exo_track_selection_dialog, null);
- final TrackSelectionView selectionView = dialogView.findViewById(R.id.exo_track_selection_view);
- selectionView.init(trackSelector, rendererIndex);
- Dialog.OnClickListener okClickListener = (dialog, which) -> selectionView.applySelection();
+ TrackSelectionView selectionView = dialogView.findViewById(R.id.exo_track_selection_view);
+ selectionView.init(mappedTrackInfo, rendererIndex, parameters);
+ Dialog.OnClickListener okClickListener =
+ (dialog, which) -> callback.onTracksSelected(selectionView.getSelectionParameters());
AlertDialog dialog =
builder
@@ -113,6 +153,8 @@ public class TrackSelectionView extends LinearLayout {
inflater = LayoutInflater.from(context);
componentListener = new ComponentListener();
trackNameProvider = new DefaultTrackNameProvider(getResources());
+ parameters = DefaultTrackSelector.Parameters.DEFAULT;
+ trackGroups = TrackGroupArray.EMPTY;
// View for disabling the renderer.
disableView =
@@ -176,18 +218,35 @@ public class TrackSelectionView extends LinearLayout {
}
/**
- * Initialize the view to select tracks for a specified renderer using a {@link
- * DefaultTrackSelector}.
+ * Initialize the view to select tracks for a specified renderer using {@link MappedTrackInfo} and
+ * a set of {@link DefaultTrackSelector.Parameters}.
*
- * @param trackSelector The {@link DefaultTrackSelector}.
+ * @param mappedTrackInfo The {@link MappedTrackInfo}.
* @param rendererIndex The index of the renderer.
+ * @param parameters The {@link DefaultTrackSelector.Parameters}.
*/
- public void init(DefaultTrackSelector trackSelector, int rendererIndex) {
- this.trackSelector = trackSelector;
+ public void init(
+ MappedTrackInfo mappedTrackInfo,
+ int rendererIndex,
+ DefaultTrackSelector.Parameters parameters) {
+ this.mappedTrackInfo = mappedTrackInfo;
this.rendererIndex = rendererIndex;
+ this.parameters = parameters;
updateViews();
}
+ /** Returns the {@link DefaultTrackSelector.Parameters} for the current selection. */
+ public DefaultTrackSelector.Parameters getSelectionParameters() {
+ DefaultTrackSelector.ParametersBuilder parametersBuilder = parameters.buildUpon();
+ parametersBuilder.setRendererDisabled(rendererIndex, isDisabled);
+ if (override != null) {
+ parametersBuilder.setSelectionOverride(rendererIndex, trackGroups, override);
+ } else {
+ parametersBuilder.clearSelectionOverrides(rendererIndex);
+ }
+ return parametersBuilder.build();
+ }
+
// Private methods.
private void updateViews() {
@@ -196,9 +255,7 @@ public class TrackSelectionView extends LinearLayout {
removeViewAt(i);
}
- MappingTrackSelector.MappedTrackInfo trackInfo =
- trackSelector == null ? null : trackSelector.getCurrentMappedTrackInfo();
- if (trackSelector == null || trackInfo == null) {
+ if (mappedTrackInfo == null) {
// The view is not initialized.
disableView.setEnabled(false);
defaultView.setEnabled(false);
@@ -207,9 +264,8 @@ public class TrackSelectionView extends LinearLayout {
disableView.setEnabled(true);
defaultView.setEnabled(true);
- trackGroups = trackInfo.getTrackGroups(rendererIndex);
+ trackGroups = mappedTrackInfo.getTrackGroups(rendererIndex);
- DefaultTrackSelector.Parameters parameters = trackSelector.getParameters();
isDisabled = parameters.getRendererDisabled(rendererIndex);
override = parameters.getSelectionOverride(rendererIndex, trackGroups);
@@ -220,7 +276,7 @@ public class TrackSelectionView extends LinearLayout {
boolean enableAdaptiveSelections =
allowAdaptiveSelections
&& trackGroups.get(groupIndex).length > 1
- && trackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false)
+ && mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false)
!= RendererCapabilities.ADAPTIVE_NOT_SUPPORTED;
trackViews[groupIndex] = new CheckedTextView[group.length];
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
@@ -235,7 +291,7 @@ public class TrackSelectionView extends LinearLayout {
(CheckedTextView) inflater.inflate(trackViewLayoutId, this, false);
trackView.setBackgroundResource(selectableItemBackgroundResourceId);
trackView.setText(trackNameProvider.getTrackName(group.getFormat(trackIndex)));
- if (trackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)
+ if (mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)
== RendererCapabilities.FORMAT_HANDLED) {
trackView.setFocusable(true);
trackView.setTag(Pair.create(groupIndex, trackIndex));
@@ -263,17 +319,6 @@ public class TrackSelectionView extends LinearLayout {
}
}
- private void applySelection() {
- DefaultTrackSelector.ParametersBuilder parametersBuilder = trackSelector.buildUponParameters();
- parametersBuilder.setRendererDisabled(rendererIndex, isDisabled);
- if (override != null) {
- parametersBuilder.setSelectionOverride(rendererIndex, trackGroups, override);
- } else {
- parametersBuilder.clearSelectionOverrides(rendererIndex);
- }
- trackSelector.setParameters(parametersBuilder);
- }
-
private void onClick(View view) {
if (view == disableView) {
onDisableViewClicked();
diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java
index 6d37961005..ac39ba8de6 100644
--- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java
+++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java
@@ -79,8 +79,19 @@ public class FakeTrackSelector extends DefaultTrackSelector {
}
@Override
- public TrackSelection createTrackSelection(
- TrackGroup trackGroup, BandwidthMeter bandwidthMeter, int... tracks) {
+ public TrackSelection[] createTrackSelections(
+ TrackSelection.Definition[] definitions, BandwidthMeter bandwidthMeter) {
+ TrackSelection[] selections = new TrackSelection[definitions.length];
+ for (int i = 0; i < definitions.length; i++) {
+ TrackSelection.Definition definition = definitions[i];
+ if (definition != null) {
+ selections[i] = createTrackSelection(definition.group);
+ }
+ }
+ return selections;
+ }
+
+ private TrackSelection createTrackSelection(TrackGroup trackGroup) {
if (mayReuseTrackSelection) {
for (FakeTrackSelection trackSelection : trackSelections) {
if (trackSelection.getTrackGroup().equals(trackGroup)) {
@@ -92,18 +103,5 @@ public class FakeTrackSelector extends DefaultTrackSelector {
trackSelections.add(trackSelection);
return trackSelection;
}
-
- @Override
- public TrackSelection[] createTrackSelections(
- TrackSelection.Definition[] definitions, BandwidthMeter bandwidthMeter) {
- TrackSelection[] selections = new TrackSelection[definitions.length];
- for (int i = 0; i < definitions.length; i++) {
- TrackSelection.Definition definition = definitions[i];
- if (definition != null) {
- selections[i] = createTrackSelection(definition.group, bandwidthMeter, definition.tracks);
- }
- }
- return selections;
- }
}
}
diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java
index 7e0ffc1772..e7b850d52b 100644
--- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java
+++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java
@@ -17,8 +17,8 @@ package com.google.android.exoplayer2.testutil;
import static com.google.common.truth.Truth.assertThat;
-import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadManager;
+import com.google.android.exoplayer2.offline.DownloadManager.DownloadState;
import java.util.HashMap;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CountDownLatch;
@@ -31,10 +31,10 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
private final DownloadManager downloadManager;
private final DummyMainThread dummyMainThread;
- private final HashMap> actionStates;
+ private final HashMap> actionStates;
private CountDownLatch downloadFinishedCondition;
- private Throwable downloadError;
+ @DownloadState.FailureReason private int failureReason;
public TestDownloadManagerListener(
DownloadManager downloadManager, DummyMainThread dummyMainThread) {
@@ -43,12 +43,12 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
actionStates = new HashMap<>();
}
- public int pollStateChange(DownloadAction action, long timeoutMs) throws InterruptedException {
- return getStateQueue(action).poll(timeoutMs, TimeUnit.MILLISECONDS);
+ public Integer pollStateChange(String taskId, long timeoutMs) throws InterruptedException {
+ return getStateQueue(taskId).poll(timeoutMs, TimeUnit.MILLISECONDS);
}
public void clearDownloadError() {
- this.downloadError = null;
+ this.failureReason = DownloadState.FAILURE_REASON_NONE;
}
@Override
@@ -57,12 +57,11 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
}
@Override
- public void onTaskStateChanged(
- DownloadManager downloadManager, DownloadManager.TaskState taskState) {
- if (taskState.state == DownloadManager.TaskState.STATE_FAILED && downloadError == null) {
- downloadError = taskState.error;
+ public void onDownloadStateChanged(DownloadManager downloadManager, DownloadState downloadState) {
+ if (downloadState.state == DownloadState.STATE_FAILED) {
+ failureReason = downloadState.failureReason;
}
- getStateQueue(taskState.action).add(taskState.state);
+ getStateQueue(downloadState.id).add(downloadState.state);
}
@Override
@@ -77,6 +76,14 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
* error.
*/
public void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable {
+ blockUntilTasksComplete();
+ if (failureReason != DownloadState.FAILURE_REASON_NONE) {
+ throw new Exception("Failure reason: " + DownloadState.getFailureString(failureReason));
+ }
+ }
+
+ /** Blocks until all remove and download tasks are complete. Task errors are ignored. */
+ public void blockUntilTasksComplete() throws InterruptedException {
synchronized (this) {
downloadFinishedCondition = new CountDownLatch(1);
}
@@ -87,17 +94,14 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
}
});
assertThat(downloadFinishedCondition.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue();
- if (downloadError != null) {
- throw new Exception(downloadError);
- }
}
- private ArrayBlockingQueue getStateQueue(DownloadAction action) {
+ private ArrayBlockingQueue getStateQueue(String taskId) {
synchronized (actionStates) {
- if (!actionStates.containsKey(action)) {
- actionStates.put(action, new ArrayBlockingQueue<>(10));
+ if (!actionStates.containsKey(taskId)) {
+ actionStates.put(taskId, new ArrayBlockingQueue<>(10));
}
- return actionStates.get(action);
+ return actionStates.get(taskId);
}
}
}