Merge branch 'dev-v2' into dev-v2

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

View File

@ -5,15 +5,37 @@
* Support for playing spherical videos on Daydream. * Support for playing spherical videos on Daydream.
* Improve decoder re-use between playbacks. TODO: Write and link a blog post * Improve decoder re-use between playbacks. TODO: Write and link a blog post
here ([#2826](https://github.com/google/ExoPlayer/issues/2826)). here ([#2826](https://github.com/google/ExoPlayer/issues/2826)).
* Add options for controlling audio track selections to `DefaultTrackSelector` * Track selection:
([#3314](https://github.com/google/ExoPlayer/issues/3314)). * 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`. * Do not retry failed loads whose error is `FileNotFoundException`.
* Prevent Cea608Decoder from generating Subtitles with null Cues list * Prevent Cea608Decoder from generating Subtitles with null Cues list.
* Caching: Cache data with unknown length by default. The previous flag to opt in * Offline:
to this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been * Speed up removal of segmented downloads
replaced with an opt out flag (`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`). ([#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 * MP3: Fix issue where streams would play twice on Samsung devices
([#4519](https://github.com/google/ExoPlayer/issues/4519)). ([#4519](https://github.com/google/ExoPlayer/issues/4519)).
### 2.9.2 ### ### 2.9.2 ###
* HLS: * HLS:
@ -61,10 +83,10 @@
* DASH: Parse ProgramInformation element if present in the manifest. * DASH: Parse ProgramInformation element if present in the manifest.
* HLS: * HLS:
* Add constructor to `DefaultHlsExtractorFactory` for adding TS payload * 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 * Fix bug in segment sniffing
([#5039](https://github.com/google/ExoPlayer/issues/5039)). ([#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 * SubRip: Add support for alignment tags, and remove tags from the displayed
captions ([#4306](https://github.com/google/ExoPlayer/issues/4306)). captions ([#4306](https://github.com/google/ExoPlayer/issues/4306)).
* Fix issue with blind seeking to windows with non-zero offset in a * Fix issue with blind seeking to windows with non-zero offset in a

View File

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

View File

@ -23,7 +23,7 @@
android:largeHeap="true" android:allowBackup="false"> android:largeHeap="true" android:allowBackup="false">
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" <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" <activity android:name="com.google.android.exoplayer2.castdemo.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"

View File

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

View File

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

View File

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

View File

@ -16,6 +16,8 @@
package com.google.android.exoplayer2.demo; package com.google.android.exoplayer2.demo;
import android.app.Application; 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.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
@ -72,6 +74,17 @@ public class DemoApplication extends Application {
return "withExtensions".equals(BuildConfig.FLAVOR); 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() { public DownloadManager getDownloadManager() {
initDownloadManager(); initDownloadManager();
return downloadManager; return downloadManager;

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/track_title"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="4dp"/>
<TextView
android:id="@+id/track_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="4dp"/>
</LinearLayout>
<ImageButton
android:id="@+id/edit_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:contentDescription="@string/download_edit_track"
android:src="@drawable/ic_edit"/>
</LinearLayout>

View File

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

View File

@ -51,6 +51,10 @@
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string> <string name="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_start_error">Failed to start download</string>
<string name="download_playlist_unsupported">This demo app does not support downloading playlists</string> <string name="download_playlist_unsupported">This demo app does not support downloading playlists</string>

View File

@ -65,13 +65,6 @@ public final class TimelineQueueEditor
* {@link MediaSessionConnector}. * {@link MediaSessionConnector}.
*/ */
public interface QueueDataAdapter { 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}. * Adds a {@link MediaDescriptionCompat} at the given {@code position}.
* *

View File

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

View File

@ -77,7 +77,7 @@ public final class DownloadAction {
* *
* @param type The type of the action. * @param type The type of the action.
* @param uri The URI of the media to be downloaded. * @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 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. * @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); /* data= */ null);
} }
/** The unique content id. */
public final String id;
/** The type of the action. */ /** The type of the action. */
public final String type; public final String type;
/** The uri being downloaded or removed. */ /** 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. */ /** Whether this is a remove action. If false, this is a download action. */
public final boolean isRemoveAction; public final boolean isRemoveAction;
/** /**
* Keys of tracks to be downloaded. If empty, all tracks will be downloaded. Empty if this action * Keys of streams to be downloaded. If empty, all streams will be downloaded. Empty if this
* is a remove action. * action is a remove action.
*/ */
public final List<StreamKey> keys; public final List<StreamKey> keys;
/** A custom key for cache indexing, or null. */ /** A custom key for cache indexing, or null. */
@ -128,8 +130,8 @@ public final class DownloadAction {
* @param type The type of the action. * @param type The type of the action.
* @param uri The uri being downloaded or removed. * @param uri The uri being downloaded or removed.
* @param isRemoveAction Whether this is a remove action. If false, this is a download action. * @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 * @param keys Keys of streams to be downloaded. If empty, all streams will be downloaded. Empty
* this action is a remove action. * if this action is a remove action.
* @param customCacheKey A custom key for cache indexing, or null. * @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. * @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, List<StreamKey> keys,
@Nullable String customCacheKey, @Nullable String customCacheKey,
@Nullable byte[] data) { @Nullable byte[] data) {
this.id = customCacheKey != null ? customCacheKey : uri.toString();
this.type = type; this.type = type;
this.uri = uri; this.uri = uri;
this.isRemoveAction = isRemoveAction; 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}. */ /** Returns whether this is an action for the same media as the {@code other}. */
public boolean isSameMedia(DownloadAction other) { public boolean isSameMedia(DownloadAction other) {
return customCacheKey == null return id.equals(other.id);
? other.customCacheKey == null && uri.equals(other.uri)
: customCacheKey.equals(other.customCacheKey);
} }
/** Returns keys of tracks to be downloaded. */ /** Returns keys of streams to be downloaded. */
public List<StreamKey> getKeys() { public List<StreamKey> getKeys() {
return keys; return keys;
} }
@ -187,7 +188,8 @@ public final class DownloadAction {
return false; return false;
} }
DownloadAction that = (DownloadAction) o; DownloadAction that = (DownloadAction) o;
return type.equals(that.type) return id.equals(that.id)
&& type.equals(that.type)
&& uri.equals(that.uri) && uri.equals(that.uri)
&& isRemoveAction == that.isRemoveAction && isRemoveAction == that.isRemoveAction
&& keys.equals(that.keys) && keys.equals(that.keys)
@ -198,6 +200,7 @@ public final class DownloadAction {
@Override @Override
public final int hashCode() { public final int hashCode() {
int result = type.hashCode(); int result = type.hashCode();
result = 31 * result + id.hashCode();
result = 31 * result + uri.hashCode(); result = 31 * result + uri.hashCode();
result = 31 * result + (isRemoveAction ? 1 : 0); result = 31 * result + (isRemoveAction ? 1 : 0);
result = 31 * result + keys.hashCode(); result = 31 * result + keys.hashCode();

View File

@ -0,0 +1,79 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.offline;
import com.google.android.exoplayer2.util.Assertions;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashSet;
/** {@link DownloadAction} related utility methods. */
public class DownloadActionUtil {
private DownloadActionUtil() {}
/**
* Merge {@link DownloadAction}s in {@code actionQueue} to minimum number of actions.
*
* <p>All actions must have the same type and must be for the same media.
*
* @param actionQueue Queue of actions. Must not be empty.
* @return The first action in the queue.
*/
public static DownloadAction mergeActions(ArrayDeque<DownloadAction> actionQueue) {
DownloadAction removeAction = null;
DownloadAction downloadAction = null;
HashSet<StreamKey> keys = new HashSet<>();
boolean downloadAllTracks = false;
DownloadAction firstAction = Assertions.checkNotNull(actionQueue.peek());
while (!actionQueue.isEmpty()) {
DownloadAction action = actionQueue.remove();
Assertions.checkState(action.type.equals(firstAction.type));
Assertions.checkState(action.isSameMedia(firstAction));
if (action.isRemoveAction) {
removeAction = action;
downloadAction = null;
keys.clear();
downloadAllTracks = false;
} else {
if (!downloadAllTracks) {
if (action.keys.isEmpty()) {
downloadAllTracks = true;
keys.clear();
} else {
keys.addAll(action.keys);
}
}
downloadAction = action;
}
}
if (removeAction != null) {
actionQueue.add(removeAction);
}
if (downloadAction != null) {
actionQueue.add(
DownloadAction.createDownloadAction(
downloadAction.type,
downloadAction.uri,
new ArrayList<>(keys),
downloadAction.customCacheKey,
downloadAction.data));
}
return Assertions.checkNotNull(actionQueue.peek());
}
}

View File

@ -19,18 +19,66 @@ import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.support.annotation.Nullable; 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.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.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List; 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. * 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. * @param <T> The manifest type.
*/ */
public abstract class DownloadHelper<T> { 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. */ /** A callback to be notified when the {@link DownloadHelper} is prepared. */
public interface Callback { public interface Callback {
@ -39,7 +87,7 @@ public abstract class DownloadHelper<T> {
* *
* @param helper The reporting {@link DownloadHelper}. * @param helper The reporting {@link DownloadHelper}.
*/ */
void onPrepared(DownloadHelper helper); void onPrepared(DownloadHelper<?> helper);
/** /**
* Called when preparation fails. * Called when preparation fails.
@ -47,27 +95,51 @@ public abstract class DownloadHelper<T> {
* @param helper The reporting {@link DownloadHelper}. * @param helper The reporting {@link DownloadHelper}.
* @param e The error. * @param e The error.
*/ */
void onPrepareError(DownloadHelper helper, IOException e); void onPrepareError(DownloadHelper<?> helper, IOException e);
} }
private final String downloadType; private final String downloadType;
private final Uri uri; private final Uri uri;
@Nullable private final String cacheKey; @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 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 downloadType A download type. This value will be used as {@link DownloadAction#type}.
* @param uri A {@link Uri}. * @param uri A {@link Uri}.
* @param cacheKey An optional cache key. * @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.downloadType = downloadType;
this.uri = uri; this.uri = uri;
this.cacheKey = cacheKey; 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 * 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. * Looper}, in which case it will be called on the application's main thread.
*/ */
public final void prepare(final Callback callback) { public final void prepare(Callback callback) {
final Handler handler = Handler handler =
new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper()); new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper());
new Thread() { new Thread(
@Override () -> {
public void run() { try {
try { manifest = loadManifest(uri);
manifest = loadManifest(uri); trackGroupArrays = getTrackGroupArrays(manifest);
trackGroupArrays = getTrackGroupArrays(manifest); initializeTrackSelectionLists(trackGroupArrays.length, rendererCapabilities.length);
handler.post(() -> callback.onPrepared(DownloadHelper.this)); mappedTrackInfos = new MappedTrackInfo[trackGroupArrays.length];
} catch (final IOException e) { for (int i = 0; i < trackGroupArrays.length; i++) {
handler.post(() -> callback.onPrepareError(DownloadHelper.this, e)); TrackSelectorResult trackSelectorResult = runTrackSelection(/* periodIndex= */ i);
} trackSelector.onSelectionActivated(trackSelectorResult.info);
} mappedTrackInfos[i] =
}.start(); 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. */ /** 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 * Returns the track groups for the given period. Must not be called until after preparation
* completes. * completes.
* *
* <p>Use {@link #getMappedTrackInfo(int)} to get the track groups mapped to renderers.
*
* @param periodIndex The period index. * @param periodIndex The period index.
* @return The track groups for the period. May be {@link TrackGroupArray#EMPTY} for single stream * @return The track groups for the period. May be {@link TrackGroupArray#EMPTY} for single stream
* content. * 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. * after preparation completes.
* *
* @param data Application provided data to store in {@link DownloadAction#data}. * @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}. * @return The built {@link DownloadAction}.
*/ */
public final DownloadAction getDownloadAction(@Nullable byte[] data, List<TrackKey> trackKeys) { public final DownloadAction getDownloadAction(@Nullable byte[] data) {
return DownloadAction.createDownloadAction( Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer);
downloadType, uri, toStreamKeys(trackKeys), cacheKey, data); 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); 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. * @param periodIndex The index of the containing period.
* @return A corresponding list of stream keys. * @param trackGroupIndex The index of the containing track group within the period.
* @param trackIndexInTrackGroup The index of the track within the track group.
* @return The corresponding {@link StreamKey}.
*/ */
protected abstract List<StreamKey> toStreamKeys(List<TrackKey> trackKeys); protected abstract StreamKey toStreamKey(
int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup);
@SuppressWarnings("unchecked")
@EnsuresNonNull("trackSelectionsByPeriodAndRenderer")
private void initializeTrackSelectionLists(int periodCount, int rendererCount) {
trackSelectionsByPeriodAndRenderer =
(List<TrackSelection>[][]) new List<?>[periodCount][rendererCount];
immutableTrackSelectionsByPeriodAndRenderer =
(List<TrackSelection>[][]) new List<?>[periodCount][rendererCount];
for (int i = 0; i < periodCount; i++) {
for (int j = 0; j < rendererCount; j++) {
trackSelectionsByPeriodAndRenderer[i][j] = new ArrayList<>();
immutableTrackSelectionsByPeriodAndRenderer[i][j] =
Collections.unmodifiableList(trackSelectionsByPeriodAndRenderer[i][j]);
}
}
}
/**
* Runs the track selection for a given period index with the current parameters. The selected
* tracks will be added to {@link #trackSelectionsByPeriodAndRenderer}.
*/
// Intentional reference comparison of track group instances.
@SuppressWarnings("ReferenceEquality")
@RequiresNonNull({"trackGroupArrays", "trackSelectionsByPeriodAndRenderer"})
private TrackSelectorResult runTrackSelection(int periodIndex) {
// TODO: Use actual timeline and media period id.
MediaPeriodId dummyMediaPeriodId = new MediaPeriodId(new Object());
Timeline dummyTimeline = Timeline.EMPTY;
currentTrackSelectionPeriodIndex = periodIndex;
try {
TrackSelectorResult trackSelectorResult =
trackSelector.selectTracks(
rendererCapabilities,
trackGroupArrays[periodIndex],
dummyMediaPeriodId,
dummyTimeline);
for (int i = 0; i < trackSelectorResult.length; i++) {
TrackSelection newSelection = trackSelectorResult.selections.get(i);
if (newSelection == null) {
continue;
}
List<TrackSelection> existingSelectionList =
trackSelectionsByPeriodAndRenderer[currentTrackSelectionPeriodIndex][i];
boolean mergedWithExistingSelection = false;
for (int j = 0; j < existingSelectionList.size(); j++) {
TrackSelection existingSelection = existingSelectionList.get(j);
if (existingSelection.getTrackGroup() == newSelection.getTrackGroup()) {
// Merge with existing selection.
scratchSet.clear();
for (int k = 0; k < existingSelection.length(); k++) {
scratchSet.put(existingSelection.getIndexInTrackGroup(k), 0);
}
for (int k = 0; k < newSelection.length(); k++) {
scratchSet.put(newSelection.getIndexInTrackGroup(k), 0);
}
int[] mergedTracks = new int[scratchSet.size()];
for (int k = 0; k < scratchSet.size(); k++) {
mergedTracks[k] = scratchSet.keyAt(k);
}
existingSelectionList.set(
j, new DownloadTrackSelection(existingSelection.getTrackGroup(), mergedTracks));
mergedWithExistingSelection = true;
break;
}
}
if (!mergedWithExistingSelection) {
existingSelectionList.add(newSelection);
}
}
return trackSelectorResult;
} catch (ExoPlaybackException e) {
// DefaultTrackSelector does not throw exceptions during track selection.
throw new UnsupportedOperationException(e);
}
}
private static final class DownloadTrackSelection extends BaseTrackSelection {
private static final class Factory implements TrackSelection.Factory {
@Override
public @NullableType TrackSelection[] createTrackSelections(
@NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
@NullableType TrackSelection[] selections = new TrackSelection[definitions.length];
for (int i = 0; i < definitions.length; i++) {
selections[i] =
definitions[i] == null
? null
: new DownloadTrackSelection(definitions[i].group, definitions[i].tracks);
}
return selections;
}
}
public DownloadTrackSelection(TrackGroup trackGroup, int[] tracks) {
super(trackGroup, tracks);
}
@Override
public int getSelectedIndex() {
return 0;
}
@Override
public int getSelectionReason() {
return C.SELECTION_REASON_UNKNOWN;
}
@Nullable
@Override
public Object getSelectionData() {
return null;
}
}
private static final class DummyBandwidthMeter implements BandwidthMeter {
@Override
public long getBitrateEstimate() {
return 0;
}
@Nullable
@Override
public TransferListener getTransferListener() {
return null;
}
@Override
public void addEventListener(Handler eventHandler, EventListener eventListener) {
// Do nothing.
}
@Override
public void removeEventListener(EventListener eventListener) {
// Do nothing.
}
}
} }

View File

@ -15,11 +15,12 @@
*/ */
package com.google.android.exoplayer2.offline; 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.DownloadState.FAILURE_REASON_NONE;
import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_COMPLETED; import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.FAILURE_REASON_UNKNOWN;
import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_FAILED; import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_COMPLETED;
import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_QUEUED; import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_FAILED;
import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_STARTED; 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.ConditionVariable;
import android.os.Handler; import android.os.Handler;
@ -35,6 +36,7 @@ import java.io.IOException;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
@ -58,41 +60,40 @@ public final class DownloadManager {
*/ */
void onInitialized(DownloadManager 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 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. * @param downloadManager The reporting instance.
*/ */
void onIdle(DownloadManager downloadManager); 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; 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; public static final int DEFAULT_MIN_RETRY_COUNT = 5;
private static final String TAG = "DownloadManager"; private static final String TAG = "DownloadManager";
private static final boolean DEBUG = false; private static final boolean DEBUG = false;
private final int maxActiveDownloadTasks; private final int maxActiveDownloads;
private final int minRetryCount; private final int minRetryCount;
private final ActionFile actionFile; private final ActionFile actionFile;
private final DownloaderFactory downloaderFactory; private final DownloaderFactory downloaderFactory;
private final ArrayList<Task> tasks; private final ArrayList<Download> downloads;
private final ArrayList<Task> activeDownloadTasks; private final ArrayList<Download> activeDownloads;
private final Handler handler; private final Handler handler;
private final HandlerThread fileIOThread; private final HandlerThread fileIOThread;
private final Handler fileIOHandler; private final Handler fileIOHandler;
private final CopyOnWriteArraySet<Listener> listeners; private final CopyOnWriteArraySet<Listener> listeners;
private int nextTaskId;
private boolean initialized; private boolean initialized;
private boolean released; private boolean released;
private boolean downloadsStopped; private boolean downloadsStopped;
@ -113,8 +114,8 @@ public final class DownloadManager {
* *
* @param actionFile The file in which active actions are saved. * @param actionFile The file in which active actions are saved.
* @param downloaderFactory A factory for creating {@link Downloader}s. * @param downloaderFactory A factory for creating {@link Downloader}s.
* @param maxSimultaneousDownloads The maximum number of simultaneous download tasks. * @param maxSimultaneousDownloads The maximum number of simultaneous downloads.
* @param minRetryCount The minimum number of times a task must be retried before failing. * @param minRetryCount The minimum number of times a download must be retried before failing.
*/ */
public DownloadManager( public DownloadManager(
File actionFile, File actionFile,
@ -123,12 +124,12 @@ public final class DownloadManager {
int minRetryCount) { int minRetryCount) {
this.actionFile = new ActionFile(actionFile); this.actionFile = new ActionFile(actionFile);
this.downloaderFactory = downloaderFactory; this.downloaderFactory = downloaderFactory;
this.maxActiveDownloadTasks = maxSimultaneousDownloads; this.maxActiveDownloads = maxSimultaneousDownloads;
this.minRetryCount = minRetryCount; this.minRetryCount = minRetryCount;
this.downloadsStopped = true; this.downloadsStopped = true;
tasks = new ArrayList<>(); downloads = new ArrayList<>();
activeDownloadTasks = new ArrayList<>(); activeDownloads = new ArrayList<>();
Looper looper = Looper.myLooper(); Looper looper = Looper.myLooper();
if (looper == null) { if (looper == null) {
@ -164,85 +165,78 @@ public final class DownloadManager {
listeners.remove(listener); listeners.remove(listener);
} }
/** Starts the download tasks. */ /** Starts the downloads. */
public void startDownloads() { public void startDownloads() {
Assertions.checkState(!released); Assertions.checkState(!released);
if (downloadsStopped) { if (downloadsStopped) {
downloadsStopped = false; downloadsStopped = false;
maybeStartTasks(); maybeStartDownloads();
logd("Downloads are started"); 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() { public void stopDownloads() {
Assertions.checkState(!released); Assertions.checkState(!released);
if (!downloadsStopped) { if (!downloadsStopped) {
downloadsStopped = true; downloadsStopped = true;
for (int i = 0; i < activeDownloadTasks.size(); i++) { for (int i = 0; i < activeDownloads.size(); i++) {
activeDownloadTasks.get(i).stop(); activeDownloads.get(i).stop();
} }
logd("Downloads are stopping"); logd("Downloads are stopping");
} }
} }
/** /**
* Handles the given action. A task is created and added to the task queue. If it's a remove * Handles the given action.
* action then any download tasks for the same media are immediately canceled.
* *
* @param action The action to be executed. * @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); Assertions.checkState(!released);
Task task = addTaskForAction(action); Download download = getDownloadForAction(action);
if (initialized) { if (initialized) {
saveActions(); saveActions();
maybeStartTasks(); maybeStartDownloads();
if (task.state == STATE_QUEUED) { if (download.state == STATE_QUEUED) {
// Task did not change out of its initial state, and so its initial state won't have been // Download did not change out of its initial state, and so its initial state won't have
// been
// reported to listeners. Do so now. // reported to listeners. Do so now.
notifyListenersTaskStateChange(task); notifyListenersDownloadStateChange(download);
} }
} }
return task.id;
} }
/** Returns the number of tasks. */ /** Returns the number of downloads. */
public int getTaskCount() {
Assertions.checkState(!released);
return tasks.size();
}
/** Returns the number of download tasks. */
public int getDownloadCount() { public int getDownloadCount() {
int count = 0; Assertions.checkState(!released);
for (int i = 0; i < tasks.size(); i++) { return downloads.size();
if (!tasks.get(i).action.isRemoveAction) {
count++;
}
}
return count;
} }
/** 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); Assertions.checkState(!released);
for (int i = 0; i < tasks.size(); i++) { for (int i = 0; i < downloads.size(); i++) {
Task task = tasks.get(i); Download download = downloads.get(i);
if (task.id == taskId) { if (download.id.equals(id)) {
return task.getTaskState(); return download.getDownloadState();
} }
} }
return null; return null;
} }
/** Returns the states of all current tasks. */ /** Returns the states of all current downloads. */
public TaskState[] getAllTaskStates() { public DownloadState[] getAllDownloadStates() {
Assertions.checkState(!released); Assertions.checkState(!released);
TaskState[] states = new TaskState[tasks.size()]; DownloadState[] states = new DownloadState[downloads.size()];
for (int i = 0; i < states.length; i++) { for (int i = 0; i < states.length; i++) {
states[i] = tasks.get(i).getTaskState(); states[i] = downloads.get(i).getDownloadState();
} }
return states; return states;
} }
@ -253,14 +247,14 @@ public final class DownloadManager {
return initialized; return initialized;
} }
/** Returns whether there are no active tasks. */ /** Returns whether there are no active downloads. */
public boolean isIdle() { public boolean isIdle() {
Assertions.checkState(!released); Assertions.checkState(!released);
if (!initialized) { if (!initialized) {
return false; return false;
} }
for (int i = 0; i < tasks.size(); i++) { for (int i = 0; i < downloads.size(); i++) {
if (tasks.get(i).isStarted()) { if (downloads.get(i).isStarted()) {
return false; 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 * Stops all of the downloads and releases resources. If the action file isn't up to date, waits
* the changes to be written. The manager must not be accessed after this method has been called. * for the changes to be written. The manager must not be accessed after this method has been
* called.
*/ */
public void release() { public void release() {
if (released) { if (released) {
return; return;
} }
released = true; released = true;
for (int i = 0; i < tasks.size(); i++) { for (int i = 0; i < downloads.size(); i++) {
tasks.get(i).stop(); downloads.get(i).stop();
} }
final ConditionVariable fileIOFinishedCondition = new ConditionVariable(); final ConditionVariable fileIOFinishedCondition = new ConditionVariable();
fileIOHandler.post(fileIOFinishedCondition::open); fileIOHandler.post(fileIOFinishedCondition::open);
@ -286,66 +281,46 @@ public final class DownloadManager {
logd("Released"); logd("Released");
} }
private Task addTaskForAction(DownloadAction action) { private Download getDownloadForAction(DownloadAction action) {
Task task = new Task(nextTaskId++, this, downloaderFactory, action, minRetryCount); for (int i = 0; i < downloads.size(); i++) {
tasks.add(task); Download download = downloads.get(i);
logd("Task is added", task); if (download.action.isSameMedia(action)) {
return task; 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> * <ul>
* <li>It hasn't started yet. * <li>It hasn't started yet.
* <li>There are no preceding conflicting tasks. * <li>The maximum number of active downloads hasn't been reached.
* <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.
* </ul> * </ul>
*
* If the task is a remove action then preceding conflicting tasks are canceled.
*/ */
private void maybeStartTasks() { private void maybeStartDownloads() {
if (!initialized || released) { if (!initialized || released) {
return; return;
} }
boolean skipDownloadActions = downloadsStopped boolean skipDownloads = downloadsStopped || activeDownloads.size() == maxActiveDownloads;
|| activeDownloadTasks.size() == maxActiveDownloadTasks; for (int i = 0; i < downloads.size(); i++) {
for (int i = 0; i < tasks.size(); i++) { Download download = downloads.get(i);
Task task = tasks.get(i); if (!download.canStart()) {
if (!task.canStart()) {
continue; continue;
} }
boolean isRemoveAction = download.action.isRemoveAction;
DownloadAction action = task.action; if (isRemoveAction || !skipDownloads) {
boolean isRemoveAction = action.isRemoveAction; download.start();
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();
if (!isRemoveAction) { if (!isRemoveAction) {
activeDownloadTasks.add(task); activeDownloads.add(download);
skipDownloadActions = activeDownloadTasks.size() == maxActiveDownloadTasks; skipDownloads = activeDownloads.size() == maxActiveDownloads;
} }
} }
} }
@ -361,30 +336,30 @@ public final class DownloadManager {
} }
} }
private void onTaskStateChange(Task task) { private void onDownloadStateChange(Download download) {
if (released) { if (released) {
return; return;
} }
boolean stopped = !task.isStarted(); boolean stopped = !download.isStarted();
if (stopped) { if (stopped) {
activeDownloadTasks.remove(task); activeDownloads.remove(download);
} }
notifyListenersTaskStateChange(task); notifyListenersDownloadStateChange(download);
if (task.isFinished()) { if (download.isFinished()) {
tasks.remove(task); downloads.remove(download);
saveActions(); saveActions();
} }
if (stopped) { if (stopped) {
maybeStartTasks(); maybeStartDownloads();
maybeNotifyListenersIdle(); maybeNotifyListenersIdle();
} }
} }
private void notifyListenersTaskStateChange(Task task) { private void notifyListenersDownloadStateChange(Download download) {
logd("Task state is changed", task); logd("Download state is changed", download);
TaskState taskState = task.getTaskState(); DownloadState downloadState = download.getDownloadState();
for (Listener listener : listeners) { for (Listener listener : listeners) {
listener.onTaskStateChanged(this, taskState); listener.onDownloadStateChanged(this, downloadState);
} }
} }
@ -405,27 +380,27 @@ public final class DownloadManager {
if (released) { if (released) {
return; return;
} }
List<Task> pendingTasks = new ArrayList<>(tasks); List<Download> pendingDownloads = new ArrayList<>(downloads);
tasks.clear(); downloads.clear();
for (DownloadAction action : actions) { for (DownloadAction action : actions) {
addTaskForAction(action); getDownloadForAction(action);
} }
logd("Tasks are created."); logd("Downloads are created.");
initialized = true; initialized = true;
for (Listener listener : listeners) { for (Listener listener : listeners) {
listener.onInitialized(DownloadManager.this); listener.onInitialized(DownloadManager.this);
} }
if (!pendingTasks.isEmpty()) { if (!pendingDownloads.isEmpty()) {
tasks.addAll(pendingTasks); downloads.addAll(pendingDownloads);
saveActions(); saveActions();
} }
maybeStartTasks(); maybeStartDownloads();
for (int i = 0; i < tasks.size(); i++) { for (int i = 0; i < downloads.size(); i++) {
Task task = tasks.get(i); Download download = downloads.get(i);
if (task.state == STATE_QUEUED) { if (download.state == STATE_QUEUED) {
// Task did not change out of its initial state, and so its initial state // Download did not change out of its initial state, and so its initial state
// won't have been reported to listeners. Do so now. // won't have been reported to listeners. Do so now.
notifyListenersTaskStateChange(task); notifyListenersDownloadStateChange(download);
} }
} }
}); });
@ -436,14 +411,15 @@ public final class DownloadManager {
if (released) { if (released) {
return; return;
} }
final DownloadAction[] actions = new DownloadAction[tasks.size()]; ArrayList<DownloadAction> actions = new ArrayList<>(downloads.size());
for (int i = 0; i < tasks.size(); i++) { for (int i = 0; i < downloads.size(); i++) {
actions[i] = tasks.get(i).action; actions.addAll(downloads.get(i).actionQueue);
} }
final DownloadAction[] actionsArray = actions.toArray(new DownloadAction[0]);
fileIOHandler.post( fileIOHandler.post(
() -> { () -> {
try { try {
actionFile.store(actions); actionFile.store(actionsArray);
logd("Actions persisted."); logd("Actions persisted.");
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Persisting actions failed.", e); Log.e(TAG, "Persisting actions failed.", e);
@ -457,39 +433,46 @@ public final class DownloadManager {
} }
} }
private static void logd(String message, Task task) { private static void logd(String message, Download download) {
logd(message + ": " + task); logd(message + ": " + download);
} }
/** Represents state of a task. */ /** Represents state of a download. */
public static final class TaskState { public static final class DownloadState {
/** /**
* Task states. One of {@link #STATE_QUEUED}, {@link #STATE_STARTED}, {@link #STATE_COMPLETED}, * Download states. One of {@link #STATE_QUEUED}, {@link #STATE_STARTED}, {@link
* {@link #STATE_CANCELED} or {@link #STATE_FAILED}. * #STATE_COMPLETED} or {@link #STATE_FAILED}.
* *
* <p>Transition diagram: * <p>Transition diagram:
* *
* <pre> * <pre>
* canceled
* queued started completed * queued started completed
* failed * failed
* </pre> * </pre>
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @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 {} public @interface State {}
/** The task is waiting to be started. */ /** The download is waiting to be started. */
public static final int STATE_QUEUED = 0; public static final int STATE_QUEUED = 0;
/** The task is currently started. */ /** The download is currently started. */
public static final int STATE_STARTED = 1; public static final int STATE_STARTED = 1;
/** The task completed. */ /** The download completed. */
public static final int STATE_COMPLETED = 2; public static final int STATE_COMPLETED = 2;
/** The task was canceled. */ /** The download failed. */
public static final int STATE_CANCELED = 3; public static final int STATE_FAILED = 3;
/** The task failed. */
public static final int STATE_FAILED = 4; /** 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. */ /** Returns the state string for the given state value. */
public static String getStateString(@State int state) { public static String getStateString(@State int state) {
@ -500,8 +483,6 @@ public final class DownloadManager {
return "STARTED"; return "STARTED";
case STATE_COMPLETED: case STATE_COMPLETED:
return "COMPLETED"; return "COMPLETED";
case STATE_CANCELED:
return "CANCELED";
case STATE_FAILED: case STATE_FAILED:
return "FAILED"; return "FAILED";
default: default:
@ -509,97 +490,151 @@ public final class DownloadManager {
} }
} }
/** The unique task id. */ /** Returns the failure string for the given failure reason value. */
public final int taskId; 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. */ /** The action being executed. */
public final DownloadAction action; public final DownloadAction action;
/** The state of the task. */ /** The state of the download. */
public final @State int state; public final @State int state;
/** The estimated download percentage, or {@link C#PERCENTAGE_UNSET} if unavailable. */
/**
* The estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is available
* or if this is a removal task.
*/
public final float downloadPercentage; public final float downloadPercentage;
/** The total number of downloaded bytes. */ /** The total number of downloaded bytes. */
public final long downloadedBytes; 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( private DownloadState(
int taskId,
DownloadAction action, DownloadAction action,
@State int state, @State int state,
float downloadPercentage, float downloadPercentage,
long downloadedBytes, long downloadedBytes,
@Nullable Throwable error) { long totalBytes,
this.taskId = taskId; @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.action = action;
this.state = state; this.state = state;
this.downloadPercentage = downloadPercentage; this.downloadPercentage = downloadPercentage;
this.downloadedBytes = downloadedBytes; 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. */ /** Target states for the download thread. */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_COMPLETED, STATE_QUEUED, STATE_CANCELED}) @IntDef({STATE_QUEUED, STATE_COMPLETED})
public @interface TargetState {} public @interface TargetState {}
private final int id; private final String id;
private final DownloadManager downloadManager; private final DownloadManager downloadManager;
private final DownloaderFactory downloaderFactory; private final DownloaderFactory downloaderFactory;
private final DownloadAction action;
private final int minRetryCount; private final int minRetryCount;
/** The current state of the task. */ private final long startTimeMs;
@TaskState.State private int state; 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. * thread stops.
*/ */
@TargetState private volatile int targetState; @TargetState private volatile int targetState;
@MonotonicNonNull private Downloader downloader; @MonotonicNonNull private Downloader downloader;
@MonotonicNonNull private Thread thread; @MonotonicNonNull private DownloadThread downloadThread;
@MonotonicNonNull private Throwable error; @MonotonicNonNull @DownloadState.FailureReason private int failureReason;
private Task( private Download(
int id,
DownloadManager downloadManager, DownloadManager downloadManager,
DownloaderFactory downloaderFactory, DownloaderFactory downloaderFactory,
DownloadAction action, DownloadAction action,
int minRetryCount) { int minRetryCount) {
this.id = id; this.id = action.id;
this.downloadManager = downloadManager; this.downloadManager = downloadManager;
this.downloaderFactory = downloaderFactory; this.downloaderFactory = downloaderFactory;
this.action = action; this.action = action;
this.minRetryCount = minRetryCount; this.minRetryCount = minRetryCount;
this.startTimeMs = System.currentTimeMillis();
state = STATE_QUEUED; state = STATE_QUEUED;
targetState = STATE_COMPLETED; 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; float downloadPercentage = C.PERCENTAGE_UNSET;
long downloadedBytes = 0; long downloadedBytes = 0;
long totalBytes = C.LENGTH_UNSET;
if (downloader != null) { if (downloader != null) {
downloadPercentage = downloader.getDownloadPercentage(); downloadPercentage = downloader.getDownloadPercentage();
downloadedBytes = downloader.getDownloadedBytes(); 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() { 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() { public boolean isStarted() {
return state == STATE_STARTED; return state == STATE_STARTED;
} }
@ -610,9 +645,9 @@ public final class DownloadManager {
+ ' ' + ' '
+ (action.isRemoveAction ? "remove" : "download") + (action.isRemoveAction ? "remove" : "download")
+ ' ' + ' '
+ TaskState.getStateString(state) + DownloadState.getStateString(state)
+ ' ' + ' '
+ TaskState.getStateString(targetState); + DownloadState.getStateString(targetState);
} }
public boolean canStart() { public boolean canStart() {
@ -622,77 +657,108 @@ public final class DownloadManager {
public void start() { public void start() {
if (state == STATE_QUEUED) { if (state == STATE_QUEUED) {
state = STATE_STARTED; state = STATE_STARTED;
action = actionQueue.peek();
targetState = STATE_COMPLETED; targetState = STATE_COMPLETED;
downloadManager.onTaskStateChange(this);
downloader = downloaderFactory.createDownloader(action); downloader = downloaderFactory.createDownloader(action);
thread = new Thread(this); downloadThread =
thread.start(); new DownloadThread(
} this, downloader, action.isRemoveAction, minRetryCount, downloadManager.handler);
} downloadManager.onDownloadStateChange(this);
public void cancel() {
if (state == STATE_STARTED) {
stopDownloadThread(STATE_CANCELED);
} else if (state == STATE_QUEUED) {
state = STATE_CANCELED;
downloadManager.handler.post(() -> downloadManager.onTaskStateChange(this));
} }
} }
public void stop() { public void stop() {
if (state == STATE_STARTED && targetState == STATE_COMPLETED) { if (state == STATE_STARTED) {
stopDownloadThread(STATE_QUEUED); stopDownloadThread();
} }
} }
// Internal methods running on the main thread. // Internal methods running on the main thread.
private void stopDownloadThread(@TargetState int targetState) { private void stopDownloadThread() {
this.targetState = targetState; this.targetState = DownloadState.STATE_QUEUED;
Assertions.checkNotNull(downloader).cancel(); Assertions.checkNotNull(downloadThread).cancel();
Assertions.checkNotNull(thread).interrupt();
} }
private void onDownloadThreadStopped(@Nullable Throwable finalError) { private void onDownloadThreadStopped(@Nullable Throwable finalError) {
@TaskState.State int finalState = targetState; state = targetState;
if (targetState == STATE_COMPLETED && finalError != null) { failureReason = FAILURE_REASON_NONE;
finalState = STATE_FAILED; if (targetState == STATE_COMPLETED) {
} else { if (finalError != null) {
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; downloadManager.onDownloadStateChange(this);
error = finalError; }
downloadManager.onTaskStateChange(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. // Methods running on download thread.
@Override @Override
public void run() { public void run() {
logd("Task is started", this); logd("Download is started", download);
Throwable error = null; Throwable error = null;
try { try {
if (action.isRemoveAction) { if (remove) {
downloader.remove(); downloader.remove();
} else { } else {
int errorCount = 0; int errorCount = 0;
long errorPosition = C.LENGTH_UNSET; long errorPosition = C.LENGTH_UNSET;
while (targetState == STATE_COMPLETED) { while (!isCanceled) {
try { try {
downloader.download(); downloader.download();
break; break;
} catch (IOException e) { } catch (IOException e) {
if (targetState == STATE_COMPLETED) { if (!isCanceled) {
long downloadedBytes = downloader.getDownloadedBytes(); long downloadedBytes = downloader.getDownloadedBytes();
if (downloadedBytes != errorPosition) { if (downloadedBytes != errorPosition) {
logd("Reset error count. downloadedBytes = " + downloadedBytes, this); logd("Reset error count. downloadedBytes = " + downloadedBytes, download);
errorPosition = downloadedBytes; errorPosition = downloadedBytes;
errorCount = 0; errorCount = 0;
} }
if (++errorCount > minRetryCount) { if (++errorCount > minRetryCount) {
throw e; throw e;
} }
logd("Download error. Retry " + errorCount, this); logd("Download error. Retry " + errorCount, download);
Thread.sleep(getRetryDelayMillis(errorCount)); Thread.sleep(getRetryDelayMillis(errorCount));
} }
} }
@ -702,7 +768,7 @@ public final class DownloadManager {
error = e; error = e;
} }
final Throwable finalError = error; final Throwable finalError = error;
downloadManager.handler.post(() -> onDownloadThreadStopped(finalError)); callbackHandler.post(() -> download.onDownloadThreadStopped(isCanceled ? null : finalError));
} }
private int getRetryDelayMillis(int errorCount) { private int getRetryDelayMillis(int errorCount) {

View File

@ -24,7 +24,7 @@ import android.os.IBinder;
import android.os.Looper; import android.os.Looper;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.StringRes; 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.Requirements;
import com.google.android.exoplayer2.scheduler.RequirementsWatcher; import com.google.android.exoplayer2.scheduler.RequirementsWatcher;
import com.google.android.exoplayer2.scheduler.Scheduler; 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 String TAG = "DownloadService";
private static final boolean DEBUG = false; private static final boolean DEBUG = false;
// Keep the requirements helper for each DownloadService as long as there are tasks (and the // Keep the requirements helper for each DownloadService as long as there are downloads (and the
// process is running). This allows tasks to resume when there's no scheduler. It may also allow // process is running). This allows downloads to resume when there's no scheduler. It may also
// tasks the resume more quickly than when relying on the scheduler alone. // allow downloads the resume more quickly than when relying on the scheduler alone.
private static final HashMap<Class<? extends DownloadService>, RequirementsHelper> private static final HashMap<Class<? extends DownloadService>, RequirementsHelper>
requirementsHelpers = new HashMap<>(); requirementsHelpers = new HashMap<>();
private static final Requirements DEFAULT_REQUIREMENTS = 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 * <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 * {@value #FOREGROUND_NOTIFICATION_ID_NONE}) the service runs in the foreground with {@link
* #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}. In that case {@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 * @param foregroundNotificationId The notification id for the foreground notification, or {@link
* #FOREGROUND_NOTIFICATION_ID_NONE} (value {@value #FOREGROUND_NOTIFICATION_ID_NONE}) * #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 * 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 * @param foregroundNotificationId The notification id for the foreground notification, must not
* be 0. * be 0.
@ -128,7 +128,7 @@ public abstract class DownloadService extends Service {
/** /**
* Creates a DownloadService which will run in the foreground. {@link * 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 * @param foregroundNotificationId The notification id for the foreground notification. Must not
* be 0. * 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>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 * <p>This method is called when there is a download state change and periodically while there are
* active tasks. The periodic update interval can be set using {@link #DownloadService(int, * active downloads. The periodic update interval can be set using {@link #DownloadService(int,
* long)}. * long)}.
* *
* <p>On API level 26 and above, this method may also be called just before the service stops, * <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. * 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. * @return The foreground notification to display.
*/ */
protected Notification getForegroundNotification(TaskState[] taskStates) { protected Notification getForegroundNotification(DownloadState[] downloadStates) {
throw new IllegalStateException( throw new IllegalStateException(
getClass().getName() getClass().getName()
+ " is started in the foreground but getForegroundNotification() is not implemented."); + " 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. // Do nothing.
} }
@ -428,10 +428,11 @@ public abstract class DownloadService extends Service {
} }
@Override @Override
public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) { public void onDownloadStateChanged(
DownloadService.this.onTaskStateChanged(taskState); DownloadManager downloadManager, DownloadState downloadState) {
DownloadService.this.onDownloadStateChanged(downloadState);
if (foregroundNotificationUpdater != null) { if (foregroundNotificationUpdater != null) {
if (taskState.state == TaskState.STATE_STARTED) { if (downloadState.state == DownloadState.STATE_STARTED) {
foregroundNotificationUpdater.startPeriodicUpdates(); foregroundNotificationUpdater.startPeriodicUpdates();
} else { } else {
foregroundNotificationUpdater.update(); foregroundNotificationUpdater.update();
@ -471,8 +472,8 @@ public abstract class DownloadService extends Service {
} }
public void update() { public void update() {
TaskState[] taskStates = downloadManager.getAllTaskStates(); DownloadState[] downloadStates = downloadManager.getAllDownloadStates();
startForeground(notificationId, getForegroundNotification(taskStates)); startForeground(notificationId, getForegroundNotification(downloadStates));
notificationDisplayed = true; notificationDisplayed = true;
if (periodicUpdatesStarted) { if (periodicUpdatesStarted) {
handler.removeCallbacks(this); handler.removeCallbacks(this);

View File

@ -16,22 +16,27 @@
package com.google.android.exoplayer2.offline; package com.google.android.exoplayer2.offline;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.upstream.ParsingLoadable.Parser; import com.google.android.exoplayer2.upstream.ParsingLoadable.Parser;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.List; 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> { public final class FilteringManifestParser<T extends FilterableManifest<T>> implements Parser<T> {
private final Parser<T> parser; private final Parser<? extends T> parser;
private final List<StreamKey> streamKeys; @Nullable private final List<StreamKey> streamKeys;
/** /**
* @param parser A parser for the manifest that will be filtered. * @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. * @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.parser = parser;
this.streamKeys = streamKeys; this.streamKeys = streamKeys;
} }

View File

@ -17,19 +17,35 @@ package com.google.android.exoplayer2.offline;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import java.util.Collections;
import java.util.List;
/** A {@link DownloadHelper} for progressive streams. */ /** A {@link DownloadHelper} for progressive streams. */
public final class ProgressiveDownloadHelper extends DownloadHelper<Void> { public final class ProgressiveDownloadHelper extends DownloadHelper<Void> {
/**
* Creates download helper for progressive streams.
*
* @param uri The stream {@link Uri}.
*/
public ProgressiveDownloadHelper(Uri 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 @Override
@ -43,7 +59,8 @@ public final class ProgressiveDownloadHelper extends DownloadHelper<Void> {
} }
@Override @Override
protected List<StreamKey> toStreamKeys(List<TrackKey> trackKeys) { protected StreamKey toStreamKey(
return Collections.emptyList(); int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) {
return new StreamKey(periodIndex, trackGroupIndex, trackIndexInTrackGroup);
} }
} }

View File

@ -19,8 +19,11 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
/** /**
* Identifies a given track by the index of the containing period, the index of the containing group * A key for a subset of media which can be separately loaded (a "stream").
* within the period, and the index of the track within the group. *
* <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> { public final class StreamKey implements Comparable<StreamKey> {

View File

@ -1,41 +0,0 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.offline;
/**
* Identifies a given track by the index of the containing period, the index of the containing group
* within the period, and the index of the track within the group.
*/
public final class TrackKey {
/** The period index. */
public final int periodIndex;
/** The group index. */
public final int groupIndex;
/** The track index. */
public final int trackIndex;
/**
* @param periodIndex The period index.
* @param groupIndex The group index.
* @param trackIndex The track index.
*/
public TrackKey(int periodIndex, int groupIndex, int trackIndex) {
this.periodIndex = periodIndex;
this.groupIndex = groupIndex;
this.trackIndex = trackIndex;
}
}

View File

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

View File

@ -19,8 +19,11 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.compatqual.NullableType;
/** /**
@ -83,6 +86,22 @@ public interface MediaPeriod extends SequenceableLoader {
*/ */
TrackGroupArray getTrackGroups(); 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. * Performs a track selection.
* *

View File

@ -68,6 +68,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
private static final String ATTR_END = "end"; private static final String ATTR_END = "end";
private static final String ATTR_STYLE = "style"; private static final String ATTR_STYLE = "style";
private static final String ATTR_REGION = "region"; private static final String ATTR_REGION = "region";
private static final String ATTR_IMAGE = "backgroundImage";
private static final Pattern CLOCK_TIME = private static final Pattern CLOCK_TIME =
Pattern.compile("^([0-9][0-9]+):([0-9][0-9]):([0-9][0-9])" 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 FONT_SIZE = Pattern.compile("^(([0-9]*.)?[0-9]+)(px|em|%)$");
private static final Pattern PERCENTAGE_COORDINATES = private static final Pattern PERCENTAGE_COORDINATES =
Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$"); 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 Pattern CELL_RESOLUTION = Pattern.compile("^(\\d+) (\\d+)$");
private static final int DEFAULT_FRAME_RATE = 30; private static final int DEFAULT_FRAME_RATE = 30;
@ -105,6 +108,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
XmlPullParser xmlParser = xmlParserFactory.newPullParser(); XmlPullParser xmlParser = xmlParserFactory.newPullParser();
Map<String, TtmlStyle> globalStyles = new HashMap<>(); Map<String, TtmlStyle> globalStyles = new HashMap<>();
Map<String, TtmlRegion> regionMap = new HashMap<>(); Map<String, TtmlRegion> regionMap = new HashMap<>();
Map<String, String> imageMap = new HashMap<>();
regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion(null)); regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion(null));
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length); ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length);
xmlParser.setInput(inputStream, null); xmlParser.setInput(inputStream, null);
@ -114,6 +118,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
int eventType = xmlParser.getEventType(); int eventType = xmlParser.getEventType();
FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE; FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE;
CellResolution cellResolution = DEFAULT_CELL_RESOLUTION; CellResolution cellResolution = DEFAULT_CELL_RESOLUTION;
TtsExtent ttsExtent = null;
while (eventType != XmlPullParser.END_DOCUMENT) { while (eventType != XmlPullParser.END_DOCUMENT) {
TtmlNode parent = nodeStack.peek(); TtmlNode parent = nodeStack.peek();
if (unsupportedNodeDepth == 0) { if (unsupportedNodeDepth == 0) {
@ -122,12 +127,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
if (TtmlNode.TAG_TT.equals(name)) { if (TtmlNode.TAG_TT.equals(name)) {
frameAndTickRate = parseFrameAndTickRates(xmlParser); frameAndTickRate = parseFrameAndTickRates(xmlParser);
cellResolution = parseCellResolution(xmlParser, DEFAULT_CELL_RESOLUTION); cellResolution = parseCellResolution(xmlParser, DEFAULT_CELL_RESOLUTION);
ttsExtent = parseTtsExtent(xmlParser);
} }
if (!isSupportedTag(name)) { if (!isSupportedTag(name)) {
Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName()); Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName());
unsupportedNodeDepth++; unsupportedNodeDepth++;
} else if (TtmlNode.TAG_HEAD.equals(name)) { } else if (TtmlNode.TAG_HEAD.equals(name)) {
parseHeader(xmlParser, globalStyles, regionMap, cellResolution); parseHeader(xmlParser, globalStyles, cellResolution, ttsExtent, regionMap, imageMap);
} else { } else {
try { try {
TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate); TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate);
@ -145,7 +151,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
parent.addChild(TtmlNode.buildTextNode(xmlParser.getText())); parent.addChild(TtmlNode.buildTextNode(xmlParser.getText()));
} else if (eventType == XmlPullParser.END_TAG) { } else if (eventType == XmlPullParser.END_TAG) {
if (xmlParser.getName().equals(TtmlNode.TAG_TT)) { if (xmlParser.getName().equals(TtmlNode.TAG_TT)) {
ttmlSubtitle = new TtmlSubtitle(nodeStack.peek(), globalStyles, regionMap); ttmlSubtitle = new TtmlSubtitle(nodeStack.peek(), globalStyles, regionMap, imageMap);
} }
nodeStack.pop(); 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( private Map<String, TtmlStyle> parseHeader(
XmlPullParser xmlParser, XmlPullParser xmlParser,
Map<String, TtmlStyle> globalStyles, Map<String, TtmlStyle> globalStyles,
CellResolution cellResolution,
TtsExtent ttsExtent,
Map<String, TtmlRegion> globalRegions, Map<String, TtmlRegion> globalRegions,
CellResolution cellResolution) Map<String, String> imageMap)
throws IOException, XmlPullParserException { throws IOException, XmlPullParserException {
do { do {
xmlParser.next(); xmlParser.next();
@ -246,23 +275,41 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
globalStyles.put(style.getId(), style); globalStyles.put(style.getId(), style);
} }
} else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) { } else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) {
TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser, cellResolution); TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser, cellResolution, ttsExtent);
if (ttmlRegion != null) { if (ttmlRegion != null) {
globalRegions.put(ttmlRegion.id, ttmlRegion); globalRegions.put(ttmlRegion.id, ttmlRegion);
} }
} else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_METADATA)) {
parseMetadata(xmlParser, imageMap);
} }
} while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_HEAD)); } while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_HEAD));
return globalStyles; 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. * Parses a region declaration.
* *
* <p>If the region defines an origin and extent, it is required that they're defined as * <p>Supports both percentage and pixel defined regions. In case of pixel defined regions the
* percentages of the viewport. Region declarations that define origin and extent in other formats * passed {@code ttsExtent} is used as a reference window to convert the pixel values to
* are unsupported, and null is returned. * 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); String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID);
if (regionId == null) { if (regionId == null) {
return null; return null;
@ -270,13 +317,30 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
float position; float position;
float line; float line;
String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN); String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN);
if (regionOrigin != null) { if (regionOrigin != null) {
Matcher originMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin); Matcher originPercentageMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin);
if (originMatcher.matches()) { Matcher originPixelMatcher = PIXEL_COORDINATES.matcher(regionOrigin);
if (originPercentageMatcher.matches()) {
try { try {
position = Float.parseFloat(originMatcher.group(1)) / 100f; position = Float.parseFloat(originPercentageMatcher.group(1)) / 100f;
line = Float.parseFloat(originMatcher.group(2)) / 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) { } catch (NumberFormatException e) {
Log.w(TAG, "Ignoring region with malformed origin: " + regionOrigin); Log.w(TAG, "Ignoring region with malformed origin: " + regionOrigin);
return null; return null;
@ -299,11 +363,27 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
float height; float height;
String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT); String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT);
if (regionExtent != null) { if (regionExtent != null) {
Matcher extentMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent); Matcher extentPercentageMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent);
if (extentMatcher.matches()) { Matcher extentPixelMatcher = PIXEL_COORDINATES.matcher(regionExtent);
if (extentPercentageMatcher.matches()) {
try { try {
width = Float.parseFloat(extentMatcher.group(1)) / 100f; width = Float.parseFloat(extentPercentageMatcher.group(1)) / 100f;
height = Float.parseFloat(extentMatcher.group(2)) / 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) { } catch (NumberFormatException e) {
Log.w(TAG, "Ignoring region with malformed extent: " + regionOrigin); Log.w(TAG, "Ignoring region with malformed extent: " + regionOrigin);
return null; return null;
@ -457,6 +537,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
long startTime = C.TIME_UNSET; long startTime = C.TIME_UNSET;
long endTime = C.TIME_UNSET; long endTime = C.TIME_UNSET;
String regionId = TtmlNode.ANONYMOUS_REGION_ID; String regionId = TtmlNode.ANONYMOUS_REGION_ID;
String imageId = null;
String[] styleIds = null; String[] styleIds = null;
int attributeCount = parser.getAttributeCount(); int attributeCount = parser.getAttributeCount();
TtmlStyle style = parseStyleAttributes(parser, null); TtmlStyle style = parseStyleAttributes(parser, null);
@ -487,6 +568,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
regionId = value; regionId = value;
} }
break; 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: default:
// Do nothing. // Do nothing.
break; break;
@ -509,7 +597,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
endTime = parent.endTimeUs; 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) { 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_LAYOUT)
|| tag.equals(TtmlNode.TAG_REGION) || tag.equals(TtmlNode.TAG_REGION)
|| tag.equals(TtmlNode.TAG_METADATA) || tag.equals(TtmlNode.TAG_METADATA)
|| tag.equals(TtmlNode.TAG_SMPTE_IMAGE) || tag.equals(TtmlNode.TAG_IMAGE)
|| tag.equals(TtmlNode.TAG_SMPTE_DATA) || tag.equals(TtmlNode.TAG_DATA)
|| tag.equals(TtmlNode.TAG_SMPTE_INFORMATION); || tag.equals(TtmlNode.TAG_INFORMATION);
} }
private static void parseFontSize(String expression, TtmlStyle out) throws private static void parseFontSize(String expression, TtmlStyle out) throws
@ -651,4 +740,15 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
this.rows = rows; this.rows = rows;
} }
} }
/** Represents the tts:extent for a TTML file. */
private static final class TtsExtent {
final int width;
final int height;
TtsExtent(int width, int height) {
this.width = width;
this.height = height;
}
}
} }

View File

@ -15,7 +15,12 @@
*/ */
package com.google.android.exoplayer2.text.ttml; 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.text.SpannableStringBuilder;
import android.util.Base64;
import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.util.Assertions; 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_LAYOUT = "layout";
public static final String TAG_REGION = "region"; public static final String TAG_REGION = "region";
public static final String TAG_METADATA = "metadata"; public static final String TAG_METADATA = "metadata";
public static final String TAG_SMPTE_IMAGE = "smpte:image"; public static final String TAG_IMAGE = "image";
public static final String TAG_SMPTE_DATA = "smpte:data"; public static final String TAG_DATA = "data";
public static final String TAG_SMPTE_INFORMATION = "smpte:information"; public static final String TAG_INFORMATION = "information";
public static final String ANONYMOUS_REGION_ID = ""; public static final String ANONYMOUS_REGION_ID = "";
public static final String ATTR_ID = "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 START = "start";
public static final String END = "end"; public static final String END = "end";
public final String tag; @Nullable public final String tag;
public final String text; @Nullable public final String text;
public final boolean isTextNode; public final boolean isTextNode;
public final long startTimeUs; public final long startTimeUs;
public final long endTimeUs; public final long endTimeUs;
public final TtmlStyle style; @Nullable public final TtmlStyle style;
@Nullable private final String[] styleIds;
public final String regionId; public final String regionId;
@Nullable public final String imageId;
private final String[] styleIds;
private final HashMap<String, Integer> nodeStartsByRegion; private final HashMap<String, Integer> nodeStartsByRegion;
private final HashMap<String, Integer> nodeEndsByRegion; private final HashMap<String, Integer> nodeEndsByRegion;
private List<TtmlNode> children; private List<TtmlNode> children;
public static TtmlNode buildTextNode(String text) { public static TtmlNode buildTextNode(String text) {
return new TtmlNode(null, TtmlRenderUtil.applyTextElementSpacePolicy(text), C.TIME_UNSET, return new TtmlNode(
C.TIME_UNSET, null, null, ANONYMOUS_REGION_ID); /* 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, public static TtmlNode buildNode(
TtmlStyle style, String[] styleIds, String regionId) { @Nullable String tag,
return new TtmlNode(tag, null, startTimeUs, endTimeUs, style, styleIds, regionId); 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, private TtmlNode(
TtmlStyle style, String[] styleIds, String regionId) { @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.tag = tag;
this.text = text; this.text = text;
this.imageId = imageId;
this.style = style; this.style = style;
this.styleIds = styleIds; this.styleIds = styleIds;
this.isTextNode = text != null; this.isTextNode = text != null;
@ -151,7 +179,8 @@ import java.util.TreeSet;
private void getEventTimes(TreeSet<Long> out, boolean descendsPNode) { private void getEventTimes(TreeSet<Long> out, boolean descendsPNode) {
boolean isPNode = TAG_P.equals(tag); 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) { if (startTimeUs != C.TIME_UNSET) {
out.add(startTimeUs); out.add(startTimeUs);
} }
@ -171,13 +200,46 @@ import java.util.TreeSet;
return styleIds; return styleIds;
} }
public List<Cue> getCues(long timeUs, Map<String, TtmlStyle> globalStyles, public List<Cue> getCues(
Map<String, TtmlRegion> regionMap) { long timeUs,
TreeMap<String, SpannableStringBuilder> regionOutputs = new TreeMap<>(); Map<String, TtmlStyle> globalStyles,
traverseForText(timeUs, false, regionId, regionOutputs); Map<String, TtmlRegion> regionMap,
traverseForStyle(timeUs, globalStyles, regionOutputs); 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<>(); 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()); TtmlRegion region = regionMap.get(entry.getKey());
cues.add( cues.add(
new Cue( new Cue(
@ -192,9 +254,22 @@ import java.util.TreeSet;
region.textSizeType, region.textSizeType,
region.textSize)); region.textSize));
} }
return cues; 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( private void traverseForText(
long timeUs, long timeUs,
boolean descendsPNode, boolean descendsPNode,

View File

@ -33,11 +33,16 @@ import java.util.Map;
private final long[] eventTimesUs; private final long[] eventTimesUs;
private final Map<String, TtmlStyle> globalStyles; private final Map<String, TtmlStyle> globalStyles;
private final Map<String, TtmlRegion> regionMap; private final Map<String, TtmlRegion> regionMap;
private final Map<String, String> imageMap;
public TtmlSubtitle(TtmlNode root, Map<String, TtmlStyle> globalStyles, public TtmlSubtitle(
Map<String, TtmlRegion> regionMap) { TtmlNode root,
Map<String, TtmlStyle> globalStyles,
Map<String, TtmlRegion> regionMap,
Map<String, String> imageMap) {
this.root = root; this.root = root;
this.regionMap = regionMap; this.regionMap = regionMap;
this.imageMap = imageMap;
this.globalStyles = this.globalStyles =
globalStyles != null ? Collections.unmodifiableMap(globalStyles) : Collections.emptyMap(); globalStyles != null ? Collections.unmodifiableMap(globalStyles) : Collections.emptyMap();
this.eventTimesUs = root.getEventTimesUs(); this.eventTimesUs = root.getEventTimesUs();
@ -66,7 +71,7 @@ import java.util.Map;
@Override @Override
public List<Cue> getCues(long timeUs) { public List<Cue> getCues(long timeUs) {
return root.getCues(timeUs, globalStyles, regionMap); return root.getCues(timeUs, globalStyles, regionMap, imageMap);
} }
@VisibleForTesting @VisibleForTesting

View File

@ -227,8 +227,36 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
} }
@Override @Override
public AdaptiveTrackSelection createTrackSelection( public @NullableType TrackSelection[] createTrackSelections(
TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks) { @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) { if (this.bandwidthMeter != null) {
bandwidthMeter = this.bandwidthMeter; bandwidthMeter = this.bandwidthMeter;
} }
@ -246,34 +274,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
adaptiveTrackSelection.experimental_setTrackBitrateEstimator(trackBitrateEstimator); adaptiveTrackSelection.experimental_setTrackBitrateEstimator(trackBitrateEstimator);
return adaptiveTrackSelection; 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; public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000;

View File

@ -24,12 +24,14 @@ import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; 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.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.PriorityTaskManager; import com.google.android.exoplayer2.util.PriorityTaskManager;
import java.util.List; import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/** /**
* Builder for a {@link TrackSelection.Factory} and {@link LoadControl} that implement buffer size * Builder for a {@link TrackSelection.Factory} and {@link LoadControl} that implement buffer size
@ -273,19 +275,22 @@ public final class BufferSizeAdaptationBuilder {
TrackSelection.Factory trackSelectionFactory = TrackSelection.Factory trackSelectionFactory =
new TrackSelection.Factory() { new TrackSelection.Factory() {
@Override @Override
public TrackSelection createTrackSelection( public @NullableType TrackSelection[] createTrackSelections(
TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks) { @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
return new BufferSizeAdaptiveTrackSelection( return TrackSelectionUtil.createTrackSelectionsForDefinitions(
group, definitions,
tracks, definition ->
bandwidthMeter, new BufferSizeAdaptiveTrackSelection(
minBufferMs, definition.group,
maxBufferMs, definition.tracks,
hysteresisBufferMs, bandwidthMeter,
startUpBandwidthFraction, minBufferMs,
startUpMinBufferForQualityIncreaseMs, maxBufferMs,
dynamicFormatFilter, hysteresisBufferMs,
clock); startUpBandwidthFraction,
startUpMinBufferForQualityIncreaseMs,
dynamicFormatFilter,
clock));
} }
}; };

View File

@ -21,8 +21,8 @@ import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.Assertions;
import java.util.List; import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/** /**
* A {@link TrackSelection} consisting of a single track. * A {@link TrackSelection} consisting of a single track.
@ -56,10 +56,12 @@ public final class FixedTrackSelection extends BaseTrackSelection {
} }
@Override @Override
public FixedTrackSelection createTrackSelection( public @NullableType TrackSelection[] createTrackSelections(
TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks) { @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
Assertions.checkArgument(tracks.length == 1); return TrackSelectionUtil.createTrackSelectionsForDefinitions(
return new FixedTrackSelection(group, tracks[0], reason, data); definitions,
definition ->
new FixedTrackSelection(definition.group, definition.tracks[0], reason, data));
} }
} }

View File

@ -24,6 +24,7 @@ import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.BandwidthMeter;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/** /**
* A {@link TrackSelection} whose selected track is updated randomly. * A {@link TrackSelection} whose selected track is updated randomly.
@ -49,9 +50,11 @@ public final class RandomTrackSelection extends BaseTrackSelection {
} }
@Override @Override
public RandomTrackSelection createTrackSelection( public @NullableType TrackSelection[] createTrackSelections(
TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks) { @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
return new RandomTrackSelection(group, tracks, random); return TrackSelectionUtil.createTrackSelectionsForDefinitions(
definitions,
definition -> new RandomTrackSelection(definition.group, definition.tracks, random));
} }
} }

View File

@ -21,8 +21,8 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; 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.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.Assertions;
import java.util.List; import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.compatqual.NullableType;
@ -61,42 +61,31 @@ public interface TrackSelection {
interface Factory { interface Factory {
/** /**
* Creates a new selection. * @deprecated Implement {@link #createTrackSelections(Definition[], BandwidthMeter)} instead.
* * Calling {@link TrackSelectionUtil#createTrackSelectionsForDefinitions(Definition[],
* @param group The {@link TrackGroup}. Must not be null. * AdaptiveTrackSelectionFactory)} helps to create a single adaptive track selection in the
* @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks. * same way as using this deprecated method.
* @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.
*/ */
TrackSelection createTrackSelection( @Deprecated
TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks); default TrackSelection createTrackSelection(
TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks) {
throw new UnsupportedOperationException();
}
/** /**
* Creates a new selection for each {@link Definition}. * Creates a new selection for each {@link Definition}.
* *
* @param definitions A {@link Definition} array. May include null values. * @param definitions A {@link Definition} array. May include null values.
* @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks. * @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( default @NullableType TrackSelection[] createTrackSelections(
@NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) { @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
TrackSelection[] selections = new TrackSelection[definitions.length]; return TrackSelectionUtil.createTrackSelectionsForDefinitions(
boolean createdAdaptiveTrackSelection = false; definitions,
for (int i = 0; i < definitions.length; i++) { definition -> createTrackSelection(definition.group, bandwidthMeter, definition.tracks));
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;
} }
} }

View File

@ -22,15 +22,59 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.source.chunk.MediaChunkListIterator; import com.google.android.exoplayer2.source.chunk.MediaChunkListIterator;
import com.google.android.exoplayer2.trackselection.TrackSelection.Definition;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Track selection related utility methods. */ /** Track selection related utility methods. */
public final class TrackSelectionUtil { public final class TrackSelectionUtil {
private 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 * Returns average bitrate for chunks in bits per second. Chunks are included in average until
* {@code maxDurationMs} or the first unknown length chunk. * {@code maxDurationMs} or the first unknown length chunk.

View File

@ -108,10 +108,7 @@ public final class DataSpec {
* {@link DataSpec} is not intended to be used in conjunction with a cache. * {@link DataSpec} is not intended to be used in conjunction with a cache.
*/ */
public final @Nullable String key; public final @Nullable String key;
/** /** Request {@link Flags flags}. */
* Request flags. Currently {@link #FLAG_ALLOW_GZIP} and
* {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} are the only supported flags.
*/
public final @Flags int flags; public final @Flags int flags;
/** /**

View File

@ -82,7 +82,7 @@ public interface Cache {
* Releases the cache. This method must be called when the cache is no longer required. The 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. * 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. * 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); 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 * 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. * CachedContent} is added if there isn't one already with the given key.

View File

@ -42,10 +42,6 @@ import java.util.Map;
* A {@link DataSource} that reads and writes a {@link Cache}. Requests are fulfilled from the cache * 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 * when possible. When data is not cached it is requested from an upstream {@link DataSource} and
* written into the cache. * 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 { public final class CacheDataSource implements DataSource {
@ -303,7 +299,7 @@ public final class CacheDataSource implements DataSource {
if (dataSpec.length != C.LENGTH_UNSET || currentRequestIgnoresCache) { if (dataSpec.length != C.LENGTH_UNSET || currentRequestIgnoresCache) {
bytesRemaining = dataSpec.length; bytesRemaining = dataSpec.length;
} else { } else {
bytesRemaining = cache.getContentLength(key); bytesRemaining = ContentMetadata.getContentLength(cache.getContentMetadata(key));
if (bytesRemaining != C.LENGTH_UNSET) { if (bytesRemaining != C.LENGTH_UNSET) {
bytesRemaining -= dataSpec.position; bytesRemaining -= dataSpec.position;
if (bytesRemaining <= 0) { if (bytesRemaining <= 0) {
@ -488,16 +484,12 @@ public final class CacheDataSource implements DataSource {
ContentMetadataMutations mutations = new ContentMetadataMutations(); ContentMetadataMutations mutations = new ContentMetadataMutations();
if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) { if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) {
bytesRemaining = resolvedLength; bytesRemaining = resolvedLength;
ContentMetadataInternal.setContentLength(mutations, readPosition + bytesRemaining); ContentMetadataMutations.setContentLength(mutations, readPosition + bytesRemaining);
} }
if (isReadingFromUpstream()) { if (isReadingFromUpstream()) {
actualUri = currentDataSource.getUri(); actualUri = currentDataSource.getUri();
boolean isRedirected = !uri.equals(actualUri); boolean isRedirected = !uri.equals(actualUri);
if (isRedirected) { ContentMetadataMutations.setRedirectedUri(mutations, isRedirected ? actualUri : null);
ContentMetadataInternal.setRedirectedUri(mutations, actualUri);
} else {
ContentMetadataInternal.removeRedirectedUri(mutations);
}
} }
if (isWritingToCache()) { if (isWritingToCache()) {
cache.applyContentMetadataMutations(key, mutations); cache.applyContentMetadataMutations(key, mutations);
@ -507,14 +499,15 @@ public final class CacheDataSource implements DataSource {
private void setNoBytesRemainingAndMaybeStoreLength() throws IOException { private void setNoBytesRemainingAndMaybeStoreLength() throws IOException {
bytesRemaining = 0; bytesRemaining = 0;
if (isWritingToCache()) { 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) { private static Uri getRedirectedUriOrDefault(Cache cache, String key, Uri defaultUri) {
ContentMetadata contentMetadata = cache.getContentMetadata(key); Uri redirectedUri = ContentMetadata.getRedirectedUri(cache.getContentMetadata(key));
Uri redirectedUri = ContentMetadataInternal.getRedirectedUri(contentMetadata); return redirectedUri != null ? redirectedUri : defaultUri;
return redirectedUri == null ? defaultUri : redirectedUri;
} }
private static boolean isCausedByPositionOutOfRange(IOException e) { private static boolean isCausedByPositionOutOfRange(IOException e) {

View File

@ -84,7 +84,10 @@ public final class CacheUtil {
CachingCounters counters) { CachingCounters counters) {
String key = buildCacheKey(dataSpec, cacheKeyFactory); String key = buildCacheKey(dataSpec, cacheKeyFactory);
long start = dataSpec.absoluteStreamPosition; 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.contentLength = left;
counters.alreadyCachedBytes = 0; counters.alreadyCachedBytes = 0;
counters.newlyCachedBytes = 0; counters.newlyCachedBytes = 0;
@ -188,7 +191,10 @@ public final class CacheUtil {
String key = buildCacheKey(dataSpec, cacheKeyFactory); String key = buildCacheKey(dataSpec, cacheKeyFactory);
long start = dataSpec.absoluteStreamPosition; 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) { while (left != 0) {
throwExceptionIfInterruptedOrCancelled(isCanceled); throwExceptionIfInterruptedOrCancelled(isCanceled);
long blockLength = long blockLength =

View File

@ -55,7 +55,7 @@ import java.util.TreeSet;
if (version < VERSION_METADATA_INTRODUCED) { if (version < VERSION_METADATA_INTRODUCED) {
long length = input.readLong(); long length = input.readLong();
ContentMetadataMutations mutations = new ContentMetadataMutations(); ContentMetadataMutations mutations = new ContentMetadataMutations();
ContentMetadataInternal.setContentLength(mutations, length); ContentMetadataMutations.setContentLength(mutations, length);
cachedContent.applyMetadataMutations(mutations); cachedContent.applyMetadataMutations(mutations);
} else { } else {
cachedContent.metadata = DefaultContentMetadata.readFromStream(input); cachedContent.metadata = DefaultContentMetadata.readFromStream(input);
@ -216,7 +216,7 @@ import java.util.TreeSet;
int result = id; int result = id;
result = 31 * result + key.hashCode(); result = 31 * result + key.hashCode();
if (version < VERSION_METADATA_INTRODUCED) { if (version < VERSION_METADATA_INTRODUCED) {
long length = ContentMetadataInternal.getContentLength(metadata); long length = ContentMetadata.getContentLength(metadata);
result = 31 * result + (int) (length ^ (length >>> 32)); result = 31 * result + (int) (length ^ (length >>> 32));
} else { } else {
result = 31 * result + metadata.hashCode(); result = 31 * result + metadata.hashCode();

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.upstream.cache;
import android.support.annotation.VisibleForTesting; import android.support.annotation.VisibleForTesting;
import android.util.SparseArray; import android.util.SparseArray;
import android.util.SparseBooleanArray;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.AtomicFile; import com.google.android.exoplayer2.util.AtomicFile;
@ -42,6 +43,7 @@ import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException; import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Maintains the index of cached content. */ /** Maintains the index of cached content. */
/* package */ class CachedContentIndex { /* package */ class CachedContentIndex {
@ -53,7 +55,30 @@ import javax.crypto.spec.SecretKeySpec;
private static final int FLAG_ENCRYPTED_INDEX = 1; private static final int FLAG_ENCRYPTED_INDEX = 1;
private final HashMap<String, CachedContent> keyToContent; 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 AtomicFile atomicFile;
private final Cipher cipher; private final Cipher cipher;
private final SecretKeySpec secretKeySpec; private final SecretKeySpec secretKeySpec;
@ -105,6 +130,7 @@ import javax.crypto.spec.SecretKeySpec;
} }
keyToContent = new HashMap<>(); keyToContent = new HashMap<>();
idToKey = new SparseArray<>(); idToKey = new SparseArray<>();
removedIds = new SparseBooleanArray();
atomicFile = new AtomicFile(new File(cacheDir, FILE_NAME)); atomicFile = new AtomicFile(new File(cacheDir, FILE_NAME));
} }
@ -125,6 +151,12 @@ import javax.crypto.spec.SecretKeySpec;
} }
writeFile(); writeFile();
changed = false; 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); CachedContent cachedContent = keyToContent.get(key);
if (cachedContent != null && cachedContent.isEmpty() && !cachedContent.isLocked()) { if (cachedContent != null && cachedContent.isEmpty() && !cachedContent.isLocked()) {
keyToContent.remove(key); keyToContent.remove(key);
idToKey.remove(cachedContent.id);
changed = true; changed = true;
// Keep an entry in idToKey to stop the id from being reused until the index is next stored.
idToKey.put(cachedContent.id, /* value= */ null);
// Track that the entry should be removed from idToKey when the index is next stored.
removedIds.put(cachedContent.id, /* value= */ true);
} }
} }

View File

@ -15,44 +15,73 @@
*/ */
package com.google.android.exoplayer2.upstream.cache; 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. * 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 { 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. * 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. * @param defaultValue Value to return if the metadata doesn't exist.
* @return The metadata value. * @return The metadata value.
*/ */
byte[] get(String name, byte[] defaultValue); @Nullable
byte[] get(String key, @Nullable byte[] defaultValue);
/** /**
* Returns a metadata value. * 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. * @param defaultValue Value to return if the metadata doesn't exist.
* @return The metadata value. * @return The metadata value.
*/ */
String get(String name, String defaultValue); @Nullable
String get(String key, @Nullable String defaultValue);
/** /**
* Returns a metadata value. * 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. * @param defaultValue Value to return if the metadata doesn't exist.
* @return The metadata value. * @return The metadata value.
*/ */
long get(String name, long defaultValue); long get(String key, long defaultValue);
/** Returns whether the metadata is available. */ /** Returns whether the metadata is available. */
boolean contains(String name); boolean contains(String key);
/**
* Returns the value stored under {@link #KEY_CONTENT_LENGTH}, or {@link C#LENGTH_UNSET} if not
* set.
*/
static long getContentLength(ContentMetadata contentMetadata) {
return contentMetadata.get(KEY_CONTENT_LENGTH, C.LENGTH_UNSET);
}
/**
* Returns the value stored under {@link #KEY_REDIRECTED_URI} as a {@link Uri}, or {code null} if
* not set.
*/
@Nullable
static Uri getRedirectedUri(ContentMetadata contentMetadata) {
String redirectedUri = contentMetadata.get(KEY_REDIRECTED_URI, (String) null);
return redirectedUri == null ? null : Uri.parse(redirectedUri);
}
} }

View File

@ -1,66 +0,0 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.upstream.cache;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
/** Helper classes to easily access and modify internal metadata values. */
/* package */ final class ContentMetadataInternal {
private static final String PREFIX = ContentMetadata.INTERNAL_METADATA_NAME_PREFIX;
private static final String METADATA_NAME_REDIRECTED_URI = PREFIX + "redir";
private static final String METADATA_NAME_CONTENT_LENGTH = PREFIX + "len";
/** Returns the content length metadata, or {@link C#LENGTH_UNSET} if not set. */
public static long getContentLength(ContentMetadata contentMetadata) {
return contentMetadata.get(METADATA_NAME_CONTENT_LENGTH, C.LENGTH_UNSET);
}
/** Adds a mutation to set content length metadata value. */
public static void setContentLength(ContentMetadataMutations mutations, long length) {
mutations.set(METADATA_NAME_CONTENT_LENGTH, length);
}
/** Adds a mutation to remove content length metadata value. */
public static void removeContentLength(ContentMetadataMutations mutations) {
mutations.remove(METADATA_NAME_CONTENT_LENGTH);
}
/** Returns the redirected uri metadata, or {@code null} if not set. */
public @Nullable static Uri getRedirectedUri(ContentMetadata contentMetadata) {
String redirectedUri = contentMetadata.get(METADATA_NAME_REDIRECTED_URI, (String) null);
return redirectedUri == null ? null : Uri.parse(redirectedUri);
}
/**
* Adds a mutation to set redirected uri metadata value. Passing {@code null} as {@code uri} isn't
* allowed.
*/
public static void setRedirectedUri(ContentMetadataMutations mutations, Uri uri) {
mutations.set(METADATA_NAME_REDIRECTED_URI, uri.toString());
}
/** Adds a mutation to remove redirected uri metadata value. */
public static void removeRedirectedUri(ContentMetadataMutations mutations) {
mutations.remove(METADATA_NAME_REDIRECTED_URI);
}
private ContentMetadataInternal() {
// Prevent instantiation.
}
}

View File

@ -15,6 +15,9 @@
*/ */
package com.google.android.exoplayer2.upstream.cache; 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 com.google.android.exoplayer2.util.Assertions;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -30,6 +33,36 @@ import java.util.Map.Entry;
*/ */
public class ContentMetadataMutations { 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 Map<String, Object> editedValues;
private final List<String> removedValues; private final List<String> removedValues;
@ -45,7 +78,7 @@ public class ContentMetadataMutations {
* *
* @param name The name of the metadata value. * @param name The name of the metadata value.
* @param value The value to be set. * @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) { public ContentMetadataMutations set(String name, String value) {
return checkAndSet(name, value); return checkAndSet(name, value);
@ -56,7 +89,7 @@ public class ContentMetadataMutations {
* *
* @param name The name of the metadata value. * @param name The name of the metadata value.
* @param value The value to be set. * @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) { public ContentMetadataMutations set(String name, long value) {
return checkAndSet(name, value); return checkAndSet(name, value);
@ -68,7 +101,7 @@ public class ContentMetadataMutations {
* *
* @param name The name of the metadata value. * @param name The name of the metadata value.
* @param value The value to be set. * @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) { public ContentMetadataMutations set(String name, byte[] value) {
return checkAndSet(name, Arrays.copyOf(value, value.length)); return checkAndSet(name, Arrays.copyOf(value, value.length));
@ -78,7 +111,7 @@ public class ContentMetadataMutations {
* Adds a mutation to remove a metadata value. * Adds a mutation to remove a metadata value.
* *
* @param name The name of the 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) { public ContentMetadataMutations remove(String name) {
removedValues.add(name); removedValues.add(name);

View File

@ -64,6 +64,10 @@ public final class DefaultContentMetadata implements ContentMetadata {
private final Map<String, byte[]> metadata; private final Map<String, byte[]> metadata;
public DefaultContentMetadata() {
this(Collections.emptyMap());
}
private DefaultContentMetadata(Map<String, byte[]> metadata) { private DefaultContentMetadata(Map<String, byte[]> metadata) {
this.metadata = Collections.unmodifiableMap(metadata); this.metadata = Collections.unmodifiableMap(metadata);
} }
@ -74,7 +78,7 @@ public final class DefaultContentMetadata implements ContentMetadata {
*/ */
public DefaultContentMetadata copyWithMutationsApplied(ContentMetadataMutations mutations) { public DefaultContentMetadata copyWithMutationsApplied(ContentMetadataMutations mutations) {
Map<String, byte[]> mutatedMetadata = applyMutations(metadata, mutations); Map<String, byte[]> mutatedMetadata = applyMutations(metadata, mutations);
if (isMetadataEqual(mutatedMetadata)) { if (isMetadataEqual(metadata, mutatedMetadata)) {
return this; return this;
} }
return new DefaultContentMetadata(mutatedMetadata); return new DefaultContentMetadata(mutatedMetadata);
@ -97,7 +101,8 @@ public final class DefaultContentMetadata implements ContentMetadata {
} }
@Override @Override
public final byte[] get(String name, byte[] defaultValue) { @Nullable
public final byte[] get(String name, @Nullable byte[] defaultValue) {
if (metadata.containsKey(name)) { if (metadata.containsKey(name)) {
byte[] bytes = metadata.get(name); byte[] bytes = metadata.get(name);
return Arrays.copyOf(bytes, bytes.length); return Arrays.copyOf(bytes, bytes.length);
@ -107,7 +112,8 @@ public final class DefaultContentMetadata implements ContentMetadata {
} }
@Override @Override
public final String get(String name, String defaultValue) { @Nullable
public final String get(String name, @Nullable String defaultValue) {
if (metadata.containsKey(name)) { if (metadata.containsKey(name)) {
byte[] bytes = metadata.get(name); byte[] bytes = metadata.get(name);
return new String(bytes, Charset.forName(C.UTF8_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()) { if (o == null || getClass() != o.getClass()) {
return false; return false;
} }
return isMetadataEqual(((DefaultContentMetadata) o).metadata); return isMetadataEqual(metadata, ((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;
} }
@Override @Override
@ -168,6 +160,20 @@ public final class DefaultContentMetadata implements ContentMetadata {
return hashCode; 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( private static Map<String, byte[]> applyMutations(
Map<String, byte[]> otherMetadata, ContentMetadataMutations mutations) { Map<String, byte[]> otherMetadata, ContentMetadataMutations mutations) {
HashMap<String, byte[]> metadata = new HashMap<>(otherMetadata); HashMap<String, byte[]> metadata = new HashMap<>(otherMetadata);

View File

@ -146,13 +146,16 @@ public final class SimpleCache implements Cache {
} }
@Override @Override
public synchronized void release() throws CacheException { public synchronized void release() {
if (released) { if (released) {
return; return;
} }
listeners.clear(); listeners.clear();
removeStaleSpans();
try { try {
removeStaleSpansAndCachedContents(); index.store();
} catch (CacheException e) {
Log.e(TAG, "Storing index file failed", e);
} finally { } finally {
unlockFolder(cacheDir); unlockFolder(cacheDir);
released = true; released = true;
@ -265,7 +268,7 @@ public final class SimpleCache implements Cache {
if (!cacheDir.exists()) { if (!cacheDir.exists()) {
// For some reason the cache directory doesn't exist. Make a best effort to create it. // For some reason the cache directory doesn't exist. Make a best effort to create it.
cacheDir.mkdirs(); cacheDir.mkdirs();
removeStaleSpansAndCachedContents(); removeStaleSpans();
} }
evictor.onStartFile(this, key, position, maxLength); evictor.onStartFile(this, key, position, maxLength);
return SimpleCacheSpan.getCacheFile( return SimpleCacheSpan.getCacheFile(
@ -290,7 +293,7 @@ public final class SimpleCache implements Cache {
return; return;
} }
// Check if the span conflicts with the set content length // 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) { if (length != C.LENGTH_UNSET) {
Assertions.checkState((span.position + span.length) <= length); Assertions.checkState((span.position + span.length) <= length);
} }
@ -311,9 +314,9 @@ public final class SimpleCache implements Cache {
} }
@Override @Override
public synchronized void removeSpan(CacheSpan span) throws CacheException { public synchronized void removeSpan(CacheSpan span) {
Assertions.checkState(!released); Assertions.checkState(!released);
removeSpan(span, true); removeSpanInternal(span);
} }
@Override @Override
@ -330,18 +333,6 @@ public final class SimpleCache implements Cache {
return cachedContent != null ? cachedContent.getCachedBytesLength(position, length) : -length; 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 @Override
public synchronized void applyContentMetadataMutations( public synchronized void applyContentMetadataMutations(
String key, ContentMetadataMutations mutations) throws CacheException { String key, ContentMetadataMutations mutations) throws CacheException {
@ -379,7 +370,7 @@ public final class SimpleCache implements Cache {
if (span.isCached && !span.file.exists()) { if (span.isCached && !span.file.exists()) {
// The file has been deleted from under us. It's likely that other files will have been // 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. // deleted too, so scan the whole in-memory representation.
removeStaleSpansAndCachedContents(); removeStaleSpans();
continue; continue;
} }
return span; return span;
@ -431,27 +422,21 @@ public final class SimpleCache implements Cache {
notifySpanAdded(span); notifySpanAdded(span);
} }
private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) throws CacheException { private void removeSpanInternal(CacheSpan span) {
CachedContent cachedContent = index.get(span.key); CachedContent cachedContent = index.get(span.key);
if (cachedContent == null || !cachedContent.removeSpan(span)) { if (cachedContent == null || !cachedContent.removeSpan(span)) {
return; return;
} }
totalSpace -= span.length; totalSpace -= span.length;
try { index.maybeRemove(cachedContent.key);
if (removeEmptyCachedContent) { notifySpanRemoved(span);
index.maybeRemove(cachedContent.key);
index.store();
}
} finally {
notifySpanRemoved(span);
}
} }
/** /**
* Scans all of the cached spans in the in-memory representation, removing any for which files no * Scans all of the cached spans in the in-memory representation, removing any for which files no
* longer exist. * longer exist.
*/ */
private void removeStaleSpansAndCachedContents() throws CacheException { private void removeStaleSpans() {
ArrayList<CacheSpan> spansToBeRemoved = new ArrayList<>(); ArrayList<CacheSpan> spansToBeRemoved = new ArrayList<>();
for (CachedContent cachedContent : index.getAll()) { for (CachedContent cachedContent : index.getAll()) {
for (CacheSpan span : cachedContent.getSpans()) { for (CacheSpan span : cachedContent.getSpans()) {
@ -461,11 +446,8 @@ public final class SimpleCache implements Cache {
} }
} }
for (int i = 0; i < spansToBeRemoved.size(); i++) { for (int i = 0; i < spansToBeRemoved.size(); i++) {
// Remove span but not CachedContent to prevent multiple index.store() calls. removeSpanInternal(spansToBeRemoved.get(i));
removeSpan(spansToBeRemoved.get(i), false);
} }
index.removeEmpty();
index.store();
} }
private void notifySpanRemoved(CacheSpan span) { private void notifySpanRemoved(CacheSpan span) {

View File

@ -98,7 +98,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
private final EventDispatcher eventDispatcher; private final EventDispatcher eventDispatcher;
private final long allowedJoiningTimeMs; private final long allowedJoiningTimeMs;
private final int maxDroppedFramesToNotify; private final int maxDroppedFramesToNotify;
private final boolean deviceNeedsAutoFrcWorkaround; private final boolean deviceNeedsNoPostProcessWorkaround;
private final long[] pendingOutputStreamOffsetsUs; private final long[] pendingOutputStreamOffsetsUs;
private final long[] pendingOutputStreamSwitchTimesUs; private final long[] pendingOutputStreamSwitchTimesUs;
@ -226,7 +226,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(this.context); frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(this.context);
eventDispatcher = new EventDispatcher(eventHandler, eventListener); eventDispatcher = new EventDispatcher(eventHandler, eventListener);
deviceNeedsAutoFrcWorkaround = deviceNeedsAutoFrcWorkaround(); deviceNeedsNoPostProcessWorkaround = deviceNeedsNoPostProcessWorkaround();
pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];
pendingOutputStreamSwitchTimesUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; pendingOutputStreamSwitchTimesUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];
outputStreamOffsetUs = C.TIME_UNSET; outputStreamOffsetUs = C.TIME_UNSET;
@ -484,7 +484,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
format, format,
codecMaxValues, codecMaxValues,
codecOperatingRate, codecOperatingRate,
deviceNeedsAutoFrcWorkaround, deviceNeedsNoPostProcessWorkaround,
tunnelingAudioSessionId); tunnelingAudioSessionId);
if (surface == null) { if (surface == null) {
Assertions.checkState(shouldUseDummySurface(codecInfo)); 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 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 * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if
* no codec operating rate should be set. * no codec operating rate should be set.
* @param deviceNeedsAutoFrcWorkaround Whether the device is known to enable frame-rate conversion * @param deviceNeedsNoPostProcessWorkaround Whether the device is known to do post processing by
* logic that negatively impacts ExoPlayer. * default that isn't compatible with ExoPlayer.
* @param tunnelingAudioSessionId The audio session id to use for tunneling, or {@link * @param tunnelingAudioSessionId The audio session id to use for tunneling, or {@link
* C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled. * C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled.
* @return The framework {@link MediaFormat} that should be used to configure the decoder. * @return The framework {@link MediaFormat} that should be used to configure the decoder.
@ -1047,7 +1047,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
Format format, Format format,
CodecMaxValues codecMaxValues, CodecMaxValues codecMaxValues,
float codecOperatingRate, float codecOperatingRate,
boolean deviceNeedsAutoFrcWorkaround, boolean deviceNeedsNoPostProcessWorkaround,
int tunnelingAudioSessionId) { int tunnelingAudioSessionId) {
MediaFormat mediaFormat = new MediaFormat(); MediaFormat mediaFormat = new MediaFormat();
// Set format parameters that should always be set. // Set format parameters that should always be set.
@ -1071,7 +1071,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
mediaFormat.setFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate); mediaFormat.setFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate);
} }
} }
if (deviceNeedsAutoFrcWorkaround) { if (deviceNeedsNoPostProcessWorkaround) {
mediaFormat.setInteger("no-post-process", 1);
mediaFormat.setInteger("auto-frc", 0); mediaFormat.setInteger("auto-frc", 0);
} }
if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { 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 * Returns whether the device is known to do post processing by default that isn't compatible with
* impacts ExoPlayer. * ExoPlayer.
* <p>
* If true is returned then we explicitly disable the feature.
* *
* @return True if the device is known to enable frame-rate conversion logic that negatively * @return Whether the device is known to do post processing by default that isn't compatible with
* impacts ExoPlayer. False otherwise. * ExoPlayer.
*/ */
private static boolean deviceNeedsAutoFrcWorkaround() { private static boolean deviceNeedsNoPostProcessWorkaround() {
// nVidia Shield prior to M tries to adjust the playback rate to better map the frame-rate of // 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 // 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 // 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 // implementation causes ExoPlayer's reported playback position to drift out of sync. Captions
// also lose sync [Internal: b/26453592]. // also lose sync [Internal: b/26453592]. Even after M, the devices may apply post processing
return Util.SDK_INT <= 22 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER); // 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. * incorrectly.
*/ */
protected boolean codecNeedsSetOutputSurfaceWorkaround(String name) { protected boolean codecNeedsSetOutputSurfaceWorkaround(String name) {
if (Util.SDK_INT >= 27 || name.startsWith("OMX.google")) { if (name.startsWith("OMX.google")) {
// Devices running API level 27 or later should also be unaffected. Google OMX decoders are // Google OMX decoders are not known to have this issue on any API level.
// not known to have this issue on any API level.
return false; 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) { synchronized (MediaCodecVideoRenderer.class) {
if (!evaluatedDeviceNeedsSetOutputSurfaceWorkaround) { if (!evaluatedDeviceNeedsSetOutputSurfaceWorkaround) {
switch (Util.DEVICE) { if (Util.SDK_INT <= 27 && "dangal".equals(Util.DEVICE)) {
case "1601": // Dangal is affected on API level 27: https://github.com/google/ExoPlayer/issues/5169.
case "1713": deviceNeedsSetOutputSurfaceWorkaround = true;
case "1714": } else if (Util.SDK_INT >= 27) {
case "A10-70F": // In general, devices running API level 27 or later should be unaffected. Do nothing.
case "A1601": } else {
case "A2016a40": // Enable the workaround on a per-device basis. Works around:
case "A7000-a": // https://github.com/google/ExoPlayer/issues/3236,
case "A7000plus": // https://github.com/google/ExoPlayer/issues/3355,
case "A7010a48": // https://github.com/google/ExoPlayer/issues/3439,
case "A7020a48": // https://github.com/google/ExoPlayer/issues/3724,
case "AquaPowerM": // https://github.com/google/ExoPlayer/issues/3835,
case "ASUS_X00AD_2": // https://github.com/google/ExoPlayer/issues/4006,
case "Aura_Note_2": // https://github.com/google/ExoPlayer/issues/4084,
case "BLACK-1X": // https://github.com/google/ExoPlayer/issues/4104,
case "BRAVIA_ATV2": // https://github.com/google/ExoPlayer/issues/4134,
case "C1": // https://github.com/google/ExoPlayer/issues/4315,
case "ComioS1": // https://github.com/google/ExoPlayer/issues/4419,
case "CP8676_I02": // https://github.com/google/ExoPlayer/issues/4460,
case "CPH1609": // https://github.com/google/ExoPlayer/issues/4468.
case "CPY83_I00": switch (Util.DEVICE) {
case "cv1": case "1601":
case "cv3": case "1713":
case "deb": case "1714":
case "E5643": case "A10-70F":
case "ELUGA_A3_Pro": case "A1601":
case "ELUGA_Note": case "A2016a40":
case "ELUGA_Prim": case "A7000-a":
case "ELUGA_Ray_X": case "A7000plus":
case "EverStar_S": case "A7010a48":
case "F3111": case "A7020a48":
case "F3113": case "AquaPowerM":
case "F3116": case "ASUS_X00AD_2":
case "F3211": case "Aura_Note_2":
case "F3213": case "BLACK-1X":
case "F3215": case "BRAVIA_ATV2":
case "F3311": case "BRAVIA_ATV3_4K":
case "flo": case "C1":
case "GiONEE_CBL7513": case "ComioS1":
case "GiONEE_GBL7319": case "CP8676_I02":
case "GIONEE_GBL7360": case "CPH1609":
case "GIONEE_SWW1609": case "CPY83_I00":
case "GIONEE_SWW1627": case "cv1":
case "GIONEE_SWW1631": case "cv3":
case "GIONEE_WBL5708": case "deb":
case "GIONEE_WBL7365": case "E5643":
case "GIONEE_WBL7519": case "ELUGA_A3_Pro":
case "griffin": case "ELUGA_Note":
case "htc_e56ml_dtul": case "ELUGA_Prim":
case "hwALE-H": case "ELUGA_Ray_X":
case "HWBLN-H": case "EverStar_S":
case "HWCAM-H": case "F3111":
case "HWVNS-H": case "F3113":
case "i9031": case "F3116":
case "iball8735_9806": case "F3211":
case "Infinix-X572": case "F3213":
case "iris60": case "F3215":
case "itel_S41": case "F3311":
case "j2xlteins": case "flo":
case "JGZ": case "fugu":
case "K50a40": case "GiONEE_CBL7513":
case "kate": case "GiONEE_GBL7319":
case "le_x6": case "GIONEE_GBL7360":
case "LS-5017": case "GIONEE_SWW1609":
case "M5c": case "GIONEE_SWW1627":
case "manning": case "GIONEE_SWW1631":
case "marino_f": case "GIONEE_WBL5708":
case "MEIZU_M5": case "GIONEE_WBL7365":
case "mh": case "GIONEE_WBL7519":
case "mido": case "griffin":
case "MX6": case "htc_e56ml_dtul":
case "namath": case "hwALE-H":
case "nicklaus_f": case "HWBLN-H":
case "NX541J": case "HWCAM-H":
case "NX573J": case "HWVNS-H":
case "OnePlus5T": case "i9031":
case "p212": case "iball8735_9806":
case "P681": case "Infinix-X572":
case "P85": case "iris60":
case "panell_d": case "itel_S41":
case "panell_dl": case "j2xlteins":
case "panell_ds": case "JGZ":
case "panell_dt": case "K50a40":
case "PB2-670M": case "kate":
case "PGN528": case "le_x6":
case "PGN610": case "LS-5017":
case "PGN611": case "M5c":
case "Phantom6": case "manning":
case "Pixi4-7_3G": case "marino_f":
case "Pixi5-10_4G": case "MEIZU_M5":
case "PLE": case "mh":
case "PRO7S": case "mido":
case "Q350": case "MX6":
case "Q4260": case "namath":
case "Q427": case "nicklaus_f":
case "Q4310": case "NX541J":
case "Q5": case "NX573J":
case "QM16XE_U": case "OnePlus5T":
case "QX1": case "p212":
case "santoni": case "P681":
case "Slate_Pro": case "P85":
case "SVP-DTV15": case "panell_d":
case "s905x018": case "panell_dl":
case "taido_row": case "panell_ds":
case "TB3-730F": case "panell_dt":
case "TB3-730X": case "PB2-670M":
case "TB3-850F": case "PGN528":
case "TB3-850M": case "PGN610":
case "tcl_eu": case "PGN611":
case "V1": case "Phantom6":
case "V23GB": case "Pixi4-7_3G":
case "V5": case "Pixi5-10_4G":
case "vernee_M5": case "PLE":
case "watson": case "PRO7S":
case "whyred": case "Q350":
case "woods_f": case "Q4260":
case "woods_fn": case "Q427":
case "X3_HK": case "Q4310":
case "XE2X": case "Q5":
case "XT1663": case "QM16XE_U":
case "Z12_PRO": case "QX1":
case "Z80": case "santoni":
deviceNeedsSetOutputSurfaceWorkaround = true; case "Slate_Pro":
break; case "SVP-DTV15":
default: case "s905x018":
// Do nothing. case "taido_row":
break; case "TB3-730F":
} case "TB3-730X":
switch (Util.MODEL) { case "TB3-850F":
case "AFTA": case "TB3-850M":
case "AFTN": case "tcl_eu":
deviceNeedsSetOutputSurfaceWorkaround = true; case "V1":
break; case "V23GB":
default: case "V5":
// Do nothing. case "vernee_M5":
break; 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; evaluatedDeviceNeedsSetOutputSurfaceWorkaround = true;
} }

View File

@ -0,0 +1,26 @@
<tt xmlns="http://www.w3.org/ns/ttml" xmlns:ttm="http://www.w3.org/ns/ttml#metadata" xmlns:tts="http://www.w3.org/ns/ttml#styling" xmlns:smpte="http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt" xml:lang="eng">
<head>
<metadata>
<smpte:image imagetype="PNG" encoding="Base64" xml:id="img_0">
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
</smpte:image>
<smpte:image imagetype="PNG" encoding="Base64" xml:id="img_1">
iVBORw0KGgoAAAANSUhEUgAAAaAAAAAkCAIAAABAAnl0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAIBklEQVR4nO1d65GrPAz1nfkKSAu4FWglqeXWQFohrZhW7o8zaBS/kB+QhE/nx86uA5YsnT1+CHb/3G43o1AoFFfEf592QKFQKI6CCpxCobgsVOAUCsVloQL3SxiGwRizruuht5TiBBM/hJ+Lxs85XAQVuJ/BOI7TNBljlmV5vV4H3XKCVxfGz0Xj5xwuRaXAjeOIb7ygoN05J58QqCuOoh7+PyAu8sZULjK3lOIEE5fBB6ORT1MK105fjcANwzBNk7XWU/1xHB+PhzFmnmehPOEWa63X7pxblsU5d8lZpSOQi2maEK4jZoUTTCjaoWmKokbgrLWQJOdc2I74VnRLd6Gfx+OBWUU1jlAXWMWZ0Bx9FeoFzhOyYRhI9Spmj2VZaJGM/jEdoU/VOMOCoNH4WmiOvg3FApcSstSyTg5ODpwmQOCmaVK6ABqH74fm6KsgFTjUkg0TMt5I7VC39sIzWGI3jOMY5Q05kLIVeiL0TXJZx4c2hO3hj5QOnpeObh9qopon8rTmTVTctbtT2R1U46hTDlekqdHDipHWpanuSkAkcFRLNix8tH+kdnx9PB6QucbC8+v1ggkgLNeSS8YY51xYkeAlcPOeeBQxUkShnqPdUo0l31WIYRhQhHHOPZ/PqJ/c1v1+hxUUbbzL8COndSbyIfUlbh9kojqAYbhCi8iXZyLqMPXj1cRS6aBoUAs8D6+JmpNQa3fI0XuL0pRC9/T1SlOR0RAigeOJ4Y3cM6+9y1Hrsiywa60dhoGvXHBCxy+GRS86dDt95X56zIYAhd1aa0mPotfYoKCcAepcfDh8LNQJL1XzKw2r6Idu0OIiE4dMBKLe9jXRHkCTTatzjv+2cxPmnR4IO/LrBSF8ciJa7o8u5aJPXUiolYKE7fI0pXBE+rqkqdSoB5HAQS+5017+SNRI17o84YEOU0rKfaDTuujGAb55Q4DQcNGkINJAaP0IPeLX8N7orrxY0KfoEz9623wSPs7RVHDmeeZzD7kU3iKJwAkmGgMosUhx82pWQB0n4TZ1S9wouj1Prd1OMmwvSlMKjemLClOmZ3maGjkjErjX6wWT9/vdBCtzSj8C3fcBHAicMYZPs+u6zvNsNsmARnhrXQ6UaBGLYRicc9gq8lDyiNPo6MAFhiisy7JgpGQabNudjbEm5Vnk8s2Fj9QtxdF1XZ/PJzlALlVH4AQT7QGUWAQ/6TeTFl9yNfHAucEjIDwPklArg122F6Upher04UqbLgY2pqmRM2VVVJtY91L+Tnu8kBvCJGbeBYKDO4yLvYUh7Qc97V7X1TvRN9u6Bu3rui7Lgq52F0TkTChq9Cn4RLZMp+eqdiNwgoleAdy16JGe2ruM0SPe7i0Sau2iiO11KEofpQnpozk7WgxsSVM7ZwoEjozt7k9PA7mUp++uY5TIzCj4NZ45osIu4XgWMU3xkIIomKDIVhcGn5CaoiBXB7DIYkfAQywZipIioZYQQrbXofp3BJqVmSxbRt3OmQKBIyHjjZigzGFsiy5kxnGkkfdKOfWQCZZl5WPyx1uO5U95vOnXMCHzevPar4H2AH4EfDnz9+9fzEbyN6Ik1MrgCLbXITUQnsru6WvkzI7ARYPr1XSonW+tu6w7UupGR358Mjkz65n5aheOHVHTnIFGbyb8yKL4HLQE8HzgFMxthT9+AN/4LNQuvoHtuzjHqzrO7AhcWJ82QU3aM790/bME3qTB8w3Oof1+v0NeT0BGdyR6RMsBCqPX4eUFrjGAHwEO8vl5EzJo09XDdnwD278E1ZzZETi31Xf5ZjhcKPLGXsdGtPk1QS3ZGDPPM2fVyccxjcXicKWG3kLhO61ocybaA/gpQObM9hTrdPCrhN/AdgmiO62+qObMjsBR+RkzCSq19Cm2pWgnDepFXL4k9NbA3ePID1lSTxLw+kAL+DEc9ex9E/3x19ErgN8AyA1NRZmnTyTUSuGrzmFTA+le8Y8are5hv8hAD56YYE3Bl299J2T+4Dg/0eMn9HxOa/y14ZWgaZqiT9bQNdHKdNErcjxt3uKXtxcNAc4fuixqNNExgB3hMYfvGzjGcYySfxcSaqVQx/aDmJAaSHQh0t1oNWekr2rZ2IMgaO8yMGvt/X53rPRLi3PeOX3PS7c29iZZKfiTNaEhLI/pGjoZoWsQByF9vRFx+Y4KXwb87YiD9rYdTfQKYBd4KabvQy5hunVbIaj0JEFCLYmHebafwIToQGghctCJSiNnCgRu6foH4EIT3rmpe3/QmTdibMS5Lrue1+tlt2Njr2fafWMWRaBD6/I9CM1L5l3saPdqSqK6bG/s0pl3d6XoZaJXALuAS9W0vZQavdJuTyqEH/HDmRQk1Ep5WMT2o5kQpo+cmee5b3UxY9SUcEYqcKl9qHChkULqdrf9yXLPolewt+w1t2jiU53TbMzbn8+ne38HGFdykaXDF+KQ21D0cAzmpdCHlG/54dAsZ4MHFYsikEJHE10CWDGosJHrDh+mCbQMVJzeX0dP+Ry1LqFWiAq2Z9KUQnv6woVIRc+ZW1o48+d2u2U+BrBYC+Wmy7kJP6QEJIsX/q9quKhH/wlOWORKjSj0J/XHW/g18tWW51vKAZOIan44UZ8rIhBFXxONASy1KPTEbe9LRrlk3nctpjBHRkatKIRsrzPRmL5M7jqmKRyakDMigVMoFIpfhP5fVIVCcVmowCkUistCBU6hUFwWKnAKheKyUIFTKBSXhQqcQqG4LFTgFArFZfEPuuTdBr3uWzgAAAAASUVORK5CYII=
</smpte:image>
</metadata>
<styling>
<style/>
</styling>
<layout>
<region xml:id="region_0" tts:extent="51% 12%" tts:origin="24% 78%"/>
<region xml:id="region_1" tts:extent="57% 6%" tts:origin="21% 85%"/>
<region xml:id="region_2" tts:extent="51% 12%" tts:origin="24% 28%"/>
<region xml:id="region_3" tts:extent="57% 6%" tts:origin="21% 35%"/>
</layout>
</head>
<body>
<div begin="00:00:00.200" end="00:00:03.000" region="region_2" smpte:backgroundImage="#img_0"/>
<div begin="00:00:03.200" end="00:00:06.937" region="region_3" smpte:backgroundImage="#img_1"/>
<div begin="00:00:07.200" end="00:59:03.000" region="region_2" smpte:backgroundImage="#img_0"/>
</body>
</tt>

View File

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

View File

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

View File

@ -0,0 +1,335 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.offline;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.net.Uri;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link DownloadActionUtil} class. */
@RunWith(RobolectricTestRunner.class)
public class DownloadActionUtilTest {
private Uri uri1;
private Uri uri2;
@Before
public void setUp() throws Exception {
uri1 = Uri.parse("http://abc.com/media1");
uri2 = Uri.parse("http://abc.com/media2");
}
@Test
public void mergeActions_ifQueueEmpty_throwsException() {
try {
DownloadActionUtil.mergeActions(toActionQueue());
fail();
} catch (Exception e) {
// Expected.
}
}
@Test
public void mergeActions_ifOneActionInQueue_returnsTheSameAction() {
DownloadAction action = createDownloadAction(uri1);
assertThat(DownloadActionUtil.mergeActions(toActionQueue(action))).isEqualTo(action);
}
@Test
public void mergeActions_ifActionsHaveDifferentType_throwsException() {
DownloadAction downloadAction1 =
DownloadAction.createDownloadAction(
DownloadAction.TYPE_PROGRESSIVE,
uri1,
Collections.emptyList(),
/* customCacheKey= */ null,
/* data= */ null);
DownloadAction downloadAction2 =
DownloadAction.createDownloadAction(
DownloadAction.TYPE_DASH,
uri1,
Collections.emptyList(),
/* customCacheKey= */ null,
/* data= */ null);
ArrayDeque<DownloadAction> actionQueue = toActionQueue(downloadAction1, downloadAction2);
try {
DownloadActionUtil.mergeActions(actionQueue);
fail();
} catch (Exception e) {
// Expected.
}
}
@Test
public void mergeActions_ifActionsHaveDifferentCacheKeys_throwsException() {
DownloadAction downloadAction1 =
DownloadAction.createDownloadAction(
DownloadAction.TYPE_PROGRESSIVE,
uri1,
Collections.emptyList(),
/* customCacheKey= */ "cacheKey1",
/* data= */ null);
DownloadAction downloadAction2 =
DownloadAction.createDownloadAction(
DownloadAction.TYPE_PROGRESSIVE,
uri1,
Collections.emptyList(),
/* customCacheKey= */ "cacheKey2",
/* data= */ null);
ArrayDeque<DownloadAction> actionQueue = toActionQueue(downloadAction1, downloadAction2);
try {
DownloadActionUtil.mergeActions(actionQueue);
fail();
} catch (Exception e) {
// Expected.
}
}
@Test
public void mergeActions_nullCacheKeyAndDifferentUrl_throwsException() {
DownloadAction downloadAction1 =
DownloadAction.createDownloadAction(
DownloadAction.TYPE_PROGRESSIVE,
uri1,
Collections.emptyList(),
/* customCacheKey= */ null,
/* data= */ null);
DownloadAction downloadAction2 =
DownloadAction.createDownloadAction(
DownloadAction.TYPE_PROGRESSIVE,
uri2,
Collections.emptyList(),
/* customCacheKey= */ null,
/* data= */ null);
ArrayDeque<DownloadAction> actionQueue = toActionQueue(downloadAction1, downloadAction2);
try {
DownloadActionUtil.mergeActions(actionQueue);
fail();
} catch (Exception e) {
// Expected.
}
}
@Test
public void mergeActions_sameCacheKeyAndDifferentUrl_latterUrlUsed() {
DownloadAction downloadAction1 =
DownloadAction.createDownloadAction(
DownloadAction.TYPE_PROGRESSIVE,
uri1,
Collections.emptyList(),
/* customCacheKey= */ "cacheKey1",
/* data= */ null);
DownloadAction downloadAction2 =
DownloadAction.createDownloadAction(
DownloadAction.TYPE_PROGRESSIVE,
uri2,
Collections.emptyList(),
/* customCacheKey= */ "cacheKey1",
/* data= */ null);
ArrayDeque<DownloadAction> actionQueue = toActionQueue(downloadAction1, downloadAction2);
DownloadActionUtil.mergeActions(actionQueue);
DownloadAction mergedAction = DownloadActionUtil.mergeActions(actionQueue);
assertThat(mergedAction.uri).isEqualTo(uri2);
}
@Test
public void mergeActions_differentData_latterDataUsed() {
byte[] data1 = "data1".getBytes();
DownloadAction downloadAction1 =
DownloadAction.createDownloadAction(
DownloadAction.TYPE_PROGRESSIVE,
uri1,
Collections.emptyList(),
/* customCacheKey= */ null,
/* data= */ data1);
byte[] data2 = "data2".getBytes();
DownloadAction downloadAction2 =
DownloadAction.createDownloadAction(
DownloadAction.TYPE_PROGRESSIVE,
uri1,
Collections.emptyList(),
/* customCacheKey= */ null,
/* data= */ data2);
ArrayDeque<DownloadAction> actionQueue = toActionQueue(downloadAction1, downloadAction2);
DownloadActionUtil.mergeActions(actionQueue);
DownloadAction mergedAction = DownloadActionUtil.mergeActions(actionQueue);
assertThat(mergedAction.data).isEqualTo(data2);
}
@Test
public void mergeActions_ifRemoveActionLast_returnsRemoveAction() {
DownloadAction downloadAction = createDownloadAction(uri1);
DownloadAction removeAction = createRemoveAction(uri1);
ArrayDeque<DownloadAction> actionQueue = toActionQueue(downloadAction, removeAction);
DownloadAction action = DownloadActionUtil.mergeActions(actionQueue);
assertThat(action).isEqualTo(removeAction);
assertThat(actionQueue).containsExactly(removeAction);
}
@Test
public void mergeActions_downloadActionAfterRemove_returnsRemoveKeepsDownload() {
DownloadAction removeAction = createRemoveAction(uri1);
DownloadAction downloadAction = createDownloadAction(uri1);
ArrayDeque<DownloadAction> actionQueue = toActionQueue(removeAction, downloadAction);
DownloadAction action = DownloadActionUtil.mergeActions(actionQueue);
assertThat(action).isEqualTo(removeAction);
assertThat(actionQueue).containsExactly(removeAction, downloadAction);
}
@Test
public void mergeActions_downloadActionsAfterRemove_returnsRemoveMergesDownloads() {
DownloadAction removeAction = createRemoveAction(uri1);
StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
DownloadAction downloadAction1 =
createDownloadAction(uri1, Collections.singletonList(streamKey1));
StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1);
DownloadAction downloadAction2 =
createDownloadAction(uri1, Collections.singletonList(streamKey2));
ArrayDeque<DownloadAction> actionQueue =
toActionQueue(removeAction, downloadAction1, downloadAction2);
DownloadAction mergedDownloadAction =
createDownloadAction(uri1, Arrays.asList(streamKey1, streamKey2));
DownloadAction action = DownloadActionUtil.mergeActions(actionQueue);
assertThat(action).isEqualTo(removeAction);
assertThat(actionQueue).containsExactly(removeAction, mergedDownloadAction);
}
@Test
public void mergeActions_actionBeforeRemove_ignoresActionBeforeRemove() {
DownloadAction removeAction = createRemoveAction(uri1);
StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
DownloadAction downloadAction1 =
createDownloadAction(uri1, Collections.singletonList(streamKey1));
StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1);
DownloadAction downloadAction2 =
createDownloadAction(uri1, Collections.singletonList(streamKey2));
StreamKey streamKey3 = new StreamKey(/* groupIndex= */ 2, /* trackIndex= */ 2);
DownloadAction downloadAction3 =
createDownloadAction(uri1, Collections.singletonList(streamKey3));
ArrayDeque<DownloadAction> actionQueue =
toActionQueue(downloadAction1, removeAction, downloadAction2, downloadAction3);
DownloadAction mergedDownloadAction =
createDownloadAction(uri1, Arrays.asList(streamKey2, streamKey3));
DownloadAction action = DownloadActionUtil.mergeActions(actionQueue);
assertThat(action).isEqualTo(removeAction);
assertThat(actionQueue).containsExactly(removeAction, mergedDownloadAction);
}
@Test
public void mergeActions_returnsMergedAction() {
StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1);
StreamKey[] keys1 = new StreamKey[] {streamKey1};
StreamKey[] keys2 = new StreamKey[] {streamKey2};
StreamKey[] expectedKeys = new StreamKey[] {streamKey1, streamKey2};
doTestMergeActionsReturnsMergedKeys(keys1, keys2, expectedKeys);
}
@Test
public void mergeActions_returnsUniqueKeys() {
StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
StreamKey streamKey1Copy = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1);
StreamKey[] keys1 = new StreamKey[] {streamKey1};
StreamKey[] keys2 = new StreamKey[] {streamKey2, streamKey1Copy};
StreamKey[] expectedKeys = new StreamKey[] {streamKey1, streamKey2};
doTestMergeActionsReturnsMergedKeys(keys1, keys2, expectedKeys);
}
@Test
public void mergeActions_ifFirstActionKeysEmpty_returnsEmptyKeys() {
StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1);
StreamKey[] keys1 = new StreamKey[] {};
StreamKey[] keys2 = new StreamKey[] {streamKey2, streamKey1};
StreamKey[] expectedKeys = new StreamKey[] {};
doTestMergeActionsReturnsMergedKeys(keys1, keys2, expectedKeys);
}
@Test
public void mergeActions_ifNotFirstActionKeysEmpty_returnsEmptyKeys() {
StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1);
StreamKey[] keys1 = new StreamKey[] {streamKey2, streamKey1};
StreamKey[] keys2 = new StreamKey[] {};
StreamKey[] expectedKeys = new StreamKey[] {};
doTestMergeActionsReturnsMergedKeys(keys1, keys2, expectedKeys);
}
private void doTestMergeActionsReturnsMergedKeys(
StreamKey[] keys1, StreamKey[] keys2, StreamKey[] expectedKeys) {
DownloadAction action1 = createDownloadAction(uri1, Arrays.asList(keys1));
DownloadAction action2 = createDownloadAction(uri1, Arrays.asList(keys2));
ArrayDeque<DownloadAction> actionQueue = toActionQueue(action1, action2);
DownloadAction mergedAction = DownloadActionUtil.mergeActions(actionQueue);
assertThat(mergedAction.type).isEqualTo(action1.type);
assertThat(mergedAction.uri).isEqualTo(action1.uri);
assertThat(mergedAction.customCacheKey).isEqualTo(action1.customCacheKey);
assertThat(mergedAction.isRemoveAction).isEqualTo(action1.isRemoveAction);
assertThat(mergedAction.keys).containsExactly((Object[]) expectedKeys);
assertThat(actionQueue).containsExactly(mergedAction);
}
private ArrayDeque<DownloadAction> toActionQueue(DownloadAction... actions) {
return new ArrayDeque<>(Arrays.asList(actions));
}
private static DownloadAction createDownloadAction(Uri uri) {
return createDownloadAction(uri, Collections.emptyList());
}
private static DownloadAction createDownloadAction(Uri uri, List<StreamKey> keys) {
return DownloadAction.createDownloadAction(
DownloadAction.TYPE_PROGRESSIVE, uri, keys, /* customCacheKey= */ null, /* data= */ null);
}
private static DownloadAction createRemoveAction(Uri uri) {
return DownloadAction.createRemoveAction(
DownloadAction.TYPE_PROGRESSIVE, uri, /* customCacheKey= */ null);
}
}

View File

@ -0,0 +1,445 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.offline;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.offline.DownloadHelper.Callback;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.testutil.FakeRenderer;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.ParametersBuilder;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.util.ConditionVariable;
import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.shadows.ShadowLooper;
/** Unit tests for {@link DownloadHelper}. */
@RunWith(RobolectricTestRunner.class)
public class DownloadHelperTest {
private static final String TEST_DOWNLOAD_TYPE = "downloadType";
private static final String TEST_CACHE_KEY = "cacheKey";
private static final ManifestType TEST_MANIFEST = new ManifestType();
private static final Format VIDEO_FORMAT_LOW = createVideoFormat(/* bitrate= */ 200_000);
private static final Format VIDEO_FORMAT_HIGH = createVideoFormat(/* bitrate= */ 800_000);
private static final Format AUDIO_FORMAT_US = createAudioFormat(/* language= */ "US");
private static final Format AUDIO_FORMAT_ZH = createAudioFormat(/* language= */ "ZH");
private static final Format TEXT_FORMAT_US = createTextFormat(/* language= */ "US");
private static final Format TEXT_FORMAT_ZH = createTextFormat(/* language= */ "ZH");
private static final TrackGroup TRACK_GROUP_VIDEO_BOTH =
new TrackGroup(VIDEO_FORMAT_LOW, VIDEO_FORMAT_HIGH);
private static final TrackGroup TRACK_GROUP_VIDEO_SINGLE = new TrackGroup(VIDEO_FORMAT_LOW);
private static final TrackGroup TRACK_GROUP_AUDIO_US = new TrackGroup(AUDIO_FORMAT_US);
private static final TrackGroup TRACK_GROUP_AUDIO_ZH = new TrackGroup(AUDIO_FORMAT_ZH);
private static final TrackGroup TRACK_GROUP_TEXT_US = new TrackGroup(TEXT_FORMAT_US);
private static final TrackGroup TRACK_GROUP_TEXT_ZH = new TrackGroup(TEXT_FORMAT_ZH);
private static final TrackGroupArray TRACK_GROUP_ARRAY_ALL =
new TrackGroupArray(
TRACK_GROUP_VIDEO_BOTH,
TRACK_GROUP_AUDIO_US,
TRACK_GROUP_AUDIO_ZH,
TRACK_GROUP_TEXT_US,
TRACK_GROUP_TEXT_ZH);
private static final TrackGroupArray TRACK_GROUP_ARRAY_SINGLE =
new TrackGroupArray(TRACK_GROUP_VIDEO_SINGLE, TRACK_GROUP_AUDIO_US);
private static final TrackGroupArray[] TRACK_GROUP_ARRAYS =
new TrackGroupArray[] {TRACK_GROUP_ARRAY_ALL, TRACK_GROUP_ARRAY_SINGLE};
private Uri testUri;
private FakeDownloadHelper downloadHelper;
@Before
public void setUp() {
testUri = Uri.parse("http://test.uri");
FakeRenderer videoRenderer = new FakeRenderer(VIDEO_FORMAT_LOW, VIDEO_FORMAT_HIGH);
FakeRenderer audioRenderer = new FakeRenderer(AUDIO_FORMAT_US, AUDIO_FORMAT_ZH);
FakeRenderer textRenderer = new FakeRenderer(TEXT_FORMAT_US, TEXT_FORMAT_ZH);
RenderersFactory renderersFactory =
(handler, videoListener, audioListener, metadata, text, drm) ->
new Renderer[] {textRenderer, audioRenderer, videoRenderer};
downloadHelper = new FakeDownloadHelper(testUri, renderersFactory);
}
@Test
public void getManifest_returnsManifest() throws Exception {
prepareDownloadHelper(downloadHelper);
ManifestType manifest = downloadHelper.getManifest();
assertThat(manifest).isEqualTo(TEST_MANIFEST);
}
@Test
public void getPeriodCount_returnsPeriodCount() throws Exception {
prepareDownloadHelper(downloadHelper);
int periodCount = downloadHelper.getPeriodCount();
assertThat(periodCount).isEqualTo(2);
}
@Test
public void getTrackGroups_returnsTrackGroups() throws Exception {
prepareDownloadHelper(downloadHelper);
TrackGroupArray trackGroupArrayPeriod0 = downloadHelper.getTrackGroups(/* periodIndex= */ 0);
TrackGroupArray trackGroupArrayPeriod1 = downloadHelper.getTrackGroups(/* periodIndex= */ 1);
assertThat(trackGroupArrayPeriod0).isEqualTo(TRACK_GROUP_ARRAYS[0]);
assertThat(trackGroupArrayPeriod1).isEqualTo(TRACK_GROUP_ARRAYS[1]);
}
@Test
public void getMappedTrackInfo_returnsMappedTrackInfo() throws Exception {
prepareDownloadHelper(downloadHelper);
MappedTrackInfo mappedTracks0 = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
MappedTrackInfo mappedTracks1 = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 1);
assertThat(mappedTracks0.getRendererCount()).isEqualTo(3);
assertThat(mappedTracks0.getRendererType(/* rendererIndex= */ 0)).isEqualTo(C.TRACK_TYPE_TEXT);
assertThat(mappedTracks0.getRendererType(/* rendererIndex= */ 1)).isEqualTo(C.TRACK_TYPE_AUDIO);
assertThat(mappedTracks0.getRendererType(/* rendererIndex= */ 2)).isEqualTo(C.TRACK_TYPE_VIDEO);
assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 0).length).isEqualTo(2);
assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 1).length).isEqualTo(2);
assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 2).length).isEqualTo(1);
assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 0).get(/* index= */ 0))
.isEqualTo(TRACK_GROUP_TEXT_US);
assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 0).get(/* index= */ 1))
.isEqualTo(TRACK_GROUP_TEXT_ZH);
assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 1).get(/* index= */ 0))
.isEqualTo(TRACK_GROUP_AUDIO_US);
assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 1).get(/* index= */ 1))
.isEqualTo(TRACK_GROUP_AUDIO_ZH);
assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 2).get(/* index= */ 0))
.isEqualTo(TRACK_GROUP_VIDEO_BOTH);
assertThat(mappedTracks1.getRendererCount()).isEqualTo(3);
assertThat(mappedTracks1.getRendererType(/* rendererIndex= */ 0)).isEqualTo(C.TRACK_TYPE_TEXT);
assertThat(mappedTracks1.getRendererType(/* rendererIndex= */ 1)).isEqualTo(C.TRACK_TYPE_AUDIO);
assertThat(mappedTracks1.getRendererType(/* rendererIndex= */ 2)).isEqualTo(C.TRACK_TYPE_VIDEO);
assertThat(mappedTracks1.getTrackGroups(/* rendererIndex= */ 0).length).isEqualTo(0);
assertThat(mappedTracks1.getTrackGroups(/* rendererIndex= */ 1).length).isEqualTo(1);
assertThat(mappedTracks1.getTrackGroups(/* rendererIndex= */ 2).length).isEqualTo(1);
assertThat(mappedTracks1.getTrackGroups(/* rendererIndex= */ 1).get(/* index= */ 0))
.isEqualTo(TRACK_GROUP_AUDIO_US);
assertThat(mappedTracks1.getTrackGroups(/* rendererIndex= */ 2).get(/* index= */ 0))
.isEqualTo(TRACK_GROUP_VIDEO_SINGLE);
}
@Test
public void getTrackSelections_returnsInitialSelection() throws Exception {
prepareDownloadHelper(downloadHelper);
List<TrackSelection> selectedText0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);
List<TrackSelection> selectedAudio0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);
List<TrackSelection> selectedVideo0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);
List<TrackSelection> selectedText1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);
List<TrackSelection> selectedAudio1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);
List<TrackSelection> selectedVideo1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2);
assertSingleTrackSelectionEquals(selectedText0, TRACK_GROUP_TEXT_US, 0);
assertSingleTrackSelectionEquals(selectedAudio0, TRACK_GROUP_AUDIO_US, 0);
assertSingleTrackSelectionEquals(selectedVideo0, TRACK_GROUP_VIDEO_BOTH, 1);
assertThat(selectedText1).isEmpty();
assertSingleTrackSelectionEquals(selectedAudio1, TRACK_GROUP_AUDIO_US, 0);
assertSingleTrackSelectionEquals(selectedVideo1, TRACK_GROUP_VIDEO_SINGLE, 0);
}
@Test
public void getTrackSelections_afterClearTrackSelections_isEmpty() throws Exception {
prepareDownloadHelper(downloadHelper);
// Clear only one period selection to verify second period selection is untouched.
downloadHelper.clearTrackSelections(/* periodIndex= */ 0);
List<TrackSelection> selectedText0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);
List<TrackSelection> selectedAudio0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);
List<TrackSelection> selectedVideo0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);
List<TrackSelection> selectedText1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);
List<TrackSelection> selectedAudio1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);
List<TrackSelection> selectedVideo1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2);
assertThat(selectedText0).isEmpty();
assertThat(selectedAudio0).isEmpty();
assertThat(selectedVideo0).isEmpty();
// Verify
assertThat(selectedText1).isEmpty();
assertSingleTrackSelectionEquals(selectedAudio1, TRACK_GROUP_AUDIO_US, 0);
assertSingleTrackSelectionEquals(selectedVideo1, TRACK_GROUP_VIDEO_SINGLE, 0);
}
@Test
public void getTrackSelections_afterReplaceTrackSelections_returnsNewSelections()
throws Exception {
prepareDownloadHelper(downloadHelper);
DefaultTrackSelector.Parameters parameters =
new ParametersBuilder()
.setPreferredAudioLanguage("ZH")
.setPreferredTextLanguage("ZH")
.setRendererDisabled(/* rendererIndex= */ 2, true)
.build();
// Replace only one period selection to verify second period selection is untouched.
downloadHelper.replaceTrackSelections(/* periodIndex= */ 0, parameters);
List<TrackSelection> selectedText0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);
List<TrackSelection> selectedAudio0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);
List<TrackSelection> selectedVideo0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);
List<TrackSelection> selectedText1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);
List<TrackSelection> selectedAudio1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);
List<TrackSelection> selectedVideo1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2);
assertSingleTrackSelectionEquals(selectedText0, TRACK_GROUP_TEXT_ZH, 0);
assertSingleTrackSelectionEquals(selectedAudio0, TRACK_GROUP_AUDIO_ZH, 0);
assertThat(selectedVideo0).isEmpty();
assertThat(selectedText1).isEmpty();
assertSingleTrackSelectionEquals(selectedAudio1, TRACK_GROUP_AUDIO_US, 0);
assertSingleTrackSelectionEquals(selectedVideo1, TRACK_GROUP_VIDEO_SINGLE, 0);
}
@Test
public void getTrackSelections_afterAddTrackSelections_returnsCombinedSelections()
throws Exception {
prepareDownloadHelper(downloadHelper);
// Select parameters to require some merging of track groups because the new parameters add
// all video tracks to initial video single track selection.
DefaultTrackSelector.Parameters parameters =
new ParametersBuilder()
.setPreferredAudioLanguage("ZH")
.setPreferredTextLanguage("US")
.build();
// Add only to one period selection to verify second period selection is untouched.
downloadHelper.addTrackSelection(/* periodIndex= */ 0, parameters);
List<TrackSelection> selectedText0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);
List<TrackSelection> selectedAudio0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);
List<TrackSelection> selectedVideo0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);
List<TrackSelection> selectedText1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);
List<TrackSelection> selectedAudio1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);
List<TrackSelection> selectedVideo1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2);
assertSingleTrackSelectionEquals(selectedText0, TRACK_GROUP_TEXT_US, 0);
assertThat(selectedAudio0).hasSize(2);
assertTrackSelectionEquals(selectedAudio0.get(0), TRACK_GROUP_AUDIO_US, 0);
assertTrackSelectionEquals(selectedAudio0.get(1), TRACK_GROUP_AUDIO_ZH, 0);
assertSingleTrackSelectionEquals(selectedVideo0, TRACK_GROUP_VIDEO_BOTH, 0, 1);
assertThat(selectedText1).isEmpty();
assertSingleTrackSelectionEquals(selectedAudio1, TRACK_GROUP_AUDIO_US, 0);
assertSingleTrackSelectionEquals(selectedVideo1, TRACK_GROUP_VIDEO_SINGLE, 0);
}
@Test
public void getDownloadAction_createsDownloadAction_withAllSelectedTracks() throws Exception {
prepareDownloadHelper(downloadHelper);
// Ensure we have track groups with multiple indices, renderers with multiple track groups and
// also renderers without any track groups.
DefaultTrackSelector.Parameters parameters =
new ParametersBuilder()
.setPreferredAudioLanguage("ZH")
.setPreferredTextLanguage("US")
.build();
downloadHelper.addTrackSelection(/* periodIndex= */ 0, parameters);
byte[] data = new byte[10];
Arrays.fill(data, (byte) 123);
DownloadAction downloadAction = downloadHelper.getDownloadAction(data);
assertThat(downloadAction.type).isEqualTo(TEST_DOWNLOAD_TYPE);
assertThat(downloadAction.uri).isEqualTo(testUri);
assertThat(downloadAction.customCacheKey).isEqualTo(TEST_CACHE_KEY);
assertThat(downloadAction.isRemoveAction).isFalse();
assertThat(downloadAction.data).isEqualTo(data);
assertThat(downloadAction.keys)
.containsExactly(
new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 0, /* trackIndex= */ 0),
new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 0, /* trackIndex= */ 1),
new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 0),
new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 2, /* trackIndex= */ 0),
new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 3, /* trackIndex= */ 0),
new StreamKey(/* periodIndex= */ 1, /* groupIndex= */ 0, /* trackIndex= */ 0),
new StreamKey(/* periodIndex= */ 1, /* groupIndex= */ 1, /* trackIndex= */ 0));
}
@Test
public void getRemoveAction_returnsRemoveAction() {
DownloadAction removeAction = downloadHelper.getRemoveAction();
assertThat(removeAction.type).isEqualTo(TEST_DOWNLOAD_TYPE);
assertThat(removeAction.uri).isEqualTo(testUri);
assertThat(removeAction.customCacheKey).isEqualTo(TEST_CACHE_KEY);
assertThat(removeAction.isRemoveAction).isTrue();
}
private static void prepareDownloadHelper(FakeDownloadHelper downloadHelper) throws Exception {
AtomicReference<Exception> prepareException = new AtomicReference<>(null);
ConditionVariable preparedCondition = new ConditionVariable();
downloadHelper.prepare(
new Callback() {
@Override
public void onPrepared(DownloadHelper<?> helper) {
preparedCondition.open();
}
@Override
public void onPrepareError(DownloadHelper<?> helper, IOException e) {
prepareException.set(e);
preparedCondition.open();
}
});
while (!preparedCondition.block(0)) {
ShadowLooper.runMainLooperToNextTask();
}
if (prepareException.get() != null) {
throw prepareException.get();
}
}
private static Format createVideoFormat(int bitrate) {
return Format.createVideoSampleFormat(
/* id= */ null,
/* sampleMimeType= */ MimeTypes.VIDEO_H264,
/* codecs= */ null,
/* bitrate= */ bitrate,
/* maxInputSize= */ Format.NO_VALUE,
/* width= */ 480,
/* height= */ 360,
/* frameRate= */ Format.NO_VALUE,
/* initializationData= */ null,
/* drmInitData= */ null);
}
private static Format createAudioFormat(String language) {
return Format.createAudioSampleFormat(
/* id= */ null,
/* sampleMimeType= */ MimeTypes.AUDIO_AAC,
/* codecs= */ null,
/* bitrate= */ 48000,
/* maxInputSize= */ Format.NO_VALUE,
/* channelCount= */ 2,
/* sampleRate */ 44100,
/* initializationData= */ null,
/* drmInitData= */ null,
/* selectionFlags= */ C.SELECTION_FLAG_DEFAULT,
/* language= */ language);
}
private static Format createTextFormat(String language) {
return Format.createTextSampleFormat(
/* id= */ null,
/* sampleMimeType= */ MimeTypes.TEXT_VTT,
/* selectionFlags= */ C.SELECTION_FLAG_DEFAULT,
/* language= */ language);
}
private static void assertSingleTrackSelectionEquals(
List<TrackSelection> trackSelectionList, TrackGroup trackGroup, int... tracks) {
assertThat(trackSelectionList).hasSize(1);
assertTrackSelectionEquals(trackSelectionList.get(0), trackGroup, tracks);
}
private static void assertTrackSelectionEquals(
TrackSelection trackSelection, TrackGroup trackGroup, int... tracks) {
assertThat(trackSelection.getTrackGroup()).isEqualTo(trackGroup);
assertThat(trackSelection.length()).isEqualTo(tracks.length);
int[] selectedTracksInGroup = new int[trackSelection.length()];
for (int i = 0; i < trackSelection.length(); i++) {
selectedTracksInGroup[i] = trackSelection.getIndexInTrackGroup(i);
}
Arrays.sort(selectedTracksInGroup);
Arrays.sort(tracks);
assertThat(selectedTracksInGroup).isEqualTo(tracks);
}
private static final class ManifestType {}
private static final class FakeDownloadHelper extends DownloadHelper<ManifestType> {
public FakeDownloadHelper(Uri testUri, RenderersFactory renderersFactory) {
super(
TEST_DOWNLOAD_TYPE,
testUri,
TEST_CACHE_KEY,
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
renderersFactory,
/* drmSessionManager= */ null);
}
@Override
protected ManifestType loadManifest(Uri uri) throws IOException {
return TEST_MANIFEST;
}
@Override
protected TrackGroupArray[] getTrackGroupArrays(ManifestType manifest) {
assertThat(manifest).isEqualTo(TEST_MANIFEST);
return TRACK_GROUP_ARRAYS;
}
@Override
protected StreamKey toStreamKey(
int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) {
return new StreamKey(periodIndex, trackGroupIndex, trackIndexInTrackGroup);
}
}
}

View File

@ -20,16 +20,17 @@ import static org.junit.Assert.fail;
import android.net.Uri; import android.net.Uri;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.offline.DownloadManager.TaskState; import com.google.android.exoplayer2.offline.DownloadManager.DownloadState;
import com.google.android.exoplayer2.offline.DownloadManager.TaskState.State; import com.google.android.exoplayer2.offline.DownloadManager.DownloadState.State;
import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.DummyMainThread;
import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.RobolectricUtil;
import com.google.android.exoplayer2.testutil.TestDownloadManagerListener; import com.google.android.exoplayer2.testutil.TestDownloadManagerListener;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.ArrayList;
import java.util.IdentityHashMap; import java.util.Arrays;
import java.util.HashMap;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.junit.After; import org.junit.After;
@ -52,7 +53,9 @@ public class DownloadManagerTest {
private static final int ASSERT_FALSE_TIME = 1000; private static final int ASSERT_FALSE_TIME = 1000;
/* Maximum retry delay in DownloadManager. */ /* Maximum retry delay in DownloadManager. */
private static final int MAX_RETRY_DELAY = 5000; 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 static final int MIN_RETRY_COUNT = 3;
private Uri uri1; private Uri uri1;
@ -84,309 +87,329 @@ public class DownloadManagerTest {
} }
@Test @Test
public void testDownloadActionRuns() throws Throwable { public void downloadRunner_multipleInstancePerContent_throwsException() {
doTestDownloaderRuns(createDownloadRunner(uri1)); 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 @Test
public void testRemoveActionRuns() throws Throwable { public void downloadRunner_handleActionReturnsDifferentTaskId_throwsException() {
doTestDownloaderRuns(createRemoveRunner(uri1)); 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 @Test
public void testDownloadRetriesThenFails() throws Throwable { public void multipleActionsForTheSameContent_executedOnTheSameTask() {
DownloadRunner downloadRunner = createDownloadRunner(uri1); // Two download actions on first task
downloadRunner.postAction(); new DownloadRunner(uri1).postDownloadAction().postDownloadAction();
FakeDownloader fakeDownloader = downloadRunner.downloader; // One download, one remove actions on second task
fakeDownloader.enableDownloadIOException = true; 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++) { 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 @Test
public void testDownloadNoRetryWhenCanceled() throws Throwable { public void downloadFails_retries() throws Throwable {
DownloadRunner downloadRunner = createDownloadRunner(uri1).ignoreInterrupts(); DownloadRunner runner = new DownloadRunner(uri1);
downloadRunner.downloader.enableDownloadIOException = true; runner.postDownloadAction();
downloadRunner.postAction().assertStarted(); FakeDownloader downloader = runner.getDownloader(0);
DownloadRunner removeRunner = createRemoveRunner(uri1).postAction(); for (int i = 0; i < MIN_RETRY_COUNT; i++) {
downloader.assertStarted(MAX_RETRY_DELAY).fail();
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();
} }
downloadRunner.assertCompleted(); downloader.assertStarted(MAX_RETRY_DELAY).unblock();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); downloader.assertReleased().assertStartCount(MIN_RETRY_COUNT + 1);
runner.getTask().assertCompleted();
downloadManagerListener.blockUntilTasksComplete();
} }
@Test @Test
@SuppressWarnings({"NonAtomicVolatileUpdate", "NonAtomicOperationOnVolatileField"}) public void downloadProgressOnRetry_retryCountResets() throws Throwable {
public void testDownloadRetryCountResetsOnProgress() throws Throwable { DownloadRunner runner = new DownloadRunner(uri1);
DownloadRunner downloadRunner = createDownloadRunner(uri1); runner.postDownloadAction();
downloadRunner.postAction(); FakeDownloader downloader = runner.getDownloader(0);
FakeDownloader fakeDownloader = downloadRunner.downloader;
fakeDownloader.enableDownloadIOException = true; int tooManyRetries = MIN_RETRY_COUNT + 10;
fakeDownloader.downloadedBytes = 0; for (int i = 0; i < tooManyRetries; i++) {
for (int i = 0; i <= MIN_RETRY_COUNT + 10; i++) { downloader.increaseDownloadedByteCount();
fakeDownloader.assertStarted(MAX_RETRY_DELAY); downloader.assertStarted(MAX_RETRY_DELAY).fail();
fakeDownloader.downloadedBytes++;
if (i == MIN_RETRY_COUNT + 10) {
fakeDownloader.enableDownloadIOException = false;
}
fakeDownloader.unblock();
} }
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(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
@Test @Test
public void testDifferentMediaDownloadActionsStartInParallel() throws Throwable { public void downloadNotCancelRemove() throws Throwable {
doTestDownloadersRunInParallel(createDownloadRunner(uri1), createDownloadRunner(uri2)); DownloadRunner runner = new DownloadRunner(uri1);
} FakeDownloader downloader1 = runner.getDownloader(0);
@Test runner.postRemoveAction();
public void testDifferentMediaDifferentActionsStartInParallel() throws Throwable { downloader1.assertStarted();
doTestDownloadersRunInParallel(createDownloadRunner(uri1), createRemoveRunner(uri2)); runner.postDownloadAction();
}
@Test downloader1.unblock().assertNotCanceled();
public void testSameMediaDownloadActionsStartInParallel() throws Throwable { runner.getDownloader(1).unblock().assertNotCanceled();
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();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
@Test @Test
public void testMultipleRemoveActionWaitsLastCancelsAllOther() throws Throwable { public void secondSameRemoveActionIgnored() throws Throwable {
DownloadRunner removeAction1 = createRemoveRunner(uri1).ignoreInterrupts(); DownloadRunner runner = new DownloadRunner(uri1);
DownloadRunner removeAction2 = createRemoveRunner(uri1); FakeDownloader downloader1 = runner.getDownloader(0);
DownloadRunner removeAction3 = createRemoveRunner(uri1);
removeAction1.postAction().assertStarted(); runner.postRemoveAction();
removeAction2.postAction().assertDoesNotStart(); downloader1.assertStarted();
removeAction3.postAction().assertDoesNotStart(); runner.postRemoveAction();
removeAction2.assertCanceled();
removeAction1.unblock().assertCanceled();
removeAction3.assertStarted().unblock().assertCompleted();
downloader1.unblock().assertNotCanceled();
runner.getTask().assertCompleted();
runner.assertCreatedDownloaderCount(1);
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
@Test @Test
public void testGetTasks() throws Throwable { public void secondSameDownloadActionIgnored() throws Throwable {
DownloadRunner removeAction = createRemoveRunner(uri1); DownloadRunner runner = new DownloadRunner(uri1);
DownloadRunner downloadAction1 = createDownloadRunner(uri1); FakeDownloader downloader1 = runner.getDownloader(0);
DownloadRunner downloadAction2 = createDownloadRunner(uri1);
removeAction.postAction().assertStarted(); runner.postDownloadAction();
downloadAction1.postAction().assertDoesNotStart(); downloader1.assertStarted();
downloadAction2.postAction().assertDoesNotStart(); 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).hasLength(3);
assertThat(states[0].action).isEqualTo(removeAction.action); String[] taskIds = {task1.taskId, task2.taskId, task3.taskId};
assertThat(states[1].action).isEqualTo(downloadAction1.action); String[] stateTaskIds = {states[0].id, states[1].id, states[2].id};
assertThat(states[2].action).isEqualTo(downloadAction2.action); assertThat(stateTaskIds).isEqualTo(taskIds);
} }
@Test @Test
public void testMultipleWaitingDownloadActionStartsInParallel() throws Throwable { public void stopAndResume() throws Throwable {
DownloadRunner removeAction = createRemoveRunner(uri1); DownloadRunner runner1 = new DownloadRunner(uri1);
DownloadRunner downloadAction1 = createDownloadRunner(uri1); DownloadRunner runner2 = new DownloadRunner(uri2);
DownloadRunner downloadAction2 = createDownloadRunner(uri1); DownloadRunner runner3 = new DownloadRunner(uri3);
removeAction.postAction().assertStarted(); runner1.postDownloadAction().getTask().assertStarted();
downloadAction1.postAction().assertDoesNotStart(); runner2.postRemoveAction().getTask().assertStarted();
downloadAction2.postAction().assertDoesNotStart(); runner2.postDownloadAction();
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();
runOnMainThread(() -> downloadManager.stopDownloads()); runOnMainThread(() -> downloadManager.stopDownloads());
download1Runner.assertStopped(); runner1.getTask().assertQueued();
// remove actions aren't stopped. // remove actions aren't stopped.
remove2Runner.unblock().assertCompleted(); runner2.getDownloader(0).unblock().assertReleased();
runner2.getTask().assertQueued();
// Although remove2 is finished, download2 doesn't start. // 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. // When a new remove action is added, it cancels stopped download actions with the same media.
remove1Runner.postAction(); runner1.postRemoveAction();
download1Runner.assertCanceled(); runner1.getDownloader(1).assertStarted().unblock();
remove1Runner.assertStarted().unblock().assertCompleted(); runner1.getTask().assertCompleted();
// New download actions can be added but they don't start. // New download actions can be added but they don't start.
download3Runner.postAction().assertDoesNotStart(); runner3.postDownloadAction().getDownloader(0).assertDoesNotStart();
runOnMainThread(() -> downloadManager.startDownloads()); runOnMainThread(() -> downloadManager.startDownloads());
download2Runner.assertStarted().unblock().assertCompleted(); runner2.getDownloader(1).assertStarted().unblock();
download3Runner.assertStarted().unblock().assertCompleted(); runner3.getDownloader(0).assertStarted().unblock();
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();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); 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) { private void runOnMainThread(final Runnable r) {
dummyMainThread.runOnMainThread(r); dummyMainThread.runOnMainThread(r);
} }
private class DownloadRunner { private final class DownloadRunner {
public final DownloadAction action; private final Uri uri;
public final FakeDownloader downloader; private final ArrayList<FakeDownloader> downloaders;
private int createdDownloaderCount = 0;
private FakeDownloader downloader;
private TaskWrapper taskWrapper;
private DownloadRunner(Uri uri, boolean isRemoveAction) { private DownloadRunner(Uri uri) {
action = this.uri = uri;
isRemoveAction downloaders = new ArrayList<>();
? DownloadAction.createRemoveAction( downloader = addDownloader();
DownloadAction.TYPE_PROGRESSIVE, uri, /* customCacheKey= */ null) downloaderFactory.registerDownloadRunner(this);
: DownloadAction.createDownloadAction(
DownloadAction.TYPE_PROGRESSIVE,
uri,
/* keys= */ Collections.emptyList(),
/* customCacheKey= */ null,
/* data= */ null);
downloader = new FakeDownloader(isRemoveAction);
downloaderFactory.putFakeDownloader(action, downloader);
} }
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)); runOnMainThread(() -> downloadManager.handleAction(action));
if (taskWrapper == null) {
taskWrapper = new TaskWrapper(action.id);
} else {
assertThat(action.id).isEqualTo(taskWrapper.taskId);
}
return this; return this;
} }
private DownloadRunner assertDoesNotStart() throws InterruptedException { private synchronized FakeDownloader addDownloader() {
Thread.sleep(ASSERT_FALSE_TIME); FakeDownloader fakeDownloader = new FakeDownloader();
assertThat(downloader.started.getCount()).isEqualTo(1); downloaders.add(fakeDownloader);
return this; return fakeDownloader;
} }
private DownloadRunner assertStarted() throws InterruptedException { private synchronized FakeDownloader getDownloader(int index) {
downloader.assertStarted(ASSERT_TRUE_TIMEOUT); while (downloaders.size() <= index) {
return assertState(TaskState.STATE_STARTED); addDownloader();
}
return downloaders.get(index);
} }
private DownloadRunner assertCompleted() { private synchronized Downloader createDownloader(DownloadAction action) {
return assertState(TaskState.STATE_COMPLETED); downloader = getDownloader(createdDownloaderCount++);
downloader.action = action;
return downloader;
} }
private DownloadRunner assertFailed() { private TaskWrapper getTask() {
return assertState(TaskState.STATE_FAILED); return taskWrapper;
} }
private DownloadRunner assertCanceled() { public void setTask(TaskWrapper taskWrapper) {
return assertState(TaskState.STATE_CANCELED); this.taskWrapper = taskWrapper;
} }
private DownloadRunner assertStopped() { private void assertCreatedDownloaderCount(int count) {
return assertState(TaskState.STATE_QUEUED); 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) { while (true) {
Integer state = null; Integer state = null;
try { try {
state = downloadManagerListener.pollStateChange(action, ASSERT_TRUE_TIMEOUT); state = downloadManagerListener.pollStateChange(taskId, ASSERT_TRUE_TIMEOUT);
} catch (InterruptedException e) { } catch (InterruptedException e) {
fail(e.getMessage()); fail(e.getMessage());
} }
@ -523,69 +548,98 @@ public class DownloadManagerTest {
} }
} }
private DownloadRunner unblock() { @Override
downloader.unblock(); public boolean equals(Object o) {
return this; if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
return taskId.equals(((TaskWrapper) o).taskId);
} }
private DownloadRunner ignoreInterrupts() { @Override
downloader.ignoreInterrupts = true; public int hashCode() {
return this; 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() { public FakeDownloaderFactory() {
downloaders = new IdentityHashMap<>(); downloaders = new HashMap<>();
} }
public void putFakeDownloader(DownloadAction action, FakeDownloader downloader) { public void registerDownloadRunner(DownloadRunner downloadRunner) {
downloaders.put(action, downloader); assertThat(downloaders.put(downloadRunner.uri, downloadRunner)).isNull();
} }
@Override @Override
public Downloader createDownloader(DownloadAction action) { 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 com.google.android.exoplayer2.util.ConditionVariable blocker;
private final boolean isRemove;
private DownloadAction action;
private CountDownLatch started; private CountDownLatch started;
private boolean ignoreInterrupts; private volatile boolean interrupted;
private volatile boolean cancelled;
private volatile boolean enableDownloadIOException; private volatile boolean enableDownloadIOException;
private volatile int downloadedBytes = C.LENGTH_UNSET; private volatile int downloadedBytes;
private volatile int startCount;
private FakeDownloader(boolean isRemove) { private FakeDownloader() {
this.isRemove = isRemove;
this.started = new CountDownLatch(1); this.started = new CountDownLatch(1);
this.blocker = new com.google.android.exoplayer2.util.ConditionVariable(); this.blocker = new com.google.android.exoplayer2.util.ConditionVariable();
downloadedBytes = C.LENGTH_UNSET;
} }
@SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"})
@Override @Override
public void download() throws InterruptedException, IOException { 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(); started.countDown();
block(); block();
if (enableDownloadIOException) { if (enableDownloadIOException) {
enableDownloadIOException = false;
throw new IOException(); throw new IOException();
} }
} }
@Override @Override
public void cancel() { public void cancel() {
// Do nothing. cancelled = true;
} }
@SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"})
@Override @Override
public void remove() throws InterruptedException { 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(); started.countDown();
block(); block();
} }
@ -597,9 +651,8 @@ public class DownloadManagerTest {
blocker.block(); blocker.block();
break; break;
} catch (InterruptedException e) { } catch (InterruptedException e) {
if (!ignoreInterrupts) { interrupted = true;
throw e; throw e;
}
} }
} }
} finally { } 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 { private FakeDownloader assertStarted(int timeout) throws InterruptedException {
assertThat(started.await(timeout, TimeUnit.MILLISECONDS)).isTrue(); assertThat(started.await(timeout, TimeUnit.MILLISECONDS)).isTrue();
started = new CountDownLatch(1); started = new CountDownLatch(1);
return this; 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() { private FakeDownloader unblock() {
blocker.open(); blocker.open();
return this; return this;
} }
private FakeDownloader fail() {
enableDownloadIOException = true;
return unblock();
}
@Override @Override
public long getDownloadedBytes() { public long getDownloadedBytes() {
return downloadedBytes; return downloadedBytes;
@ -632,5 +724,15 @@ public class DownloadManagerTest {
public float getDownloadPercentage() { public float getDownloadPercentage() {
return C.PERCENTAGE_UNSET; return C.PERCENTAGE_UNSET;
} }
private void assertDoesNotStart() throws InterruptedException {
Thread.sleep(ASSERT_FALSE_TIME);
assertThat(started.getCount()).isEqualTo(1);
}
@SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"})
private void increaseDownloadedByteCount() {
downloadedBytes++;
}
} }
} }

View File

@ -63,6 +63,9 @@ public final class TtmlDecoderTest {
private static final String FONT_SIZE_INVALID_TTML_FILE = "ttml/font_size_invalid.xml"; private static final String FONT_SIZE_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 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 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 @Test
public void testInlineAttributes() throws IOException, SubtitleDecoderException { public void testInlineAttributes() throws IOException, SubtitleDecoderException {
@ -259,56 +262,56 @@ public final class TtmlDecoderTest {
@Test @Test
public void testMultipleRegions() throws IOException, SubtitleDecoderException { public void testMultipleRegions() throws IOException, SubtitleDecoderException {
TtmlSubtitle subtitle = getSubtitle(MULTIPLE_REGIONS_TTML_FILE); TtmlSubtitle subtitle = getSubtitle(MULTIPLE_REGIONS_TTML_FILE);
List<Cue> output = subtitle.getCues(1000000); List<Cue> cues = subtitle.getCues(1000000);
assertThat(output).hasSize(2); assertThat(cues).hasSize(2);
Cue ttmlCue = output.get(0); Cue cue = cues.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("lorem"); assertThat(cue.text.toString()).isEqualTo("lorem");
assertThat(ttmlCue.position).isEqualTo(10f / 100f); assertThat(cue.position).isEqualTo(10f / 100f);
assertThat(ttmlCue.line).isEqualTo(10f / 100f); assertThat(cue.line).isEqualTo(10f / 100f);
assertThat(ttmlCue.size).isEqualTo(20f / 100f); assertThat(cue.size).isEqualTo(20f / 100f);
ttmlCue = output.get(1); cue = cues.get(1);
assertThat(ttmlCue.text.toString()).isEqualTo("amet"); assertThat(cue.text.toString()).isEqualTo("amet");
assertThat(ttmlCue.position).isEqualTo(60f / 100f); assertThat(cue.position).isEqualTo(60f / 100f);
assertThat(ttmlCue.line).isEqualTo(10f / 100f); assertThat(cue.line).isEqualTo(10f / 100f);
assertThat(ttmlCue.size).isEqualTo(20f / 100f); assertThat(cue.size).isEqualTo(20f / 100f);
output = subtitle.getCues(5000000); cues = subtitle.getCues(5000000);
assertThat(output).hasSize(1); assertThat(cues).hasSize(1);
ttmlCue = output.get(0); cue = cues.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("ipsum"); assertThat(cue.text.toString()).isEqualTo("ipsum");
assertThat(ttmlCue.position).isEqualTo(40f / 100f); assertThat(cue.position).isEqualTo(40f / 100f);
assertThat(ttmlCue.line).isEqualTo(40f / 100f); assertThat(cue.line).isEqualTo(40f / 100f);
assertThat(ttmlCue.size).isEqualTo(20f / 100f); assertThat(cue.size).isEqualTo(20f / 100f);
output = subtitle.getCues(9000000); cues = subtitle.getCues(9000000);
assertThat(output).hasSize(1); assertThat(cues).hasSize(1);
ttmlCue = output.get(0); cue = cues.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("dolor"); assertThat(cue.text.toString()).isEqualTo("dolor");
assertThat(ttmlCue.position).isEqualTo(Cue.DIMEN_UNSET); assertThat(cue.position).isEqualTo(Cue.DIMEN_UNSET);
assertThat(ttmlCue.line).isEqualTo(Cue.DIMEN_UNSET); assertThat(cue.line).isEqualTo(Cue.DIMEN_UNSET);
assertThat(ttmlCue.size).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. // TODO: Should be as below, once https://github.com/google/ExoPlayer/issues/2953 is fixed.
// assertEquals(10f / 100f, ttmlCue.position); // assertEquals(10f / 100f, cue.position);
// assertEquals(80f / 100f, ttmlCue.line); // assertEquals(80f / 100f, cue.line);
// assertEquals(1f, ttmlCue.size); // assertEquals(1f, cue.size);
output = subtitle.getCues(21000000); cues = subtitle.getCues(21000000);
assertThat(output).hasSize(1); assertThat(cues).hasSize(1);
ttmlCue = output.get(0); cue = cues.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("She first said this"); assertThat(cue.text.toString()).isEqualTo("She first said this");
assertThat(ttmlCue.position).isEqualTo(45f / 100f); assertThat(cue.position).isEqualTo(45f / 100f);
assertThat(ttmlCue.line).isEqualTo(45f / 100f); assertThat(cue.line).isEqualTo(45f / 100f);
assertThat(ttmlCue.size).isEqualTo(35f / 100f); assertThat(cue.size).isEqualTo(35f / 100f);
output = subtitle.getCues(25000000); cues = subtitle.getCues(25000000);
ttmlCue = output.get(0); cue = cues.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("She first said this\nThen this"); assertThat(cue.text.toString()).isEqualTo("She first said this\nThen this");
output = subtitle.getCues(29000000); cues = subtitle.getCues(29000000);
assertThat(output).hasSize(1); assertThat(cues).hasSize(1);
ttmlCue = output.get(0); cue = cues.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("She first said this\nThen this\nFinally this"); assertThat(cue.text.toString()).isEqualTo("She first said this\nThen this\nFinally this");
assertThat(ttmlCue.position).isEqualTo(45f / 100f); assertThat(cue.position).isEqualTo(45f / 100f);
assertThat(ttmlCue.line).isEqualTo(45f / 100f); assertThat(cue.line).isEqualTo(45f / 100f);
} }
@Test @Test
@ -499,6 +502,91 @@ public final class TtmlDecoderTest {
assertThat((double) subtitle.getEventTime(3)).isWithin(2000).of(2_002_000_000); 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( private void assertSpans(
TtmlSubtitle subtitle, TtmlSubtitle subtitle,
int second, int second,

View File

@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.testutil.FakeClock; import com.google.android.exoplayer2.testutil.FakeClock;
import com.google.android.exoplayer2.testutil.FakeMediaChunk; 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.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.util.ArrayList; import java.util.ArrayList;
@ -66,15 +67,20 @@ public final class AdaptiveTrackSelectionTest {
} }
@Test @Test
@SuppressWarnings("deprecation")
public void testFactoryUsesInitiallyProvidedBandwidthMeter() { public void testFactoryUsesInitiallyProvidedBandwidthMeter() {
BandwidthMeter initialBandwidthMeter = mock(BandwidthMeter.class); BandwidthMeter initialBandwidthMeter = mock(BandwidthMeter.class);
BandwidthMeter injectedBandwidthMeter = mock(BandwidthMeter.class); BandwidthMeter injectedBandwidthMeter = mock(BandwidthMeter.class);
Format format = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);
@SuppressWarnings("deprecation") Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);
AdaptiveTrackSelection adaptiveTrackSelection = TrackSelection[] trackSelections =
new AdaptiveTrackSelection.Factory(initialBandwidthMeter) new AdaptiveTrackSelection.Factory(initialBandwidthMeter)
.createTrackSelection(new TrackGroup(format), injectedBandwidthMeter, /* tracks= */ 0); .createTrackSelections(
adaptiveTrackSelection.updateSelectedTrack( new Definition[] {
new Definition(new TrackGroup(format1, format2), /* tracks= */ 0, 1)
},
injectedBandwidthMeter);
trackSelections[0].updateSelectedTrack(
/* playbackPositionUs= */ 0, /* playbackPositionUs= */ 0,
/* bufferedDurationUs= */ 0, /* bufferedDurationUs= */ 0,
/* availableDurationUs= */ C.TIME_UNSET, /* availableDurationUs= */ C.TIME_UNSET,

View File

@ -247,7 +247,8 @@ public final class CacheDataSourceTest {
// Read partial at EOS but don't cross it so length is unknown. // Read partial at EOS but don't cross it so length is unknown.
CacheDataSource cacheDataSource = createCacheDataSource(false, true); CacheDataSource cacheDataSource = createCacheDataSource(false, true);
assertReadData(cacheDataSource, dataSpec, 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. // 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. // 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(); cacheDataSource.close();
assertThat(upstream.getAndClearOpenedDataSpecs()).hasLength(1); assertThat(upstream.getAndClearOpenedDataSpecs()).hasLength(1);
assertThat(cache.getContentLength(defaultCacheKey)).isEqualTo(TEST_DATA.length); assertThat(ContentMetadata.getContentLength(cache.getContentMetadata(defaultCacheKey)))
.isEqualTo(TEST_DATA.length);
} }
@Test @Test
@ -467,11 +469,7 @@ public final class CacheDataSourceTest {
NavigableSet<CacheSpan> cachedSpans = cache.getCachedSpans(defaultCacheKey); NavigableSet<CacheSpan> cachedSpans = cache.getCachedSpans(defaultCacheKey);
for (CacheSpan cachedSpan : cachedSpans) { for (CacheSpan cachedSpan : cachedSpans) {
if (cachedSpan.position >= halfDataLength) { if (cachedSpan.position >= halfDataLength) {
try { cache.removeSpan(cachedSpan);
cache.removeSpan(cachedSpan);
} catch (Cache.CacheException e) {
// do nothing
}
} }
} }
@ -516,7 +514,9 @@ public final class CacheDataSourceTest {
// If the request was unbounded then the content length should be cached, either because the // 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 // content length was known or because EOS was read. If the request was bounded then the content
// length will not have been determined. // 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); .isEqualTo(dataSpec.length == C.LENGTH_UNSET ? TEST_DATA.length : C.LENGTH_UNSET);
} }

View File

@ -79,8 +79,11 @@ public final class CacheUtilTest {
} }
@Override @Override
public long getContentLength(String key) { public ContentMetadata getContentMetadata(String key) {
return contentLength; DefaultContentMetadata metadata = new DefaultContentMetadata();
ContentMetadataMutations mutations = new ContentMetadataMutations();
ContentMetadataMutations.setContentLength(mutations, contentLength);
return metadata.copyWithMutationsApplied(mutations);
} }
} }

View File

@ -154,11 +154,11 @@ public class CachedContentIndexTest {
assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5); assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5);
ContentMetadata metadata = index.get("ABCDE").getMetadata(); ContentMetadata metadata = index.get("ABCDE").getMetadata();
assertThat(ContentMetadataInternal.getContentLength(metadata)).isEqualTo(10); assertThat(ContentMetadata.getContentLength(metadata)).isEqualTo(10);
assertThat(index.assignIdForKey("KLMNO")).isEqualTo(2); assertThat(index.assignIdForKey("KLMNO")).isEqualTo(2);
ContentMetadata metadata2 = index.get("KLMNO").getMetadata(); ContentMetadata metadata2 = index.get("KLMNO").getMetadata();
assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560); assertThat(ContentMetadata.getContentLength(metadata2)).isEqualTo(2560);
} }
@Test @Test
@ -172,12 +172,12 @@ public class CachedContentIndexTest {
assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5); assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5);
ContentMetadata metadata = index.get("ABCDE").getMetadata(); ContentMetadata metadata = index.get("ABCDE").getMetadata();
assertThat(ContentMetadataInternal.getContentLength(metadata)).isEqualTo(10); assertThat(ContentMetadata.getContentLength(metadata)).isEqualTo(10);
assertThat(ContentMetadataInternal.getRedirectedUri(metadata)).isEqualTo(Uri.parse("abcde")); assertThat(ContentMetadata.getRedirectedUri(metadata)).isEqualTo(Uri.parse("abcde"));
assertThat(index.assignIdForKey("KLMNO")).isEqualTo(2); assertThat(index.assignIdForKey("KLMNO")).isEqualTo(2);
ContentMetadata metadata2 = index.get("KLMNO").getMetadata(); ContentMetadata metadata2 = index.get("KLMNO").getMetadata();
assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560); assertThat(ContentMetadata.getContentLength(metadata2)).isEqualTo(2560);
} }
@Test @Test
@ -297,11 +297,11 @@ public class CachedContentIndexTest {
private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2) private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2)
throws IOException { throws IOException {
ContentMetadataMutations mutations1 = new ContentMetadataMutations(); ContentMetadataMutations mutations1 = new ContentMetadataMutations();
ContentMetadataInternal.setContentLength(mutations1, 2560); ContentMetadataMutations.setContentLength(mutations1, 2560);
index.getOrAdd("KLMNO").applyMetadataMutations(mutations1); index.getOrAdd("KLMNO").applyMetadataMutations(mutations1);
ContentMetadataMutations mutations2 = new ContentMetadataMutations(); ContentMetadataMutations mutations2 = new ContentMetadataMutations();
ContentMetadataInternal.setContentLength(mutations2, 10); ContentMetadataMutations.setContentLength(mutations2, 10);
ContentMetadataInternal.setRedirectedUri(mutations2, Uri.parse("abcde")); ContentMetadataMutations.setRedirectedUri(mutations2, Uri.parse("abcde"));
index.getOrAdd("ABCDE").applyMetadataMutations(mutations2); index.getOrAdd("ABCDE").applyMetadataMutations(mutations2);
index.store(); index.store();

View File

@ -47,6 +47,7 @@ import org.robolectric.RuntimeEnvironment;
public class SimpleCacheTest { public class SimpleCacheTest {
private static final String KEY_1 = "key1"; private static final String KEY_1 = "key1";
private static final String KEY_2 = "key2";
private File cacheDir; private File cacheDir;
@ -105,18 +106,26 @@ public class SimpleCacheTest {
} }
@Test @Test
public void testSetGetLength() throws Exception { public void testSetGetContentMetadata() throws Exception {
SimpleCache simpleCache = getSimpleCache(); 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); ContentMetadataMutations mutations = new ContentMetadataMutations();
assertThat(simpleCache.getContentLength(KEY_1)).isEqualTo(15); ContentMetadataMutations.setContentLength(mutations, 15);
simpleCache.applyContentMetadataMutations(KEY_1, mutations);
assertThat(ContentMetadata.getContentLength(simpleCache.getContentMetadata(KEY_1)))
.isEqualTo(15);
simpleCache.startReadWrite(KEY_1, 0); simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15); 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); addCache(simpleCache, KEY_1, 140, 10);
@ -124,14 +133,16 @@ public class SimpleCacheTest {
// Check if values are kept after cache is reloaded. // Check if values are kept after cache is reloaded.
SimpleCache simpleCache2 = getSimpleCache(); 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 // Removing the last span shouldn't cause the length be change next time cache loaded
SimpleCacheSpan lastSpan = simpleCache2.startReadWrite(KEY_1, 145); SimpleCacheSpan lastSpan = simpleCache2.startReadWrite(KEY_1, 145);
simpleCache2.removeSpan(lastSpan); simpleCache2.removeSpan(lastSpan);
simpleCache2.release(); simpleCache2.release();
simpleCache2 = getSimpleCache(); simpleCache2 = getSimpleCache();
assertThat(simpleCache2.getContentLength(KEY_1)).isEqualTo(150); assertThat(ContentMetadata.getContentLength(simpleCache2.getContentMetadata(KEY_1)))
.isEqualTo(150);
} }
@Test @Test
@ -152,6 +163,40 @@ public class SimpleCacheTest {
assertCachedDataReadCorrect(cacheSpan2); 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 @Test
public void testEncryptedIndex() throws Exception { public void testEncryptedIndex() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key

View File

@ -26,6 +26,8 @@ import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.Timeline; 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.BaseMediaSource;
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory;
@ -59,6 +61,7 @@ import java.io.InputStreamReader;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.TimeZone; import java.util.TimeZone;
import java.util.regex.Matcher; import java.util.regex.Matcher;
@ -75,15 +78,16 @@ public final class DashMediaSource extends BaseMediaSource {
public static final class Factory implements AdsMediaSource.MediaSourceFactory { public static final class Factory implements AdsMediaSource.MediaSourceFactory {
private final DashChunkSource.Factory chunkSourceFactory; 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 CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private long livePresentationDelayMs; private long livePresentationDelayMs;
private boolean livePresentationDelayOverridesManifest; private boolean livePresentationDelayOverridesManifest;
private boolean isCreateCalled; private boolean isCreateCalled;
private @Nullable Object tag; @Nullable private Object tag;
/** /**
* Creates a new factory for {@link DashMediaSource}s. * Creates a new factory for {@link DashMediaSource}s.
@ -210,6 +214,19 @@ public final class DashMediaSource extends BaseMediaSource {
return this; 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 * 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 * 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) { public DashMediaSource createMediaSource(DashManifest manifest) {
Assertions.checkArgument(!manifest.dynamic); Assertions.checkArgument(!manifest.dynamic);
isCreateCalled = true; isCreateCalled = true;
if (streamKeys != null && !streamKeys.isEmpty()) {
manifest = manifest.copy(streamKeys);
}
return new DashMediaSource( return new DashMediaSource(
manifest, manifest,
/* manifestUri= */ null, /* manifestUri= */ null,
@ -281,6 +301,9 @@ public final class DashMediaSource extends BaseMediaSource {
if (manifestParser == null) { if (manifestParser == null) {
manifestParser = new DashManifestParser(); manifestParser = new DashManifestParser();
} }
if (streamKeys != null) {
manifestParser = new FilteringManifestParser<>(manifestParser, streamKeys);
}
return new DashMediaSource( return new DashMediaSource(
/* manifest= */ null, /* manifest= */ null,
Assertions.checkNotNull(manifestUri), Assertions.checkNotNull(manifestUri),

View File

@ -16,22 +16,25 @@
package com.google.android.exoplayer2.source.dash.offline; package com.google.android.exoplayer2.source.dash.offline;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; 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.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadHelper; import com.google.android.exoplayer2.offline.DownloadHelper;
import com.google.android.exoplayer2.offline.StreamKey; 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.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; 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.DashManifest;
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.source.dash.manifest.Representation;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.upstream.ParsingLoadable;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** A {@link DownloadHelper} for DASH streams. */ /** A {@link DownloadHelper} for DASH streams. */
@ -39,8 +42,52 @@ public final class DashDownloadHelper extends DownloadHelper<DashManifest> {
private final DataSource.Factory manifestDataSourceFactory; 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; this.manifestDataSourceFactory = manifestDataSourceFactory;
} }
@ -72,12 +119,8 @@ public final class DashDownloadHelper extends DownloadHelper<DashManifest> {
} }
@Override @Override
protected List<StreamKey> toStreamKeys(List<TrackKey> trackKeys) { protected StreamKey toStreamKey(
List<StreamKey> streamKeys = new ArrayList<>(trackKeys.size()); int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) {
for (int i = 0; i < trackKeys.size(); i++) { return new StreamKey(periodIndex, trackGroupIndex, trackIndexInTrackGroup);
TrackKey trackKey = trackKeys.get(i);
streamKeys.add(new StreamKey(trackKey.periodIndex, trackKey.groupIndex, trackKey.trackIndex));
}
return streamKeys;
} }
} }

View File

@ -22,6 +22,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.extractor.Extractor; 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.BaseMediaSource;
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; 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.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistParserFactory; 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.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.HlsMediaPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
@ -64,12 +66,13 @@ public final class HlsMediaSource extends BaseMediaSource
private HlsExtractorFactory extractorFactory; private HlsExtractorFactory extractorFactory;
private HlsPlaylistParserFactory playlistParserFactory; private HlsPlaylistParserFactory playlistParserFactory;
@Nullable private List<StreamKey> streamKeys;
private HlsPlaylistTracker.Factory playlistTrackerFactory; private HlsPlaylistTracker.Factory playlistTrackerFactory;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private boolean allowChunklessPreparation; private boolean allowChunklessPreparation;
private boolean isCreateCalled; private boolean isCreateCalled;
private @Nullable Object tag; @Nullable private Object tag;
/** /**
* Creates a new factory for {@link HlsMediaSource}s. * 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 * Sets the factory from which playlist parsers will be obtained. The default value is a {@link
* by calling {@link DefaultHlsPlaylistParserFactory#DefaultHlsPlaylistParserFactory()}. * DefaultHlsPlaylistParserFactory}.
* *
* @param playlistParserFactory An {@link HlsPlaylistParserFactory}. * @param playlistParserFactory An {@link HlsPlaylistParserFactory}.
* @return This factory, for convenience. * @return This factory, for convenience.
@ -177,6 +180,19 @@ public final class HlsMediaSource extends BaseMediaSource
return this; 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 * Sets the {@link HlsPlaylistTracker} factory. The default value is {@link
* DefaultHlsPlaylistTracker#FACTORY}. * DefaultHlsPlaylistTracker#FACTORY}.
@ -232,6 +248,10 @@ public final class HlsMediaSource extends BaseMediaSource
@Override @Override
public HlsMediaSource createMediaSource(Uri playlistUri) { public HlsMediaSource createMediaSource(Uri playlistUri) {
isCreateCalled = true; isCreateCalled = true;
if (streamKeys != null) {
playlistParserFactory =
new FilteringHlsPlaylistParserFactory(playlistParserFactory, streamKeys);
}
return new HlsMediaSource( return new HlsMediaSource(
playlistUri, playlistUri,
hlsDataSourceFactory, hlsDataSourceFactory,

View File

@ -16,23 +16,26 @@
package com.google.android.exoplayer2.source.hls.offline; package com.google.android.exoplayer2.source.hls.offline;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; 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.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadHelper; import com.google.android.exoplayer2.offline.DownloadHelper;
import com.google.android.exoplayer2.offline.StreamKey; 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.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; 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.HlsMediaPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.upstream.ParsingLoadable;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -43,8 +46,52 @@ public final class HlsDownloadHelper extends DownloadHelper<HlsPlaylist> {
private int[] renditionGroups; 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; this.manifestDataSourceFactory = manifestDataSourceFactory;
} }
@ -61,7 +108,7 @@ public final class HlsDownloadHelper extends DownloadHelper<HlsPlaylist> {
renditionGroups = new int[0]; renditionGroups = new int[0];
return new TrackGroupArray[] {TrackGroupArray.EMPTY}; 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; HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist;
TrackGroup[] trackGroups = new TrackGroup[3]; TrackGroup[] trackGroups = new TrackGroup[3];
renditionGroups = new int[3]; renditionGroups = new int[3];
@ -82,14 +129,9 @@ public final class HlsDownloadHelper extends DownloadHelper<HlsPlaylist> {
} }
@Override @Override
protected List<StreamKey> toStreamKeys(List<TrackKey> trackKeys) { protected StreamKey toStreamKey(
List<StreamKey> representationKeys = new ArrayList<>(trackKeys.size()); int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) {
for (int i = 0; i < trackKeys.size(); i++) { return new StreamKey(renditionGroups[trackGroupIndex], trackIndexInTrackGroup);
TrackKey trackKey = trackKeys.get(i);
representationKeys.add(
new StreamKey(renditionGroups[trackKey.groupIndex], trackKey.trackIndex));
}
return representationKeys;
} }
private static Format[] toFormats(List<HlsMasterPlaylist.HlsUrl> hlsUrls) { private static Format[] toFormats(List<HlsMasterPlaylist.HlsUrl> hlsUrls) {

View File

@ -15,40 +15,19 @@
*/ */
package com.google.android.exoplayer2.source.hls.playlist; 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 com.google.android.exoplayer2.upstream.ParsingLoadable;
import java.util.Collections;
import java.util.List;
/** Default implementation for {@link HlsPlaylistParserFactory}. */ /** Default implementation for {@link HlsPlaylistParserFactory}. */
public final class DefaultHlsPlaylistParserFactory implements 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 @Override
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser() { public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser() {
return new FilteringManifestParser<>(new HlsPlaylistParser(), streamKeys); return new HlsPlaylistParser();
} }
@Override @Override
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser( public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
HlsMasterPlaylist masterPlaylist) { HlsMasterPlaylist masterPlaylist) {
return new FilteringManifestParser<>(new HlsPlaylistParser(masterPlaylist), streamKeys); return new HlsPlaylistParser(masterPlaylist);
} }
} }

View File

@ -0,0 +1,55 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source.hls.playlist;
import com.google.android.exoplayer2.offline.FilteringManifestParser;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
import java.util.List;
/**
* A {@link HlsPlaylistParserFactory} that includes only the streams identified by the given stream
* keys.
*/
public final class FilteringHlsPlaylistParserFactory implements HlsPlaylistParserFactory {
private final HlsPlaylistParserFactory hlsPlaylistParserFactory;
private final List<StreamKey> streamKeys;
/**
* @param hlsPlaylistParserFactory A factory for the parsers of the playlists which will be
* filtered.
* @param streamKeys The stream keys. If null or empty then filtering will not occur.
*/
public FilteringHlsPlaylistParserFactory(
HlsPlaylistParserFactory hlsPlaylistParserFactory, List<StreamKey> streamKeys) {
this.hlsPlaylistParserFactory = hlsPlaylistParserFactory;
this.streamKeys = streamKeys;
}
@Override
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser() {
return new FilteringManifestParser<>(
hlsPlaylistParserFactory.createPlaylistParser(), streamKeys);
}
@Override
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
HlsMasterPlaylist masterPlaylist) {
return new FilteringManifestParser<>(
hlsPlaylistParserFactory.createPlaylistParser(masterPlaylist), streamKeys);
}
}

View File

@ -20,6 +20,7 @@ import android.util.Base64;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; 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.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; 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 com.google.android.exoplayer2.upstream.TransferListener;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
/** /** A SmoothStreaming {@link MediaPeriod}. */
* A SmoothStreaming {@link MediaPeriod}. /* package */ final class SsMediaPeriod
*/ implements MediaPeriod, SequenceableLoader.Callback<ChunkSampleStream<SsChunkSource>> {
/* package */ final class SsMediaPeriod implements MediaPeriod,
SequenceableLoader.Callback<ChunkSampleStream<SsChunkSource>> {
private static final int INITIALIZATION_VECTOR_SIZE = 8; private static final int INITIALIZATION_VECTOR_SIZE = 8;
@ -112,6 +112,8 @@ import java.util.ArrayList;
eventDispatcher.mediaPeriodReleased(); eventDispatcher.mediaPeriodReleased();
} }
// MediaPeriod implementation.
@Override @Override
public void prepare(Callback callback, long positionUs) { public void prepare(Callback callback, long positionUs) {
this.callback = callback; this.callback = callback;
@ -157,6 +159,16 @@ import java.util.ArrayList;
return positionUs; 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 @Override
public void discardBuffer(long positionUs, boolean toKeyframe) { public void discardBuffer(long positionUs, boolean toKeyframe) {
for (ChunkSampleStream<SsChunkSource> sampleStream : sampleStreams) { for (ChunkSampleStream<SsChunkSource> sampleStream : sampleStreams) {
@ -211,7 +223,7 @@ import java.util.ArrayList;
return positionUs; return positionUs;
} }
// SequenceableLoader.Callback implementation // SequenceableLoader.Callback implementation.
@Override @Override
public void onContinueLoadingRequested(ChunkSampleStream<SsChunkSource> sampleStream) { public void onContinueLoadingRequested(ChunkSampleStream<SsChunkSource> sampleStream) {
@ -277,5 +289,4 @@ import java.util.ArrayList;
data[firstPosition] = data[secondPosition]; data[firstPosition] = data[secondPosition];
data[secondPosition] = temp; data[secondPosition] = temp;
} }
} }

View File

@ -24,6 +24,8 @@ import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.Timeline; 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.BaseMediaSource;
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; 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 com.google.android.exoplayer2.util.Assertions;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
/** A SmoothStreaming {@link MediaSource}. */ /** A SmoothStreaming {@link MediaSource}. */
public final class SsMediaSource extends BaseMediaSource public final class SsMediaSource extends BaseMediaSource
@ -63,14 +66,15 @@ public final class SsMediaSource extends BaseMediaSource
public static final class Factory implements AdsMediaSource.MediaSourceFactory { public static final class Factory implements AdsMediaSource.MediaSourceFactory {
private final SsChunkSource.Factory chunkSourceFactory; 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 CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private long livePresentationDelayMs; private long livePresentationDelayMs;
private boolean isCreateCalled; private boolean isCreateCalled;
private @Nullable Object tag; @Nullable private Object tag;
/** /**
* Creates a new factory for {@link SsMediaSource}s. * Creates a new factory for {@link SsMediaSource}s.
@ -178,6 +182,19 @@ public final class SsMediaSource extends BaseMediaSource
return this; 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 * 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 * 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) { public SsMediaSource createMediaSource(SsManifest manifest) {
Assertions.checkArgument(!manifest.isLive); Assertions.checkArgument(!manifest.isLive);
isCreateCalled = true; isCreateCalled = true;
if (streamKeys != null && !streamKeys.isEmpty()) {
manifest = manifest.copy(streamKeys);
}
return new SsMediaSource( return new SsMediaSource(
manifest, manifest,
/* manifestUri= */ null, /* manifestUri= */ null,
@ -248,6 +268,9 @@ public final class SsMediaSource extends BaseMediaSource
if (manifestParser == null) { if (manifestParser == null) {
manifestParser = new SsManifestParser(); manifestParser = new SsManifestParser();
} }
if (streamKeys != null) {
manifestParser = new FilteringManifestParser<>(manifestParser, streamKeys);
}
return new SsMediaSource( return new SsMediaSource(
/* manifest= */ null, /* manifest= */ null,
Assertions.checkNotNull(manifestUri), Assertions.checkNotNull(manifestUri),

View File

@ -16,35 +16,83 @@
package com.google.android.exoplayer2.source.smoothstreaming.offline; package com.google.android.exoplayer2.source.smoothstreaming.offline;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; 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.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadHelper; import com.google.android.exoplayer2.offline.DownloadHelper;
import com.google.android.exoplayer2.offline.StreamKey; 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.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; 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.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.DataSource;
import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.upstream.ParsingLoadable;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/** A {@link DownloadHelper} for SmoothStreaming streams. */ /** A {@link DownloadHelper} for SmoothStreaming streams. */
public final class SsDownloadHelper extends DownloadHelper<SsManifest> { public final class SsDownloadHelper extends DownloadHelper<SsManifest> {
private final DataSource.Factory manifestDataSourceFactory; 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; this.manifestDataSourceFactory = manifestDataSourceFactory;
} }
@Override @Override
protected SsManifest loadManifest(Uri uri) throws IOException { protected SsManifest loadManifest(Uri uri) throws IOException {
DataSource dataSource = manifestDataSourceFactory.createDataSource(); 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 @Override
@ -58,12 +106,8 @@ public final class SsDownloadHelper extends DownloadHelper<SsManifest> {
} }
@Override @Override
protected List<StreamKey> toStreamKeys(List<TrackKey> trackKeys) { protected StreamKey toStreamKey(
List<StreamKey> representationKeys = new ArrayList<>(trackKeys.size()); int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) {
for (int i = 0; i < trackKeys.size(); i++) { return new StreamKey(trackGroupIndex, trackIndexInTrackGroup);
TrackKey trackKey = trackKeys.get(i);
representationKeys.add(new StreamKey(trackKey.groupIndex, trackKey.trackIndex));
}
return representationKeys;
} }
} }

View File

@ -23,7 +23,7 @@ import android.support.annotation.Nullable;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import com.google.android.exoplayer2.C; 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. */ /** Helper for creating download notifications. */
public final class DownloadNotificationUtil { public final class DownloadNotificationUtil {
@ -33,7 +33,7 @@ public final class DownloadNotificationUtil {
private 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 context A context for accessing resources.
* @param smallIcon A small icon for the notification. * @param smallIcon A small icon for the notification.
@ -41,7 +41,7 @@ public final class DownloadNotificationUtil {
* above. * above.
* @param contentIntent An optional content intent to send when the notification is clicked. * @param contentIntent An optional content intent to send when the notification is clicked.
* @param message An optional message to display on the notification. * @param message An optional message to display on the notification.
* @param taskStates The task states. * @param downloadStates The download states.
* @return The notification. * @return The notification.
*/ */
public static Notification buildProgressNotification( public static Notification buildProgressNotification(
@ -50,28 +50,28 @@ public final class DownloadNotificationUtil {
String channelId, String channelId,
@Nullable PendingIntent contentIntent, @Nullable PendingIntent contentIntent,
@Nullable String message, @Nullable String message,
TaskState[] taskStates) { DownloadState[] downloadStates) {
float totalPercentage = 0; float totalPercentage = 0;
int downloadTaskCount = 0; int downloadTaskCount = 0;
boolean allDownloadPercentagesUnknown = true; boolean allDownloadPercentagesUnknown = true;
boolean haveDownloadedBytes = false; boolean haveDownloadedBytes = false;
boolean haveDownloadTasks = false; boolean haveDownloadTasks = false;
boolean haveRemoveTasks = false; boolean haveRemoveTasks = false;
for (TaskState taskState : taskStates) { for (DownloadState downloadState : downloadStates) {
if (taskState.state != TaskState.STATE_STARTED if (downloadState.state != DownloadState.STATE_STARTED
&& taskState.state != TaskState.STATE_COMPLETED) { && downloadState.state != DownloadState.STATE_COMPLETED) {
continue; continue;
} }
if (taskState.action.isRemoveAction) { if (downloadState.action.isRemoveAction) {
haveRemoveTasks = true; haveRemoveTasks = true;
continue; continue;
} }
haveDownloadTasks = true; haveDownloadTasks = true;
if (taskState.downloadPercentage != C.PERCENTAGE_UNSET) { if (downloadState.downloadPercentage != C.PERCENTAGE_UNSET) {
allDownloadPercentagesUnknown = false; allDownloadPercentagesUnknown = false;
totalPercentage += taskState.downloadPercentage; totalPercentage += downloadState.downloadPercentage;
} }
haveDownloadedBytes |= taskState.downloadedBytes > 0; haveDownloadedBytes |= downloadState.downloadedBytes > 0;
downloadTaskCount++; downloadTaskCount++;
} }

View File

@ -29,7 +29,6 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Looper; import android.os.Looper;
import android.support.annotation.IntDef; import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.util.AttributeSet; import android.util.AttributeSet;
@ -187,8 +186,9 @@ import java.util.List;
* <li>Type: {@link AspectRatioFrameLayout} * <li>Type: {@link AspectRatioFrameLayout}
* </ul> * </ul>
* <li><b>{@code exo_shutter}</b> - A view that's made visible when video should be hidden. This * <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 * view is typically an opaque view that covers the video surface, thereby obscuring it when
* when visible. * visible. Obscuring the surface in this way also helps to prevent flicker at the start of
* playback when {@code surface_type="surface_view"}.
* <ul> * <ul>
* <li>Type: {@link View} * <li>Type: {@link View}
* </ul> * </ul>
@ -271,13 +271,13 @@ public class PlayerView extends FrameLayout {
private static final int SURFACE_TYPE_MONO360_VIEW = 3; private static final int SURFACE_TYPE_MONO360_VIEW = 3;
// LINT.ThenChange(../../../../../../res/values/attrs.xml) // LINT.ThenChange(../../../../../../res/values/attrs.xml)
private final AspectRatioFrameLayout contentFrame; @Nullable private final AspectRatioFrameLayout contentFrame;
private final View shutterView; private final View shutterView;
private final View surfaceView; @Nullable private final View surfaceView;
private final ImageView artworkView; private final ImageView artworkView;
private final SubtitleView subtitleView; private final SubtitleView subtitleView;
private final @Nullable View bufferingView; @Nullable private final View bufferingView;
private final @Nullable TextView errorMessageView; @Nullable private final TextView errorMessageView;
private final PlayerControlView controller; private final PlayerControlView controller;
private final ComponentListener componentListener; private final ComponentListener componentListener;
private final FrameLayout overlayFrameLayout; private final FrameLayout overlayFrameLayout;
@ -285,11 +285,11 @@ public class PlayerView extends FrameLayout {
private Player player; private Player player;
private boolean useController; private boolean useController;
private boolean useArtwork; private boolean useArtwork;
private @Nullable Drawable defaultArtwork; @Nullable private Drawable defaultArtwork;
private @ShowBuffering int showBuffering; private @ShowBuffering int showBuffering;
private boolean keepContentOnPlayerReset; private boolean keepContentOnPlayerReset;
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider; @Nullable private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
private @Nullable CharSequence customErrorMessage; @Nullable private CharSequence customErrorMessage;
private int controllerShowTimeoutMs; private int controllerShowTimeoutMs;
private boolean controllerAutoShow; private boolean controllerAutoShow;
private boolean controllerHideDuringAds; private boolean controllerHideDuringAds;
@ -474,9 +474,7 @@ public class PlayerView extends FrameLayout {
* @param newPlayerView The new view to attach to the player. * @param newPlayerView The new view to attach to the player.
*/ */
public static void switchTargetView( public static void switchTargetView(
@NonNull Player player, Player player, @Nullable PlayerView oldPlayerView, @Nullable PlayerView newPlayerView) {
@Nullable PlayerView oldPlayerView,
@Nullable PlayerView newPlayerView) {
if (oldPlayerView == newPlayerView) { if (oldPlayerView == newPlayerView) {
return; 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() { private boolean toggleControllerVisibility() {
if (!useController || player == null) { if (!useController || player == null) {
return false; return false;
@ -1193,9 +1211,8 @@ public class PlayerView extends FrameLayout {
int drawableWidth = drawable.getIntrinsicWidth(); int drawableWidth = drawable.getIntrinsicWidth();
int drawableHeight = drawable.getIntrinsicHeight(); int drawableHeight = drawable.getIntrinsicHeight();
if (drawableWidth > 0 && drawableHeight > 0) { if (drawableWidth > 0 && drawableHeight > 0) {
if (contentFrame != null) { float artworkAspectRatio = (float) drawableWidth / drawableHeight;
contentFrame.setAspectRatio((float) drawableWidth / drawableHeight); onContentAspectRatioChanged(artworkAspectRatio, contentFrame, artworkView);
}
artworkView.setImageDrawable(drawable); artworkView.setImageDrawable(drawable);
artworkView.setVisibility(VISIBLE); artworkView.setVisibility(VISIBLE);
return true; return true;
@ -1328,9 +1345,6 @@ public class PlayerView extends FrameLayout {
@Override @Override
public void onVideoSizeChanged( public void onVideoSizeChanged(
int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
if (contentFrame == null) {
return;
}
float videoAspectRatio = float videoAspectRatio =
(height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height; (height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height;
@ -1351,11 +1365,9 @@ public class PlayerView extends FrameLayout {
surfaceView.addOnLayoutChangeListener(this); surfaceView.addOnLayoutChangeListener(this);
} }
applyTextureViewRotation((TextureView) surfaceView, textureViewRotation); applyTextureViewRotation((TextureView) surfaceView, textureViewRotation);
} else if (surfaceView instanceof SphericalSurfaceView) {
videoAspectRatio = 0;
} }
contentFrame.setAspectRatio(videoAspectRatio); onContentAspectRatioChanged(videoAspectRatio, contentFrame, surfaceView);
} }
@Override @Override

View File

@ -15,7 +15,6 @@
*/ */
package com.google.android.exoplayer2.ui; package com.google.android.exoplayer2.ui;
import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
import android.content.Context; 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.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; 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 com.google.android.exoplayer2.util.Assertions;
import java.util.Arrays; import java.util.Arrays;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** A view for making track selections. */ /** A view for making track selections. */
public class TrackSelectionView extends LinearLayout { 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 int selectableItemBackgroundResourceId;
private final LayoutInflater inflater; private final LayoutInflater inflater;
private final CheckedTextView disableView; private final CheckedTextView disableView;
@ -51,35 +62,64 @@ public class TrackSelectionView extends LinearLayout {
private TrackNameProvider trackNameProvider; private TrackNameProvider trackNameProvider;
private CheckedTextView[][] trackViews; private CheckedTextView[][] trackViews;
private DefaultTrackSelector trackSelector; private @MonotonicNonNull MappedTrackInfo mappedTrackInfo;
private int rendererIndex; private int rendererIndex;
private DefaultTrackSelector.Parameters parameters;
private TrackGroupArray trackGroups; private TrackGroupArray trackGroups;
private boolean isDisabled; 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. * 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 title The dialog's title.
* @param trackSelector The track selector. * @param trackSelector The track selector.
* @param rendererIndex The index of the renderer. * @param rendererIndex The index of the renderer.
* @return The dialog and the {@link TrackSelectionView} that will be shown by it. * @return The dialog and the {@link TrackSelectionView} that will be shown by it.
*/ */
public static Pair<AlertDialog, TrackSelectionView> getDialog( 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, CharSequence title,
DefaultTrackSelector trackSelector, MappedTrackInfo mappedTrackInfo,
int rendererIndex) { int rendererIndex,
AlertDialog.Builder builder = new AlertDialog.Builder(activity); 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. // Inflate with the builder's context to ensure the correct style is used.
LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext()); LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
View dialogView = dialogInflater.inflate(R.layout.exo_track_selection_dialog, null); View dialogView = dialogInflater.inflate(R.layout.exo_track_selection_dialog, null);
final TrackSelectionView selectionView = dialogView.findViewById(R.id.exo_track_selection_view); TrackSelectionView selectionView = dialogView.findViewById(R.id.exo_track_selection_view);
selectionView.init(trackSelector, rendererIndex); selectionView.init(mappedTrackInfo, rendererIndex, parameters);
Dialog.OnClickListener okClickListener = (dialog, which) -> selectionView.applySelection(); Dialog.OnClickListener okClickListener =
(dialog, which) -> callback.onTracksSelected(selectionView.getSelectionParameters());
AlertDialog dialog = AlertDialog dialog =
builder builder
@ -113,6 +153,8 @@ public class TrackSelectionView extends LinearLayout {
inflater = LayoutInflater.from(context); inflater = LayoutInflater.from(context);
componentListener = new ComponentListener(); componentListener = new ComponentListener();
trackNameProvider = new DefaultTrackNameProvider(getResources()); trackNameProvider = new DefaultTrackNameProvider(getResources());
parameters = DefaultTrackSelector.Parameters.DEFAULT;
trackGroups = TrackGroupArray.EMPTY;
// View for disabling the renderer. // View for disabling the renderer.
disableView = disableView =
@ -176,18 +218,35 @@ public class TrackSelectionView extends LinearLayout {
} }
/** /**
* Initialize the view to select tracks for a specified renderer using a {@link * Initialize the view to select tracks for a specified renderer using {@link MappedTrackInfo} and
* DefaultTrackSelector}. * a set of {@link DefaultTrackSelector.Parameters}.
* *
* @param trackSelector The {@link DefaultTrackSelector}. * @param mappedTrackInfo The {@link MappedTrackInfo}.
* @param rendererIndex The index of the renderer. * @param rendererIndex The index of the renderer.
* @param parameters The {@link DefaultTrackSelector.Parameters}.
*/ */
public void init(DefaultTrackSelector trackSelector, int rendererIndex) { public void init(
this.trackSelector = trackSelector; MappedTrackInfo mappedTrackInfo,
int rendererIndex,
DefaultTrackSelector.Parameters parameters) {
this.mappedTrackInfo = mappedTrackInfo;
this.rendererIndex = rendererIndex; this.rendererIndex = rendererIndex;
this.parameters = parameters;
updateViews(); 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 methods.
private void updateViews() { private void updateViews() {
@ -196,9 +255,7 @@ public class TrackSelectionView extends LinearLayout {
removeViewAt(i); removeViewAt(i);
} }
MappingTrackSelector.MappedTrackInfo trackInfo = if (mappedTrackInfo == null) {
trackSelector == null ? null : trackSelector.getCurrentMappedTrackInfo();
if (trackSelector == null || trackInfo == null) {
// The view is not initialized. // The view is not initialized.
disableView.setEnabled(false); disableView.setEnabled(false);
defaultView.setEnabled(false); defaultView.setEnabled(false);
@ -207,9 +264,8 @@ public class TrackSelectionView extends LinearLayout {
disableView.setEnabled(true); disableView.setEnabled(true);
defaultView.setEnabled(true); defaultView.setEnabled(true);
trackGroups = trackInfo.getTrackGroups(rendererIndex); trackGroups = mappedTrackInfo.getTrackGroups(rendererIndex);
DefaultTrackSelector.Parameters parameters = trackSelector.getParameters();
isDisabled = parameters.getRendererDisabled(rendererIndex); isDisabled = parameters.getRendererDisabled(rendererIndex);
override = parameters.getSelectionOverride(rendererIndex, trackGroups); override = parameters.getSelectionOverride(rendererIndex, trackGroups);
@ -220,7 +276,7 @@ public class TrackSelectionView extends LinearLayout {
boolean enableAdaptiveSelections = boolean enableAdaptiveSelections =
allowAdaptiveSelections allowAdaptiveSelections
&& trackGroups.get(groupIndex).length > 1 && trackGroups.get(groupIndex).length > 1
&& trackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false) && mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false)
!= RendererCapabilities.ADAPTIVE_NOT_SUPPORTED; != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED;
trackViews[groupIndex] = new CheckedTextView[group.length]; trackViews[groupIndex] = new CheckedTextView[group.length];
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
@ -235,7 +291,7 @@ public class TrackSelectionView extends LinearLayout {
(CheckedTextView) inflater.inflate(trackViewLayoutId, this, false); (CheckedTextView) inflater.inflate(trackViewLayoutId, this, false);
trackView.setBackgroundResource(selectableItemBackgroundResourceId); trackView.setBackgroundResource(selectableItemBackgroundResourceId);
trackView.setText(trackNameProvider.getTrackName(group.getFormat(trackIndex))); trackView.setText(trackNameProvider.getTrackName(group.getFormat(trackIndex)));
if (trackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex) if (mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)
== RendererCapabilities.FORMAT_HANDLED) { == RendererCapabilities.FORMAT_HANDLED) {
trackView.setFocusable(true); trackView.setFocusable(true);
trackView.setTag(Pair.create(groupIndex, trackIndex)); 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) { private void onClick(View view) {
if (view == disableView) { if (view == disableView) {
onDisableViewClicked(); onDisableViewClicked();

View File

@ -79,8 +79,19 @@ public class FakeTrackSelector extends DefaultTrackSelector {
} }
@Override @Override
public TrackSelection createTrackSelection( public TrackSelection[] createTrackSelections(
TrackGroup trackGroup, BandwidthMeter bandwidthMeter, int... tracks) { 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) { if (mayReuseTrackSelection) {
for (FakeTrackSelection trackSelection : trackSelections) { for (FakeTrackSelection trackSelection : trackSelections) {
if (trackSelection.getTrackGroup().equals(trackGroup)) { if (trackSelection.getTrackGroup().equals(trackGroup)) {
@ -92,18 +103,5 @@ public class FakeTrackSelector extends DefaultTrackSelector {
trackSelections.add(trackSelection); trackSelections.add(trackSelection);
return trackSelection; return trackSelection;
} }
@Override
public TrackSelection[] createTrackSelections(
TrackSelection.Definition[] definitions, BandwidthMeter bandwidthMeter) {
TrackSelection[] selections = new TrackSelection[definitions.length];
for (int i = 0; i < definitions.length; i++) {
TrackSelection.Definition definition = definitions[i];
if (definition != null) {
selections[i] = createTrackSelection(definition.group, bandwidthMeter, definition.tracks);
}
}
return selections;
}
} }
} }

View File

@ -17,8 +17,8 @@ package com.google.android.exoplayer2.testutil;
import static com.google.common.truth.Truth.assertThat; import 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;
import com.google.android.exoplayer2.offline.DownloadManager.DownloadState;
import java.util.HashMap; import java.util.HashMap;
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
@ -31,10 +31,10 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
private final DownloadManager downloadManager; private final DownloadManager downloadManager;
private final DummyMainThread dummyMainThread; private final DummyMainThread dummyMainThread;
private final HashMap<DownloadAction, ArrayBlockingQueue<Integer>> actionStates; private final HashMap<String, ArrayBlockingQueue<Integer>> actionStates;
private CountDownLatch downloadFinishedCondition; private CountDownLatch downloadFinishedCondition;
private Throwable downloadError; @DownloadState.FailureReason private int failureReason;
public TestDownloadManagerListener( public TestDownloadManagerListener(
DownloadManager downloadManager, DummyMainThread dummyMainThread) { DownloadManager downloadManager, DummyMainThread dummyMainThread) {
@ -43,12 +43,12 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
actionStates = new HashMap<>(); actionStates = new HashMap<>();
} }
public int pollStateChange(DownloadAction action, long timeoutMs) throws InterruptedException { public Integer pollStateChange(String taskId, long timeoutMs) throws InterruptedException {
return getStateQueue(action).poll(timeoutMs, TimeUnit.MILLISECONDS); return getStateQueue(taskId).poll(timeoutMs, TimeUnit.MILLISECONDS);
} }
public void clearDownloadError() { public void clearDownloadError() {
this.downloadError = null; this.failureReason = DownloadState.FAILURE_REASON_NONE;
} }
@Override @Override
@ -57,12 +57,11 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
} }
@Override @Override
public void onTaskStateChanged( public void onDownloadStateChanged(DownloadManager downloadManager, DownloadState downloadState) {
DownloadManager downloadManager, DownloadManager.TaskState taskState) { if (downloadState.state == DownloadState.STATE_FAILED) {
if (taskState.state == DownloadManager.TaskState.STATE_FAILED && downloadError == null) { failureReason = downloadState.failureReason;
downloadError = taskState.error;
} }
getStateQueue(taskState.action).add(taskState.state); getStateQueue(downloadState.id).add(downloadState.state);
} }
@Override @Override
@ -77,6 +76,14 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
* error. * error.
*/ */
public void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable { 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) { synchronized (this) {
downloadFinishedCondition = new CountDownLatch(1); downloadFinishedCondition = new CountDownLatch(1);
} }
@ -87,17 +94,14 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
} }
}); });
assertThat(downloadFinishedCondition.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue(); 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) { synchronized (actionStates) {
if (!actionStates.containsKey(action)) { if (!actionStates.containsKey(taskId)) {
actionStates.put(action, new ArrayBlockingQueue<>(10)); actionStates.put(taskId, new ArrayBlockingQueue<>(10));
} }
return actionStates.get(action); return actionStates.get(taskId);
} }
} }
} }