diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 4a42639c84..979543f7be 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -2,19 +2,19 @@
### dev-v2 (not yet released) ###
-* Optimize seeking in FMP4 by enabling seeking to the nearest sync sample within
- a fragment. This benefits standalone FMP4 playbacks, DASH and SmoothStreaming.
-* Moved initial bitrate estimate from `AdaptiveTrackSelection` to
- `DefaultBandwidthMeter`.
-* Updated default max buffer length in `DefaultLoadControl`.
-* Added `AnalyticsListener` interface which can be registered in
- `SimpleExoPlayer` to receive detailed meta data for each ExoPlayer event.
-* UI components:
- * Add support for listening to `AspectRatioFrameLayout`'s aspect ratio update
- ([#3736](https://github.com/google/ExoPlayer/issues/3736)).
- * Add PlayerNotificationManager.
-* Downloading: Add `DownloadService`, `DownloadManager` and
- related classes ([#2643](https://github.com/google/ExoPlayer/issues/2643)).
+* Coming soon...
+
+### 2.8.0 ###
+
+* Downloading:
+ * Add `DownloadService`, `DownloadManager` and related classes
+ ([#2643](https://github.com/google/ExoPlayer/issues/2643)). Information on
+ using these components to download progressive formats can be found
+ [here](https://medium.com/google-exoplayer/downloading-streams-6d259eec7f95).
+ To see how to download DASH, HLS and SmoothStreaming media, take a look at
+ the app.
+ * Updated main demo app to support downloading DASH, HLS, SmoothStreaming and
+ progressive media.
* MediaSources:
* Allow reusing media sources after they have been released and
also in parallel to allow adding them multiple times to a concatenation.
@@ -31,9 +31,23 @@
* Support live stream clipping with `ClippingMediaSource`.
* Allow setting tags for all media sources in their factories. The tag of the
current window can be retrieved with `ExoPlayer.getCurrentTag`.
-* IMA: Allow setting the ad media load timeout
- ([#3691](https://github.com/google/ExoPlayer/issues/3691)).
+* UI components:
+ * Add support for displaying error messages and a buffering spinner in
+ `PlayerView`.
+ * Add support for listening to `AspectRatioFrameLayout`'s aspect ratio update
+ ([#3736](https://github.com/google/ExoPlayer/issues/3736)).
+ * Add `PlayerNotificationManager` for displaying notifications reflecting the
+ player state.
+ * Add `TrackSelectionView` for selecting tracks with `DefaultTrackSelector`.
+ * Add `TrackNameProvider` for converting track `Format`s to textual
+ descriptions, and `DefaultTrackNameProvider` as a default implementation.
+* Track selection:
+ * Reworked `MappingTrackSelector` and `DefaultTrackSelector`.
+ * `DefaultTrackSelector.Parameters` now implements `Parcelable`.
+ * Added UI components for track selection (see above).
* Audio:
+ * Support extracting data from AMR container formats, including both narrow
+ and wide band ([#2527](https://github.com/google/ExoPlayer/issues/2527)).
* FLAC:
* Sniff FLAC files correctly if they have ID3 headers
([#4055](https://github.com/google/ExoPlayer/issues/4055)).
@@ -48,14 +62,17 @@
* Fix an issue where playback of TrueHD streams would get stuck after seeking
due to not finding a syncframe
((#3845)[https://github.com/google/ExoPlayer/issues/3845]).
+ * Fix an issue with eac3-joc playback where a codec would fail to configure
+ ((#4165)[https://github.com/google/ExoPlayer/issues/4165]).
* Handle non-empty end-of-stream buffers, to fix gapless playback of streams
with encoder padding when the decoder returns a non-empty final buffer.
* Allow trimming more than one sample when applying an elst audio edit via
gapless playback info.
+ * Allow overriding skipping/scaling with custom `AudioProcessor`s
+ ((#3142)[https://github.com/google/ExoPlayer/issues/3142]).
* Caching:
- * Add release method to Cache interface.
- * Prevent multiple instances of SimpleCache in the same folder.
- Previous instance must be released.
+ * Add release method to the `Cache` interface, and prevent multiple instances
+ of `SimpleCache` using the same folder at the same time.
* Cache redirect URLs
([#2360](https://github.com/google/ExoPlayer/issues/2360)).
* DRM:
@@ -65,17 +82,41 @@
([#4022][https://github.com/google/ExoPlayer/issues/4022]).
* Fix handling of 307/308 redirects when making license requests
([#4108](https://github.com/google/ExoPlayer/issues/4108)).
-* HLS: Fix playlist loading error propagation when the current selection does
- not include all of the playlist's variants.
+* HLS:
+ * Fix playlist loading error propagation when the current selection does
+ not include all of the playlist's variants.
+ * Fix SAMPLE-AES-CENC and SAMPLE-AES-CTR EXT-X-KEY methods
+ ([#4145](https://github.com/google/ExoPlayer/issues/4145)).
+ * Preeptively declare an ID3 track in chunkless preparation
+ ([#4016](https://github.com/google/ExoPlayer/issues/4016)).
+ * Add support for multiple #EXT-X-MAP tags in a media playlist
+ ([#4164](https://github.com/google/ExoPlayer/issues/4182)).
+ * Fix seeking in live streams
+ ([#4187](https://github.com/google/ExoPlayer/issues/4187)).
+* IMA:
+ * Allow setting the ad media load timeout
+ ([#3691](https://github.com/google/ExoPlayer/issues/3691)).
+ * Expose ad load errors via `MediaSourceEventListener` on `AdsMediaSource`,
+ and allow setting an ad event listener on `ImaAdsLoader`. Deprecate the
+ `AdsMediaSource.EventListener`.
+* Add `AnalyticsListener` interface which can be registered in
+ `SimpleExoPlayer` to receive detailed metadata for each ExoPlayer event.
+* Optimize seeking in FMP4 by enabling seeking to the nearest sync sample within
+ a fragment. This benefits standalone FMP4 playbacks, DASH and SmoothStreaming.
+* Updated default max buffer length in `DefaultLoadControl`.
* Fix ClearKey decryption error if the key contains a forward slash
([#4075](https://github.com/google/ExoPlayer/issues/4075)).
* Fix crash when switching surface on Huawei P9 Lite
([#4084](https://github.com/google/ExoPlayer/issues/4084)), and Philips QM163E
([#4104](https://github.com/google/ExoPlayer/issues/4104)).
* Support ZLIB compressed PGS subtitles.
+* Added `getPlaybackError` to `Player` interface.
+* Moved initial bitrate estimate from `AdaptiveTrackSelection` to
+ `DefaultBandwidthMeter`.
* Removed default renderer time offset of 60000000 from internal player. The
actual renderer timestamp offset can be obtained by listening to
`BaseRenderer.onStreamChanged`.
+* Added dependencies on checkerframework annotations for static code analysis.
### 2.7.3 ###
@@ -240,6 +281,7 @@
([#3792](https://github.com/google/ExoPlayer/issues/3792).
* Support 14-bit mode and little endianness in DTS PES packets
([#3340](https://github.com/google/ExoPlayer/issues/3340)).
+* Demo app: Add ability to download not DRM protected content.
### 2.6.1 ###
diff --git a/extensions/mediasession/src/main/res/values-ms-rMY/strings.xml b/checker-framework-lint.xml
similarity index 62%
rename from extensions/mediasession/src/main/res/values-ms-rMY/strings.xml
rename to checker-framework-lint.xml
index 829542b668..1d45f9de05 100644
--- a/extensions/mediasession/src/main/res/values-ms-rMY/strings.xml
+++ b/checker-framework-lint.xml
@@ -1,6 +1,4 @@
-
-
-
- "Ulang semua"
- "Tiada ulangan"
- "Ulangan"
-
+
+
+
+
+
diff --git a/constants.gradle b/constants.gradle
index ea0c2436c0..dcadcceb4f 100644
--- a/constants.gradle
+++ b/constants.gradle
@@ -13,8 +13,8 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
- releaseVersion = '2.7.3'
- releaseVersionCode = 2703
+ releaseVersion = '2.8.0'
+ releaseVersionCode = 2800
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
// components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality provided
@@ -31,6 +31,8 @@ project.ext {
junitVersion = '4.12'
truthVersion = '0.39'
robolectricVersion = '3.7.1'
+ autoValueVersion = '1.6'
+ checkerframeworkVersion = '2.5.0'
modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix
diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml
index cde95300ab..3bedefc60e 100644
--- a/demos/main/src/main/AndroidManifest.xml
+++ b/demos/main/src/main/AndroidManifest.xml
@@ -19,6 +19,8 @@
+
+
@@ -73,6 +75,18 @@
+
+
+
+
+
+
+
+
+
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java
index f54a1a95e0..b5c127d2e3 100644
--- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java
@@ -16,6 +16,13 @@
package com.google.android.exoplayer2.demo;
import android.app.Application;
+import com.google.android.exoplayer2.offline.DownloadAction.Deserializer;
+import com.google.android.exoplayer2.offline.DownloadManager;
+import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
+import com.google.android.exoplayer2.offline.ProgressiveDownloadAction;
+import com.google.android.exoplayer2.source.dash.offline.DashDownloadAction;
+import com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction;
+import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
@@ -35,10 +42,24 @@ import java.io.File;
*/
public class DemoApplication extends Application {
- private static final String DOWNLOAD_CACHE_FOLDER = "downloads";
+ private static final String DOWNLOAD_ACTION_FILE = "actions";
+ private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
+ private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
+ private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
+ private static final Deserializer[] DOWNLOAD_DESERIALIZERS =
+ new Deserializer[] {
+ DashDownloadAction.DESERIALIZER,
+ HlsDownloadAction.DESERIALIZER,
+ SsDownloadAction.DESERIALIZER,
+ ProgressiveDownloadAction.DESERIALIZER
+ };
protected String userAgent;
+
+ private File downloadDirectory;
private Cache downloadCache;
+ private DownloadManager downloadManager;
+ private DownloadTracker downloadTracker;
@Override
public void onCreate() {
@@ -50,7 +71,7 @@ public class DemoApplication extends Application {
public DataSource.Factory buildDataSourceFactory(TransferListener super DataSource> listener) {
DefaultDataSourceFactory upstreamFactory =
new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener));
- return createReadOnlyCacheDataSource(upstreamFactory, getDownloadCache());
+ return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache());
}
/** Returns a {@link HttpDataSource.Factory}. */
@@ -59,31 +80,69 @@ public class DemoApplication extends Application {
return new DefaultHttpDataSourceFactory(userAgent, listener);
}
- /** Returns the download {@link Cache}. */
- public Cache getDownloadCache() {
- if (downloadCache == null) {
- File dir = getExternalFilesDir(null);
- if (dir == null) {
- dir = getFilesDir();
- }
- File downloadCacheFolder = new File(dir, DOWNLOAD_CACHE_FOLDER);
- downloadCache = new SimpleCache(downloadCacheFolder, new NoOpCacheEvictor());
- }
- return downloadCache;
- }
-
+ /** Returns whether extension renderers should be used. */
public boolean useExtensionRenderers() {
return "withExtensions".equals(BuildConfig.FLAVOR);
}
- private static CacheDataSourceFactory createReadOnlyCacheDataSource(
+ public DownloadManager getDownloadManager() {
+ initDownloadManager();
+ return downloadManager;
+ }
+
+ public DownloadTracker getDownloadTracker() {
+ initDownloadManager();
+ return downloadTracker;
+ }
+
+ private synchronized void initDownloadManager() {
+ if (downloadManager == null) {
+ DownloaderConstructorHelper downloaderConstructorHelper =
+ new DownloaderConstructorHelper(
+ getDownloadCache(), buildHttpDataSourceFactory(/* listener= */ null));
+ downloadManager =
+ new DownloadManager(
+ downloaderConstructorHelper,
+ MAX_SIMULTANEOUS_DOWNLOADS,
+ DownloadManager.DEFAULT_MIN_RETRY_COUNT,
+ new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE),
+ DOWNLOAD_DESERIALIZERS);
+ downloadTracker =
+ new DownloadTracker(
+ /* context= */ this,
+ buildDataSourceFactory(/* listener= */ null),
+ new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE),
+ DOWNLOAD_DESERIALIZERS);
+ downloadManager.addListener(downloadTracker);
+ }
+ }
+
+ private synchronized Cache getDownloadCache() {
+ if (downloadCache == null) {
+ File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY);
+ downloadCache = new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor());
+ }
+ return downloadCache;
+ }
+
+ private File getDownloadDirectory() {
+ if (downloadDirectory == null) {
+ downloadDirectory = getExternalFilesDir(null);
+ if (downloadDirectory == null) {
+ downloadDirectory = getFilesDir();
+ }
+ }
+ return downloadDirectory;
+ }
+
+ private static CacheDataSourceFactory buildReadOnlyCacheDataSource(
DefaultDataSourceFactory upstreamFactory, Cache cache) {
return new CacheDataSourceFactory(
cache,
upstreamFactory,
new FileDataSourceFactory(),
- /*cacheWriteDataSinkFactory=*/ null,
+ /* cacheWriteDataSinkFactory= */ null,
CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
- /*eventListener=*/ null);
+ /* eventListener= */ null);
}
}
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java
new file mode 100644
index 0000000000..7d1ab16ce4
--- /dev/null
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2017 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.demo;
+
+import android.app.Notification;
+import com.google.android.exoplayer2.offline.DownloadManager;
+import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
+import com.google.android.exoplayer2.offline.DownloadService;
+import com.google.android.exoplayer2.scheduler.PlatformScheduler;
+import com.google.android.exoplayer2.ui.DownloadNotificationUtil;
+import com.google.android.exoplayer2.util.NotificationUtil;
+import com.google.android.exoplayer2.util.Util;
+
+/** A service for downloading media. */
+public class DemoDownloadService extends DownloadService {
+
+ private static final String CHANNEL_ID = "download_channel";
+ private static final int JOB_ID = 1;
+ private static final int FOREGROUND_NOTIFICATION_ID = 1;
+
+ public DemoDownloadService() {
+ super(
+ FOREGROUND_NOTIFICATION_ID,
+ DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
+ CHANNEL_ID,
+ R.string.exo_download_notification_channel_name);
+ }
+
+ @Override
+ protected DownloadManager getDownloadManager() {
+ return ((DemoApplication) getApplication()).getDownloadManager();
+ }
+
+ @Override
+ protected PlatformScheduler getScheduler() {
+ return Util.SDK_INT >= 21 ? new PlatformScheduler(this, JOB_ID) : null;
+ }
+
+ @Override
+ protected Notification getForegroundNotification(TaskState[] taskStates) {
+ return DownloadNotificationUtil.buildProgressNotification(
+ /* context= */ this,
+ R.drawable.exo_controls_play,
+ CHANNEL_ID,
+ /* contentIntent= */ null,
+ /* message= */ null,
+ taskStates);
+ }
+
+ @Override
+ protected void onTaskStateChanged(TaskState taskState) {
+ if (taskState.action.isRemoveAction) {
+ return;
+ }
+ Notification notification = null;
+ if (taskState.state == TaskState.STATE_COMPLETED) {
+ notification =
+ DownloadNotificationUtil.buildDownloadCompletedNotification(
+ /* context= */ this,
+ R.drawable.exo_controls_play,
+ CHANNEL_ID,
+ /* contentIntent= */ null,
+ Util.fromUtf8Bytes(taskState.action.data));
+ } else if (taskState.state == TaskState.STATE_FAILED) {
+ notification =
+ DownloadNotificationUtil.buildDownloadFailedNotification(
+ /* context= */ this,
+ R.drawable.exo_controls_play,
+ CHANNEL_ID,
+ /* contentIntent= */ null,
+ Util.fromUtf8Bytes(taskState.action.data));
+ }
+ int notificationId = FOREGROUND_NOTIFICATION_ID + 1 + taskState.taskId;
+ NotificationUtil.setNotification(this, notificationId, notification);
+ }
+}
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java
deleted file mode 100644
index 2692bc4531..0000000000
--- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2017 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.demo;
-
-import android.text.TextUtils;
-import com.google.android.exoplayer2.Format;
-import com.google.android.exoplayer2.util.MimeTypes;
-import java.util.Locale;
-
-/**
- * Utility methods for demo application.
- */
-/* package */ final class DemoUtil {
-
- /**
- * Builds a track name for display.
- *
- * @param format {@link Format} of the track.
- * @return a generated name specific to the track.
- */
- public static String buildTrackName(Format format) {
- String trackName;
- if (MimeTypes.isVideo(format.sampleMimeType)) {
- trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(
- buildResolutionString(format), buildBitrateString(format)), buildTrackIdString(format)),
- buildSampleMimeTypeString(format));
- } else if (MimeTypes.isAudio(format.sampleMimeType)) {
- trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(joinWithSeparator(
- buildLanguageString(format), buildAudioPropertyString(format)),
- buildBitrateString(format)), buildTrackIdString(format)),
- buildSampleMimeTypeString(format));
- } else {
- trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format),
- buildBitrateString(format)), buildTrackIdString(format)),
- buildSampleMimeTypeString(format));
- }
- return trackName.length() == 0 ? "unknown" : trackName;
- }
-
- private static String buildResolutionString(Format format) {
- return format.width == Format.NO_VALUE || format.height == Format.NO_VALUE
- ? "" : format.width + "x" + format.height;
- }
-
- private static String buildAudioPropertyString(Format format) {
- return format.channelCount == Format.NO_VALUE || format.sampleRate == Format.NO_VALUE
- ? "" : format.channelCount + "ch, " + format.sampleRate + "Hz";
- }
-
- private static String buildLanguageString(Format format) {
- return TextUtils.isEmpty(format.language) || "und".equals(format.language) ? ""
- : format.language;
- }
-
- private static String buildBitrateString(Format format) {
- return format.bitrate == Format.NO_VALUE ? ""
- : String.format(Locale.US, "%.2fMbit", format.bitrate / 1000000f);
- }
-
- private static String joinWithSeparator(String first, String second) {
- return first.length() == 0 ? second : (second.length() == 0 ? first : first + ", " + second);
- }
-
- private static String buildTrackIdString(Format format) {
- return format.id == null ? "" : ("id:" + format.id);
- }
-
- private static String buildSampleMimeTypeString(Format format) {
- return format.sampleMimeType == null ? "" : format.sampleMimeType;
- }
-
- private DemoUtil() {}
-}
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java
new file mode 100644
index 0000000000..b4bce01c7a
--- /dev/null
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2017 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.demo;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.Toast;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.offline.ActionFile;
+import com.google.android.exoplayer2.offline.DownloadAction;
+import com.google.android.exoplayer2.offline.DownloadHelper;
+import com.google.android.exoplayer2.offline.DownloadManager;
+import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
+import com.google.android.exoplayer2.offline.DownloadService;
+import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper;
+import com.google.android.exoplayer2.offline.SegmentDownloadAction;
+import com.google.android.exoplayer2.offline.TrackKey;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.source.dash.offline.DashDownloadHelper;
+import com.google.android.exoplayer2.source.hls.offline.HlsDownloadHelper;
+import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadHelper;
+import com.google.android.exoplayer2.ui.DefaultTrackNameProvider;
+import com.google.android.exoplayer2.ui.TrackNameProvider;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.util.Util;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * Tracks media that has been downloaded.
+ *
+ *
Tracked downloads are persisted using an {@link ActionFile}, however in a real application
+ * it's expected that state will be stored directly in the application's media database, so that it
+ * can be queried efficiently together with other information about the media.
+ */
+public class DownloadTracker implements DownloadManager.Listener {
+
+ /** Listens for changes in the tracked downloads. */
+ public interface Listener {
+
+ /** Called when the tracked downloads changed. */
+ void onDownloadsChanged();
+ }
+
+ private static final String TAG = "DownloadTracker";
+
+ private final Context context;
+ private final DataSource.Factory dataSourceFactory;
+ private final TrackNameProvider trackNameProvider;
+ private final CopyOnWriteArraySet listeners;
+ private final HashMap trackedDownloadStates;
+ private final ActionFile actionFile;
+ private final Handler actionFileWriteHandler;
+
+ public DownloadTracker(
+ Context context,
+ DataSource.Factory dataSourceFactory,
+ File actionFile,
+ DownloadAction.Deserializer[] deserializers) {
+ this.context = context.getApplicationContext();
+ this.dataSourceFactory = dataSourceFactory;
+ this.actionFile = new ActionFile(actionFile);
+ trackNameProvider = new DefaultTrackNameProvider(context.getResources());
+ listeners = new CopyOnWriteArraySet<>();
+ trackedDownloadStates = new HashMap<>();
+ HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker");
+ actionFileWriteThread.start();
+ actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper());
+ loadTrackedActions(deserializers);
+ }
+
+ public void addListener(Listener listener) {
+ listeners.add(listener);
+ }
+
+ public void removeListener(Listener listener) {
+ listeners.remove(listener);
+ }
+
+ public boolean isDownloaded(Uri uri) {
+ return trackedDownloadStates.containsKey(uri);
+ }
+
+ @SuppressWarnings("unchecked")
+ public List getOfflineStreamKeys(Uri uri) {
+ if (!trackedDownloadStates.containsKey(uri)) {
+ return Collections.emptyList();
+ }
+ DownloadAction action = trackedDownloadStates.get(uri);
+ if (action instanceof SegmentDownloadAction) {
+ return ((SegmentDownloadAction) action).keys;
+ }
+ return Collections.emptyList();
+ }
+
+ public void toggleDownload(Activity activity, String name, Uri uri, String extension) {
+ if (isDownloaded(uri)) {
+ DownloadAction removeAction =
+ getDownloadHelper(uri, extension).getRemoveAction(Util.getUtf8Bytes(name));
+ startServiceWithAction(removeAction);
+ } else {
+ StartDownloadDialogHelper helper =
+ new StartDownloadDialogHelper(activity, getDownloadHelper(uri, extension), name);
+ helper.prepare();
+ }
+ }
+
+ // DownloadManager.Listener
+
+ @Override
+ public void onInitialized(DownloadManager downloadManager) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) {
+ DownloadAction action = taskState.action;
+ Uri uri = action.uri;
+ if ((action.isRemoveAction && taskState.state == TaskState.STATE_COMPLETED)
+ || (!action.isRemoveAction && taskState.state == TaskState.STATE_FAILED)) {
+ // A download has been removed, or has failed. Stop tracking it.
+ if (trackedDownloadStates.remove(uri) != null) {
+ handleTrackedDownloadStatesChanged();
+ }
+ }
+ }
+
+ @Override
+ public void onIdle(DownloadManager downloadManager) {
+ // Do nothing.
+ }
+
+ // Internal methods
+
+ private void loadTrackedActions(DownloadAction.Deserializer[] deserializers) {
+ try {
+ DownloadAction[] allActions = actionFile.load(deserializers);
+ for (DownloadAction action : allActions) {
+ trackedDownloadStates.put(action.uri, action);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to load tracked actions", e);
+ }
+ }
+
+ private void handleTrackedDownloadStatesChanged() {
+ for (Listener listener : listeners) {
+ listener.onDownloadsChanged();
+ }
+ final DownloadAction[] actions = trackedDownloadStates.values().toArray(new DownloadAction[0]);
+ actionFileWriteHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ try {
+ actionFile.store(actions);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to store tracked actions", e);
+ }
+ }
+ });
+ }
+
+ private void startDownload(DownloadAction action) {
+ if (trackedDownloadStates.containsKey(action.uri)) {
+ // This content is already being downloaded. Do nothing.
+ return;
+ }
+ trackedDownloadStates.put(action.uri, action);
+ handleTrackedDownloadStatesChanged();
+ startServiceWithAction(action);
+ }
+
+ private void startServiceWithAction(DownloadAction action) {
+ DownloadService.startWithAction(context, DemoDownloadService.class, action, false);
+ }
+
+ private DownloadHelper getDownloadHelper(Uri uri, String extension) {
+ int type = Util.inferContentType(uri, extension);
+ switch (type) {
+ case C.TYPE_DASH:
+ return new DashDownloadHelper(uri, dataSourceFactory);
+ case C.TYPE_SS:
+ return new SsDownloadHelper(uri, dataSourceFactory);
+ case C.TYPE_HLS:
+ return new HlsDownloadHelper(uri, dataSourceFactory);
+ case C.TYPE_OTHER:
+ return new ProgressiveDownloadHelper(uri);
+ default:
+ throw new IllegalStateException("Unsupported type: " + type);
+ }
+ }
+
+ private final class StartDownloadDialogHelper
+ implements DownloadHelper.Callback, DialogInterface.OnClickListener {
+
+ private final DownloadHelper downloadHelper;
+ private final String name;
+
+ private final AlertDialog.Builder builder;
+ private final View dialogView;
+ private final List trackKeys;
+ private final ArrayAdapter trackTitles;
+ private final ListView representationList;
+
+ public StartDownloadDialogHelper(
+ Activity activity, DownloadHelper downloadHelper, String name) {
+ this.downloadHelper = downloadHelper;
+ this.name = name;
+ builder =
+ new AlertDialog.Builder(activity)
+ .setTitle(R.string.exo_download_description)
+ .setPositiveButton(android.R.string.ok, this)
+ .setNegativeButton(android.R.string.cancel, null);
+
+ // Inflate with the builder's context to ensure the correct style is used.
+ LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
+ dialogView = dialogInflater.inflate(R.layout.start_download_dialog, null);
+
+ trackKeys = new ArrayList<>();
+ trackTitles =
+ new ArrayAdapter<>(
+ builder.getContext(), android.R.layout.simple_list_item_multiple_choice);
+ representationList = dialogView.findViewById(R.id.representation_list);
+ representationList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+ representationList.setAdapter(trackTitles);
+ }
+
+ public void prepare() {
+ downloadHelper.prepare(this);
+ }
+
+ @Override
+ public void onPrepared(DownloadHelper helper) {
+ for (int i = 0; i < downloadHelper.getPeriodCount(); i++) {
+ TrackGroupArray trackGroups = downloadHelper.getTrackGroups(i);
+ for (int j = 0; j < trackGroups.length; j++) {
+ TrackGroup trackGroup = trackGroups.get(j);
+ for (int k = 0; k < trackGroup.length; k++) {
+ trackKeys.add(new TrackKey(i, j, k));
+ trackTitles.add(trackNameProvider.getTrackName(trackGroup.getFormat(k)));
+ }
+ }
+ if (!trackKeys.isEmpty()) {
+ builder.setView(dialogView);
+ }
+ builder.create().show();
+ }
+ }
+
+ @Override
+ public void onPrepareError(DownloadHelper helper, IOException e) {
+ Toast.makeText(
+ context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG)
+ .show();
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ ArrayList selectedTrackKeys = new ArrayList<>();
+ for (int i = 0; i < representationList.getChildCount(); i++) {
+ if (representationList.isItemChecked(i)) {
+ selectedTrackKeys.add(trackKeys.get(i));
+ }
+ }
+ if (!selectedTrackKeys.isEmpty() || trackKeys.isEmpty()) {
+ // We have selected keys, or we're dealing with single stream content.
+ DownloadAction downloadAction =
+ downloadHelper.getDownloadAction(Util.getUtf8Bytes(name), selectedTrackKeys);
+ startDownload(downloadAction);
+ }
+ }
+ }
+}
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
index 4765278fcb..091e483155 100644
--- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
@@ -16,14 +16,14 @@
package com.google.android.exoplayer2.demo;
import android.app.Activity;
+import android.app.AlertDialog;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.util.Pair;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
@@ -48,6 +48,7 @@ import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
+import com.google.android.exoplayer2.offline.FilteringManifestParser;
import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
@@ -57,14 +58,15 @@ import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
-import com.google.android.exoplayer2.source.dash.manifest.FilteringDashManifestParser;
+import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
-import com.google.android.exoplayer2.source.hls.playlist.FilteringHlsPlaylistParser;
+import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
+import com.google.android.exoplayer2.source.hls.playlist.RenditionKey;
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
-import com.google.android.exoplayer2.source.smoothstreaming.manifest.FilteringSsManifestParser;
-import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey;
+import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
+import com.google.android.exoplayer2.source.smoothstreaming.manifest.StreamKey;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
@@ -74,11 +76,12 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.DebugTextViewHelper;
import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView;
+import com.google.android.exoplayer2.ui.TrackSelectionView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.HttpDataSource;
+import com.google.android.exoplayer2.util.ErrorMessageProvider;
import com.google.android.exoplayer2.util.EventLogger;
-import com.google.android.exoplayer2.util.ParcelableArray;
import com.google.android.exoplayer2.util.Util;
import java.lang.reflect.Constructor;
import java.net.CookieHandler;
@@ -99,13 +102,11 @@ public class PlayerActivity extends Activity
public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
public static final String EXTENSION_EXTRA = "extension";
- public static final String MANIFEST_FILTER_EXTRA = "manifest_filter";
public static final String ACTION_VIEW_LIST =
"com.google.android.exoplayer.demo.action.VIEW_LIST";
public static final String URI_LIST_EXTRA = "uri_list";
public static final String EXTENSION_LIST_EXTRA = "extension_list";
- public static final String MANIFEST_FILTER_LIST_EXTRA = "manifest_filter_list";
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
@@ -116,6 +117,12 @@ public class PlayerActivity extends Activity
// For backwards compatibility only.
private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
+ // Saved instance state keys.
+ private static final String KEY_TRACK_SELECTOR_PARAMETERS = "track_selector_parameters";
+ private static final String KEY_WINDOW = "window";
+ private static final String KEY_POSITION = "position";
+ private static final String KEY_AUTO_PLAY = "auto_play";
+
private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
private static final CookieManager DEFAULT_COOKIE_MANAGER;
static {
@@ -123,7 +130,6 @@ public class PlayerActivity extends Activity
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
}
- private Handler mainHandler;
private PlayerView playerView;
private LinearLayout debugRootView;
private TextView debugTextView;
@@ -132,14 +138,13 @@ public class PlayerActivity extends Activity
private SimpleExoPlayer player;
private MediaSource mediaSource;
private DefaultTrackSelector trackSelector;
- private TrackSelectionHelper trackSelectionHelper;
+ private DefaultTrackSelector.Parameters trackSelectorParameters;
private DebugTextViewHelper debugViewHelper;
- private boolean inErrorState;
private TrackGroupArray lastSeenTrackGroupArray;
- private boolean shouldAutoPlay;
- private int resumeWindow;
- private long resumePosition;
+ private boolean startAutoPlay;
+ private int startWindow;
+ private long startPosition;
// Fields used only for ad playback. The ads loader is loaded via reflection.
@@ -152,10 +157,7 @@ public class PlayerActivity extends Activity
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- shouldAutoPlay = true;
- clearResumePosition();
mediaDataSourceFactory = buildDataSourceFactory(true);
- mainHandler = new Handler();
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
}
@@ -168,14 +170,24 @@ public class PlayerActivity extends Activity
playerView = findViewById(R.id.player_view);
playerView.setControllerVisibilityListener(this);
+ playerView.setErrorMessageProvider(new PlayerErrorMessageProvider());
playerView.requestFocus();
+
+ if (savedInstanceState != null) {
+ trackSelectorParameters = savedInstanceState.getParcelable(KEY_TRACK_SELECTOR_PARAMETERS);
+ startAutoPlay = savedInstanceState.getBoolean(KEY_AUTO_PLAY);
+ startWindow = savedInstanceState.getInt(KEY_WINDOW);
+ startPosition = savedInstanceState.getLong(KEY_POSITION);
+ } else {
+ trackSelectorParameters = new DefaultTrackSelector.ParametersBuilder().build();
+ clearStartPosition();
+ }
}
@Override
public void onNewIntent(Intent intent) {
releasePlayer();
- shouldAutoPlay = true;
- clearResumePosition();
+ clearStartPosition();
setIntent(intent);
}
@@ -220,17 +232,27 @@ public class PlayerActivity extends Activity
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
- if (grantResults.length > 0) {
- if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- initializePlayer();
- } else {
- showToast(R.string.storage_permission_denied);
- finish();
- }
- } else {
+ if (grantResults.length == 0) {
// Empty results are triggered if a permission is requested while another request was already
// pending and can be safely ignored in this case.
+ return;
}
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ initializePlayer();
+ } else {
+ showToast(R.string.storage_permission_denied);
+ finish();
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ updateTrackSelectorParameters();
+ updateStartPosition();
+ outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters);
+ outState.putBoolean(KEY_AUTO_PLAY, startAutoPlay);
+ outState.putInt(KEY_WINDOW, startWindow);
+ outState.putLong(KEY_POSITION, startPosition);
}
// Activity input
@@ -248,8 +270,19 @@ public class PlayerActivity extends Activity
if (view.getParent() == debugRootView) {
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo != null) {
- trackSelectionHelper.showSelectionDialog(
- this, ((Button) view).getText(), mappedTrackInfo, (int) view.getTag());
+ CharSequence title = ((Button) view).getText();
+ int rendererIndex = (int) view.getTag();
+ int rendererType = mappedTrackInfo.getRendererType(rendererIndex);
+ boolean allowAdaptiveSelections =
+ rendererType == C.TRACK_TYPE_VIDEO
+ || (rendererType == C.TRACK_TYPE_AUDIO
+ && mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
+ == MappedTrackInfo.RENDERER_SUPPORT_NO_TRACKS);
+ Pair dialogPair =
+ TrackSelectionView.getDialog(this, title, trackSelector, rendererIndex);
+ dialogPair.second.setShowDisableOption(true);
+ dialogPair.second.setAllowAdaptiveSelections(allowAdaptiveSelections);
+ dialogPair.first.show();
}
}
}
@@ -276,11 +309,9 @@ public class PlayerActivity extends Activity
String action = intent.getAction();
Uri[] uris;
String[] extensions;
- Parcelable[] manifestFilters;
if (ACTION_VIEW.equals(action)) {
uris = new Uri[] {intent.getData()};
extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)};
- manifestFilters = new Parcelable[] {intent.getParcelableExtra(MANIFEST_FILTER_EXTRA)};
} else if (ACTION_VIEW_LIST.equals(action)) {
String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA);
uris = new Uri[uriStrings.length];
@@ -291,10 +322,6 @@ public class PlayerActivity extends Activity
if (extensions == null) {
extensions = new String[uriStrings.length];
}
- manifestFilters = intent.getParcelableArrayExtra(MANIFEST_FILTER_LIST_EXTRA);
- if (manifestFilters == null) {
- manifestFilters = new Parcelable[uriStrings.length];
- }
} else {
showToast(getString(R.string.unexpected_intent_action, action));
finish();
@@ -361,23 +388,14 @@ public class PlayerActivity extends Activity
new DefaultRenderersFactory(this, extensionRendererMode);
trackSelector = new DefaultTrackSelector(trackSelectionFactory);
- trackSelectionHelper = new TrackSelectionHelper(trackSelector, trackSelectionFactory);
+ trackSelector.setParameters(trackSelectorParameters);
lastSeenTrackGroupArray = null;
player =
ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, drmSessionManager);
player.addListener(new PlayerEventListener());
-
- EventLogger eventLogger = new EventLogger(trackSelector);
- player.addListener(eventLogger);
- player.addMetadataOutput(eventLogger);
- player.addAudioDebugListener(eventLogger);
- player.addVideoDebugListener(eventLogger);
- if (drmSessionManager != null) {
- drmSessionManager.addListener(mainHandler, eventLogger);
- }
-
- player.setPlayWhenReady(shouldAutoPlay);
+ player.setPlayWhenReady(startAutoPlay);
+ player.addAnalyticsListener(new EventLogger(trackSelector));
playerView.setPlayer(player);
playerView.setPlaybackPreparer(this);
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
@@ -385,9 +403,7 @@ public class PlayerActivity extends Activity
MediaSource[] mediaSources = new MediaSource[uris.length];
for (int i = 0; i < uris.length; i++) {
- ParcelableArray> manifestFilter = (ParcelableArray>) manifestFilters[i];
- List> filter = manifestFilter != null ? manifestFilter.asList() : null;
- mediaSources[i] = buildMediaSource(uris[i], extensions[i], filter);
+ mediaSources[i] = buildMediaSource(uris[i], extensions[i]);
}
mediaSource =
mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources);
@@ -398,8 +414,7 @@ public class PlayerActivity extends Activity
releaseAdsLoader();
loadedAdTagUri = adTagUri;
}
- MediaSource adsMediaSource =
- createAdsMediaSource(mediaSource, Uri.parse(adTagUriString), eventLogger);
+ MediaSource adsMediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString));
if (adsMediaSource != null) {
mediaSource = adsMediaSource;
} else {
@@ -408,24 +423,21 @@ public class PlayerActivity extends Activity
} else {
releaseAdsLoader();
}
- mediaSource.addEventListener(mainHandler, eventLogger);
}
- boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
- if (haveResumePosition) {
- player.seekTo(resumeWindow, resumePosition);
+ boolean haveStartPosition = startWindow != C.INDEX_UNSET;
+ if (haveStartPosition) {
+ player.seekTo(startWindow, startPosition);
}
- player.prepare(mediaSource, !haveResumePosition, false);
- inErrorState = false;
+ player.prepare(mediaSource, !haveStartPosition, false);
updateButtonVisibilities();
}
private MediaSource buildMediaSource(Uri uri) {
- return buildMediaSource(uri, null, null);
+ return buildMediaSource(uri, null);
}
@SuppressWarnings("unchecked")
- private MediaSource buildMediaSource(
- Uri uri, @Nullable String overrideExtension, @Nullable List> manifestFilter) {
+ private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
@ContentType int type = Util.inferContentType(uri, overrideExtension);
switch (type) {
case C.TYPE_DASH:
@@ -433,17 +445,22 @@ public class PlayerActivity extends Activity
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
buildDataSourceFactory(false))
.setManifestParser(
- new FilteringDashManifestParser((List) manifestFilter))
+ new FilteringManifestParser<>(
+ new DashManifestParser(), (List) getOfflineStreamKeys(uri)))
.createMediaSource(uri);
case C.TYPE_SS:
return new SsMediaSource.Factory(
new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
buildDataSourceFactory(false))
- .setManifestParser(new FilteringSsManifestParser((List) manifestFilter))
+ .setManifestParser(
+ new FilteringManifestParser<>(
+ new SsManifestParser(), (List) getOfflineStreamKeys(uri)))
.createMediaSource(uri);
case C.TYPE_HLS:
return new HlsMediaSource.Factory(mediaDataSourceFactory)
- .setPlaylistParser(new FilteringHlsPlaylistParser((List) manifestFilter))
+ .setPlaylistParser(
+ new FilteringManifestParser<>(
+ new HlsPlaylistParser(), (List) getOfflineStreamKeys(uri)))
.createMediaSource(uri);
case C.TYPE_OTHER:
return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
@@ -453,45 +470,58 @@ public class PlayerActivity extends Activity
}
}
+ private List> getOfflineStreamKeys(Uri uri) {
+ return ((DemoApplication) getApplication()).getDownloadTracker().getOfflineStreamKeys(uri);
+ }
+
private DefaultDrmSessionManager buildDrmSessionManagerV18(
UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)
throws UnsupportedDrmException {
- HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl,
- buildHttpDataSourceFactory(false));
+ HttpDataSource.Factory licenseDataSourceFactory =
+ ((DemoApplication) getApplication()).buildHttpDataSourceFactory(/* listener= */ null);
+ HttpMediaDrmCallback drmCallback =
+ new HttpMediaDrmCallback(licenseUrl, licenseDataSourceFactory);
if (keyRequestPropertiesArray != null) {
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i],
keyRequestPropertiesArray[i + 1]);
}
}
- DefaultDrmSessionManager drmSessionManager =
- new DefaultDrmSessionManager<>(
- uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, null, multiSession);
- return drmSessionManager;
+ return new DefaultDrmSessionManager<>(
+ uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, null, multiSession);
}
private void releasePlayer() {
if (player != null) {
+ updateTrackSelectorParameters();
+ updateStartPosition();
debugViewHelper.stop();
debugViewHelper = null;
- shouldAutoPlay = player.getPlayWhenReady();
- updateResumePosition();
player.release();
player = null;
mediaSource = null;
trackSelector = null;
- trackSelectionHelper = null;
}
}
- private void updateResumePosition() {
- resumeWindow = player.getCurrentWindowIndex();
- resumePosition = Math.max(0, player.getContentPosition());
+ private void updateTrackSelectorParameters() {
+ if (trackSelector != null) {
+ trackSelectorParameters = trackSelector.getParameters();
+ }
}
- private void clearResumePosition() {
- resumeWindow = C.INDEX_UNSET;
- resumePosition = C.TIME_UNSET;
+ private void updateStartPosition() {
+ if (player != null) {
+ startAutoPlay = player.getPlayWhenReady();
+ startWindow = player.getCurrentWindowIndex();
+ startPosition = Math.max(0, player.getContentPosition());
+ }
+ }
+
+ private void clearStartPosition() {
+ startAutoPlay = true;
+ startWindow = C.INDEX_UNSET;
+ startPosition = C.TIME_UNSET;
}
/**
@@ -506,21 +536,8 @@ public class PlayerActivity extends Activity
.buildDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
}
- /**
- * Returns a new HttpDataSource factory.
- *
- * @param useBandwidthMeter Whether to set {@link #BANDWIDTH_METER} as a listener to the new
- * DataSource factory.
- * @return A new HttpDataSource factory.
- */
- private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) {
- return ((DemoApplication) getApplication())
- .buildHttpDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
- }
-
/** Returns an ads media source, reusing the ads loader if one exists. */
- private @Nullable MediaSource createAdsMediaSource(
- MediaSource mediaSource, Uri adTagUri, EventLogger eventLogger) {
+ private @Nullable MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) {
// Load the extension source using reflection so the demo app doesn't have to depend on it.
// The ads loader is reused for multiple playbacks, so that ad playback can resume.
try {
@@ -550,8 +567,7 @@ public class PlayerActivity extends Activity
return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER};
}
};
- return new AdsMediaSource(
- mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup, mainHandler, eventLogger);
+ return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup);
} catch (ClassNotFoundException e) {
// IMA extension not loaded.
return null;
@@ -582,20 +598,20 @@ public class PlayerActivity extends Activity
return;
}
- for (int i = 0; i < mappedTrackInfo.length; i++) {
+ for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(i);
if (trackGroups.length != 0) {
Button button = new Button(this);
int label;
switch (player.getRendererType(i)) {
case C.TRACK_TYPE_AUDIO:
- label = R.string.audio;
+ label = R.string.exo_track_selection_title_audio;
break;
case C.TRACK_TYPE_VIDEO:
- label = R.string.video;
+ label = R.string.exo_track_selection_title_video;
break;
case C.TRACK_TYPE_TEXT:
- label = R.string.text;
+ label = R.string.exo_track_selection_title_text;
break;
default:
continue;
@@ -646,48 +662,20 @@ public class PlayerActivity extends Activity
@Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
- if (inErrorState) {
- // This will only occur if the user has performed a seek whilst in the error state. Update
- // the resume position so that if the user then retries, playback will resume from the
- // position to which they seeked.
- updateResumePosition();
+ if (player.getPlaybackError() != null) {
+ // The user has performed a seek whilst in the error state. Update the resume position so
+ // that if the user then retries, playback resumes from the position to which they seeked.
+ updateStartPosition();
}
}
@Override
public void onPlayerError(ExoPlaybackException e) {
- String errorString = null;
- if (e.type == ExoPlaybackException.TYPE_RENDERER) {
- Exception cause = e.getRendererException();
- if (cause instanceof DecoderInitializationException) {
- // Special case for decoder initialization failures.
- DecoderInitializationException decoderInitializationException =
- (DecoderInitializationException) cause;
- if (decoderInitializationException.decoderName == null) {
- if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
- errorString = getString(R.string.error_querying_decoders);
- } else if (decoderInitializationException.secureDecoderRequired) {
- errorString = getString(R.string.error_no_secure_decoder,
- decoderInitializationException.mimeType);
- } else {
- errorString = getString(R.string.error_no_decoder,
- decoderInitializationException.mimeType);
- }
- } else {
- errorString = getString(R.string.error_instantiating_decoder,
- decoderInitializationException.decoderName);
- }
- }
- }
- if (errorString != null) {
- showToast(errorString);
- }
- inErrorState = true;
if (isBehindLiveWindow(e)) {
- clearResumePosition();
+ clearStartPosition();
initializePlayer();
} else {
- updateResumePosition();
+ updateStartPosition();
updateButtonVisibilities();
showControls();
}
@@ -700,11 +688,11 @@ public class PlayerActivity extends Activity
if (trackGroups != lastSeenTrackGroupArray) {
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo != null) {
- if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_VIDEO)
+ if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
showToast(R.string.error_unsupported_video);
}
- if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_AUDIO)
+ if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO)
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
showToast(R.string.error_unsupported_audio);
}
@@ -712,7 +700,40 @@ public class PlayerActivity extends Activity
lastSeenTrackGroupArray = trackGroups;
}
}
+ }
+ private class PlayerErrorMessageProvider implements ErrorMessageProvider {
+
+ @Override
+ public Pair getErrorMessage(ExoPlaybackException e) {
+ String errorString = getString(R.string.error_generic);
+ if (e.type == ExoPlaybackException.TYPE_RENDERER) {
+ Exception cause = e.getRendererException();
+ if (cause instanceof DecoderInitializationException) {
+ // Special case for decoder initialization failures.
+ DecoderInitializationException decoderInitializationException =
+ (DecoderInitializationException) cause;
+ if (decoderInitializationException.decoderName == null) {
+ if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
+ errorString = getString(R.string.error_querying_decoders);
+ } else if (decoderInitializationException.secureDecoderRequired) {
+ errorString =
+ getString(
+ R.string.error_no_secure_decoder, decoderInitializationException.mimeType);
+ } else {
+ errorString =
+ getString(R.string.error_no_decoder, decoderInitializationException.mimeType);
+ }
+ } else {
+ errorString =
+ getString(
+ R.string.error_instantiating_decoder,
+ decoderInitializationException.decoderName);
+ }
+ }
+ }
+ return Pair.create(0, errorString);
+ }
}
}
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
index 5febb949f1..5524f98257 100644
--- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
@@ -24,15 +24,17 @@ import android.os.AsyncTask;
import android.os.Bundle;
import android.util.JsonReader;
import android.util.Log;
-import android.view.LayoutInflater;
import android.view.View;
+import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.ExpandableListView;
import android.widget.ExpandableListView.OnChildClickListener;
+import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
import com.google.android.exoplayer2.upstream.DataSpec;
@@ -44,19 +46,27 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
-/**
- * An activity for selecting from a list of samples.
- */
-public class SampleChooserActivity extends Activity {
+/** An activity for selecting from a list of media samples. */
+public class SampleChooserActivity extends Activity
+ implements DownloadTracker.Listener, OnChildClickListener {
private static final String TAG = "SampleChooserActivity";
+ private DownloadTracker downloadTracker;
+ private SampleAdapter sampleAdapter;
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.sample_chooser_activity);
+ sampleAdapter = new SampleAdapter();
+ ExpandableListView sampleListView = findViewById(R.id.sample_list);
+ sampleListView.setAdapter(sampleAdapter);
+ sampleListView.setOnChildClickListener(this);
+
Intent intent = getIntent();
String dataUri = intent.getDataString();
String[] uris;
@@ -79,8 +89,32 @@ public class SampleChooserActivity extends Activity {
uriList.toArray(uris);
Arrays.sort(uris);
}
+
+ downloadTracker = ((DemoApplication) getApplication()).getDownloadTracker();
SampleListLoader loaderTask = new SampleListLoader();
loaderTask.execute(uris);
+
+ // Ping the download service in case it's not running (but should be).
+ startService(
+ new Intent(this, DemoDownloadService.class).setAction(DownloadService.ACTION_INIT));
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ downloadTracker.addListener(this);
+ sampleAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onStop() {
+ downloadTracker.removeListener(this);
+ super.onStop();
+ }
+
+ @Override
+ public void onDownloadsChanged() {
+ sampleAdapter.notifyDataSetChanged();
}
private void onSampleGroups(final List groups, boolean sawError) {
@@ -88,20 +122,44 @@ public class SampleChooserActivity extends Activity {
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
.show();
}
- ExpandableListView sampleList = findViewById(R.id.sample_list);
- sampleList.setAdapter(new SampleAdapter(this, groups));
- sampleList.setOnChildClickListener(new OnChildClickListener() {
- @Override
- public boolean onChildClick(ExpandableListView parent, View view, int groupPosition,
- int childPosition, long id) {
- onSampleSelected(groups.get(groupPosition).samples.get(childPosition));
- return true;
- }
- });
+ sampleAdapter.setSampleGroups(groups);
}
- private void onSampleSelected(Sample sample) {
+ @Override
+ public boolean onChildClick(
+ ExpandableListView parent, View view, int groupPosition, int childPosition, long id) {
+ Sample sample = (Sample) view.getTag();
startActivity(sample.buildIntent(this));
+ return true;
+ }
+
+ private void onSampleDownloadButtonClicked(Sample sample) {
+ int downloadUnsupportedStringId = getDownloadUnsupportedStringId(sample);
+ if (downloadUnsupportedStringId != 0) {
+ Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG)
+ .show();
+ } else {
+ UriSample uriSample = (UriSample) sample;
+ downloadTracker.toggleDownload(this, sample.name, uriSample.uri, uriSample.extension);
+ }
+ }
+
+ private int getDownloadUnsupportedStringId(Sample sample) {
+ if (sample instanceof PlaylistSample) {
+ return R.string.download_playlist_unsupported;
+ }
+ UriSample uriSample = (UriSample) sample;
+ if (uriSample.drmInfo != null) {
+ return R.string.download_drm_unsupported;
+ }
+ if (uriSample.adTagUri != null) {
+ return R.string.download_ads_unsupported;
+ }
+ String scheme = uriSample.uri.getScheme();
+ if (!("http".equals(scheme) || "https".equals(scheme))) {
+ return R.string.download_scheme_unsupported;
+ }
+ return 0;
}
private final class SampleListLoader extends AsyncTask> {
@@ -175,7 +233,7 @@ public class SampleChooserActivity extends Activity {
private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException {
String sampleName = null;
- String uri = null;
+ Uri uri = null;
String extension = null;
String drmScheme = null;
String drmLicenseUrl = null;
@@ -194,7 +252,7 @@ public class SampleChooserActivity extends Activity {
sampleName = reader.nextString();
break;
case "uri":
- uri = reader.nextString();
+ uri = Uri.parse(reader.nextString());
break;
case "extension":
extension = reader.nextString();
@@ -278,14 +336,17 @@ public class SampleChooserActivity extends Activity {
}
- private static final class SampleAdapter extends BaseExpandableListAdapter {
+ private final class SampleAdapter extends BaseExpandableListAdapter implements OnClickListener {
- private final Context context;
- private final List sampleGroups;
+ private List sampleGroups;
- public SampleAdapter(Context context, List sampleGroups) {
- this.context = context;
+ public SampleAdapter() {
+ sampleGroups = Collections.emptyList();
+ }
+
+ public void setSampleGroups(List sampleGroups) {
this.sampleGroups = sampleGroups;
+ notifyDataSetChanged();
}
@Override
@@ -303,10 +364,12 @@ public class SampleChooserActivity extends Activity {
View convertView, ViewGroup parent) {
View view = convertView;
if (view == null) {
- view = LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, parent,
- false);
+ view = getLayoutInflater().inflate(R.layout.sample_list_item, parent, false);
+ View downloadButton = view.findViewById(R.id.download_button);
+ downloadButton.setOnClickListener(this);
+ downloadButton.setFocusable(false);
}
- ((TextView) view).setText(getChild(groupPosition, childPosition).name);
+ initializeChildView(view, getChild(groupPosition, childPosition));
return view;
}
@@ -330,8 +393,9 @@ public class SampleChooserActivity extends Activity {
ViewGroup parent) {
View view = convertView;
if (view == null) {
- view = LayoutInflater.from(context).inflate(android.R.layout.simple_expandable_list_item_1,
- parent, false);
+ view =
+ getLayoutInflater()
+ .inflate(android.R.layout.simple_expandable_list_item_1, parent, false);
}
((TextView) view).setText(getGroup(groupPosition).title);
return view;
@@ -352,6 +416,25 @@ public class SampleChooserActivity extends Activity {
return true;
}
+ @Override
+ public void onClick(View view) {
+ onSampleDownloadButtonClicked((Sample) view.getTag());
+ }
+
+ private void initializeChildView(View view, Sample sample) {
+ view.setTag(sample);
+ TextView sampleTitle = view.findViewById(R.id.sample_title);
+ sampleTitle.setText(sample.name);
+
+ boolean canDownload = getDownloadUnsupportedStringId(sample) == 0;
+ boolean isDownloaded = canDownload && downloadTracker.isDownloaded(((UriSample) sample).uri);
+ ImageButton downloadButton = view.findViewById(R.id.download_button);
+ downloadButton.setTag(sample);
+ downloadButton.setColorFilter(
+ canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFFEEEEEE);
+ downloadButton.setImageResource(
+ isDownloaded ? R.drawable.ic_download_done : R.drawable.ic_download);
+ }
}
private static final class SampleGroup {
@@ -420,7 +503,7 @@ public class SampleChooserActivity extends Activity {
private static final class UriSample extends Sample {
- public final String uri;
+ public final Uri uri;
public final String extension;
public final String adTagUri;
@@ -429,7 +512,7 @@ public class SampleChooserActivity extends Activity {
boolean preferExtensionDecoders,
String abrAlgorithm,
DrmInfo drmInfo,
- String uri,
+ Uri uri,
String extension,
String adTagUri) {
super(name, preferExtensionDecoders, abrAlgorithm, drmInfo);
@@ -441,7 +524,7 @@ public class SampleChooserActivity extends Activity {
@Override
public Intent buildIntent(Context context) {
return super.buildIntent(context)
- .setData(Uri.parse(uri))
+ .setData(uri)
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
.putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri)
.setAction(PlayerActivity.ACTION_VIEW);
@@ -468,7 +551,7 @@ public class SampleChooserActivity extends Activity {
String[] uris = new String[children.length];
String[] extensions = new String[children.length];
for (int i = 0; i < children.length; i++) {
- uris[i] = children[i].uri;
+ uris[i] = children[i].uri.toString();
extensions[i] = children[i].extension;
}
return super.buildIntent(context)
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java
deleted file mode 100644
index 89b008dbfe..0000000000
--- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java
+++ /dev/null
@@ -1,255 +0,0 @@
-/*
- * Copyright (C) 2016 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.demo;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.res.TypedArray;
-import android.util.Pair;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CheckedTextView;
-import com.google.android.exoplayer2.RendererCapabilities;
-import com.google.android.exoplayer2.source.TrackGroup;
-import com.google.android.exoplayer2.source.TrackGroupArray;
-import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
-import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;
-import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
-import java.util.Arrays;
-
-/**
- * Helper class for displaying track selection dialogs.
- */
-/* package */ final class TrackSelectionHelper implements View.OnClickListener,
- DialogInterface.OnClickListener {
-
- private final DefaultTrackSelector selector;
- private final TrackSelection.Factory trackSelectionFactory;
-
- private MappedTrackInfo trackInfo;
- private int rendererIndex;
- private TrackGroupArray trackGroups;
- private boolean[] trackGroupsAdaptive;
- private boolean isDisabled;
- private SelectionOverride override;
-
- private CheckedTextView disableView;
- private CheckedTextView defaultView;
- private CheckedTextView[][] trackViews;
-
- /**
- * @param selector The track selector.
- * @param trackSelectionFactory A factory for overriding {@link TrackSelection}s.
- */
- public TrackSelectionHelper(
- DefaultTrackSelector selector, TrackSelection.Factory trackSelectionFactory) {
- this.selector = selector;
- this.trackSelectionFactory = trackSelectionFactory;
- }
-
- /**
- * Shows the selection dialog for a given renderer.
- *
- * @param activity The parent activity.
- * @param title The dialog's title.
- * @param trackInfo The current track information.
- * @param rendererIndex The index of the renderer.
- */
- public void showSelectionDialog(Activity activity, CharSequence title, MappedTrackInfo trackInfo,
- int rendererIndex) {
- this.trackInfo = trackInfo;
- this.rendererIndex = rendererIndex;
-
- trackGroups = trackInfo.getTrackGroups(rendererIndex);
- trackGroupsAdaptive = new boolean[trackGroups.length];
- for (int i = 0; i < trackGroups.length; i++) {
- trackGroupsAdaptive[i] =
- trackInfo.getAdaptiveSupport(rendererIndex, i, false)
- != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED
- && trackGroups.get(i).length > 1;
- }
- isDisabled = selector.getRendererDisabled(rendererIndex);
- override = selector.getSelectionOverride(rendererIndex, trackGroups);
-
- AlertDialog.Builder builder = new AlertDialog.Builder(activity);
- builder.setTitle(title)
- .setView(buildView(builder.getContext()))
- .setPositiveButton(android.R.string.ok, this)
- .setNegativeButton(android.R.string.cancel, null)
- .create()
- .show();
- }
-
- @SuppressLint("InflateParams")
- private View buildView(Context context) {
- LayoutInflater inflater = LayoutInflater.from(context);
- View view = inflater.inflate(R.layout.track_selection_dialog, null);
- ViewGroup root = view.findViewById(R.id.root);
-
- TypedArray attributeArray = context.getTheme().obtainStyledAttributes(
- new int[] {android.R.attr.selectableItemBackground});
- int selectableItemBackgroundResourceId = attributeArray.getResourceId(0, 0);
- attributeArray.recycle();
-
- // View for disabling the renderer.
- disableView = (CheckedTextView) inflater.inflate(
- android.R.layout.simple_list_item_single_choice, root, false);
- disableView.setBackgroundResource(selectableItemBackgroundResourceId);
- disableView.setText(R.string.selection_disabled);
- disableView.setFocusable(true);
- disableView.setOnClickListener(this);
- root.addView(disableView);
-
- // View for clearing the override to allow the selector to use its default selection logic.
- defaultView = (CheckedTextView) inflater.inflate(
- android.R.layout.simple_list_item_single_choice, root, false);
- defaultView.setBackgroundResource(selectableItemBackgroundResourceId);
- defaultView.setText(R.string.selection_default);
- defaultView.setFocusable(true);
- defaultView.setOnClickListener(this);
- root.addView(inflater.inflate(R.layout.list_divider, root, false));
- root.addView(defaultView);
-
- // Per-track views.
- trackViews = new CheckedTextView[trackGroups.length][];
- for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {
- TrackGroup group = trackGroups.get(groupIndex);
- boolean groupIsAdaptive = trackGroupsAdaptive[groupIndex];
- trackViews[groupIndex] = new CheckedTextView[group.length];
- for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
- if (trackIndex == 0) {
- root.addView(inflater.inflate(R.layout.list_divider, root, false));
- }
- int trackViewLayoutId = groupIsAdaptive ? android.R.layout.simple_list_item_multiple_choice
- : android.R.layout.simple_list_item_single_choice;
- CheckedTextView trackView = (CheckedTextView) inflater.inflate(
- trackViewLayoutId, root, false);
- trackView.setBackgroundResource(selectableItemBackgroundResourceId);
- trackView.setText(DemoUtil.buildTrackName(group.getFormat(trackIndex)));
- if (trackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex)
- == RendererCapabilities.FORMAT_HANDLED) {
- trackView.setFocusable(true);
- trackView.setTag(Pair.create(groupIndex, trackIndex));
- trackView.setOnClickListener(this);
- } else {
- trackView.setFocusable(false);
- trackView.setEnabled(false);
- }
- trackViews[groupIndex][trackIndex] = trackView;
- root.addView(trackView);
- }
- }
-
- updateViews();
- return view;
- }
-
- private void updateViews() {
- disableView.setChecked(isDisabled);
- defaultView.setChecked(!isDisabled && override == null);
- for (int i = 0; i < trackViews.length; i++) {
- for (int j = 0; j < trackViews[i].length; j++) {
- trackViews[i][j].setChecked(override != null && override.groupIndex == i
- && override.containsTrack(j));
- }
- }
- }
-
- // DialogInterface.OnClickListener
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- selector.setRendererDisabled(rendererIndex, isDisabled);
- if (override != null) {
- selector.setSelectionOverride(rendererIndex, trackGroups, override);
- } else {
- selector.clearSelectionOverrides(rendererIndex);
- }
- }
-
- // View.OnClickListener
-
- @Override
- public void onClick(View view) {
- if (view == disableView) {
- isDisabled = true;
- override = null;
- } else if (view == defaultView) {
- isDisabled = false;
- override = null;
- } else {
- isDisabled = false;
- @SuppressWarnings("unchecked")
- Pair tag = (Pair) view.getTag();
- int groupIndex = tag.first;
- int trackIndex = tag.second;
- if (!trackGroupsAdaptive[groupIndex] || override == null
- || override.groupIndex != groupIndex) {
- setOverride(groupIndex, trackIndex);
- } else {
- // The group being modified is adaptive and we already have a non-null override.
- boolean isEnabled = ((CheckedTextView) view).isChecked();
- int overrideLength = override.length;
- if (isEnabled) {
- // Remove the track from the override.
- if (overrideLength == 1) {
- // The last track is being removed, so the override becomes empty.
- override = null;
- isDisabled = true;
- } else {
- setOverride(groupIndex, getTracksRemoving(override, trackIndex));
- }
- } else {
- // Add the track to the override.
- setOverride(groupIndex, getTracksAdding(override, trackIndex));
- }
- }
- }
- // Update the views with the new state.
- updateViews();
- }
-
- private void setOverride(int group, int... tracks) {
- override = new SelectionOverride(trackSelectionFactory, group, tracks);
- }
-
- // Track array manipulation.
-
- private static int[] getTracksAdding(SelectionOverride override, int addedTrack) {
- int[] tracks = override.tracks;
- tracks = Arrays.copyOf(tracks, tracks.length + 1);
- tracks[tracks.length - 1] = addedTrack;
- return tracks;
- }
-
- private static int[] getTracksRemoving(SelectionOverride override, int removedTrack) {
- int[] tracks = new int[override.length - 1];
- int trackCount = 0;
- for (int i = 0; i < tracks.length + 1; i++) {
- int track = override.tracks[i];
- if (track != removedTrack) {
- tracks[trackCount++] = track;
- }
- }
- return tracks;
- }
-
-}
diff --git a/demos/main/src/main/res/drawable-hdpi/ic_download.png b/demos/main/src/main/res/drawable-hdpi/ic_download.png
new file mode 100644
index 0000000000..fa3ebbb310
Binary files /dev/null and b/demos/main/src/main/res/drawable-hdpi/ic_download.png differ
diff --git a/demos/main/src/main/res/drawable-hdpi/ic_download_done.png b/demos/main/src/main/res/drawable-hdpi/ic_download_done.png
new file mode 100644
index 0000000000..fa0ec9dd68
Binary files /dev/null and b/demos/main/src/main/res/drawable-hdpi/ic_download_done.png differ
diff --git a/demos/main/src/main/res/drawable-mdpi/ic_download.png b/demos/main/src/main/res/drawable-mdpi/ic_download.png
new file mode 100644
index 0000000000..c8a2039c58
Binary files /dev/null and b/demos/main/src/main/res/drawable-mdpi/ic_download.png differ
diff --git a/demos/main/src/main/res/drawable-mdpi/ic_download_done.png b/demos/main/src/main/res/drawable-mdpi/ic_download_done.png
new file mode 100644
index 0000000000..08073a2a6d
Binary files /dev/null and b/demos/main/src/main/res/drawable-mdpi/ic_download_done.png differ
diff --git a/demos/main/src/main/res/drawable-xhdpi/ic_download.png b/demos/main/src/main/res/drawable-xhdpi/ic_download.png
new file mode 100644
index 0000000000..671e0b3ece
Binary files /dev/null and b/demos/main/src/main/res/drawable-xhdpi/ic_download.png differ
diff --git a/demos/main/src/main/res/drawable-xhdpi/ic_download_done.png b/demos/main/src/main/res/drawable-xhdpi/ic_download_done.png
new file mode 100644
index 0000000000..2339c0bf16
Binary files /dev/null and b/demos/main/src/main/res/drawable-xhdpi/ic_download_done.png differ
diff --git a/demos/main/src/main/res/drawable-xxhdpi/ic_download.png b/demos/main/src/main/res/drawable-xxhdpi/ic_download.png
new file mode 100644
index 0000000000..f02715177a
Binary files /dev/null and b/demos/main/src/main/res/drawable-xxhdpi/ic_download.png differ
diff --git a/demos/main/src/main/res/drawable-xxhdpi/ic_download_done.png b/demos/main/src/main/res/drawable-xxhdpi/ic_download_done.png
new file mode 100644
index 0000000000..b631a00088
Binary files /dev/null and b/demos/main/src/main/res/drawable-xxhdpi/ic_download_done.png differ
diff --git a/demos/main/src/main/res/drawable-xxxhdpi/ic_download.png b/demos/main/src/main/res/drawable-xxxhdpi/ic_download.png
new file mode 100644
index 0000000000..6602791545
Binary files /dev/null and b/demos/main/src/main/res/drawable-xxxhdpi/ic_download.png differ
diff --git a/demos/main/src/main/res/drawable-xxxhdpi/ic_download_done.png b/demos/main/src/main/res/drawable-xxxhdpi/ic_download_done.png
new file mode 100644
index 0000000000..52fe8f6990
Binary files /dev/null and b/demos/main/src/main/res/drawable-xxxhdpi/ic_download_done.png differ
diff --git a/demos/main/src/main/res/layout/sample_list_item.xml b/demos/main/src/main/res/layout/sample_list_item.xml
new file mode 100644
index 0000000000..cdb0058688
--- /dev/null
+++ b/demos/main/src/main/res/layout/sample_list_item.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
diff --git a/extensions/mediasession/src/main/res/values-gl-rES/strings.xml b/demos/main/src/main/res/layout/start_download_dialog.xml
similarity index 62%
rename from extensions/mediasession/src/main/res/values-gl-rES/strings.xml
rename to demos/main/src/main/res/layout/start_download_dialog.xml
index 6b65b3e843..acb9af5d97 100644
--- a/extensions/mediasession/src/main/res/values-gl-rES/strings.xml
+++ b/demos/main/src/main/res/layout/start_download_dialog.xml
@@ -1,6 +1,5 @@
-
-
-
- "Repetir todo"
- "Non repetir"
- "Repetir un"
-
+
diff --git a/demos/main/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml
index 42ef358a1a..eb260e6ffc 100644
--- a/demos/main/src/main/res/values/strings.xml
+++ b/demos/main/src/main/res/values/strings.xml
@@ -17,18 +17,10 @@
ExoPlayer
- Video
-
- Audio
-
- Text
-
- Disabled
-
- Default
-
Unexpected intent action: %1$s
+ Playback failed
+
Unrecognized ABR algorithm
Protected content not supported on API levels below 18
@@ -55,4 +47,14 @@
Playing sample without ads, as the IMA extension was not loaded
+ Failed to start download
+
+ This demo app does not support downloading playlists
+
+ This demo app does not support downloading protected content
+
+ This demo app only supports downloading http streams
+
+ IMA does not support offline ads
+
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java
index 7e0753ba10..84724cbb47 100644
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java
@@ -19,6 +19,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
@@ -307,6 +308,11 @@ public final class CastPlayer implements Player {
return playbackState;
}
+ @Override
+ public ExoPlaybackException getPlaybackError() {
+ return null;
+ }
+
@Override
public void setPlayWhenReady(boolean playWhenReady) {
if (remoteMediaClient == null) {
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
index 29bc874cd8..db980aa72b 100644
--- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
@@ -280,6 +280,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
new SocketTimeoutException(), dataSpec, getStatus(currentUrlRequest));
}
} catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
throw new OpenException(new InterruptedIOException(e), dataSpec, Status.INVALID);
}
@@ -352,17 +353,18 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
if (!operation.block(readTimeoutMs)) {
throw new SocketTimeoutException();
}
- } catch (InterruptedException | SocketTimeoutException e) {
- // If we're timing out or getting interrupted, the operation is still ongoing.
- // So we'll need to replace readBuffer to avoid the possibility of it being written to by
- // this operation during a subsequent request.
+ } catch (InterruptedException e) {
+ // The operation is ongoing so replace readBuffer to avoid it being written to by this
+ // operation during a subsequent request.
readBuffer = null;
+ Thread.currentThread().interrupt();
throw new HttpDataSourceException(
- e instanceof InterruptedException
- ? new InterruptedIOException((InterruptedException) e)
- : (SocketTimeoutException) e,
- currentDataSpec,
- HttpDataSourceException.TYPE_READ);
+ new InterruptedIOException(e), currentDataSpec, HttpDataSourceException.TYPE_READ);
+ } catch (SocketTimeoutException e) {
+ // The operation is ongoing so replace readBuffer to avoid it being written to by this
+ // operation during a subsequent request.
+ readBuffer = null;
+ throw new HttpDataSourceException(e, currentDataSpec, HttpDataSourceException.TYPE_READ);
}
if (exception != null) {
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java
index efe30d6525..db1394c1d6 100644
--- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java
@@ -21,6 +21,7 @@ import android.util.Log;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@@ -86,7 +87,7 @@ public final class CronetEngineWrapper {
public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) {
CronetEngine cronetEngine = null;
@CronetEngineSource int cronetEngineSource = SOURCE_UNAVAILABLE;
- List cronetProviders = CronetProvider.getAllProviders(context);
+ List cronetProviders = new ArrayList<>(CronetProvider.getAllProviders(context));
// Remove disabled and fallback Cronet providers from list
for (int i = cronetProviders.size() - 1; i >= 0; i--) {
if (!cronetProviders.get(i).isEnabled()
diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java
index 3e23659bf8..d7687e42ac 100644
--- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java
+++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java
@@ -74,7 +74,12 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
*/
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioSink audioSink, boolean enableFloatOutput) {
- super(eventHandler, eventListener, null, false, audioSink);
+ super(
+ eventHandler,
+ eventListener,
+ /* drmSessionManager= */ null,
+ /* playClearSamplesWithoutKeys= */ false,
+ audioSink);
this.enableFloatOutput = enableFloatOutput;
}
diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle
index 9bf8d39435..609953130b 100644
--- a/extensions/flac/build.gradle
+++ b/extensions/flac/build.gradle
@@ -34,6 +34,7 @@ dependencies {
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
implementation project(modulePrefix + 'library-core')
androidTestImplementation project(modulePrefix + 'testutils')
+ testImplementation project(modulePrefix + 'testutils-robolectric')
}
ext {
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
index 729a406315..34a6e6820d 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
@@ -79,7 +79,7 @@ public final class FlacExtractor implements Extractor {
private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22};
private final Id3Peeker id3Peeker;
- private final @Flags int flags;
+ private final boolean isId3MetadataDisabled;
private FlacDecoderJni decoderJni;
@@ -90,7 +90,6 @@ public final class FlacExtractor implements Extractor {
private ByteBuffer outputByteBuffer;
private Metadata id3Metadata;
- private long id3SectionSize;
private boolean metadataParsed;
@@ -105,8 +104,8 @@ public final class FlacExtractor implements Extractor {
* @param flags Flags that control the extractor's behavior.
*/
public FlacExtractor(int flags) {
- this.flags = flags;
id3Peeker = new Id3Peeker();
+ isId3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
}
@Override
@@ -125,24 +124,16 @@ public final class FlacExtractor implements Extractor {
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
if (input.getPosition() == 0) {
id3Metadata = peekId3Data(input);
- id3SectionSize = input.getPeekPosition();
}
- boolean isFlacFormat = peekFlacSignature(input);
- if (isFlacFormat) {
- // If this is FLAC format, we should skip the whole ID3 section.
- skipFullyId3Section(input);
- }
- return isFlacFormat;
+ return peekFlacSignature(input);
}
@Override
public int read(final ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
- if (input.getPosition() == 0) {
+ if (input.getPosition() == 0 && !isId3MetadataDisabled && id3Metadata == null) {
id3Metadata = peekId3Data(input);
- id3SectionSize = input.getPeekPosition();
}
- skipFullyId3Section(input);
decoderJni.setData(input);
@@ -155,7 +146,7 @@ public final class FlacExtractor implements Extractor {
}
} catch (IOException e) {
decoderJni.reset(0);
- input.setRetryPosition(id3SectionSize, e);
+ input.setRetryPosition(0, e);
throw e; // never executes
}
metadataParsed = true;
@@ -163,7 +154,7 @@ public final class FlacExtractor implements Extractor {
boolean isSeekable = decoderJni.getSeekPosition(0) != -1;
extractorOutput.seekMap(
isSeekable
- ? new FlacSeekMap(streamInfo.durationUs(), decoderJni, id3SectionSize)
+ ? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
: new SeekMap.Unseekable(streamInfo.durationUs(), 0));
Format mediaFormat =
Format.createAudioSampleFormat(
@@ -181,7 +172,7 @@ public final class FlacExtractor implements Extractor {
/* drmInitData= */ null,
/* selectionFlags= */ 0,
/* language= */ null,
- (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : id3Metadata);
+ isId3MetadataDisabled ? null : id3Metadata);
trackOutput.format(mediaFormat);
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
@@ -196,7 +187,7 @@ public final class FlacExtractor implements Extractor {
} catch (IOException e) {
if (lastDecodePosition >= 0) {
decoderJni.reset(lastDecodePosition);
- input.setRetryPosition(id3SectionSize + lastDecodePosition, e);
+ input.setRetryPosition(lastDecodePosition, e);
}
throw e;
}
@@ -212,12 +203,11 @@ public final class FlacExtractor implements Extractor {
@Override
public void seek(long position, long timeUs) {
- if (position <= id3SectionSize) {
+ if (position == 0) {
metadataParsed = false;
}
- long flacStreamPosition = Math.max(0, position - id3SectionSize);
if (decoderJni != null) {
- decoderJni.reset(flacStreamPosition);
+ decoderJni.reset(position);
}
}
@@ -238,9 +228,8 @@ public final class FlacExtractor implements Extractor {
@Nullable
private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
input.resetPeekPosition();
- boolean disableId3Frames = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
Id3Decoder.FramePredicate id3FramePredicate =
- disableId3Frames ? Id3Decoder.NO_FRAMES_PREDICATE : null;
+ isId3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;
return id3Peeker.peekId3Data(input, id3FramePredicate);
}
@@ -255,22 +244,14 @@ public final class FlacExtractor implements Extractor {
return Arrays.equals(header, FLAC_SIGNATURE);
}
- /** Skips input until we have passed the whole Id3 section. */
- private void skipFullyId3Section(ExtractorInput input) throws IOException, InterruptedException {
- int bytesToSkip = Math.max(0, (int) (id3SectionSize - input.getPosition()));
- input.skipFully(bytesToSkip);
- }
-
private static final class FlacSeekMap implements SeekMap {
private final long durationUs;
private final FlacDecoderJni decoderJni;
- private final long id3SectionSize;
- public FlacSeekMap(long durationUs, FlacDecoderJni decoderJni, long id3SectionSize) {
+ public FlacSeekMap(long durationUs, FlacDecoderJni decoderJni) {
this.durationUs = durationUs;
this.decoderJni = decoderJni;
- this.id3SectionSize = id3SectionSize;
}
@Override
@@ -281,8 +262,7 @@ public final class FlacExtractor implements Extractor {
@Override
public SeekPoints getSeekPoints(long timeUs) {
// TODO: Access the seek table via JNI to return two seek points when appropriate.
- return new SeekPoints(
- new SeekPoint(timeUs, id3SectionSize + decoderJni.getSeekPosition(timeUs)));
+ return new SeekPoints(new SeekPoint(timeUs, decoderJni.getSeekPosition(timeUs)));
}
@Override
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
index 60017a27e4..d3dbaaec96 100644
--- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
@@ -52,6 +52,8 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.source.ads.AdPlaybackState.AdState;
import com.google.android.exoplayer2.source.ads.AdsLoader;
+import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
+import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
@@ -80,6 +82,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
private final Context context;
private @Nullable ImaSdkSettings imaSdkSettings;
+ private @Nullable AdEventListener adEventListener;
private int vastLoadTimeoutMs;
private int mediaLoadTimeoutMs;
@@ -108,6 +111,18 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
return this;
}
+ /**
+ * Sets a listener for ad events that will be passed to {@link
+ * AdsManager#addAdEventListener(AdEventListener)}.
+ *
+ * @param adEventListener The ad event listener.
+ * @return This builder, for convenience.
+ */
+ public Builder setAdEventListener(AdEventListener adEventListener) {
+ this.adEventListener = Assertions.checkNotNull(adEventListener);
+ return this;
+ }
+
/**
* Sets the VAST load timeout, in milliseconds.
*
@@ -144,7 +159,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
*/
public ImaAdsLoader buildForAdTag(Uri adTagUri) {
return new ImaAdsLoader(
- context, adTagUri, imaSdkSettings, null, vastLoadTimeoutMs, mediaLoadTimeoutMs);
+ context,
+ adTagUri,
+ imaSdkSettings,
+ null,
+ vastLoadTimeoutMs,
+ mediaLoadTimeoutMs,
+ adEventListener);
}
/**
@@ -156,7 +177,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
*/
public ImaAdsLoader buildForAdsResponse(String adsResponse) {
return new ImaAdsLoader(
- context, null, imaSdkSettings, adsResponse, vastLoadTimeoutMs, mediaLoadTimeoutMs);
+ context,
+ null,
+ imaSdkSettings,
+ adsResponse,
+ vastLoadTimeoutMs,
+ mediaLoadTimeoutMs,
+ adEventListener);
}
}
@@ -214,6 +241,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
private final @Nullable String adsResponse;
private final int vastLoadTimeoutMs;
private final int mediaLoadTimeoutMs;
+ private final @Nullable AdEventListener adEventListener;
private final Timeline.Period period;
private final List adCallbacks;
private final ImaSdkFactory imaSdkFactory;
@@ -229,7 +257,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
private VideoProgressUpdate lastAdProgress;
private AdsManager adsManager;
- private AdErrorEvent pendingAdErrorEvent;
+ private AdLoadException pendingAdLoadError;
private Timeline timeline;
private long contentDurationMs;
private int podIndexOffset;
@@ -308,7 +336,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
/* imaSdkSettings= */ null,
/* adsResponse= */ null,
/* vastLoadTimeoutMs= */ TIMEOUT_UNSET,
- /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET);
+ /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,
+ /* adEventListener= */ null);
}
/**
@@ -330,7 +359,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
imaSdkSettings,
/* adsResponse= */ null,
/* vastLoadTimeoutMs= */ TIMEOUT_UNSET,
- /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET);
+ /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,
+ /* adEventListener= */ null);
}
private ImaAdsLoader(
@@ -339,12 +369,14 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Nullable ImaSdkSettings imaSdkSettings,
@Nullable String adsResponse,
int vastLoadTimeoutMs,
- int mediaLoadTimeoutMs) {
+ int mediaLoadTimeoutMs,
+ @Nullable AdEventListener adEventListener) {
Assertions.checkArgument(adTagUri != null || adsResponse != null);
this.adTagUri = adTagUri;
this.adsResponse = adsResponse;
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
this.mediaLoadTimeoutMs = mediaLoadTimeoutMs;
+ this.adEventListener = adEventListener;
period = new Timeline.Period();
adCallbacks = new ArrayList<>(1);
imaSdkFactory = ImaSdkFactory.getInstance();
@@ -500,6 +532,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
this.adsManager = adsManager;
adsManager.addAdErrorListener(this);
adsManager.addAdEventListener(this);
+ if (adEventListener != null) {
+ adsManager.addAdEventListener(adEventListener);
+ }
if (player != null) {
// If a player is attached already, start playback immediately.
try {
@@ -544,13 +579,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
updateAdPlaybackState();
} else if (isAdGroupLoadError(error)) {
try {
- handleAdGroupLoadError();
+ handleAdGroupLoadError(error);
} catch (Exception e) {
maybeNotifyInternalError("onAdError", e);
}
}
- if (pendingAdErrorEvent == null) {
- pendingAdErrorEvent = adErrorEvent;
+ if (pendingAdLoadError == null) {
+ pendingAdLoadError = AdLoadException.createForAllAds(error);
}
maybeNotifyPendingAdLoadError();
}
@@ -937,9 +972,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
break;
case LOG:
Map adData = adEvent.getAdData();
- Log.i(TAG, "Log AdEvent: " + adData);
+ String message = "AdEvent: " + adData;
+ Log.i(TAG, message);
if ("adLoadError".equals(adData.get("type"))) {
- handleAdGroupLoadError();
+ handleAdGroupLoadError(new IOException(message));
}
break;
case ALL_ADS_COMPLETED:
@@ -1011,7 +1047,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
}
}
- private void handleAdGroupLoadError() {
+ private void handleAdGroupLoadError(Exception error) {
int adGroupIndex =
this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex;
if (adGroupIndex == C.INDEX_UNSET) {
@@ -1033,6 +1069,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
}
}
updateAdPlaybackState();
+ if (pendingAdLoadError == null) {
+ pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex);
+ }
}
private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) {
@@ -1111,21 +1150,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
}
private void maybeNotifyPendingAdLoadError() {
- if (pendingAdErrorEvent != null) {
- if (eventListener != null) {
- eventListener.onAdLoadError(
- new IOException("Ad error: " + pendingAdErrorEvent, pendingAdErrorEvent.getError()));
- }
- pendingAdErrorEvent = null;
+ if (pendingAdLoadError != null && eventListener != null) {
+ eventListener.onAdLoadError(pendingAdLoadError, new DataSpec(adTagUri));
+ pendingAdLoadError = null;
}
}
private void maybeNotifyInternalError(String name, Exception cause) {
String message = "Internal error in " + name;
Log.e(TAG, message, cause);
- if (eventListener != null) {
- eventListener.onInternalAdLoadError(new RuntimeException(message, cause));
- }
// We can't recover from an unexpected error in general, so skip all remaining ads.
if (adPlaybackState == null) {
adPlaybackState = new AdPlaybackState();
@@ -1135,6 +1168,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
}
}
updateAdPlaybackState();
+ if (eventListener != null) {
+ eventListener.onAdLoadError(
+ AdLoadException.createForUnexpected(new RuntimeException(message, cause)),
+ new DataSpec(adTagUri));
+ }
}
private static long[] getAdGroupTimesUs(List cuePoints) {
diff --git a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
index 9232efaaa9..c6701da964 100644
--- a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
+++ b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
@@ -15,8 +15,6 @@
*/
package com.google.android.exoplayer2.ext.jobdispatcher;
-import android.app.Notification;
-import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
@@ -34,13 +32,8 @@ import com.google.android.exoplayer2.scheduler.Scheduler;
import com.google.android.exoplayer2.util.Util;
/**
- * A {@link Scheduler} which uses {@link com.firebase.jobdispatcher.FirebaseJobDispatcher} to
- * schedule a {@link Service} to be started when its requirements are met. The started service must
- * call {@link Service#startForeground(int, Notification)} to make itself a foreground service upon
- * being started, as documented by {@link Service#startForegroundService(Intent)}.
- *
- * To use {@link JobDispatcherScheduler} application needs to have RECEIVE_BOOT_COMPLETED
- * permission and you need to define JobDispatcherSchedulerService in your manifest:
+ * A {@link Scheduler} that uses {@link FirebaseJobDispatcher}. To use this scheduler, you must add
+ * {@link JobDispatcherSchedulerService} to your manifest:
*
*
{@literal
*
@@ -54,18 +47,6 @@ import com.google.android.exoplayer2.util.Util;
*
* }
*
- * The service to be scheduled must be defined in the manifest with an intent-filter:
- *
- * {@literal
- *
- *
- *
- *
- *
- *
- * }
- *
* This Scheduler uses Google Play services but does not do any availability checks. Any uses
* should be guarded with a call to {@code
* GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)}
@@ -76,44 +57,37 @@ import com.google.android.exoplayer2.util.Util;
public final class JobDispatcherScheduler implements Scheduler {
private static final String TAG = "JobDispatcherScheduler";
- private static final String SERVICE_ACTION = "SERVICE_ACTION";
- private static final String SERVICE_PACKAGE = "SERVICE_PACKAGE";
- private static final String REQUIREMENTS = "REQUIREMENTS";
+ private static final String KEY_SERVICE_ACTION = "service_action";
+ private static final String KEY_SERVICE_PACKAGE = "service_package";
+ private static final String KEY_REQUIREMENTS = "requirements";
private final String jobTag;
- private final Job job;
private final FirebaseJobDispatcher jobDispatcher;
/**
- * @param context Used to create a {@link FirebaseJobDispatcher} service.
- * @param requirements The requirements to execute the job.
- * @param jobTag Unique tag for the job. Using the same tag as a previous job can cause that job
- * to be replaced or canceled.
- * @param serviceAction The action which the service will be started with.
- * @param servicePackage The package of the service which contains the logic of the job.
+ * @param context A context.
+ * @param jobTag A tag for jobs scheduled by this instance. If the same tag was used by a previous
+ * instance, anything scheduled by the previous instance will be canceled by this instance if
+ * {@link #schedule(Requirements, String, String)} or {@link #cancel()} are called.
*/
- public JobDispatcherScheduler(
- Context context,
- Requirements requirements,
- String jobTag,
- String serviceAction,
- String servicePackage) {
- this.jobDispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(context));
+ public JobDispatcherScheduler(Context context, String jobTag) {
+ this.jobDispatcher =
+ new FirebaseJobDispatcher(new GooglePlayDriver(context.getApplicationContext()));
this.jobTag = jobTag;
- this.job = buildJob(jobDispatcher, requirements, jobTag, serviceAction, servicePackage);
}
@Override
- public boolean schedule() {
+ public boolean schedule(Requirements requirements, String serviceAction, String servicePackage) {
+ Job job = buildJob(jobDispatcher, requirements, jobTag, serviceAction, servicePackage);
int result = jobDispatcher.schedule(job);
- logd("Scheduling JobDispatcher job: " + jobTag + " result: " + result);
+ logd("Scheduling job: " + jobTag + " result: " + result);
return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS;
}
@Override
public boolean cancel() {
int result = jobDispatcher.cancel(jobTag);
- logd("Canceling JobDispatcher job: " + jobTag + " result: " + result);
+ logd("Canceling job: " + jobTag + " result: " + result);
return result == FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS;
}
@@ -151,13 +125,12 @@ public final class JobDispatcherScheduler implements Scheduler {
}
builder.setLifetime(Lifetime.FOREVER).setReplaceCurrent(true);
- // Extras, work duration.
Bundle extras = new Bundle();
- extras.putString(SERVICE_ACTION, serviceAction);
- extras.putString(SERVICE_PACKAGE, servicePackage);
- extras.putInt(REQUIREMENTS, requirements.getRequirementsData());
-
+ extras.putString(KEY_SERVICE_ACTION, serviceAction);
+ extras.putString(KEY_SERVICE_PACKAGE, servicePackage);
+ extras.putInt(KEY_REQUIREMENTS, requirements.getRequirementsData());
builder.setExtras(extras);
+
return builder.build();
}
@@ -167,26 +140,22 @@ public final class JobDispatcherScheduler implements Scheduler {
}
}
- /** A {@link JobService} to start a service if the requirements are met. */
+ /** A {@link JobService} that starts the target service if the requirements are met. */
public static final class JobDispatcherSchedulerService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
logd("JobDispatcherSchedulerService is started");
Bundle extras = params.getExtras();
- Requirements requirements = new Requirements(extras.getInt(REQUIREMENTS));
+ Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS));
if (requirements.checkRequirements(this)) {
- logd("requirements are met");
- String serviceAction = extras.getString(SERVICE_ACTION);
- String servicePackage = extras.getString(SERVICE_PACKAGE);
+ logd("Requirements are met");
+ String serviceAction = extras.getString(KEY_SERVICE_ACTION);
+ String servicePackage = extras.getString(KEY_SERVICE_PACKAGE);
Intent intent = new Intent(serviceAction).setPackage(servicePackage);
- logd("starting service action: " + serviceAction + " package: " + servicePackage);
- if (Util.SDK_INT >= 26) {
- startForegroundService(intent);
- } else {
- startService(intent);
- }
+ logd("Starting service action: " + serviceAction + " package: " + servicePackage);
+ Util.startForegroundService(this, intent);
} else {
- logd("requirements are not met");
+ logd("Requirements are not met");
jobFinished(params, /* needsReschedule */ true);
}
return false;
diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java
index e513084974..03f53c263f 100644
--- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java
+++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java
@@ -53,7 +53,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
private @Nullable PlaybackPreparer playbackPreparer;
private ControlDispatcher controlDispatcher;
- private ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
+ private @Nullable ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
private SurfaceHolderGlueHost surfaceHolderGlueHost;
private boolean hasSurface;
private boolean lastNotifiedPreparedState;
@@ -110,7 +110,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
* @param errorMessageProvider The {@link ErrorMessageProvider}.
*/
public void setErrorMessageProvider(
- ErrorMessageProvider super ExoPlaybackException> errorMessageProvider) {
+ @Nullable ErrorMessageProvider super ExoPlaybackException> errorMessageProvider) {
this.errorMessageProvider = errorMessageProvider;
}
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
index 26e0d0e006..83fb16236d 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
@@ -334,12 +334,11 @@ public final class MediaSessionConnector {
private Player player;
private CustomActionProvider[] customActionProviders;
private Map customActionMap;
- private ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
+ private @Nullable ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
private PlaybackPreparer playbackPreparer;
private QueueNavigator queueNavigator;
private QueueEditor queueEditor;
private RatingCallback ratingCallback;
- private ExoPlaybackException playbackException;
/**
* Creates an instance. Must be called on the same thread that is used to construct the player
@@ -403,16 +402,18 @@ public final class MediaSessionConnector {
/**
* Sets the player to be connected to the media session.
- *
- * The order in which any {@link CustomActionProvider}s are passed determines the order of the
+ *
+ *
The order in which any {@link CustomActionProvider}s are passed determines the order of the
* actions published with the playback state of the session.
*
* @param player The player to be connected to the {@code MediaSession}.
* @param playbackPreparer An optional {@link PlaybackPreparer} for preparing the player.
- * @param customActionProviders An optional {@link CustomActionProvider}s to publish and handle
+ * @param customActionProviders Optional {@link CustomActionProvider}s to publish and handle
* custom actions.
*/
- public void setPlayer(Player player, PlaybackPreparer playbackPreparer,
+ public void setPlayer(
+ Player player,
+ @Nullable PlaybackPreparer playbackPreparer,
CustomActionProvider... customActionProviders) {
if (this.player != null) {
this.player.removeListener(exoPlayerEventListener);
@@ -435,13 +436,16 @@ public final class MediaSessionConnector {
}
/**
- * Sets the {@link ErrorMessageProvider}.
+ * Sets the optional {@link ErrorMessageProvider}.
*
* @param errorMessageProvider The error message provider.
*/
public void setErrorMessageProvider(
- ErrorMessageProvider super ExoPlaybackException> errorMessageProvider) {
- this.errorMessageProvider = errorMessageProvider;
+ @Nullable ErrorMessageProvider super ExoPlaybackException> errorMessageProvider) {
+ if (this.errorMessageProvider != errorMessageProvider) {
+ this.errorMessageProvider = errorMessageProvider;
+ updateMediaSessionPlaybackState();
+ }
}
/**
@@ -451,9 +455,11 @@ public final class MediaSessionConnector {
* @param queueNavigator The queue navigator.
*/
public void setQueueNavigator(QueueNavigator queueNavigator) {
- unregisterCommandReceiver(this.queueNavigator);
- this.queueNavigator = queueNavigator;
- registerCommandReceiver(queueNavigator);
+ if (this.queueNavigator != queueNavigator) {
+ unregisterCommandReceiver(this.queueNavigator);
+ this.queueNavigator = queueNavigator;
+ registerCommandReceiver(queueNavigator);
+ }
}
/**
@@ -462,11 +468,13 @@ public final class MediaSessionConnector {
* @param queueEditor The queue editor.
*/
public void setQueueEditor(QueueEditor queueEditor) {
- unregisterCommandReceiver(this.queueEditor);
- this.queueEditor = queueEditor;
- registerCommandReceiver(queueEditor);
- mediaSession.setFlags(queueEditor == null ? BASE_MEDIA_SESSION_FLAGS
- : EDITOR_MEDIA_SESSION_FLAGS);
+ if (this.queueEditor != queueEditor) {
+ unregisterCommandReceiver(this.queueEditor);
+ this.queueEditor = queueEditor;
+ registerCommandReceiver(queueEditor);
+ mediaSession.setFlags(
+ queueEditor == null ? BASE_MEDIA_SESSION_FLAGS : EDITOR_MEDIA_SESSION_FLAGS);
+ }
}
/**
@@ -475,9 +483,11 @@ public final class MediaSessionConnector {
* @param ratingCallback The rating callback.
*/
public void setRatingCallback(RatingCallback ratingCallback) {
- unregisterCommandReceiver(this.ratingCallback);
- this.ratingCallback = ratingCallback;
- registerCommandReceiver(this.ratingCallback);
+ if (this.ratingCallback != ratingCallback) {
+ unregisterCommandReceiver(this.ratingCallback);
+ this.ratingCallback = ratingCallback;
+ registerCommandReceiver(this.ratingCallback);
+ }
}
private void registerCommandReceiver(CommandReceiver commandReceiver) {
@@ -514,16 +524,16 @@ public final class MediaSessionConnector {
}
customActionMap = Collections.unmodifiableMap(currentActions);
- int sessionPlaybackState = playbackException != null ? PlaybackStateCompat.STATE_ERROR
- : mapPlaybackState(player.getPlaybackState(), player.getPlayWhenReady());
- if (playbackException != null) {
- if (errorMessageProvider != null) {
- Pair message = errorMessageProvider.getErrorMessage(playbackException);
- builder.setErrorMessage(message.first, message.second);
- }
- if (player.getPlaybackState() != Player.STATE_IDLE) {
- playbackException = null;
- }
+ int playbackState = player.getPlaybackState();
+ ExoPlaybackException playbackError =
+ playbackState == Player.STATE_IDLE ? player.getPlaybackError() : null;
+ int sessionPlaybackState =
+ playbackError != null
+ ? PlaybackStateCompat.STATE_ERROR
+ : mapPlaybackState(player.getPlaybackState(), player.getPlayWhenReady());
+ if (playbackError != null && errorMessageProvider != null) {
+ Pair message = errorMessageProvider.getErrorMessage(playbackError);
+ builder.setErrorMessage(message.first, message.second);
}
long activeQueueItemId = queueNavigator != null ? queueNavigator.getActiveQueueItemId(player)
: MediaSessionCompat.QueueItem.UNKNOWN_ID;
@@ -699,12 +709,6 @@ public final class MediaSessionConnector {
updateMediaSessionPlaybackState();
}
- @Override
- public void onPlayerError(ExoPlaybackException error) {
- playbackException = error;
- updateMediaSessionPlaybackState();
- }
-
@Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
if (currentWindowIndex != player.getCurrentWindowIndex()) {
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
index 1b9bd3ecd9..26a7b6150a 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
@@ -73,10 +73,11 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
/**
* Gets the {@link MediaDescriptionCompat} for a given timeline window index.
*
+ * @param player The current player.
* @param windowIndex The timeline window index for which to provide a description.
* @return A {@link MediaDescriptionCompat}.
*/
- public abstract MediaDescriptionCompat getMediaDescription(int windowIndex);
+ public abstract MediaDescriptionCompat getMediaDescription(Player player, int windowIndex);
@Override
public long getSupportedQueueNavigatorActions(Player player) {
@@ -185,7 +186,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
windowCount - queueSize);
List queue = new ArrayList<>();
for (int i = startIndex; i < startIndex + queueSize; i++) {
- queue.add(new MediaSessionCompat.QueueItem(getMediaDescription(i), i));
+ queue.add(new MediaSessionCompat.QueueItem(getMediaDescription(player, i), i));
}
mediaSession.setQueue(queue);
activeQueueItemId = currentWindowIndex;
diff --git a/extensions/mediasession/src/main/res/values-af/strings.xml b/extensions/mediasession/src/main/res/values-af/strings.xml
index 4ef78cd84f..92d171cfdc 100644
--- a/extensions/mediasession/src/main/res/values-af/strings.xml
+++ b/extensions/mediasession/src/main/res/values-af/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Herhaal alles"
- "Herhaal niks"
- "Herhaal een"
+ Herhaal niks
+ Herhaal een
+ Herhaal alles
diff --git a/extensions/mediasession/src/main/res/values-am/strings.xml b/extensions/mediasession/src/main/res/values-am/strings.xml
index 531f605584..54509a65ab 100644
--- a/extensions/mediasession/src/main/res/values-am/strings.xml
+++ b/extensions/mediasession/src/main/res/values-am/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "ሁሉንም ድገም"
- "ምንም አትድገም"
- "አንዱን ድገም"
+ ምንም አትድገም
+ አንድ ድገም
+ ሁሉንም ድገም
diff --git a/extensions/mediasession/src/main/res/values-ar/strings.xml b/extensions/mediasession/src/main/res/values-ar/strings.xml
index 0101a746e0..707ad41a16 100644
--- a/extensions/mediasession/src/main/res/values-ar/strings.xml
+++ b/extensions/mediasession/src/main/res/values-ar/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "تكرار الكل"
- "عدم التكرار"
- "تكرار مقطع واحد"
+ عدم التكرار
+ تكرار مقطع صوتي واحد
+ تكرار الكل
diff --git a/extensions/mediasession/src/main/res/values-az-rAZ/strings.xml b/extensions/mediasession/src/main/res/values-az-rAZ/strings.xml
deleted file mode 100644
index 34408143fa..0000000000
--- a/extensions/mediasession/src/main/res/values-az-rAZ/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "Bütün təkrarlayın"
- "Təkrar bir"
- "Heç bir təkrar"
-
diff --git a/extensions/mediasession/src/main/res/values-az/strings.xml b/extensions/mediasession/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..33c1f341ba
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-az/strings.xml
@@ -0,0 +1,6 @@
+
+
+ Heç biri təkrarlanmasın
+ Biri təkrarlansın
+ Hamısı təkrarlansın
+
diff --git a/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml b/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml
index 67a51cf85e..dcdcb9d977 100644
--- a/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml
+++ b/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Ponovi sve"
- "Ne ponavljaj nijednu"
- "Ponovi jednu"
+ Ne ponavljaj nijednu
+ Ponovi jednu
+ Ponovi sve
diff --git a/extensions/mediasession/src/main/res/values-be-rBY/strings.xml b/extensions/mediasession/src/main/res/values-be-rBY/strings.xml
deleted file mode 100644
index 2f05607235..0000000000
--- a/extensions/mediasession/src/main/res/values-be-rBY/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "Паўтарыць усё"
- "Паўтараць ні"
- "Паўтарыць адзін"
-
diff --git a/extensions/mediasession/src/main/res/values-be/strings.xml b/extensions/mediasession/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..380794f281
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-be/strings.xml
@@ -0,0 +1,6 @@
+
+
+ Не паўтараць нічога
+ Паўтарыць адзін элемент
+ Паўтарыць усе
+
diff --git a/extensions/mediasession/src/main/res/values-bg/strings.xml b/extensions/mediasession/src/main/res/values-bg/strings.xml
index 16910d640a..8a639c6cff 100644
--- a/extensions/mediasession/src/main/res/values-bg/strings.xml
+++ b/extensions/mediasession/src/main/res/values-bg/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Повтаряне на всички"
- "Без повтаряне"
- "Повтаряне на един елемент"
+ Без повтаряне
+ Повтаряне на един елемент
+ Повтаряне на всички
diff --git a/extensions/mediasession/src/main/res/values-bn-rBD/strings.xml b/extensions/mediasession/src/main/res/values-bn-rBD/strings.xml
deleted file mode 100644
index 8872b464c6..0000000000
--- a/extensions/mediasession/src/main/res/values-bn-rBD/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "সবগুলির পুনরাবৃত্তি করুন"
- "একটিরও পুনরাবৃত্তি করবেন না"
- "একটির পুনরাবৃত্তি করুন"
-
diff --git a/extensions/mediasession/src/main/res/values-bn/strings.xml b/extensions/mediasession/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..c39f11e570
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-bn/strings.xml
@@ -0,0 +1,6 @@
+
+
+ কোনও আইটেম আবার চালাবেন না
+ একটি আইটেম আবার চালান
+ সবগুলি আইটেম আবার চালান
+
diff --git a/extensions/mediasession/src/main/res/values-bs-rBA/strings.xml b/extensions/mediasession/src/main/res/values-bs-rBA/strings.xml
deleted file mode 100644
index d0bf068573..0000000000
--- a/extensions/mediasession/src/main/res/values-bs-rBA/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "Ponovite sve"
- "Ne ponavljaju"
- "Ponovite jedan"
-
diff --git a/extensions/mediasession/src/main/res/values-bs/strings.xml b/extensions/mediasession/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..44b5cb5dd6
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-bs/strings.xml
@@ -0,0 +1,6 @@
+
+
+ Ne ponavljaj
+ Ponovi jedno
+ Ponovi sve
+
diff --git a/extensions/mediasession/src/main/res/values-ca/strings.xml b/extensions/mediasession/src/main/res/values-ca/strings.xml
index 89414d736e..cdb41b2b0a 100644
--- a/extensions/mediasession/src/main/res/values-ca/strings.xml
+++ b/extensions/mediasession/src/main/res/values-ca/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Repeteix-ho tot"
- "No en repeteixis cap"
- "Repeteix-ne un"
+ No en repeteixis cap
+ Repeteix una
+ Repeteix tot
diff --git a/extensions/mediasession/src/main/res/values-cs/strings.xml b/extensions/mediasession/src/main/res/values-cs/strings.xml
index 784d872570..4d25b3a3ba 100644
--- a/extensions/mediasession/src/main/res/values-cs/strings.xml
+++ b/extensions/mediasession/src/main/res/values-cs/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Opakovat vše"
- "Neopakovat"
- "Opakovat jednu položku"
+ Neopakovat
+ Opakovat jednu
+ Opakovat vše
diff --git a/extensions/mediasession/src/main/res/values-da/strings.xml b/extensions/mediasession/src/main/res/values-da/strings.xml
index 2c9784d122..f74409a50b 100644
--- a/extensions/mediasession/src/main/res/values-da/strings.xml
+++ b/extensions/mediasession/src/main/res/values-da/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Gentag alle"
- "Gentag ingen"
- "Gentag en"
+ Gentag ingen
+ Gentag én
+ Gentag alle
diff --git a/extensions/mediasession/src/main/res/values-de/strings.xml b/extensions/mediasession/src/main/res/values-de/strings.xml
index c11e449665..af3564cb41 100644
--- a/extensions/mediasession/src/main/res/values-de/strings.xml
+++ b/extensions/mediasession/src/main/res/values-de/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Alle wiederholen"
- "Keinen Titel wiederholen"
- "Einen Titel wiederholen"
+ Keinen wiederholen
+ Einen wiederholen
+ Alle wiederholen
diff --git a/extensions/mediasession/src/main/res/values-el/strings.xml b/extensions/mediasession/src/main/res/values-el/strings.xml
index 6279af5d64..e4f6666622 100644
--- a/extensions/mediasession/src/main/res/values-el/strings.xml
+++ b/extensions/mediasession/src/main/res/values-el/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Επανάληψη όλων"
- "Καμία επανάληψη"
- "Επανάληψη ενός στοιχείου"
+ Καμία επανάληψη
+ Επανάληψη ενός κομματιού
+ Επανάληψη όλων
diff --git a/extensions/mediasession/src/main/res/values-en-rAU/strings.xml b/extensions/mediasession/src/main/res/values-en-rAU/strings.xml
index a3fccf8b52..4170902688 100644
--- a/extensions/mediasession/src/main/res/values-en-rAU/strings.xml
+++ b/extensions/mediasession/src/main/res/values-en-rAU/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Repeat all"
- "Repeat none"
- "Repeat one"
+ Repeat none
+ Repeat one
+ Repeat all
diff --git a/extensions/mediasession/src/main/res/values-en-rGB/strings.xml b/extensions/mediasession/src/main/res/values-en-rGB/strings.xml
index a3fccf8b52..4170902688 100644
--- a/extensions/mediasession/src/main/res/values-en-rGB/strings.xml
+++ b/extensions/mediasession/src/main/res/values-en-rGB/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Repeat all"
- "Repeat none"
- "Repeat one"
+ Repeat none
+ Repeat one
+ Repeat all
diff --git a/extensions/mediasession/src/main/res/values-en-rIN/strings.xml b/extensions/mediasession/src/main/res/values-en-rIN/strings.xml
index a3fccf8b52..4170902688 100644
--- a/extensions/mediasession/src/main/res/values-en-rIN/strings.xml
+++ b/extensions/mediasession/src/main/res/values-en-rIN/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Repeat all"
- "Repeat none"
- "Repeat one"
+ Repeat none
+ Repeat one
+ Repeat all
diff --git a/extensions/mediasession/src/main/res/values-es-rUS/strings.xml b/extensions/mediasession/src/main/res/values-es-rUS/strings.xml
index 0fe29d3d5a..700e6de4e2 100644
--- a/extensions/mediasession/src/main/res/values-es-rUS/strings.xml
+++ b/extensions/mediasession/src/main/res/values-es-rUS/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Repetir todo"
- "No repetir"
- "Repetir uno"
+ No repetir
+ Repetir uno
+ Repetir todo
diff --git a/extensions/mediasession/src/main/res/values-es/strings.xml b/extensions/mediasession/src/main/res/values-es/strings.xml
index 0fe29d3d5a..700e6de4e2 100644
--- a/extensions/mediasession/src/main/res/values-es/strings.xml
+++ b/extensions/mediasession/src/main/res/values-es/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Repetir todo"
- "No repetir"
- "Repetir uno"
+ No repetir
+ Repetir uno
+ Repetir todo
diff --git a/extensions/mediasession/src/main/res/values-et-rEE/strings.xml b/extensions/mediasession/src/main/res/values-et-rEE/strings.xml
deleted file mode 100644
index 1bc3b59706..0000000000
--- a/extensions/mediasession/src/main/res/values-et-rEE/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "Korda kõike"
- "Ära korda midagi"
- "Korda ühte"
-
diff --git a/extensions/mediasession/src/main/res/values-et/strings.xml b/extensions/mediasession/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..1f629e68f5
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-et/strings.xml
@@ -0,0 +1,6 @@
+
+
+ Ära korda ühtegi
+ Korda ühte
+ Korda kõiki
+
diff --git a/extensions/mediasession/src/main/res/values-eu-rES/strings.xml b/extensions/mediasession/src/main/res/values-eu-rES/strings.xml
deleted file mode 100644
index f15f03160f..0000000000
--- a/extensions/mediasession/src/main/res/values-eu-rES/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "Errepikatu guztiak"
- "Ez errepikatu"
- "Errepikatu bat"
-
diff --git a/extensions/mediasession/src/main/res/values-eu/strings.xml b/extensions/mediasession/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..34c1b9cde9
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-eu/strings.xml
@@ -0,0 +1,6 @@
+
+
+ Ez errepikatu
+ Errepikatu bat
+ Errepikatu guztiak
+
diff --git a/extensions/mediasession/src/main/res/values-fa/strings.xml b/extensions/mediasession/src/main/res/values-fa/strings.xml
index e37a08de64..96e8a1e819 100644
--- a/extensions/mediasession/src/main/res/values-fa/strings.xml
+++ b/extensions/mediasession/src/main/res/values-fa/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "تکرار همه"
- "تکرار هیچکدام"
- "یکبار تکرار"
+ تکرار هیچکدام
+ یکبار تکرار
+ تکرار همه
diff --git a/extensions/mediasession/src/main/res/values-fi/strings.xml b/extensions/mediasession/src/main/res/values-fi/strings.xml
index c920827976..db1aca3f5c 100644
--- a/extensions/mediasession/src/main/res/values-fi/strings.xml
+++ b/extensions/mediasession/src/main/res/values-fi/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Toista kaikki"
- "Toista ei mitään"
- "Toista yksi"
+ Ei uudelleentoistoa
+ Toista yksi uudelleen
+ Toista kaikki uudelleen
diff --git a/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml b/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml
index c5191e74a9..17e17fc8b5 100644
--- a/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml
+++ b/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Tout lire en boucle"
- "Aucune répétition"
- "Répéter un élément"
+ Ne rien lire en boucle
+ Lire une chanson en boucle
+ Tout lire en boucle
diff --git a/extensions/mediasession/src/main/res/values-fr/strings.xml b/extensions/mediasession/src/main/res/values-fr/strings.xml
index 1d76358d1f..9e35e35a0c 100644
--- a/extensions/mediasession/src/main/res/values-fr/strings.xml
+++ b/extensions/mediasession/src/main/res/values-fr/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Tout lire en boucle"
- "Ne rien lire en boucle"
- "Lire en boucle un élément"
+ Ne rien lire en boucle
+ Lire un titre en boucle
+ Tout lire en boucle
diff --git a/extensions/mediasession/src/main/res/values-gl/strings.xml b/extensions/mediasession/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..633e9669a7
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-gl/strings.xml
@@ -0,0 +1,6 @@
+
+
+ Non repetir
+ Repetir unha pista
+ Repetir todas as pistas
+
diff --git a/extensions/mediasession/src/main/res/values-gu-rIN/strings.xml b/extensions/mediasession/src/main/res/values-gu-rIN/strings.xml
deleted file mode 100644
index 0eb9cab37e..0000000000
--- a/extensions/mediasession/src/main/res/values-gu-rIN/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "બધા પુનરાવર્તન કરો"
- "કંઈ પુનરાવર્તન કરો"
- "એક પુનરાવર્તન કરો"
-
diff --git a/extensions/mediasession/src/main/res/values-gu/strings.xml b/extensions/mediasession/src/main/res/values-gu/strings.xml
new file mode 100644
index 0000000000..ab17db814e
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-gu/strings.xml
@@ -0,0 +1,6 @@
+
+
+ કોઈ રિપીટ કરતા નહીં
+ એક રિપીટ કરો
+ બધાને રિપીટ કરો
+
diff --git a/extensions/mediasession/src/main/res/values-hi/strings.xml b/extensions/mediasession/src/main/res/values-hi/strings.xml
index 8ce336d5e5..66415ed45d 100644
--- a/extensions/mediasession/src/main/res/values-hi/strings.xml
+++ b/extensions/mediasession/src/main/res/values-hi/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "सभी को दोहराएं"
- "कुछ भी न दोहराएं"
- "एक दोहराएं"
+ किसी को न दोहराएं
+ एक को दोहराएं
+ सभी को दोहराएं
diff --git a/extensions/mediasession/src/main/res/values-hr/strings.xml b/extensions/mediasession/src/main/res/values-hr/strings.xml
index 9f995ec15b..3b3f8170db 100644
--- a/extensions/mediasession/src/main/res/values-hr/strings.xml
+++ b/extensions/mediasession/src/main/res/values-hr/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Ponovi sve"
- "Bez ponavljanja"
- "Ponovi jedno"
+ Bez ponavljanja
+ Ponovi jedno
+ Ponovi sve
diff --git a/extensions/mediasession/src/main/res/values-hu/strings.xml b/extensions/mediasession/src/main/res/values-hu/strings.xml
index 2335ade72e..392959a462 100644
--- a/extensions/mediasession/src/main/res/values-hu/strings.xml
+++ b/extensions/mediasession/src/main/res/values-hu/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Összes ismétlése"
- "Nincs ismétlés"
- "Egy ismétlése"
+ Nincs ismétlés
+ Egy szám ismétlése
+ Összes szám ismétlése
diff --git a/extensions/mediasession/src/main/res/values-hy-rAM/strings.xml b/extensions/mediasession/src/main/res/values-hy-rAM/strings.xml
deleted file mode 100644
index 19a89e6c87..0000000000
--- a/extensions/mediasession/src/main/res/values-hy-rAM/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "կրկնել այն ամենը"
- "Չկրկնել"
- "Կրկնել մեկը"
-
diff --git a/extensions/mediasession/src/main/res/values-hy/strings.xml b/extensions/mediasession/src/main/res/values-hy/strings.xml
new file mode 100644
index 0000000000..ba4fff8fd2
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-hy/strings.xml
@@ -0,0 +1,6 @@
+
+
+ Չկրկնել
+ Կրկնել մեկը
+ Կրկնել բոլորը
+
diff --git a/extensions/mediasession/src/main/res/values-in/strings.xml b/extensions/mediasession/src/main/res/values-in/strings.xml
index 093a7f8576..1388877293 100644
--- a/extensions/mediasession/src/main/res/values-in/strings.xml
+++ b/extensions/mediasession/src/main/res/values-in/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Ulangi Semua"
- "Jangan Ulangi"
- "Ulangi Satu"
+ Jangan ulangi
+ Ulangi 1
+ Ulangi semua
diff --git a/extensions/mediasession/src/main/res/values-is-rIS/strings.xml b/extensions/mediasession/src/main/res/values-is-rIS/strings.xml
deleted file mode 100644
index b200abbdb2..0000000000
--- a/extensions/mediasession/src/main/res/values-is-rIS/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "Endurtaka allt"
- "Endurtaka ekkert"
- "Endurtaka eitt"
-
diff --git a/extensions/mediasession/src/main/res/values-is/strings.xml b/extensions/mediasession/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..9db4df88dd
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-is/strings.xml
@@ -0,0 +1,6 @@
+
+
+ Endurtaka ekkert
+ Endurtaka eitt
+ Endurtaka allt
+
diff --git a/extensions/mediasession/src/main/res/values-it/strings.xml b/extensions/mediasession/src/main/res/values-it/strings.xml
index c0682519f9..8922453204 100644
--- a/extensions/mediasession/src/main/res/values-it/strings.xml
+++ b/extensions/mediasession/src/main/res/values-it/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Ripeti tutti"
- "Non ripetere nessuno"
- "Ripeti uno"
+ Non ripetere nulla
+ Ripeti uno
+ Ripeti tutto
diff --git a/extensions/mediasession/src/main/res/values-iw/strings.xml b/extensions/mediasession/src/main/res/values-iw/strings.xml
index 5cf23d5a4c..193a3ac606 100644
--- a/extensions/mediasession/src/main/res/values-iw/strings.xml
+++ b/extensions/mediasession/src/main/res/values-iw/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "חזור על הכל"
- "אל תחזור על כלום"
- "חזור על פריט אחד"
+ אל תחזור על אף פריט
+ חזור על פריט אחד
+ חזור על הכול
diff --git a/extensions/mediasession/src/main/res/values-ja/strings.xml b/extensions/mediasession/src/main/res/values-ja/strings.xml
index 6f543fbdee..d1cd378d53 100644
--- a/extensions/mediasession/src/main/res/values-ja/strings.xml
+++ b/extensions/mediasession/src/main/res/values-ja/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "全曲を繰り返し"
- "繰り返しなし"
- "1曲を繰り返し"
+ リピートなし
+ 1 曲をリピート
+ 全曲をリピート
diff --git a/extensions/mediasession/src/main/res/values-ka-rGE/strings.xml b/extensions/mediasession/src/main/res/values-ka-rGE/strings.xml
deleted file mode 100644
index 96656612a7..0000000000
--- a/extensions/mediasession/src/main/res/values-ka-rGE/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "გამეორება ყველა"
- "გაიმეორეთ არცერთი"
- "გაიმეორეთ ერთი"
-
diff --git a/extensions/mediasession/src/main/res/values-ka/strings.xml b/extensions/mediasession/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..5acf78cbf2
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ka/strings.xml
@@ -0,0 +1,6 @@
+
+
+ არცერთის გამეორება
+ ერთის გამეორება
+ ყველას გამეორება
+
diff --git a/extensions/mediasession/src/main/res/values-kk-rKZ/strings.xml b/extensions/mediasession/src/main/res/values-kk-rKZ/strings.xml
deleted file mode 100644
index be4140120d..0000000000
--- a/extensions/mediasession/src/main/res/values-kk-rKZ/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "Барлығын қайталау"
- "Ешқайсысын қайталамау"
- "Біреуін қайталау"
-
diff --git a/extensions/mediasession/src/main/res/values-kk/strings.xml b/extensions/mediasession/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..d13ea893a0
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-kk/strings.xml
@@ -0,0 +1,6 @@
+
+
+ Ешқайсысын қайталамау
+ Біреуін қайталау
+ Барлығын қайталау
+
diff --git a/extensions/mediasession/src/main/res/values-km-rKH/strings.xml b/extensions/mediasession/src/main/res/values-km-rKH/strings.xml
deleted file mode 100644
index dd4b734e30..0000000000
--- a/extensions/mediasession/src/main/res/values-km-rKH/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "ធ្វើម្ដងទៀតទាំងអស់"
- "មិនធ្វើឡើងវិញ"
- "ធ្វើឡើងវិញម្ដង"
-
diff --git a/extensions/mediasession/src/main/res/values-km/strings.xml b/extensions/mediasession/src/main/res/values-km/strings.xml
new file mode 100644
index 0000000000..8cf4a2d344
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-km/strings.xml
@@ -0,0 +1,6 @@
+
+
+ មិនលេងឡើងវិញ
+ លេងឡើងវិញម្ដង
+ លេងឡើងវិញទាំងអស់
+
diff --git a/extensions/mediasession/src/main/res/values-kn-rIN/strings.xml b/extensions/mediasession/src/main/res/values-kn-rIN/strings.xml
deleted file mode 100644
index 3d79aca9e2..0000000000
--- a/extensions/mediasession/src/main/res/values-kn-rIN/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "ಎಲ್ಲವನ್ನು ಪುನರಾವರ್ತಿಸಿ"
- "ಯಾವುದನ್ನೂ ಪುನರಾವರ್ತಿಸಬೇಡಿ"
- "ಒಂದನ್ನು ಪುನರಾವರ್ತಿಸಿ"
-
diff --git a/extensions/mediasession/src/main/res/values-kn/strings.xml b/extensions/mediasession/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..2dea20044a
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-kn/strings.xml
@@ -0,0 +1,6 @@
+
+
+ ಯಾವುದನ್ನೂ ಪುನರಾವರ್ತಿಸಬೇಡಿ
+ ಒಂದನ್ನು ಪುನರಾವರ್ತಿಸಿ
+ ಎಲ್ಲವನ್ನು ಪುನರಾವರ್ತಿಸಿ
+
diff --git a/extensions/mediasession/src/main/res/values-ko/strings.xml b/extensions/mediasession/src/main/res/values-ko/strings.xml
index d269937771..b561abc1d7 100644
--- a/extensions/mediasession/src/main/res/values-ko/strings.xml
+++ b/extensions/mediasession/src/main/res/values-ko/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "전체 반복"
- "반복 안함"
- "한 항목 반복"
+ 반복 안함
+ 현재 미디어 반복
+ 모두 반복
diff --git a/extensions/mediasession/src/main/res/values-ky-rKG/strings.xml b/extensions/mediasession/src/main/res/values-ky-rKG/strings.xml
deleted file mode 100644
index a8978ecc61..0000000000
--- a/extensions/mediasession/src/main/res/values-ky-rKG/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "Баарын кайталоо"
- "Эч бирин кайталабоо"
- "Бирөөнү кайталоо"
-
diff --git a/extensions/mediasession/src/main/res/values-ky/strings.xml b/extensions/mediasession/src/main/res/values-ky/strings.xml
new file mode 100644
index 0000000000..9352c7c4ca
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ky/strings.xml
@@ -0,0 +1,6 @@
+
+
+ Кайталанбасын
+ Бирөөнү кайталоо
+ Баарын кайталоо
+
diff --git a/extensions/mediasession/src/main/res/values-lo-rLA/strings.xml b/extensions/mediasession/src/main/res/values-lo-rLA/strings.xml
deleted file mode 100644
index 950a9ba097..0000000000
--- a/extensions/mediasession/src/main/res/values-lo-rLA/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "ຫຼິ້ນຊ້ຳທັງໝົດ"
- "ບໍ່ຫຼິ້ນຊ້ຳ"
- "ຫຼິ້ນຊ້ຳ"
-
diff --git a/extensions/mediasession/src/main/res/values-lo/strings.xml b/extensions/mediasession/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..e89ee44e5e
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-lo/strings.xml
@@ -0,0 +1,6 @@
+
+
+ ບໍ່ຫຼິ້ນຊ້ຳ
+ ຫຼິ້ນຊໍ້າ
+ ຫຼິ້ນຊ້ຳທັງໝົດ
+
diff --git a/extensions/mediasession/src/main/res/values-lt/strings.xml b/extensions/mediasession/src/main/res/values-lt/strings.xml
index ae8f1cf8c3..20eb0e9b1f 100644
--- a/extensions/mediasession/src/main/res/values-lt/strings.xml
+++ b/extensions/mediasession/src/main/res/values-lt/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Kartoti viską"
- "Nekartoti nieko"
- "Kartoti vieną"
+ Nekartoti nieko
+ Kartoti vieną
+ Kartoti viską
diff --git a/extensions/mediasession/src/main/res/values-lv/strings.xml b/extensions/mediasession/src/main/res/values-lv/strings.xml
index a69f6a0ad5..44cddec124 100644
--- a/extensions/mediasession/src/main/res/values-lv/strings.xml
+++ b/extensions/mediasession/src/main/res/values-lv/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Atkārtot visu"
- "Neatkārtot nevienu"
- "Atkārtot vienu"
+ Neatkārtot nevienu
+ Atkārtot vienu
+ Atkārtot visu
diff --git a/extensions/mediasession/src/main/res/values-mk-rMK/strings.xml b/extensions/mediasession/src/main/res/values-mk-rMK/strings.xml
deleted file mode 100644
index ddf2a60c20..0000000000
--- a/extensions/mediasession/src/main/res/values-mk-rMK/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "Повтори ги сите"
- "Не повторувај ниту една"
- "Повтори една"
-
diff --git a/extensions/mediasession/src/main/res/values-mk/strings.xml b/extensions/mediasession/src/main/res/values-mk/strings.xml
new file mode 100644
index 0000000000..0906c35cc3
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-mk/strings.xml
@@ -0,0 +1,6 @@
+
+
+ Не повторувај ниту една
+ Повтори една
+ Повтори ги сите
+
diff --git a/extensions/mediasession/src/main/res/values-ml-rIN/strings.xml b/extensions/mediasession/src/main/res/values-ml-rIN/strings.xml
deleted file mode 100644
index 6f869e2931..0000000000
--- a/extensions/mediasession/src/main/res/values-ml-rIN/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "എല്ലാം ആവർത്തിക്കുക"
- "ഒന്നും ആവർത്തിക്കരുത്"
- "ഒന്ന് ആവർത്തിക്കുക"
-
diff --git a/extensions/mediasession/src/main/res/values-ml/strings.xml b/extensions/mediasession/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000000..1f3f023c88
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ml/strings.xml
@@ -0,0 +1,6 @@
+
+
+ ഒന്നും ആവർത്തിക്കരുത്
+ ഒരെണ്ണം ആവർത്തിക്കുക
+ എല്ലാം ആവർത്തിക്കുക
+
diff --git a/extensions/mediasession/src/main/res/values-mn-rMN/strings.xml b/extensions/mediasession/src/main/res/values-mn-rMN/strings.xml
deleted file mode 100644
index 8d3074b91a..0000000000
--- a/extensions/mediasession/src/main/res/values-mn-rMN/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "Бүгдийг давтах"
- "Алийг нь ч давтахгүй"
- "Нэгийг давтах"
-
diff --git a/extensions/mediasession/src/main/res/values-mn/strings.xml b/extensions/mediasession/src/main/res/values-mn/strings.xml
new file mode 100644
index 0000000000..4167e40548
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-mn/strings.xml
@@ -0,0 +1,6 @@
+
+
+ Алийг нь ч дахин тоглуулахгүй
+ Одоогийн тоглуулж буй медиаг дахин тоглуулах
+ Бүгдийг нь дахин тоглуулах
+
diff --git a/extensions/mediasession/src/main/res/values-mr-rIN/strings.xml b/extensions/mediasession/src/main/res/values-mr-rIN/strings.xml
deleted file mode 100644
index 6e4bfccc16..0000000000
--- a/extensions/mediasession/src/main/res/values-mr-rIN/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "सर्व पुनरावृत्ती करा"
- "काहीही पुनरावृत्ती करू नका"
- "एक पुनरावृत्ती करा"
-
diff --git a/extensions/mediasession/src/main/res/values-mr/strings.xml b/extensions/mediasession/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..fe42b346bf
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-mr/strings.xml
@@ -0,0 +1,6 @@
+
+
+ रीपीट करू नका
+ एक रीपीट करा
+ सर्व रीपीट करा
+
diff --git a/extensions/mediasession/src/main/res/values-ms/strings.xml b/extensions/mediasession/src/main/res/values-ms/strings.xml
new file mode 100644
index 0000000000..5735d50947
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ms/strings.xml
@@ -0,0 +1,6 @@
+
+
+ Jangan ulang
+ Ulang satu
+ Ulang semua
+
diff --git a/extensions/mediasession/src/main/res/values-my-rMM/strings.xml b/extensions/mediasession/src/main/res/values-my-rMM/strings.xml
deleted file mode 100644
index aeb1375ebf..0000000000
--- a/extensions/mediasession/src/main/res/values-my-rMM/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "အားလုံး ထပ်တလဲလဲဖွင့်ရန်"
- "ထပ်တလဲလဲမဖွင့်ရန်"
- "တစ်ခုအား ထပ်တလဲလဲဖွင့်ရန်"
-
diff --git a/extensions/mediasession/src/main/res/values-my/strings.xml b/extensions/mediasession/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..11677e06f7
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-my/strings.xml
@@ -0,0 +1,6 @@
+
+
+ မည်သည်ကိုမျှ ပြန်မကျော့ရန်
+ တစ်ခုကို ပြန်ကျော့ရန်
+ အားလုံး ပြန်ကျော့ရန်
+
diff --git a/extensions/mediasession/src/main/res/values-nb/strings.xml b/extensions/mediasession/src/main/res/values-nb/strings.xml
index 10f334b226..eab972792f 100644
--- a/extensions/mediasession/src/main/res/values-nb/strings.xml
+++ b/extensions/mediasession/src/main/res/values-nb/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Gjenta alle"
- "Ikke gjenta noen"
- "Gjenta én"
+ Ikke gjenta noen
+ Gjenta én
+ Gjenta alle
diff --git a/extensions/mediasession/src/main/res/values-ne-rNP/strings.xml b/extensions/mediasession/src/main/res/values-ne-rNP/strings.xml
deleted file mode 100644
index 6d81ce5684..0000000000
--- a/extensions/mediasession/src/main/res/values-ne-rNP/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "सबै दोहोर्याउनुहोस्"
- "कुनै पनि नदोहोर्याउनुहोस्"
- "एउटा दोहोर्याउनुहोस्"
-
diff --git a/extensions/mediasession/src/main/res/values-ne/strings.xml b/extensions/mediasession/src/main/res/values-ne/strings.xml
new file mode 100644
index 0000000000..0ef156ed57
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ne/strings.xml
@@ -0,0 +1,6 @@
+
+
+ कुनै पनि नदोहोर्याउनुहोस्
+ एउटा दोहोर्याउनुहोस्
+ सबै दोहोर्याउनुहोस्
+
diff --git a/extensions/mediasession/src/main/res/values-nl/strings.xml b/extensions/mediasession/src/main/res/values-nl/strings.xml
index 55997be098..b1309f40d6 100644
--- a/extensions/mediasession/src/main/res/values-nl/strings.xml
+++ b/extensions/mediasession/src/main/res/values-nl/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Alles herhalen"
- "Niet herhalen"
- "Eén herhalen"
+ Niets herhalen
+ Eén herhalen
+ Alles herhalen
diff --git a/extensions/mediasession/src/main/res/values-pa-rIN/strings.xml b/extensions/mediasession/src/main/res/values-pa-rIN/strings.xml
deleted file mode 100644
index 8eee0bee16..0000000000
--- a/extensions/mediasession/src/main/res/values-pa-rIN/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "ਸਭ ਨੂੰ ਦੁਹਰਾਓ"
- "ਕੋਈ ਵੀ ਨਹੀਂ ਦੁਹਰਾਓ"
- "ਇੱਕ ਦੁਹਰਾਓ"
-
diff --git a/extensions/mediasession/src/main/res/values-pa/strings.xml b/extensions/mediasession/src/main/res/values-pa/strings.xml
new file mode 100644
index 0000000000..0b7d72841c
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-pa/strings.xml
@@ -0,0 +1,6 @@
+
+
+ ਕਿਸੇ ਨੂੰ ਨਾ ਦੁਹਰਾਓ
+ ਇੱਕ ਵਾਰ ਦੁਹਰਾਓ
+ ਸਾਰਿਆਂ ਨੂੰ ਦੁਹਰਾਓ
+
diff --git a/extensions/mediasession/src/main/res/values-pl/strings.xml b/extensions/mediasession/src/main/res/values-pl/strings.xml
index 6a52d58b63..5654c0f095 100644
--- a/extensions/mediasession/src/main/res/values-pl/strings.xml
+++ b/extensions/mediasession/src/main/res/values-pl/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Powtórz wszystkie"
- "Nie powtarzaj"
- "Powtórz jeden"
+ Nie powtarzaj
+ Powtórz jeden
+ Powtórz wszystkie
diff --git a/extensions/mediasession/src/main/res/values-pt-rBR/strings.xml b/extensions/mediasession/src/main/res/values-pt-rBR/strings.xml
deleted file mode 100644
index efb8fc433f..0000000000
--- a/extensions/mediasession/src/main/res/values-pt-rBR/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "Repetir tudo"
- "Não repetir"
- "Repetir um"
-
diff --git a/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml b/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml
index efb8fc433f..612be4b8f4 100644
--- a/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml
+++ b/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Repetir tudo"
- "Não repetir"
- "Repetir um"
+ Não repetir nenhum
+ Repetir um
+ Repetir tudo
diff --git a/extensions/mediasession/src/main/res/values-pt/strings.xml b/extensions/mediasession/src/main/res/values-pt/strings.xml
index aadebbb3b0..a858ea4fc6 100644
--- a/extensions/mediasession/src/main/res/values-pt/strings.xml
+++ b/extensions/mediasession/src/main/res/values-pt/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Repetir tudo"
- "Não repetir"
- "Repetir uma"
+ Não repetir
+ Repetir uma
+ Repetir tudo
diff --git a/extensions/mediasession/src/main/res/values-ro/strings.xml b/extensions/mediasession/src/main/res/values-ro/strings.xml
index f6aee447e5..a88088fb0c 100644
--- a/extensions/mediasession/src/main/res/values-ro/strings.xml
+++ b/extensions/mediasession/src/main/res/values-ro/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Repetați toate"
- "Repetați niciuna"
- "Repetați unul"
+ Nu repetați niciunul
+ Repetați unul
+ Repetați-le pe toate
diff --git a/extensions/mediasession/src/main/res/values-ru/strings.xml b/extensions/mediasession/src/main/res/values-ru/strings.xml
index 575ad9f930..f350724813 100644
--- a/extensions/mediasession/src/main/res/values-ru/strings.xml
+++ b/extensions/mediasession/src/main/res/values-ru/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Повторять все"
- "Не повторять"
- "Повторять один элемент"
+ Не повторять
+ Повторять трек
+ Повторять все
diff --git a/extensions/mediasession/src/main/res/values-si-rLK/strings.xml b/extensions/mediasession/src/main/res/values-si-rLK/strings.xml
deleted file mode 100644
index 8e172ac268..0000000000
--- a/extensions/mediasession/src/main/res/values-si-rLK/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "සියලු නැවත"
- "කිසිවක් නැවත"
- "නැවත නැවත එක්"
-
diff --git a/extensions/mediasession/src/main/res/values-si/strings.xml b/extensions/mediasession/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..0d86d38e7f
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-si/strings.xml
@@ -0,0 +1,6 @@
+
+
+ කිසිවක් පුනරාවර්තනය නොකරන්න
+ එකක් පුනරාවර්තනය කරන්න
+ සියල්ල නැවත කරන්න
+
diff --git a/extensions/mediasession/src/main/res/values-sk/strings.xml b/extensions/mediasession/src/main/res/values-sk/strings.xml
index 5d092003e5..9c0235daec 100644
--- a/extensions/mediasession/src/main/res/values-sk/strings.xml
+++ b/extensions/mediasession/src/main/res/values-sk/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Opakovať všetko"
- "Neopakovať"
- "Opakovať jednu položku"
+ Neopakovať
+ Opakovať jednu
+ Opakovať všetko
diff --git a/extensions/mediasession/src/main/res/values-sl/strings.xml b/extensions/mediasession/src/main/res/values-sl/strings.xml
index ecac3800c8..9ee3add8bc 100644
--- a/extensions/mediasession/src/main/res/values-sl/strings.xml
+++ b/extensions/mediasession/src/main/res/values-sl/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Ponovi vse"
- "Ne ponovi"
- "Ponovi eno"
+ Brez ponavljanja
+ Ponavljanje ene
+ Ponavljanje vseh
diff --git a/extensions/mediasession/src/main/res/values-sq-rAL/strings.xml b/extensions/mediasession/src/main/res/values-sq-rAL/strings.xml
deleted file mode 100644
index 6da24cc4c7..0000000000
--- a/extensions/mediasession/src/main/res/values-sq-rAL/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "Përsërit të gjithë"
- "Përsëritni asnjë"
- "Përsëritni një"
-
diff --git a/extensions/mediasession/src/main/res/values-sq/strings.xml b/extensions/mediasession/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..2461dcf0ca
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-sq/strings.xml
@@ -0,0 +1,6 @@
+
+
+ Mos përsërit asnjë
+ Përsërit një
+ Përsërit të gjitha
+
diff --git a/extensions/mediasession/src/main/res/values-sr/strings.xml b/extensions/mediasession/src/main/res/values-sr/strings.xml
index 881cb2703b..71edd5c341 100644
--- a/extensions/mediasession/src/main/res/values-sr/strings.xml
+++ b/extensions/mediasession/src/main/res/values-sr/strings.xml
@@ -1,18 +1,6 @@
-
-
+
+ Не понављај ниједну
+ Понови једну
+ Понови све
diff --git a/extensions/mediasession/src/main/res/values-sv/strings.xml b/extensions/mediasession/src/main/res/values-sv/strings.xml
index 3a7bb630aa..0956ac9fc7 100644
--- a/extensions/mediasession/src/main/res/values-sv/strings.xml
+++ b/extensions/mediasession/src/main/res/values-sv/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Upprepa alla"
- "Upprepa inga"
- "Upprepa en"
+ Upprepa inga
+ Upprepa en
+ Upprepa alla
diff --git a/extensions/mediasession/src/main/res/values-sw/strings.xml b/extensions/mediasession/src/main/res/values-sw/strings.xml
index 726012ab88..0010774a6f 100644
--- a/extensions/mediasession/src/main/res/values-sw/strings.xml
+++ b/extensions/mediasession/src/main/res/values-sw/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Rudia zote"
- "Usirudie Yoyote"
- "Rudia Moja"
+ Usirudie yoyote
+ Rudia moja
+ Rudia zote
diff --git a/extensions/mediasession/src/main/res/values-ta-rIN/strings.xml b/extensions/mediasession/src/main/res/values-ta-rIN/strings.xml
deleted file mode 100644
index 9364bc0be2..0000000000
--- a/extensions/mediasession/src/main/res/values-ta-rIN/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "அனைத்தையும் மீண்டும் இயக்கு"
- "எதையும் மீண்டும் இயக்காதே"
- "ஒன்றை மட்டும் மீண்டும் இயக்கு"
-
diff --git a/extensions/mediasession/src/main/res/values-ta/strings.xml b/extensions/mediasession/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..b6fbcca4a1
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ta/strings.xml
@@ -0,0 +1,6 @@
+
+
+ எதையும் மீண்டும் இயக்காதே
+ இதை மட்டும் மீண்டும் இயக்கு
+ அனைத்தையும் மீண்டும் இயக்கு
+
diff --git a/extensions/mediasession/src/main/res/values-te-rIN/strings.xml b/extensions/mediasession/src/main/res/values-te-rIN/strings.xml
deleted file mode 100644
index b7ee7345d5..0000000000
--- a/extensions/mediasession/src/main/res/values-te-rIN/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "అన్నీ పునరావృతం చేయి"
- "ఏదీ పునరావృతం చేయవద్దు"
- "ఒకదాన్ని పునరావృతం చేయి"
-
diff --git a/extensions/mediasession/src/main/res/values-te/strings.xml b/extensions/mediasession/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..b1249c7400
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-te/strings.xml
@@ -0,0 +1,6 @@
+
+
+ దేన్నీ పునరావృతం చేయకండి
+ ఒకదాన్ని పునరావృతం చేయండి
+ అన్నింటినీ పునరావృతం చేయండి
+
diff --git a/extensions/mediasession/src/main/res/values-th/strings.xml b/extensions/mediasession/src/main/res/values-th/strings.xml
index af502b3a4c..bec0410a44 100644
--- a/extensions/mediasession/src/main/res/values-th/strings.xml
+++ b/extensions/mediasession/src/main/res/values-th/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "เล่นซ้ำทั้งหมด"
- "ไม่เล่นซ้ำ"
- "เล่นซ้ำรายการเดียว"
+ ไม่เล่นซ้ำ
+ เล่นซ้ำเพลงเดียว
+ เล่นซ้ำทั้งหมด
diff --git a/extensions/mediasession/src/main/res/values-tl/strings.xml b/extensions/mediasession/src/main/res/values-tl/strings.xml
index 239972a4c7..6f8d8f4f88 100644
--- a/extensions/mediasession/src/main/res/values-tl/strings.xml
+++ b/extensions/mediasession/src/main/res/values-tl/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Ulitin Lahat"
- "Walang Uulitin"
- "Ulitin ang Isa"
+ Walang uulitin
+ Mag-ulit ng isa
+ Ulitin lahat
diff --git a/extensions/mediasession/src/main/res/values-tr/strings.xml b/extensions/mediasession/src/main/res/values-tr/strings.xml
index 89a98b1ed9..20c05d9fa6 100644
--- a/extensions/mediasession/src/main/res/values-tr/strings.xml
+++ b/extensions/mediasession/src/main/res/values-tr/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Tümünü Tekrarla"
- "Hiçbirini Tekrarlama"
- "Birini Tekrarla"
+ Hiçbirini tekrarlama
+ Bir şarkıyı tekrarla
+ Tümünü tekrarla
diff --git a/extensions/mediasession/src/main/res/values-uk/strings.xml b/extensions/mediasession/src/main/res/values-uk/strings.xml
index 4e1d25eb8a..44db07ef9c 100644
--- a/extensions/mediasession/src/main/res/values-uk/strings.xml
+++ b/extensions/mediasession/src/main/res/values-uk/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Повторити все"
- "Не повторювати"
- "Повторити один елемент"
+ Не повторювати
+ Повторити 1
+ Повторити всі
diff --git a/extensions/mediasession/src/main/res/values-ur-rPK/strings.xml b/extensions/mediasession/src/main/res/values-ur-rPK/strings.xml
deleted file mode 100644
index ab2631a4ec..0000000000
--- a/extensions/mediasession/src/main/res/values-ur-rPK/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "سبھی کو دہرائیں"
- "کسی کو نہ دہرائیں"
- "ایک کو دہرائیں"
-
diff --git a/extensions/mediasession/src/main/res/values-ur/strings.xml b/extensions/mediasession/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..3860986e9c
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-ur/strings.xml
@@ -0,0 +1,6 @@
+
+
+ کسی کو نہ دہرائیں
+ ایک کو دہرائیں
+ سبھی کو دہرائیں
+
diff --git a/extensions/mediasession/src/main/res/values-uz-rUZ/strings.xml b/extensions/mediasession/src/main/res/values-uz-rUZ/strings.xml
deleted file mode 100644
index c32d00af8e..0000000000
--- a/extensions/mediasession/src/main/res/values-uz-rUZ/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- "Barchasini takrorlash"
- "Takrorlamaslik"
- "Bir marta takrorlash"
-
diff --git a/extensions/mediasession/src/main/res/values-uz/strings.xml b/extensions/mediasession/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..3424c9f583
--- /dev/null
+++ b/extensions/mediasession/src/main/res/values-uz/strings.xml
@@ -0,0 +1,6 @@
+
+
+ Takrorlanmasin
+ Bittasini takrorlash
+ Hammasini takrorlash
+
diff --git a/extensions/mediasession/src/main/res/values-vi/strings.xml b/extensions/mediasession/src/main/res/values-vi/strings.xml
index dabc9e05d5..9de007cdb9 100644
--- a/extensions/mediasession/src/main/res/values-vi/strings.xml
+++ b/extensions/mediasession/src/main/res/values-vi/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Lặp lại tất cả"
- "Không lặp lại"
- "Lặp lại một mục"
+ Không lặp lại
+ Lặp lại một
+ Lặp lại tất cả
diff --git a/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml b/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml
index beb3403cb9..4d1f1346b9 100644
--- a/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml
+++ b/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "重复播放全部"
- "不重复播放"
- "重复播放单个视频"
+ 不重复播放
+ 重复播放一项
+ 全部重复播放
diff --git a/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml b/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml
index 775cd6441c..e0ec62c533 100644
--- a/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml
+++ b/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "重複播放所有媒體項目"
- "不重複播放任何媒體項目"
- "重複播放一個媒體項目"
+ 不重複播放
+ 重複播放一個
+ 全部重複播放
diff --git a/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml b/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml
index d3789f4145..5b91fbd9fe 100644
--- a/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml
+++ b/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "重複播放所有媒體項目"
- "不重複播放"
- "重複播放單一媒體項目"
+ 不重複播放
+ 重複播放單一項目
+ 重複播放所有項目
diff --git a/extensions/mediasession/src/main/res/values-zu/strings.xml b/extensions/mediasession/src/main/res/values-zu/strings.xml
index 789b6fecb4..a6299ba987 100644
--- a/extensions/mediasession/src/main/res/values-zu/strings.xml
+++ b/extensions/mediasession/src/main/res/values-zu/strings.xml
@@ -1,21 +1,6 @@
-
-
+
- "Phinda konke"
- "Ungaphindi lutho"
- "Phida okukodwa"
+ Phinda okungekho
+ Phinda okukodwa
+ Phinda konke
diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle
index 2f7d84d33b..2b653c3f0e 100644
--- a/extensions/okhttp/build.gradle
+++ b/extensions/okhttp/build.gradle
@@ -23,19 +23,12 @@ android {
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
}
-
- lintOptions {
- // See: https://github.com/square/okio/issues/58
- warning 'InvalidPackage'
- }
}
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
- api('com.squareup.okhttp3:okhttp:3.10.0') {
- exclude group: 'org.json'
- }
+ api 'com.squareup.okhttp3:okhttp:3.10.0'
}
ext {
diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java
index 4094b3a5d2..f2898005c1 100644
--- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java
+++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java
@@ -325,7 +325,7 @@ public class OkHttpDataSource implements HttpDataSource {
while (bytesSkipped != bytesToSkip) {
int readLength = (int) Math.min(bytesToSkip - bytesSkipped, skipBuffer.length);
int read = responseByteStream.read(skipBuffer, 0, readLength);
- if (Thread.interrupted()) {
+ if (Thread.currentThread().isInterrupted()) {
throw new InterruptedIOException();
}
if (read == -1) {
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java
index f3720fee0a..7fde7678b8 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java
@@ -169,8 +169,15 @@ public class LibvpxVideoRenderer extends BaseRenderer {
public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs,
Handler eventHandler, VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify) {
- this(scaleToFit, allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify,
- null, false, false);
+ this(
+ scaleToFit,
+ allowedJoiningTimeMs,
+ eventHandler,
+ eventListener,
+ maxDroppedFramesToNotify,
+ /* drmSessionManager= */ null,
+ /* playClearSamplesWithoutKeys= */ false,
+ /* disableLoopFilter= */ false);
}
/**
diff --git a/library/core/build.gradle b/library/core/build.gradle
index fe6045c2e7..52249220e0 100644
--- a/library/core/build.gradle
+++ b/library/core/build.gradle
@@ -42,10 +42,15 @@ android {
// testCoverageEnabled = true
// }
}
+
+ lintOptions {
+ lintConfig file("../../checker-framework-lint.xml")
+ }
}
dependencies {
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
+ implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion
androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestImplementation 'com.google.truth:truth:' + truthVersion
@@ -54,6 +59,8 @@ dependencies {
testImplementation 'junit:junit:' + junitVersion
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
+ testImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion
+ testAnnotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion
}
ext {
diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt
index 7dc81c3f73..fe204822a8 100644
--- a/library/core/proguard-rules.txt
+++ b/library/core/proguard-rules.txt
@@ -29,3 +29,6 @@
-keepclassmembers class com.google.android.exoplayer2.ext.rtmp.RtmpDataSource {
();
}
+
+# Don't warn about checkerframework
+-dontwarn org.checkerframework.**
diff --git a/library/core/src/main/AndroidManifest.xml b/library/core/src/main/AndroidManifest.xml
index 430930a3ca..1a6971fdcc 100644
--- a/library/core/src/main/AndroidManifest.xml
+++ b/library/core/src/main/AndroidManifest.xml
@@ -14,4 +14,7 @@
limitations under the License.
-->
-
+
+
+
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java
index 045f3bfc6e..de210f5eff 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/C.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java
@@ -64,6 +64,9 @@ public final class C {
*/
public static final int LENGTH_UNSET = -1;
+ /** Represents an unset or unknown percentage. */
+ public static final int PERCENTAGE_UNSET = -1;
+
/**
* The number of microseconds in one second.
*/
@@ -490,6 +493,8 @@ public final class C {
* A data type constant for time synchronization data.
*/
public static final int DATA_TYPE_TIME_SYNCHRONIZATION = 5;
+ /** A data type constant for ads loader data. */
+ public static final int DATA_TYPE_AD = 6;
/**
* Applications or extensions may define custom {@code DATA_TYPE_*} constants greater than or
* equal to this value.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
index 940d9376c1..6cab53b78a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
@@ -199,9 +199,16 @@ public class DefaultRenderersFactory implements RenderersFactory {
long allowedVideoJoiningTimeMs, Handler eventHandler,
VideoRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode,
ArrayList out) {
- out.add(new MediaCodecVideoRenderer(context, MediaCodecSelector.DEFAULT,
- allowedVideoJoiningTimeMs, drmSessionManager, false, eventHandler, eventListener,
- MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));
+ out.add(
+ new MediaCodecVideoRenderer(
+ context,
+ MediaCodecSelector.DEFAULT,
+ allowedVideoJoiningTimeMs,
+ drmSessionManager,
+ /* playClearSamplesWithoutKeys= */ false,
+ eventHandler,
+ eventListener,
+ MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));
if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
return;
@@ -259,8 +266,16 @@ public class DefaultRenderersFactory implements RenderersFactory {
AudioProcessor[] audioProcessors, Handler eventHandler,
AudioRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode,
ArrayList out) {
- out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true,
- eventHandler, eventListener, AudioCapabilities.getCapabilities(context), audioProcessors));
+ out.add(
+ new MediaCodecAudioRenderer(
+ context,
+ MediaCodecSelector.DEFAULT,
+ drmSessionManager,
+ /* playClearSamplesWithoutKeys= */ false,
+ eventHandler,
+ eventListener,
+ AudioCapabilities.getCapabilities(context),
+ audioProcessors));
if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
return;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
index 39a6243933..6d8dd5b7a8 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
@@ -185,6 +185,10 @@ public interface ExoPlayer extends Player {
*/
Looper getPlaybackLooper();
+ @Override
+ @Nullable
+ ExoPlaybackException getPlaybackError();
+
/**
* Prepares the player to play the provided {@link MediaSource}. Equivalent to
* {@code prepare(mediaSource, true, true)}.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
index daabbd5a72..5ca5994b6e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
@@ -61,6 +61,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
private boolean hasPendingPrepare;
private boolean hasPendingSeek;
private PlaybackParameters playbackParameters;
+ private @Nullable ExoPlaybackException playbackError;
// Playback information when there is no pending seek/set source operation.
private PlaybackInfo playbackInfo;
@@ -156,6 +157,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
return playbackInfo.playbackState;
}
+ @Override
+ public @Nullable ExoPlaybackException getPlaybackError() {
+ return playbackError;
+ }
+
@Override
public void prepare(MediaSource mediaSource) {
prepare(mediaSource, true, true);
@@ -163,6 +169,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
+ playbackError = null;
PlaybackInfo playbackInfo =
getResetPlaybackInfo(
resetPosition, resetState, /* playbackState= */ Player.STATE_BUFFERING);
@@ -325,6 +332,9 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public void stop(boolean reset) {
+ if (reset) {
+ playbackError = null;
+ }
PlaybackInfo playbackInfo =
getResetPlaybackInfo(
/* resetPosition= */ reset,
@@ -560,9 +570,9 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
break;
case ExoPlayerImplInternal.MSG_ERROR:
- ExoPlaybackException exception = (ExoPlaybackException) msg.obj;
+ playbackError = (ExoPlaybackException) msg.obj;
for (Player.EventListener listener : listeners) {
- listener.onPlayerError(exception);
+ listener.onPlayerError(playbackError);
}
break;
default:
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
index e91495227e..98d5fe91b7 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
- public static final String VERSION = "2.7.3";
+ public static final String VERSION = "2.8.0";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- public static final String VERSION_SLASHY = "ExoPlayerLib/2.7.3";
+ public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.0";
/**
* The version of the library expressed as an integer, for example 1002003.
@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- public static final int VERSION_INT = 2007003;
+ public static final int VERSION_INT = 2008000;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java
index b18225fc70..61d416da09 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2;
import android.os.Parcel;
import android.os.Parcelable;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.MimeTypes;
@@ -43,29 +44,21 @@ public final class Format implements Parcelable {
*/
public static final long OFFSET_SAMPLE_RELATIVE = Long.MAX_VALUE;
- /**
- * An identifier for the format, or null if unknown or not applicable.
- */
- public final String id;
+ /** An identifier for the format, or null if unknown or not applicable. */
+ public final @Nullable String id;
/**
* The average bandwidth in bits per second, or {@link #NO_VALUE} if unknown or not applicable.
*/
public final int bitrate;
- /**
- * Codecs of the format as described in RFC 6381, or null if unknown or not applicable.
- */
- public final String codecs;
- /**
- * Metadata, or null if unknown or not applicable.
- */
- public final Metadata metadata;
+ /** Codecs of the format as described in RFC 6381, or null if unknown or not applicable. */
+ public final @Nullable String codecs;
+ /** Metadata, or null if unknown or not applicable. */
+ public final @Nullable Metadata metadata;
// Container specific.
- /**
- * The mime type of the container, or null if unknown or not applicable.
- */
- public final String containerMimeType;
+ /** The mime type of the container, or null if unknown or not applicable. */
+ public final @Nullable String containerMimeType;
// Elementary stream specific.
@@ -73,7 +66,7 @@ public final class Format implements Parcelable {
* The mime type of the elementary stream (i.e. the individual samples), or null if unknown or not
* applicable.
*/
- public final String sampleMimeType;
+ public final @Nullable String sampleMimeType;
/**
* The maximum size of a buffer of data (typically one sample), or {@link #NO_VALUE} if unknown or
* not applicable.
@@ -84,10 +77,8 @@ public final class Format implements Parcelable {
* if initialization data is not required.
*/
public final List initializationData;
- /**
- * DRM initialization data if the stream is protected, or null otherwise.
- */
- public final DrmInitData drmInitData;
+ /** DRM initialization data if the stream is protected, or null otherwise. */
+ public final @Nullable DrmInitData drmInitData;
// Video specific.
@@ -117,14 +108,10 @@ public final class Format implements Parcelable {
*/
@C.StereoMode
public final int stereoMode;
- /**
- * The projection data for 360/VR video, or null if not applicable.
- */
- public final byte[] projectionData;
- /**
- * The color metadata associated with the video, helps with accurate color reproduction.
- */
- public final ColorInfo colorInfo;
+ /** The projection data for 360/VR video, or null if not applicable. */
+ public final @Nullable byte[] projectionData;
+ /** The color metadata associated with the video, helps with accurate color reproduction. */
+ public final @Nullable ColorInfo colorInfo;
// Audio specific.
@@ -171,10 +158,8 @@ public final class Format implements Parcelable {
@C.SelectionFlags
public final int selectionFlags;
- /**
- * The language, or null if unknown or not applicable.
- */
- public final String language;
+ /** The language, or null if unknown or not applicable. */
+ public final @Nullable String language;
/**
* The Accessibility channel, or {@link #NO_VALUE} if not known or applicable.
@@ -186,36 +171,72 @@ public final class Format implements Parcelable {
// Video.
- public static Format createVideoContainerFormat(String id, String containerMimeType,
- String sampleMimeType, String codecs, int bitrate, int width, int height,
- float frameRate, List initializationData, @C.SelectionFlags int selectionFlags) {
+ public static Format createVideoContainerFormat(
+ @Nullable String id,
+ @Nullable String containerMimeType,
+ String sampleMimeType,
+ String codecs,
+ int bitrate,
+ int width,
+ int height,
+ float frameRate,
+ List initializationData,
+ @C.SelectionFlags int selectionFlags) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, width,
height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, selectionFlags, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
initializationData, null, null);
}
- public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs,
- int bitrate, int maxInputSize, int width, int height, float frameRate,
- List initializationData, DrmInitData drmInitData) {
+ public static Format createVideoSampleFormat(
+ @Nullable String id,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ int maxInputSize,
+ int width,
+ int height,
+ float frameRate,
+ List initializationData,
+ @Nullable DrmInitData drmInitData) {
return createVideoSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, width,
height, frameRate, initializationData, NO_VALUE, NO_VALUE, drmInitData);
}
- public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs,
- int bitrate, int maxInputSize, int width, int height, float frameRate,
- List initializationData, int rotationDegrees, float pixelWidthHeightRatio,
- DrmInitData drmInitData) {
+ public static Format createVideoSampleFormat(
+ @Nullable String id,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ int maxInputSize,
+ int width,
+ int height,
+ float frameRate,
+ List initializationData,
+ int rotationDegrees,
+ float pixelWidthHeightRatio,
+ @Nullable DrmInitData drmInitData) {
return createVideoSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, width,
height, frameRate, initializationData, rotationDegrees, pixelWidthHeightRatio, null,
NO_VALUE, null, drmInitData);
}
- public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs,
- int bitrate, int maxInputSize, int width, int height, float frameRate,
- List initializationData, int rotationDegrees, float pixelWidthHeightRatio,
- byte[] projectionData, @C.StereoMode int stereoMode, ColorInfo colorInfo,
- DrmInitData drmInitData) {
+ public static Format createVideoSampleFormat(
+ @Nullable String id,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ int maxInputSize,
+ int width,
+ int height,
+ float frameRate,
+ List initializationData,
+ int rotationDegrees,
+ float pixelWidthHeightRatio,
+ byte[] projectionData,
+ @C.StereoMode int stereoMode,
+ @Nullable ColorInfo colorInfo,
+ @Nullable DrmInitData drmInitData) {
return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, width, height,
frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
colorInfo, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, NO_VALUE,
@@ -224,37 +245,73 @@ public final class Format implements Parcelable {
// Audio.
- public static Format createAudioContainerFormat(String id, String containerMimeType,
- String sampleMimeType, String codecs, int bitrate, int channelCount, int sampleRate,
- List initializationData, @C.SelectionFlags int selectionFlags, String language) {
+ public static Format createAudioContainerFormat(
+ @Nullable String id,
+ @Nullable String containerMimeType,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ int channelCount,
+ int sampleRate,
+ List initializationData,
+ @C.SelectionFlags int selectionFlags,
+ @Nullable String language) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, channelCount, sampleRate,
NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
initializationData, null, null);
}
- public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs,
- int bitrate, int maxInputSize, int channelCount, int sampleRate,
- List initializationData, DrmInitData drmInitData,
- @C.SelectionFlags int selectionFlags, String language) {
+ public static Format createAudioSampleFormat(
+ @Nullable String id,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ int maxInputSize,
+ int channelCount,
+ int sampleRate,
+ List initializationData,
+ @Nullable DrmInitData drmInitData,
+ @C.SelectionFlags int selectionFlags,
+ @Nullable String language) {
return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount,
sampleRate, NO_VALUE, initializationData, drmInitData, selectionFlags, language);
}
- public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs,
- int bitrate, int maxInputSize, int channelCount, int sampleRate,
- @C.PcmEncoding int pcmEncoding, List initializationData, DrmInitData drmInitData,
- @C.SelectionFlags int selectionFlags, String language) {
+ public static Format createAudioSampleFormat(
+ @Nullable String id,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ int maxInputSize,
+ int channelCount,
+ int sampleRate,
+ @C.PcmEncoding int pcmEncoding,
+ List initializationData,
+ @Nullable DrmInitData drmInitData,
+ @C.SelectionFlags int selectionFlags,
+ @Nullable String language) {
return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount,
sampleRate, pcmEncoding, NO_VALUE, NO_VALUE, initializationData, drmInitData,
selectionFlags, language, null);
}
- public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs,
- int bitrate, int maxInputSize, int channelCount, int sampleRate,
- @C.PcmEncoding int pcmEncoding, int encoderDelay, int encoderPadding,
- List initializationData, DrmInitData drmInitData,
- @C.SelectionFlags int selectionFlags, String language, Metadata metadata) {
+ public static Format createAudioSampleFormat(
+ @Nullable String id,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ int maxInputSize,
+ int channelCount,
+ int sampleRate,
+ @C.PcmEncoding int pcmEncoding,
+ int encoderDelay,
+ int encoderPadding,
+ List initializationData,
+ @Nullable DrmInitData drmInitData,
+ @C.SelectionFlags int selectionFlags,
+ @Nullable String language,
+ @Nullable Metadata metadata) {
return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, channelCount, sampleRate, pcmEncoding,
encoderDelay, encoderPadding, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
@@ -263,50 +320,87 @@ public final class Format implements Parcelable {
// Text.
- public static Format createTextContainerFormat(String id, String containerMimeType,
- String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags,
- String language) {
+ public static Format createTextContainerFormat(
+ @Nullable String id,
+ @Nullable String containerMimeType,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ @C.SelectionFlags int selectionFlags,
+ @Nullable String language) {
return createTextContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate,
selectionFlags, language, NO_VALUE);
}
- public static Format createTextContainerFormat(String id, String containerMimeType,
- String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags,
- String language, int accessibilityChannel) {
+ public static Format createTextContainerFormat(
+ @Nullable String id,
+ @Nullable String containerMimeType,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ @C.SelectionFlags int selectionFlags,
+ @Nullable String language,
+ int accessibilityChannel) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, accessibilityChannel,
OFFSET_SAMPLE_RELATIVE, null, null, null);
}
- public static Format createTextSampleFormat(String id, String sampleMimeType,
- @C.SelectionFlags int selectionFlags, String language) {
+ public static Format createTextSampleFormat(
+ @Nullable String id,
+ String sampleMimeType,
+ @C.SelectionFlags int selectionFlags,
+ @Nullable String language) {
return createTextSampleFormat(id, sampleMimeType, selectionFlags, language, null);
}
- public static Format createTextSampleFormat(String id, String sampleMimeType,
- @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData) {
+ public static Format createTextSampleFormat(
+ @Nullable String id,
+ String sampleMimeType,
+ @C.SelectionFlags int selectionFlags,
+ @Nullable String language,
+ @Nullable DrmInitData drmInitData) {
return createTextSampleFormat(id, sampleMimeType, null, NO_VALUE, selectionFlags, language,
NO_VALUE, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.emptyList());
}
- public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
- int bitrate, @C.SelectionFlags int selectionFlags, String language, int accessibilityChannel,
- DrmInitData drmInitData) {
+ public static Format createTextSampleFormat(
+ @Nullable String id,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ @C.SelectionFlags int selectionFlags,
+ @Nullable String language,
+ int accessibilityChannel,
+ @Nullable DrmInitData drmInitData) {
return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.emptyList());
}
- public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
- int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData,
+ public static Format createTextSampleFormat(
+ @Nullable String id,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ @C.SelectionFlags int selectionFlags,
+ @Nullable String language,
+ @Nullable DrmInitData drmInitData,
long subsampleOffsetUs) {
return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
NO_VALUE, drmInitData, subsampleOffsetUs, Collections.emptyList());
}
- public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
- int bitrate, @C.SelectionFlags int selectionFlags, String language,
- int accessibilityChannel, DrmInitData drmInitData, long subsampleOffsetUs,
+ public static Format createTextSampleFormat(
+ @Nullable String id,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ @C.SelectionFlags int selectionFlags,
+ @Nullable String language,
+ int accessibilityChannel,
+ @Nullable DrmInitData drmInitData,
+ long subsampleOffsetUs,
List initializationData) {
return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
@@ -317,14 +411,14 @@ public final class Format implements Parcelable {
// Image.
public static Format createImageSampleFormat(
- String id,
- String sampleMimeType,
- String codecs,
+ @Nullable String id,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
int bitrate,
@C.SelectionFlags int selectionFlags,
List initializationData,
- String language,
- DrmInitData drmInitData) {
+ @Nullable String language,
+ @Nullable DrmInitData drmInitData) {
return new Format(
id,
null,
@@ -356,36 +450,65 @@ public final class Format implements Parcelable {
// Generic.
- public static Format createContainerFormat(String id, String containerMimeType,
- String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags,
- String language) {
+ public static Format createContainerFormat(
+ @Nullable String id,
+ @Nullable String containerMimeType,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ @C.SelectionFlags int selectionFlags,
+ @Nullable String language) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, null,
null);
}
- public static Format createSampleFormat(String id, String sampleMimeType,
- long subsampleOffsetUs) {
+ public static Format createSampleFormat(
+ @Nullable String id, @Nullable String sampleMimeType, long subsampleOffsetUs) {
return new Format(id, null, sampleMimeType, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, 0, null, NO_VALUE, subsampleOffsetUs, null, null, null);
}
- public static Format createSampleFormat(String id, String sampleMimeType, String codecs,
- int bitrate, DrmInitData drmInitData) {
+ public static Format createSampleFormat(
+ @Nullable String id,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ @Nullable DrmInitData drmInitData) {
return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, drmInitData, null);
}
- /* package */ Format(String id, String containerMimeType, String sampleMimeType, String codecs,
- int bitrate, int maxInputSize, int width, int height, float frameRate, int rotationDegrees,
- float pixelWidthHeightRatio, byte[] projectionData, @C.StereoMode int stereoMode,
- ColorInfo colorInfo, int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding,
- int encoderDelay, int encoderPadding, @C.SelectionFlags int selectionFlags, String language,
- int accessibilityChannel, long subsampleOffsetUs, List initializationData,
- DrmInitData drmInitData, Metadata metadata) {
+ /* package */ Format(
+ @Nullable String id,
+ @Nullable String containerMimeType,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ int maxInputSize,
+ int width,
+ int height,
+ float frameRate,
+ int rotationDegrees,
+ float pixelWidthHeightRatio,
+ @Nullable byte[] projectionData,
+ @C.StereoMode int stereoMode,
+ @Nullable ColorInfo colorInfo,
+ int channelCount,
+ int sampleRate,
+ @C.PcmEncoding int pcmEncoding,
+ int encoderDelay,
+ int encoderPadding,
+ @C.SelectionFlags int selectionFlags,
+ @Nullable String language,
+ int accessibilityChannel,
+ long subsampleOffsetUs,
+ @Nullable List initializationData,
+ @Nullable DrmInitData drmInitData,
+ @Nullable Metadata metadata) {
this.id = id;
this.containerMimeType = containerMimeType;
this.sampleMimeType = sampleMimeType;
@@ -468,14 +591,14 @@ public final class Format implements Parcelable {
}
public Format copyWithContainerInfo(
- String id,
- String sampleMimeType,
- String codecs,
+ @Nullable String id,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
int bitrate,
int width,
int height,
@C.SelectionFlags int selectionFlags,
- String language) {
+ @Nullable String language) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
@@ -512,7 +635,7 @@ public final class Format implements Parcelable {
drmInitData, metadata);
}
- public Format copyWithDrmInitData(DrmInitData drmInitData) {
+ public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
@@ -520,7 +643,7 @@ public final class Format implements Parcelable {
drmInitData, metadata);
}
- public Format copyWithMetadata(Metadata metadata) {
+ public Format copyWithMetadata(@Nullable Metadata metadata) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
@@ -574,7 +697,7 @@ public final class Format implements Parcelable {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java
index 85872339a3..328816d709 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java
@@ -459,6 +459,17 @@ public interface Player {
*/
int getPlaybackState();
+ /**
+ * Returns the error that caused playback to fail. This is the same error that will have been
+ * reported via {@link Player.EventListener#onPlayerError(ExoPlaybackException)} at the time of
+ * failure. It can be queried using this method until {@code stop(true)} is called or the player
+ * is re-prepared.
+ *
+ * @return The error, or {@code null}.
+ */
+ @Nullable
+ ExoPlaybackException getPlaybackError();
+
/**
* Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
*
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
index b998027eb3..482e2c970a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
@@ -47,6 +47,7 @@ import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;
@@ -181,8 +182,8 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock);
analyticsCollector = analyticsCollectorFactory.createAnalyticsCollector(player, clock);
addListener(analyticsCollector);
- addVideoDebugListener(analyticsCollector);
- addAudioDebugListener(analyticsCollector);
+ videoDebugListeners.add(analyticsCollector);
+ audioDebugListeners.add(analyticsCollector);
addMetadataOutput(analyticsCollector);
if (drmSessionManager instanceof DefaultDrmSessionManager) {
((DefaultDrmSessionManager) drmSessionManager).addListener(eventHandler, analyticsCollector);
@@ -534,10 +535,20 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
removeTextOutput(output);
}
+ /**
+ * Adds a {@link MetadataOutput} to receive metadata.
+ *
+ * @param listener The output to register.
+ */
public void addMetadataOutput(MetadataOutput listener) {
metadataOutputs.add(listener);
}
+ /**
+ * Removes a {@link MetadataOutput}.
+ *
+ * @param listener The output to remove.
+ */
public void removeMetadataOutput(MetadataOutput listener) {
metadataOutputs.remove(listener);
}
@@ -550,7 +561,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
*/
@Deprecated
public void setMetadataOutput(MetadataOutput output) {
- metadataOutputs.clear();
+ metadataOutputs.retainAll(Collections.singleton(analyticsCollector));
if (output != null) {
addMetadataOutput(output);
}
@@ -568,65 +579,61 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
}
/**
- * Sets a listener to receive debug events from the video renderer.
- *
- * @param listener The listener.
- * @deprecated Use {@link #addVideoDebugListener(VideoRendererEventListener)}.
+ * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} to get more detailed debug
+ * information.
*/
@Deprecated
public void setVideoDebugListener(VideoRendererEventListener listener) {
- videoDebugListeners.clear();
+ videoDebugListeners.retainAll(Collections.singleton(analyticsCollector));
if (listener != null) {
addVideoDebugListener(listener);
}
}
/**
- * Adds a listener to receive debug events from the video renderer.
- *
- * @param listener The listener.
+ * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} to get more detailed debug
+ * information.
*/
+ @Deprecated
public void addVideoDebugListener(VideoRendererEventListener listener) {
videoDebugListeners.add(listener);
}
/**
- * Removes a listener to receive debug events from the video renderer.
- *
- * @param listener The listener.
+ * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} and {@link
+ * #removeAnalyticsListener(AnalyticsListener)} to get more detailed debug information.
*/
+ @Deprecated
public void removeVideoDebugListener(VideoRendererEventListener listener) {
videoDebugListeners.remove(listener);
}
/**
- * Sets a listener to receive debug events from the audio renderer.
- *
- * @param listener The listener.
- * @deprecated Use {@link #addAudioDebugListener(AudioRendererEventListener)}.
+ * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} to get more detailed debug
+ * information.
*/
@Deprecated
public void setAudioDebugListener(AudioRendererEventListener listener) {
- audioDebugListeners.clear();
+ audioDebugListeners.retainAll(Collections.singleton(analyticsCollector));
if (listener != null) {
addAudioDebugListener(listener);
}
}
/**
- * Adds a listener to receive debug events from the audio renderer.
- *
- * @param listener The listener.
+ * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} to get more detailed debug
+ * information.
*/
+ @Deprecated
public void addAudioDebugListener(AudioRendererEventListener listener) {
audioDebugListeners.add(listener);
}
/**
- * Removes a listener to receive debug events from the audio renderer.
- *
- * @param listener The listener.
+ * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} and {@link
+ * #removeAnalyticsListener(AnalyticsListener)} to get more detailed debug information.
*/
+ @Deprecated
public void removeAudioDebugListener(AudioRendererEventListener listener) {
audioDebugListeners.remove(listener);
}
@@ -653,6 +660,11 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
return player.getPlaybackState();
}
+ @Override
+ public ExoPlaybackException getPlaybackError() {
+ return player.getPlaybackError();
+ }
+
@Override
public void prepare(MediaSource mediaSource) {
prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java
index 4397b945b4..43ef308f27 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java
@@ -35,7 +35,6 @@ import com.google.android.exoplayer2.metadata.MetadataOutput;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.TrackGroupArray;
-import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.Assertions;
@@ -59,7 +58,6 @@ public class AnalyticsCollector
VideoRendererEventListener,
MediaSourceEventListener,
BandwidthMeter.EventListener,
- AdsMediaSource.EventListener,
DefaultDrmSessionEventListener {
/** Factory for an analytics collector. */
@@ -80,7 +78,6 @@ public class AnalyticsCollector
private final CopyOnWriteArraySet listeners;
private final Player player;
private final Clock clock;
- private final Period period;
private final Window window;
private final MediaPeriodQueueTracker mediaPeriodQueueTracker;
@@ -95,7 +92,6 @@ public class AnalyticsCollector
this.clock = Assertions.checkNotNull(clock);
listeners = new CopyOnWriteArraySet<>();
mediaPeriodQueueTracker = new MediaPeriodQueueTracker();
- period = new Period();
window = new Window();
}
@@ -164,13 +160,10 @@ public class AnalyticsCollector
*/
public final void resetForNewMediaSource() {
// Copying the list is needed because onMediaPeriodReleased will modify the list.
- List activeMediaPeriods =
+ List activeMediaPeriods =
new ArrayList<>(mediaPeriodQueueTracker.activeMediaPeriods);
- Timeline timeline = mediaPeriodQueueTracker.timeline;
- for (MediaPeriodId mediaPeriod : activeMediaPeriods) {
- int windowIndex =
- timeline.isEmpty() ? 0 : timeline.getPeriod(mediaPeriod.periodIndex, period).windowIndex;
- onMediaPeriodReleased(windowIndex, mediaPeriod);
+ for (WindowAndMediaPeriodId mediaPeriod : activeMediaPeriods) {
+ onMediaPeriodReleased(mediaPeriod.windowIndex, mediaPeriod.mediaPeriodId);
}
}
@@ -309,7 +302,7 @@ public class AnalyticsCollector
@Override
public final void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {
- mediaPeriodQueueTracker.onMediaPeriodCreated(mediaPeriodId);
+ mediaPeriodQueueTracker.onMediaPeriodCreated(windowIndex, mediaPeriodId);
EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId);
for (AnalyticsListener listener : listeners) {
listener.onMediaPeriodCreated(eventTime);
@@ -318,7 +311,7 @@ public class AnalyticsCollector
@Override
public final void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) {
- mediaPeriodQueueTracker.onMediaPeriodReleased(mediaPeriodId);
+ mediaPeriodQueueTracker.onMediaPeriodReleased(windowIndex, mediaPeriodId);
EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId);
for (AnalyticsListener listener : listeners) {
listener.onMediaPeriodReleased(eventTime);
@@ -377,7 +370,7 @@ public class AnalyticsCollector
@Override
public final void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) {
- mediaPeriodQueueTracker.onReadingStarted(mediaPeriodId);
+ mediaPeriodQueueTracker.onReadingStarted(windowIndex, mediaPeriodId);
EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId);
for (AnalyticsListener listener : listeners) {
listener.onReadingStarted(eventTime);
@@ -539,40 +532,6 @@ public class AnalyticsCollector
}
}
- // AdsMediaSource.EventListener implementation.
-
- @Override
- public final void onAdLoadError(IOException error) {
- EventTime eventTime = generatePlayingMediaPeriodEventTime();
- for (AnalyticsListener listener : listeners) {
- listener.onAdLoadError(eventTime, error);
- }
- }
-
- @Override
- public final void onInternalAdLoadError(RuntimeException error) {
- EventTime eventTime = generatePlayingMediaPeriodEventTime();
- for (AnalyticsListener listener : listeners) {
- listener.onInternalAdLoadError(eventTime, error);
- }
- }
-
- @Override
- public final void onAdClicked() {
- EventTime eventTime = generatePlayingMediaPeriodEventTime();
- for (AnalyticsListener listener : listeners) {
- listener.onAdClicked(eventTime);
- }
- }
-
- @Override
- public final void onAdTapped() {
- EventTime eventTime = generatePlayingMediaPeriodEventTime();
- for (AnalyticsListener listener : listeners) {
- listener.onAdTapped(eventTime);
- }
- }
-
// Internal methods.
/** Returns read-only set of registered listeners. */
@@ -597,7 +556,8 @@ public class AnalyticsCollector
// This event is for content in the currently playing window.
eventPositionMs = player.getContentPosition();
}
- } else if (timeline.isEmpty() || (mediaPeriodId != null && mediaPeriodId.isAd())) {
+ } else if (windowIndex >= timeline.getWindowCount()
+ || (mediaPeriodId != null && mediaPeriodId.isAd())) {
// This event is for an unknown future window or for an ad in a future window.
// Assume start position of zero.
eventPositionMs = 0;
@@ -617,16 +577,13 @@ public class AnalyticsCollector
bufferedDurationMs);
}
- private EventTime generateEventTime(@Nullable MediaPeriodId mediaPeriodId) {
- Timeline timeline = player.getCurrentTimeline();
- if (mediaPeriodId == null) {
- mediaPeriodId = mediaPeriodQueueTracker.tryResolveWindowIndex(player.getCurrentWindowIndex());
+ private EventTime generateEventTime(@Nullable WindowAndMediaPeriodId mediaPeriod) {
+ if (mediaPeriod == null) {
+ int windowIndex = player.getCurrentWindowIndex();
+ MediaPeriodId mediaPeriodId = mediaPeriodQueueTracker.tryResolveWindowIndex(windowIndex);
+ return generateEventTime(windowIndex, mediaPeriodId);
}
- int windowIndex =
- mediaPeriodId == null || timeline.isEmpty()
- ? player.getCurrentWindowIndex()
- : timeline.getPeriod(mediaPeriodId.periodIndex, period).windowIndex;
- return generateEventTime(windowIndex, mediaPeriodId);
+ return generateEventTime(mediaPeriod.windowIndex, mediaPeriod.mediaPeriodId);
}
private EventTime generateLastReportedPlayingMediaPeriodEventTime() {
@@ -651,11 +608,11 @@ public class AnalyticsCollector
// TODO: Investigate reporting MediaPeriodId in renderer events and adding a listener of queue
// changes, which would hopefully remove the need to track the queue here.
- private final ArrayList activeMediaPeriods;
+ private final ArrayList activeMediaPeriods;
private final Period period;
- private MediaPeriodId lastReportedPlayingMediaPeriod;
- private MediaPeriodId readingMediaPeriod;
+ private WindowAndMediaPeriodId lastReportedPlayingMediaPeriod;
+ private WindowAndMediaPeriodId readingMediaPeriod;
private Timeline timeline;
private boolean isSeeking;
@@ -666,33 +623,34 @@ public class AnalyticsCollector
}
/**
- * Returns the {@link MediaPeriodId} of the media period in the front of the queue. This is the
- * playing media period unless the player hasn't started playing yet (in which case it is the
- * loading media period or null). While the player is seeking or preparing, this method will
- * always return null to reflect the uncertainty about the current playing period. May also be
- * null, if the timeline is empty or no media period is active yet.
+ * Returns the {@link WindowAndMediaPeriodId} of the media period in the front of the queue.
+ * This is the playing media period unless the player hasn't started playing yet (in which case
+ * it is the loading media period or null). While the player is seeking or preparing, this
+ * method will always return null to reflect the uncertainty about the current playing period.
+ * May also be null, if the timeline is empty or no media period is active yet.
*/
- public @Nullable MediaPeriodId getPlayingMediaPeriod() {
+ public @Nullable WindowAndMediaPeriodId getPlayingMediaPeriod() {
return activeMediaPeriods.isEmpty() || timeline.isEmpty() || isSeeking
? null
: activeMediaPeriods.get(0);
}
/**
- * Returns the {@link MediaPeriodId} of the currently playing media period. This is the publicly
- * reported period which should always match {@link Player#getCurrentPeriodIndex()} unless the
- * player is currently seeking or being prepared in which case the previous period is reported
- * until the seek or preparation is processed. May be null, if no media period is active yet.
+ * Returns the {@link WindowAndMediaPeriodId} of the currently playing media period. This is the
+ * publicly reported period which should always match {@link Player#getCurrentPeriodIndex()}
+ * unless the player is currently seeking or being prepared in which case the previous period is
+ * reported until the seek or preparation is processed. May be null, if no media period is
+ * active yet.
*/
- public @Nullable MediaPeriodId getLastReportedPlayingMediaPeriod() {
+ public @Nullable WindowAndMediaPeriodId getLastReportedPlayingMediaPeriod() {
return lastReportedPlayingMediaPeriod;
}
/**
- * Returns the {@link MediaPeriodId} of the media period currently being read by the player. May
- * be null, if the player is not reading a media period.
+ * Returns the {@link WindowAndMediaPeriodId} of the media period currently being read by the
+ * player. May be null, if the player is not reading a media period.
*/
- public @Nullable MediaPeriodId getReadingMediaPeriod() {
+ public @Nullable WindowAndMediaPeriodId getReadingMediaPeriod() {
return readingMediaPeriod;
}
@@ -701,7 +659,7 @@ public class AnalyticsCollector
* currently loading or will be the next one loading. May be null, if no media period is active
* yet.
*/
- public @Nullable MediaPeriodId getLoadingMediaPeriod() {
+ public @Nullable WindowAndMediaPeriodId getLoadingMediaPeriod() {
return activeMediaPeriods.isEmpty()
? null
: activeMediaPeriods.get(activeMediaPeriods.size() - 1);
@@ -713,20 +671,23 @@ public class AnalyticsCollector
}
/**
- * Tries to find an existing media period from the specified window index. Only returns a
+ * Tries to find an existing media period id from the specified window index. Only returns a
* non-null media period id if there is a unique, unambiguous match.
*/
public @Nullable MediaPeriodId tryResolveWindowIndex(int windowIndex) {
MediaPeriodId match = null;
- if (timeline != null && !timeline.isEmpty()) {
+ if (timeline != null) {
+ int timelinePeriodCount = timeline.getPeriodCount();
for (int i = 0; i < activeMediaPeriods.size(); i++) {
- MediaPeriodId mediaPeriodId = activeMediaPeriods.get(i);
- if (timeline.getPeriod(mediaPeriodId.periodIndex, period).windowIndex == windowIndex) {
+ WindowAndMediaPeriodId mediaPeriod = activeMediaPeriods.get(i);
+ int periodIndex = mediaPeriod.mediaPeriodId.periodIndex;
+ if (periodIndex < timelinePeriodCount
+ && timeline.getPeriod(periodIndex, period).windowIndex == windowIndex) {
if (match != null) {
// Ambiguous match.
return null;
}
- match = mediaPeriodId;
+ match = mediaPeriod.mediaPeriodId;
}
}
}
@@ -742,10 +703,10 @@ public class AnalyticsCollector
public void onTimelineChanged(Timeline timeline) {
for (int i = 0; i < activeMediaPeriods.size(); i++) {
activeMediaPeriods.set(
- i, updateMediaPeriodIdToNewTimeline(activeMediaPeriods.get(i), timeline));
+ i, updateMediaPeriodToNewTimeline(activeMediaPeriods.get(i), timeline));
}
if (readingMediaPeriod != null) {
- readingMediaPeriod = updateMediaPeriodIdToNewTimeline(readingMediaPeriod, timeline);
+ readingMediaPeriod = updateMediaPeriodToNewTimeline(readingMediaPeriod, timeline);
}
this.timeline = timeline;
updateLastReportedPlayingMediaPeriod();
@@ -763,24 +724,25 @@ public class AnalyticsCollector
}
/** Updates the queue with a newly created media period. */
- public void onMediaPeriodCreated(MediaPeriodId mediaPeriodId) {
- activeMediaPeriods.add(mediaPeriodId);
+ public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {
+ activeMediaPeriods.add(new WindowAndMediaPeriodId(windowIndex, mediaPeriodId));
if (activeMediaPeriods.size() == 1 && !timeline.isEmpty()) {
updateLastReportedPlayingMediaPeriod();
}
}
/** Updates the queue with a released media period. */
- public void onMediaPeriodReleased(MediaPeriodId mediaPeriodId) {
- activeMediaPeriods.remove(mediaPeriodId);
- if (mediaPeriodId.equals(readingMediaPeriod)) {
+ public void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) {
+ WindowAndMediaPeriodId mediaPeriod = new WindowAndMediaPeriodId(windowIndex, mediaPeriodId);
+ activeMediaPeriods.remove(mediaPeriod);
+ if (mediaPeriod.equals(readingMediaPeriod)) {
readingMediaPeriod = activeMediaPeriods.isEmpty() ? null : activeMediaPeriods.get(0);
}
}
/** Update the queue with a change in the reading media period. */
- public void onReadingStarted(MediaPeriodId mediaPeriodId) {
- readingMediaPeriod = mediaPeriodId;
+ public void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) {
+ readingMediaPeriod = new WindowAndMediaPeriodId(windowIndex, mediaPeriodId);
}
private void updateLastReportedPlayingMediaPeriod() {
@@ -789,16 +751,48 @@ public class AnalyticsCollector
}
}
- private MediaPeriodId updateMediaPeriodIdToNewTimeline(
- MediaPeriodId mediaPeriodId, Timeline newTimeline) {
+ private WindowAndMediaPeriodId updateMediaPeriodToNewTimeline(
+ WindowAndMediaPeriodId mediaPeriod, Timeline newTimeline) {
if (newTimeline.isEmpty() || timeline.isEmpty()) {
- return mediaPeriodId;
+ return mediaPeriod;
}
- Object uid = timeline.getPeriod(mediaPeriodId.periodIndex, period, /* setIds= */ true).uid;
- int newIndex = newTimeline.getIndexOfPeriod(uid);
- return newIndex == C.INDEX_UNSET
- ? mediaPeriodId
- : mediaPeriodId.copyWithPeriodIndex(newIndex);
+ Object uid =
+ timeline.getPeriod(mediaPeriod.mediaPeriodId.periodIndex, period, /* setIds= */ true).uid;
+ int newPeriodIndex = newTimeline.getIndexOfPeriod(uid);
+ if (newPeriodIndex == C.INDEX_UNSET) {
+ return mediaPeriod;
+ }
+ int newWindowIndex = newTimeline.getPeriod(newPeriodIndex, period).windowIndex;
+ return new WindowAndMediaPeriodId(
+ newWindowIndex, mediaPeriod.mediaPeriodId.copyWithPeriodIndex(newPeriodIndex));
+ }
+ }
+
+ private static final class WindowAndMediaPeriodId {
+
+ public final int windowIndex;
+ public final MediaPeriodId mediaPeriodId;
+
+ public WindowAndMediaPeriodId(int windowIndex, MediaPeriodId mediaPeriodId) {
+ this.windowIndex = windowIndex;
+ this.mediaPeriodId = mediaPeriodId;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ WindowAndMediaPeriodId that = (WindowAndMediaPeriodId) other;
+ return windowIndex == that.windowIndex && mediaPeriodId.equals(that.mediaPeriodId);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * windowIndex + mediaPeriodId.hashCode();
}
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java
index 30357b08ef..48057f2bff 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java
@@ -433,38 +433,6 @@ public interface AnalyticsListener {
*/
void onRenderedFirstFrame(EventTime eventTime, Surface surface);
- /**
- * Called if there was an error loading one or more ads. The ads loader will skip the problematic
- * ad(s).
- *
- * @param eventTime The event time.
- * @param error The error.
- */
- void onAdLoadError(EventTime eventTime, IOException error);
-
- /**
- * Called when an unexpected internal error is encountered while loading ads. The ads loader will
- * skip all remaining ads, as the error is not recoverable.
- *
- * @param eventTime The event time.
- * @param error The error.
- */
- void onInternalAdLoadError(EventTime eventTime, RuntimeException error);
-
- /**
- * Called when the user clicks through an ad (for example, following a 'learn more' link).
- *
- * @param eventTime The event time.
- */
- void onAdClicked(EventTime eventTime);
-
- /**
- * Called when the user taps a non-clickthrough part of an ad.
- *
- * @param eventTime The event time.
- */
- void onAdTapped(EventTime eventTime);
-
/**
* Called each time drm keys are loaded.
*
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java
index a53a85ed71..4a49de56b0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java
@@ -152,18 +152,6 @@ public abstract class DefaultAnalyticsListener implements AnalyticsListener {
@Override
public void onRenderedFirstFrame(EventTime eventTime, Surface surface) {}
- @Override
- public void onAdLoadError(EventTime eventTime, IOException error) {}
-
- @Override
- public void onInternalAdLoadError(EventTime eventTime, RuntimeException error) {}
-
- @Override
- public void onAdClicked(EventTime eventTime) {}
-
- @Override
- public void onAdTapped(EventTime eventTime) {}
-
@Override
public void onDrmKeysLoaded(EventTime eventTime) {}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java
index a206d2c1ec..1025cb953b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java
@@ -35,6 +35,8 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
/**
* Plays audio data. The implementation delegates to an {@link AudioTrack} and handles playback
@@ -64,6 +66,92 @@ public final class DefaultAudioSink implements AudioSink {
}
+ /**
+ * Provides a chain of audio processors, which are used for any user-defined processing and
+ * applying playback parameters (if supported). Because applying playback parameters can skip and
+ * stretch/compress audio, the sink will query the chain for information on how to transform its
+ * output position to map it onto a media position, via {@link #getMediaDuration(long)} and {@link
+ * #getSkippedOutputFrameCount()}.
+ */
+ public interface AudioProcessorChain {
+
+ /**
+ * Returns the fixed chain of audio processors that will process audio. This method is called
+ * once during initialization, but audio processors may change state to become active/inactive
+ * during playback.
+ */
+ AudioProcessor[] getAudioProcessors();
+
+ /**
+ * Configures audio processors to apply the specified playback parameters immediately, returning
+ * the new parameters, which may differ from those passed in. Only called when processors have
+ * no input pending.
+ *
+ * @param playbackParameters The playback parameters to try to apply.
+ * @return The playback parameters that were actually applied.
+ */
+ PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters);
+
+ /**
+ * Scales the specified playout duration to take into account speedup due to audio processing,
+ * returning an input media duration, in arbitrary units.
+ */
+ long getMediaDuration(long playoutDuration);
+
+ /**
+ * Returns the number of output audio frames skipped since the audio processors were last
+ * flushed.
+ */
+ long getSkippedOutputFrameCount();
+ }
+
+ /**
+ * The default audio processor chain, which applies a (possibly empty) chain of user-defined audio
+ * processors followed by {@link SilenceSkippingAudioProcessor} and {@link SonicAudioProcessor}.
+ */
+ public static class DefaultAudioProcessorChain implements AudioProcessorChain {
+
+ private final AudioProcessor[] audioProcessors;
+ private final SilenceSkippingAudioProcessor silenceSkippingAudioProcessor;
+ private final SonicAudioProcessor sonicAudioProcessor;
+
+ /**
+ * Creates a new default chain of audio processors, with the user-defined {@code
+ * audioProcessors} applied before silence skipping and playback parameters.
+ */
+ public DefaultAudioProcessorChain(AudioProcessor... audioProcessors) {
+ this.audioProcessors = Arrays.copyOf(audioProcessors, audioProcessors.length + 2);
+ silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor();
+ sonicAudioProcessor = new SonicAudioProcessor();
+ this.audioProcessors[audioProcessors.length] = silenceSkippingAudioProcessor;
+ this.audioProcessors[audioProcessors.length + 1] = sonicAudioProcessor;
+ }
+
+ @Override
+ public AudioProcessor[] getAudioProcessors() {
+ return audioProcessors;
+ }
+
+ @Override
+ public PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters) {
+ silenceSkippingAudioProcessor.setEnabled(playbackParameters.skipSilence);
+ return new PlaybackParameters(
+ sonicAudioProcessor.setSpeed(playbackParameters.speed),
+ sonicAudioProcessor.setPitch(playbackParameters.pitch),
+ playbackParameters.skipSilence);
+ }
+
+ @Override
+ public long getMediaDuration(long playoutDuration) {
+ return sonicAudioProcessor.scaleDurationForSpeedup(playoutDuration);
+ }
+
+ @Override
+ public long getSkippedOutputFrameCount() {
+ return silenceSkippingAudioProcessor.getSkippedFrames();
+ }
+ }
+
/**
* A minimum length for the {@link AudioTrack} buffer, in microseconds.
*/
@@ -135,11 +223,10 @@ public final class DefaultAudioSink implements AudioSink {
public static boolean failOnSpuriousAudioTimestamp = false;
@Nullable private final AudioCapabilities audioCapabilities;
+ private final AudioProcessorChain audioProcessorChain;
private final boolean enableConvertHighResIntPcmToFloat;
private final ChannelMappingAudioProcessor channelMappingAudioProcessor;
private final TrimmingAudioProcessor trimmingAudioProcessor;
- private final SilenceSkippingAudioProcessor silenceSkippingAudioProcessor;
- private final SonicAudioProcessor sonicAudioProcessor;
private final AudioProcessor[] toIntPcmAvailableAudioProcessors;
private final AudioProcessor[] toFloatPcmAvailableAudioProcessors;
private final ConditionVariable releasingConditionVariable;
@@ -181,7 +268,7 @@ public final class DefaultAudioSink implements AudioSink {
private long startMediaTimeUs;
private float volume;
- private AudioProcessor[] audioProcessors;
+ private AudioProcessor[] activeAudioProcessors;
private ByteBuffer[] outputBuffers;
@Nullable private ByteBuffer inputBuffer;
@Nullable private ByteBuffer outputBuffer;
@@ -196,17 +283,21 @@ public final class DefaultAudioSink implements AudioSink {
private long lastFeedElapsedRealtimeMs;
/**
+ * Creates a new default audio sink.
+ *
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
* @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before
* output. May be empty.
*/
- public DefaultAudioSink(@Nullable AudioCapabilities audioCapabilities,
- AudioProcessor[] audioProcessors) {
+ public DefaultAudioSink(
+ @Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors) {
this(audioCapabilities, audioProcessors, /* enableConvertHighResIntPcmToFloat= */ false);
}
/**
+ * Creates a new default audio sink, optionally using float output for high resolution PCM.
+ *
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
* @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before
@@ -220,22 +311,45 @@ public final class DefaultAudioSink implements AudioSink {
@Nullable AudioCapabilities audioCapabilities,
AudioProcessor[] audioProcessors,
boolean enableConvertHighResIntPcmToFloat) {
+ this(
+ audioCapabilities,
+ new DefaultAudioProcessorChain(audioProcessors),
+ enableConvertHighResIntPcmToFloat);
+ }
+
+ /**
+ * Creates a new default audio sink, optionally using float output for high resolution PCM and
+ * with the specified {@code audioProcessorChain}.
+ *
+ * @param audioCapabilities The audio capabilities for playback on this device. May be null if the
+ * default capabilities (no encoded audio passthrough support) should be assumed.
+ * @param audioProcessorChain An {@link AudioProcessorChain} which is used to apply playback
+ * parameters adjustments. The instance passed in must not be reused in other sinks.
+ * @param enableConvertHighResIntPcmToFloat Whether to enable conversion of high resolution
+ * integer PCM to 32-bit float for output, if possible. Functionality that uses 16-bit integer
+ * audio processing (for example, speed and pitch adjustment) will not be available when float
+ * output is in use.
+ */
+ public DefaultAudioSink(
+ @Nullable AudioCapabilities audioCapabilities,
+ AudioProcessorChain audioProcessorChain,
+ boolean enableConvertHighResIntPcmToFloat) {
this.audioCapabilities = audioCapabilities;
+ this.audioProcessorChain = Assertions.checkNotNull(audioProcessorChain);
this.enableConvertHighResIntPcmToFloat = enableConvertHighResIntPcmToFloat;
releasingConditionVariable = new ConditionVariable(true);
audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener());
channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
trimmingAudioProcessor = new TrimmingAudioProcessor();
- silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor();
- sonicAudioProcessor = new SonicAudioProcessor();
- toIntPcmAvailableAudioProcessors = new AudioProcessor[5 + audioProcessors.length];
- toIntPcmAvailableAudioProcessors[0] = new ResamplingAudioProcessor();
- toIntPcmAvailableAudioProcessors[1] = channelMappingAudioProcessor;
- toIntPcmAvailableAudioProcessors[2] = trimmingAudioProcessor;
- System.arraycopy(
- audioProcessors, 0, toIntPcmAvailableAudioProcessors, 3, audioProcessors.length);
- toIntPcmAvailableAudioProcessors[3 + audioProcessors.length] = silenceSkippingAudioProcessor;
- toIntPcmAvailableAudioProcessors[4 + audioProcessors.length] = sonicAudioProcessor;
+ ArrayList toIntPcmAudioProcessors = new ArrayList<>();
+ Collections.addAll(
+ toIntPcmAudioProcessors,
+ new ResamplingAudioProcessor(),
+ channelMappingAudioProcessor,
+ trimmingAudioProcessor);
+ Collections.addAll(toIntPcmAudioProcessors, audioProcessorChain.getAudioProcessors());
+ toIntPcmAvailableAudioProcessors =
+ toIntPcmAudioProcessors.toArray(new AudioProcessor[toIntPcmAudioProcessors.size()]);
toFloatPcmAvailableAudioProcessors = new AudioProcessor[] {new FloatResamplingAudioProcessor()};
volume = 1.0f;
startMediaTimeState = START_NOT_SET;
@@ -243,7 +357,7 @@ public final class DefaultAudioSink implements AudioSink {
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
playbackParameters = PlaybackParameters.DEFAULT;
drainingAudioProcessorIndex = C.INDEX_UNSET;
- this.audioProcessors = new AudioProcessor[0];
+ activeAudioProcessors = new AudioProcessor[0];
outputBuffers = new ByteBuffer[0];
playbackParametersCheckpoints = new ArrayDeque<>();
}
@@ -423,14 +537,14 @@ public final class DefaultAudioSink implements AudioSink {
}
}
int count = newAudioProcessors.size();
- audioProcessors = newAudioProcessors.toArray(new AudioProcessor[count]);
+ activeAudioProcessors = newAudioProcessors.toArray(new AudioProcessor[count]);
outputBuffers = new ByteBuffer[count];
flushAudioProcessors();
}
private void flushAudioProcessors() {
- for (int i = 0; i < audioProcessors.length; i++) {
- AudioProcessor audioProcessor = audioProcessors[i];
+ for (int i = 0; i < activeAudioProcessors.length; i++) {
+ AudioProcessor audioProcessor = activeAudioProcessors[i];
audioProcessor.flush();
outputBuffers[i] = audioProcessor.getOutput();
}
@@ -468,7 +582,7 @@ public final class DefaultAudioSink implements AudioSink {
playbackParameters =
canApplyPlaybackParameters
- ? applyPlaybackParameters(playbackParameters)
+ ? audioProcessorChain.applyPlaybackParameters(playbackParameters)
: PlaybackParameters.DEFAULT;
setupAudioProcessors();
@@ -536,7 +650,7 @@ public final class DefaultAudioSink implements AudioSink {
}
PlaybackParameters newPlaybackParameters = afterDrainPlaybackParameters;
afterDrainPlaybackParameters = null;
- newPlaybackParameters = applyPlaybackParameters(newPlaybackParameters);
+ newPlaybackParameters = audioProcessorChain.applyPlaybackParameters(newPlaybackParameters);
// Store the position and corresponding media time from which the parameters will apply.
playbackParametersCheckpoints.add(
new PlaybackParametersCheckpoint(
@@ -601,7 +715,7 @@ public final class DefaultAudioSink implements AudioSink {
}
private void processBuffers(long avSyncPresentationTimeUs) throws WriteException {
- int count = audioProcessors.length;
+ int count = activeAudioProcessors.length;
int index = count;
while (index >= 0) {
ByteBuffer input = index > 0 ? outputBuffers[index - 1]
@@ -609,7 +723,7 @@ public final class DefaultAudioSink implements AudioSink {
if (index == count) {
writeBuffer(input, avSyncPresentationTimeUs);
} else {
- AudioProcessor audioProcessor = audioProcessors[index];
+ AudioProcessor audioProcessor = activeAudioProcessors[index];
audioProcessor.queueInput(input);
ByteBuffer output = audioProcessor.getOutput();
outputBuffers[index] = output;
@@ -706,11 +820,11 @@ public final class DefaultAudioSink implements AudioSink {
private boolean drainAudioProcessorsToEndOfStream() throws WriteException {
boolean audioProcessorNeedsEndOfStream = false;
if (drainingAudioProcessorIndex == C.INDEX_UNSET) {
- drainingAudioProcessorIndex = processingEnabled ? 0 : audioProcessors.length;
+ drainingAudioProcessorIndex = processingEnabled ? 0 : activeAudioProcessors.length;
audioProcessorNeedsEndOfStream = true;
}
- while (drainingAudioProcessorIndex < audioProcessors.length) {
- AudioProcessor audioProcessor = audioProcessors[drainingAudioProcessorIndex];
+ while (drainingAudioProcessorIndex < activeAudioProcessors.length) {
+ AudioProcessor audioProcessor = activeAudioProcessors[drainingAudioProcessorIndex];
if (audioProcessorNeedsEndOfStream) {
audioProcessor.queueEndOfStream();
}
@@ -762,7 +876,7 @@ public final class DefaultAudioSink implements AudioSink {
afterDrainPlaybackParameters = playbackParameters;
} else {
// Update the playback parameters now.
- this.playbackParameters = applyPlaybackParameters(playbackParameters);
+ this.playbackParameters = audioProcessorChain.applyPlaybackParameters(playbackParameters);
}
}
return this.playbackParameters;
@@ -920,29 +1034,14 @@ public final class DefaultAudioSink implements AudioSink {
}.start();
}
- /**
- * Configures audio processors to apply the specified playback parameters, returning the new
- * parameters, which may differ from those passed in.
- *
- * @param playbackParameters The playback parameters to try to apply.
- * @return The playback parameters that were actually applied.
- */
- private PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters) {
- silenceSkippingAudioProcessor.setEnabled(playbackParameters.skipSilence);
- return new PlaybackParameters(
- sonicAudioProcessor.setSpeed(playbackParameters.speed),
- sonicAudioProcessor.setPitch(playbackParameters.pitch),
- playbackParameters.skipSilence);
- }
-
- /**
- * Returns the underlying audio track {@code positionUs} with any applicable speedup applied.
- */
private long applySpeedup(long positionUs) {
+ @Nullable PlaybackParametersCheckpoint checkpoint = null;
while (!playbackParametersCheckpoints.isEmpty()
&& positionUs >= playbackParametersCheckpoints.getFirst().positionUs) {
+ checkpoint = playbackParametersCheckpoints.remove();
+ }
+ if (checkpoint != null) {
// We are playing (or about to play) media with the new playback parameters, so update them.
- PlaybackParametersCheckpoint checkpoint = playbackParametersCheckpoints.remove();
playbackParameters = checkpoint.playbackParameters;
playbackParametersPositionUs = checkpoint.positionUs;
playbackParametersOffsetUs = checkpoint.mediaTimeUs - startMediaTimeUs;
@@ -954,8 +1053,9 @@ public final class DefaultAudioSink implements AudioSink {
if (playbackParametersCheckpoints.isEmpty()) {
return playbackParametersOffsetUs
- + sonicAudioProcessor.scaleDurationForSpeedup(positionUs - playbackParametersPositionUs);
+ + audioProcessorChain.getMediaDuration(positionUs - playbackParametersPositionUs);
}
+
// We are playing data at a previous playback speed, so fall back to multiplying by the speed.
return playbackParametersOffsetUs
+ Util.getMediaDurationForPlayoutDuration(
@@ -963,7 +1063,7 @@ public final class DefaultAudioSink implements AudioSink {
}
private long applySkipping(long positionUs) {
- return positionUs + framesToDurationUs(silenceSkippingAudioProcessor.getSkippedFrames());
+ return positionUs + framesToDurationUs(audioProcessorChain.getSkippedOutputFrameCount());
}
private boolean isInitialized() {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
index c73081e2ab..9ab066ee7d 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
@@ -17,6 +17,8 @@ package com.google.android.exoplayer2.audio;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.pm.PackageManager;
import android.media.MediaCodec;
import android.media.MediaCrypto;
import android.media.MediaFormat;
@@ -61,6 +63,7 @@ import java.nio.ByteBuffer;
@TargetApi(16)
public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock {
+ private final Context context;
private final EventDispatcher eventDispatcher;
private final AudioSink audioSink;
@@ -78,13 +81,19 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
private boolean allowPositionDiscontinuity;
/**
+ * @param context A context.
* @param mediaCodecSelector A decoder selector.
*/
- public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector) {
- this(mediaCodecSelector, null, true);
+ public MediaCodecAudioRenderer(Context context, MediaCodecSelector mediaCodecSelector) {
+ this(
+ context,
+ mediaCodecSelector,
+ /* drmSessionManager= */ null,
+ /* playClearSamplesWithoutKeys= */ false);
}
/**
+ * @param context A context.
* @param mediaCodecSelector A decoder selector.
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted
* content is not required.
@@ -94,24 +103,43 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media.
*/
- public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
+ public MediaCodecAudioRenderer(
+ Context context,
+ MediaCodecSelector mediaCodecSelector,
@Nullable DrmSessionManager drmSessionManager,
boolean playClearSamplesWithoutKeys) {
- this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, null, null);
+ this(
+ context,
+ mediaCodecSelector,
+ drmSessionManager,
+ playClearSamplesWithoutKeys,
+ /* eventHandler= */ null,
+ /* eventListener= */ null);
}
/**
+ * @param context A context.
* @param mediaCodecSelector A decoder selector.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
*/
- public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
- @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener) {
- this(mediaCodecSelector, null, true, eventHandler, eventListener);
+ public MediaCodecAudioRenderer(
+ Context context,
+ MediaCodecSelector mediaCodecSelector,
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener) {
+ this(
+ context,
+ mediaCodecSelector,
+ /* drmSessionManager= */ null,
+ /* playClearSamplesWithoutKeys= */ false,
+ eventHandler,
+ eventListener);
}
/**
+ * @param context A context.
* @param mediaCodecSelector A decoder selector.
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted
* content is not required.
@@ -124,15 +152,25 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
*/
- public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
+ public MediaCodecAudioRenderer(
+ Context context,
+ MediaCodecSelector mediaCodecSelector,
@Nullable DrmSessionManager drmSessionManager,
- boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler,
+ boolean playClearSamplesWithoutKeys,
+ @Nullable Handler eventHandler,
@Nullable AudioRendererEventListener eventListener) {
- this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler,
- eventListener, (AudioCapabilities) null);
+ this(
+ context,
+ mediaCodecSelector,
+ drmSessionManager,
+ playClearSamplesWithoutKeys,
+ eventHandler,
+ eventListener,
+ (AudioCapabilities) null);
}
/**
+ * @param context A context.
* @param mediaCodecSelector A decoder selector.
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted
* content is not required.
@@ -149,16 +187,27 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
* @param audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before
* output.
*/
- public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
+ public MediaCodecAudioRenderer(
+ Context context,
+ MediaCodecSelector mediaCodecSelector,
@Nullable DrmSessionManager drmSessionManager,
- boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler,
+ boolean playClearSamplesWithoutKeys,
+ @Nullable Handler eventHandler,
@Nullable AudioRendererEventListener eventListener,
- @Nullable AudioCapabilities audioCapabilities, AudioProcessor... audioProcessors) {
- this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys,
- eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors));
+ @Nullable AudioCapabilities audioCapabilities,
+ AudioProcessor... audioProcessors) {
+ this(
+ context,
+ mediaCodecSelector,
+ drmSessionManager,
+ playClearSamplesWithoutKeys,
+ eventHandler,
+ eventListener,
+ new DefaultAudioSink(audioCapabilities, audioProcessors));
}
/**
+ * @param context A context.
* @param mediaCodecSelector A decoder selector.
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted
* content is not required.
@@ -172,13 +221,18 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioSink The sink to which audio will be output.
*/
- public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
+ public MediaCodecAudioRenderer(
+ Context context,
+ MediaCodecSelector mediaCodecSelector,
@Nullable DrmSessionManager drmSessionManager,
- boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler,
- @Nullable AudioRendererEventListener eventListener, AudioSink audioSink) {
+ boolean playClearSamplesWithoutKeys,
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
+ AudioSink audioSink) {
super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
- eventDispatcher = new EventDispatcher(eventHandler, eventListener);
+ this.context = context.getApplicationContext();
this.audioSink = audioSink;
+ eventDispatcher = new EventDispatcher(eventHandler, eventListener);
audioSink.setListener(new AudioSinkListener());
}
@@ -233,11 +287,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
if (allowPassthrough(format.sampleMimeType)) {
MediaCodecInfo passthroughDecoderInfo = mediaCodecSelector.getPassthroughDecoderInfo();
if (passthroughDecoderInfo != null) {
- passthroughEnabled = true;
return passthroughDecoderInfo;
}
}
- passthroughEnabled = false;
return super.getDecoderInfo(mediaCodecSelector, format, requiresSecureDecoder);
}
@@ -257,28 +309,31 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Override
protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
MediaCrypto crypto) {
- codecMaxInputSize = getCodecMaxInputSize(format, getStreamFormats());
+ codecMaxInputSize = getCodecMaxInputSize(codecInfo, format, getStreamFormats());
codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name);
- MediaFormat mediaFormat = getMediaFormat(format, codecMaxInputSize);
+ passthroughEnabled = codecInfo.passthrough;
+ String codecMimeType = codecInfo.mimeType == null ? MimeTypes.AUDIO_RAW : codecInfo.mimeType;
+ MediaFormat mediaFormat = getMediaFormat(format, codecMimeType, codecMaxInputSize);
+ codec.configure(mediaFormat, /* surface= */ null, crypto, /* flags= */ 0);
if (passthroughEnabled) {
- // Override the MIME type used to configure the codec if we are using a passthrough decoder.
+ // Store the input MIME type if we're using the passthrough codec.
passthroughMediaFormat = mediaFormat;
- passthroughMediaFormat.setString(MediaFormat.KEY_MIME, MimeTypes.AUDIO_RAW);
- codec.configure(passthroughMediaFormat, null, crypto, 0);
passthroughMediaFormat.setString(MediaFormat.KEY_MIME, format.sampleMimeType);
} else {
- codec.configure(mediaFormat, null, crypto, 0);
passthroughMediaFormat = null;
}
}
@Override
protected @KeepCodecResult int canKeepCodec(
- MediaCodec codec, boolean codecIsAdaptive, Format oldFormat, Format newFormat) {
- return newFormat.maxInputSize <= codecMaxInputSize
- && areAdaptationCompatible(oldFormat, newFormat)
- ? KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION
- : KEEP_CODEC_RESULT_NO;
+ MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) {
+ return KEEP_CODEC_RESULT_NO;
+ // TODO: Determine when codecs can be safely kept. When doing so, also uncomment the commented
+ // out code in getCodecMaxInputSize.
+ // return getCodecMaxInputSize(codecInfo, newFormat) <= codecMaxInputSize
+ // && areAdaptationCompatible(oldFormat, newFormat)
+ // ? KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION
+ // : KEEP_CODEC_RESULT_NO;
}
@Override
@@ -511,23 +566,55 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
* Returns a maximum input size suitable for configuring a codec for {@code format} in a way that
* will allow possible adaptation to other compatible formats in {@code streamFormats}.
*
+ * @param codecInfo A {@link MediaCodecInfo} describing the decoder.
* @param format The format for which the codec is being configured.
* @param streamFormats The possible stream formats.
* @return A suitable maximum input size.
*/
- protected int getCodecMaxInputSize(Format format, Format[] streamFormats) {
- int maxInputSize = format.maxInputSize;
- if (streamFormats.length == 1) {
- // The single entry in streamFormats must correspond to the format for which the codec is
- // being configured.
- return maxInputSize;
- }
- for (Format streamFormat : streamFormats) {
- if (areAdaptationCompatible(format, streamFormat)) {
- maxInputSize = Math.max(maxInputSize, streamFormat.maxInputSize);
+ protected int getCodecMaxInputSize(
+ MediaCodecInfo codecInfo, Format format, Format[] streamFormats) {
+ int maxInputSize = getCodecMaxInputSize(codecInfo, format);
+ // if (streamFormats.length == 1) {
+ // // The single entry in streamFormats must correspond to the format for which the codec is
+ // // being configured.
+ // return maxInputSize;
+ // }
+ // for (Format streamFormat : streamFormats) {
+ // if (areAdaptationCompatible(format, streamFormat)) {
+ // maxInputSize = Math.max(maxInputSize, getCodecMaxInputSize(codecInfo, streamFormat));
+ // }
+ // }
+ return maxInputSize;
+ }
+
+ /**
+ * Returns a maximum input buffer size for a given format.
+ *
+ * @param codecInfo A {@link MediaCodecInfo} describing the decoder.
+ * @param format The format.
+ * @return A maximum input buffer size in bytes, or {@link Format#NO_VALUE} if a maximum could not
+ * be determined.
+ */
+ private int getCodecMaxInputSize(MediaCodecInfo codecInfo, Format format) {
+ if (Util.SDK_INT < 24 && "OMX.google.raw.decoder".equals(codecInfo.name)) {
+ // OMX.google.raw.decoder didn't resize its output buffers correctly prior to N, so there's no
+ // point requesting a non-default input size. Doing so may cause a native crash, where-as not
+ // doing so will cause a more controlled failure when attempting to fill an input buffer. See:
+ // https://github.com/google/ExoPlayer/issues/4057.
+ boolean needsRawDecoderWorkaround = true;
+ if (Util.SDK_INT == 23) {
+ PackageManager packageManager = context.getPackageManager();
+ if (packageManager != null
+ && packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
+ // The workaround is not required for AndroidTV devices running M.
+ needsRawDecoderWorkaround = false;
+ }
+ }
+ if (needsRawDecoderWorkaround) {
+ return Format.NO_VALUE;
}
}
- return maxInputSize;
+ return format.maxInputSize;
}
/**
@@ -535,13 +622,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
* for decoding the given {@link Format} for playback.
*
* @param format The format of the media.
+ * @param codecMimeType The MIME type handled by the codec.
+ * @param codecMaxInputSize The maximum input size supported by the codec.
* @return The framework media format.
*/
@SuppressLint("InlinedApi")
- protected MediaFormat getMediaFormat(Format format, int codecMaxInputSize) {
+ protected MediaFormat getMediaFormat(Format format, String codecMimeType, int codecMaxInputSize) {
MediaFormat mediaFormat = new MediaFormat();
// Set format parameters that should always be set.
- mediaFormat.setString(MediaFormat.KEY_MIME, format.sampleMimeType);
+ mediaFormat.setString(MediaFormat.KEY_MIME, codecMimeType);
mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, format.channelCount);
mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, format.sampleRate);
MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java
index b748dbab5d..a289ced128 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java
@@ -27,7 +27,7 @@ import java.nio.ByteOrder;
* An {@link AudioProcessor} that skips silence in the input stream. Input and output are 16-bit
* PCM.
*/
-/* package */ final class SilenceSkippingAudioProcessor implements AudioProcessor {
+public final class SilenceSkippingAudioProcessor implements AudioProcessor {
/**
* The minimum duration of audio that must be below {@link #SILENCE_THRESHOLD_LEVEL} to classify
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
index 89560f2977..c404912882 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
@@ -112,7 +112,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
private boolean waitingForKeys;
public SimpleDecoderAudioRenderer() {
- this(null, null);
+ this(/* eventHandler= */ null, /* eventListener= */ null);
}
/**
@@ -123,7 +123,13 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
*/
public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioProcessor... audioProcessors) {
- this(eventHandler, eventListener, null, null, false, audioProcessors);
+ this(
+ eventHandler,
+ eventListener,
+ /* audioCapabilities= */ null,
+ /* drmSessionManager= */ null,
+ /* playClearSamplesWithoutKeys= */ false,
+ audioProcessors);
}
/**
@@ -135,7 +141,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
*/
public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioCapabilities audioCapabilities) {
- this(eventHandler, eventListener, audioCapabilities, null, false);
+ this(
+ eventHandler,
+ eventListener,
+ audioCapabilities,
+ /* drmSessionManager= */ null,
+ /* playClearSamplesWithoutKeys= */ false);
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java
index cecc840511..2699559c5f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java
@@ -15,13 +15,13 @@
*/
package com.google.android.exoplayer2.drm;
-import android.annotation.TargetApi;
import android.media.DeniedByServerException;
import android.media.MediaCryptoException;
import android.media.MediaDrm;
import android.media.MediaDrmException;
import android.media.NotProvisionedException;
import android.os.Handler;
+import android.support.annotation.Nullable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -30,7 +30,6 @@ import java.util.UUID;
/**
* Used to obtain keys for decrypting protected media streams. See {@link android.media.MediaDrm}.
*/
-@TargetApi(18)
public interface ExoMediaDrm {
/**
@@ -72,14 +71,18 @@ public interface ExoMediaDrm {
/**
* Called when an event occurs that requires the app to be notified
*
- * @param mediaDrm the {@link ExoMediaDrm} object on which the event occurred.
- * @param sessionId the DRM session ID on which the event occurred
- * @param event indicates the event type
- * @param extra an secondary error code
- * @param data optional byte array of data that may be associated with the event
+ * @param mediaDrm The {@link ExoMediaDrm} object on which the event occurred.
+ * @param sessionId The DRM session ID on which the event occurred.
+ * @param event Indicates the event type.
+ * @param extra A secondary error code.
+ * @param data Optional byte array of data that may be associated with the event.
*/
- void onEvent(ExoMediaDrm extends T> mediaDrm, byte[] sessionId, int event, int extra,
- byte[] data);
+ void onEvent(
+ ExoMediaDrm extends T> mediaDrm,
+ byte[] sessionId,
+ int event,
+ int extra,
+ @Nullable byte[] data);
}
/**
@@ -90,20 +93,25 @@ public interface ExoMediaDrm {
* Called when the keys in a session change status, such as when the license is renewed or
* expires.
*
- * @param mediaDrm the {@link ExoMediaDrm} object on which the event occurred.
- * @param sessionId the DRM session ID on which the event occurred.
- * @param exoKeyInfo a list of {@link KeyStatus} that contains key ID and status.
- * @param hasNewUsableKey true if new key becomes usable.
+ * @param mediaDrm The {@link ExoMediaDrm} object on which the event occurred.
+ * @param sessionId The DRM session ID on which the event occurred.
+ * @param exoKeyInformation A list of {@link KeyStatus} that contains key ID and status.
+ * @param hasNewUsableKey Whether a new key became usable.
*/
- void onKeyStatusChange(ExoMediaDrm extends T> mediaDrm, byte[] sessionId,
- List exoKeyInfo, boolean hasNewUsableKey);
+ void onKeyStatusChange(
+ ExoMediaDrm extends T> mediaDrm,
+ byte[] sessionId,
+ List exoKeyInformation,
+ boolean hasNewUsableKey);
}
/**
* @see android.media.MediaDrm.KeyStatus
*/
interface KeyStatus {
+ /** Returns the status code for the key. */
int getStatusCode();
+ /** Returns the id for the key. */
byte[] getKeyId();
}
@@ -218,15 +226,16 @@ public interface ExoMediaDrm {
*/
void closeSession(byte[] sessionId);
- /**
- * @see MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)
- */
- KeyRequest getKeyRequest(byte[] scope, byte[] init, String mimeType, int keyType,
- HashMap optionalParameters) throws NotProvisionedException;
+ /** @see MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap) */
+ KeyRequest getKeyRequest(
+ byte[] scope,
+ byte[] init,
+ String mimeType,
+ int keyType,
+ HashMap optionalParameters)
+ throws NotProvisionedException;
- /**
- * @see MediaDrm#provideKeyResponse(byte[], byte[])
- */
+ /** @see MediaDrm#provideKeyResponse(byte[], byte[]) */
byte[] provideKeyResponse(byte[] scope, byte[] response)
throws NotProvisionedException, DeniedByServerException;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java
index d0c66f930a..7ddd03bbd5 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.util.Util;
+import java.util.Arrays;
/**
* Defines chunks of samples within a media stream.
@@ -102,4 +103,19 @@ public final class ChunkIndex implements SeekMap {
}
}
+ @Override
+ public String toString() {
+ return "ChunkIndex("
+ + "length="
+ + length
+ + ", sizes="
+ + Arrays.toString(sizes)
+ + ", offsets="
+ + Arrays.toString(offsets)
+ + ", timeUs="
+ + Arrays.toString(timesUs)
+ + ", durationsUs="
+ + Arrays.toString(durationsUs)
+ + ")";
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java
index b85ecba3a4..425f2b77cd 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.extractor;
+import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
@@ -35,18 +36,19 @@ import java.lang.reflect.Constructor;
* An {@link ExtractorsFactory} that provides an array of extractors for the following formats:
*
*
- * - MP4, including M4A ({@link Mp4Extractor})
- * - fMP4 ({@link FragmentedMp4Extractor})
- * - Matroska and WebM ({@link MatroskaExtractor})
- * - Ogg Vorbis/FLAC ({@link OggExtractor}
- * - MP3 ({@link Mp3Extractor})
- * - AAC ({@link AdtsExtractor})
- * - MPEG TS ({@link TsExtractor})
- * - MPEG PS ({@link PsExtractor})
- * - FLV ({@link FlvExtractor})
- * - WAV ({@link WavExtractor})
- * - AC3 ({@link Ac3Extractor})
- * - FLAC (only available if the FLAC extension is built and included)
+ * - MP4, including M4A ({@link Mp4Extractor})
+ *
- fMP4 ({@link FragmentedMp4Extractor})
+ *
- Matroska and WebM ({@link MatroskaExtractor})
+ *
- Ogg Vorbis/FLAC ({@link OggExtractor}
+ *
- MP3 ({@link Mp3Extractor})
+ *
- AAC ({@link AdtsExtractor})
+ *
- MPEG TS ({@link TsExtractor})
+ *
- MPEG PS ({@link PsExtractor})
+ *
- FLV ({@link FlvExtractor})
+ *
- WAV ({@link WavExtractor})
+ *
- AC3 ({@link Ac3Extractor})
+ *
- AMR ({@link AmrExtractor})
+ *
- FLAC (only available if the FLAC extension is built and included)
*
*/
public final class DefaultExtractorsFactory implements ExtractorsFactory {
@@ -159,7 +161,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
@Override
public synchronized Extractor[] createExtractors() {
- Extractor[] extractors = new Extractor[FLAC_EXTRACTOR_CONSTRUCTOR == null ? 11 : 12];
+ Extractor[] extractors = new Extractor[FLAC_EXTRACTOR_CONSTRUCTOR == null ? 12 : 13];
extractors[0] = new MatroskaExtractor(matroskaFlags);
extractors[1] = new FragmentedMp4Extractor(fragmentedMp4Flags);
extractors[2] = new Mp4Extractor(mp4Flags);
@@ -171,9 +173,10 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
extractors[8] = new OggExtractor();
extractors[9] = new PsExtractor();
extractors[10] = new WavExtractor();
+ extractors[11] = new AmrExtractor();
if (FLAC_EXTRACTOR_CONSTRUCTOR != null) {
try {
- extractors[11] = FLAC_EXTRACTOR_CONSTRUCTOR.newInstance();
+ extractors[12] = FLAC_EXTRACTOR_CONSTRUCTOR.newInstance();
} catch (Exception e) {
// Should never happen.
throw new IllegalStateException("Unexpected error creating FLAC extractor", e);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java
new file mode 100644
index 0000000000..b58e979c26
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java
@@ -0,0 +1,299 @@
+/*
+ * 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.extractor.amr;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * Extracts data from the AMR containers format (either AMR or AMR-WB). This follows RFC-4867,
+ * section 5.
+ *
+ * This extractor only supports single-channel AMR container formats.
+ */
+public final class AmrExtractor implements Extractor {
+
+ /** Factory for {@link AmrExtractor} instances. */
+ public static final ExtractorsFactory FACTORY =
+ new ExtractorsFactory() {
+
+ @Override
+ public Extractor[] createExtractors() {
+ return new Extractor[] {new AmrExtractor()};
+ }
+ };
+
+ /**
+ * The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR
+ * narrow band.
+ */
+ private static final int[] frameSizeBytesByTypeNb = {
+ 13,
+ 14,
+ 16,
+ 18,
+ 20,
+ 21,
+ 27,
+ 32,
+ 6, // AMR SID
+ 7, // GSM-EFR SID
+ 6, // TDMA-EFR SID
+ 6, // PDC-EFR SID
+ 1, // Future use
+ 1, // Future use
+ 1, // Future use
+ 1 // No data
+ };
+
+ /**
+ * The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR wide
+ * band.
+ */
+ private static final int[] frameSizeBytesByTypeWb = {
+ 18,
+ 24,
+ 33,
+ 37,
+ 41,
+ 47,
+ 51,
+ 59,
+ 61,
+ 6, // AMR-WB SID
+ 1, // Future use
+ 1, // Future use
+ 1, // Future use
+ 1, // Future use
+ 1, // speech lost
+ 1 // No data
+ };
+
+ private static final byte[] amrSignatureNb = Util.getUtf8Bytes("#!AMR\n");
+ private static final byte[] amrSignatureWb = Util.getUtf8Bytes("#!AMR-WB\n");
+
+ /** Theoretical maximum frame size for a AMR frame. */
+ private static final int MAX_FRAME_SIZE_BYTES = frameSizeBytesByTypeWb[8];
+
+ private static final int SAMPLE_RATE_WB = 16_000;
+ private static final int SAMPLE_RATE_NB = 8_000;
+ private static final int SAMPLE_TIME_PER_FRAME_US = 20_000;
+
+ private final byte[] scratch;
+
+ private boolean isWideBand;
+ private long currentSampleTimeUs;
+ private int currentSampleTotalBytes;
+ private int currentSampleBytesRemaining;
+
+ private TrackOutput trackOutput;
+ private boolean hasOutputFormat;
+
+ public AmrExtractor() {
+ scratch = new byte[1];
+ }
+
+ // Extractor implementation.
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ return readAmrHeader(input);
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
+ trackOutput = output.track(/* id= */ 0, C.TRACK_TYPE_AUDIO);
+ output.endTracks();
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ if (input.getPosition() == 0) {
+ if (!readAmrHeader(input)) {
+ throw new ParserException("Could not find AMR header.");
+ }
+ }
+ maybeOutputFormat();
+ return readSample(input);
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ currentSampleTimeUs = 0;
+ currentSampleTotalBytes = 0;
+ currentSampleBytesRemaining = 0;
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ /* package */ static int frameSizeBytesByTypeNb(int frameType) {
+ return frameSizeBytesByTypeNb[frameType];
+ }
+
+ /* package */ static int frameSizeBytesByTypeWb(int frameType) {
+ return frameSizeBytesByTypeWb[frameType];
+ }
+
+ /* package */ static byte[] amrSignatureNb() {
+ return Arrays.copyOf(amrSignatureNb, amrSignatureNb.length);
+ }
+
+ /* package */ static byte[] amrSignatureWb() {
+ return Arrays.copyOf(amrSignatureWb, amrSignatureWb.length);
+ }
+
+ // Internal methods.
+
+ /**
+ * Peeks the AMR header from the beginning of the input, and consumes it if it exists.
+ *
+ * @param input The {@link ExtractorInput} from which data should be peeked/read.
+ * @return Whether the AMR header has been read.
+ */
+ private boolean readAmrHeader(ExtractorInput input) throws IOException, InterruptedException {
+ if (peekAmrSignature(input, amrSignatureNb)) {
+ isWideBand = false;
+ input.skipFully(amrSignatureNb.length);
+ return true;
+ } else if (peekAmrSignature(input, amrSignatureWb)) {
+ isWideBand = true;
+ input.skipFully(amrSignatureWb.length);
+ return true;
+ }
+ return false;
+ }
+
+ /** Peeks from the beginning of the input to see if the given AMR signature exists. */
+ private boolean peekAmrSignature(ExtractorInput input, byte[] amrSignature)
+ throws IOException, InterruptedException {
+ input.resetPeekPosition();
+ byte[] header = new byte[amrSignature.length];
+ input.peekFully(header, 0, amrSignature.length);
+ return Arrays.equals(header, amrSignature);
+ }
+
+ private void maybeOutputFormat() {
+ if (!hasOutputFormat) {
+ hasOutputFormat = true;
+ String mimeType = isWideBand ? MimeTypes.AUDIO_AMR_WB : MimeTypes.AUDIO_AMR_NB;
+ int sampleRate = isWideBand ? SAMPLE_RATE_WB : SAMPLE_RATE_NB;
+ trackOutput.format(
+ Format.createAudioSampleFormat(
+ /* id= */ null,
+ mimeType,
+ /* codecs= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ MAX_FRAME_SIZE_BYTES,
+ /* channelCount= */ 1,
+ sampleRate,
+ /* pcmEncoding= */ Format.NO_VALUE,
+ /* initializationData= */ null,
+ /* drmInitData= */ null,
+ /* selectionFlags= */ 0,
+ /* language= */ null));
+ }
+ }
+
+ private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException {
+ if (currentSampleBytesRemaining == 0) {
+ try {
+ currentSampleTotalBytes = readNextSampleSize(extractorInput);
+ } catch (EOFException e) {
+ return RESULT_END_OF_INPUT;
+ }
+ currentSampleBytesRemaining = currentSampleTotalBytes;
+ }
+
+ int bytesAppended =
+ trackOutput.sampleData(
+ extractorInput, currentSampleBytesRemaining, /* allowEndOfInput= */ true);
+ if (bytesAppended == C.RESULT_END_OF_INPUT) {
+ return RESULT_END_OF_INPUT;
+ }
+ currentSampleBytesRemaining -= bytesAppended;
+ if (currentSampleBytesRemaining > 0) {
+ return RESULT_CONTINUE;
+ }
+
+ trackOutput.sampleMetadata(
+ currentSampleTimeUs,
+ C.BUFFER_FLAG_KEY_FRAME,
+ currentSampleTotalBytes,
+ /* offset= */ 0,
+ /* encryptionData= */ null);
+ currentSampleTimeUs += SAMPLE_TIME_PER_FRAME_US;
+ return RESULT_CONTINUE;
+ }
+
+ private int readNextSampleSize(ExtractorInput extractorInput)
+ throws IOException, InterruptedException {
+ extractorInput.resetPeekPosition();
+ extractorInput.peekFully(scratch, /* offset= */ 0, /* length= */ 1);
+
+ byte frameHeader = scratch[0];
+ if ((frameHeader & 0x83) > 0) {
+ // The padding bits are at bit-1 positions in the following pattern: 1000 0011
+ // Padding bits must be 0.
+ throw new ParserException("Invalid padding bits for frame header " + frameHeader);
+ }
+
+ int frameType = (frameHeader >> 3) & 0x0f;
+ return getFrameSizeInBytes(frameType);
+ }
+
+ private int getFrameSizeInBytes(int frameType) throws ParserException {
+ if (!isValidFrameType(frameType)) {
+ throw new ParserException(
+ "Illegal AMR " + (isWideBand ? "WB" : "NB") + " frame type " + frameType);
+ }
+
+ return isWideBand ? frameSizeBytesByTypeWb[frameType] : frameSizeBytesByTypeNb[frameType];
+ }
+
+ private boolean isValidFrameType(int frameType) {
+ return frameType >= 0
+ && frameType <= 15
+ && (isWideBandValidFrameType(frameType) || isNarrowBandValidFrameType(frameType));
+ }
+
+ private boolean isWideBandValidFrameType(int frameType) {
+ // For wide band, type 10-13 are for future use.
+ return isWideBand && (frameType < 10 || frameType > 13);
+ }
+
+ private boolean isNarrowBandValidFrameType(int frameType) {
+ // For narrow band, type 12-14 are for future use.
+ return !isWideBand && (frameType < 12 || frameType > 14);
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
index d30355699c..d1134dc3f6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
@@ -121,11 +121,11 @@ public final class FragmentedMp4Extractor implements Extractor {
// Workarounds.
@Flags private final int flags;
- private final Track sideloadedTrack;
+ private final @Nullable Track sideloadedTrack;
// Sideloaded data.
private final List closedCaptionFormats;
- private final DrmInitData sideloadedDrmInitData;
+ private final @Nullable DrmInitData sideloadedDrmInitData;
// Track-linked data bundle, accessible as a whole through trackID.
private final SparseArray trackBundles;
@@ -136,7 +136,7 @@ public final class FragmentedMp4Extractor implements Extractor {
private final ParsableByteArray nalBuffer;
// Adjusts sample timestamps.
- private final TimestampAdjuster timestampAdjuster;
+ private final @Nullable TimestampAdjuster timestampAdjuster;
// Parser state.
private final ParsableByteArray atomHeader;
@@ -185,20 +185,23 @@ public final class FragmentedMp4Extractor implements Extractor {
* @param flags Flags that control the extractor's behavior.
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
*/
- public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster) {
+ public FragmentedMp4Extractor(@Flags int flags, @Nullable TimestampAdjuster timestampAdjuster) {
this(flags, timestampAdjuster, null, null);
}
/**
* @param flags Flags that control the extractor's behavior.
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
- * @param sideloadedTrack Sideloaded track information, in the case that the extractor
- * will not receive a moov box in the input data. Null if a moov box is expected.
+ * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not
+ * receive a moov box in the input data. Null if a moov box is expected.
* @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the
* pssh boxes (if present) will be used.
*/
- public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster,
- Track sideloadedTrack, DrmInitData sideloadedDrmInitData) {
+ public FragmentedMp4Extractor(
+ @Flags int flags,
+ @Nullable TimestampAdjuster timestampAdjuster,
+ @Nullable Track sideloadedTrack,
+ @Nullable DrmInitData sideloadedDrmInitData) {
this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData,
Collections.emptyList());
}
@@ -206,15 +209,19 @@ public final class FragmentedMp4Extractor implements Extractor {
/**
* @param flags Flags that control the extractor's behavior.
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
- * @param sideloadedTrack Sideloaded track information, in the case that the extractor
- * will not receive a moov box in the input data. Null if a moov box is expected.
+ * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not
+ * receive a moov box in the input data. Null if a moov box is expected.
* @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the
* pssh boxes (if present) will be used.
* @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed
* caption channels to expose.
*/
- public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster,
- Track sideloadedTrack, DrmInitData sideloadedDrmInitData, List closedCaptionFormats) {
+ public FragmentedMp4Extractor(
+ @Flags int flags,
+ @Nullable TimestampAdjuster timestampAdjuster,
+ @Nullable Track sideloadedTrack,
+ @Nullable DrmInitData sideloadedDrmInitData,
+ List closedCaptionFormats) {
this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData,
closedCaptionFormats, null);
}
@@ -222,8 +229,8 @@ public final class FragmentedMp4Extractor implements Extractor {
/**
* @param flags Flags that control the extractor's behavior.
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
- * @param sideloadedTrack Sideloaded track information, in the case that the extractor
- * will not receive a moov box in the input data. Null if a moov box is expected.
+ * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not
+ * receive a moov box in the input data. Null if a moov box is expected.
* @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the
* pssh boxes (if present) will be used.
* @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed
@@ -232,8 +239,12 @@ public final class FragmentedMp4Extractor implements Extractor {
* targeting the player, even if {@link #FLAG_ENABLE_EMSG_TRACK} is not set. Null if special
* handling of emsg messages for players is not required.
*/
- public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster,
- Track sideloadedTrack, DrmInitData sideloadedDrmInitData, List closedCaptionFormats,
+ public FragmentedMp4Extractor(
+ @Flags int flags,
+ @Nullable TimestampAdjuster timestampAdjuster,
+ @Nullable Track sideloadedTrack,
+ @Nullable DrmInitData sideloadedDrmInitData,
+ List closedCaptionFormats,
@Nullable TrackOutput additionalEmsgTrackOutput) {
this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0);
this.timestampAdjuster = timestampAdjuster;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java
index 55ce41e4b1..84513ef4d3 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.extractor.mp4;
+import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.nio.ByteBuffer;
@@ -36,7 +37,7 @@ public final class PsshAtomUtil {
* @param data The scheme specific data.
* @return The PSSH atom.
*/
- public static byte[] buildPsshAtom(UUID systemId, byte[] data) {
+ public static byte[] buildPsshAtom(UUID systemId, @Nullable byte[] data) {
return buildPsshAtom(systemId, null, data);
}
@@ -48,7 +49,8 @@ public final class PsshAtomUtil {
* @param data The scheme specific data.
* @return The PSSH atom.
*/
- public static byte[] buildPsshAtom(UUID systemId, UUID[] keyIds, byte[] data) {
+ public static byte[] buildPsshAtom(
+ UUID systemId, @Nullable UUID[] keyIds, @Nullable byte[] data) {
boolean buildV1Atom = keyIds != null;
int dataLength = data != null ? data.length : 0;
int psshBoxLength = Atom.FULL_HEADER_SIZE + 16 /* SystemId */ + 4 /* DataSize */ + dataLength;
@@ -77,14 +79,14 @@ public final class PsshAtomUtil {
/**
* Parses the UUID from a PSSH atom. Version 0 and 1 PSSH atoms are supported.
- *
- * The UUID is only parsed if the data is a valid PSSH atom.
+ *
+ *
The UUID is only parsed if the data is a valid PSSH atom.
*
* @param atom The atom to parse.
- * @return The parsed UUID. Null if the input is not a valid PSSH atom, or if the PSSH atom has
- * an unsupported version.
+ * @return The parsed UUID. Null if the input is not a valid PSSH atom, or if the PSSH atom has an
+ * unsupported version.
*/
- public static UUID parseUuid(byte[] atom) {
+ public static @Nullable UUID parseUuid(byte[] atom) {
PsshAtom parsedAtom = parsePsshAtom(atom);
if (parsedAtom == null) {
return null;
@@ -111,8 +113,8 @@ public final class PsshAtomUtil {
/**
* Parses the scheme specific data from a PSSH atom. Version 0 and 1 PSSH atoms are supported.
- *
- * The scheme specific data is only parsed if the data is a valid PSSH atom matching the given
+ *
+ *
The scheme specific data is only parsed if the data is a valid PSSH atom matching the given
* UUID, or if the data is a valid PSSH atom of any type in the case that the passed UUID is null.
*
* @param atom The atom to parse.
@@ -120,7 +122,7 @@ public final class PsshAtomUtil {
* @return The parsed scheme specific data. Null if the input is not a valid PSSH atom, or if the
* PSSH atom has an unsupported version, or if the PSSH atom does not match the passed UUID.
*/
- public static byte[] parseSchemeSpecificData(byte[] atom, UUID uuid) {
+ public static @Nullable byte[] parseSchemeSpecificData(byte[] atom, UUID uuid) {
PsshAtom parsedAtom = parsePsshAtom(atom);
if (parsedAtom == null) {
return null;
@@ -140,7 +142,7 @@ public final class PsshAtomUtil {
* has an unsupported version.
*/
// TODO: Support parsing of the key ids for version 1 PSSH atoms.
- private static PsshAtom parsePsshAtom(byte[] atom) {
+ private static @Nullable PsshAtom parsePsshAtom(byte[] atom) {
ParsableByteArray atomData = new ParsableByteArray(atom);
if (atomData.limit() < Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */) {
// Data too short.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
index cd33051bd2..d822916bce 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
@@ -52,6 +52,15 @@ public final class MediaCodecInfo {
*/
public final String name;
+ /** The MIME type handled by the codec, or {@code null} if this is a passthrough codec. */
+ public final @Nullable String mimeType;
+
+ /**
+ * The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if this
+ * is a passthrough codec.
+ */
+ public final @Nullable CodecCapabilities capabilities;
+
/**
* Whether the decoder supports seamless resolution switches.
*
@@ -76,13 +85,8 @@ public final class MediaCodecInfo {
*/
public final boolean secure;
- /**
- * The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if this
- * is a passthrough codec.
- */
- public final @Nullable CodecCapabilities capabilities;
-
- private final String mimeType;
+ /** Whether this instance describes a passthrough codec. */
+ public final boolean passthrough;
/**
* Creates an instance representing an audio passthrough decoder.
@@ -95,6 +99,7 @@ public final class MediaCodecInfo {
name,
/* mimeType= */ null,
/* capabilities= */ null,
+ /* passthrough= */ true,
/* forceDisableAdaptive= */ false,
/* forceSecure= */ false);
}
@@ -110,7 +115,12 @@ public final class MediaCodecInfo {
public static MediaCodecInfo newInstance(String name, String mimeType,
CodecCapabilities capabilities) {
return new MediaCodecInfo(
- name, mimeType, capabilities, /* forceDisableAdaptive= */ false, /* forceSecure= */ false);
+ name,
+ mimeType,
+ capabilities,
+ /* passthrough= */ false,
+ /* forceDisableAdaptive= */ false,
+ /* forceSecure= */ false);
}
/**
@@ -129,18 +139,21 @@ public final class MediaCodecInfo {
CodecCapabilities capabilities,
boolean forceDisableAdaptive,
boolean forceSecure) {
- return new MediaCodecInfo(name, mimeType, capabilities, forceDisableAdaptive, forceSecure);
+ return new MediaCodecInfo(
+ name, mimeType, capabilities, /* passthrough= */ false, forceDisableAdaptive, forceSecure);
}
private MediaCodecInfo(
String name,
- String mimeType,
+ @Nullable String mimeType,
@Nullable CodecCapabilities capabilities,
+ boolean passthrough,
boolean forceDisableAdaptive,
boolean forceSecure) {
this.name = Assertions.checkNotNull(name);
- this.capabilities = capabilities;
this.mimeType = mimeType;
+ this.capabilities = capabilities;
+ this.passthrough = passthrough;
adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities);
tunneling = capabilities != null && isTunneling(capabilities);
secure = forceSecure || (capabilities != null && isSecure(capabilities));
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
index 2f95d15f12..03a0b66661 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
@@ -129,7 +129,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
*/
private static final long MAX_CODEC_HOTSWAP_TIME_MS = 1000;
- /** The possible return values for {@link #canKeepCodec(MediaCodec, boolean, Format, Format)}. */
+ /**
+ * The possible return values for {@link #canKeepCodec(MediaCodec, MediaCodecInfo, Format,
+ * Format)}.
+ */
@Retention(RetentionPolicy.SOURCE)
@IntDef({
KEEP_CODEC_RESULT_NO,
@@ -885,7 +888,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
boolean keepingCodec = false;
if (pendingDrmSession == drmSession && codec != null) {
- switch (canKeepCodec(codec, codecInfo.adaptive, oldFormat, format)) {
+ switch (canKeepCodec(codec, codecInfo, oldFormat, format)) {
case KEEP_CODEC_RESULT_NO:
// Do nothing.
break;
@@ -962,13 +965,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
*
The default implementation returns {@link #KEEP_CODEC_RESULT_NO}.
*
* @param codec The existing {@link MediaCodec} instance.
- * @param codecIsAdaptive Whether the codec is adaptive.
+ * @param codecInfo A {@link MediaCodecInfo} describing the decoder.
* @param oldFormat The format for which the existing instance is configured.
* @param newFormat The new format.
* @return Whether the instance can be kept, and if it can whether it requires reconfiguration.
*/
protected @KeepCodecResult int canKeepCodec(
- MediaCodec codec, boolean codecIsAdaptive, Format oldFormat, Format newFormat) {
+ MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) {
return KEEP_CODEC_RESULT_NO;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java
index 0724755ac7..e37e09a090 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java
@@ -29,6 +29,8 @@ import java.io.InputStream;
*/
public final class ActionFile {
+ /* package */ static final int VERSION = 0;
+
private final AtomicFile atomicFile;
private final File actionFile;
@@ -56,13 +58,13 @@ public final class ActionFile {
inputStream = atomicFile.openRead();
DataInputStream dataInputStream = new DataInputStream(inputStream);
int version = dataInputStream.readInt();
- if (version > DownloadAction.MASTER_VERSION) {
- throw new IOException("Not supported action file version: " + version);
+ if (version > VERSION) {
+ throw new IOException("Unsupported action file version: " + version);
}
int actionCount = dataInputStream.readInt();
DownloadAction[] actions = new DownloadAction[actionCount];
for (int i = 0; i < actionCount; i++) {
- actions[i] = DownloadAction.deserializeFromStream(deserializers, dataInputStream, version);
+ actions[i] = DownloadAction.deserializeFromStream(deserializers, dataInputStream);
}
return actions;
} finally {
@@ -80,7 +82,7 @@ public final class ActionFile {
DataOutputStream output = null;
try {
output = new DataOutputStream(atomicFile.startWrite());
- output.writeInt(DownloadAction.MASTER_VERSION);
+ output.writeInt(VERSION);
output.writeInt(downloadActions.length);
for (DownloadAction action : downloadActions) {
DownloadAction.serializeToStream(action, output);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java
index cec46c3664..cf061f3745 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.offline;
+import android.net.Uri;
import android.support.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
@@ -22,78 +23,58 @@ import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.Arrays;
/** Contains the necessary parameters for a download or remove action. */
public abstract class DownloadAction {
- /**
- * Master version for all {@link DownloadAction} serialization/deserialization implementations. On
- * each change on any {@link DownloadAction} serialization format this version needs to be
- * increased.
- */
- public static final int MASTER_VERSION = 0;
-
/** Used to deserialize {@link DownloadAction}s. */
- public interface Deserializer {
+ public abstract static class Deserializer {
- /** Returns the type string of the {@link DownloadAction}. This string should be unique. */
- String getType();
+ public final String type;
+ public final int version;
+
+ public Deserializer(String type, int version) {
+ this.type = type;
+ this.version = version;
+ }
/**
- * Deserializes a {@link DownloadAction} from the {@code input}.
+ * Deserializes an action from the {@code input}.
*
- * @param version Version of the data.
- * @param input DataInputStream to read data from.
+ * @param version The version of the serialized action.
+ * @param input The stream from which to read the action.
* @see DownloadAction#writeToStream(DataOutputStream)
- * @see DownloadAction#MASTER_VERSION
*/
- DownloadAction readFromStream(int version, DataInputStream input) throws IOException;
+ public abstract DownloadAction readFromStream(int version, DataInputStream input)
+ throws IOException;
}
/**
- * Deserializes one {@code action} which was serialized by {@link
- * #serializeToStream(DownloadAction, OutputStream)} from the {@code input} using one of the
- * {@link Deserializer}s which supports the type of the action.
+ * Deserializes one action that was serialized with {@link #serializeToStream(DownloadAction,
+ * OutputStream)} from the {@code input}, using the {@link Deserializer}s that supports the
+ * action's type.
*
*
The caller is responsible for closing the given {@link InputStream}.
*
- * @param deserializers Array of {@link Deserializer}s to deserialize a {@link DownloadAction}.
- * @param input Input stream to read serialized data.
- * @return The deserialized {@link DownloadAction}.
- * @throws IOException If there is an IO error from {@code input} or the action type isn't
- * supported by any of the {@code deserializers}.
+ * @param deserializers {@link Deserializer}s for supported actions.
+ * @param input The stream from which to read the action.
+ * @return The deserialized action.
+ * @throws IOException If there is an IO error reading from {@code input}, or if the action type
+ * isn't supported by any of the {@code deserializers}.
*/
public static DownloadAction deserializeFromStream(
Deserializer[] deserializers, InputStream input) throws IOException {
- return deserializeFromStream(deserializers, input, MASTER_VERSION);
- }
-
- /**
- * Deserializes one {@code action} which was serialized by {@link
- * #serializeToStream(DownloadAction, OutputStream)} from the {@code input} using one of the
- * {@link Deserializer}s which supports the type of the action.
- *
- *
The caller is responsible for closing the given {@link InputStream}.
- *
- * @param deserializers Array of {@link Deserializer}s to deserialize a {@link DownloadAction}.
- * @param input Input stream to read serialized data.
- * @param version Master version of the serialization. See {@link DownloadAction#MASTER_VERSION}.
- * @return The deserialized {@link DownloadAction}.
- * @throws IOException If there is an IO error from {@code input}.
- * @throws DownloadException If the action type isn't supported by any of the {@code
- * deserializers}.
- */
- public static DownloadAction deserializeFromStream(
- Deserializer[] deserializers, InputStream input, int version) throws IOException {
// Don't close the stream as it closes the underlying stream too.
DataInputStream dataInputStream = new DataInputStream(input);
String type = dataInputStream.readUTF();
+ int version = dataInputStream.readInt();
for (Deserializer deserializer : deserializers) {
- if (type.equals(deserializer.getType())) {
+ if (type.equals(deserializer.type) && deserializer.version >= version) {
return deserializer.readFromStream(version, dataInputStream);
}
}
- throw new DownloadException("No Deserializer can be found to parse the data.");
+ throw new DownloadException("No deserializer found for:" + type + ", " + version);
}
/** Serializes {@code action} type and data into the {@code output}. */
@@ -101,16 +82,37 @@ public abstract class DownloadAction {
throws IOException {
// Don't close the stream as it closes the underlying stream too.
DataOutputStream dataOutputStream = new DataOutputStream(output);
- dataOutputStream.writeUTF(action.getType());
+ dataOutputStream.writeUTF(action.type);
+ dataOutputStream.writeInt(action.version);
action.writeToStream(dataOutputStream);
dataOutputStream.flush();
}
- private final String data;
+ /** The type of the action. */
+ public final String type;
+ /** The action version. */
+ public final int version;
+ /** The uri being downloaded or removed. */
+ public final Uri uri;
+ /** Whether this is a remove action. If false, this is a download action. */
+ public final boolean isRemoveAction;
+ /** Custom data for this action. May be empty. */
+ public final byte[] data;
- /** @param data Optional custom data for this action. If null, an empty string is used. */
- protected DownloadAction(@Nullable String data) {
- this.data = data != null ? data : "";
+ /**
+ * @param type The type of the action.
+ * @param version The action version.
+ * @param uri The uri being downloaded or removed.
+ * @param isRemoveAction Whether this is a remove action. If false, this is a download action.
+ * @param data Optional custom data for this action.
+ */
+ protected DownloadAction(
+ String type, int version, Uri uri, boolean isRemoveAction, @Nullable byte[] data) {
+ this.type = type;
+ this.version = version;
+ this.uri = uri;
+ this.isRemoveAction = isRemoveAction;
+ this.data = data != null ? data : new byte[0];
}
/** Serializes itself into a byte array. */
@@ -125,23 +127,14 @@ public abstract class DownloadAction {
return output.toByteArray();
}
- /** Returns custom data for this action. */
- public final String getData() {
- return data;
+ /** Returns whether this is an action for the same media as the {@code other}. */
+ public boolean isSameMedia(DownloadAction other) {
+ return uri.equals(other.uri);
}
- /** Returns whether this is a remove action or a download action. */
- public abstract boolean isRemoveAction();
-
- /** Returns the type string of the {@link DownloadAction}. This string should be unique. */
- protected abstract String getType();
-
/** Serializes itself into the {@code output}. */
protected abstract void writeToStream(DataOutputStream output) throws IOException;
- /** Returns whether this is an action for the same media as the {@code other}. */
- protected abstract boolean isSameMedia(DownloadAction other);
-
/** Creates a {@link Downloader} with the given parameters. */
protected abstract Downloader createDownloader(
DownloaderConstructorHelper downloaderConstructorHelper);
@@ -152,13 +145,18 @@ public abstract class DownloadAction {
return false;
}
DownloadAction that = (DownloadAction) o;
- return data.equals(that.data) && isRemoveAction() == that.isRemoveAction();
+ return type.equals(that.type)
+ && version == that.version
+ && uri.equals(that.uri)
+ && isRemoveAction == that.isRemoveAction
+ && Arrays.equals(data, that.data);
}
@Override
public int hashCode() {
- int result = data.hashCode();
- result = 31 * result + (isRemoveAction() ? 1 : 0);
+ int result = uri.hashCode();
+ result = 31 * result + (isRemoveAction ? 1 : 0);
+ result = 31 * result + Arrays.hashCode(data);
return result;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java
new file mode 100644
index 0000000000..f6157c1dc3
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java
@@ -0,0 +1,122 @@
+/*
+ * 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 android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import java.io.IOException;
+import java.util.List;
+
+/** A helper for initializing and removing downloads. */
+public abstract class DownloadHelper {
+
+ /** A callback to be notified when the {@link DownloadHelper} is prepared. */
+ public interface Callback {
+
+ /**
+ * Called when preparation completes.
+ *
+ * @param helper The reporting {@link DownloadHelper}.
+ */
+ void onPrepared(DownloadHelper helper);
+
+ /**
+ * Called when preparation fails.
+ *
+ * @param helper The reporting {@link DownloadHelper}.
+ * @param e The error.
+ */
+ void onPrepareError(DownloadHelper helper, IOException e);
+ }
+
+ /**
+ * Initializes the helper for starting a download.
+ *
+ * @param callback A callback to be notified when preparation completes or fails. The callback
+ * will be invoked on the calling thread unless that thread does not have an associated {@link
+ * Looper}, in which case it will be called on the application's main thread.
+ */
+ public void prepare(final Callback callback) {
+ final Handler handler =
+ new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper());
+ new Thread() {
+ @Override
+ public void run() {
+ try {
+ prepareInternal();
+ handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ callback.onPrepared(DownloadHelper.this);
+ }
+ });
+ } catch (final IOException e) {
+ handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ callback.onPrepareError(DownloadHelper.this, e);
+ }
+ });
+ }
+ }
+ }.start();
+ }
+
+ /**
+ * Called on a background thread during preparation.
+ *
+ * @throws IOException If preparation fails.
+ */
+ protected abstract void prepareInternal() throws IOException;
+
+ /**
+ * Returns the number of periods for which media is available. Must not be called until after
+ * preparation completes.
+ */
+ public abstract int getPeriodCount();
+
+ /**
+ * Returns the track groups for the given period. Must not be called until after preparation
+ * completes.
+ *
+ * @param periodIndex The period index.
+ * @return The track groups for the period. May be {@link TrackGroupArray#EMPTY} for single stream
+ * content.
+ */
+ public abstract TrackGroupArray getTrackGroups(int periodIndex);
+
+ /**
+ * Builds a {@link DownloadAction} for downloading the specified tracks. Must not be called until
+ * after preparation completes.
+ *
+ * @param data Application provided data to store in {@link DownloadAction#data}.
+ * @param trackKeys The selected tracks. If empty, all streams will be downloaded.
+ * @return The built {@link DownloadAction}.
+ */
+ public abstract DownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys);
+
+ /**
+ * Builds a {@link DownloadAction} for removing the media. May be called in any state.
+ *
+ * @param data Application provided data to store in {@link DownloadAction#data}.
+ * @return The built {@link DownloadAction}.
+ */
+ public abstract DownloadAction getRemoveAction(@Nullable byte[] data);
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java
index 9be42ac5bb..8be822b6ca 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java
@@ -15,20 +15,23 @@
*/
package com.google.android.exoplayer2.offline;
-import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_CANCELED;
-import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_ENDED;
-import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_ERROR;
-import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_QUEUED;
-import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_STARTED;
+import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_CANCELED;
+import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_COMPLETED;
+import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_FAILED;
+import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_QUEUED;
+import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_STARTED;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.offline.DownloadAction.Deserializer;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.util.Assertions;
import java.io.ByteArrayInputStream;
import java.io.File;
@@ -36,29 +39,33 @@ import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* Manages multiple stream download and remove requests.
*
- * By default downloads are stopped. Call {@link #startDownloads()} to start downloads.
- *
- *
WARNING: Methods of this class must be called only on the main thread of the application.
+ *
A download manager instance must be accessed only from the thread that created it, unless that
+ * thread does not have a {@link Looper}. In that case, it must be accessed only from the
+ * application's main thread. Registered listeners will be called on the same thread.
*/
public final class DownloadManager {
- /**
- * Listener for download events. Listener methods are called on the main thread of the
- * application.
- */
- public interface DownloadListener {
+ /** Listener for {@link DownloadManager} events. */
+ public interface Listener {
/**
- * Called on download state change.
+ * Called when all actions have been restored.
*
* @param downloadManager The reporting instance.
- * @param downloadState The download task.
*/
- void onStateChange(DownloadManager downloadManager, DownloadState downloadState);
+ void onInitialized(DownloadManager downloadManager);
+ /**
+ * Called when the state of a task changes.
+ *
+ * @param downloadManager The reporting instance.
+ * @param taskState The state of the task.
+ */
+ void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState);
/**
* Called when there is no active task left.
@@ -68,9 +75,9 @@ public final class DownloadManager {
void onIdle(DownloadManager downloadManager);
}
- /** The default maximum number of simultaneous downloads. */
+ /** The default maximum number of simultaneous download tasks. */
public static final int DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS = 1;
- /** The default minimum number of times the downloads must be retried before failing. */
+ /** The default minimum number of times a task must be retried before failing. */
public static final int DEFAULT_MIN_RETRY_COUNT = 5;
private static final String TAG = "DownloadManager";
@@ -81,34 +88,34 @@ public final class DownloadManager {
private final int minRetryCount;
private final ActionFile actionFile;
private final DownloadAction.Deserializer[] deserializers;
- private final ArrayList tasks;
- private final ArrayList activeDownloadTasks;
+ private final ArrayList tasks;
+ private final ArrayList activeDownloadTasks;
private final Handler handler;
private final HandlerThread fileIOThread;
private final Handler fileIOHandler;
- private final CopyOnWriteArraySet listeners;
+ private final CopyOnWriteArraySet listeners;
private int nextTaskId;
- private boolean actionFileLoadCompleted;
+ private boolean initialized;
private boolean released;
private boolean downloadsStopped;
/**
- * Constructs a {@link DownloadManager}.
+ * Creates a {@link DownloadManager}.
*
- * @param constructorHelper A {@link DownloaderConstructorHelper} to create {@link Downloader}s
- * for downloading data.
+ * @param cache Cache instance to be used to store downloaded data.
+ * @param upstreamDataSourceFactory A {@link DataSource.Factory} for creating data sources for
+ * downloading upstream data.
* @param actionSaveFile File to save active actions.
* @param deserializers Used to deserialize {@link DownloadAction}s.
*/
public DownloadManager(
- DownloaderConstructorHelper constructorHelper,
- String actionSaveFile,
+ Cache cache,
+ DataSource.Factory upstreamDataSourceFactory,
+ File actionSaveFile,
Deserializer... deserializers) {
this(
- constructorHelper,
- DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS,
- DEFAULT_MIN_RETRY_COUNT,
+ new DownloaderConstructorHelper(cache, upstreamDataSourceFactory),
actionSaveFile,
deserializers);
}
@@ -118,24 +125,43 @@ public final class DownloadManager {
*
* @param constructorHelper A {@link DownloaderConstructorHelper} to create {@link Downloader}s
* for downloading data.
- * @param maxSimultaneousDownloads The maximum number of simultaneous downloads.
- * @param minRetryCount The minimum number of times the downloads must be retried before failing.
- * @param actionSaveFile File to save active actions.
+ * @param actionFile The file in which active actions are saved.
+ * @param deserializers Used to deserialize {@link DownloadAction}s.
+ */
+ public DownloadManager(
+ DownloaderConstructorHelper constructorHelper,
+ File actionFile,
+ Deserializer... deserializers) {
+ this(
+ constructorHelper,
+ DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS,
+ DEFAULT_MIN_RETRY_COUNT,
+ actionFile,
+ deserializers);
+ }
+
+ /**
+ * Constructs a {@link DownloadManager}.
+ *
+ * @param constructorHelper A {@link DownloaderConstructorHelper} to create {@link Downloader}s
+ * for downloading data.
+ * @param maxSimultaneousDownloads The maximum number of simultaneous download tasks.
+ * @param minRetryCount The minimum number of times a task must be retried before failing.
+ * @param actionFile The file in which active actions are saved.
* @param deserializers Used to deserialize {@link DownloadAction}s.
*/
public DownloadManager(
DownloaderConstructorHelper constructorHelper,
int maxSimultaneousDownloads,
int minRetryCount,
- String actionSaveFile,
+ File actionFile,
Deserializer... deserializers) {
- Assertions.checkArgument(
- deserializers.length > 0, "At least one Deserializer should be given.");
+ Assertions.checkArgument(deserializers.length > 0, "At least one Deserializer is required.");
this.downloaderConstructorHelper = constructorHelper;
this.maxActiveDownloadTasks = maxSimultaneousDownloads;
this.minRetryCount = minRetryCount;
- this.actionFile = new ActionFile(new File(actionSaveFile));
+ this.actionFile = new ActionFile(actionFile);
this.deserializers = deserializers;
this.downloadsStopped = true;
@@ -155,14 +181,143 @@ public final class DownloadManager {
listeners = new CopyOnWriteArraySet<>();
loadActions();
- logd("DownloadManager is created");
+ logd("Created");
}
/**
- * Stops all of the tasks and releases resources. If the action file isn't up to date,
- * waits for the changes to be written.
+ * Adds a {@link Listener}.
+ *
+ * @param listener The listener to be added.
+ */
+ public void addListener(Listener listener) {
+ listeners.add(listener);
+ }
+
+ /**
+ * Removes a {@link Listener}.
+ *
+ * @param listener The listener to be removed.
+ */
+ public void removeListener(Listener listener) {
+ listeners.remove(listener);
+ }
+
+ /** Starts the download tasks. */
+ public void startDownloads() {
+ Assertions.checkState(!released);
+ if (downloadsStopped) {
+ downloadsStopped = false;
+ maybeStartTasks();
+ logd("Downloads are started");
+ }
+ }
+
+ /** Stops all of the download tasks. Call {@link #startDownloads()} to restart tasks. */
+ public void stopDownloads() {
+ Assertions.checkState(!released);
+ if (!downloadsStopped) {
+ downloadsStopped = true;
+ for (int i = 0; i < activeDownloadTasks.size(); i++) {
+ activeDownloadTasks.get(i).stop();
+ }
+ logd("Downloads are stopping");
+ }
+ }
+
+ /**
+ * Deserializes an action from {@code actionData}, and calls {@link
+ * #handleAction(DownloadAction)}.
+ *
+ * @param actionData Serialized version of the action to be executed.
+ * @return The id of the newly created task.
+ * @throws IOException If an error occurs deserializing the action.
+ */
+ public int handleAction(byte[] actionData) throws IOException {
+ Assertions.checkState(!released);
+ ByteArrayInputStream input = new ByteArrayInputStream(actionData);
+ DownloadAction action = DownloadAction.deserializeFromStream(deserializers, input);
+ return handleAction(action);
+ }
+
+ /**
+ * Handles the given action. A task is created and added to the task queue. If it's a remove
+ * action then any download tasks for the same media are immediately canceled.
+ *
+ * @param action The action to be executed.
+ * @return The id of the newly created task.
+ */
+ public int handleAction(DownloadAction action) {
+ Assertions.checkState(!released);
+ Task task = addTaskForAction(action);
+ if (initialized) {
+ notifyListenersTaskStateChange(task);
+ saveActions();
+ maybeStartTasks();
+ if (task.currentState == STATE_QUEUED) {
+ // Task did not change out of its initial state, and so its initial state won't have been
+ // reported to listeners. Do so now.
+ notifyListenersTaskStateChange(task);
+ }
+ }
+ return task.id;
+ }
+
+ /** Returns the current number of tasks. */
+ public int getTaskCount() {
+ Assertions.checkState(!released);
+ return tasks.size();
+ }
+
+ /** Returns the state of a task, or null if no such task exists */
+ public @Nullable TaskState getTaskState(int taskId) {
+ Assertions.checkState(!released);
+ for (int i = 0; i < tasks.size(); i++) {
+ Task task = tasks.get(i);
+ if (task.id == taskId) {
+ return task.getDownloadState();
+ }
+ }
+ return null;
+ }
+
+ /** Returns the states of all current tasks. */
+ public TaskState[] getAllTaskStates() {
+ Assertions.checkState(!released);
+ TaskState[] states = new TaskState[tasks.size()];
+ for (int i = 0; i < states.length; i++) {
+ states[i] = tasks.get(i).getDownloadState();
+ }
+ return states;
+ }
+
+ /** Returns whether the manager has completed initialization. */
+ public boolean isInitialized() {
+ Assertions.checkState(!released);
+ return initialized;
+ }
+
+ /** Returns whether there are no active tasks. */
+ public boolean isIdle() {
+ Assertions.checkState(!released);
+ if (!initialized) {
+ return false;
+ }
+ for (int i = 0; i < tasks.size(); i++) {
+ if (tasks.get(i).isActive()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Stops all of the tasks and releases resources. If the action file isn't up to date, waits for
+ * the changes to be written. The manager must not be accessed after this method has been called.
*/
public void release() {
+ if (released) {
+ return;
+ }
released = true;
for (int i = 0; i < tasks.size(); i++) {
tasks.get(i).stop();
@@ -176,125 +331,14 @@ public final class DownloadManager {
});
fileIOFinishedCondition.block();
fileIOThread.quit();
- logd("DownloadManager is released");
+ logd("Released");
}
- /** Stops all of the download tasks. Call {@link #startDownloads()} to restart tasks. */
- public void stopDownloads() {
- if (!downloadsStopped) {
- downloadsStopped = true;
- for (int i = 0; i < activeDownloadTasks.size(); i++) {
- activeDownloadTasks.get(i).stop();
- }
- logd("Downloads are stopping");
- }
- }
-
- /** Starts the download tasks. */
- public void startDownloads() {
- if (downloadsStopped) {
- downloadsStopped = false;
- maybeStartTasks();
- logd("Downloads are started");
- }
- }
-
- /**
- * Adds a {@link DownloadListener}.
- *
- * @param listener The listener to be added.
- */
- public void addListener(DownloadListener listener) {
- listeners.add(listener);
- }
-
- /**
- * Removes a {@link DownloadListener}.
- *
- * @param listener The listener to be removed.
- */
- public void removeListener(DownloadListener listener) {
- listeners.remove(listener);
- }
-
- /**
- * Deserializes one {@link DownloadAction} from {@code actionData} and calls {@link
- * #handleAction(DownloadAction)}.
- *
- * @param actionData Serialized {@link DownloadAction} data.
- * @return The task id.
- * @throws IOException If an error occurs during handling action.
- */
- public int handleAction(byte[] actionData) throws IOException {
- ByteArrayInputStream input = new ByteArrayInputStream(actionData);
- DownloadAction action = DownloadAction.deserializeFromStream(deserializers, input);
- return handleAction(action);
- }
-
- /**
- * Handles the given {@link DownloadAction}. A task is created and added to the task queue. If
- * it's a remove action then this method cancels any download tasks which works on the same media
- * immediately.
- *
- * @param downloadAction Action to be executed.
- * @return The task id.
- */
- public int handleAction(DownloadAction downloadAction) {
- DownloadTask downloadTask = createDownloadTask(downloadAction);
- saveActions();
- if (downloadsStopped && !downloadAction.isRemoveAction()) {
- logd("Can't start the task as downloads are stopped", downloadTask);
- } else {
- maybeStartTasks();
- }
- return downloadTask.id;
- }
-
- private DownloadTask createDownloadTask(DownloadAction downloadAction) {
- DownloadTask downloadTask = new DownloadTask(nextTaskId++, this, downloadAction, minRetryCount);
- tasks.add(downloadTask);
- logd("Task is added", downloadTask);
- notifyListenersTaskStateChange(downloadTask);
- return downloadTask;
- }
-
- /** Returns number of tasks. */
- public int getTaskCount() {
- return tasks.size();
- }
-
- /** Returns a {@link DownloadTask} for a task. */
- public DownloadState getDownloadState(int taskId) {
- for (int i = 0; i < tasks.size(); i++) {
- DownloadTask task = tasks.get(i);
- if (task.id == taskId) {
- return task.getDownloadState();
- }
- }
- return null;
- }
-
- /** Returns {@link DownloadState}s for all tasks. */
- public DownloadState[] getDownloadStates() {
- return getDownloadStates(tasks);
- }
-
- /** Returns an array of {@link DownloadState}s for active download tasks. */
- public DownloadState[] getActiveDownloadStates() {
- return getDownloadStates(activeDownloadTasks);
- }
-
- /** Returns whether there are no active tasks. */
- public boolean isIdle() {
- if (!actionFileLoadCompleted) {
- return false;
- }
- for (int i = 0; i < tasks.size(); i++) {
- if (tasks.get(i).isActive()) {
- return false;
- }
- }
- return true;
+ private Task addTaskForAction(DownloadAction action) {
+ Task task = new Task(nextTaskId++, this, action, minRetryCount);
+ tasks.add(task);
+ logd("Task is added", task);
+ return task;
}
/**
@@ -310,34 +354,34 @@ public final class DownloadManager {
* If the task is a remove action then preceding conflicting tasks are canceled.
*/
private void maybeStartTasks() {
- if (released) {
+ if (!initialized || released) {
return;
}
boolean skipDownloadActions = downloadsStopped
|| activeDownloadTasks.size() == maxActiveDownloadTasks;
for (int i = 0; i < tasks.size(); i++) {
- DownloadTask downloadTask = tasks.get(i);
- if (!downloadTask.canStart()) {
+ Task task = tasks.get(i);
+ if (!task.canStart()) {
continue;
}
- DownloadAction downloadAction = downloadTask.downloadAction;
- boolean removeAction = downloadAction.isRemoveAction();
- if (!removeAction && skipDownloadActions) {
+ DownloadAction action = task.action;
+ boolean isRemoveAction = action.isRemoveAction;
+ if (!isRemoveAction && skipDownloadActions) {
continue;
}
boolean canStartTask = true;
for (int j = 0; j < i; j++) {
- DownloadTask task = tasks.get(j);
- if (task.downloadAction.isSameMedia(downloadAction)) {
- if (removeAction) {
+ Task otherTask = tasks.get(j);
+ if (otherTask.action.isSameMedia(action)) {
+ if (isRemoveAction) {
canStartTask = false;
- logd(downloadTask + " clashes with " + task);
- task.cancel();
+ logd(task + " clashes with " + otherTask);
+ otherTask.cancel();
// Continue loop to cancel any other preceding clashing tasks.
- } else if (task.downloadAction.isRemoveAction()) {
+ } else if (otherTask.action.isRemoveAction) {
canStartTask = false;
skipDownloadActions = true;
break;
@@ -346,9 +390,9 @@ public final class DownloadManager {
}
if (canStartTask) {
- downloadTask.start();
- if (!removeAction) {
- activeDownloadTasks.add(downloadTask);
+ task.start();
+ if (!isRemoveAction) {
+ activeDownloadTasks.add(task);
skipDownloadActions = activeDownloadTasks.size() == maxActiveDownloadTasks;
}
}
@@ -360,23 +404,23 @@ public final class DownloadManager {
return;
}
logd("Notify idle state");
- for (DownloadListener listener : listeners) {
+ for (Listener listener : listeners) {
listener.onIdle(this);
}
}
- private void onTaskStateChange(DownloadTask downloadTask) {
+ private void onTaskStateChange(Task task) {
if (released) {
return;
}
- logd("Task state is changed", downloadTask);
- boolean stopped = !downloadTask.isActive();
+ logd("Task state is changed", task);
+ boolean stopped = !task.isActive();
if (stopped) {
- activeDownloadTasks.remove(downloadTask);
+ activeDownloadTasks.remove(task);
}
- notifyListenersTaskStateChange(downloadTask);
- if (downloadTask.isFinished()) {
- tasks.remove(downloadTask);
+ notifyListenersTaskStateChange(task);
+ if (task.isFinished()) {
+ tasks.remove(task);
saveActions();
}
if (stopped) {
@@ -385,10 +429,10 @@ public final class DownloadManager {
}
}
- private void notifyListenersTaskStateChange(DownloadTask downloadTask) {
- DownloadState downloadState = downloadTask.getDownloadState();
- for (DownloadListener listener : listeners) {
- listener.onStateChange(this, downloadState);
+ private void notifyListenersTaskStateChange(Task task) {
+ TaskState taskState = task.getDownloadState();
+ for (Listener listener : listeners) {
+ listener.onTaskStateChanged(this, taskState);
}
}
@@ -410,15 +454,33 @@ public final class DownloadManager {
new Runnable() {
@Override
public void run() {
- try {
- for (DownloadAction action : actions) {
- createDownloadTask(action);
+ if (released) {
+ return;
+ }
+ List pendingTasks = new ArrayList<>(tasks);
+ tasks.clear();
+ for (DownloadAction action : actions) {
+ addTaskForAction(action);
+ }
+ logd("Tasks are created.");
+ initialized = true;
+ for (Listener listener : listeners) {
+ listener.onInitialized(DownloadManager.this);
+ }
+ if (!pendingTasks.isEmpty()) {
+ for (int i = 0; i < pendingTasks.size(); i++) {
+ tasks.add(pendingTasks.get(i));
+ }
+ saveActions();
+ }
+ maybeStartTasks();
+ for (int i = 0; i < pendingTasks.size(); i++) {
+ Task pendingTask = pendingTasks.get(i);
+ if (pendingTask.currentState == STATE_QUEUED) {
+ // Task did not change out of its initial state, and so its initial state
+ // won't have been reported to listeners. Do so now.
+ notifyListenersTaskStateChange(pendingTask);
}
- logd("Tasks are created.");
- maybeStartTasks();
- } finally {
- actionFileLoadCompleted = true;
- maybeNotifyListenersIdle();
}
}
});
@@ -427,12 +489,12 @@ public final class DownloadManager {
}
private void saveActions() {
- if (!actionFileLoadCompleted || released) {
+ if (released) {
return;
}
final DownloadAction[] actions = new DownloadAction[tasks.size()];
for (int i = 0; i < tasks.size(); i++) {
- actions[i] = tasks.get(i).downloadAction;
+ actions[i] = tasks.get(i).action;
}
fileIOHandler.post(new Runnable() {
@Override
@@ -447,27 +509,18 @@ public final class DownloadManager {
});
}
- private void logd(String message) {
+ private static void logd(String message) {
if (DEBUG) {
Log.d(TAG, message);
}
}
- private void logd(String message, DownloadTask task) {
+ private static void logd(String message, Task task) {
logd(message + ": " + task);
}
- private static DownloadState[] getDownloadStates(ArrayList tasks) {
- DownloadState[] states = new DownloadState[tasks.size()];
- for (int i = 0; i < tasks.size(); i++) {
- DownloadTask task = tasks.get(i);
- states[i] = task.getDownloadState();
- }
- return states;
- }
-
- /** Represents state of a download task. */
- public static final class DownloadState {
+ /** Represents state of a task. */
+ public static final class TaskState {
/**
* Task states.
@@ -476,23 +529,23 @@ public final class DownloadManager {
*
*
* -> canceled
- * queued <-> started -> ended
- * -> error
+ * queued <-> started -> completed
+ * -> failed
*
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef({STATE_QUEUED, STATE_STARTED, STATE_ENDED, STATE_CANCELED, STATE_ERROR})
+ @IntDef({STATE_QUEUED, STATE_STARTED, STATE_COMPLETED, STATE_CANCELED, STATE_FAILED})
public @interface State {}
/** The task is waiting to be started. */
public static final int STATE_QUEUED = 0;
/** The task is currently started. */
public static final int STATE_STARTED = 1;
/** The task completed. */
- public static final int STATE_ENDED = 2;
+ public static final int STATE_COMPLETED = 2;
/** The task was canceled. */
public static final int STATE_CANCELED = 3;
/** The task failed. */
- public static final int STATE_ERROR = 4;
+ public static final int STATE_FAILED = 4;
/** Returns the state string for the given state value. */
public static String getStateString(@State int state) {
@@ -501,45 +554,44 @@ public final class DownloadManager {
return "QUEUED";
case STATE_STARTED:
return "STARTED";
- case STATE_ENDED:
- return "ENDED";
+ case STATE_COMPLETED:
+ return "COMPLETED";
case STATE_CANCELED:
return "CANCELED";
- case STATE_ERROR:
- return "ERROR";
+ case STATE_FAILED:
+ return "FAILED";
default:
throw new IllegalStateException();
}
}
- /** Unique id of the task. */
+ /** The unique task id. */
public final int taskId;
- /** The {@link DownloadAction} which is being executed. */
- public final DownloadAction downloadAction;
- /** The state of the task. See {@link State}. */
+ /** The action being executed. */
+ public final DownloadAction action;
+ /** The state of the task. */
public final @State int state;
+
/**
- * The download percentage, or {@link Float#NaN} if it can't be calculated or the task is for
- * removing.
+ * 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;
- /**
- * The downloaded bytes, or {@link C#LENGTH_UNSET} if it hasn't been calculated yet or the task
- * is for removing.
- */
+ /** The total number of downloaded bytes. */
public final long downloadedBytes;
- /** If {@link #state} is {@link #STATE_ERROR} then this is the cause, otherwise null. */
+
+ /** If {@link #state} is {@link #STATE_FAILED} then this is the cause, otherwise null. */
public final Throwable error;
- private DownloadState(
+ private TaskState(
int taskId,
- DownloadAction downloadAction,
+ DownloadAction action,
@State int state,
float downloadPercentage,
long downloadedBytes,
Throwable error) {
this.taskId = taskId;
- this.downloadAction = downloadAction;
+ this.action = action;
this.state = state;
this.downloadPercentage = downloadPercentage;
this.downloadedBytes = downloadedBytes;
@@ -548,7 +600,7 @@ public final class DownloadManager {
}
- private static final class DownloadTask implements Runnable {
+ private static final class Task implements Runnable {
/**
* Task states.
@@ -556,24 +608,24 @@ public final class DownloadManager {
* Transition map (vertical states are source states):
*
*
- * +------+-------+-----+-----------+-----------+--------+--------+-----+
- * |queued|started|ended|q_canceling|s_canceling|canceled|stopping|error|
- * +-----------+------+-------+-----+-----------+-----------+--------+--------+-----+
- * |queued | | X | | X | | | | |
- * |started | | | X | | X | | X | X |
- * |q_canceling| | | | | | X | | |
- * |s_canceling| | | | | | X | | |
- * |stopping | X | | | | | | | |
- * +-----------+------+-------+-----+-----------+-----------+--------+--------+-----+
+ * +------+-------+---------+-----------+-----------+--------+--------+------+
+ * |queued|started|completed|q_canceling|s_canceling|canceled|stopping|failed|
+ * +-----------+------+-------+---------+-----------+-----------+--------+--------+------+
+ * |queued | | X | | X | | | | |
+ * |started | | | X | | X | | X | X |
+ * |q_canceling| | | | | | X | | |
+ * |s_canceling| | | | | | X | | |
+ * |stopping | X | | | | | | | |
+ * +-----------+------+-------+---------+-----------+-----------+--------+--------+------+
*
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({
STATE_QUEUED,
STATE_STARTED,
- STATE_ENDED,
+ STATE_COMPLETED,
STATE_CANCELED,
- STATE_ERROR,
+ STATE_FAILED,
STATE_QUEUED_CANCELING,
STATE_STARTED_CANCELING,
STATE_STARTED_STOPPING
@@ -588,32 +640,32 @@ public final class DownloadManager {
private final int id;
private final DownloadManager downloadManager;
- private final DownloadAction downloadAction;
+ private final DownloadAction action;
private final int minRetryCount;
private volatile @InternalState int currentState;
private volatile Downloader downloader;
private Thread thread;
private Throwable error;
- private DownloadTask(
- int id, DownloadManager downloadManager, DownloadAction downloadAction, int minRetryCount) {
+ private Task(
+ int id, DownloadManager downloadManager, DownloadAction action, int minRetryCount) {
this.id = id;
this.downloadManager = downloadManager;
- this.downloadAction = downloadAction;
+ this.action = action;
this.currentState = STATE_QUEUED;
this.minRetryCount = minRetryCount;
}
- public DownloadState getDownloadState() {
+ public TaskState getDownloadState() {
int externalState = getExternalState();
- return new DownloadState(
- id, downloadAction, externalState, getDownloadPercentage(), getDownloadedBytes(), error);
+ return new TaskState(
+ id, action, externalState, getDownloadPercentage(), getDownloadedBytes(), error);
}
/** Returns whether the task is finished. */
public boolean isFinished() {
- return currentState == STATE_ERROR
- || currentState == STATE_ENDED
+ return currentState == STATE_FAILED
+ || currentState == STATE_COMPLETED
|| currentState == STATE_CANCELED;
}
@@ -626,19 +678,16 @@ public final class DownloadManager {
}
/**
- * Returns the download percentage, or {@link Float#NaN} if it can't be calculated yet. This
- * value can be an estimation.
+ * Returns the estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is
+ * available.
*/
public float getDownloadPercentage() {
- return downloader != null ? downloader.getDownloadPercentage() : Float.NaN;
+ return downloader != null ? downloader.getDownloadPercentage() : C.PERCENTAGE_UNSET;
}
- /**
- * Returns the total number of downloaded bytes, or {@link C#LENGTH_UNSET} if it hasn't been
- * calculated yet.
- */
+ /** Returns the total number of downloaded bytes. */
public long getDownloadedBytes() {
- return downloader != null ? downloader.getDownloadedBytes() : C.LENGTH_UNSET;
+ return downloader != null ? downloader.getDownloadedBytes() : 0;
}
@Override
@@ -646,11 +695,9 @@ public final class DownloadManager {
if (!DEBUG) {
return super.toString();
}
- return downloadAction.getType()
+ return action.type
+ ' '
- + (downloadAction.isRemoveAction() ? "remove" : "download")
- + ' '
- + downloadAction.getData()
+ + (action.isRemoveAction ? "remove" : "download")
+ ' '
+ getStateString();
}
@@ -663,7 +710,7 @@ public final class DownloadManager {
case STATE_STARTED_STOPPING:
return "STOPPING";
default:
- return DownloadState.getStateString(currentState);
+ return TaskState.getStateString(currentState);
}
}
@@ -700,13 +747,13 @@ public final class DownloadManager {
}
});
} else if (changeStateAndNotify(STATE_STARTED, STATE_STARTED_CANCELING)) {
- thread.interrupt();
+ cancelDownload();
}
}
private void stop() {
if (changeStateAndNotify(STATE_STARTED, STATE_STARTED_STOPPING)) {
- downloadManager.logd("Stopping", this);
+ logd("Stopping", this);
thread.interrupt();
}
}
@@ -724,40 +771,46 @@ public final class DownloadManager {
this.error = error;
boolean isInternalState = currentState != getExternalState();
if (!isInternalState) {
- downloadManager.onTaskStateChange(DownloadTask.this);
+ downloadManager.onTaskStateChange(this);
}
return true;
}
- /* Methods running on download thread. */
+ private void cancelDownload() {
+ if (downloader != null) {
+ downloader.cancel();
+ }
+ thread.interrupt();
+ }
+
+ // Methods running on download thread.
@Override
public void run() {
- downloadManager.logd("Task is started", DownloadTask.this);
+ logd("Task is started", this);
Throwable error = null;
try {
- downloader = downloadAction.createDownloader(downloadManager.downloaderConstructorHelper);
- if (downloadAction.isRemoveAction()) {
+ downloader = action.createDownloader(downloadManager.downloaderConstructorHelper);
+ if (action.isRemoveAction) {
downloader.remove();
} else {
int errorCount = 0;
long errorPosition = C.LENGTH_UNSET;
- while (true) {
+ while (!Thread.interrupted()) {
try {
- downloader.download(null);
+ downloader.download();
break;
} catch (IOException e) {
long downloadedBytes = downloader.getDownloadedBytes();
if (downloadedBytes != errorPosition) {
- downloadManager.logd(
- "Reset error count. downloadedBytes = " + downloadedBytes, this);
+ logd("Reset error count. downloadedBytes = " + downloadedBytes, this);
errorPosition = downloadedBytes;
errorCount = 0;
}
if (currentState != STATE_STARTED || ++errorCount > minRetryCount) {
throw e;
}
- downloadManager.logd("Download error. Retry " + errorCount, this);
+ logd("Download error. Retry " + errorCount, this);
Thread.sleep(getRetryDelayMillis(errorCount));
}
}
@@ -771,7 +824,9 @@ public final class DownloadManager {
@Override
public void run() {
if (changeStateAndNotify(
- STATE_STARTED, finalError != null ? STATE_ERROR : STATE_ENDED, finalError)
+ STATE_STARTED,
+ finalError != null ? STATE_FAILED : STATE_COMPLETED,
+ finalError)
|| changeStateAndNotify(STATE_STARTED_CANCELING, STATE_CANCELED)
|| changeStateAndNotify(STATE_STARTED_STOPPING, STATE_QUEUED)) {
return;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java
index a5656ec109..908aae481a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java
@@ -16,150 +16,244 @@
package com.google.android.exoplayer2.offline;
import android.app.Notification;
-import android.app.Notification.Builder;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
-import android.support.annotation.CallSuper;
import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
import android.util.Log;
-import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.offline.DownloadManager.DownloadState;
+import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
import com.google.android.exoplayer2.scheduler.Requirements;
import com.google.android.exoplayer2.scheduler.RequirementsWatcher;
import com.google.android.exoplayer2.scheduler.Scheduler;
+import com.google.android.exoplayer2.util.NotificationUtil;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
+import java.util.HashMap;
-/**
- * A {@link Service} that downloads streams in the background.
- *
- * To start the service, create an instance of one of the subclasses of {@link DownloadAction}
- * and call {@link #addDownloadAction(Context, Class, DownloadAction)} with it.
- */
-public abstract class DownloadService extends Service implements DownloadManager.DownloadListener {
+/** A {@link Service} for downloading media. */
+public abstract class DownloadService extends Service {
- /** Use this action to initialize {@link DownloadManager}. */
+ /** Starts a download service without adding a new {@link DownloadAction}. */
public static final String ACTION_INIT =
"com.google.android.exoplayer.downloadService.action.INIT";
- /** Use this action to add a {@link DownloadAction} to {@link DownloadManager} action queue. */
+ /** Starts a download service, adding a new {@link DownloadAction} to be executed. */
public static final String ACTION_ADD = "com.google.android.exoplayer.downloadService.action.ADD";
- /** Use this action to make {@link DownloadManager} stop download tasks. */
- private static final String ACTION_STOP =
- "com.google.android.exoplayer.downloadService.action.STOP";
+ /** Like {@link #ACTION_INIT}, but with {@link #KEY_FOREGROUND} implicitly set to true. */
+ private static final String ACTION_RESTART =
+ "com.google.android.exoplayer.downloadService.action.RESTART";
- /** Use this action to make {@link DownloadManager} start download tasks. */
- private static final String ACTION_START =
- "com.google.android.exoplayer.downloadService.action.START";
+ /** Starts download tasks. */
+ private static final String ACTION_START_DOWNLOADS =
+ "com.google.android.exoplayer.downloadService.action.START_DOWNLOADS";
- /** A {@link DownloadAction} to be executed. */
- public static final String DOWNLOAD_ACTION = "DownloadAction";
+ /** Stops download tasks. */
+ private static final String ACTION_STOP_DOWNLOADS =
+ "com.google.android.exoplayer.downloadService.action.STOP_DOWNLOADS";
- /** Default progress update interval in milliseconds. */
- public static final long DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS = 1000;
+ /** Key for the {@link DownloadAction} in an {@link #ACTION_ADD} intent. */
+ public static final String KEY_DOWNLOAD_ACTION = "download_action";
+
+ /**
+ * Key for a boolean flag in any intent to indicate whether the service was started in the
+ * foreground. If set, the service is guaranteed to call {@link #startForeground(int,
+ * Notification)}.
+ */
+ public static final String KEY_FOREGROUND = "foreground";
+
+ /** Default foreground notification update interval in milliseconds. */
+ public static final long DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL = 1000;
private static final String TAG = "DownloadService";
private static final boolean DEBUG = false;
- // Keep requirementsWatcher and scheduler alive beyond DownloadService life span (until the app is
- // killed) because it may take long time for Scheduler to start the service.
- private static RequirementsWatcher requirementsWatcher;
- private static Scheduler scheduler;
+ // Keep the requirements helper for each DownloadService as long as there are tasks (and the
+ // process is running). This allows tasks to resume when there's no scheduler. It may also allow
+ // tasks the resume more quickly than when relying on the scheduler alone.
+ private static final HashMap, RequirementsHelper>
+ requirementsHelpers = new HashMap<>();
- private final int notificationIdOffset;
- private final long progressUpdateIntervalMillis;
+ private final ForegroundNotificationUpdater foregroundNotificationUpdater;
+ private final @Nullable String channelId;
+ private final @StringRes int channelName;
private DownloadManager downloadManager;
- private ProgressUpdater progressUpdater;
+ private DownloadManagerListener downloadManagerListener;
private int lastStartId;
-
- /** @param notificationIdOffset Value to offset notification ids. Must be greater than 0. */
- protected DownloadService(int notificationIdOffset) {
- this(notificationIdOffset, DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS);
- }
+ private boolean startedInForeground;
/**
- * @param notificationIdOffset Value to offset notification ids. Must be greater than 0.
- * @param progressUpdateIntervalMillis {@link #onProgressUpdate(DownloadState[])} is called using
- * this interval. If it's {@link C#TIME_UNSET}, then {@link
- * #onProgressUpdate(DownloadState[])} isn't called.
- */
- protected DownloadService(int notificationIdOffset, long progressUpdateIntervalMillis) {
- this.notificationIdOffset = notificationIdOffset;
- this.progressUpdateIntervalMillis = progressUpdateIntervalMillis;
- }
-
- /**
- * Creates an {@link Intent} to be used to start this service and adds the {@link DownloadAction}
- * to the {@link DownloadManager}.
+ * Creates a DownloadService with {@link #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}.
*
- * @param context A {@link Context} of the application calling this service.
- * @param clazz Class object of DownloadService or subclass.
- * @param downloadAction A {@link DownloadAction} to be executed.
+ * @param foregroundNotificationId The notification id for the foreground notification, must not
+ * be 0.
+ */
+ protected DownloadService(int foregroundNotificationId) {
+ this(foregroundNotificationId, DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL);
+ }
+
+ /**
+ * @param foregroundNotificationId The notification id for the foreground notification, must not
+ * be 0.
+ * @param foregroundNotificationUpdateInterval The maximum interval to update foreground
+ * notification, in milliseconds.
+ */
+ protected DownloadService(
+ int foregroundNotificationId, long foregroundNotificationUpdateInterval) {
+ this(
+ foregroundNotificationId,
+ foregroundNotificationUpdateInterval,
+ /* channelId= */ null,
+ /* channelName= */ 0);
+ }
+
+ /**
+ * @param foregroundNotificationId The notification id for the foreground notification. Must not
+ * be 0.
+ * @param foregroundNotificationUpdateInterval The maximum interval between updates to the
+ * foreground notification, in milliseconds.
+ * @param channelId An id for a low priority notification channel to create, or {@code null} if
+ * the app will take care of creating a notification channel if needed. If specified, must be
+ * unique per package and the value may be truncated if it is too long.
+ * @param channelName A string resource identifier for the user visible name of the channel, if
+ * {@code channelId} is specified. The recommended maximum length is 40 characters; the value
+ * may be truncated if it is too long.
+ */
+ protected DownloadService(
+ int foregroundNotificationId,
+ long foregroundNotificationUpdateInterval,
+ @Nullable String channelId,
+ @StringRes int channelName) {
+ foregroundNotificationUpdater =
+ new ForegroundNotificationUpdater(
+ foregroundNotificationId, foregroundNotificationUpdateInterval);
+ this.channelId = channelId;
+ this.channelName = channelName;
+ }
+
+ /**
+ * Builds an {@link Intent} for adding an action to be executed by the service.
+ *
+ * @param context A {@link Context}.
+ * @param clazz The concrete download service being targeted by the intent.
+ * @param downloadAction The action to be executed.
+ * @param foreground Whether this intent will be used to start the service in the foreground.
* @return Created Intent.
*/
- public static Intent createAddDownloadActionIntent(
- Context context, Class extends DownloadService> clazz, DownloadAction downloadAction) {
+ public static Intent buildAddActionIntent(
+ Context context,
+ Class extends DownloadService> clazz,
+ DownloadAction downloadAction,
+ boolean foreground) {
return new Intent(context, clazz)
.setAction(ACTION_ADD)
- .putExtra(DOWNLOAD_ACTION, downloadAction.toByteArray());
+ .putExtra(KEY_DOWNLOAD_ACTION, downloadAction.toByteArray())
+ .putExtra(KEY_FOREGROUND, foreground);
}
/**
- * Adds a {@link DownloadAction} to the {@link DownloadManager}. This will start the download
- * service if it was not running.
+ * Starts the service, adding an action to be executed.
*
- * @param context A {@link Context} of the application calling this service.
- * @param clazz Class object of DownloadService or subclass.
- * @param downloadAction A {@link DownloadAction} to be executed.
- * @see #createAddDownloadActionIntent(Context, Class, DownloadAction)
+ * @param context A {@link Context}.
+ * @param clazz The concrete download service being targeted by the intent.
+ * @param downloadAction The action to be executed.
+ * @param foreground Whether this intent will be used to start the service in the foreground.
*/
- public static void addDownloadAction(
- Context context, Class extends DownloadService> clazz, DownloadAction downloadAction) {
- context.startService(createAddDownloadActionIntent(context, clazz, downloadAction));
+ public static void startWithAction(
+ Context context,
+ Class extends DownloadService> clazz,
+ DownloadAction downloadAction,
+ boolean foreground) {
+ Intent intent = buildAddActionIntent(context, clazz, downloadAction, foreground);
+ if (foreground) {
+ Util.startForegroundService(context, intent);
+ } else {
+ context.startService(intent);
+ }
}
@Override
public void onCreate() {
logd("onCreate");
+ if (channelId != null) {
+ NotificationUtil.createNotificationChannel(
+ this, channelId, channelName, NotificationUtil.IMPORTANCE_LOW);
+ }
downloadManager = getDownloadManager();
- downloadManager.addListener(this);
+ downloadManagerListener = new DownloadManagerListener();
+ downloadManager.addListener(downloadManagerListener);
- if (requirementsWatcher == null) {
- Requirements requirements = getRequirements();
- if (requirements != null) {
- scheduler = getScheduler();
- RequirementsListener listener =
- new RequirementsListener(getApplicationContext(), getClass(), scheduler);
- requirementsWatcher =
- new RequirementsWatcher(getApplicationContext(), listener, requirements);
- requirementsWatcher.start();
+ RequirementsHelper requirementsHelper;
+ synchronized (requirementsHelpers) {
+ Class extends DownloadService> clazz = getClass();
+ requirementsHelper = requirementsHelpers.get(clazz);
+ if (requirementsHelper == null) {
+ requirementsHelper = new RequirementsHelper(this, getRequirements(), getScheduler(), clazz);
+ requirementsHelpers.put(clazz, requirementsHelper);
}
}
+ requirementsHelper.start();
+ }
- progressUpdater = new ProgressUpdater(this, progressUpdateIntervalMillis);
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ lastStartId = startId;
+ String intentAction = null;
+ if (intent != null) {
+ intentAction = intent.getAction();
+ startedInForeground |=
+ intent.getBooleanExtra(KEY_FOREGROUND, false) || ACTION_RESTART.equals(intentAction);
+ }
+ logd("onStartCommand action: " + intentAction + " startId: " + startId);
+ switch (intentAction) {
+ case ACTION_INIT:
+ case ACTION_RESTART:
+ // Do nothing. The RequirementsWatcher will start downloads when possible.
+ break;
+ case ACTION_ADD:
+ byte[] actionData = intent.getByteArrayExtra(KEY_DOWNLOAD_ACTION);
+ if (actionData == null) {
+ Log.e(TAG, "Ignoring ADD action with no action data");
+ } else {
+ try {
+ downloadManager.handleAction(actionData);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to handle ADD action", e);
+ }
+ }
+ break;
+ case ACTION_STOP_DOWNLOADS:
+ downloadManager.stopDownloads();
+ break;
+ case ACTION_START_DOWNLOADS:
+ downloadManager.startDownloads();
+ break;
+ default:
+ Log.e(TAG, "Ignoring unrecognized action: " + intentAction);
+ break;
+ }
+ if (downloadManager.isIdle()) {
+ stop();
+ }
+ return START_STICKY;
}
@Override
public void onDestroy() {
logd("onDestroy");
- progressUpdater.stop();
- downloadManager.removeListener(this);
+ foregroundNotificationUpdater.stopPeriodicUpdates();
+ downloadManager.removeListener(downloadManagerListener);
if (downloadManager.getTaskCount() == 0) {
- if (requirementsWatcher != null) {
- requirementsWatcher.stop();
- requirementsWatcher = null;
- }
- if (scheduler != null) {
- scheduler.cancel();
- scheduler = null;
+ synchronized (requirementsHelpers) {
+ RequirementsHelper requirementsHelper = requirementsHelpers.remove(getClass());
+ if (requirementsHelper != null) {
+ requirementsHelper.stop();
+ }
}
}
}
@@ -170,206 +264,173 @@ public abstract class DownloadService extends Service implements DownloadManager
return null;
}
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- this.lastStartId = startId;
- String intentAction = intent != null ? intent.getAction() : null;
- if (intentAction == null) {
- intentAction = ACTION_INIT;
- }
- logd("onStartCommand action: " + intentAction + " startId: " + startId);
- switch (intentAction) {
- case ACTION_INIT:
- // Do nothing. DownloadManager and RequirementsWatcher is initialized. If there are download
- // or remove tasks loaded from file, they will start if the requirements are met.
- break;
- case ACTION_ADD:
- byte[] actionData = intent.getByteArrayExtra(DOWNLOAD_ACTION);
- if (actionData == null) {
- onCommandError(intent, new IllegalArgumentException("DownloadAction is missing."));
- } else {
- try {
- onNewTask(intent, downloadManager.handleAction(actionData));
- } catch (IOException e) {
- onCommandError(intent, e);
- }
- }
- break;
- case ACTION_STOP:
- downloadManager.stopDownloads();
- break;
- case ACTION_START:
- downloadManager.startDownloads();
- break;
- default:
- onCommandError(intent, new IllegalArgumentException("Unknown action: " + intentAction));
- break;
- }
- if (downloadManager.isIdle()) {
- onIdle(null);
- }
- return START_STICKY;
- }
-
/**
* Returns a {@link DownloadManager} to be used to downloaded content. Called only once in the
- * life cycle of the service.
+ * life cycle of the service. The service will call {@link DownloadManager#startDownloads()} and
+ * {@link DownloadManager#stopDownloads} as necessary when requirements returned by {@link
+ * #getRequirements()} are met or stop being met.
*/
protected abstract DownloadManager getDownloadManager();
/**
- * Returns a {@link Scheduler} which contains a job to initialize {@link DownloadService} when the
- * requirements are met, or null. If not null, scheduler is used to start downloads even when the
- * app isn't running.
+ * Returns a {@link Scheduler} to restart the service when requirements allowing downloads to take
+ * place are met. If {@code null}, the service will only be restarted if the process is still in
+ * memory when the requirements are met.
*/
protected abstract @Nullable Scheduler getScheduler();
- /** Returns requirements for downloads to take place, or null. */
- protected abstract @Nullable Requirements getRequirements();
+ /**
+ * Returns requirements for downloads to take place. By default the only requirement is that the
+ * device has network connectivity.
+ */
+ protected Requirements getRequirements() {
+ return new Requirements(Requirements.NETWORK_TYPE_ANY, false, false);
+ }
- /** Called on error in start command. */
- protected void onCommandError(Intent intent, Exception error) {
+ /**
+ * Returns a notification to be displayed when this service running in the foreground.
+ *
+ * This method is called when there is a task state change and periodically while there are
+ * active tasks. The periodic update interval can be set using {@link #DownloadService(int,
+ * long)}.
+ *
+ *
On API level 26 and above, this method may also be called just before the service stops,
+ * with an empty {@code taskStates} array. The returned notification is used to satisfy system
+ * requirements for foreground services.
+ *
+ * @param taskStates The states of all current tasks.
+ * @return The foreground notification to display.
+ */
+ protected abstract Notification getForegroundNotification(TaskState[] taskStates);
+
+ /**
+ * Called when the state of a task changes.
+ *
+ * @param taskState The state of the task.
+ */
+ protected void onTaskStateChanged(TaskState taskState) {
// Do nothing.
}
- /** Called when a new task is added to the {@link DownloadManager}. */
- protected void onNewTask(Intent intent, int taskId) {
- // Do nothing.
- }
-
- /** Returns a notification channelId. See {@link NotificationChannel}. */
- protected abstract String getNotificationChannelId();
-
- /**
- * Helper method which calls {@link #startForeground(int, Notification)} with {@code
- * notificationIdOffset} and {@code foregroundNotification}.
- */
- public void startForeground(Notification foregroundNotification) {
- // logd("start foreground");
- startForeground(notificationIdOffset, foregroundNotification);
- }
-
- /**
- * Sets/replaces or cancels the notification for the given id.
- *
- * @param id A unique id for the notification. This value is offset by {@code
- * notificationIdOffset}.
- * @param notification If not null, it's showed, replacing any previous notification. Otherwise
- * any previous notification is canceled.
- */
- public void setNotification(int id, @Nullable Notification notification) {
- NotificationManager notificationManager =
- (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- if (notification != null) {
- notificationManager.notify(notificationIdOffset + 1 + id, notification);
- } else {
- notificationManager.cancel(notificationIdOffset + 1 + id);
- }
- }
-
- /**
- * Override this method to get notified.
- *
- *
{@inheritDoc}
- */
- @CallSuper
- @Override
- public void onStateChange(DownloadManager downloadManager, DownloadState downloadState) {
- if (downloadState.state == DownloadState.STATE_STARTED) {
- progressUpdater.start();
- }
- }
-
- /**
- * Override this method to get notified.
- *
- *
{@inheritDoc}
- */
- @CallSuper
- @Override
- public void onIdle(DownloadManager downloadManager) {
- // Make sure startForeground is called before stopping.
- // Workaround for https://buganizer.corp.google.com/issues/69424260
- if (Util.SDK_INT >= 26) {
- Builder notificationBuilder = new Builder(this, getNotificationChannelId());
- Notification foregroundNotification = notificationBuilder.build();
- startForeground(foregroundNotification);
+ private void stop() {
+ foregroundNotificationUpdater.stopPeriodicUpdates();
+ // Make sure startForeground is called before stopping. Workaround for [Internal: b/69424260].
+ if (startedInForeground && Util.SDK_INT >= 26) {
+ foregroundNotificationUpdater.showNotificationIfNotAlready();
}
boolean stopSelfResult = stopSelfResult(lastStartId);
logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult);
}
- /** Override this method to get notified on every second while there are active downloads. */
- protected void onProgressUpdate(DownloadState[] activeDownloadTasks) {
- // Do nothing.
- }
-
private void logd(String message) {
if (DEBUG) {
Log.d(TAG, message);
}
}
- private static final class ProgressUpdater implements Runnable {
+ private final class DownloadManagerListener implements DownloadManager.Listener {
+ @Override
+ public void onInitialized(DownloadManager downloadManager) {
+ // Do nothing.
+ }
- private final DownloadService downloadService;
- private final long progressUpdateIntervalMillis;
+ @Override
+ public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) {
+ DownloadService.this.onTaskStateChanged(taskState);
+ if (taskState.state == TaskState.STATE_STARTED) {
+ foregroundNotificationUpdater.startPeriodicUpdates();
+ } else {
+ foregroundNotificationUpdater.update();
+ }
+ }
+
+ @Override
+ public final void onIdle(DownloadManager downloadManager) {
+ stop();
+ }
+ }
+
+ private final class ForegroundNotificationUpdater implements Runnable {
+
+ private final int notificationId;
+ private final long updateInterval;
private final Handler handler;
- private boolean stopped;
- public ProgressUpdater(DownloadService downloadService, long progressUpdateIntervalMillis) {
- this.downloadService = downloadService;
- this.progressUpdateIntervalMillis = progressUpdateIntervalMillis;
+ private boolean periodicUpdatesStarted;
+ private boolean notificationDisplayed;
+
+ public ForegroundNotificationUpdater(int notificationId, long updateInterval) {
+ this.notificationId = notificationId;
+ this.updateInterval = updateInterval;
this.handler = new Handler(Looper.getMainLooper());
- stopped = true;
+ }
+
+ public void startPeriodicUpdates() {
+ periodicUpdatesStarted = true;
+ update();
+ }
+
+ public void stopPeriodicUpdates() {
+ periodicUpdatesStarted = false;
+ handler.removeCallbacks(this);
+ }
+
+ public void update() {
+ TaskState[] taskStates = downloadManager.getAllTaskStates();
+ startForeground(notificationId, getForegroundNotification(taskStates));
+ notificationDisplayed = true;
+ if (periodicUpdatesStarted) {
+ handler.removeCallbacks(this);
+ handler.postDelayed(this, updateInterval);
+ }
+ }
+
+ public void showNotificationIfNotAlready() {
+ if (!notificationDisplayed) {
+ update();
+ }
}
@Override
public void run() {
- DownloadState[] activeDownloadTasks =
- downloadService.downloadManager.getActiveDownloadStates();
- if (activeDownloadTasks.length > 0) {
- downloadService.onProgressUpdate(activeDownloadTasks);
- if (progressUpdateIntervalMillis != C.TIME_UNSET) {
- handler.postDelayed(this, progressUpdateIntervalMillis);
- }
- } else {
- stop();
- }
+ update();
}
+ }
- public void stop() {
- stopped = true;
- handler.removeCallbacks(this);
+ private static final class RequirementsHelper implements RequirementsWatcher.Listener {
+
+ private final Context context;
+ private final Requirements requirements;
+ private final @Nullable Scheduler scheduler;
+ private final Class extends DownloadService> serviceClass;
+ private final RequirementsWatcher requirementsWatcher;
+
+ private RequirementsHelper(
+ Context context,
+ Requirements requirements,
+ @Nullable Scheduler scheduler,
+ Class extends DownloadService> serviceClass) {
+ this.context = context;
+ this.requirements = requirements;
+ this.scheduler = scheduler;
+ this.serviceClass = serviceClass;
+ requirementsWatcher = new RequirementsWatcher(context, this, requirements);
}
public void start() {
- if (stopped) {
- stopped = false;
- if (progressUpdateIntervalMillis != C.TIME_UNSET) {
- handler.post(this);
- }
- }
+ requirementsWatcher.start();
}
- }
-
- private static final class RequirementsListener implements RequirementsWatcher.Listener {
-
- private final Context context;
- private final Class extends DownloadService> serviceClass;
- private final Scheduler scheduler;
-
- private RequirementsListener(
- Context context, Class extends DownloadService> serviceClass, Scheduler scheduler) {
- this.context = context;
- this.serviceClass = serviceClass;
- this.scheduler = scheduler;
+ public void stop() {
+ requirementsWatcher.stop();
+ if (scheduler != null) {
+ scheduler.cancel();
+ }
}
@Override
public void requirementsMet(RequirementsWatcher requirementsWatcher) {
- startServiceWithAction(DownloadService.ACTION_START);
+ startServiceWithAction(DownloadService.ACTION_START_DOWNLOADS);
if (scheduler != null) {
scheduler.cancel();
}
@@ -377,21 +438,20 @@ public abstract class DownloadService extends Service implements DownloadManager
@Override
public void requirementsNotMet(RequirementsWatcher requirementsWatcher) {
- startServiceWithAction(DownloadService.ACTION_STOP);
+ startServiceWithAction(DownloadService.ACTION_STOP_DOWNLOADS);
if (scheduler != null) {
- if (!scheduler.schedule()) {
+ String servicePackage = context.getPackageName();
+ boolean success = scheduler.schedule(requirements, servicePackage, ACTION_RESTART);
+ if (!success) {
Log.e(TAG, "Scheduling downloads failed.");
}
}
}
private void startServiceWithAction(String action) {
- Intent intent = new Intent(context, serviceClass).setAction(action);
- if (Util.SDK_INT >= 26) {
- context.startForegroundService(intent);
- } else {
- context.startService(intent);
- }
+ Intent intent =
+ new Intent(context, serviceClass).setAction(action).putExtra(KEY_FOREGROUND, true);
+ Util.startForegroundService(context, intent);
}
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java
index b8d9432c63..10523d6bc6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java
@@ -15,7 +15,6 @@
*/
package com.google.android.exoplayer2.offline;
-import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import java.io.IOException;
@@ -24,69 +23,31 @@ import java.io.IOException;
*/
public interface Downloader {
- /**
- * Listener notified when download progresses.
- *
- * No guarantees are made about the thread or threads on which the listener is called, but it is
- * guaranteed that listener methods will be called in a serial fashion (i.e. one at a time) and in
- * the same order as events occurred.
- */
- interface ProgressListener {
- /**
- * Called during the download. Calling intervals depend on the {@link Downloader}
- * implementation.
- *
- * @param downloader The reporting instance.
- * @param downloadPercentage The download percentage. This value can be an estimation.
- * @param downloadedBytes Total number of downloaded bytes.
- * @see #download(ProgressListener)
- */
- void onDownloadProgress(Downloader downloader, float downloadPercentage, long downloadedBytes);
- }
-
- /**
- * Initializes the downloader.
- *
- * @throws DownloadException Thrown if the media cannot be downloaded.
- * @throws InterruptedException If the thread has been interrupted.
- * @throws IOException Thrown when there is an io error while reading from cache.
- * @see #getDownloadedBytes()
- * @see #getDownloadPercentage()
- */
- void init() throws InterruptedException, IOException;
-
/**
* Downloads the media.
*
- * @param listener If not null, called during download.
* @throws DownloadException Thrown if the media cannot be downloaded.
* @throws InterruptedException If the thread has been interrupted.
* @throws IOException Thrown when there is an io error while downloading.
*/
- void download(@Nullable ProgressListener listener)
- throws InterruptedException, IOException;
+ void download() throws InterruptedException, IOException;
+
+ /** Interrupts any current download operation and prevents future operations from running. */
+ void cancel();
+
+ /** Returns the total number of downloaded bytes. */
+ long getDownloadedBytes();
/**
- * Removes all of the downloaded data of the media.
+ * Returns the estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is
+ * available.
+ */
+ float getDownloadPercentage();
+
+ /**
+ * Removes the media.
*
* @throws InterruptedException Thrown if the thread was interrupted.
*/
void remove() throws InterruptedException;
-
- /**
- * Returns the total number of downloaded bytes, or {@link C#LENGTH_UNSET} if it hasn't been
- * calculated yet.
- *
- * @see #init()
- */
- long getDownloadedBytes();
-
- /**
- * Returns the download percentage, or {@link Float#NaN} if it can't be calculated yet. This
- * value can be an estimation.
- *
- * @see #init()
- */
- float getDownloadPercentage();
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java
index 9ef9366397..18387b9d92 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java
@@ -49,14 +49,17 @@ public final class DownloaderConstructorHelper {
/**
* @param cache Cache instance to be used to store downloaded data.
* @param upstreamDataSourceFactory A {@link Factory} for downloading data.
- * @param cacheReadDataSourceFactory A {@link Factory} for reading data from the cache.
- * If null, null is passed to {@link Downloader} constructor.
+ * @param cacheReadDataSourceFactory A {@link Factory} for reading data from the cache. If null
+ * then standard {@link FileDataSource} instances will be used.
* @param cacheWriteDataSinkFactory A {@link DataSink.Factory} for writing data to the cache. If
- * null, null is passed to {@link Downloader} constructor.
- * @param priorityTaskManager If one is given then the download priority is set lower than
- * loading. If null, null is passed to {@link Downloader} constructor.
+ * null then standard {@link CacheDataSink} instances will be used.
+ * @param priorityTaskManager A {@link PriorityTaskManager} to use when downloading. If non-null,
+ * downloaders will register as tasks with priority {@link C#PRIORITY_DOWNLOAD} whilst
+ * downloading.
*/
- public DownloaderConstructorHelper(Cache cache, Factory upstreamDataSourceFactory,
+ public DownloaderConstructorHelper(
+ Cache cache,
+ Factory upstreamDataSourceFactory,
@Nullable Factory cacheReadDataSourceFactory,
@Nullable DataSink.Factory cacheWriteDataSinkFactory,
@Nullable PriorityTaskManager priorityTaskManager) {
@@ -73,7 +76,7 @@ public final class DownloaderConstructorHelper {
return cache;
}
- /** Returns a {@link PriorityTaskManager} instance.*/
+ /** Returns a {@link PriorityTaskManager} instance. */
public PriorityTaskManager getPriorityTaskManager() {
// Return a dummy PriorityTaskManager if none is provided. Create a new PriorityTaskManager
// each time so clients don't affect each other over the dummy PriorityTaskManager instance.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/FilterableManifest.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/FilterableManifest.java
new file mode 100644
index 0000000000..35d05fd43b
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/FilterableManifest.java
@@ -0,0 +1,37 @@
+/*
+ * 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 java.util.List;
+
+/**
+ * A manifest that can generate copies of itself including only the streams specified by the given
+ * keys.
+ *
+ * @param The manifest type.
+ * @param The stream key type.
+ */
+public interface FilterableManifest {
+
+ /**
+ * Returns a copy of the manifest including only the streams specified by the given keys. If the
+ * manifest is unchanged then the instance may return itself.
+ *
+ * @param streamKeys A non-empty list of stream keys.
+ * @return The filtered manifest.
+ */
+ T copy(List streamKeys);
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/FilteringManifestParser.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/FilteringManifestParser.java
new file mode 100644
index 0000000000..8fec07552b
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/FilteringManifestParser.java
@@ -0,0 +1,45 @@
+/*
+ * 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 android.net.Uri;
+import com.google.android.exoplayer2.upstream.ParsingLoadable.Parser;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+/** A manifest parser that includes only the tracks identified by the given track keys. */
+public final class FilteringManifestParser, K>
+ implements Parser {
+
+ private final Parser parser;
+ private final List trackKeys;
+
+ /**
+ * @param parser A parser for the manifest that will be filtered.
+ * @param trackKeys The track keys. If null or empty then filtering will not occur.
+ */
+ public FilteringManifestParser(Parser parser, List trackKeys) {
+ this.parser = parser;
+ this.trackKeys = trackKeys;
+ }
+
+ @Override
+ public T parse(Uri uri, InputStream inputStream) throws IOException {
+ T manifest = parser.parse(uri, inputStream);
+ return trackKeys == null || trackKeys.isEmpty() ? manifest : manifest.copy(trackKeys);
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java
index 130e9c0dc7..02ef7a7aa7 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java
@@ -18,7 +18,6 @@ package com.google.android.exoplayer2.offline;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.upstream.cache.CacheUtil;
-import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.DataInputStream;
import java.io.DataOutputStream;
@@ -27,46 +26,37 @@ import java.io.IOException;
/** An action to download or remove downloaded progressive streams. */
public final class ProgressiveDownloadAction extends DownloadAction {
- public static final Deserializer DESERIALIZER = new Deserializer() {
+ private static final String TYPE = "progressive";
+ private static final int VERSION = 0;
- @Override
- public String getType() {
- return TYPE;
- }
+ public static final Deserializer DESERIALIZER =
+ new Deserializer(TYPE, VERSION) {
+ @Override
+ public ProgressiveDownloadAction readFromStream(int version, DataInputStream input)
+ throws IOException {
+ Uri uri = Uri.parse(input.readUTF());
+ boolean isRemoveAction = input.readBoolean();
+ int dataLength = input.readInt();
+ byte[] data = new byte[dataLength];
+ input.readFully(data);
+ String customCacheKey = input.readBoolean() ? input.readUTF() : null;
+ return new ProgressiveDownloadAction(uri, isRemoveAction, data, customCacheKey);
+ }
+ };
- @Override
- public ProgressiveDownloadAction readFromStream(int version, DataInputStream input)
- throws IOException {
- return new ProgressiveDownloadAction(input.readUTF(),
- input.readBoolean() ? input.readUTF() : null, input.readBoolean(), input.readUTF());
- }
-
- };
-
- private static final String TYPE = "ProgressiveDownloadAction";
-
- private final String uri;
- private final @Nullable String customCacheKey;
- private final boolean removeAction;
+ public final @Nullable String customCacheKey;
/**
* @param uri Uri of the data to be downloaded.
+ * @param isRemoveAction Whether this is a remove action. If false, this is a download action.
+ * @param data Optional custom data for this action.
* @param customCacheKey A custom key that uniquely identifies the original stream. If not null it
* is used for cache indexing.
- * @param removeAction Whether the data should be downloaded or removed.
- * @param data Optional custom data for this action. If null, an empty string is used.
*/
public ProgressiveDownloadAction(
- String uri, @Nullable String customCacheKey, boolean removeAction, @Nullable String data) {
- super(data);
- this.uri = Assertions.checkNotNull(uri);
+ Uri uri, boolean isRemoveAction, @Nullable byte[] data, @Nullable String customCacheKey) {
+ super(TYPE, VERSION, uri, isRemoveAction, data);
this.customCacheKey = customCacheKey;
- this.removeAction = removeAction;
- }
-
- @Override
- public boolean isRemoveAction() {
- return removeAction;
}
@Override
@@ -74,29 +64,23 @@ public final class ProgressiveDownloadAction extends DownloadAction {
return new ProgressiveDownloader(uri, customCacheKey, constructorHelper);
}
- @Override
- protected String getType() {
- return TYPE;
- }
-
@Override
protected void writeToStream(DataOutputStream output) throws IOException {
- output.writeUTF(uri);
- boolean customCacheKeyAvailable = customCacheKey != null;
- output.writeBoolean(customCacheKeyAvailable);
- if (customCacheKeyAvailable) {
+ output.writeUTF(uri.toString());
+ output.writeBoolean(isRemoveAction);
+ output.writeInt(data.length);
+ output.write(data);
+ boolean customCacheKeySet = customCacheKey != null;
+ output.writeBoolean(customCacheKeySet);
+ if (customCacheKeySet) {
output.writeUTF(customCacheKey);
}
- output.writeBoolean(isRemoveAction());
- output.writeUTF(getData());
}
@Override
- protected boolean isSameMedia(DownloadAction other) {
- if (!(other instanceof ProgressiveDownloadAction)) {
- return false;
- }
- return getCacheKey().equals(((ProgressiveDownloadAction) other).getCacheKey());
+ public boolean isSameMedia(DownloadAction other) {
+ return ((other instanceof ProgressiveDownloadAction)
+ && getCacheKey().equals(((ProgressiveDownloadAction) other).getCacheKey()));
}
@Override
@@ -108,18 +92,17 @@ public final class ProgressiveDownloadAction extends DownloadAction {
return false;
}
ProgressiveDownloadAction that = (ProgressiveDownloadAction) o;
- return uri.equals(that.uri) && Util.areEqual(customCacheKey, that.customCacheKey);
+ return Util.areEqual(customCacheKey, that.customCacheKey);
}
@Override
public int hashCode() {
int result = super.hashCode();
- result = 31 * result + uri.hashCode();
result = 31 * result + (customCacheKey != null ? customCacheKey.hashCode() : 0);
return result;
}
private String getCacheKey() {
- return customCacheKey != null ? customCacheKey : CacheUtil.generateKey(Uri.parse(uri));
+ return customCacheKey != null ? customCacheKey : CacheUtil.generateKey(uri);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java
new file mode 100644
index 0000000000..49b7e36ea6
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java
@@ -0,0 +1,62 @@
+/*
+ * 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 android.net.Uri;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import java.util.List;
+
+/** A {@link DownloadHelper} for progressive streams. */
+public final class ProgressiveDownloadHelper extends DownloadHelper {
+
+ private final Uri uri;
+ private final @Nullable String customCacheKey;
+
+ public ProgressiveDownloadHelper(Uri uri) {
+ this(uri, null);
+ }
+
+ public ProgressiveDownloadHelper(Uri uri, @Nullable String customCacheKey) {
+ this.uri = uri;
+ this.customCacheKey = customCacheKey;
+ }
+
+ @Override
+ protected void prepareInternal() {
+ // Do nothing.
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return 1;
+ }
+
+ @Override
+ public TrackGroupArray getTrackGroups(int periodIndex) {
+ return TrackGroupArray.EMPTY;
+ }
+
+ @Override
+ public DownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) {
+ return new ProgressiveDownloadAction(uri, false, data, customCacheKey);
+ }
+
+ @Override
+ public DownloadAction getRemoveAction(@Nullable byte[] data) {
+ return new ProgressiveDownloadAction(uri, true, data, customCacheKey);
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java
index e5aa429424..cf64d26bb5 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java
@@ -16,7 +16,6 @@
package com.google.android.exoplayer2.offline;
import android.net.Uri;
-import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.cache.Cache;
@@ -25,6 +24,7 @@ import com.google.android.exoplayer2.upstream.cache.CacheUtil;
import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters;
import com.google.android.exoplayer2.util.PriorityTaskManager;
import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* A downloader for progressive media streams.
@@ -38,49 +38,46 @@ public final class ProgressiveDownloader implements Downloader {
private final CacheDataSource dataSource;
private final PriorityTaskManager priorityTaskManager;
private final CacheUtil.CachingCounters cachingCounters;
+ private final AtomicBoolean isCanceled;
/**
* @param uri Uri of the data to be downloaded.
* @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
* indexing. May be null.
- * @param constructorHelper a {@link DownloaderConstructorHelper} instance.
+ * @param constructorHelper A {@link DownloaderConstructorHelper} instance.
*/
public ProgressiveDownloader(
- String uri, String customCacheKey, DownloaderConstructorHelper constructorHelper) {
- this.dataSpec = new DataSpec(Uri.parse(uri), 0, C.LENGTH_UNSET, customCacheKey, 0);
+ Uri uri, String customCacheKey, DownloaderConstructorHelper constructorHelper) {
+ this.dataSpec = new DataSpec(uri, 0, C.LENGTH_UNSET, customCacheKey, 0);
this.cache = constructorHelper.getCache();
this.dataSource = constructorHelper.buildCacheDataSource(false);
this.priorityTaskManager = constructorHelper.getPriorityTaskManager();
cachingCounters = new CachingCounters();
+ isCanceled = new AtomicBoolean();
}
@Override
- public void init() {
- CacheUtil.getCached(dataSpec, cache, cachingCounters);
- }
-
- @Override
- public void download(@Nullable ProgressListener listener) throws InterruptedException,
- IOException {
+ public void download() throws InterruptedException, IOException {
priorityTaskManager.add(C.PRIORITY_DOWNLOAD);
try {
- byte[] buffer = new byte[BUFFER_SIZE_BYTES];
- CacheUtil.cache(dataSpec, cache, dataSource, buffer, priorityTaskManager, C.PRIORITY_DOWNLOAD,
- cachingCounters, true);
- // TODO: Work out how to call onDownloadProgress periodically during the download, or else
- // get rid of ProgressListener and move to a model where the manager periodically polls
- // Downloaders.
- if (listener != null) {
- listener.onDownloadProgress(this, 100, cachingCounters.contentLength);
- }
+ CacheUtil.cache(
+ dataSpec,
+ cache,
+ dataSource,
+ new byte[BUFFER_SIZE_BYTES],
+ priorityTaskManager,
+ C.PRIORITY_DOWNLOAD,
+ cachingCounters,
+ isCanceled,
+ /* enableEOFException= */ true);
} finally {
priorityTaskManager.remove(C.PRIORITY_DOWNLOAD);
}
}
@Override
- public void remove() {
- CacheUtil.remove(cache, CacheUtil.getKey(dataSpec));
+ public void cancel() {
+ isCanceled.set(true);
}
@Override
@@ -91,8 +88,13 @@ public final class ProgressiveDownloader implements Downloader {
@Override
public float getDownloadPercentage() {
long contentLength = cachingCounters.contentLength;
- return contentLength == C.LENGTH_UNSET ? Float.NaN
+ return contentLength == C.LENGTH_UNSET
+ ? C.PERCENTAGE_UNSET
: ((cachingCounters.totalCachedBytes() * 100f) / contentLength);
}
+ @Override
+ public void remove() {
+ CacheUtil.remove(cache, CacheUtil.getKey(dataSpec));
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java
index 6bc9a10c38..f6a32a1253 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java
@@ -21,95 +21,96 @@ import com.google.android.exoplayer2.util.Assertions;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
-import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
/**
* {@link DownloadAction} for {@link SegmentDownloader}s.
*
* @param The type of the representation key object.
*/
-public abstract class SegmentDownloadAction extends DownloadAction {
+public abstract class SegmentDownloadAction> extends DownloadAction {
/**
* Base class for {@link SegmentDownloadAction} {@link Deserializer}s.
*
* @param The type of the representation key object.
*/
- protected abstract static class SegmentDownloadActionDeserializer implements Deserializer {
+ protected abstract static class SegmentDownloadActionDeserializer extends Deserializer {
+
+ public SegmentDownloadActionDeserializer(String type, int version) {
+ super(type, version);
+ }
@Override
- public DownloadAction readFromStream(int version, DataInputStream input) throws IOException {
- Uri manifestUri = Uri.parse(input.readUTF());
- String data = input.readUTF();
- boolean removeAction = input.readBoolean();
+ public final DownloadAction readFromStream(int version, DataInputStream input)
+ throws IOException {
+ Uri uri = Uri.parse(input.readUTF());
+ boolean isRemoveAction = input.readBoolean();
+ int dataLength = input.readInt();
+ byte[] data = new byte[dataLength];
+ input.readFully(data);
int keyCount = input.readInt();
- K[] keys = createKeyArray(keyCount);
+ List keys = new ArrayList<>();
for (int i = 0; i < keyCount; i++) {
- keys[i] = readKey(input);
+ keys.add(readKey(input));
}
- return createDownloadAction(manifestUri, removeAction, data, keys);
+ return createDownloadAction(uri, isRemoveAction, data, keys);
}
/** Deserializes a key from the {@code input}. */
protected abstract K readKey(DataInputStream input) throws IOException;
- /** Returns a key array. */
- protected abstract K[] createKeyArray(int keyCount);
-
/** Returns a {@link DownloadAction}. */
- protected abstract DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction,
- String data, K[] keys);
-
+ protected abstract DownloadAction createDownloadAction(
+ Uri manifestUri, boolean isRemoveAction, byte[] data, List keys);
}
- protected final Uri manifestUri;
- protected final K[] keys;
- private final boolean removeAction;
+ public final List keys;
/**
- * @param manifestUri The {@link Uri} of the manifest to be downloaded.
- * @param removeAction Whether the data will be removed. If {@code false} it will be downloaded.
- * @param data Optional custom data for this action. If null, an empty string is used.
- * @param keys Keys of representations to be downloaded. If empty, all representations are
- * downloaded. If {@code removeAction} is true, {@code keys} should be an empty array.
+ * @param type The type of the action.
+ * @param version The action version.
+ * @param uri The URI of the media being downloaded.
+ * @param isRemoveAction Whether the data will be removed. If {@code false} it will be downloaded.
+ * @param data Optional custom data for this action. If {@code null} an empty array will be used.
+ * @param keys Keys of tracks to be downloaded. If empty, all tracks will be downloaded. If {@code
+ * removeAction} is true, {@code keys} must be empty.
*/
protected SegmentDownloadAction(
- Uri manifestUri, boolean removeAction, @Nullable String data, K[] keys) {
- super(data);
- this.manifestUri = manifestUri;
- this.keys = Assertions.checkNotNull(keys);
- this.removeAction = removeAction;
- if (removeAction) {
- Assertions.checkArgument(keys.length == 0);
+ String type,
+ int version,
+ Uri uri,
+ boolean isRemoveAction,
+ @Nullable byte[] data,
+ List keys) {
+ super(type, version, uri, isRemoveAction, data);
+ if (isRemoveAction) {
+ Assertions.checkArgument(keys.isEmpty());
+ this.keys = Collections.emptyList();
+ } else {
+ ArrayList mutableKeys = new ArrayList<>(keys);
+ Collections.sort(mutableKeys);
+ this.keys = Collections.unmodifiableList(mutableKeys);
}
}
- @Override
- public final boolean isRemoveAction() {
- return removeAction;
- }
-
@Override
public final void writeToStream(DataOutputStream output) throws IOException {
- output.writeUTF(manifestUri.toString());
- output.writeUTF(getData());
- output.writeBoolean(removeAction);
- output.writeInt(keys.length);
- for (K key : keys) {
- writeKey(output, key);
+ output.writeUTF(uri.toString());
+ output.writeBoolean(isRemoveAction);
+ output.writeInt(data.length);
+ output.write(data);
+ output.writeInt(keys.size());
+ for (int i = 0; i < keys.size(); i++) {
+ writeKey(output, keys.get(i));
}
}
/** Serializes the {@code key} into the {@code output}. */
protected abstract void writeKey(DataOutputStream output, K key) throws IOException;
-
- @Override
- public boolean isSameMedia(DownloadAction other) {
- return other instanceof SegmentDownloadAction
- && manifestUri.equals(((SegmentDownloadAction>) other).manifestUri);
- }
-
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -119,17 +120,13 @@ public abstract class SegmentDownloadAction extends DownloadAction {
return false;
}
SegmentDownloadAction> that = (SegmentDownloadAction>) o;
- return manifestUri.equals(that.manifestUri)
- && removeAction == that.removeAction
- && keys.length == that.keys.length
- && Arrays.asList(keys).containsAll(Arrays.asList(that.keys));
+ return keys.equals(that.keys);
}
@Override
public int hashCode() {
int result = super.hashCode();
- result = 31 * result + manifestUri.hashCode();
- result = 31 * result + Arrays.hashCode(keys);
+ result = 31 * result + keys.hashCode();
return result;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java
index 63414dc39e..6ce2121acd 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java
@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.offline;
import android.net.Uri;
import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
@@ -27,19 +26,19 @@ import com.google.android.exoplayer2.upstream.cache.CacheUtil;
import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters;
import com.google.android.exoplayer2.util.PriorityTaskManager;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* Base class for multi segment stream downloaders.
*
- * All of the methods are blocking. Also they are not thread safe, except {@link
- * #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link #getDownloadedBytes()}.
- *
* @param The type of the manifest object.
- * @param The type of the representation key object.
+ * @param The type of the streams key object.
*/
-public abstract class SegmentDownloader implements Downloader {
+public abstract class SegmentDownloader, K>
+ implements Downloader {
/** Smallest unit of content to be downloaded. */
protected static class Segment implements Comparable {
@@ -69,149 +68,89 @@ public abstract class SegmentDownloader implements Downloader {
private final Cache cache;
private final CacheDataSource dataSource;
private final CacheDataSource offlineDataSource;
+ private final ArrayList streamKeys;
+ private final AtomicBoolean isCanceled;
- private M manifest;
- private K[] keys;
private volatile int totalSegments;
private volatile int downloadedSegments;
private volatile long downloadedBytes;
/**
* @param manifestUri The {@link Uri} of the manifest to be downloaded.
- * @param constructorHelper a {@link DownloaderConstructorHelper} instance.
+ * @param streamKeys Keys defining which streams in the manifest should be selected for download.
+ * If empty, all streams are downloaded.
+ * @param constructorHelper A {@link DownloaderConstructorHelper} instance.
*/
- public SegmentDownloader(Uri manifestUri, DownloaderConstructorHelper constructorHelper) {
+ public SegmentDownloader(
+ Uri manifestUri, List streamKeys, DownloaderConstructorHelper constructorHelper) {
this.manifestUri = manifestUri;
+ this.streamKeys = new ArrayList<>(streamKeys);
this.cache = constructorHelper.getCache();
this.dataSource = constructorHelper.buildCacheDataSource(false);
this.offlineDataSource = constructorHelper.buildCacheDataSource(true);
this.priorityTaskManager = constructorHelper.getPriorityTaskManager();
- resetCounters();
+ totalSegments = C.LENGTH_UNSET;
+ isCanceled = new AtomicBoolean();
}
/**
- * Returns the manifest. Downloads and parses it if necessary.
+ * Downloads the selected streams in the media. If multiple streams are selected, they are
+ * downloaded in sync with one another.
*
- * @return The manifest.
- * @throws IOException If an error occurs reading data.
- */
- public final M getManifest() throws IOException {
- return getManifestIfNeeded(false);
- }
-
- /**
- * Selects multiple representations pointed to by the keys for downloading, checking status. Any
- * previous selection is cleared. If keys array is empty, all representations are downloaded.
- */
- public final void selectRepresentations(K[] keys) {
- this.keys = keys.length > 0 ? keys.clone() : null;
- resetCounters();
- }
-
- /**
- * Returns keys for all representations.
- *
- * @see #selectRepresentations(Object[])
- */
- public abstract K[] getAllRepresentationKeys() throws IOException;
-
- /**
- * Initializes the total segments, downloaded segments and downloaded bytes counters for the
- * selected representations.
- *
- * @throws IOException Thrown when there is an io error while reading from cache.
- * @throws DownloadException Thrown if the media cannot be downloaded.
- * @throws InterruptedException If the thread has been interrupted.
- * @see #getTotalSegments()
- * @see #getDownloadedSegments()
- * @see #getDownloadedBytes()
- */
- @Override
- public final void init() throws InterruptedException, IOException {
- try {
- getManifestIfNeeded(true);
- } catch (IOException e) {
- // Either the manifest file isn't available offline or not parsable.
- return;
- }
- try {
- initStatus(true);
- } catch (IOException | InterruptedException e) {
- resetCounters();
- throw e;
- }
- }
-
- /**
- * Downloads the content for the selected representations in sync or resumes a previously stopped
- * download.
- *
- * @param listener If not null, called during download.
- * @throws IOException Thrown when there is an io error while downloading.
- * @throws DownloadException Thrown if the media cannot be downloaded.
+ * @throws IOException Thrown when there is an error downloading.
* @throws InterruptedException If the thread has been interrupted.
*/
+ // downloadedSegments and downloadedBytes are only written from this method, and this method
+ // should not be called from more than one thread. Hence non-atomic updates are valid.
+ @SuppressWarnings("NonAtomicVolatileUpdate")
@Override
- public final synchronized void download(@Nullable ProgressListener listener)
- throws IOException, InterruptedException {
+ public final void download() throws IOException, InterruptedException {
priorityTaskManager.add(C.PRIORITY_DOWNLOAD);
+
try {
- getManifestIfNeeded(false);
- List segments = initStatus(false);
- notifyListener(listener); // Initial notification.
+ List segments = initDownload();
Collections.sort(segments);
byte[] buffer = new byte[BUFFER_SIZE_BYTES];
CachingCounters cachingCounters = new CachingCounters();
for (int i = 0; i < segments.size(); i++) {
- CacheUtil.cache(segments.get(i).dataSpec, cache, dataSource, buffer,
- priorityTaskManager, C.PRIORITY_DOWNLOAD, cachingCounters, true);
- downloadedBytes += cachingCounters.newlyCachedBytes;
- downloadedSegments++;
- notifyListener(listener);
+ try {
+ CacheUtil.cache(
+ segments.get(i).dataSpec,
+ cache,
+ dataSource,
+ buffer,
+ priorityTaskManager,
+ C.PRIORITY_DOWNLOAD,
+ cachingCounters,
+ isCanceled,
+ true);
+ downloadedSegments++;
+ } finally {
+ downloadedBytes += cachingCounters.newlyCachedBytes;
+ }
}
} finally {
priorityTaskManager.remove(C.PRIORITY_DOWNLOAD);
}
}
- /**
- * Returns the total number of segments in the representations which are selected, or {@link
- * C#LENGTH_UNSET} if it hasn't been calculated yet.
- *
- * @see #init()
- */
- public final int getTotalSegments() {
- return totalSegments;
+ @Override
+ public void cancel() {
+ isCanceled.set(true);
}
- /**
- * Returns the total number of downloaded segments in the representations which are selected, or
- * {@link C#LENGTH_UNSET} if it hasn't been calculated yet.
- *
- * @see #init()
- */
- public final int getDownloadedSegments() {
- return downloadedSegments;
- }
-
- /**
- * Returns the total number of downloaded bytes in the representations which are selected, or
- * {@link C#LENGTH_UNSET} if it hasn't been calculated yet.
- *
- * @see #init()
- */
@Override
public final long getDownloadedBytes() {
return downloadedBytes;
}
@Override
- public float getDownloadPercentage() {
+ public final float getDownloadPercentage() {
// Take local snapshot of the volatile fields
int totalSegments = this.totalSegments;
int downloadedSegments = this.downloadedSegments;
if (totalSegments == C.LENGTH_UNSET || downloadedSegments == C.LENGTH_UNSET) {
- return Float.NaN;
+ return C.PERCENTAGE_UNSET;
}
return totalSegments == 0 ? 100f : (downloadedSegments * 100f) / totalSegments;
}
@@ -219,29 +158,21 @@ public abstract class SegmentDownloader implements Downloader {
@Override
public final void remove() throws InterruptedException {
try {
- getManifestIfNeeded(true);
+ M manifest = getManifest(offlineDataSource, manifestUri);
+ List segments = getSegments(offlineDataSource, manifest, true);
+ for (int i = 0; i < segments.size(); i++) {
+ removeUri(segments.get(i).dataSpec.uri);
+ }
} catch (IOException e) {
- // Either the manifest file isn't available offline, or it's not parsable. Continue anyway to
- // reset the counters and attempt to remove the manifest file.
+ // Ignore exceptions when removing.
+ } finally {
+ // Always attempt to remove the manifest.
+ removeUri(manifestUri);
}
- resetCounters();
- if (manifest != null) {
- List segments = null;
- try {
- segments = getSegments(offlineDataSource, manifest, getAllRepresentationKeys(), true);
- } catch (IOException e) {
- // Ignore exceptions. We do our best with what's available offline.
- }
- if (segments != null) {
- for (int i = 0; i < segments.size(); i++) {
- remove(segments.get(i).dataSpec.uri);
- }
- }
- manifest = null;
- }
- remove(manifestUri);
}
+ // Internal methods.
+
/**
* Loads and parses the manifest.
*
@@ -253,51 +184,29 @@ public abstract class SegmentDownloader implements Downloader {
protected abstract M getManifest(DataSource dataSource, Uri uri) throws IOException;
/**
- * Returns a list of {@link Segment}s for given keys.
+ * Returns a list of all downloadable {@link Segment}s for a given manifest.
*
* @param dataSource The {@link DataSource} through which to load any required data.
* @param manifest The manifest containing the segments.
- * @param keys The selected representation keys.
- * @param allowIncompleteIndex Whether to continue in the case that a load error prevents all
+ * @param allowIncompleteList Whether to continue in the case that a load error prevents all
* segments from being listed. If true then a partial segment list will be returned. If false
* an {@link IOException} will be thrown.
* @throws InterruptedException Thrown if the thread was interrupted.
* @throws IOException Thrown if {@code allowPartialIndex} is false and a load error occurs, or if
* the media is not in a form that allows for its segments to be listed.
- * @return A list of {@link Segment}s for given keys.
+ * @return The list of downloadable {@link Segment}s.
*/
- protected abstract List getSegments(DataSource dataSource, M manifest, K[] keys,
- boolean allowIncompleteIndex) throws InterruptedException, IOException;
+ protected abstract List getSegments(
+ DataSource dataSource, M manifest, boolean allowIncompleteList)
+ throws InterruptedException, IOException;
- private void resetCounters() {
- totalSegments = C.LENGTH_UNSET;
- downloadedSegments = C.LENGTH_UNSET;
- downloadedBytes = C.LENGTH_UNSET;
- }
-
- private void remove(Uri uri) {
- CacheUtil.remove(cache, CacheUtil.generateKey(uri));
- }
-
- private void notifyListener(ProgressListener listener) {
- if (listener != null) {
- listener.onDownloadProgress(this, getDownloadPercentage(), downloadedBytes);
+ /** Initializes the download, returning a list of {@link Segment}s that need to be downloaded. */
+ private List initDownload() throws IOException, InterruptedException {
+ M manifest = getManifest(dataSource, manifestUri);
+ if (!streamKeys.isEmpty()) {
+ manifest = manifest.copy(streamKeys);
}
- }
-
- /**
- * Initializes totalSegments, downloadedSegments and downloadedBytes for selected representations.
- * If not offline then downloads missing metadata.
- *
- * @return A list of not fully downloaded segments.
- */
- private synchronized List initStatus(boolean offline)
- throws IOException, InterruptedException {
- DataSource dataSource = getDataSource(offline);
- if (keys == null) {
- keys = getAllRepresentationKeys();
- }
- List segments = getSegments(dataSource, manifest, keys, offline);
+ List segments = getSegments(dataSource, manifest, /* allowIncompleteList= */ false);
CachingCounters cachingCounters = new CachingCounters();
totalSegments = segments.size();
downloadedSegments = 0;
@@ -315,15 +224,8 @@ public abstract class SegmentDownloader implements Downloader {
return segments;
}
- private M getManifestIfNeeded(boolean offline) throws IOException {
- if (manifest == null) {
- manifest = getManifest(getDataSource(offline), manifestUri);
- }
- return manifest;
- }
-
- private DataSource getDataSource(boolean offline) {
- return offline ? offlineDataSource : dataSource;
+ private void removeUri(Uri uri) {
+ CacheUtil.remove(cache, CacheUtil.generateKey(uri));
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/TrackKey.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/TrackKey.java
new file mode 100644
index 0000000000..f6a411c3a1
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/TrackKey.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.offline;
+
+/**
+ * Identifies a given track by the index of the containing period, the index of the containing group
+ * within the period, and the index of the track within the group.
+ */
+public final class TrackKey {
+
+ /** The period index. */
+ public final int periodIndex;
+ /** The group index. */
+ public final int groupIndex;
+ /** The track index. */
+ public final int trackIndex;
+
+ /**
+ * @param periodIndex The period index.
+ * @param groupIndex The group index.
+ * @param trackIndex The track index.
+ */
+ public TrackKey(int periodIndex, int groupIndex, int trackIndex) {
+ this.periodIndex = periodIndex;
+ this.groupIndex = groupIndex;
+ this.trackIndex = trackIndex;
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
index cd1e12520e..b3737eb8bc 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
@@ -16,8 +16,6 @@
package com.google.android.exoplayer2.scheduler;
import android.annotation.TargetApi;
-import android.app.Notification;
-import android.app.Service;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
@@ -26,91 +24,73 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.PersistableBundle;
+import android.support.annotation.RequiresPermission;
import android.util.Log;
import com.google.android.exoplayer2.util.Util;
/**
- * A {@link Scheduler} which uses {@link android.app.job.JobScheduler} to schedule a {@link Service}
- * to be started when its requirements are met. The started service must call {@link
- * Service#startForeground(int, Notification)} to make itself a foreground service upon being
- * started, as documented by {@link Service#startForegroundService(Intent)}.
- *
- * To use {@link PlatformScheduler} application needs to have RECEIVE_BOOT_COMPLETED permission
- * and you need to define PlatformSchedulerService in your manifest:
+ * A {@link Scheduler} that uses {@link JobScheduler}. To use this scheduler, you must add {@link
+ * PlatformSchedulerService} to your manifest:
*
*
{@literal
*
*
*
- * }
- *
- * The service to be scheduled must be defined in the manifest with an intent-filter:
- *
- * {@literal
- *
- *
- *
- *
- *
- *
+ * android:permission="android.permission.BIND_JOB_SERVICE"
+ * android:exported="true"/>
* }
*/
@TargetApi(21)
public final class PlatformScheduler implements Scheduler {
private static final String TAG = "PlatformScheduler";
- private static final String SERVICE_ACTION = "SERVICE_ACTION";
- private static final String SERVICE_PACKAGE = "SERVICE_PACKAGE";
- private static final String REQUIREMENTS = "REQUIREMENTS";
+ private static final String KEY_SERVICE_ACTION = "service_action";
+ private static final String KEY_SERVICE_PACKAGE = "service_package";
+ private static final String KEY_REQUIREMENTS = "requirements";
private final int jobId;
- private final JobInfo jobInfo;
+ private final ComponentName jobServiceComponentName;
private final JobScheduler jobScheduler;
/**
- * @param context Used to access to {@link JobScheduler} service.
- * @param requirements The requirements to execute the job.
- * @param jobId Unique identifier for the job. Using the same id as a previous job can cause that
- * job to be replaced or canceled.
- * @param serviceAction The action which the service will be started with.
- * @param servicePackage The package of the service which contains the logic of the job.
+ * @param context Any context.
+ * @param jobId An identifier for the jobs scheduled by this instance. If the same identifier was
+ * used by a previous instance, anything scheduled by the previous instance will be canceled
+ * by this instance if {@link #schedule(Requirements, String, String)} or {@link #cancel()}
+ * are called.
*/
- public PlatformScheduler(
- Context context,
- Requirements requirements,
- int jobId,
- String serviceAction,
- String servicePackage) {
+ @RequiresPermission(android.Manifest.permission.RECEIVE_BOOT_COMPLETED)
+ public PlatformScheduler(Context context, int jobId) {
this.jobId = jobId;
- this.jobInfo = buildJobInfo(context, requirements, jobId, serviceAction, servicePackage);
- this.jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+ jobServiceComponentName = new ComponentName(context, PlatformSchedulerService.class);
+ jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
}
@Override
- public boolean schedule() {
+ public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) {
+ JobInfo jobInfo =
+ buildJobInfo(jobId, jobServiceComponentName, requirements, serviceAction, servicePackage);
int result = jobScheduler.schedule(jobInfo);
- logd("Scheduling JobScheduler job: " + jobId + " result: " + result);
+ logd("Scheduling job: " + jobId + " result: " + result);
return result == JobScheduler.RESULT_SUCCESS;
}
@Override
public boolean cancel() {
- logd("Canceling JobScheduler job: " + jobId);
+ logd("Canceling job: " + jobId);
jobScheduler.cancel(jobId);
return true;
}
+ // @RequiresPermission constructor annotation should ensure the permission is present.
+ @SuppressWarnings("MissingPermission")
private static JobInfo buildJobInfo(
- Context context,
- Requirements requirements,
int jobId,
+ ComponentName jobServiceComponentName,
+ Requirements requirements,
String serviceAction,
String servicePackage) {
- JobInfo.Builder builder =
- new JobInfo.Builder(jobId, new ComponentName(context, PlatformSchedulerService.class));
+ JobInfo.Builder builder = new JobInfo.Builder(jobId, jobServiceComponentName);
int networkType;
switch (requirements.getRequiredNetworkType()) {
@@ -146,13 +126,12 @@ public final class PlatformScheduler implements Scheduler {
builder.setRequiresCharging(requirements.isChargingRequired());
builder.setPersisted(true);
- // Extras, work duration.
PersistableBundle extras = new PersistableBundle();
- extras.putString(SERVICE_ACTION, serviceAction);
- extras.putString(SERVICE_PACKAGE, servicePackage);
- extras.putInt(REQUIREMENTS, requirements.getRequirementsData());
-
+ extras.putString(KEY_SERVICE_ACTION, serviceAction);
+ extras.putString(KEY_SERVICE_PACKAGE, servicePackage);
+ extras.putInt(KEY_REQUIREMENTS, requirements.getRequirementsData());
builder.setExtras(extras);
+
return builder.build();
}
@@ -162,26 +141,22 @@ public final class PlatformScheduler implements Scheduler {
}
}
- /** A {@link JobService} to start a service if the requirements are met. */
+ /** A {@link JobService} that starts the target service if the requirements are met. */
public static final class PlatformSchedulerService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
- logd("PlatformSchedulerService is started");
+ logd("PlatformSchedulerService started");
PersistableBundle extras = params.getExtras();
- Requirements requirements = new Requirements(extras.getInt(REQUIREMENTS));
+ Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS));
if (requirements.checkRequirements(this)) {
- logd("requirements are met");
- String serviceAction = extras.getString(SERVICE_ACTION);
- String servicePackage = extras.getString(SERVICE_PACKAGE);
+ logd("Requirements are met");
+ String serviceAction = extras.getString(KEY_SERVICE_ACTION);
+ String servicePackage = extras.getString(KEY_SERVICE_PACKAGE);
Intent intent = new Intent(serviceAction).setPackage(servicePackage);
- logd("starting service action: " + serviceAction + " package: " + servicePackage);
- if (Util.SDK_INT >= 26) {
- startForegroundService(intent);
- } else {
- startService(intent);
- }
+ logd("Starting service action: " + serviceAction + " package: " + servicePackage);
+ Util.startForegroundService(this, intent);
} else {
- logd("requirements are not met");
+ logd("Requirements are not met");
jobFinished(params, /* needsReschedule */ true);
}
return false;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java
index 8c8cf6a3b5..30b07da3eb 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java
@@ -32,8 +32,6 @@ import java.lang.annotation.RetentionPolicy;
/**
* Defines a set of device state requirements.
- *
- * To use network type requirement, application needs to have ACCESS_NETWORK_STATE permission.
*/
public final class Requirements {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java
index 9509c7e5b8..46aa55f094 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java
@@ -69,14 +69,14 @@ public final class RequirementsWatcher {
private CapabilityValidatedCallback networkCallback;
/**
- * @param context Used to register for broadcasts.
+ * @param context Any context.
* @param listener Notified whether the {@link Requirements} are met.
* @param requirements The requirements to watch.
*/
public RequirementsWatcher(Context context, Listener listener, Requirements requirements) {
this.requirements = requirements;
this.listener = listener;
- this.context = context;
+ this.context = context.getApplicationContext();
logd(this + " created");
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java
index 9a9c57443f..1b225d9a4d 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java
@@ -15,25 +15,36 @@
*/
package com.google.android.exoplayer2.scheduler;
-/**
- * Implementer of this interface schedules one implementation specific job to be run when some
- * requirements are met even if the app isn't running.
- */
+import android.app.Notification;
+import android.app.Service;
+import android.content.Intent;
+
+/** Schedules a service to be started in the foreground when some {@link Requirements} are met. */
public interface Scheduler {
- /*package*/ boolean DEBUG = false;
+ /* package */ boolean DEBUG = false;
/**
- * Schedules the job to be run when the requirements are met.
+ * Schedules a service to be started in the foreground when some {@link Requirements} are met.
+ * Anything that was previously scheduled will be canceled.
*
- * @return Whether the job scheduled successfully.
+ *
The service to be started must be declared in the manifest of {@code servicePackage} with an
+ * intent filter containing {@code serviceAction}. Note that when started with {@code
+ * serviceAction}, the service must call {@link Service#startForeground(int, Notification)} to
+ * make itself a foreground service, as documented by {@link
+ * Service#startForegroundService(Intent)}.
+ *
+ * @param requirements The requirements.
+ * @param servicePackage The package name.
+ * @param serviceAction The action with which the service will be started.
+ * @return Whether scheduling was successful.
*/
- boolean schedule();
+ boolean schedule(Requirements requirements, String servicePackage, String serviceAction);
/**
- * Cancels any previous schedule.
+ * Cancels anything that was previously scheduled, or else does nothing.
*
- * @return Whether the job cancelled successfully.
+ * @return Whether cancellation was successful.
*/
boolean cancel();
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java
index ab6b3a311a..229043b127 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java
@@ -37,7 +37,7 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
/**
* Called the first time an error occurs while refreshing source info or preparing the period.
*/
- void onPrepareError(IOException exception);
+ void onPrepareError(MediaPeriodId mediaPeriodId, IOException exception);
}
public final MediaSource mediaSource;
@@ -140,7 +140,7 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
}
if (!notifiedPrepareError) {
notifiedPrepareError = true;
- listener.onPrepareError(e);
+ listener.onPrepareError(id, e);
}
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java
index 6295ca4229..d05c51a793 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java
@@ -18,6 +18,8 @@ package com.google.android.exoplayer2.source.ads;
import android.view.ViewGroup;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
+import com.google.android.exoplayer2.upstream.DataSpec;
import java.io.IOException;
/**
@@ -54,19 +56,12 @@ public interface AdsLoader {
void onAdPlaybackState(AdPlaybackState adPlaybackState);
/**
- * Called when there was an error loading ads. The loader will skip the problematic ad(s).
+ * Called when there was an error loading ads.
*
* @param error The error.
+ * @param dataSpec The data spec associated with the load error.
*/
- void onAdLoadError(IOException error);
-
- /**
- * Called when an unexpected internal error is encountered while loading ads. The loader will
- * skip all remaining ads, as the error is not recoverable.
- *
- * @param error The error.
- */
- void onInternalAdLoadError(RuntimeException error);
+ void onAdLoadError(AdLoadException error, DataSpec dataSpec);
/**
* Called when the user clicks through an ad (for example, following a 'learn more' link).
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java
index 9e93f8d92d..7f9dc18eaf 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java
@@ -18,8 +18,8 @@ package com.google.android.exoplayer2.source.ads;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
+import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
-import android.util.Log;
import android.view.ViewGroup;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
@@ -30,10 +30,16 @@ import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import com.google.android.exoplayer2.source.MediaSourceEventListener;
+import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo;
+import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -64,7 +70,75 @@ public final class AdsMediaSource extends CompositeMediaSource {
int[] getSupportedTypes();
}
- /** Listener for ads media source events. */
+ /**
+ * Wrapper for exceptions that occur while loading ads, which are notified via {@link
+ * MediaSourceEventListener#onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData,
+ * IOException, boolean)}.
+ */
+ public static final class AdLoadException extends IOException {
+
+ /** Types of ad load exceptions. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_AD, TYPE_AD_GROUP, TYPE_ALL_ADS, TYPE_UNEXPECTED})
+ public @interface Type {}
+ /** Type for when an ad failed to load. The ad will be skipped. */
+ public static final int TYPE_AD = 0;
+ /** Type for when an ad group failed to load. The ad group will be skipped. */
+ public static final int TYPE_AD_GROUP = 1;
+ /** Type for when all ad groups failed to load. All ads will be skipped. */
+ public static final int TYPE_ALL_ADS = 2;
+ /** Type for when an unexpected error occurred while loading ads. All ads will be skipped. */
+ public static final int TYPE_UNEXPECTED = 3;
+
+ /** Returns a new ad load exception of {@link #TYPE_AD}. */
+ public static AdLoadException createForAd(Exception error) {
+ return new AdLoadException(TYPE_AD, error);
+ }
+
+ /** Returns a new ad load exception of {@link #TYPE_AD_GROUP}. */
+ public static AdLoadException createForAdGroup(Exception error, int adGroupIndex) {
+ return new AdLoadException(
+ TYPE_AD_GROUP, new IOException("Failed to load ad group " + adGroupIndex, error));
+ }
+
+ /** Returns a new ad load exception of {@link #TYPE_ALL_ADS}. */
+ public static AdLoadException createForAllAds(Exception error) {
+ return new AdLoadException(TYPE_ALL_ADS, error);
+ }
+
+ /** Returns a new ad load exception of {@link #TYPE_UNEXPECTED}. */
+ public static AdLoadException createForUnexpected(RuntimeException error) {
+ return new AdLoadException(TYPE_UNEXPECTED, error);
+ }
+
+ /** The {@link Type} of the ad load exception. */
+ public final @Type int type;
+
+ private AdLoadException(@Type int type, Exception cause) {
+ super(cause);
+ this.type = type;
+ }
+
+ /**
+ * Returns the {@link RuntimeException} that caused the exception if its type is {@link
+ * #TYPE_UNEXPECTED}.
+ */
+ public RuntimeException getRuntimeExceptionForUnexpected() {
+ Assertions.checkState(type == TYPE_UNEXPECTED);
+ return (RuntimeException) getCause();
+ }
+ }
+
+ /**
+ * Listener for ads media source events.
+ *
+ * @deprecated To listen for ad load error events, add a listener via {@link
+ * #addEventListener(Handler, MediaSourceEventListener)} and check for {@link
+ * AdLoadException}s in {@link MediaSourceEventListener#onLoadError(int, MediaPeriodId,
+ * LoadEventInfo, MediaLoadData, IOException, boolean)}. Individual ads loader implementations
+ * should expose ad interaction events, if applicable.
+ */
+ @Deprecated
public interface EventListener {
/**
@@ -131,7 +205,30 @@ public final class AdsMediaSource extends CompositeMediaSource {
ViewGroup adUiViewGroup) {
this(
contentMediaSource,
- dataSourceFactory,
+ new ExtractorMediaSource.Factory(dataSourceFactory),
+ adsLoader,
+ adUiViewGroup,
+ /* eventHandler= */ null,
+ /* eventListener= */ null);
+ }
+
+ /**
+ * Constructs a new source that inserts ads linearly with the content specified by {@code
+ * contentMediaSource}.
+ *
+ * @param contentMediaSource The {@link MediaSource} providing the content to play.
+ * @param adMediaSourceFactory Factory for media sources used to load ad media.
+ * @param adsLoader The loader for ads.
+ * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
+ */
+ public AdsMediaSource(
+ MediaSource contentMediaSource,
+ MediaSourceFactory adMediaSourceFactory,
+ AdsLoader adsLoader,
+ ViewGroup adUiViewGroup) {
+ this(
+ contentMediaSource,
+ adMediaSourceFactory,
adsLoader,
adUiViewGroup,
/* eventHandler= */ null,
@@ -148,7 +245,13 @@ public final class AdsMediaSource extends CompositeMediaSource {
* @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
* @param eventHandler A handler for events. May be null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @deprecated To listen for ad load error events, add a listener via {@link
+ * #addEventListener(Handler, MediaSourceEventListener)} and check for {@link
+ * AdLoadException}s in {@link MediaSourceEventListener#onLoadError(int, MediaPeriodId,
+ * LoadEventInfo, MediaLoadData, IOException, boolean)}. Individual ads loader implementations
+ * should expose ad interaction events, if applicable.
*/
+ @Deprecated
public AdsMediaSource(
MediaSource contentMediaSource,
DataSource.Factory dataSourceFactory,
@@ -175,7 +278,13 @@ public final class AdsMediaSource extends CompositeMediaSource {
* @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
* @param eventHandler A handler for events. May be null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @deprecated To listen for ad load error events, add a listener via {@link
+ * #addEventListener(Handler, MediaSourceEventListener)} and check for {@link
+ * AdLoadException}s in {@link MediaSourceEventListener#onLoadError(int, MediaPeriodId,
+ * LoadEventInfo, MediaLoadData, IOException, boolean)}. Individual ads loader implementations
+ * should expose ad interaction events, if applicable.
*/
+ @Deprecated
public AdsMediaSource(
MediaSource contentMediaSource,
MediaSourceFactory adMediaSourceFactory,
@@ -217,10 +326,10 @@ public final class AdsMediaSource extends CompositeMediaSource {
if (adPlaybackState.adGroupCount > 0 && id.isAd()) {
int adGroupIndex = id.adGroupIndex;
int adIndexInAdGroup = id.adIndexInAdGroup;
+ Uri adUri = adPlaybackState.adGroups[adGroupIndex].uris[adIndexInAdGroup];
if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) {
- Uri adUri = adPlaybackState.adGroups[id.adGroupIndex].uris[id.adIndexInAdGroup];
MediaSource adMediaSource = adMediaSourceFactory.createMediaSource(adUri);
- int oldAdCount = adGroupMediaSources[id.adGroupIndex].length;
+ int oldAdCount = adGroupMediaSources[adGroupIndex].length;
if (adIndexInAdGroup >= oldAdCount) {
int adCount = adIndexInAdGroup + 1;
adGroupMediaSources[adGroupIndex] =
@@ -239,7 +348,7 @@ public final class AdsMediaSource extends CompositeMediaSource {
new MediaPeriodId(/* periodIndex= */ 0, id.windowSequenceNumber),
allocator);
deferredMediaPeriod.setPrepareErrorListener(
- new AdPrepareErrorListener(adGroupIndex, adIndexInAdGroup));
+ new AdPrepareErrorListener(adUri, adGroupIndex, adIndexInAdGroup));
List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource);
if (mediaPeriods == null) {
deferredMediaPeriod.createPeriod();
@@ -357,6 +466,7 @@ public final class AdsMediaSource extends CompositeMediaSource {
private final class ComponentListener implements AdsLoader.EventListener {
private final Handler playerHandler;
+
private volatile boolean released;
/**
@@ -424,37 +534,30 @@ public final class AdsMediaSource extends CompositeMediaSource {
}
@Override
- public void onAdLoadError(final IOException error) {
+ public void onAdLoadError(final AdLoadException error, DataSpec dataSpec) {
if (released) {
return;
}
- Log.w(TAG, "Ad load error", error);
+ createEventDispatcher(/* mediaPeriodId= */ null)
+ .loadError(
+ dataSpec,
+ C.DATA_TYPE_AD,
+ C.TRACK_TYPE_UNKNOWN,
+ /* loadDurationMs= */ 0,
+ /* bytesLoaded= */ 0,
+ error,
+ /* wasCanceled= */ true);
if (eventHandler != null && eventListener != null) {
eventHandler.post(
new Runnable() {
@Override
public void run() {
if (!released) {
- eventListener.onAdLoadError(error);
- }
- }
- });
- }
- }
-
- @Override
- public void onInternalAdLoadError(final RuntimeException error) {
- if (released) {
- return;
- }
- Log.w(TAG, "Internal ad load error", error);
- if (eventHandler != null && eventListener != null) {
- eventHandler.post(
- new Runnable() {
- @Override
- public void run() {
- if (!released) {
- eventListener.onInternalAdLoadError(error);
+ if (error.type == AdLoadException.TYPE_UNEXPECTED) {
+ eventListener.onInternalAdLoadError(error.getRuntimeExceptionForUnexpected());
+ } else {
+ eventListener.onAdLoadError(error);
+ }
}
}
});
@@ -464,16 +567,27 @@ public final class AdsMediaSource extends CompositeMediaSource {
private final class AdPrepareErrorListener implements DeferredMediaPeriod.PrepareErrorListener {
+ private final Uri adUri;
private final int adGroupIndex;
private final int adIndexInAdGroup;
- public AdPrepareErrorListener(int adGroupIndex, int adIndexInAdGroup) {
+ public AdPrepareErrorListener(Uri adUri, int adGroupIndex, int adIndexInAdGroup) {
+ this.adUri = adUri;
this.adGroupIndex = adGroupIndex;
this.adIndexInAdGroup = adIndexInAdGroup;
}
@Override
- public void onPrepareError(final IOException exception) {
+ public void onPrepareError(MediaPeriodId mediaPeriodId, final IOException exception) {
+ createEventDispatcher(mediaPeriodId)
+ .loadError(
+ new DataSpec(adUri),
+ C.DATA_TYPE_AD,
+ C.TRACK_TYPE_UNKNOWN,
+ /* loadDurationMs= */ 0,
+ /* bytesLoaded= */ 0,
+ AdLoadException.createForAd(exception),
+ /* wasCanceled= */ true);
mainHandler.post(
new Runnable() {
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java
index 6c9ae690fc..0453a8fa12 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.source.chunk;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.upstream.DataSource;
@@ -51,7 +52,7 @@ public abstract class Chunk implements Loadable {
* Optional data associated with the selection of the track to which this chunk belongs. Null if
* the chunk does not belong to a track.
*/
- public final Object trackSelectionData;
+ public final @Nullable Object trackSelectionData;
/**
* The start time of the media contained by the chunk, or {@link C#TIME_UNSET} if the data
* being loaded does not contain media samples.
@@ -75,8 +76,15 @@ public abstract class Chunk implements Loadable {
* @param startTimeUs See {@link #startTimeUs}.
* @param endTimeUs See {@link #endTimeUs}.
*/
- public Chunk(DataSource dataSource, DataSpec dataSpec, int type, Format trackFormat,
- int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs) {
+ public Chunk(
+ DataSource dataSource,
+ DataSpec dataSpec,
+ int type,
+ Format trackFormat,
+ int trackSelectionReason,
+ @Nullable Object trackSelectionData,
+ long startTimeUs,
+ long endTimeUs) {
this.dataSource = Assertions.checkNotNull(dataSource);
this.dataSpec = Assertions.checkNotNull(dataSpec);
this.type = type;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
index f505beb511..6dd90b8735 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.source.chunk;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
@@ -44,8 +45,12 @@ public final class InitializationChunk extends Chunk {
* @param trackSelectionData See {@link #trackSelectionData}.
* @param extractorWrapper A wrapped extractor to use for parsing the initialization data.
*/
- public InitializationChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat,
- int trackSelectionReason, Object trackSelectionData,
+ public InitializationChunk(
+ DataSource dataSource,
+ DataSpec dataSpec,
+ Format trackFormat,
+ int trackSelectionReason,
+ @Nullable Object trackSelectionData,
ChunkExtractorWrapper extractorWrapper) {
super(dataSource, dataSpec, C.DATA_TYPE_MEDIA_INITIALIZATION, trackFormat, trackSelectionReason,
trackSelectionData, C.TIME_UNSET, C.TIME_UNSET);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
index 7edcf65320..f2b4c7ed3e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
@@ -37,6 +37,7 @@ import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -44,24 +45,91 @@ import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
/**
- * A default {@link TrackSelector} suitable for most use cases.
+ * A default {@link TrackSelector} suitable for most use cases. Track selections are made according
+ * to configurable {@link Parameters}, which can be set by calling {@link
+ * #setParameters(Parameters)}.
+ *
+ * Modifying parameters
+ *
+ * To modify only some aspects of the parameters currently used by a selector, it's possible to
+ * obtain a {@link ParametersBuilder} initialized with the current {@link Parameters}. The desired
+ * modifications can be made on the builder, and the resulting {@link Parameters} can then be built
+ * and set on the selector. For example the following code modifies the parameters to restrict video
+ * track selections to SD, and to prefer German audio tracks:
+ *
+ * {@code
+ * // Build on the current parameters.
+ * Parameters currentParameters = trackSelector.getParameters();
+ * // Build the resulting parameters.
+ * Parameters newParameters = currentParameters
+ * .buildUpon()
+ * .setMaxVideoSizeSd()
+ * .setPreferredAudioLanguage("deu")
+ * .build();
+ * // Set the new parameters.
+ * trackSelector.setParameters(newParameters);
+ * }
+ *
+ * Convenience methods and chaining allow this to be written more concisely as:
+ *
+ * {@code
+ * trackSelector.setParameters(
+ * trackSelector
+ * .buildUponParameters()
+ * .setMaxVideoSizeSd()
+ * .setPreferredAudioLanguage("deu"));
+ * }
+ *
+ * Selection {@link Parameters} support many different options, some of which are described below.
+ *
+ * Track selection overrides
+ *
+ * Track selection overrides can be used to select specific tracks. To specify an override for a
+ * renderer, it's first necessary to obtain the tracks that have been mapped to it:
+ *
+ * {@code
+ * MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
+ * TrackGroupArray rendererTrackGroups = mappedTrackInfo == null ? null
+ * : mappedTrackInfo.getTrackGroups(rendererIndex);
+ * }
+ *
+ * If {@code rendererTrackGroups} is null then there aren't any currently mapped tracks, and so
+ * setting an override isn't possible. Note that a {@link Player.EventListener} registered on the
+ * player can be used to determine when the current tracks (and therefore the mapping) changes. If
+ * {@code rendererTrackGroups} is non-null then an override can be set. The next step is to query
+ * the properties of the available tracks to determine the {@code groupIndex} and the {@code
+ * trackIndices} within the group it that should be selected. The override can then be specified
+ * using {@link ParametersBuilder#setSelectionOverride}:
+ *
+ * {@code
+ * SelectionOverride selectionOverride = new SelectionOverride(groupIndex, trackIndices);
+ * trackSelector.setParameters(
+ * trackSelector
+ * .buildUponParameters()
+ * .setSelectionOverride(rendererIndex, rendererTrackGroups, selectionOverride));
+ * }
+ *
+ * Disabling renderers
+ *
+ * Renderers can be disabled using {@link ParametersBuilder#setRendererDisabled}. Disabling a
+ * renderer differs from setting a {@code null} override because the renderer is disabled
+ * unconditionally, whereas a {@code null} override is applied only when the track groups available
+ * to the renderer match the {@link TrackGroupArray} for which it was specified.
*
* Constraint based track selection
*
- * Whilst this selector supports setting specific track overrides, the recommended way of changing
- * which tracks are selected is by setting {@link Parameters} that constrain the track selection
- * process. For example an instance can specify a preferred language for the audio track, and impose
- * constraints on the maximum video resolution that should be selected for adaptive playbacks.
- * Modifying the parameters is simple:
+ * Whilst track selection overrides make it possible to select specific tracks, the recommended way
+ * of controlling which tracks are selected is by specifying constraints. For example consider the
+ * case of wanting to restrict video track selections to SD, and preferring German audio tracks.
+ * Track selection overrides could be used to select specific tracks meeting these criteria, however
+ * a simpler and more flexible approach is to specify these constraints directly:
*
* {@code
- * Parameters currentParameters = trackSelector.getParameters();
- * // Generate new parameters to prefer German audio and impose a maximum video size constraint.
- * Parameters newParameters = currentParameters
- * .withPreferredAudioLanguage("deu")
- * .withMaxVideoSize(1024, 768);
- * // Set the new parameters on the selector.
- * trackSelector.setParameters(newParameters);
+ * trackSelector.setParameters(
+ * trackSelector
+ * .buildUponParameters()
+ * .setMaxVideoSizeSd()
+ * .setPreferredAudioLanguage("deu"));
* }
*
* There are several benefits to using constraint based track selection instead of specific track
@@ -76,46 +144,11 @@ import java.util.concurrent.atomic.AtomicReference;
* only applied to periods whose tracks match those for which the override was set.
*
*
- * Track overrides
- *
- * This selector supports overriding of track selections for each renderer. To specify an override
- * for a renderer it's first necessary to obtain the tracks that have been mapped to it:
- *
- * {@code
- * MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
- * TrackGroupArray rendererTrackGroups = mappedTrackInfo == null ? null
- * : mappedTrackInfo.getTrackGroups(rendererIndex);
- * }
- *
- * If {@code rendererTrackGroups} is null then there aren't any currently mapped tracks, and so
- * setting an override isn't possible. Note that a {@link Player.EventListener} registered on the
- * player can be used to determine when the current tracks (and therefore the mapping) changes. If
- * {@code rendererTrackGroups} is non-null then an override can be set. The next step is to query
- * the properties of the available tracks to determine the {@code groupIndex} of the track group you
- * want to select and the {@code trackIndices} within it. You can then create and set the override:
- *
- * {@code
- * trackSelector.setSelectionOverride(rendererIndex, rendererTrackGroups,
- * new SelectionOverride(groupIndex, trackIndices));
- * }
- *
- * If the override is {@code null} then no tracks will be selected.
- *
- * Note that an override applies only when the track groups available to the renderer match the
- * {@link TrackGroupArray} for which the override was specified. Overrides can be cleared using the
- * {@code clearSelectionOverride} methods.
- *
- *
Disabling renderers
- *
- * Renderers can be disabled using {@link #setRendererDisabled(int, boolean)}. Disabling a renderer
- * differs from setting a {@code null} override because the renderer is disabled unconditionally,
- * whereas a {@code null} override is applied only when the track groups available to the renderer
- * match the {@link TrackGroupArray} for which it was specified.
- *
* Tunneling
*
* Tunneled playback can be enabled in cases where the combination of renderers and selected tracks
- * support it. See {@link #setTunnelingAudioSessionId(int)} for more details.
+ * support it. Tunneled playback is enabled by passing an audio session ID to {@link
+ * ParametersBuilder#setTunnelingAudioSessionId(int)}.
*/
public class DefaultTrackSelector extends MappingTrackSelector {
@@ -124,6 +157,9 @@ public class DefaultTrackSelector extends MappingTrackSelector {
*/
public static final class ParametersBuilder {
+ private final SparseArray