mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Merge branch 'dev-v2' into dev-v2
This commit is contained in:
commit
49a99beaef
@ -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
|
||||
|
@ -49,6 +49,16 @@ android {
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
|
||||
flavorDimensions "receiver"
|
||||
|
||||
productFlavors {
|
||||
defaultCast {
|
||||
dimension "receiver"
|
||||
manifestPlaceholders =
|
||||
[castOptionsProvider: "com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
BIN
demos/main/src/main/res/drawable-hdpi/ic_edit.png
Executable file
BIN
demos/main/src/main/res/drawable-hdpi/ic_edit.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 304 B |
BIN
demos/main/src/main/res/drawable-mdpi/ic_edit.png
Executable file
BIN
demos/main/src/main/res/drawable-mdpi/ic_edit.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 242 B |
BIN
demos/main/src/main/res/drawable-xhdpi/ic_edit.png
Executable file
BIN
demos/main/src/main/res/drawable-xhdpi/ic_edit.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 299 B |
BIN
demos/main/src/main/res/drawable-xxhdpi/ic_edit.png
Executable file
BIN
demos/main/src/main/res/drawable-xxhdpi/ic_edit.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 413 B |
BIN
demos/main/src/main/res/drawable-xxxhdpi/ic_edit.png
Executable file
BIN
demos/main/src/main/res/drawable-xxxhdpi/ic_edit.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 449 B |
53
demos/main/src/main/res/layout/download_track_item.xml
Normal file
53
demos/main/src/main/res/layout/download_track_item.xml
Normal 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>
|
@ -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"/>
|
||||
|
@ -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>
|
||||
|
@ -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}.
|
||||
*
|
||||
|
@ -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)));
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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> {
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
||||
/**
|
||||
|
@ -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.
|
||||
|
@ -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) {
|
||||
|
@ -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 =
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
23
library/core/src/test/assets/ttml/bitmap_pixel_region.xml
Normal file
23
library/core/src/test/assets/ttml/bitmap_pixel_region.xml
Normal 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>
|
@ -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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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++;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user