Merge branch 'dev-v2' into dev-v2

This commit is contained in:
Michał Seroczyński 2018-12-17 11:19:50 +01:00 committed by GitHub
commit 49a99beaef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
79 changed files with 3869 additions and 1571 deletions

View File

@ -5,15 +5,37 @@
* Support for playing spherical videos on Daydream.
* Improve decoder re-use between playbacks. TODO: Write and link a blog post
here ([#2826](https://github.com/google/ExoPlayer/issues/2826)).
* Add options for controlling audio track selections to `DefaultTrackSelector`
([#3314](https://github.com/google/ExoPlayer/issues/3314)).
* Track selection:
* Add options for controlling audio track selections to `DefaultTrackSelector`
([#3314](https://github.com/google/ExoPlayer/issues/3314)).
* Update `TrackSelection.Factory` interface to support creating all track
selections together.
* Captions:
* Support PNG subtitles in SMPTE-TT
([#1583](https://github.com/google/ExoPlayer/issues/1583)).
* Do not retry failed loads whose error is `FileNotFoundException`.
* Prevent Cea608Decoder from generating Subtitles with null Cues list
* Caching: Cache data with unknown length by default. The previous flag to opt in
to this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been
replaced with an opt out flag (`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`).
* Prevent Cea608Decoder from generating Subtitles with null Cues list.
* Offline:
* Speed up removal of segmented downloads
([#5136](https://github.com/google/ExoPlayer/issues/5136)).
* Add `setStreamKeys` method to factories of DASH, SmoothStreaming and HLS
media sources to simplify filtering by downloaded streams.
* Caching:
* Improve performance of `SimpleCache`.
* Cache data with unknown length by default. The previous flag to opt in to
this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been
replaced with an opt out flag
(`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`).
* Disable post processing on Nvidia devices, as it breaks decode-only frame
skippping.
* Workaround for MiTV (dangal) issue when swapping output surface
([#5169](https://github.com/google/ExoPlayer/issues/5169)).
* DownloadManager:
* Create only one task for all DownloadActions for the same content.
* Rename TaskState to DownloadState.
* MP3: Fix issue where streams would play twice on Samsung devices
([#4519](https://github.com/google/ExoPlayer/issues/4519)).
### 2.9.2 ###
* HLS:
@ -61,10 +83,10 @@
* DASH: Parse ProgramInformation element if present in the manifest.
* HLS:
* Add constructor to `DefaultHlsExtractorFactory` for adding TS payload
reader factory flags.
reader factory flags
([#4861](https://github.com/google/ExoPlayer/issues/4861)).
* Fix bug in segment sniffing
([#5039](https://github.com/google/ExoPlayer/issues/5039)).
([#4861](https://github.com/google/ExoPlayer/issues/4861)).
* SubRip: Add support for alignment tags, and remove tags from the displayed
captions ([#4306](https://github.com/google/ExoPlayer/issues/4306)).
* Fix issue with blind seeking to windows with non-zero offset in a

View File

@ -49,6 +49,16 @@ android {
disable 'MissingTranslation'
}
flavorDimensions "receiver"
productFlavors {
defaultCast {
dimension "receiver"
manifestPlaceholders =
[castOptionsProvider: "com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"]
}
}
}
dependencies {

View File

@ -23,7 +23,7 @@
android:largeHeap="true" android:allowBackup="false">
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider" />
android:value="${castOptionsProvider}" />
<activity android:name="com.google.android.exoplayer2.castdemo.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"

View File

@ -268,7 +268,7 @@ import java.util.ArrayList;
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
updateCurrentItemIndex();
if (timeline.isEmpty()) {
if (currentPlayer == castPlayer && timeline.isEmpty()) {
castMediaQueueCreationPending = true;
}
}

View File

@ -15,7 +15,6 @@
*/
package com.google.android.exoplayer2.castdemo;
import com.google.android.exoplayer2.ext.cast.MediaItem;
import com.google.android.exoplayer2.util.MimeTypes;
import java.util.ArrayList;
import java.util.Collections;
@ -24,50 +23,64 @@ import java.util.List;
/** Utility methods and constants for the Cast demo application. */
/* package */ final class DemoUtil {
/** Represents a media sample. */
public static final class Sample {
/** The uri of the media content. */
public final String uri;
/** The name of the sample. */
public final String name;
/** The mime type of the sample media content. */
public final String mimeType;
/**
* @param uri See {@link #uri}.
* @param name See {@link #name}.
* @param mimeType See {@link #mimeType}.
*/
public Sample(String uri, String name, String mimeType) {
this.uri = uri;
this.name = name;
this.mimeType = mimeType;
}
@Override
public String toString() {
return name;
}
}
public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD;
public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8;
public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS;
public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;
/** The list of samples available in the cast demo app. */
public static final List<MediaItem> SAMPLES;
public static final List<Sample> SAMPLES;
static {
// App samples.
ArrayList<MediaItem> samples = new ArrayList<>();
MediaItem.Builder sampleBuilder = new MediaItem.Builder();
ArrayList<Sample> samples = new ArrayList<>();
samples.add(
sampleBuilder
.setTitle("DASH (clear,MP4,H264)")
.setMimeType(MIME_TYPE_DASH)
.setMedia("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd")
.buildAndClear());
new Sample(
"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
"DASH (clear,MP4,H264)",
MIME_TYPE_DASH));
samples.add(
sampleBuilder
.setTitle("Tears of Steel (HLS)")
.setMimeType(MIME_TYPE_HLS)
.setMedia(
"https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/"
+ "hls/TearsOfSteel.m3u8")
.buildAndClear());
new Sample(
"https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/"
+ "hls/TearsOfSteel.m3u8",
"Tears of Steel (HLS)",
MIME_TYPE_HLS));
samples.add(
sampleBuilder
.setTitle("HLS Basic (TS)")
.setMimeType(MIME_TYPE_HLS)
.setMedia(
"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3"
+ "/bipbop_4x3_variant.m3u8")
.buildAndClear());
new Sample(
"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3"
+ "/bipbop_4x3_variant.m3u8",
"HLS Basic (TS)",
MIME_TYPE_HLS));
samples.add(
sampleBuilder
.setTitle("Dizzy (MP4)")
.setMimeType(MIME_TYPE_VIDEO_MP4)
.setMedia("https://html5demos.com/assets/dizzy.mp4")
.buildAndClear());
new Sample("https://html5demos.com/assets/dizzy.mp4", "Dizzy (MP4)", MIME_TYPE_VIDEO_MP4));
SAMPLES = Collections.unmodifiableList(samples);
}

View File

@ -17,7 +17,6 @@ package com.google.android.exoplayer2.castdemo;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.graphics.ColorUtils;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
@ -50,6 +49,8 @@ import com.google.android.gms.cast.framework.CastContext;
public class MainActivity extends AppCompatActivity
implements OnClickListener, PlayerManager.QueuePositionListener {
private final MediaItem.Builder mediaItemBuilder;
private PlayerView localPlayerView;
private PlayerControlView castControlView;
private PlayerManager playerManager;
@ -57,6 +58,10 @@ public class MainActivity extends AppCompatActivity
private MediaQueueListAdapter mediaQueueListAdapter;
private CastContext castContext;
public MainActivity() {
mediaItemBuilder = new MediaItem.Builder();
}
// Activity lifecycle methods.
@Override
@ -154,7 +159,14 @@ public class MainActivity extends AppCompatActivity
sampleList.setAdapter(new SampleListAdapter(this));
sampleList.setOnItemClickListener(
(parent, view, position, id) -> {
playerManager.addItem(DemoUtil.SAMPLES.get(position));
DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position);
playerManager.addItem(
mediaItemBuilder
.clear()
.setMedia(sample.uri)
.setTitle(sample.name)
.setMimeType(sample.mimeType)
.build());
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
});
return dialogList;
@ -254,19 +266,11 @@ public class MainActivity extends AppCompatActivity
}
private static final class SampleListAdapter extends ArrayAdapter<MediaItem> {
private static final class SampleListAdapter extends ArrayAdapter<DemoUtil.Sample> {
public SampleListAdapter(Context context) {
super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
}
@Override
public View getView(int position, @Nullable View convertView, ViewGroup parent) {
TextView view = (TextView) super.getView(position, convertView, parent);
MediaItem sample = DemoUtil.SAMPLES.get(position);
view.setText(sample.title);
return view;
}
}
}

View File

@ -16,6 +16,8 @@
package com.google.android.exoplayer2.demo;
import android.app.Application;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
@ -72,6 +74,17 @@ public class DemoApplication extends Application {
return "withExtensions".equals(BuildConfig.FLAVOR);
}
public RenderersFactory buildRenderersFactory(boolean preferExtensionRenderer) {
@DefaultRenderersFactory.ExtensionRendererMode
int extensionRendererMode =
useExtensionRenderers()
? (preferExtensionRenderer
? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
return new DefaultRenderersFactory(this, extensionRendererMode);
}
public DownloadManager getDownloadManager() {
initDownloadManager();
return downloadManager;

View File

@ -17,7 +17,7 @@ package com.google.android.exoplayer2.demo;
import android.app.Notification;
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
import com.google.android.exoplayer2.offline.DownloadManager.DownloadState;
import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.scheduler.PlatformScheduler;
import com.google.android.exoplayer2.ui.DownloadNotificationUtil;
@ -31,12 +31,15 @@ public class DemoDownloadService extends DownloadService {
private static final int JOB_ID = 1;
private static final int FOREGROUND_NOTIFICATION_ID = 1;
private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
public DemoDownloadService() {
super(
FOREGROUND_NOTIFICATION_ID,
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
CHANNEL_ID,
R.string.exo_download_notification_channel_name);
nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
}
@Override
@ -50,40 +53,41 @@ public class DemoDownloadService extends DownloadService {
}
@Override
protected Notification getForegroundNotification(TaskState[] taskStates) {
protected Notification getForegroundNotification(DownloadState[] downloadStates) {
return DownloadNotificationUtil.buildProgressNotification(
/* context= */ this,
R.drawable.exo_controls_play,
R.drawable.ic_download,
CHANNEL_ID,
/* contentIntent= */ null,
/* message= */ null,
taskStates);
downloadStates);
}
@Override
protected void onTaskStateChanged(TaskState taskState) {
if (taskState.action.isRemoveAction) {
protected void onDownloadStateChanged(DownloadState downloadState) {
if (downloadState.action.isRemoveAction) {
return;
}
Notification notification = null;
if (taskState.state == TaskState.STATE_COMPLETED) {
if (downloadState.state == DownloadState.STATE_COMPLETED) {
notification =
DownloadNotificationUtil.buildDownloadCompletedNotification(
/* context= */ this,
R.drawable.exo_controls_play,
R.drawable.ic_download_done,
CHANNEL_ID,
/* contentIntent= */ null,
Util.fromUtf8Bytes(taskState.action.data));
} else if (taskState.state == TaskState.STATE_FAILED) {
Util.fromUtf8Bytes(downloadState.action.data));
} else if (downloadState.state == DownloadState.STATE_FAILED) {
notification =
DownloadNotificationUtil.buildDownloadFailedNotification(
/* context= */ this,
R.drawable.exo_controls_play,
R.drawable.ic_download_done,
CHANNEL_ID,
/* contentIntent= */ null,
Util.fromUtf8Bytes(taskState.action.data));
Util.fromUtf8Bytes(downloadState.action.data));
} else {
return;
}
int notificationId = FOREGROUND_NOTIFICATION_ID + 1 + taskState.taskId;
NotificationUtil.setNotification(this, notificationId, notification);
NotificationUtil.setNotification(this, nextNotificationId++, notification);
}
}

View File

@ -19,37 +19,43 @@ import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.support.annotation.Nullable;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.offline.ActionFile;
import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadHelper;
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
import com.google.android.exoplayer2.offline.DownloadManager.DownloadState;
import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper;
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.offline.DashDownloadHelper;
import com.google.android.exoplayer2.source.hls.offline.HlsDownloadHelper;
import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadHelper;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.ui.DefaultTrackNameProvider;
import com.google.android.exoplayer2.ui.TrackNameProvider;
import com.google.android.exoplayer2.ui.TrackSelectionView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -114,14 +120,19 @@ public class DownloadTracker implements DownloadManager.Listener {
return trackedDownloadStates.get(uri).getKeys();
}
public void toggleDownload(Activity activity, String name, Uri uri, String extension) {
public void toggleDownload(
Activity activity,
String name,
Uri uri,
String extension,
RenderersFactory renderersFactory) {
if (isDownloaded(uri)) {
DownloadAction removeAction = getDownloadHelper(uri, extension).getRemoveAction();
DownloadAction removeAction =
getDownloadHelper(uri, extension, renderersFactory).getRemoveAction();
startServiceWithAction(removeAction);
} else {
StartDownloadDialogHelper helper =
new StartDownloadDialogHelper(activity, getDownloadHelper(uri, extension), name);
helper.prepare();
new StartDownloadDialogHelper(
activity, getDownloadHelper(uri, extension, renderersFactory), name);
}
}
@ -133,11 +144,11 @@ public class DownloadTracker implements DownloadManager.Listener {
}
@Override
public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) {
DownloadAction action = taskState.action;
public void onDownloadStateChanged(DownloadManager downloadManager, DownloadState downloadState) {
DownloadAction action = downloadState.action;
Uri uri = action.uri;
if ((action.isRemoveAction && taskState.state == TaskState.STATE_COMPLETED)
|| (!action.isRemoveAction && taskState.state == TaskState.STATE_FAILED)) {
if ((action.isRemoveAction && downloadState.state == DownloadState.STATE_COMPLETED)
|| (!action.isRemoveAction && downloadState.state == DownloadState.STATE_FAILED)) {
// A download has been removed, or has failed. Stop tracking it.
if (trackedDownloadStates.remove(uri) != null) {
handleTrackedDownloadStatesChanged();
@ -192,15 +203,16 @@ public class DownloadTracker implements DownloadManager.Listener {
DownloadService.startWithAction(context, DemoDownloadService.class, action, false);
}
private DownloadHelper getDownloadHelper(Uri uri, String extension) {
private DownloadHelper<?> getDownloadHelper(
Uri uri, String extension, RenderersFactory renderersFactory) {
int type = Util.inferContentType(uri, extension);
switch (type) {
case C.TYPE_DASH:
return new DashDownloadHelper(uri, dataSourceFactory);
return new DashDownloadHelper(uri, dataSourceFactory, renderersFactory);
case C.TYPE_SS:
return new SsDownloadHelper(uri, dataSourceFactory);
return new SsDownloadHelper(uri, dataSourceFactory, renderersFactory);
case C.TYPE_HLS:
return new HlsDownloadHelper(uri, dataSourceFactory);
return new HlsDownloadHelper(uri, dataSourceFactory, renderersFactory);
case C.TYPE_OTHER:
return new ProgressiveDownloadHelper(uri);
default:
@ -208,84 +220,165 @@ public class DownloadTracker implements DownloadManager.Listener {
}
}
@SuppressWarnings("UngroupedOverloads")
private final class StartDownloadDialogHelper
implements DownloadHelper.Callback, DialogInterface.OnClickListener {
implements DownloadHelper.Callback,
DialogInterface.OnClickListener,
View.OnClickListener,
TrackSelectionView.DialogCallback {
private final DownloadHelper downloadHelper;
private final DownloadHelper<?> downloadHelper;
private final String name;
private final LayoutInflater dialogInflater;
private final AlertDialog dialog;
private final LinearLayout selectionList;
private final AlertDialog.Builder builder;
private final View dialogView;
private final List<TrackKey> trackKeys;
private final ArrayAdapter<String> trackTitles;
private final ListView representationList;
private MappedTrackInfo mappedTrackInfo;
private DefaultTrackSelector.Parameters parameters;
public StartDownloadDialogHelper(
Activity activity, DownloadHelper downloadHelper, String name) {
private StartDownloadDialogHelper(
Activity activity, DownloadHelper<?> downloadHelper, String name) {
this.downloadHelper = downloadHelper;
this.name = name;
builder =
AlertDialog.Builder builder =
new AlertDialog.Builder(activity)
.setTitle(R.string.exo_download_description)
.setTitle(R.string.download_preparing)
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, null);
// Inflate with the builder's context to ensure the correct style is used.
LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
dialogView = dialogInflater.inflate(R.layout.start_download_dialog, null);
dialogInflater = LayoutInflater.from(builder.getContext());
selectionList = (LinearLayout) dialogInflater.inflate(R.layout.start_download_dialog, null);
builder.setView(selectionList);
dialog = builder.create();
dialog.show();
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
trackKeys = new ArrayList<>();
trackTitles =
new ArrayAdapter<>(
builder.getContext(), android.R.layout.simple_list_item_multiple_choice);
representationList = dialogView.findViewById(R.id.representation_list);
representationList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
representationList.setAdapter(trackTitles);
}
public void prepare() {
parameters = DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS;
downloadHelper.prepare(this);
}
// DownloadHelper.Callback implementation.
@Override
public void onPrepared(DownloadHelper helper) {
for (int i = 0; i < downloadHelper.getPeriodCount(); i++) {
TrackGroupArray trackGroups = downloadHelper.getTrackGroups(i);
for (int j = 0; j < trackGroups.length; j++) {
TrackGroup trackGroup = trackGroups.get(j);
for (int k = 0; k < trackGroup.length; k++) {
trackKeys.add(new TrackKey(i, j, k));
trackTitles.add(trackNameProvider.getTrackName(trackGroup.getFormat(k)));
}
}
public void onPrepared(DownloadHelper<?> helper) {
if (helper.getPeriodCount() < 1) {
onPrepareError(downloadHelper, new IOException("Content is empty."));
return;
}
if (!trackKeys.isEmpty()) {
builder.setView(dialogView);
}
builder.create().show();
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
updateSelectionList();
dialog.setTitle(R.string.exo_download_description);
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
}
@Override
public void onPrepareError(DownloadHelper helper, IOException e) {
public void onPrepareError(DownloadHelper<?> helper, IOException e) {
Toast.makeText(
context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG)
.show();
Log.e(TAG, "Failed to start download", e);
dialog.cancel();
}
// View.OnClickListener implementation.
@Override
public void onClick(View v) {
Integer rendererIndex = (Integer) v.getTag();
String dialogTitle = getTrackTypeString(mappedTrackInfo.getRendererType(rendererIndex));
Pair<AlertDialog, TrackSelectionView> dialogPair =
TrackSelectionView.getDialog(
dialog.getContext(),
dialogTitle,
mappedTrackInfo,
rendererIndex,
parameters,
/* callback= */ this);
dialogPair.second.setShowDisableOption(true);
dialogPair.second.setAllowAdaptiveSelections(false);
dialogPair.first.show();
}
// TrackSelectionView.DialogCallback implementation.
@Override
public void onTracksSelected(DefaultTrackSelector.Parameters parameters) {
for (int i = 0; i < downloadHelper.getPeriodCount(); i++) {
downloadHelper.replaceTrackSelections(/* periodIndex= */ i, parameters);
}
this.parameters = parameters;
updateSelectionList();
}
// DialogInterface.OnClickListener implementation.
@Override
public void onClick(DialogInterface dialog, int which) {
ArrayList<TrackKey> selectedTrackKeys = new ArrayList<>();
for (int i = 0; i < representationList.getChildCount(); i++) {
if (representationList.isItemChecked(i)) {
selectedTrackKeys.add(trackKeys.get(i));
DownloadAction downloadAction = downloadHelper.getDownloadAction(Util.getUtf8Bytes(name));
startDownload(downloadAction);
}
// Internal methods.
private void updateSelectionList() {
selectionList.removeAllViews();
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i);
if (trackGroupArray.length == 0) {
continue;
}
String trackTypeString =
getTrackTypeString(mappedTrackInfo.getRendererType(/* rendererIndex= */ i));
if (trackTypeString == null) {
return;
}
String trackSelectionsString = getTrackSelectionString(/* rendererIndex= */ i);
View view = dialogInflater.inflate(R.layout.download_track_item, selectionList, false);
TextView trackTitleView = view.findViewById(R.id.track_title);
TextView trackDescView = view.findViewById(R.id.track_desc);
ImageButton editButton = view.findViewById(R.id.edit_button);
trackTitleView.setText(trackTypeString);
trackDescView.setText(trackSelectionsString);
editButton.setTag(i);
editButton.setOnClickListener(this);
selectionList.addView(view);
}
}
private String getTrackSelectionString(int rendererIndex) {
List<TrackSelection> trackSelections =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, rendererIndex);
String selectedTracks = "";
Resources resources = selectionList.getResources();
for (int i = 0; i < trackSelections.size(); i++) {
TrackSelection selection = trackSelections.get(i);
for (int j = 0; j < selection.length(); j++) {
String trackName = trackNameProvider.getTrackName(selection.getFormat(j));
if (i == 0 && j == 0) {
selectedTracks = trackName;
} else {
selectedTracks = resources.getString(R.string.exo_item_list, selectedTracks, trackName);
}
}
}
if (!selectedTrackKeys.isEmpty() || trackKeys.isEmpty()) {
// We have selected keys, or we're dealing with single stream content.
DownloadAction downloadAction =
downloadHelper.getDownloadAction(Util.getUtf8Bytes(name), selectedTrackKeys);
startDownload(downloadAction);
return selectedTracks.isEmpty()
? resources.getString(R.string.exo_track_selection_none)
: selectedTracks;
}
@Nullable
private String getTrackTypeString(int trackType) {
Resources resources = selectionList.getResources();
switch (trackType) {
case C.TRACK_TYPE_VIDEO:
return resources.getString(R.string.exo_track_selection_title_video);
case C.TRACK_TYPE_AUDIO:
return resources.getString(R.string.exo_track_selection_title_audio);
case C.TRACK_TYPE_TEXT:
return resources.getString(R.string.exo_track_selection_title_text);
default:
return null;
}
}
}

View File

@ -35,11 +35,11 @@ import android.widget.TextView;
import android.widget.Toast;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.ContentType;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
@ -48,7 +48,6 @@ import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
import com.google.android.exoplayer2.offline.FilteringManifestParser;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
@ -58,11 +57,8 @@ import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistParserFactory;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
@ -416,13 +412,8 @@ public class PlayerActivity extends Activity
boolean preferExtensionDecoders =
intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false);
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode =
((DemoApplication) getApplication()).useExtensionRenderers()
? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
DefaultRenderersFactory renderersFactory =
new DefaultRenderersFactory(this, extensionRendererMode);
RenderersFactory renderersFactory =
((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);
trackSelector = new DefaultTrackSelector(trackSelectionFactory);
trackSelector.setParameters(trackSelectorParameters);
@ -477,21 +468,19 @@ public class PlayerActivity extends Activity
@SuppressWarnings("unchecked")
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
@ContentType int type = Util.inferContentType(uri, overrideExtension);
List<StreamKey> offlineStreamKeys = getOfflineStreamKeys(uri);
switch (type) {
case C.TYPE_DASH:
return new DashMediaSource.Factory(dataSourceFactory)
.setManifestParser(
new FilteringManifestParser<>(new DashManifestParser(), getOfflineStreamKeys(uri)))
.setStreamKeys(offlineStreamKeys)
.createMediaSource(uri);
case C.TYPE_SS:
return new SsMediaSource.Factory(dataSourceFactory)
.setManifestParser(
new FilteringManifestParser<>(new SsManifestParser(), getOfflineStreamKeys(uri)))
.setStreamKeys(offlineStreamKeys)
.createMediaSource(uri);
case C.TYPE_HLS:
return new HlsMediaSource.Factory(dataSourceFactory)
.setPlaylistParserFactory(
new DefaultHlsPlaylistParserFactory(getOfflineStreamKeys(uri)))
.setStreamKeys(offlineStreamKeys)
.createMediaSource(uri);
case C.TYPE_OTHER:
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);

View File

@ -37,6 +37,7 @@ import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
@ -177,7 +178,11 @@ public class SampleChooserActivity extends Activity
.show();
} else {
UriSample uriSample = (UriSample) sample;
downloadTracker.toggleDownload(this, sample.name, uriSample.uri, uriSample.extension);
RenderersFactory renderersFactory =
((DemoApplication) getApplication())
.buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem));
downloadTracker.toggleDownload(
this, sample.name, uriSample.uri, uriSample.extension, renderersFactory);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/track_title"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="4dp"/>
<TextView
android:id="@+id/track_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="4dp"/>
</LinearLayout>
<ImageButton
android:id="@+id/edit_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:contentDescription="@string/download_edit_track"
android:src="@drawable/ic_edit"/>
</LinearLayout>

View File

@ -13,7 +13,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/representation_list"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/selection_list"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

View File

@ -51,6 +51,10 @@
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
<string name="download_edit_track">Edit selection</string>
<string name="download_preparing">Preparing download…</string>
<string name="download_start_error">Failed to start download</string>
<string name="download_playlist_unsupported">This demo app does not support downloading playlists</string>

View File

@ -65,13 +65,6 @@ public final class TimelineQueueEditor
* {@link MediaSessionConnector}.
*/
public interface QueueDataAdapter {
/**
* Gets the {@link MediaDescriptionCompat} for a {@code position}.
*
* @param position The position in the queue for which to provide a description.
* @return A {@link MediaDescriptionCompat}.
*/
MediaDescriptionCompat getMediaDescription(int position);
/**
* Adds a {@link MediaDescriptionCompat} at the given {@code position}.
*

View File

@ -1693,7 +1693,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
*/
private static boolean codecNeedsEosFlushWorkaround(String name) {
return (Util.SDK_INT <= 23 && "OMX.google.vorbis.decoder".equals(name))
|| (Util.SDK_INT <= 19 && "hb2000".equals(Util.DEVICE)
|| (Util.SDK_INT <= 19
&& ("hb2000".equals(Util.DEVICE) || "stvm8".equals(Util.DEVICE))
&& ("OMX.amlogic.avc.decoder.awesome".equals(name)
|| "OMX.amlogic.avc.decoder.awesome.secure".equals(name)));
}

View File

@ -77,7 +77,7 @@ public final class DownloadAction {
*
* @param type The type of the action.
* @param uri The URI of the media to be downloaded.
* @param keys Keys of tracks to be downloaded. If empty, all tracks will be downloaded.
* @param keys Keys of streams to be downloaded. If empty, all streams will be downloaded.
* @param customCacheKey A custom key for cache indexing, or null.
* @param data Optional custom data for this action. If {@code null} an empty array will be used.
*/
@ -108,6 +108,8 @@ public final class DownloadAction {
/* data= */ null);
}
/** The unique content id. */
public final String id;
/** The type of the action. */
public final String type;
/** The uri being downloaded or removed. */
@ -115,8 +117,8 @@ public final class DownloadAction {
/** Whether this is a remove action. If false, this is a download action. */
public final boolean isRemoveAction;
/**
* Keys of tracks to be downloaded. If empty, all tracks will be downloaded. Empty if this action
* is a remove action.
* Keys of streams to be downloaded. If empty, all streams will be downloaded. Empty if this
* action is a remove action.
*/
public final List<StreamKey> keys;
/** A custom key for cache indexing, or null. */
@ -128,8 +130,8 @@ public final class DownloadAction {
* @param type The type of the action.
* @param uri The uri being downloaded or removed.
* @param isRemoveAction Whether this is a remove action. If false, this is a download action.
* @param keys Keys of tracks to be downloaded. If empty, all tracks will be downloaded. Empty if
* this action is a remove action.
* @param keys Keys of streams to be downloaded. If empty, all streams will be downloaded. Empty
* if this action is a remove action.
* @param customCacheKey A custom key for cache indexing, or null.
* @param data Custom data for this action. Null if this action is a remove action.
*/
@ -140,6 +142,7 @@ public final class DownloadAction {
List<StreamKey> keys,
@Nullable String customCacheKey,
@Nullable byte[] data) {
this.id = customCacheKey != null ? customCacheKey : uri.toString();
this.type = type;
this.uri = uri;
this.isRemoveAction = isRemoveAction;
@ -171,12 +174,10 @@ public final class DownloadAction {
/** Returns whether this is an action for the same media as the {@code other}. */
public boolean isSameMedia(DownloadAction other) {
return customCacheKey == null
? other.customCacheKey == null && uri.equals(other.uri)
: customCacheKey.equals(other.customCacheKey);
return id.equals(other.id);
}
/** Returns keys of tracks to be downloaded. */
/** Returns keys of streams to be downloaded. */
public List<StreamKey> 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();

View File

@ -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.
*
* <p>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<DownloadAction> actionQueue) {
DownloadAction removeAction = null;
DownloadAction downloadAction = null;
HashSet<StreamKey> 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());
}
}

View File

@ -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.
*
* <p>The helper extracts track information from the media, selects tracks for downloading, and
* creates {@link DownloadAction download actions} based on the selected tracks.
*
* <p>A typical usage of DownloadHelper follows these steps:
*
* <ol>
* <li>Construct the download helper with information about the {@link RenderersFactory renderers}
* and {@link DefaultTrackSelector.Parameters parameters} for track selection.
* <li>Prepare the helper using {@link #prepare(Callback)} and wait for the callback.
* <li>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)}.
* <li>Create download actions for the selected track using {@link #getDownloadAction(byte[])}.
* </ol>
*
* @param <T> The manifest type.
*/
public abstract class DownloadHelper<T> {
/**
* 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<T> {
*
* @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<T> {
* @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<TrackSelection> @MonotonicNonNull [][] trackSelectionsByPeriodAndRenderer;
private List<TrackSelection> @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<FrameworkMediaCrypto> 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<T> {
* 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<T> {
* Returns the track groups for the given period. Must not be called until after preparation
* completes.
*
* <p>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<T> {
}
/**
* 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<TrackSelection> 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<TrackKey> trackKeys) {
return DownloadAction.createDownloadAction(
downloadType, uri, toStreamKeys(trackKeys), cacheKey, data);
public final DownloadAction getDownloadAction(@Nullable byte[] data) {
Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer);
Assertions.checkNotNull(trackGroupArrays);
List<StreamKey> 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<TrackSelection> 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<T> {
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<StreamKey> toStreamKeys(List<TrackKey> trackKeys);
protected abstract StreamKey toStreamKey(
int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup);
@SuppressWarnings("unchecked")
@EnsuresNonNull("trackSelectionsByPeriodAndRenderer")
private void initializeTrackSelectionLists(int periodCount, int rendererCount) {
trackSelectionsByPeriodAndRenderer =
(List<TrackSelection>[][]) new List<?>[periodCount][rendererCount];
immutableTrackSelectionsByPeriodAndRenderer =
(List<TrackSelection>[][]) 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<TrackSelection> 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.
}
}
}

View File

@ -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<Task> tasks;
private final ArrayList<Task> activeDownloadTasks;
private final ArrayList<Download> downloads;
private final ArrayList<Download> activeDownloads;
private final Handler handler;
private final HandlerThread fileIOThread;
private final Handler fileIOHandler;
private final CopyOnWriteArraySet<Listener> 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:
*
* <ul>
* <li>It hasn't started yet.
* <li>There are no preceding conflicting tasks.
* <li>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.
* <li>The maximum number of active downloads hasn't been reached.
* </ul>
*
* 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<Task> pendingTasks = new ArrayList<>(tasks);
tasks.clear();
List<Download> 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<DownloadAction> 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}.
*
* <p>Transition diagram:
*
* <pre>
* canceled
* queued started completed
* failed
* </pre>
*/
@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<DownloadAction> 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) {

View File

@ -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<Class<? extends DownloadService>, RequirementsHelper>
requirementsHelpers = new HashMap<>();
private static final Requirements DEFAULT_REQUIREMENTS =
@ -99,7 +99,7 @@ public abstract class DownloadService extends Service {
* <p>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 {
*
* <p>Returns a notification to be displayed when this service running in the foreground.
*
* <p>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,
* <p>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)}.
*
* <p>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);

View File

@ -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 <T> The {@link FilterableManifest} type.
*/
public final class FilteringManifestParser<T extends FilterableManifest<T>> implements Parser<T> {
private final Parser<T> parser;
private final List<StreamKey> streamKeys;
private final Parser<? extends T> parser;
@Nullable private final List<StreamKey> 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<T> parser, List<StreamKey> streamKeys) {
public FilteringManifestParser(Parser<? extends T> parser, @Nullable List<StreamKey> streamKeys) {
this.parser = parser;
this.streamKeys = streamKeys;
}

View File

@ -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<Void> {
/**
* 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<Void> {
}
@Override
protected List<StreamKey> toStreamKeys(List<TrackKey> trackKeys) {
return Collections.emptyList();
protected StreamKey toStreamKey(
int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) {
return new StreamKey(periodIndex, trackGroupIndex, trackIndexInTrackGroup);
}
}

View File

@ -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").
*
* <p>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<StreamKey> {

View File

@ -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;
}
}

View File

@ -826,7 +826,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
public MediaSourceHolder(MediaSource mediaSource) {
this.mediaSource = mediaSource;
this.timeline = new DeferredTimeline();
this.timeline = DeferredTimeline.createWithDummyTimeline(mediaSource.getTag());
this.activeMediaPeriods = new ArrayList<>();
this.uid = new Object();
}
@ -951,10 +951,18 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
private static final class DeferredTimeline extends ForwardingTimeline {
private static final Object DUMMY_ID = new Object();
private static final DummyTimeline DUMMY_TIMELINE = new DummyTimeline();
private final Object replacedId;
/**
* Returns an instance with a dummy timeline using the provided window tag.
*
* @param windowTag A window tag.
*/
public static DeferredTimeline createWithDummyTimeline(@Nullable Object windowTag) {
return new DeferredTimeline(new DummyTimeline(windowTag), DUMMY_ID);
}
/**
* Returns an instance with a real timeline, replacing the provided period ID with the already
* assigned dummy period ID.
@ -968,11 +976,6 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
return new DeferredTimeline(timeline, firstPeriodUid);
}
/** Creates deferred timeline exposing a {@link DummyTimeline}. */
public DeferredTimeline() {
this(DUMMY_TIMELINE, DUMMY_ID);
}
private DeferredTimeline(Timeline timeline, Object replacedId) {
super(timeline);
this.replacedId = replacedId;
@ -1016,6 +1019,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
/** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */
private static final class DummyTimeline extends Timeline {
@Nullable private final Object tag;
public DummyTimeline(@Nullable Object tag) {
this.tag = tag;
}
@Override
public int getWindowCount() {
return 1;
@ -1025,7 +1034,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
public Window getWindow(
int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {
return window.set(
/* tag= */ null,
tag,
/* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET,
/* isSeekable= */ false,

View File

@ -19,8 +19,11 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/**
@ -83,6 +86,22 @@ public interface MediaPeriod extends SequenceableLoader {
*/
TrackGroupArray getTrackGroups();
/**
* Returns a list of {@link StreamKey stream keys} which allow to filter the media in this period
* to load only the parts needed to play the provided {@link TrackSelection}.
*
* <p>This 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<StreamKey> getStreamKeys(TrackSelection trackSelection) {
return Collections.emptyList();
}
/**
* Performs a track selection.
*

View File

@ -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<String, TtmlStyle> globalStyles = new HashMap<>();
Map<String, TtmlRegion> regionMap = new HashMap<>();
Map<String, String> 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<String, TtmlStyle> parseHeader(
XmlPullParser xmlParser,
Map<String, TtmlStyle> globalStyles,
CellResolution cellResolution,
TtsExtent ttsExtent,
Map<String, TtmlRegion> globalRegions,
CellResolution cellResolution)
Map<String, String> 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<String, String> 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.
*
* <p>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.
* <p>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;
}
}
}

View File

@ -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<String, Integer> nodeStartsByRegion;
private final HashMap<String, Integer> nodeEndsByRegion;
private List<TtmlNode> 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<Long> 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<Cue> getCues(long timeUs, Map<String, TtmlStyle> globalStyles,
Map<String, TtmlRegion> regionMap) {
TreeMap<String, SpannableStringBuilder> regionOutputs = new TreeMap<>();
traverseForText(timeUs, false, regionId, regionOutputs);
traverseForStyle(timeUs, globalStyles, regionOutputs);
public List<Cue> getCues(
long timeUs,
Map<String, TtmlStyle> globalStyles,
Map<String, TtmlRegion> regionMap,
Map<String, String> imageMap) {
List<Pair<String, String>> regionImageOutputs = new ArrayList<>();
traverseForImage(timeUs, regionId, regionImageOutputs);
TreeMap<String, SpannableStringBuilder> regionTextOutputs = new TreeMap<>();
traverseForText(timeUs, false, regionId, regionTextOutputs);
traverseForStyle(timeUs, globalStyles, regionTextOutputs);
List<Cue> cues = new ArrayList<>();
for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
// Create image based cues.
for (Pair<String, String> 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<String, SpannableStringBuilder> 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<Pair<String, String>> 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,

View File

@ -33,11 +33,16 @@ import java.util.Map;
private final long[] eventTimesUs;
private final Map<String, TtmlStyle> globalStyles;
private final Map<String, TtmlRegion> regionMap;
private final Map<String, String> imageMap;
public TtmlSubtitle(TtmlNode root, Map<String, TtmlStyle> globalStyles,
Map<String, TtmlRegion> regionMap) {
public TtmlSubtitle(
TtmlNode root,
Map<String, TtmlStyle> globalStyles,
Map<String, TtmlRegion> regionMap,
Map<String, String> 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<Cue> getCues(long timeUs) {
return root.getCues(timeUs, globalStyles, regionMap);
return root.getCues(timeUs, globalStyles, regionMap, imageMap);
}
@VisibleForTesting

View File

@ -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;

View File

@ -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));
}
};

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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.

View File

@ -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;
/**

View File

@ -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.

View File

@ -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.
*
* <p>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) {

View File

@ -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 =

View File

@ -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();

View File

@ -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<String, CachedContent> keyToContent;
private final SparseArray<String> 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:
*
* <p>[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
*
* <p>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.
*
* <p>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);
}
}

View File

@ -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.
*
* <p>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);
}
}

View File

@ -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.
}
}

View File

@ -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<String, Object> editedValues;
private final List<String> 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);

View File

@ -64,6 +64,10 @@ public final class DefaultContentMetadata implements ContentMetadata {
private final Map<String, byte[]> metadata;
public DefaultContentMetadata() {
this(Collections.emptyMap());
}
private DefaultContentMetadata(Map<String, byte[]> metadata) {
this.metadata = Collections.unmodifiableMap(metadata);
}
@ -74,7 +78,7 @@ public final class DefaultContentMetadata implements ContentMetadata {
*/
public DefaultContentMetadata copyWithMutationsApplied(ContentMetadataMutations mutations) {
Map<String, byte[]> 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<String, byte[]> otherMetadata) {
if (metadata.size() != otherMetadata.size()) {
return false;
}
for (Entry<String, byte[]> 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<String, byte[]> first, Map<String, byte[]> second) {
if (first.size() != second.size()) {
return false;
}
for (Entry<String, byte[]> 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<String, byte[]> applyMutations(
Map<String, byte[]> otherMetadata, ContentMetadataMutations mutations) {
HashMap<String, byte[]> metadata = new HashMap<>(otherMetadata);

View File

@ -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<CacheSpan> 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) {

View File

@ -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.
* <p>
* 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;
}

View File

@ -0,0 +1,26 @@
<tt xmlns="http://www.w3.org/ns/ttml" xmlns:ttm="http://www.w3.org/ns/ttml#metadata" xmlns:tts="http://www.w3.org/ns/ttml#styling" xmlns:smpte="http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt" xml:lang="eng">
<head>
<metadata>
<smpte:image imagetype="PNG" encoding="Base64" xml:id="img_0">
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
</smpte:image>
<smpte:image imagetype="PNG" encoding="Base64" xml:id="img_1">
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
</smpte:image>
</metadata>
<styling>
<style/>
</styling>
<layout>
<region xml:id="region_0" tts:extent="51% 12%" tts:origin="24% 78%"/>
<region xml:id="region_1" tts:extent="57% 6%" tts:origin="21% 85%"/>
<region xml:id="region_2" tts:extent="51% 12%" tts:origin="24% 28%"/>
<region xml:id="region_3" tts:extent="57% 6%" tts:origin="21% 35%"/>
</layout>
</head>
<body>
<div begin="00:00:00.200" end="00:00:03.000" region="region_2" smpte:backgroundImage="#img_0"/>
<div begin="00:00:03.200" end="00:00:06.937" region="region_3" smpte:backgroundImage="#img_1"/>
<div begin="00:00:07.200" end="00:59:03.000" region="region_2" smpte:backgroundImage="#img_0"/>
</body>
</tt>

View File

@ -0,0 +1,23 @@
<tt xmlns="http://www.w3.org/ns/ttml" xmlns:ttm="http://www.w3.org/ns/ttml#metadata" xmlns:tts="http://www.w3.org/ns/ttml#styling" xmlns:smpte="http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt" xml:lang="eng" tts:extent="1280px 720px">
<head>
<metadata>
<smpte:image imagetype="PNG" encoding="Base64" xml:id="img_0">
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
</smpte:image>
<smpte:image imagetype="PNG" encoding="Base64" xml:id="img_1">
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
</smpte:image>
</metadata>
<styling>
<style/>
</styling>
<layout>
<region xml:id="region_0" tts:extent="653px 86px" tts:origin="307px 562px"/>
<region xml:id="region_1" tts:extent="730px 43px" tts:origin="269px 612px"/>
</layout>
</head>
<body>
<div begin="00:00:00.200" end="00:00:03.000" region="region_0" smpte:backgroundImage="#img_0"/>
<div begin="00:00:03.200" end="00:00:06.937" region="region_1" smpte:backgroundImage="#img_1"/>
</body>
</tt>

View File

@ -0,0 +1,23 @@
<tt xmlns="http://www.w3.org/ns/ttml" xmlns:ttm="http://www.w3.org/ns/ttml#metadata" xmlns:tts="http://www.w3.org/ns/ttml#styling" xmlns:smpte="http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt" xml:lang="eng">
<head>
<metadata>
<smpte:image imagetype="PNG" encoding="Base64" xml:id="img_0">
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
</smpte:image>
<smpte:image imagetype="PNG" encoding="Base64" xml:id="img_1">
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
</smpte:image>
</metadata>
<styling>
<style/>
</styling>
<layout>
<region xml:id="region_0" tts:extent="653px 86px" tts:origin="307px 562px"/>
<region xml:id="region_1" tts:extent="730px 43px" tts:origin="269px 612px"/>
</layout>
</head>
<body>
<div begin="00:00:00.200" end="00:00:03.000" region="region_0" smpte:backgroundImage="#img_0"/>
<div begin="00:00:03.200" end="00:00:06.937" region="region_1" smpte:backgroundImage="#img_1"/>
</body>
</tt>

View File

@ -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<DownloadAction> 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<DownloadAction> 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<DownloadAction> 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<DownloadAction> 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<DownloadAction> 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<DownloadAction> 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<DownloadAction> 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<DownloadAction> 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<DownloadAction> 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<DownloadAction> 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<DownloadAction> 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<StreamKey> 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);
}
}

View File

@ -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<TrackSelection> selectedText0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);
List<TrackSelection> selectedAudio0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);
List<TrackSelection> selectedVideo0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);
List<TrackSelection> selectedText1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);
List<TrackSelection> selectedAudio1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);
List<TrackSelection> 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<TrackSelection> selectedText0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);
List<TrackSelection> selectedAudio0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);
List<TrackSelection> selectedVideo0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);
List<TrackSelection> selectedText1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);
List<TrackSelection> selectedAudio1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);
List<TrackSelection> 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<TrackSelection> selectedText0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);
List<TrackSelection> selectedAudio0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);
List<TrackSelection> selectedVideo0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);
List<TrackSelection> selectedText1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);
List<TrackSelection> selectedAudio1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);
List<TrackSelection> 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<TrackSelection> selectedText0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);
List<TrackSelection> selectedAudio0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);
List<TrackSelection> selectedVideo0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);
List<TrackSelection> selectedText1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);
List<TrackSelection> selectedAudio1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);
List<TrackSelection> 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<Exception> 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<TrackSelection> 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<ManifestType> {
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);
}
}
}

View File

@ -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<FakeDownloader> 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<DownloadAction, FakeDownloader> 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<Uri, DownloadRunner> 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++;
}
}
}

View File

@ -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<Cue> 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<Cue> 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<Cue> 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<Cue> 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<Cue> 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,

View File

@ -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,

View File

@ -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<CacheSpan> 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);
}

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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

View File

@ -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<StreamKey> 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<StreamKey> 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),

View File

@ -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<DashManifest> {
private final DataSource.Factory manifestDataSourceFactory;
public DashDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) {
super(DownloadAction.TYPE_DASH, uri, /* cacheKey= */ null);
/**
* Creates a DASH download helper.
*
* <p>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<FrameworkMediaCrypto> drmSessionManager) {
super(
DownloadAction.TYPE_DASH,
uri,
/* cacheKey= */ null,
trackSelectorParameters,
renderersFactory,
drmSessionManager);
this.manifestDataSourceFactory = manifestDataSourceFactory;
}
@ -72,12 +119,8 @@ public final class DashDownloadHelper extends DownloadHelper<DashManifest> {
}
@Override
protected List<StreamKey> toStreamKeys(List<TrackKey> trackKeys) {
List<StreamKey> 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);
}
}

View File

@ -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<StreamKey> 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<StreamKey> 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,

View File

@ -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<HlsPlaylist> {
private int[] renditionGroups;
public HlsDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) {
super(DownloadAction.TYPE_HLS, uri, /* cacheKey= */ null);
/**
* Creates a HLS download helper.
*
* <p>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<FrameworkMediaCrypto> drmSessionManager) {
super(
DownloadAction.TYPE_HLS,
uri,
/* cacheKey= */ null,
trackSelectorParameters,
renderersFactory,
drmSessionManager);
this.manifestDataSourceFactory = manifestDataSourceFactory;
}
@ -61,7 +108,7 @@ public final class HlsDownloadHelper extends DownloadHelper<HlsPlaylist> {
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<HlsPlaylist> {
}
@Override
protected List<StreamKey> toStreamKeys(List<TrackKey> trackKeys) {
List<StreamKey> 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<HlsMasterPlaylist.HlsUrl> hlsUrls) {

View File

@ -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<StreamKey> 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<StreamKey> streamKeys) {
this.streamKeys = streamKeys;
}
@Override
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser() {
return new FilteringManifestParser<>(new HlsPlaylistParser(), streamKeys);
return new HlsPlaylistParser();
}
@Override
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
HlsMasterPlaylist masterPlaylist) {
return new FilteringManifestParser<>(new HlsPlaylistParser(masterPlaylist), streamKeys);
return new HlsPlaylistParser(masterPlaylist);
}
}

View File

@ -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<StreamKey> 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<StreamKey> streamKeys) {
this.hlsPlaylistParserFactory = hlsPlaylistParserFactory;
this.streamKeys = streamKeys;
}
@Override
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser() {
return new FilteringManifestParser<>(
hlsPlaylistParserFactory.createPlaylistParser(), streamKeys);
}
@Override
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
HlsMasterPlaylist masterPlaylist) {
return new FilteringManifestParser<>(
hlsPlaylistParserFactory.createPlaylistParser(masterPlaylist), streamKeys);
}
}

View File

@ -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<ChunkSampleStream<SsChunkSource>> {
/** A SmoothStreaming {@link MediaPeriod}. */
/* package */ final class SsMediaPeriod
implements MediaPeriod, SequenceableLoader.Callback<ChunkSampleStream<SsChunkSource>> {
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<StreamKey> getStreamKeys(TrackSelection trackSelection) {
List<StreamKey> 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<SsChunkSource> sampleStream : sampleStreams) {
@ -211,7 +223,7 @@ import java.util.ArrayList;
return positionUs;
}
// SequenceableLoader.Callback implementation
// SequenceableLoader.Callback implementation.
@Override
public void onContinueLoadingRequested(ChunkSampleStream<SsChunkSource> sampleStream) {
@ -277,5 +289,4 @@ import java.util.ArrayList;
data[firstPosition] = data[secondPosition];
data[secondPosition] = temp;
}
}

View File

@ -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<StreamKey> 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<StreamKey> 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),

View File

@ -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<SsManifest> {
private final DataSource.Factory manifestDataSourceFactory;
public SsDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) {
super(DownloadAction.TYPE_SS, uri, /* cacheKey= */ null);
/**
* Creates a SmoothStreaming download helper.
*
* <p>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<FrameworkMediaCrypto> 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<SsManifest> {
}
@Override
protected List<StreamKey> toStreamKeys(List<TrackKey> trackKeys) {
List<StreamKey> 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);
}
}

View File

@ -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++;
}

View File

@ -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;
* <li>Type: {@link AspectRatioFrameLayout}
* </ul>
* <li><b>{@code exo_shutter}</b> - 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"}.
* <ul>
* <li>Type: {@link View}
* </ul>
@ -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

View File

@ -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.
* <p>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<AlertDialog, TrackSelectionView> 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<AlertDialog, TrackSelectionView> 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();

View File

@ -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;
}
}
}

View File

@ -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<DownloadAction, ArrayBlockingQueue<Integer>> actionStates;
private final HashMap<String, ArrayBlockingQueue<Integer>> 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<Integer> getStateQueue(DownloadAction action) {
private ArrayBlockingQueue<Integer> 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);
}
}
}